summaryrefslogtreecommitdiff
path: root/src/gmdocument.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/gmdocument.c')
-rw-r--r--src/gmdocument.c200
1 files changed, 128 insertions, 72 deletions
diff --git a/src/gmdocument.c b/src/gmdocument.c
index f0d9bf08..5c2a849e 100644
--- a/src/gmdocument.c
+++ b/src/gmdocument.c
@@ -75,6 +75,15 @@ iDefineTypeConstruction(GmLink)
75 75
76/*----------------------------------------------------------------------------------------------*/ 76/*----------------------------------------------------------------------------------------------*/
77 77
78iDeclareType(GmTheme)
79
80struct Impl_GmTheme {
81 int colors[max_GmLineType];
82 int fonts[max_GmLineType];
83};
84
85/*----------------------------------------------------------------------------------------------*/
86
78struct Impl_GmDocument { 87struct Impl_GmDocument {
79 iObject object; 88 iObject object;
80 enum iSourceFormat format; 89 enum iSourceFormat format;
@@ -91,6 +100,7 @@ struct Impl_GmDocument {
91 iString title; /* the first top-level title */ 100 iString title; /* the first top-level title */
92 iArray headings; 101 iArray headings;
93 iArray preMeta; /* metadata about preformatted blocks */ 102 iArray preMeta; /* metadata about preformatted blocks */
103 iGmTheme theme;
94 uint32_t themeSeed; 104 uint32_t themeSeed;
95 iChar siteIcon; 105 iChar siteIcon;
96 iMedia * media; 106 iMedia * media;
@@ -101,6 +111,51 @@ struct Impl_GmDocument {
101 111
102iDefineObjectConstruction(GmDocument) 112iDefineObjectConstruction(GmDocument)
103 113
114static iBool isForcedMonospace_GmDocument_(const iGmDocument *d) {
115 const iRangecc scheme = urlScheme_String(&d->url);
116 if (equalCase_Rangecc(scheme, "gemini")) {
117 return prefs_App()->monospaceGemini;
118 }
119 if (equalCase_Rangecc(scheme, "gopher") ||
120 equalCase_Rangecc(scheme, "finger")) {
121 return prefs_App()->monospaceGopher;
122 }
123 return iFalse;
124}
125
126static void initTheme_GmDocument_(iGmDocument *d) {
127 static const int defaultColors[max_GmLineType] = {
128 tmParagraph_ColorId,
129 tmParagraph_ColorId, /* bullet */
130 tmPreformatted_ColorId,
131 tmQuote_ColorId,
132 tmHeading1_ColorId,
133 tmHeading2_ColorId,
134 tmHeading3_ColorId,
135 tmLinkText_ColorId,
136 };
137 iGmTheme *theme = &d->theme;
138 memcpy(theme->colors, defaultColors, sizeof(theme->colors));
139 const iPrefs *prefs = prefs_App();
140 const iBool isMono = isForcedMonospace_GmDocument_(d);
141 const iBool isDarkBg = isDark_GmDocumentTheme(
142 isDark_ColorTheme(colorTheme_App()) ? prefs->docThemeDark : prefs->docThemeLight);
143 const enum iFontId headingFont = isMono ? documentMonospace_FontId : documentHeading_FontId;
144 const enum iFontId bodyFont = isMono ? documentMonospace_FontId : documentBody_FontId;
145 theme->fonts[text_GmLineType] = FONT_ID(bodyFont, regular_FontStyle, contentRegular_FontSize);
146 theme->fonts[bullet_GmLineType] = FONT_ID(bodyFont, regular_FontStyle, contentRegular_FontSize);
147 theme->fonts[preformatted_GmLineType] = preformatted_FontId;
148 theme->fonts[quote_GmLineType] = isMono ? monospaceParagraph_FontId : quote_FontId;
149 theme->fonts[heading1_GmLineType] = FONT_ID(headingFont, bold_FontStyle, contentHuge_FontSize);
150 theme->fonts[heading2_GmLineType] = FONT_ID(headingFont, bold_FontStyle, contentLarge_FontSize);
151 theme->fonts[heading3_GmLineType] = FONT_ID(headingFont, regular_FontStyle, contentBig_FontSize);
152 theme->fonts[link_GmLineType] = FONT_ID(
153 bodyFont,
154 ((isDarkBg && prefs->boldLinkDark) || (!isDarkBg && prefs->boldLinkLight)) ? semiBold_FontStyle
155 : regular_FontStyle,
156 contentRegular_FontSize);
157}
158
104static enum iGmLineType lineType_GmDocument_(const iGmDocument *d, const iRangecc line) { 159static enum iGmLineType lineType_GmDocument_(const iGmDocument *d, const iRangecc line) {
105 if (d->format == plainText_SourceFormat) { 160 if (d->format == plainText_SourceFormat) {
106 return text_GmLineType; 161 return text_GmLineType;
@@ -318,18 +373,6 @@ static iBool isGopher_GmDocument_(const iGmDocument *d) {
318 equalCase_Rangecc(scheme, "finger")); 373 equalCase_Rangecc(scheme, "finger"));
319} 374}
320 375
321static iBool isForcedMonospace_GmDocument_(const iGmDocument *d) {
322 const iRangecc scheme = urlScheme_String(&d->url);
323 if (equalCase_Rangecc(scheme, "gemini")) {
324 return prefs_App()->monospaceGemini;
325 }
326 if (equalCase_Rangecc(scheme, "gopher") ||
327 equalCase_Rangecc(scheme, "finger")) {
328 return prefs_App()->monospaceGopher;
329 }
330 return iFalse;
331}
332
333static void linkContentWasLaidOut_GmDocument_(iGmDocument *d, const iGmMediaInfo *mediaInfo, 376static void linkContentWasLaidOut_GmDocument_(iGmDocument *d, const iGmMediaInfo *mediaInfo,
334 uint16_t linkId) { 377 uint16_t linkId) {
335 iGmLink *link = at_PtrArray(&d->links, linkId - 1); 378 iGmLink *link = at_PtrArray(&d->links, linkId - 1);
@@ -402,7 +445,8 @@ struct Impl_RunTypesetter {
402 int rightMargin; 445 int rightMargin;
403 iBool isWordWrapped; 446 iBool isWordWrapped;
404 iBool isPreformat; 447 iBool isPreformat;
405 const int *fonts; 448 int baseFont;
449 int baseColor;
406}; 450};
407 451
408static void init_RunTypesetter_(iRunTypesetter *d) { 452static void init_RunTypesetter_(iRunTypesetter *d) {
@@ -425,39 +469,47 @@ static void commit_RunTypesetter_(iRunTypesetter *d, iGmDocument *doc) {
425 469
426static const int maxLedeLines_ = 10; 470static const int maxLedeLines_ = 10;
427 471
428static const int colors[max_GmLineType] = { 472static int applyAttributes_RunTypesetter_(iRunTypesetter *d, iTextAttrib attrib) {
429 tmParagraph_ColorId, 473 /* WARNING: This is duplicated in run_Font_(). Make sure they behave identically. */
430 tmParagraph_ColorId, 474 if (attrib.bold) {
431 tmPreformatted_ColorId, 475 d->run.font = fontWithStyle_Text(d->baseFont, bold_FontStyle);
432 tmQuote_ColorId, 476 d->run.color = tmFirstParagraph_ColorId;
433 tmHeading1_ColorId, 477 }
434 tmHeading2_ColorId, 478 else if (attrib.italic) {
435 tmHeading3_ColorId, 479 d->run.font = fontWithStyle_Text(d->baseFont, italic_FontStyle);
436 tmLinkText_ColorId, 480 }
437}; 481 else if (attrib.monospace) {
482 d->run.font = fontWithFamily_Text(d->baseFont, monospace_FontId);
483 d->run.color = tmPreformatted_ColorId;
484 }
485 else {
486 d->run.font = d->baseFont;
487 d->run.color = d->baseColor;
488 }
489}
438 490
439static iBool typesetOneLine_RunTypesetter_(iWrapText *wrap, iRangecc wrapRange, int origin, 491static iBool typesetOneLine_RunTypesetter_(iWrapText *wrap, iRangecc wrapRange, iTextAttrib attrib,
440 int advance, iBool isBaseRTL) { 492 int origin, int advance) {
441 iAssert(wrapRange.start <= wrapRange.end); 493 iAssert(wrapRange.start <= wrapRange.end);
442 trimEnd_Rangecc(&wrapRange); 494 trimEnd_Rangecc(&wrapRange);
443// printf("typeset: {%s}\n", cstr_Rangecc(wrapRange)); 495// printf("typeset: {%s}\n", cstr_Rangecc(wrapRange));
444 iRunTypesetter *d = wrap->context; 496 iRunTypesetter *d = wrap->context;
445 const int fontId = d->run.font;
446 d->run.text = wrapRange; 497 d->run.text = wrapRange;
498 applyAttributes_RunTypesetter_(d, attrib);
447 if (~d->run.flags & startOfLine_GmRunFlag && d->lineHeightReduction > 0.0f) { 499 if (~d->run.flags & startOfLine_GmRunFlag && d->lineHeightReduction > 0.0f) {
448 d->pos.y -= d->lineHeightReduction * lineHeight_Text(fontId); 500 d->pos.y -= d->lineHeightReduction * lineHeight_Text(d->baseFont);
449 } 501 }
450 d->run.bounds.pos = addX_I2(d->pos, origin + d->indent); 502 d->run.bounds.pos = addX_I2(d->pos, origin + d->indent);
451 const iInt2 dims = init_I2(advance, lineHeight_Text(fontId)); 503 const iInt2 dims = init_I2(advance, lineHeight_Text(d->baseFont));
452 iChangeFlags(d->run.flags, wide_GmRunFlag, (d->isPreformat && dims.x > d->layoutWidth)); 504 iChangeFlags(d->run.flags, wide_GmRunFlag, (d->isPreformat && dims.x > d->layoutWidth));
453 d->run.bounds.size.x = iMax(wrap->maxWidth, dims.x) - origin; /* Extends to the right edge for selection. */ 505 d->run.bounds.size.x = iMax(wrap->maxWidth, dims.x) - origin; /* Extends to the right edge for selection. */
454 d->run.bounds.size.y = dims.y; 506 d->run.bounds.size.y = dims.y;
455 d->run.visBounds = d->run.bounds; 507 d->run.visBounds = d->run.bounds;
456 d->run.visBounds.size.x = dims.x; 508 d->run.visBounds.size.x = dims.x;
457 d->run.isRTL = isBaseRTL; 509 d->run.isRTL = attrib.isBaseRTL;
458 pushBack_Array(&d->layout, &d->run); 510 pushBack_Array(&d->layout, &d->run);
459 d->run.flags &= ~startOfLine_GmRunFlag; 511 d->run.flags &= ~startOfLine_GmRunFlag;
460 d->pos.y += lineHeight_Text(fontId) * prefs_App()->lineSpacing; 512 d->pos.y += lineHeight_Text(d->baseFont) * prefs_App()->lineSpacing;
461 return iTrue; /* continue to next wrapped line */ 513 return iTrue; /* continue to next wrapped line */
462} 514}
463 515
@@ -469,25 +521,10 @@ static void doLayout_GmDocument_(iGmDocument *d) {
469 const iBool isVeryNarrow = d->size.x <= 70 * gap_Text; 521 const iBool isVeryNarrow = d->size.x <= 70 * gap_Text;
470 const iBool isExtremelyNarrow = d->size.x <= 60 * gap_Text; 522 const iBool isExtremelyNarrow = d->size.x <= 60 * gap_Text;
471 const iBool isFullWidthImages = (d->outsideMargin < 5 * gap_UI); 523 const iBool isFullWidthImages = (d->outsideMargin < 5 * gap_UI);
472 const iBool isDarkBg = isDark_GmDocumentTheme( 524// const iBool isDarkBg = isDark_GmDocumentTheme(
473 isDark_ColorTheme(colorTheme_App()) ? prefs->docThemeDark : prefs->docThemeLight); 525// isDark_ColorTheme(colorTheme_App()) ? prefs->docThemeDark : prefs->docThemeLight);
526 initTheme_GmDocument_(d);
474 /* TODO: Collect these parameters into a GmTheme. */ 527 /* TODO: Collect these parameters into a GmTheme. */
475 const enum iFontId headingFont = isMono ? documentMonospace_FontId : documentHeading_FontId;
476 const enum iFontId bodyFont = isMono ? documentMonospace_FontId : documentBody_FontId;
477 const int fonts[max_GmLineType] = {
478 FONT_ID(bodyFont, regular_FontStyle, contentRegular_FontSize), /* text */
479 FONT_ID(bodyFont, regular_FontStyle, contentRegular_FontSize), /* bullet */
480 preformatted_FontId, /* pre */
481 isMono ? monospaceParagraph_FontId : quote_FontId, /* quote */
482 FONT_ID(headingFont, bold_FontStyle, contentHuge_FontSize), /* h1 */
483 FONT_ID(headingFont, bold_FontStyle, contentLarge_FontSize), /* h2 */
484 FONT_ID(headingFont, regular_FontStyle, contentBig_FontSize), /* h3 */
485 FONT_ID(bodyFont,
486 ((isDarkBg && prefs->boldLinkDark) || (!isDarkBg && prefs->boldLinkLight))
487 ? semiBold_FontStyle
488 : regular_FontStyle,
489 contentRegular_FontSize) /* link */
490 };
491 float indents[max_GmLineType] = { 5, 10, 5, isNarrow ? 5 : 10, 0, 0, 0, 5 }; 528 float indents[max_GmLineType] = { 5, 10, 5, isNarrow ? 5 : 10, 0, 0, 0, 5 };
492 if (isExtremelyNarrow) { 529 if (isExtremelyNarrow) {
493 /* Further reduce the margins. */ 530 /* Further reduce the margins. */
@@ -598,7 +635,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
598 } 635 }
599 } 636 }
600 trimLine_Rangecc(&line, type, isNormalized); 637 trimLine_Rangecc(&line, type, isNormalized);
601 run.font = fonts[type]; 638 run.font = d->theme.fonts[type];
602 /* Remember headings for the document outline. */ 639 /* Remember headings for the document outline. */
603 if (type == heading1_GmLineType || type == heading2_GmLineType || type == heading3_GmLineType) { 640 if (type == heading1_GmLineType || type == heading2_GmLineType || type == heading3_GmLineType) {
604 pushBack_Array( 641 pushBack_Array(
@@ -618,7 +655,8 @@ static void doLayout_GmDocument_(iGmDocument *d) {
618 addSiteBanner = iFalse; /* overrides the banner */ 655 addSiteBanner = iFalse; /* overrides the banner */
619 continue; 656 continue;
620 } 657 }
621 run.preId = preId; 658 run.mediaType = max_MediaType; /* preformatted block */
659 run.mediaId = preId;
622 run.font = (d->format == plainText_SourceFormat ? plainText_FontId : preFont); 660 run.font = (d->format == plainText_SourceFormat ? plainText_FontId : preFont);
623 indent = indents[type]; 661 indent = indents[type];
624 } 662 }
@@ -650,7 +688,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
650 run.visBounds.size = init_I2(gap_Text, lineHeight_Text(run.font)); 688 run.visBounds.size = init_I2(gap_Text, lineHeight_Text(run.font));
651 run.bounds = zero_Rect(); /* just visual */ 689 run.bounds = zero_Rect(); /* just visual */
652 run.text = iNullRange; 690 run.text = iNullRange;
653 run.flags = quoteBorder_GmRunFlag | decoration_GmRunFlag; 691 run.flags = quoteBorder_GmRunFlag | decoration_GmRunFlag;
654 pushBack_Array(&d->layout, &run); 692 pushBack_Array(&d->layout, &run);
655 } 693 }
656 pos.y += lineHeight_Text(run.font) * prefs->lineSpacing; 694 pos.y += lineHeight_Text(run.font) * prefs->lineSpacing;
@@ -708,7 +746,8 @@ static void doLayout_GmDocument_(iGmDocument *d) {
708 altText.text).bounds.size; 746 altText.text).bounds.size;
709 altText.bounds = altText.visBounds = init_Rect(pos.x, pos.y, d->size.x, 747 altText.bounds = altText.visBounds = init_Rect(pos.x, pos.y, d->size.x,
710 size.y + 2 * margin.y); 748 size.y + 2 * margin.y);
711 altText.preId = preId; 749 altText.mediaType = max_MediaType; /* preformatted */
750 altText.mediaId = preId;
712 pushBack_Array(&d->layout, &altText); 751 pushBack_Array(&d->layout, &altText);
713 pos.y += height_Rect(altText.bounds); 752 pos.y += height_Rect(altText.bounds);
714 contentLine = meta->bounds; /* Skip the whole thing. */ 753 contentLine = meta->bounds; /* Skip the whole thing. */
@@ -723,7 +762,6 @@ static void doLayout_GmDocument_(iGmDocument *d) {
723 setRange_String(&d->title, line); 762 setRange_String(&d->title, line);
724 } 763 }
725 /* List bullet. */ 764 /* List bullet. */
726 run.color = colors[type];
727 if (type == bullet_GmLineType) { 765 if (type == bullet_GmLineType) {
728 /* TODO: Literata bullet is broken? */ 766 /* TODO: Literata bullet is broken? */
729 iGmRun bulRun = run; 767 iGmRun bulRun = run;
@@ -795,18 +833,17 @@ static void doLayout_GmDocument_(iGmDocument *d) {
795 icon.flags |= decoration_GmRunFlag; 833 icon.flags |= decoration_GmRunFlag;
796 pushBack_Array(&d->layout, &icon); 834 pushBack_Array(&d->layout, &icon);
797 } 835 }
798 run.color = colors[type]; 836 run.lineType = type;
837 run.color = d->theme.colors[type];
799 if (d->format == plainText_SourceFormat) { 838 if (d->format == plainText_SourceFormat) {
800 run.color = colors[text_GmLineType]; 839 run.color = d->theme.colors[text_GmLineType];
801 } 840 }
802 /* Special formatting for the first paragraph (e.g., subtitle, introduction, or lede). */ 841 /* Special formatting for the first paragraph (e.g., subtitle, introduction, or lede). */
803// int bigCount = 0; 842// int bigCount = 0;
804 iBool isLedeParagraph = iFalse;
805 if (type == text_GmLineType && isFirstText) { 843 if (type == text_GmLineType && isFirstText) {
806 if (!isMono) run.font = firstParagraph_FontId; 844 if (!isMono) run.font = firstParagraph_FontId;
807 run.color = tmFirstParagraph_ColorId; 845 run.color = tmFirstParagraph_ColorId;
808// bigCount = 15; /* max lines -- what if the whole document is one paragraph? */ 846 run.isLede = iTrue;
809 isLedeParagraph = iTrue;
810 isFirstText = iFalse; 847 isFirstText = iFalse;
811 } 848 }
812 else if (type != heading1_GmLineType) { 849 else if (type != heading1_GmLineType) {
@@ -826,7 +863,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
826 init_RunTypesetter_(&rts); 863 init_RunTypesetter_(&rts);
827 rts.run = run; 864 rts.run = run;
828 rts.pos = pos; 865 rts.pos = pos;
829 rts.fonts = fonts; 866 //rts.fonts = fonts;
830 rts.isWordWrapped = (d->format == plainText_SourceFormat ? prefs->plainTextWrap 867 rts.isWordWrapped = (d->format == plainText_SourceFormat ? prefs->plainTextWrap
831 : !isPreformat); 868 : !isPreformat);
832 rts.isPreformat = isPreformat; 869 rts.isPreformat = isPreformat;
@@ -859,6 +896,8 @@ static void doLayout_GmDocument_(iGmDocument *d) {
859 } 896 }
860 for (;;) { /* need to retry if the font needs changing */ 897 for (;;) { /* need to retry if the font needs changing */
861 rts.run.flags |= startOfLine_GmRunFlag; 898 rts.run.flags |= startOfLine_GmRunFlag;
899 rts.baseFont = rts.run.font;
900 rts.baseColor = rts.run.color;
862 iWrapText wrapText = { .text = line, 901 iWrapText wrapText = { .text = line,
863 .maxWidth = rts.isWordWrapped 902 .maxWidth = rts.isWordWrapped
864 ? d->size.x - run.bounds.pos.x - 903 ? d->size.x - run.bounds.pos.x -
@@ -868,7 +907,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
868 .wrapFunc = typesetOneLine_RunTypesetter_, 907 .wrapFunc = typesetOneLine_RunTypesetter_,
869 .context = &rts }; 908 .context = &rts };
870 measure_WrapText(&wrapText, rts.run.font); 909 measure_WrapText(&wrapText, rts.run.font);
871 if (!isLedeParagraph || size_Array(&rts.layout) <= maxLedeLines_) { 910 if (!rts.run.isLede || size_Array(&rts.layout) <= maxLedeLines_) {
872 if (wrapText.baseDir < 0) { 911 if (wrapText.baseDir < 0) {
873 /* Right-aligned paragraphs need margins and decorations to be flipped. */ 912 /* Right-aligned paragraphs need margins and decorations to be flipped. */
874 iForEach(Array, pr, &rts.layout) { 913 iForEach(Array, pr, &rts.layout) {
@@ -891,11 +930,12 @@ static void doLayout_GmDocument_(iGmDocument *d) {
891 commit_RunTypesetter_(&rts, d); 930 commit_RunTypesetter_(&rts, d);
892 break; 931 break;
893 } 932 }
933 /* Try again... */
894 clear_RunTypesetter_(&rts); 934 clear_RunTypesetter_(&rts);
895 rts.pos = pos; 935 rts.pos = pos;
896 rts.run.font = rts.fonts[text_GmLineType]; 936 rts.run.font = rts.baseFont = d->theme.fonts[text_GmLineType];
897 rts.run.color = colors[text_GmLineType]; 937 rts.run.color = rts.baseColor = d->theme.colors[text_GmLineType];
898 isLedeParagraph = iFalse; 938 rts.run.isLede = iFalse;
899 } 939 }
900 pos = rts.pos; 940 pos = rts.pos;
901 deinit_RunTypesetter_(&rts); 941 deinit_RunTypesetter_(&rts);
@@ -996,7 +1036,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
996 /* TODO: Store the dimensions and ranges for later access. */ 1036 /* TODO: Store the dimensions and ranges for later access. */
997 iForEach(Array, i, &d->layout) { 1037 iForEach(Array, i, &d->layout) {
998 iGmRun *run = i.value; 1038 iGmRun *run = i.value;
999 if (run->preId && run->flags & wide_GmRunFlag) { 1039 if (preId_GmRun(run) && run->flags & wide_GmRunFlag) {
1000 iGmRunRange block = findPreformattedRange_GmDocument(d, run); 1040 iGmRunRange block = findPreformattedRange_GmDocument(d, run);
1001 for (const iGmRun *j = block.start; j != block.end; j++) { 1041 for (const iGmRun *j = block.start; j != block.end; j++) {
1002 iConstCast(iGmRun *, j)->flags |= wide_GmRunFlag; 1042 iConstCast(iGmRun *, j)->flags |= wide_GmRunFlag;
@@ -1800,11 +1840,11 @@ static void flushPendingLinks_(iArray *links, const iString *source, iString *ou
1800static void convertMarkdownToGemtext_GmDocument_(iGmDocument *d) { 1840static void convertMarkdownToGemtext_GmDocument_(iGmDocument *d) {
1801 iAssert(d->format == markdown_SourceFormat); 1841 iAssert(d->format == markdown_SourceFormat);
1802 /* Get rid of indented preformats. */ { 1842 /* Get rid of indented preformats. */ {
1803 iArray *pendingLinks = collectNew_Array(sizeof(iPendingLink)); 1843 iArray *pendingLinks = collectNew_Array(sizeof(iPendingLink));
1804 const iRegExp *imageLinkPattern = iClob(new_RegExp("\n?!\\[(.+)\\]\\(([^)]+)\\)\n?", 0)); 1844 const iRegExp *imageLinkPattern = iClob(new_RegExp("\n?!\\[(.+)\\]\\(([^)]+)\\)\n?", 0));
1805 const iRegExp *linkPattern = iClob(new_RegExp("\\[(.+?)\\]\\(([^)]+)\\)", 0)); 1845 const iRegExp *linkPattern = iClob(new_RegExp("\\[(.+?)\\]\\(([^)]+)\\)", 0));
1806 const iRegExp *namedLinkPattern = iClob(new_RegExp("\\[(.+?)\\]\\[(.+?)\\]", 0)); 1846 const iRegExp *namedLinkPattern = iClob(new_RegExp("\\[(.+?)\\]\\[(.+?)\\]", 0));
1807 const iRegExp *namePattern = iClob(new_RegExp("\\s*\\[(.+?)\\]\\s*:\\s*([^\n]+)", 0)); 1847 const iRegExp *namePattern = iClob(new_RegExp("\\s*\\[(.+?)\\]\\s*:\\s*([^\n]+)", 0));
1808 iString result; 1848 iString result;
1809 init_String(&result); 1849 init_String(&result);
1810 iRangecc line = iNullRange; 1850 iRangecc line = iNullRange;
@@ -1865,7 +1905,7 @@ static void convertMarkdownToGemtext_GmDocument_(iGmDocument *d) {
1865 replaceRegExp_String(&ln, imageLinkPattern, "\n=> \\2 \\1\n", NULL, NULL); 1905 replaceRegExp_String(&ln, imageLinkPattern, "\n=> \\2 \\1\n", NULL, NULL);
1866 replaceRegExp_String(&ln, namedLinkPattern, "\\1", addPendingNamedLink_, pendingLinks); 1906 replaceRegExp_String(&ln, namedLinkPattern, "\\1", addPendingNamedLink_, pendingLinks);
1867 replaceRegExp_String(&ln, linkPattern, "\\1", addPendingLink_, pendingLinks); 1907 replaceRegExp_String(&ln, linkPattern, "\\1", addPendingLink_, pendingLinks);
1868 replaceRegExp_String(&ln, iClob(new_RegExp("(?<!`)`([^`]*?)`(?!`)", 0)), "\x1b[4m\\1\x1b[0m", NULL, NULL); 1908 replaceRegExp_String(&ln, iClob(new_RegExp("(?<!`)`([^`]+?)`(?!`)", 0)), "\x1b[4m\\1\x1b[0m", NULL, NULL);
1869 append_String(&result, &ln); 1909 append_String(&result, &ln);
1870 deinit_String(&ln); 1910 deinit_String(&ln);
1871 } 1911 }
@@ -2054,17 +2094,17 @@ iRangecc findTextBefore_GmDocument(const iGmDocument *d, const iString *text, co
2054} 2094}
2055 2095
2056iGmRunRange findPreformattedRange_GmDocument(const iGmDocument *d, const iGmRun *run) { 2096iGmRunRange findPreformattedRange_GmDocument(const iGmDocument *d, const iGmRun *run) {
2057 iAssert(run->preId); 2097 iAssert(preId_GmRun(run));
2058 iGmRunRange range = { run, run }; 2098 iGmRunRange range = { run, run };
2059 /* Find the beginning. */ 2099 /* Find the beginning. */
2060 while (range.start > (const iGmRun *) constData_Array(&d->layout)) { 2100 while (range.start > (const iGmRun *) constData_Array(&d->layout)) {
2061 const iGmRun *prev = range.start - 1; 2101 const iGmRun *prev = range.start - 1;
2062 if (prev->preId != run->preId) break; 2102 if (preId_GmRun(prev) != preId_GmRun(run)) break;
2063 range.start = prev; 2103 range.start = prev;
2064 } 2104 }
2065 /* Find the ending. */ 2105 /* Find the ending. */
2066 while (range.end < (const iGmRun *) constEnd_Array(&d->layout)) { 2106 while (range.end < (const iGmRun *) constEnd_Array(&d->layout)) {
2067 if (range.end->preId != run->preId) break; 2107 if (preId_GmRun(range.end) != preId_GmRun(run)) break;
2068 range.end++; 2108 range.end++;
2069 } 2109 }
2070 return range; 2110 return range;
@@ -2240,6 +2280,22 @@ iChar siteIcon_GmDocument(const iGmDocument *d) {
2240 return d->siteIcon; 2280 return d->siteIcon;
2241} 2281}
2242 2282
2283void runBaseAttributes_GmDocument(const iGmDocument *d, const iGmRun *run, int *fontId_out,
2284 int *colorId_out) {
2285 /* Font and color according to the line type. These are needed because each GmRun is
2286 a segment of a paragraph, and if the font or color changes inside the run, each wrapped
2287 segment needs to know both the current font/color and ALSO the base font/color, so
2288 the default attributes can be restored. */
2289 if (run->isLede) {
2290 *fontId_out = firstParagraph_FontId;
2291 *colorId_out = tmFirstParagraph_ColorId;
2292 }
2293 else {
2294 *fontId_out = fontWithSize_Text(d->theme.fonts[run->lineType], run->font % max_FontSize); /* retain size */
2295 *colorId_out = d->theme.colors[run->lineType];
2296 }
2297}
2298
2243iRangecc findLoc_GmRun(const iGmRun *d, iInt2 pos) { 2299iRangecc findLoc_GmRun(const iGmRun *d, iInt2 pos) {
2244 if (pos.y < top_Rect(d->bounds)) { 2300 if (pos.y < top_Rect(d->bounds)) {
2245 return (iRangecc){ d->text.start, d->text.start }; 2301 return (iRangecc){ d->text.start, d->text.start };