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