summaryrefslogtreecommitdiff
path: root/src/gmdocument.c
diff options
context:
space:
mode:
authorJaakko Keränen <jaakko.keranen@iki.fi>2021-10-13 09:49:44 +0300
committerJaakko Keränen <jaakko.keranen@iki.fi>2021-10-13 09:49:44 +0300
commit6b931c95725eef2ebb7e831c4017d3d67b33294f (patch)
tree254595663d93c0dcc33baad475fb2f6c3ddeec40 /src/gmdocument.c
parentdd0a8798f32bf192ed703c6c512d4a76c4d407bc (diff)
Text attributes that change inside a run
These changes concern the situation when the attributes of text (i.e., font, color) are changed via escape sequences. The concept of "base attributes" was added so that the low-level text renderer knows which font/color to set when a "reset" escape sequence is encountered. This depends on what kind of text is being renderer, e.g., preformatted or regular paragraphs. The base attributes were added as variables in Text because it was getting unwieldy to pass all the information via the draw/measure/WrapText functions. GmDocument now has a GmTheme struct that collects the font and color information into a single place.
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 };