1 #include "documentstyle.hpp"
2 #include "kristall.hpp"
3 #include <cassert>
4 #include <QDebug>
5 #include <QString>
6 #include <QStringList>
7 #include <QFontDatabase>
8 #include <QFontInfo>
9
10 #include <QCryptographicHash>
11 #include <QDebug>
12
13 #include <cctype>
14 #include <array>
15 #include <cmath>
16
DefaultFonts()17 DocumentStyle::DefaultFonts::DefaultFonts()
18 {
19 // Initialise default fonts
20 #ifdef Q_OS_WIN32
21 // Windows default fonts are ugly, so we use standard ones.
22 this->regular = "Segoe UI";
23 this->fixed = "Consolas";
24 #else
25 // *nix
26 regular = QFontDatabase::systemFont(QFontDatabase::GeneralFont).family();
27 fixed = QFontInfo(QFont("monospace")).family();
28 #endif
29 }
30
encodeCssFont(const QFont & refFont)31 static QString encodeCssFont (const QFont& refFont)
32 {
33 //-----------------------------------------------------------------------
34 // This function assembles a CSS Font specification string from
35 // a QFont. This supports most of the QFont attributes settable in
36 // the Qt 4.8 and Qt 5.3 QFontDialog.
37 //
38 // (1) Font Family
39 // (2) Font Weight (just bold or not)
40 // (3) Font Style (possibly Italic or Oblique)
41 // (4) Font Size (in either pixels or points)
42 // (5) Decorations (possibly Underline or Strikeout)
43 //
44 // Not supported: Writing System (e.g. Latin).
45 //
46 // See the corresponding decode function, below.
47 // QFont decodeCssFontString (const QString cssFontStr)
48 //-----------------------------------------------------------------------
49
50 QStringList fields; // CSS font attribute fields
51
52 // ***************************************************
53 // *** (1) Font Family: Primary plus Substitutes ***
54 // ***************************************************
55
56 const QString family = refFont.family();
57
58 // NOTE [9-2014, Qt 4.8.6]: This isn't what I thought it was. It
59 // does not return a list of "fallback" font faces (e.g. Georgia,
60 // Serif for "Times New Roman"). In my testing, this is always
61 // returning an empty list.
62 //
63 QStringList famSubs = QFont::substitutes (family);
64
65 if (!famSubs.contains (family))
66 famSubs.prepend (family);
67
68 static const QChar DBL_QUOT ('"');
69 const int famCnt = famSubs.count();
70 QStringList famList;
71 for (int inx = 0; inx < famCnt; ++inx)
72 {
73 // Place double quotes around family names having space characters,
74 // but only if double quotes are not already there.
75 //
76 const QString fam = famSubs [inx];
77 if (fam.contains (' ') && !fam.startsWith (DBL_QUOT))
78 famList << (DBL_QUOT + fam + DBL_QUOT);
79 else
80 famList << fam;
81 }
82
83 const QString famStr = QString ("font-family: ") + famList.join (", ");
84 fields << famStr;
85
86 // **************************************
87 // *** (2) Font Weight: Bold or Not ***
88 // **************************************
89
90 const bool bold = refFont.bold();
91 if (bold)
92 fields << "font-weight: bold";
93
94 // ****************************************************
95 // *** (3) Font Style: possibly Italic or Oblique ***
96 // ****************************************************
97
98 const QFont::Style style = refFont.style();
99 switch (style)
100 {
101 case QFont::StyleNormal: break;
102 case QFont::StyleItalic: fields << "font-style: italic"; break;
103 case QFont::StyleOblique: fields << "font-style: oblique"; break;
104 }
105
106 // ************************************************
107 // *** (4) Font Size: either Pixels or Points ***
108 // ************************************************
109
110 const double sizeInPoints = refFont.pointSizeF(); // <= 0 if not defined.
111 const int sizeInPixels = refFont.pixelSize(); // <= 0 if not defined.
112 if (sizeInPoints > 0.0)
113 fields << QString ("font-size: %1pt") .arg (sizeInPoints);
114 else if (sizeInPixels > 0)
115 fields << QString ("font-size: %1px") .arg (sizeInPixels);
116
117 // ***********************************************
118 // *** (5) Decorations: Underline, Strikeout ***
119 // ***********************************************
120
121 const bool underline = refFont.underline();
122 const bool strikeOut = refFont.strikeOut();
123
124 if (underline && strikeOut)
125 fields << "text-decoration: underline line-through";
126 else if (underline)
127 fields << "text-decoration: underline";
128 else if (strikeOut)
129 fields << "text-decoration: line-through";
130
131 const QString cssFontStr = fields.join ("; ");
132 return cssFontStr;
133 }
134
DocumentStyle()135 DocumentStyle::DocumentStyle() : theme(Fixed),
136 standard_font(),
137 h1_font(),
138 h2_font(),
139 h3_font(),
140 preformatted_font(),
141 blockquote_font(),
142 background_color(0xed, 0xef, 0xff),
143 standard_color(0x00, 0x00, 0x00),
144 preformatted_color(0x00, 0x00, 0x00),
145 h1_color(0x02, 0x2f, 0x90),
146 h2_color(0x02, 0x2f, 0x90),
147 h3_color(0x02, 0x2f, 0x90),
148 blockquote_fgcolor(0x00, 0x00, 0x00),
149 blockquote_bgcolor(0xFF, 0xFF, 0xFF),
150 internal_link_color(0x0e, 0x8f, 0xff),
151 external_link_color(0x0e, 0x8f, 0xff),
152 cross_scheme_link_color(0x09, 0x60, 0xa7),
153 internal_link_prefix("→ "),
154 external_link_prefix("⇒ "),
155 margin_h(30.0),
156 margin_v(55.0),
157 text_width(900),
158 ansi_colors({"black", "darkred", "darkgreen", "darkgoldenrod",
159 "darkblue", "darkmagenta", "darkcyan", "lightgray",
160 "gray", "red", "green", "goldenrod",
161 "lightblue", "magenta", "cyan", "white"}),
162 justify_text(true),
163 text_width_enabled(true),
164 centre_h1(false),
165 line_height_p(5.0),
166 line_height_h(5.0),
167 indent_bq(1), indent_p(1), indent_h(0), indent_l(2),
168 indent_size(15.0),
169 list_symbol(QTextListFormat::ListDisc)
170 {
171 this->initialiseDefaultFonts();
172 }
173
initialiseDefaultFonts()174 void DocumentStyle::initialiseDefaultFonts()
175 {
176 DefaultFonts default_fonts;
177
178 preformatted_font.setFamily(default_fonts.fixed);
179 preformatted_font.setPointSizeF(12.0);
180
181 standard_font.setFamily(default_fonts.regular);
182 standard_font.setPointSizeF(12.0);
183
184 h1_font.setFamily(default_fonts.regular);
185 h1_font.setBold(true);
186 h1_font.setPointSizeF(22.0);
187
188 h2_font.setFamily(default_fonts.regular);
189 h2_font.setBold(true);
190 h2_font.setPointSizeF(17.0);
191
192 h3_font.setFamily(default_fonts.regular);
193 h3_font.setBold(true);
194 h3_font.setPointSizeF(14.0);
195
196 blockquote_font.setFamily(default_fonts.regular);
197 blockquote_font.setItalic(true);
198 blockquote_font.setPointSizeF(12.0);
199 }
200
createFileNameFromName(const QString & src,int index)201 QString DocumentStyle::createFileNameFromName(const QString &src, int index)
202 {
203 QString result;
204 result.reserve(src.size() + 5);
205 for(int i = 0; i < src.size(); i++)
206 {
207 QChar c = src.at(i);
208 if(c.isLetterOrNumber()) {
209 result.append(c.toLower());
210 }
211 else if(c.isSpace()) {
212 result.append('-');
213 }
214 else {
215 result.append(QString::number(c.unicode()));
216 }
217 }
218
219 if(index > 0) {
220 result.append(QString("-%1").arg(index));
221 }
222 result.append(".kthm");
223 return result;
224 }
225
save(QSettings & settings) const226 bool DocumentStyle::save(QSettings &settings) const
227 {
228 settings.setValue("version", 1);
229 settings.setValue("theme", int(theme));
230
231 settings.setValue("background_color", background_color.name());
232
233 settings.setValue("blockquote_color", blockquote_bgcolor.name());
234
235 settings.setValue("margins_h", margin_h);
236 settings.setValue("margins_v", margin_v);
237
238 settings.setValue("ansi_colors", ansi_colors);
239
240 {
241 settings.beginGroup("Standard");
242 settings.setValue("font", standard_font.toString());
243 settings.setValue("color", standard_color.name());
244 settings.endGroup();
245 }
246 {
247 settings.beginGroup("Preformatted");
248 settings.setValue("font", preformatted_font.toString());
249 settings.setValue("color", preformatted_color.name());
250 settings.endGroup();
251 }
252 {
253 settings.beginGroup("H1");
254 settings.setValue("font", h1_font.toString());
255 settings.setValue("color", h1_color.name());
256 settings.endGroup();
257 }
258 {
259 settings.beginGroup("H2");
260 settings.setValue("font", h2_font.toString());
261 settings.setValue("color", h2_color.name());
262 settings.endGroup();
263 }
264 {
265 settings.beginGroup("H3");
266 settings.setValue("font", h3_font.toString());
267 settings.setValue("color", h3_color.name());
268 settings.endGroup();
269 }
270 {
271 settings.beginGroup("Blockquote");
272 settings.setValue("font", blockquote_font.toString());
273 settings.setValue("color", blockquote_fgcolor.name());
274 settings.endGroup();
275 }
276 {
277 settings.beginGroup("Link");
278
279 settings.setValue("color_internal", internal_link_color.name());
280 settings.setValue("color_external", external_link_color.name());
281 settings.setValue("color_cross_scheme", cross_scheme_link_color.name());
282
283 settings.setValue("internal_prefix", internal_link_prefix);
284 settings.setValue("external_prefix", external_link_prefix);
285
286 settings.endGroup();
287 }
288 {
289 settings.beginGroup("Formatting");
290
291 settings.setValue("justify_text", justify_text);
292 settings.setValue("text_width_enabled", text_width_enabled);
293 settings.setValue("centre_h1", centre_h1);
294 settings.setValue("text_width", text_width);
295 settings.setValue("line_height_p", line_height_p);
296 settings.setValue("line_height_h", line_height_h);
297 settings.setValue("indent_bq", indent_bq);
298 settings.setValue("indent_p", indent_p);
299 settings.setValue("indent_h", indent_h);
300 settings.setValue("indent_l", indent_l);
301 settings.setValue("indent_size", indent_size);
302 settings.setValue("list_symbol", (int)list_symbol);
303
304 settings.endGroup();
305 }
306
307 return true;
308 }
309
load(QSettings & settings)310 bool DocumentStyle::load(QSettings &settings)
311 {
312 switch(settings.value("version", 0).toInt())
313 {
314 case 0: {
315 if(settings.contains("standard_color"))
316 {
317 standard_font.fromString(settings.value("standard_font").toString());
318 h1_font.fromString(settings.value("h1_font").toString());
319 h2_font.fromString(settings.value("h2_font").toString());
320 h3_font.fromString(settings.value("h3_font").toString());
321 preformatted_font.fromString(settings.value("preformatted_font").toString());
322 blockquote_font.fromString(settings.value("standard_font").toString());
323
324 background_color = QColor(settings.value("background_color").toString());
325 standard_color = QColor(settings.value("standard_color").toString());
326 preformatted_color = QColor(settings.value("preformatted_color").toString());
327 blockquote_bgcolor = QColor(settings.value("blockquote_color").toString());
328 blockquote_fgcolor = standard_color;
329 h1_color = QColor(settings.value("h1_color").toString());
330 h2_color = QColor(settings.value("h2_color").toString());
331 h3_color = QColor(settings.value("h3_color").toString());
332 internal_link_color = QColor(settings.value("internal_link_color").toString());
333 external_link_color = QColor(settings.value("external_link_color").toString());
334 cross_scheme_link_color = QColor(settings.value("cross_scheme_link_color").toString());
335
336 internal_link_prefix = settings.value("internal_link_prefix").toString();
337 external_link_prefix = settings.value("external_link_prefix").toString();
338
339 margin_h = margin_v = settings.value("margins").toDouble();
340 theme = Theme(settings.value("theme").toInt());
341 }
342 break;
343 }
344 case 1: {
345 theme = Theme(settings.value("theme", int(theme)).toInt());
346
347 background_color = QColor { settings.value("background_color", background_color.name()).toString() };
348 blockquote_bgcolor = QColor { settings.value("blockquote_color", blockquote_bgcolor.name()).toString() };
349
350 margin_h = settings.value("margins_h", 30).toInt();
351 margin_v = settings.value("margins_v", 55).toInt();
352
353 QStringList default_colors = {"black", "darkred", "darkgreen", "darkgoldenrod",
354 "darkblue", "darkmagenta", "darkcyan", "lightgray",
355 "gray", "red", "green", "goldenrod",
356 "lightblue", "magenta", "cyan", "white"};
357 ansi_colors = settings.value("ansi_colors", default_colors).toStringList();
358
359 {
360 settings.beginGroup("Standard");
361 standard_font.fromString(settings.value("font", standard_font.toString()).toString());
362 standard_color = QString { settings.value("color", standard_color.name()).toString() };
363 settings.endGroup();
364 }
365 {
366 settings.beginGroup("Preformatted");
367 preformatted_font.fromString(settings.value("font", preformatted_font.toString()).toString());
368 preformatted_color = QString { settings.value("color", preformatted_color.name()).toString() };
369 settings.endGroup();
370 }
371 {
372 settings.beginGroup("H1");
373 h1_font.fromString(settings.value("font", h1_font.toString()).toString());
374 h1_color = QString { settings.value("color", h1_color.name()).toString() };
375 settings.endGroup();
376 }
377 {
378 settings.beginGroup("H2");
379 h2_font.fromString(settings.value("font", h2_font.toString()).toString());
380 h2_color = QString { settings.value("color", h2_color.name()).toString() };
381 settings.endGroup();
382 }
383 {
384 settings.beginGroup("H3");
385 h3_font.fromString(settings.value("font", h3_font.toString()).toString());
386 h3_color = QString { settings.value("color", h3_color.name()).toString() };
387 settings.endGroup();
388 }
389 {
390 settings.beginGroup("Blockquote");
391 blockquote_font.fromString(settings.value("font", standard_font.toString()).toString());
392 blockquote_fgcolor = QString { settings.value("color", standard_color.name()).toString() };
393 settings.endGroup();
394 }
395 {
396 settings.beginGroup("Link");
397
398 internal_link_color = QString { settings.value("color_internal", internal_link_color.name()).toString() };
399 external_link_color = QString {settings.value("color_external", external_link_color.name()).toString() };
400 cross_scheme_link_color = QString {settings.value("color_cross_scheme", cross_scheme_link_color.name()).toString() };
401
402 internal_link_prefix = settings.value("internal_prefix", internal_link_prefix).toString();
403 external_link_prefix = settings.value("external_prefix", external_link_prefix).toString();
404
405 settings.endGroup();
406 }
407 {
408 settings.beginGroup("Formatting");
409
410 justify_text = settings.value("justify_text", justify_text).toBool();
411 text_width_enabled = settings.value("text_width_enabled", text_width_enabled).toBool();
412 centre_h1 = settings.value("centre_h1", centre_h1).toBool();
413 text_width = settings.value("text_width", text_width).toInt();
414 line_height_p = settings.value("line_height_p", line_height_p).toDouble();
415 line_height_h = settings.value("line_height_h", line_height_h).toDouble();
416 indent_bq = settings.value("indent_bq", indent_bq).toInt();
417 indent_p = settings.value("indent_p", indent_p).toInt();
418 indent_h = settings.value("indent_h", indent_h).toInt();
419 indent_l = settings.value("indent_l", indent_l).toInt();
420 indent_size = settings.value("indent_size", indent_size).toDouble();
421 list_symbol = (QTextListFormat::Style)settings.value("list_symbol", list_symbol).toInt();
422
423 settings.endGroup();
424 }
425
426 } break;
427 default:
428 return false;
429 }
430
431 return true;
432 }
433
derive(const QUrl & url) const434 DocumentStyle DocumentStyle::derive(const QUrl &url) const
435 {
436 DocumentStyle themed = *this;
437
438 // Patch font lists to allow improved emoji display:
439
440 static QStringList emojiFonts = {
441 "<PLACEHOLDER>",
442 "<FALLBACK>",
443 "Apple Color Emoji",
444 "Segoe UI Emoji",
445 "Twitter Color Emoji",
446 "Noto Color Emoji",
447 "JoyPixels",
448 };
449
450 DefaultFonts default_fonts;
451
452 auto const patchup_font = [&default_fonts](QFont & font, bool fixed=false)
453 {
454 // Set the "fallback" font, just to be absolutely sure.
455 // Note the main purpose of this is to avoid emoji fonts
456 // from taking precedence over text fonts.
457 // (fixes *nix default font issues)
458 emojiFonts[1] = fixed
459 ? default_fonts.fixed
460 : default_fonts.regular;
461
462 // Set the primary font as the preferred font.
463 // We ensure that the font family is available first,
464 // so that we don't get an ugly default font
465 // (fixes Windows' default font problem)
466 QFontDatabase db;
467 if (!db.families().contains(font.family()))
468 {
469 emojiFonts.front() = fixed
470 ? default_fonts.fixed
471 : default_fonts.regular;
472 }
473 else
474 {
475 emojiFonts.front() = font.family();
476 }
477
478 // Set emoji fonts if supported and enabled.
479 if (kristall::EMOJIS_SUPPORTED &&
480 kristall::globals().options.emojis_enabled)
481 {
482 // Redundant check to make compiler happy...
483 #if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)
484 font.setFamilies(emojiFonts);
485 #endif
486 }
487 else
488 {
489 font.setFamily(emojiFonts.front());
490 }
491 };
492
493 patchup_font(themed.h1_font);
494 patchup_font(themed.h2_font);
495 patchup_font(themed.h3_font);
496 patchup_font(themed.standard_font);
497 patchup_font(themed.preformatted_font, true);
498 patchup_font(themed.blockquote_font);
499
500 if (this->theme == Fixed)
501 return themed;
502
503 QByteArray hash = QCryptographicHash::hash(url.host().toUtf8(), QCryptographicHash::Md5);
504
505 std::array<uint8_t, 16> items;
506 assert(items.size() == hash.size());
507 memcpy(items.data(), hash.data(), items.size());
508
509 float hue = (items[0] + items[1]) / 510.0;
510 float saturation = items[2] / 255.0;
511
512 double tmp;
513 switch (this->theme)
514 {
515 case AutoDarkTheme:
516 {
517 themed.background_color = QColor::fromHslF(hue, saturation, 0.25f);
518 themed.standard_color = QColor{0xFF, 0xFF, 0xFF};
519
520 themed.h1_color = QColor::fromHslF(std::modf(hue + 0.5, &tmp), 1.0 - saturation, 0.75);
521 themed.h2_color = QColor::fromHslF(std::modf(hue + 0.5, &tmp), 1.0 - saturation, 0.75);
522 themed.h3_color = QColor::fromHslF(std::modf(hue + 0.5, &tmp), 1.0 - saturation, 0.75);
523
524 themed.external_link_color = QColor::fromHslF(std::modf(hue + 0.25, &tmp), 1.0, 0.75);
525 themed.internal_link_color = themed.external_link_color.lighter(110);
526 themed.cross_scheme_link_color = themed.external_link_color.darker(110);
527
528 themed.blockquote_bgcolor = themed.background_color.lighter(130);
529 themed.blockquote_fgcolor = QColor{0xEE, 0xEE, 0xEE};
530
531 break;
532 }
533
534 case AutoLightTheme:
535 {
536 themed.background_color = QColor::fromHslF(hue, items[2] / 255.0, 0.85);
537 themed.standard_color = QColor{0x00, 0x00, 0x00};
538
539 themed.h1_color = QColor::fromHslF(std::modf(hue + 0.5, &tmp), 1.0 - saturation, 0.25);
540 themed.h2_color = QColor::fromHslF(std::modf(hue + 0.5, &tmp), 1.0 - saturation, 0.25);
541 themed.h3_color = QColor::fromHslF(std::modf(hue + 0.5, &tmp), 1.0 - saturation, 0.25);
542
543 themed.external_link_color = QColor::fromHslF(std::modf(hue + 0.25, &tmp), 1.0, 0.25);
544 themed.internal_link_color = themed.external_link_color.darker(110);
545 themed.cross_scheme_link_color = themed.external_link_color.lighter(110);
546
547 themed.blockquote_bgcolor = themed.background_color.darker(113);
548 themed.blockquote_fgcolor = QColor{0x40, 0x40, 0x40};
549
550 break;
551 }
552
553 case Fixed:
554 assert(false);
555 }
556
557 // Same for all themes
558 themed.preformatted_color = themed.standard_color;
559
560 return themed;
561 }
562
toStyleSheet() const563 QString DocumentStyle::toStyleSheet() const
564 {
565 QString css;
566
567 css += QString("p { color: %2; %1 }\n").arg(encodeCssFont (standard_font), standard_color.name());
568 css += QString("a { color: %2; %1 }\n").arg(encodeCssFont (standard_font), external_link_color.name());
569 css += QString("pre { color: %2; %1 }\n").arg(encodeCssFont (preformatted_font), preformatted_color.name());
570 css += QString("h1 { color: %2; %1 }\n").arg(encodeCssFont (h1_font), h1_color.name());
571 css += QString("h2 { color: %2; %1 }\n").arg(encodeCssFont (h2_font), h2_color.name());
572 css += QString("h3 { color: %2; %1 }\n").arg(encodeCssFont (h3_font), h3_color.name());
573 css += QString("blockquote { background: %1; color: %2; %3 }\n")
574 .arg(blockquote_bgcolor.name(), blockquote_fgcolor.name(), encodeCssFont(blockquote_font));
575
576 // qDebug() << "CSS → " << css;
577 return css;
578 }
579