summaryrefslogtreecommitdiff
path: root/src/gmdocument.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/gmdocument.c')
-rw-r--r--src/gmdocument.c734
1 files changed, 524 insertions, 210 deletions
diff --git a/src/gmdocument.c b/src/gmdocument.c
index 3e640f08..043d3259 100644
--- a/src/gmdocument.c
+++ b/src/gmdocument.c
@@ -27,6 +27,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
27#include "ui/color.h" 27#include "ui/color.h"
28#include "ui/text.h" 28#include "ui/text.h"
29#include "ui/metrics.h" 29#include "ui/metrics.h"
30#include "ui/mediaui.h"
30#include "ui/window.h" 31#include "ui/window.h"
31#include "visited.h" 32#include "visited.h"
32#include "bookmarks.h" 33#include "bookmarks.h"
@@ -74,6 +75,16 @@ iDefineTypeConstruction(GmLink)
74 75
75/*----------------------------------------------------------------------------------------------*/ 76/*----------------------------------------------------------------------------------------------*/
76 77
78iDeclareType(GmTheme)
79
80struct Impl_GmTheme {
81 int ansiEscapes;
82 int colors[max_GmLineType];
83 int fonts[max_GmLineType];
84};
85
86/*----------------------------------------------------------------------------------------------*/
87
77struct Impl_GmDocument { 88struct Impl_GmDocument {
78 iObject object; 89 iObject object;
79 enum iSourceFormat format; 90 enum iSourceFormat format;
@@ -83,6 +94,7 @@ struct Impl_GmDocument {
83 iString localHost; 94 iString localHost;
84 iInt2 size; 95 iInt2 size;
85 int outsideMargin; 96 int outsideMargin;
97 iBool enableCommandLinks; /* `about:command?` only allowed on selected pages */
86 iArray layout; /* contents of source, laid out in document space */ 98 iArray layout; /* contents of source, laid out in document space */
87 iPtrArray links; 99 iPtrArray links;
88 enum iGmDocumentBanner bannerType; 100 enum iGmDocumentBanner bannerType;
@@ -90,6 +102,7 @@ struct Impl_GmDocument {
90 iString title; /* the first top-level title */ 102 iString title; /* the first top-level title */
91 iArray headings; 103 iArray headings;
92 iArray preMeta; /* metadata about preformatted blocks */ 104 iArray preMeta; /* metadata about preformatted blocks */
105 iGmTheme theme;
93 uint32_t themeSeed; 106 uint32_t themeSeed;
94 iChar siteIcon; 107 iChar siteIcon;
95 iMedia * media; 108 iMedia * media;
@@ -100,6 +113,51 @@ struct Impl_GmDocument {
100 113
101iDefineObjectConstruction(GmDocument) 114iDefineObjectConstruction(GmDocument)
102 115
116static iBool isForcedMonospace_GmDocument_(const iGmDocument *d) {
117 const iRangecc scheme = urlScheme_String(&d->url);
118 if (equalCase_Rangecc(scheme, "gemini")) {
119 return prefs_App()->monospaceGemini;
120 }
121 if (equalCase_Rangecc(scheme, "gopher") ||
122 equalCase_Rangecc(scheme, "finger")) {
123 return prefs_App()->monospaceGopher;
124 }
125 return iFalse;
126}
127
128static void initTheme_GmDocument_(iGmDocument *d) {
129 static const int defaultColors[max_GmLineType] = {
130 tmParagraph_ColorId,
131 tmParagraph_ColorId, /* bullet */
132 tmPreformatted_ColorId,
133 tmQuote_ColorId,
134 tmHeading1_ColorId,
135 tmHeading2_ColorId,
136 tmHeading3_ColorId,
137 tmLinkText_ColorId,
138 };
139 iGmTheme *theme = &d->theme;
140 memcpy(theme->colors, defaultColors, sizeof(theme->colors));
141 const iPrefs *prefs = prefs_App();
142 const iBool isMono = isForcedMonospace_GmDocument_(d);
143 const iBool isDarkBg = isDark_GmDocumentTheme(
144 isDark_ColorTheme(colorTheme_App()) ? prefs->docThemeDark : prefs->docThemeLight);
145 const enum iFontId headingFont = isMono ? documentMonospace_FontId : documentHeading_FontId;
146 const enum iFontId bodyFont = isMono ? documentMonospace_FontId : documentBody_FontId;
147 theme->fonts[text_GmLineType] = FONT_ID(bodyFont, regular_FontStyle, contentRegular_FontSize);
148 theme->fonts[bullet_GmLineType] = FONT_ID(bodyFont, regular_FontStyle, contentRegular_FontSize);
149 theme->fonts[preformatted_GmLineType] = preformatted_FontId;
150 theme->fonts[quote_GmLineType] = isMono ? monospaceParagraph_FontId : quote_FontId;
151 theme->fonts[heading1_GmLineType] = FONT_ID(headingFont, bold_FontStyle, contentHuge_FontSize);
152 theme->fonts[heading2_GmLineType] = FONT_ID(headingFont, bold_FontStyle, contentLarge_FontSize);
153 theme->fonts[heading3_GmLineType] = FONT_ID(headingFont, regular_FontStyle, contentBig_FontSize);
154 theme->fonts[link_GmLineType] = FONT_ID(
155 bodyFont,
156 ((isDarkBg && prefs->boldLinkDark) || (!isDarkBg && prefs->boldLinkLight)) ? semiBold_FontStyle
157 : regular_FontStyle,
158 contentRegular_FontSize);
159}
160
103static enum iGmLineType lineType_GmDocument_(const iGmDocument *d, const iRangecc line) { 161static enum iGmLineType lineType_GmDocument_(const iGmDocument *d, const iRangecc line) {
104 if (d->format == plainText_SourceFormat) { 162 if (d->format == plainText_SourceFormat) {
105 return text_GmLineType; 163 return text_GmLineType;
@@ -199,6 +257,14 @@ static iRangecc addLink_GmDocument_(iGmDocument *d, iRangecc line, iGmLinkId *li
199 link->urlRange = capturedRange_RegExpMatch(&m, 1); 257 link->urlRange = capturedRange_RegExpMatch(&m, 1);
200 setRange_String(&link->url, link->urlRange); 258 setRange_String(&link->url, link->urlRange);
201 set_String(&link->url, canonicalUrl_String(absoluteUrl_String(&d->url, &link->url))); 259 set_String(&link->url, canonicalUrl_String(absoluteUrl_String(&d->url, &link->url)));
260 if (startsWithCase_String(&link->url, "about:command")) {
261 /* This is a special internal page that allows submitting UI events. */
262 if (!d->enableCommandLinks) {
263 delete_GmLink(link);
264 *linkId = 0;
265 return line;
266 }
267 }
202 /* Check the URL. */ { 268 /* Check the URL. */ {
203 iUrl parts; 269 iUrl parts;
204 init_Url(&parts, &link->url); 270 init_Url(&parts, &link->url);
@@ -224,7 +290,7 @@ static iRangecc addLink_GmDocument_(iGmDocument *d, iRangecc line, iGmLinkId *li
224 setScheme_GmLink_(link, finger_GmLinkScheme); 290 setScheme_GmLink_(link, finger_GmLinkScheme);
225 } 291 }
226 else if (equalCase_Rangecc(parts.scheme, "file")) { 292 else if (equalCase_Rangecc(parts.scheme, "file")) {
227 setScheme_GmLink_(link, file_GmLinkScheme); 293 setScheme_GmLink_(link, file_GmLinkScheme);
228 } 294 }
229 else if (equalCase_Rangecc(parts.scheme, "data")) { 295 else if (equalCase_Rangecc(parts.scheme, "data")) {
230 setScheme_GmLink_(link, data_GmLinkScheme); 296 setScheme_GmLink_(link, data_GmLinkScheme);
@@ -251,6 +317,9 @@ static iRangecc addLink_GmDocument_(iGmDocument *d, iRangecc line, iGmLinkId *li
251 endsWithCase_String(path, ".mid") || endsWithCase_String(path, ".ogg")) { 317 endsWithCase_String(path, ".mid") || endsWithCase_String(path, ".ogg")) {
252 link->flags |= audioFileExtension_GmLinkFlag; 318 link->flags |= audioFileExtension_GmLinkFlag;
253 } 319 }
320 else if (endsWithCase_String(path, ".fontpack")) {
321 link->flags |= fontpackFileExtension_GmLinkFlag;
322 }
254 delete_String(path); 323 delete_String(path);
255 } 324 }
256 /* Check if visited. */ 325 /* Check if visited. */
@@ -276,7 +345,7 @@ static iRangecc addLink_GmDocument_(iGmDocument *d, iRangecc line, iGmLinkId *li
276 /* Check for a custom icon. */ 345 /* Check for a custom icon. */
277 enum iGmLinkScheme scheme = scheme_GmLinkFlag(link->flags); 346 enum iGmLinkScheme scheme = scheme_GmLinkFlag(link->flags);
278 if ((scheme == gemini_GmLinkScheme && ~link->flags & remote_GmLinkFlag) || 347 if ((scheme == gemini_GmLinkScheme && ~link->flags & remote_GmLinkFlag) ||
279 scheme == file_GmLinkScheme || 348 scheme == about_GmLinkScheme || scheme == file_GmLinkScheme ||
280 scheme == mailto_GmLinkScheme) { 349 scheme == mailto_GmLinkScheme) {
281 iChar icon = 0; 350 iChar icon = 0;
282 int len = 0; 351 int len = 0;
@@ -314,18 +383,6 @@ static iBool isGopher_GmDocument_(const iGmDocument *d) {
314 equalCase_Rangecc(scheme, "finger")); 383 equalCase_Rangecc(scheme, "finger"));
315} 384}
316 385
317static iBool isForcedMonospace_GmDocument_(const iGmDocument *d) {
318 const iRangecc scheme = urlScheme_String(&d->url);
319 if (equalCase_Rangecc(scheme, "gemini")) {
320 return prefs_App()->monospaceGemini;
321 }
322 if (equalCase_Rangecc(scheme, "gopher") ||
323 equalCase_Rangecc(scheme, "finger")) {
324 return prefs_App()->monospaceGopher;
325 }
326 return iFalse;
327}
328
329static void linkContentWasLaidOut_GmDocument_(iGmDocument *d, const iGmMediaInfo *mediaInfo, 386static void linkContentWasLaidOut_GmDocument_(iGmDocument *d, const iGmMediaInfo *mediaInfo,
330 uint16_t linkId) { 387 uint16_t linkId) {
331 iGmLink *link = at_PtrArray(&d->links, linkId - 1); 388 iGmLink *link = at_PtrArray(&d->links, linkId - 1);
@@ -355,7 +412,7 @@ static enum iGmDocumentTheme currentTheme_(void) {
355} 412}
356 413
357static void alignDecoration_GmRun_(iGmRun *run, iBool isCentered) { 414static void alignDecoration_GmRun_(iGmRun *run, iBool isCentered) {
358 const iRect visBounds = visualBounds_Text(run->textParams.font, run->text); 415 const iRect visBounds = visualBounds_Text(run->font, run->text);
359 const int visWidth = width_Rect(visBounds); 416 const int visWidth = width_Rect(visBounds);
360 int xAdjust = 0; 417 int xAdjust = 0;
361 if (!isCentered) { 418 if (!isCentered) {
@@ -398,7 +455,8 @@ struct Impl_RunTypesetter {
398 int rightMargin; 455 int rightMargin;
399 iBool isWordWrapped; 456 iBool isWordWrapped;
400 iBool isPreformat; 457 iBool isPreformat;
401 const int *fonts; 458 int baseFont;
459 int baseColor;
402}; 460};
403 461
404static void init_RunTypesetter_(iRunTypesetter *d) { 462static void init_RunTypesetter_(iRunTypesetter *d) {
@@ -421,39 +479,53 @@ static void commit_RunTypesetter_(iRunTypesetter *d, iGmDocument *doc) {
421 479
422static const int maxLedeLines_ = 10; 480static const int maxLedeLines_ = 10;
423 481
424static const int colors[max_GmLineType] = { 482static void applyAttributes_RunTypesetter_(iRunTypesetter *d, iTextAttrib attrib) {
425 tmParagraph_ColorId, 483 /* WARNING: This is duplicated in run_Font_(). Make sure they behave identically. */
426 tmParagraph_ColorId, 484 if (attrib.bold) {
427 tmPreformatted_ColorId, 485 d->run.font = fontWithStyle_Text(d->baseFont, bold_FontStyle);
428 tmQuote_ColorId, 486 d->run.color = tmFirstParagraph_ColorId;
429 tmHeading1_ColorId, 487 }
430 tmHeading2_ColorId, 488 else if (attrib.italic) {
431 tmHeading3_ColorId, 489 d->run.font = fontWithStyle_Text(d->baseFont, italic_FontStyle);
432 tmLinkText_ColorId, 490 }
433}; 491 else if (attrib.monospace) {
492 d->run.font = fontWithFamily_Text(d->baseFont, monospace_FontId);
493 d->run.color = tmPreformatted_ColorId;
494 }
495 else {
496 d->run.font = d->baseFont;
497 d->run.color = d->baseColor;
498 }
499}
434 500
435static iBool typesetOneLine_RunTypesetter_(iWrapText *wrap, iRangecc wrapRange, int origin, 501static iBool typesetOneLine_RunTypesetter_(iWrapText *wrap, iRangecc wrapRange, iTextAttrib attrib,
436 int advance, iBool isBaseRTL) { 502 int origin, int advance) {
437 iAssert(wrapRange.start <= wrapRange.end); 503 iAssert(wrapRange.start <= wrapRange.end);
438 trimEnd_Rangecc(&wrapRange); 504 trimEnd_Rangecc(&wrapRange);
439// printf("typeset: {%s}\n", cstr_Rangecc(wrapRange));
440 iRunTypesetter *d = wrap->context; 505 iRunTypesetter *d = wrap->context;
441 const int fontId = d->run.textParams.font;
442 d->run.text = wrapRange; 506 d->run.text = wrapRange;
507 applyAttributes_RunTypesetter_(d, attrib);
508#if 0
509 const int msr = measureRange_Text(d->run.font, wrapRange).advance.x;
510 if (iAbs(msr - advance) > 3) {
511 printf("\n[RunTypesetter] wrong wrapRange advance! actual:%d wrapped:%d\n\n", msr, advance);
512 }
513#endif
443 if (~d->run.flags & startOfLine_GmRunFlag && d->lineHeightReduction > 0.0f) { 514 if (~d->run.flags & startOfLine_GmRunFlag && d->lineHeightReduction > 0.0f) {
444 d->pos.y -= d->lineHeightReduction * lineHeight_Text(fontId); 515 d->pos.y -= d->lineHeightReduction * lineHeight_Text(d->baseFont);
445 } 516 }
446 d->run.bounds.pos = addX_I2(d->pos, origin + d->indent); 517 d->run.bounds.pos = addX_I2(d->pos, origin + d->indent);
447 const iInt2 dims = init_I2(advance, lineHeight_Text(fontId)); 518 const iInt2 dims = init_I2(advance, lineHeight_Text(d->baseFont));
448 iChangeFlags(d->run.flags, wide_GmRunFlag, (d->isPreformat && dims.x > d->layoutWidth)); 519 iChangeFlags(d->run.flags, wide_GmRunFlag, (d->isPreformat && dims.x > d->layoutWidth));
449 d->run.bounds.size.x = iMax(wrap->maxWidth, dims.x) - origin; /* Extends to the right edge for selection. */ 520 d->run.bounds.size.x = iMax(wrap->maxWidth, dims.x) - origin; /* Extends to the right edge for selection. */
450 d->run.bounds.size.y = dims.y; 521 d->run.bounds.size.y = dims.y;
451 d->run.visBounds = d->run.bounds; 522 d->run.visBounds = d->run.bounds;
452 d->run.visBounds.size.x = dims.x; 523 d->run.visBounds.size.x = dims.x;
453 d->run.textParams.isRTL = isBaseRTL; 524 d->run.isRTL = attrib.isBaseRTL;
525// printf("origin:%d isRTL:%d\n{%s}\n", origin, attrib.isBaseRTL, cstr_Rangecc(wrapRange));
454 pushBack_Array(&d->layout, &d->run); 526 pushBack_Array(&d->layout, &d->run);
455 d->run.flags &= ~startOfLine_GmRunFlag; 527 d->run.flags &= ~startOfLine_GmRunFlag;
456 d->pos.y += lineHeight_Text(fontId) * prefs_App()->lineSpacing; 528 d->pos.y += lineHeight_Text(d->baseFont) * prefs_App()->lineSpacing;
457 return iTrue; /* continue to next wrapped line */ 529 return iTrue; /* continue to next wrapped line */
458} 530}
459 531
@@ -465,25 +537,11 @@ static void doLayout_GmDocument_(iGmDocument *d) {
465 const iBool isVeryNarrow = d->size.x <= 70 * gap_Text; 537 const iBool isVeryNarrow = d->size.x <= 70 * gap_Text;
466 const iBool isExtremelyNarrow = d->size.x <= 60 * gap_Text; 538 const iBool isExtremelyNarrow = d->size.x <= 60 * gap_Text;
467 const iBool isFullWidthImages = (d->outsideMargin < 5 * gap_UI); 539 const iBool isFullWidthImages = (d->outsideMargin < 5 * gap_UI);
468 const iBool isDarkBg = isDark_GmDocumentTheme( 540// const iBool isDarkBg = isDark_GmDocumentTheme(
469 isDark_ColorTheme(colorTheme_App()) ? prefs->docThemeDark : prefs->docThemeLight); 541// isDark_ColorTheme(colorTheme_App()) ? prefs->docThemeDark : prefs->docThemeLight);
542 initTheme_GmDocument_(d);
470 /* TODO: Collect these parameters into a GmTheme. */ 543 /* TODO: Collect these parameters into a GmTheme. */
471 const int fonts[max_GmLineType] = { 544 float indents[max_GmLineType] = { 5, 10, 5, isNarrow ? 5 : 10, 0, 0, 0, 5 };
472 isMono ? regularMonospace_FontId : paragraph_FontId,
473 isMono ? regularMonospace_FontId : paragraph_FontId, /* bullet */
474 preformatted_FontId,
475 isMono ? regularMonospace_FontId : quote_FontId,
476 heading1_FontId,
477 heading2_FontId,
478 heading3_FontId,
479 isMono ? regularMonospace_FontId
480 : ((isDarkBg && prefs->boldLinkDark) || (!isDarkBg && prefs->boldLinkLight))
481 ? bold_FontId
482 : paragraph_FontId,
483 };
484 float indents[max_GmLineType] = {
485 5, 10, 5, isNarrow ? 5 : 10, 0, 0, 0, 5
486 };
487 if (isExtremelyNarrow) { 545 if (isExtremelyNarrow) {
488 /* Further reduce the margins. */ 546 /* Further reduce the margins. */
489 indents[text_GmLineType] -= 5; 547 indents[text_GmLineType] -= 5;
@@ -502,7 +560,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
502 static const char *arrow = rightArrowhead_Icon; 560 static const char *arrow = rightArrowhead_Icon;
503 static const char *envelope = envelope_Icon; 561 static const char *envelope = envelope_Icon;
504 static const char *bullet = "\u2022"; 562 static const char *bullet = "\u2022";
505 static const char *folder = "\U0001f4c1"; 563 static const char *folder = file_Icon;
506 static const char *globe = globe_Icon; 564 static const char *globe = globe_Icon;
507 static const char *quote = "\u201c"; 565 static const char *quote = "\u201c";
508 static const char *magnifyingGlass = "\U0001f50d"; 566 static const char *magnifyingGlass = "\U0001f50d";
@@ -538,12 +596,13 @@ static void doLayout_GmDocument_(iGmDocument *d) {
538 isPreformat = iTrue; 596 isPreformat = iTrue;
539 isFirstText = iFalse; 597 isFirstText = iFalse;
540 } 598 }
599 setAnsiFlags_Text(d->theme.ansiEscapes);
541 while (nextSplit_Rangecc(content, "\n", &contentLine)) { 600 while (nextSplit_Rangecc(content, "\n", &contentLine)) {
542 iRangecc line = contentLine; /* `line` will be trimmed; modifying would confuse `nextSplit_Rangecc` */ 601 iRangecc line = contentLine; /* `line` will be trimmed; modifying would confuse `nextSplit_Rangecc` */
543 if (*line.end == '\r') { 602 if (*line.end == '\r') {
544 line.end--; /* trim CR always */ 603 line.end--; /* trim CR always */
545 } 604 }
546 iGmRun run = { .textParams = { .color = white_ColorId } }; 605 iGmRun run = { .color = white_ColorId };
547 enum iGmLineType type; 606 enum iGmLineType type;
548 float indent = 0.0f; 607 float indent = 0.0f;
549 /* Detect the type of the line. */ 608 /* Detect the type of the line. */
@@ -562,10 +621,13 @@ static void doLayout_GmDocument_(iGmDocument *d) {
562 iGmPreMeta meta = { .bounds = line }; 621 iGmPreMeta meta = { .bounds = line };
563 meta.pixelRect.size = measurePreformattedBlock_GmDocument_( 622 meta.pixelRect.size = measurePreformattedBlock_GmDocument_(
564 d, line.start, preFont, &meta.contents, &meta.bounds.end); 623 d, line.start, preFont, &meta.contents, &meta.bounds.end);
565 if (meta.pixelRect.size.x > 624 const float oversizeRatio =
566 d->size.x - (enableIndents ? indents[preformatted_GmLineType] : 0) * gap_Text) { 625 meta.pixelRect.size.x /
567 preFont = preformattedSmall_FontId; 626 (float) (d->size.x -
568 meta.pixelRect.size = measureRange_Text(preFont, meta.contents).bounds.size; 627 (enableIndents ? indents[preformatted_GmLineType] : 0) * gap_Text);
628 if (oversizeRatio > 1.0f) {
629 preFont--; /* one notch smaller in the font size */
630 meta.pixelRect.size = measureRange_Text(preFont, meta.contents).bounds.size;
569 } 631 }
570 trimLine_Rangecc(&line, type, isNormalized); 632 trimLine_Rangecc(&line, type, isNormalized);
571 meta.altText = line; /* without the ``` */ 633 meta.altText = line; /* without the ``` */
@@ -581,14 +643,16 @@ static void doLayout_GmDocument_(iGmDocument *d) {
581 continue; 643 continue;
582 } 644 }
583 else if (type == link_GmLineType) { 645 else if (type == link_GmLineType) {
584 line = addLink_GmDocument_(d, line, &run.linkId); 646 iGmLinkId linkId;
647 line = addLink_GmDocument_(d, line, &linkId);
648 run.linkId = linkId;
585 if (!run.linkId) { 649 if (!run.linkId) {
586 /* Invalid formatting. */ 650 /* Invalid formatting. */
587 type = text_GmLineType; 651 type = text_GmLineType;
588 } 652 }
589 } 653 }
590 trimLine_Rangecc(&line, type, isNormalized); 654 trimLine_Rangecc(&line, type, isNormalized);
591 run.textParams.font = fonts[type]; 655 run.font = d->theme.fonts[type];
592 /* Remember headings for the document outline. */ 656 /* Remember headings for the document outline. */
593 if (type == heading1_GmLineType || type == heading2_GmLineType || type == heading3_GmLineType) { 657 if (type == heading1_GmLineType || type == heading2_GmLineType || type == heading3_GmLineType) {
594 pushBack_Array( 658 pushBack_Array(
@@ -608,8 +672,9 @@ static void doLayout_GmDocument_(iGmDocument *d) {
608 addSiteBanner = iFalse; /* overrides the banner */ 672 addSiteBanner = iFalse; /* overrides the banner */
609 continue; 673 continue;
610 } 674 }
611 run.preId = preId; 675 run.mediaType = max_MediaType; /* preformatted block */
612 run.textParams.font = (d->format == plainText_SourceFormat ? regularMonospace_FontId : preFont); 676 run.mediaId = preId;
677 run.font = (d->format == plainText_SourceFormat ? plainText_FontId : preFont);
613 indent = indents[type]; 678 indent = indents[type];
614 } 679 }
615 if (addSiteBanner) { 680 if (addSiteBanner) {
@@ -624,9 +689,9 @@ static void doLayout_GmDocument_(iGmDocument *d) {
624 banner.visBounds.size.y += iMaxi(6000 * lineHeight_Text(uiLabel_FontId) / 689 banner.visBounds.size.y += iMaxi(6000 * lineHeight_Text(uiLabel_FontId) /
625 d->size.x, lineHeight_Text(uiLabel_FontId) * 5); 690 d->size.x, lineHeight_Text(uiLabel_FontId) * 5);
626 } 691 }
627 banner.text = bannerText; 692 banner.text = bannerText;
628 banner.textParams.font = banner_FontId; 693 banner.font = banner_FontId;
629 banner.textParams.color = tmBannerTitle_ColorId; 694 banner.color = tmBannerTitle_ColorId;
630 pushBack_Array(&d->layout, &banner); 695 pushBack_Array(&d->layout, &banner);
631 pos.y += height_Rect(banner.visBounds) + 696 pos.y += height_Rect(banner.visBounds) +
632 1.5f * lineHeight_Text(paragraph_FontId) * prefs->lineSpacing; 697 1.5f * lineHeight_Text(paragraph_FontId) * prefs->lineSpacing;
@@ -637,13 +702,13 @@ static void doLayout_GmDocument_(iGmDocument *d) {
637 if (type == quote_GmLineType && !prefs->quoteIcon) { 702 if (type == quote_GmLineType && !prefs->quoteIcon) {
638 /* For quote indicators we still need to produce a run. */ 703 /* For quote indicators we still need to produce a run. */
639 run.visBounds.pos = addX_I2(pos, indents[type] * gap_Text); 704 run.visBounds.pos = addX_I2(pos, indents[type] * gap_Text);
640 run.visBounds.size = init_I2(gap_Text, lineHeight_Text(run.textParams.font)); 705 run.visBounds.size = init_I2(gap_Text, lineHeight_Text(run.font));
641 run.bounds = zero_Rect(); /* just visual */ 706 run.bounds = zero_Rect(); /* just visual */
642 run.flags = quoteBorder_GmRunFlag | decoration_GmRunFlag;
643 run.text = iNullRange; 707 run.text = iNullRange;
708 run.flags = quoteBorder_GmRunFlag | decoration_GmRunFlag;
644 pushBack_Array(&d->layout, &run); 709 pushBack_Array(&d->layout, &run);
645 } 710 }
646 pos.y += lineHeight_Text(run.textParams.font) * prefs->lineSpacing; 711 pos.y += lineHeight_Text(run.font) * prefs->lineSpacing;
647 prevType = type; 712 prevType = type;
648 if (type != quote_GmLineType) { 713 if (type != quote_GmLineType) {
649 addQuoteIcon = prefs->quoteIcon; 714 addQuoteIcon = prefs->quoteIcon;
@@ -687,18 +752,19 @@ static void doLayout_GmDocument_(iGmDocument *d) {
687 const iGmPreMeta *meta = constAt_Array(&d->preMeta, preId - 1); 752 const iGmPreMeta *meta = constAt_Array(&d->preMeta, preId - 1);
688 if (meta->flags & folded_GmPreMetaFlag) { 753 if (meta->flags & folded_GmPreMetaFlag) {
689 const iBool isBlank = isEmpty_Range(&meta->altText); 754 const iBool isBlank = isEmpty_Range(&meta->altText);
690 iGmRun altText = { 755 iGmRun altText = { .font = paragraph_FontId,
691 .textParams = { .font = paragraph_FontId, .color = tmQuote_ColorId }, 756 .color = tmQuote_ColorId,
692 .flags = (isBlank ? decoration_GmRunFlag : 0) | altText_GmRunFlag 757 .flags = (isBlank ? decoration_GmRunFlag : 0) | altText_GmRunFlag
693 }; 758 };
694 const iInt2 margin = preRunMargin_GmDocument(d, 0); 759 const iInt2 margin = preRunMargin_GmDocument(d, 0);
695 altText.text = isBlank ? range_Lang(range_CStr("doc.pre.nocaption")) 760 altText.text = isBlank ? range_Lang(range_CStr("doc.pre.nocaption"))
696 : meta->altText; 761 : meta->altText;
697 iInt2 size = measureWrapRange_Text(altText.textParams.font, d->size.x - 2 * margin.x, 762 iInt2 size = measureWrapRange_Text(altText.font, d->size.x - 2 * margin.x,
698 altText.text).bounds.size; 763 altText.text).bounds.size;
699 altText.bounds = altText.visBounds = init_Rect(pos.x, pos.y, d->size.x, 764 altText.bounds = altText.visBounds = init_Rect(pos.x, pos.y, d->size.x,
700 size.y + 2 * margin.y); 765 size.y + 2 * margin.y);
701 altText.preId = preId; 766 altText.mediaType = max_MediaType; /* preformatted */
767 altText.mediaId = preId;
702 pushBack_Array(&d->layout, &altText); 768 pushBack_Array(&d->layout, &altText);
703 pos.y += height_Rect(altText.bounds); 769 pos.y += height_Rect(altText.bounds);
704 contentLine = meta->bounds; /* Skip the whole thing. */ 770 contentLine = meta->bounds; /* Skip the whole thing. */
@@ -713,19 +779,21 @@ static void doLayout_GmDocument_(iGmDocument *d) {
713 setRange_String(&d->title, line); 779 setRange_String(&d->title, line);
714 } 780 }
715 /* List bullet. */ 781 /* List bullet. */
716 run.textParams.color = colors[type];
717 if (type == bullet_GmLineType) { 782 if (type == bullet_GmLineType) {
718 /* TODO: Literata bullet is broken? */ 783 /* TODO: Literata bullet is broken? */
719 iGmRun bulRun = run; 784 iGmRun bulRun = run;
785#if 0
720 if (prefs->font == literata_TextFont) { 786 if (prefs->font == literata_TextFont) {
721 /* Something wrong this the glyph in Literata, looks cropped. */ 787 /* Something wrong this the glyph in Literata, looks cropped. */
722 bulRun.textParams.font = defaultContentRegular_FontId; 788 bulRun.font = FONT_ID(default_FontId, regular_FontStyle,
789 contentRegular_FontSize);
723 } 790 }
724 bulRun.textParams.color = tmQuote_ColorId; 791#endif
792 bulRun.color = tmQuote_ColorId;
725 bulRun.visBounds.pos = addX_I2(pos, (indents[text_GmLineType] - 0.55f) * gap_Text); 793 bulRun.visBounds.pos = addX_I2(pos, (indents[text_GmLineType] - 0.55f) * gap_Text);
726 bulRun.visBounds.size = 794 bulRun.visBounds.size =
727 init_I2((indents[bullet_GmLineType] - indents[text_GmLineType]) * gap_Text, 795 init_I2((indents[bullet_GmLineType] - indents[text_GmLineType]) * gap_Text,
728 lineHeight_Text(bulRun.textParams.font)); 796 lineHeight_Text(bulRun.font));
729 // bulRun.visBounds.pos.x -= 4 * gap_Text - width_Rect(bulRun.visBounds) / 2; 797 // bulRun.visBounds.pos.x -= 4 * gap_Text - width_Rect(bulRun.visBounds) / 2;
730 bulRun.bounds = zero_Rect(); /* just visual */ 798 bulRun.bounds = zero_Rect(); /* just visual */
731 bulRun.text = range_CStr(bullet); 799 bulRun.text = range_CStr(bullet);
@@ -737,11 +805,11 @@ static void doLayout_GmDocument_(iGmDocument *d) {
737 if (type == quote_GmLineType && addQuoteIcon) { 805 if (type == quote_GmLineType && addQuoteIcon) {
738 addQuoteIcon = iFalse; 806 addQuoteIcon = iFalse;
739 iGmRun quoteRun = run; 807 iGmRun quoteRun = run;
740 quoteRun.textParams.font = heading1_FontId; 808 quoteRun.font = heading1_FontId;
741 quoteRun.text = range_CStr(quote); 809 quoteRun.text = range_CStr(quote);
742 quoteRun.textParams.color = tmQuoteIcon_ColorId; 810 quoteRun.color = tmQuoteIcon_ColorId;
743 iRect vis = visualBounds_Text(quoteRun.textParams.font, quoteRun.text); 811 iRect vis = visualBounds_Text(quoteRun.font, quoteRun.text);
744 quoteRun.visBounds.size = measure_Text(quoteRun.textParams.font, quote).bounds.size; 812 quoteRun.visBounds.size = measure_Text(quoteRun.font, quote).bounds.size;
745 quoteRun.visBounds.pos = 813 quoteRun.visBounds.pos =
746 add_I2(pos, 814 add_I2(pos,
747 init_I2((indents[quote_GmLineType] - 5) * gap_Text, 815 init_I2((indents[quote_GmLineType] - 5) * gap_Text,
@@ -757,17 +825,18 @@ static void doLayout_GmDocument_(iGmDocument *d) {
757 if (type == link_GmLineType) { 825 if (type == link_GmLineType) {
758 iGmRun icon = run; 826 iGmRun icon = run;
759 icon.visBounds.pos = pos; 827 icon.visBounds.pos = pos;
760 icon.visBounds.size = init_I2(indent * gap_Text, lineHeight_Text(run.textParams.font)); 828 icon.visBounds.size = init_I2(indent * gap_Text, lineHeight_Text(run.font));
761 icon.bounds = zero_Rect(); /* just visual */ 829 icon.bounds = zero_Rect(); /* just visual */
762 const iGmLink *link = constAt_PtrArray(&d->links, run.linkId - 1); 830 const iGmLink *link = constAt_PtrArray(&d->links, run.linkId - 1);
763 const enum iGmLinkScheme scheme = scheme_GmLinkFlag(link->flags); 831 const enum iGmLinkScheme scheme = scheme_GmLinkFlag(link->flags);
764 icon.text = range_CStr(link->flags & query_GmLinkFlag ? magnifyingGlass 832 icon.text = range_CStr(link->flags & query_GmLinkFlag ? magnifyingGlass
765 : scheme == file_GmLinkScheme ? folder
766 : scheme == titan_GmLinkScheme ? uploadArrow 833 : scheme == titan_GmLinkScheme ? uploadArrow
767 : scheme == finger_GmLinkScheme ? pointingFinger 834 : scheme == finger_GmLinkScheme ? pointingFinger
768 : scheme == mailto_GmLinkScheme ? envelope 835 : scheme == mailto_GmLinkScheme ? envelope
769 : link->flags & remote_GmLinkFlag ? globe 836 : link->flags & remote_GmLinkFlag ? globe
770 : link->flags & imageFileExtension_GmLinkFlag ? image 837 : link->flags & imageFileExtension_GmLinkFlag ? image
838 : link->flags & fontpackFileExtension_GmLinkFlag ? fontpack_Icon
839 : scheme == file_GmLinkScheme ? folder
771 : arrow); 840 : arrow);
772 /* Custom link icon is shown on local Gemini links only. */ 841 /* Custom link icon is shown on local Gemini links only. */
773 if (!isEmpty_Range(&link->labelIcon)) { 842 if (!isEmpty_Range(&link->labelIcon)) {
@@ -775,25 +844,24 @@ static void doLayout_GmDocument_(iGmDocument *d) {
775 } 844 }
776 /* TODO: List bullets needs the same centering logic. */ 845 /* TODO: List bullets needs the same centering logic. */
777 /* Special exception for the tiny bullet operator. */ 846 /* Special exception for the tiny bullet operator. */
778 icon.textParams.font = equal_Rangecc(link->labelIcon, "\u2219") ? regularMonospace_FontId 847 icon.font = equal_Rangecc(link->labelIcon, "\u2219") ? preformatted_FontId
779 : regular_FontId; 848 : paragraph_FontId;
780 alignDecoration_GmRun_(&icon, iFalse); 849 alignDecoration_GmRun_(&icon, iFalse);
781 icon.textParams.color = linkColor_GmDocument(d, run.linkId, icon_GmLinkPart); 850 icon.color = linkColor_GmDocument(d, run.linkId, icon_GmLinkPart);
782 icon.flags |= decoration_GmRunFlag; 851 icon.flags |= decoration_GmRunFlag;
783 pushBack_Array(&d->layout, &icon); 852 pushBack_Array(&d->layout, &icon);
784 } 853 }
785 run.textParams.color = colors[type]; 854 run.lineType = type;
855 run.color = d->theme.colors[type];
786 if (d->format == plainText_SourceFormat) { 856 if (d->format == plainText_SourceFormat) {
787 run.textParams.color = colors[text_GmLineType]; 857 run.color = d->theme.colors[text_GmLineType];
788 } 858 }
789 /* Special formatting for the first paragraph (e.g., subtitle, introduction, or lede). */ 859 /* Special formatting for the first paragraph (e.g., subtitle, introduction, or lede). */
790// int bigCount = 0; 860// int bigCount = 0;
791 iBool isLedeParagraph = iFalse;
792 if (type == text_GmLineType && isFirstText) { 861 if (type == text_GmLineType && isFirstText) {
793 if (!isMono) run.textParams.font = firstParagraph_FontId; 862 if (!isMono) run.font = firstParagraph_FontId;
794 run.textParams.color = tmFirstParagraph_ColorId; 863 run.color = tmFirstParagraph_ColorId;
795// bigCount = 15; /* max lines -- what if the whole document is one paragraph? */ 864 run.isLede = iTrue;
796 isLedeParagraph = iTrue;
797 isFirstText = iFalse; 865 isFirstText = iFalse;
798 } 866 }
799 else if (type != heading1_GmLineType) { 867 else if (type != heading1_GmLineType) {
@@ -813,7 +881,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
813 init_RunTypesetter_(&rts); 881 init_RunTypesetter_(&rts);
814 rts.run = run; 882 rts.run = run;
815 rts.pos = pos; 883 rts.pos = pos;
816 rts.fonts = fonts; 884 //rts.fonts = fonts;
817 rts.isWordWrapped = (d->format == plainText_SourceFormat ? prefs->plainTextWrap 885 rts.isWordWrapped = (d->format == plainText_SourceFormat ? prefs->plainTextWrap
818 : !isPreformat); 886 : !isPreformat);
819 rts.isPreformat = isPreformat; 887 rts.isPreformat = isPreformat;
@@ -837,8 +905,9 @@ static void doLayout_GmDocument_(iGmDocument *d) {
837 rts.lineHeightReduction = 0.06f; 905 rts.lineHeightReduction = 0.06f;
838 } 906 }
839 /* Visited links are never bold. */ 907 /* Visited links are never bold. */
840 if (run.linkId && linkFlags_GmDocument(d, run.linkId) & visited_GmLinkFlag) { 908 if (run.linkId && !prefs->boldLinkVisited &&
841 rts.run.textParams.font = paragraph_FontId; 909 linkFlags_GmDocument(d, run.linkId) & visited_GmLinkFlag) {
910 rts.run.font = paragraph_FontId;
842 } 911 }
843 } 912 }
844 if (!prefs->quoteIcon && type == quote_GmLineType) { 913 if (!prefs->quoteIcon && type == quote_GmLineType) {
@@ -846,6 +915,8 @@ static void doLayout_GmDocument_(iGmDocument *d) {
846 } 915 }
847 for (;;) { /* need to retry if the font needs changing */ 916 for (;;) { /* need to retry if the font needs changing */
848 rts.run.flags |= startOfLine_GmRunFlag; 917 rts.run.flags |= startOfLine_GmRunFlag;
918 rts.baseFont = rts.run.font;
919 rts.baseColor = rts.run.color;
849 iWrapText wrapText = { .text = line, 920 iWrapText wrapText = { .text = line,
850 .maxWidth = rts.isWordWrapped 921 .maxWidth = rts.isWordWrapped
851 ? d->size.x - run.bounds.pos.x - 922 ? d->size.x - run.bounds.pos.x -
@@ -854,8 +925,8 @@ static void doLayout_GmDocument_(iGmDocument *d) {
854 .mode = word_WrapTextMode, 925 .mode = word_WrapTextMode,
855 .wrapFunc = typesetOneLine_RunTypesetter_, 926 .wrapFunc = typesetOneLine_RunTypesetter_,
856 .context = &rts }; 927 .context = &rts };
857 measure_WrapText(&wrapText, rts.run.textParams.font); 928 measure_WrapText(&wrapText, rts.run.font);
858 if (!isLedeParagraph || size_Array(&rts.layout) <= maxLedeLines_) { 929 if (!rts.run.isLede || size_Array(&rts.layout) <= maxLedeLines_) {
859 if (wrapText.baseDir < 0) { 930 if (wrapText.baseDir < 0) {
860 /* Right-aligned paragraphs need margins and decorations to be flipped. */ 931 /* Right-aligned paragraphs need margins and decorations to be flipped. */
861 iForEach(Array, pr, &rts.layout) { 932 iForEach(Array, pr, &rts.layout) {
@@ -878,11 +949,12 @@ static void doLayout_GmDocument_(iGmDocument *d) {
878 commit_RunTypesetter_(&rts, d); 949 commit_RunTypesetter_(&rts, d);
879 break; 950 break;
880 } 951 }
952 /* Try again... */
881 clear_RunTypesetter_(&rts); 953 clear_RunTypesetter_(&rts);
882 rts.pos = pos; 954 rts.pos = pos;
883 rts.run.textParams.font = rts.fonts[text_GmLineType]; 955 rts.run.font = rts.baseFont = d->theme.fonts[text_GmLineType];
884 rts.run.textParams.color = colors[text_GmLineType]; 956 rts.run.color = rts.baseColor = d->theme.colors[text_GmLineType];
885 isLedeParagraph = iFalse; 957 rts.run.isLede = iFalse;
886 } 958 }
887 pos = rts.pos; 959 pos = rts.pos;
888 deinit_RunTypesetter_(&rts); 960 deinit_RunTypesetter_(&rts);
@@ -891,77 +963,69 @@ static void doLayout_GmDocument_(iGmDocument *d) {
891 ((iGmRun *) back_Array(&d->layout))->flags |= endOfLine_GmRunFlag; 963 ((iGmRun *) back_Array(&d->layout))->flags |= endOfLine_GmRunFlag;
892 /* Image or audio content. */ 964 /* Image or audio content. */
893 if (type == link_GmLineType) { 965 if (type == link_GmLineType) {
894 const iMediaId imageId = findLinkImage_Media(d->media, run.linkId); 966 /* TODO: Cleanup here? Move to a function of its own. */
895 const iMediaId audioId = !imageId ? findLinkAudio_Media(d->media, run.linkId) : 0; 967// enum iMediaType mediaType = none_MediaType;
896 const iMediaId downloadId = !imageId && !audioId ? findLinkDownload_Media(d->media, run.linkId) : 0; 968 const iMediaId media = findMediaForLink_Media(d->media, run.linkId, none_MediaType);
897 if (imageId) { 969 iGmMediaInfo info;
898 iGmMediaInfo img; 970 info_Media(d->media, media, &info);
899 imageInfo_Media(d->media, imageId, &img); 971 run.mediaType = media.type;
900 const iInt2 imgSize = imageSize_Media(d->media, imageId); 972 run.mediaId = media.id;
901 linkContentWasLaidOut_GmDocument_(d, &img, run.linkId); 973 run.text = iNullRange;
902 const int margin = lineHeight_Text(paragraph_FontId) / 2; 974 run.font = uiLabel_FontId;
975 run.color = 0;
976 const int margin = lineHeight_Text(paragraph_FontId) / 2;
977 if (media.type) {
903 pos.y += margin; 978 pos.y += margin;
904 run.bounds.pos = pos; 979 run.bounds.size.y = 0;
905 run.bounds.size.x = d->size.x;
906 const float aspect = (float) imgSize.y / (float) imgSize.x;
907 run.bounds.size.y = d->size.x * aspect;
908 /* Extend the image to full width, including outside margin, if the viewport
909 is narrow enough. */
910 if (isFullWidthImages) {
911 run.bounds.size.x += d->outsideMargin * 2;
912 run.bounds.size.y += d->outsideMargin * 2 * aspect;
913 run.bounds.pos.x -= d->outsideMargin;
914 }
915 run.visBounds = run.bounds;
916 const iInt2 maxSize = mulf_I2(imgSize, get_Window()->pixelRatio);
917 if (width_Rect(run.visBounds) > maxSize.x) {
918 /* Don't scale the image up. */
919 run.visBounds.size.y =
920 run.visBounds.size.y * maxSize.x / width_Rect(run.visBounds);
921 run.visBounds.size.x = maxSize.x;
922 run.visBounds.pos.x = run.bounds.size.x / 2 - width_Rect(run.visBounds) / 2;
923 run.bounds.size.y = run.visBounds.size.y;
924 }
925 run.text = iNullRange;
926 run.textParams.font = 0;
927 run.textParams.color = 0;
928 run.mediaType = image_GmRunMediaType;
929 run.mediaId = imageId;
930 pushBack_Array(&d->layout, &run);
931 pos.y += run.bounds.size.y + margin;
932 }
933 else if (audioId) {
934 iGmMediaInfo info;
935 audioInfo_Media(d->media, audioId, &info);
936 linkContentWasLaidOut_GmDocument_(d, &info, run.linkId); 980 linkContentWasLaidOut_GmDocument_(d, &info, run.linkId);
937 const int margin = lineHeight_Text(paragraph_FontId) / 2;
938 pos.y += margin;
939 run.bounds.pos = pos;
940 run.bounds.size.x = d->size.x;
941 run.bounds.size.y = lineHeight_Text(uiContent_FontId) + 3 * gap_UI;
942 run.visBounds = run.bounds;
943 run.text = iNullRange;
944 run.textParams.color = 0;
945 run.mediaType = audio_GmRunMediaType;
946 run.mediaId = audioId;
947 pushBack_Array(&d->layout, &run);
948 pos.y += run.bounds.size.y + margin;
949 } 981 }
950 else if (downloadId) { 982 switch (media.type) {
951 iGmMediaInfo info; 983 case image_MediaType: {
952 downloadInfo_Media(d->media, downloadId, &info); 984 const iInt2 imgSize = imageSize_Media(d->media, media);
953 linkContentWasLaidOut_GmDocument_(d, &info, run.linkId); 985 run.bounds.pos = pos;
954 const int margin = lineHeight_Text(paragraph_FontId) / 2; 986 run.bounds.size.x = d->size.x;
955 pos.y += margin; 987 const float aspect = (float) imgSize.y / (float) imgSize.x;
956 run.bounds.pos = pos; 988 run.bounds.size.y = d->size.x * aspect;
957 run.bounds.size.x = d->size.x; 989 /* Extend the image to full width, including outside margin, if the viewport
958 run.bounds.size.y = 2 * lineHeight_Text(uiContent_FontId) + 4 * gap_UI; 990 is narrow enough. */
959 run.visBounds = run.bounds; 991 if (isFullWidthImages) {
960 run.text = iNullRange; 992 run.bounds.size.x += d->outsideMargin * 2;
961 run.textParams.color = 0; 993 run.bounds.size.y += d->outsideMargin * 2 * aspect;
962 run.mediaType = download_GmRunMediaType; 994 run.bounds.pos.x -= d->outsideMargin;
963 run.mediaId = downloadId; 995 }
964 pushBack_Array(&d->layout, &run); 996 run.visBounds = run.bounds;
997 const iInt2 maxSize = mulf_I2(imgSize, get_Window()->pixelRatio);
998 if (width_Rect(run.visBounds) > maxSize.x) {
999 /* Don't scale the image up. */
1000 run.visBounds.size.y =
1001 run.visBounds.size.y * maxSize.x / width_Rect(run.visBounds);
1002 run.visBounds.size.x = maxSize.x;
1003 run.visBounds.pos.x = run.bounds.size.x / 2 - width_Rect(run.visBounds) / 2;
1004 run.bounds.size.y = run.visBounds.size.y;
1005 }
1006 pushBack_Array(&d->layout, &run);
1007 break;
1008 }
1009 case audio_MediaType: {
1010 run.bounds.pos = pos;
1011 run.bounds.size.x = d->size.x;
1012 run.bounds.size.y = lineHeight_Text(uiContent_FontId) + 3 * gap_UI;
1013 run.visBounds = run.bounds;
1014 pushBack_Array(&d->layout, &run);
1015 break;
1016 }
1017 case download_MediaType: {
1018 run.bounds.pos = pos;
1019 run.bounds.size.x = d->size.x;
1020 run.bounds.size.y = 2 * lineHeight_Text(uiContent_FontId) + 4 * gap_UI;
1021 run.visBounds = run.bounds;
1022 pushBack_Array(&d->layout, &run);
1023 break;
1024 }
1025 default:
1026 break;
1027 }
1028 if (media.type && run.bounds.size.y) {
965 pos.y += run.bounds.size.y + margin; 1029 pos.y += run.bounds.size.y + margin;
966 } 1030 }
967 } 1031 }
@@ -983,7 +1047,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
983 /* TODO: Store the dimensions and ranges for later access. */ 1047 /* TODO: Store the dimensions and ranges for later access. */
984 iForEach(Array, i, &d->layout) { 1048 iForEach(Array, i, &d->layout) {
985 iGmRun *run = i.value; 1049 iGmRun *run = i.value;
986 if (run->preId && run->flags & wide_GmRunFlag) { 1050 if (preId_GmRun(run) && run->flags & wide_GmRunFlag) {
987 iGmRunRange block = findPreformattedRange_GmDocument(d, run); 1051 iGmRunRange block = findPreformattedRange_GmDocument(d, run);
988 for (const iGmRun *j = block.start; j != block.end; j++) { 1052 for (const iGmRun *j = block.start; j != block.end; j++) {
989 iConstCast(iGmRun *, j)->flags |= wide_GmRunFlag; 1053 iConstCast(iGmRun *, j)->flags |= wide_GmRunFlag;
@@ -993,6 +1057,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
993 } 1057 }
994 } 1058 }
995 } 1059 }
1060 setAnsiFlags_Text(allowAll_AnsiFlag);
996// printf("[GmDocument] layout size: %zu runs (%zu bytes)\n", 1061// printf("[GmDocument] layout size: %zu runs (%zu bytes)\n",
997// size_Array(&d->layout), size_Array(&d->layout) * sizeof(iGmRun)); 1062// size_Array(&d->layout), size_Array(&d->layout) * sizeof(iGmRun));
998} 1063}
@@ -1006,6 +1071,7 @@ void init_GmDocument(iGmDocument *d) {
1006 d->bannerType = siteDomain_GmDocumentBanner; 1071 d->bannerType = siteDomain_GmDocumentBanner;
1007 d->outsideMargin = 0; 1072 d->outsideMargin = 0;
1008 d->size = zero_I2(); 1073 d->size = zero_I2();
1074 d->enableCommandLinks = iFalse;
1009 init_Array(&d->layout, sizeof(iGmRun)); 1075 init_Array(&d->layout, sizeof(iGmRun));
1010 init_PtrArray(&d->links); 1076 init_PtrArray(&d->links);
1011 init_String(&d->bannerText); 1077 init_String(&d->bannerText);
@@ -1048,21 +1114,6 @@ const iString *url_GmDocument(const iGmDocument *d) {
1048 return &d->url; 1114 return &d->url;
1049} 1115}
1050 1116
1051#if 0
1052void reset_GmDocument(iGmDocument *d) {
1053 clear_Media(d->media);
1054 clearLinks_GmDocument_(d);
1055 clear_Array(&d->layout);
1056 clear_Array(&d->headings);
1057 clear_Array(&d->preMeta);
1058 clear_String(&d->url);
1059 clear_String(&d->localHost);
1060 clear_String(&d->source);
1061 clear_String(&d->unormSource);
1062 d->themeSeed = 0;
1063}
1064#endif
1065
1066static void setDerivedThemeColors_(enum iGmDocumentTheme theme) { 1117static void setDerivedThemeColors_(enum iGmDocumentTheme theme) {
1067 set_Color(tmQuoteIcon_ColorId, 1118 set_Color(tmQuoteIcon_ColorId,
1068 mix_Color(get_Color(tmQuote_ColorId), get_Color(tmBackground_ColorId), 0.55f)); 1119 mix_Color(get_Color(tmQuote_ColorId), get_Color(tmBackground_ColorId), 0.55f));
@@ -1234,21 +1285,41 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) {
1234 } 1285 }
1235 } 1286 }
1236 else if (theme == sepia_GmDocumentTheme) { 1287 else if (theme == sepia_GmDocumentTheme) {
1237 const iHSLColor base = { 40, 0.6f, 0.9f, 1.0f }; 1288 iHSLColor base = { 40, 0.6f, 0.9f, 1.0f };
1238 setHsl_Color(tmBackground_ColorId, base); 1289 if (0 && isDark_ColorTheme(colorTheme_App())) { /* TODO */
1239 set_Color(tmParagraph_ColorId, get_Color(black_ColorId)); 1290 base.lum = 0.15f;
1240 set_Color(tmFirstParagraph_ColorId, get_Color(black_ColorId)); 1291 base.sat = 0.15f;
1241 set_Color(tmQuote_ColorId, get_Color(brown_ColorId)); 1292 setHsl_Color(tmBackground_ColorId, base);
1242 set_Color(tmPreformatted_ColorId, get_Color(brown_ColorId)); 1293 set_Color(tmParagraph_ColorId, get_Color(gray75_ColorId));
1243 set_Color(tmHeading1_ColorId, get_Color(brown_ColorId)); 1294 set_Color(tmFirstParagraph_ColorId, get_Color(white_ColorId));
1244 set_Color(tmHeading2_ColorId, mix_Color(get_Color(brown_ColorId), get_Color(black_ColorId), 0.5f)); 1295 set_Color(tmQuote_ColorId, get_Color(brown_ColorId));
1245 set_Color(tmHeading3_ColorId, get_Color(black_ColorId)); 1296 set_Color(tmPreformatted_ColorId, get_Color(brown_ColorId));
1246 set_Color(tmBannerBackground_ColorId, mix_Color(get_Color(tmBackground_ColorId), get_Color(brown_ColorId), 0.15f)); 1297 set_Color(tmHeading1_ColorId, get_Color(brown_ColorId));
1247 set_Color(tmBannerTitle_ColorId, get_Color(brown_ColorId)); 1298 set_Color(tmHeading2_ColorId, mix_Color(get_Color(brown_ColorId), get_Color(black_ColorId), 0.5f));
1248 set_Color(tmBannerIcon_ColorId, get_Color(brown_ColorId)); 1299 set_Color(tmHeading3_ColorId, get_Color(black_ColorId));
1249 set_Color(tmLinkText_ColorId, get_Color(tmHeading2_ColorId)); 1300 set_Color(tmBannerBackground_ColorId, mix_Color(get_Color(tmBackground_ColorId), get_Color(brown_ColorId), 0.15f));
1250 set_Color(tmHypertextLinkText_ColorId, get_Color(tmHeading2_ColorId)); 1301 set_Color(tmBannerTitle_ColorId, get_Color(brown_ColorId));
1251 set_Color(tmGopherLinkText_ColorId, get_Color(tmHeading2_ColorId)); 1302 set_Color(tmBannerIcon_ColorId, get_Color(brown_ColorId));
1303 set_Color(tmLinkText_ColorId, get_Color(tmHeading2_ColorId));
1304 set_Color(tmHypertextLinkText_ColorId, get_Color(tmHeading2_ColorId));
1305 set_Color(tmGopherLinkText_ColorId, get_Color(tmHeading2_ColorId));
1306 }
1307 else {
1308 setHsl_Color(tmBackground_ColorId, base);
1309 set_Color(tmParagraph_ColorId, get_Color(black_ColorId));
1310 set_Color(tmFirstParagraph_ColorId, get_Color(black_ColorId));
1311 set_Color(tmQuote_ColorId, get_Color(brown_ColorId));
1312 set_Color(tmPreformatted_ColorId, get_Color(brown_ColorId));
1313 set_Color(tmHeading1_ColorId, get_Color(brown_ColorId));
1314 set_Color(tmHeading2_ColorId, mix_Color(get_Color(brown_ColorId), get_Color(black_ColorId), 0.5f));
1315 set_Color(tmHeading3_ColorId, get_Color(black_ColorId));
1316 set_Color(tmBannerBackground_ColorId, mix_Color(get_Color(tmBackground_ColorId), get_Color(brown_ColorId), 0.15f));
1317 set_Color(tmBannerTitle_ColorId, get_Color(brown_ColorId));
1318 set_Color(tmBannerIcon_ColorId, get_Color(brown_ColorId));
1319 set_Color(tmLinkText_ColorId, get_Color(tmHeading2_ColorId));
1320 set_Color(tmHypertextLinkText_ColorId, get_Color(tmHeading2_ColorId));
1321 set_Color(tmGopherLinkText_ColorId, get_Color(tmHeading2_ColorId));
1322 }
1252 } 1323 }
1253 else if (theme == white_GmDocumentTheme) { 1324 else if (theme == white_GmDocumentTheme) {
1254 const iHSLColor base = { 40, 0, 1.0f, 1.0f }; 1325 const iHSLColor base = { 40, 0, 1.0f, 1.0f };
@@ -1572,11 +1643,11 @@ static void markLinkRunsVisited_GmDocument_(iGmDocument *d, const iIntSet *linkI
1572 iForEach(Array, r, &d->layout) { 1643 iForEach(Array, r, &d->layout) {
1573 iGmRun *run = r.value; 1644 iGmRun *run = r.value;
1574 if (run->linkId && !run->mediaId && contains_IntSet(linkIds, run->linkId)) { 1645 if (run->linkId && !run->mediaId && contains_IntSet(linkIds, run->linkId)) {
1575 if (run->textParams.font == bold_FontId) { 1646 if (run->font == bold_FontId) {
1576 run->textParams.font = paragraph_FontId; 1647 run->font = paragraph_FontId;
1577 } 1648 }
1578 else if (run->flags & decoration_GmRunFlag) { 1649 else if (run->flags & decoration_GmRunFlag) {
1579 run->textParams.color = linkColor_GmDocument(d, run->linkId, icon_GmLinkPart); 1650 run->color = linkColor_GmDocument(d, run->linkId, icon_GmLinkPart);
1580 } 1651 }
1581 } 1652 }
1582 } 1653 }
@@ -1712,10 +1783,209 @@ void setUrl_GmDocument(iGmDocument *d, const iString *url) {
1712 init_Url(&parts, url); 1783 init_Url(&parts, url);
1713 setRange_String(&d->localHost, parts.host); 1784 setRange_String(&d->localHost, parts.host);
1714 updateIconBasedOnUrl_GmDocument_(d); 1785 updateIconBasedOnUrl_GmDocument_(d);
1786 if (!cmp_String(url, "about:fonts")) {
1787 /* This is an interactive internal page. */
1788 d->enableCommandLinks = iTrue;
1789 }
1790}
1791
1792static int replaceRegExp_String(iString *d, const iRegExp *regexp, const char *replacement,
1793 void (*matchHandler)(void *, const iRegExpMatch *),
1794 void *context) {
1795 iRegExpMatch m;
1796 iString result;
1797 int numMatches = 0;
1798 const char *pos = constBegin_String(d);
1799 init_RegExpMatch(&m);
1800 init_String(&result);
1801 while (matchString_RegExp(regexp, d, &m)) {
1802 appendRange_String(&result, (iRangecc){ pos, begin_RegExpMatch(&m) });
1803 /* Replace any capture group back-references. */
1804 for (const char *ch = replacement; *ch; ch++) {
1805 if (*ch == '\\') {
1806 ch++;
1807 if (*ch == '\\') {
1808 appendCStr_String(&result, "\\");
1809 }
1810 else if (*ch >= '0' && *ch <= '9') {
1811 appendRange_String(&result, capturedRange_RegExpMatch(&m, *ch - '0'));
1812 }
1813 }
1814 else {
1815 appendData_Block(&result.chars, ch, 1);
1816 }
1817 }
1818 if (matchHandler) {
1819 matchHandler(context, &m);
1820 }
1821 pos = end_RegExpMatch(&m);
1822 numMatches++;
1823 }
1824 appendRange_String(&result, (iRangecc){ pos, constEnd_String(d) });
1825 set_String(d, &result);
1826 deinit_String(&result);
1827 return numMatches;
1828}
1829
1830iDeclareType(PendingLink)
1831struct Impl_PendingLink {
1832 iString *url;
1833 iString *title;
1834};
1835
1836static void addPendingLink_(void *context, const iRegExpMatch *m) {
1837 pushBack_Array(context, &(iPendingLink){
1838 .url = captured_RegExpMatch(m, 2),
1839 .title = captured_RegExpMatch(m, 1)
1840 });
1841}
1842
1843static void addPendingNamedLink_(void *context, const iRegExpMatch *m) {
1844 pushBack_Array(context, &(iPendingLink){
1845 .url = newFormat_String("[]%s", cstr_Rangecc(capturedRange_RegExpMatch(m, 2))),
1846 .title = captured_RegExpMatch(m, 1)
1847 });
1848}
1849
1850static void flushPendingLinks_(iArray *links, const iString *source, iString *out) {
1851 iRegExp *namePattern = new_RegExp("\n\\s*\\[(.+?)\\]\\s*:\\s*([^\n]+)", 0);
1852 if (!endsWith_String(out, "\n")) {
1853 appendCStr_String(out, "\n");
1854 }
1855 iForEach(Array, i, links) {
1856 iPendingLink *pending = i.value;
1857 const char *url = cstr_String(pending->url);
1858 if (startsWith_CStr(url, "[]")) {
1859 /* Find the matching named link. */
1860 iRegExpMatch m;
1861 init_RegExpMatch(&m);
1862 while (matchString_RegExp(namePattern, source, &m)) {
1863 if (equal_Rangecc(capturedRange_RegExpMatch(&m, 1), url + 2)) {
1864 url = cstrCollect_String(captured_RegExpMatch(&m, 2));
1865 break;
1866 }
1867 }
1868 }
1869 appendFormat_String(out, "\n=> %s %s", url, cstr_String(pending->title));
1870 delete_String(pending->url);
1871 delete_String(pending->title);
1872 }
1873 clear_Array(links);
1874 iRelease(namePattern);
1875}
1876
1877static void convertMarkdownToGemtext_GmDocument_(iGmDocument *d) {
1878 iAssert(d->format == markdown_SourceFormat);
1879 /* Get rid of indented preformats. */ {
1880 iArray *pendingLinks = collectNew_Array(sizeof(iPendingLink));
1881 const iRegExp *imageLinkPattern = iClob(new_RegExp("\n?!\\[(.+)\\]\\(([^)]+)\\)\n?", 0));
1882 const iRegExp *linkPattern = iClob(new_RegExp("\\[(.+?)\\]\\(([^)]+)\\)", 0));
1883 const iRegExp *standaloneLinkPattern = iClob(new_RegExp("^[\\s*_]*\\[(.+?)\\]\\(([^)]+)\\)[\\s*_]*$", 0));
1884 const iRegExp *namedLinkPattern = iClob(new_RegExp("\\[(.+?)\\]\\[(.+?)\\]", 0));
1885 const iRegExp *namePattern = iClob(new_RegExp("\\s*\\[(.+?)\\]\\s*:\\s*([^\n]+)", 0));
1886 iString result;
1887 init_String(&result);
1888 replace_String(&d->source, "&nbsp;", "\u00a0");
1889 replaceRegExp_String(&d->source, iClob(new_RegExp("```", 0)), "\n```\n", NULL, NULL);
1890 iRangecc line = iNullRange;
1891 iBool isPre = iFalse;
1892 iBool isBlock = iFalse;
1893 iBool isLastEmpty = iFalse;
1894 while (nextSplit_Rangecc(range_String(&d->source), "\n", &line)) {
1895 if (!isPre && !isBlock) {
1896 if (equal_Rangecc(line, "```")) {
1897 isBlock = iTrue;
1898 appendCStr_String(&result, "\n```");
1899 continue;
1900 }
1901 if (*line.start == '#') {
1902 flushPendingLinks_(pendingLinks, &d->source, &result);
1903 }
1904 if (isEmpty_Range(&line)) {
1905 isLastEmpty = iTrue;
1906 continue;
1907 }
1908 if (isLastEmpty) {
1909 appendCStr_String(&result, "\n\n");
1910 }
1911 else if (size_Range(&line) >= 2 && isnumber(line.start[0]) &&
1912 (line.start[1] == '.' ||
1913 (isnumber(line.start[1]) && line.start[2] == '.'))) {
1914 appendCStr_String(&result, "\n\n");
1915 }
1916 else if (endsWith_String(&result, " ") ||
1917 *line.start == '*' || *line.start == '>' || *line.start == '#' ||
1918 (*line.start == '|' && endsWith_String(&result, "|"))) {
1919 appendCStr_String(&result, "\n");
1920 }
1921 else {
1922 appendCStr_String(&result, " ");
1923 }
1924 isLastEmpty = iFalse;
1925 }
1926 else if (isBlock) {
1927 if (equal_Rangecc(line, "```")) {
1928 isBlock = iFalse;
1929 appendCStr_String(&result, "\n```\n");
1930 }
1931 else {
1932 appendCStr_String(&result, "\n");
1933 appendRange_String(&result, line);
1934 }
1935 continue;
1936 }
1937 if (startsWith_Rangecc(line, " ")) {
1938 line.start += 4;
1939 if (!isPre) {
1940 appendCStr_String(&result, "```\n");
1941 isPre = iTrue;
1942 }
1943 }
1944 else if (isPre) {
1945 if (!endsWith_String(&result, "\n")) {
1946 appendCStr_String(&result, "\n");
1947 }
1948 appendCStr_String(&result, "```\n");
1949 if (equal_Rangecc(line, "```")) {
1950 line.start = line.end; /* don't repeat it */
1951 }
1952 isPre = iFalse;
1953 }
1954 if (isPre) {
1955 appendRange_String(&result, line);
1956 appendCStr_String(&result, "\n");
1957 }
1958 else {
1959 iString ln;
1960 initRange_String(&ln, line);
1961 replaceRegExp_String(&ln, namePattern, "", NULL, 0);
1962 replaceRegExp_String(&ln, standaloneLinkPattern, "\n=> \\2 \\1", NULL, NULL);
1963 replaceRegExp_String(&ln, imageLinkPattern, "\n=> \\2 \\1\n", NULL, NULL);
1964 replaceRegExp_String(&ln, namedLinkPattern, "\\1", addPendingNamedLink_, pendingLinks);
1965 replaceRegExp_String(&ln, linkPattern, "\\1", addPendingLink_, pendingLinks);
1966 replaceRegExp_String(&ln, iClob(new_RegExp("\\*\\*(.+?)\\*\\*", 0)), "\x1b[1m\\1\x1b[0m", NULL, NULL);
1967 replaceRegExp_String(&ln, iClob(new_RegExp("__(.+?)__", 0)), "\x1b[1m\\1\x1b[0m", NULL, NULL);
1968 replaceRegExp_String(&ln, iClob(new_RegExp("\\*(.+?)\\*", 0)), "\x1b[3m\\1\x1b[0m", NULL, NULL);
1969 replaceRegExp_String(&ln, iClob(new_RegExp("\\b_(.+?)_\\b", 0)), "\x1b[3m\\1\x1b[0m", NULL, NULL);
1970 replaceRegExp_String(&ln, iClob(new_RegExp("(?<!`)`([^`]+?)`(?!`)", 0)), "\x1b[4m\\1\x1b[0m", NULL, NULL);
1971 append_String(&result, &ln);
1972 deinit_String(&ln);
1973 }
1974 }
1975 flushPendingLinks_(pendingLinks, &d->source, &result);
1976 set_String(&d->source, &result);
1977 deinit_String(&result);
1978 }
1979 /* Replace Markdown syntax with equivalent Gemtext, where possible. */
1980 replaceRegExp_String(&d->source, iClob(new_RegExp("(\\s*\n){2,}", 0)), "\n\n", NULL, NULL); /* normalize paragraph breaks */
1981 printf("Converted:\n%s", cstr_String(&d->source));
1982 d->format = gemini_SourceFormat;
1715} 1983}
1716 1984
1717void setSource_GmDocument(iGmDocument *d, const iString *source, int width, int outsideMargin, 1985void setSource_GmDocument(iGmDocument *d, const iString *source, int width, int outsideMargin,
1718 enum iGmDocumentUpdate updateType) { 1986 enum iGmDocumentUpdate updateType) {
1987 /* TODO: This API has been set up to allow partial/progressive updating of the content.
1988 Currently the entire source is replaced every time, though. */
1719// printf("[GmDocument] source update (%zu bytes), width:%d, final:%d\n", 1989// printf("[GmDocument] source update (%zu bytes), width:%d, final:%d\n",
1720// size_String(source), width, updateType == final_GmDocumentUpdate); 1990// size_String(source), width, updateType == final_GmDocumentUpdate);
1721 if (size_String(source) == size_String(&d->unormSource)) { 1991 if (size_String(source) == size_String(&d->unormSource)) {
@@ -1723,9 +1993,26 @@ void setSource_GmDocument(iGmDocument *d, const iString *source, int width, int
1723// printf("[GmDocument] source is unchanged!\n"); 1993// printf("[GmDocument] source is unchanged!\n");
1724 return; /* Nothing to do. */ 1994 return; /* Nothing to do. */
1725 } 1995 }
1996 /* Normalize and convert to Gemtext if needed. */
1726 set_String(&d->unormSource, source); 1997 set_String(&d->unormSource, source);
1727 /* Normalize. */ 1998 set_String(&d->source, source);
1728 set_String(&d->source, &d->unormSource); 1999 if (d->format == gemini_SourceFormat) {
2000 d->theme.ansiEscapes = prefs_App()->gemtextAnsiEscapes;
2001 }
2002 else if (d->format == markdown_SourceFormat) {
2003 /* Attempt a conversion to Gemtext when viewing local Markdown files. */
2004 if (equalCase_Rangecc(urlScheme_String(&d->url), "file")) {
2005 convertMarkdownToGemtext_GmDocument_(d);
2006 set_String(&d->unormSource, &d->source); /* use the converted source from now on */
2007 d->theme.ansiEscapes = allowAll_AnsiFlag; /* escapes are used for styling */
2008 }
2009 else {
2010 d->format = plainText_SourceFormat; /* just show as plain text */
2011 }
2012 }
2013 else {
2014 d->theme.ansiEscapes = allowAll_AnsiFlag;
2015 }
1729 if (isNormalized_GmDocument_(d)) { 2016 if (isNormalized_GmDocument_(d)) {
1730 normalize_GmDocument(d); 2017 normalize_GmDocument(d);
1731 } 2018 }
@@ -1766,6 +2053,7 @@ const iGmPreMeta *preMeta_GmDocument(const iGmDocument *d, uint16_t preId) {
1766void render_GmDocument(const iGmDocument *d, iRangei visRangeY, iGmDocumentRenderFunc render, 2053void render_GmDocument(const iGmDocument *d, iRangei visRangeY, iGmDocumentRenderFunc render,
1767 void *context) { 2054 void *context) {
1768 iBool isInside = iFalse; 2055 iBool isInside = iFalse;
2056 setAnsiFlags_Text(d->theme.ansiEscapes);
1769 /* TODO: Check lookup table for quick starting position. */ 2057 /* TODO: Check lookup table for quick starting position. */
1770 iConstForEach(Array, i, &d->layout) { 2058 iConstForEach(Array, i, &d->layout) {
1771 const iGmRun *run = i.value; 2059 const iGmRun *run = i.value;
@@ -1780,6 +2068,7 @@ void render_GmDocument(const iGmDocument *d, iRangei visRangeY, iGmDocumentRende
1780 render(context, run); 2068 render(context, run);
1781 } 2069 }
1782 } 2070 }
2071 setAnsiFlags_Text(allowAll_AnsiFlag);
1783} 2072}
1784 2073
1785static iBool isValidRun_GmDocument_(const iGmDocument *d, const iGmRun *run) { 2074static iBool isValidRun_GmDocument_(const iGmDocument *d, const iGmRun *run) {
@@ -1794,6 +2083,7 @@ const iGmRun *renderProgressive_GmDocument(const iGmDocument *d, const iGmRun *f
1794 size_t maxCount, 2083 size_t maxCount,
1795 iRangei visRangeY, iGmDocumentRenderFunc render, 2084 iRangei visRangeY, iGmDocumentRenderFunc render,
1796 void *context) { 2085 void *context) {
2086 setAnsiFlags_Text(d->theme.ansiEscapes);
1797 const iGmRun *run = first; 2087 const iGmRun *run = first;
1798 while (isValidRun_GmDocument_(d, run)) { 2088 while (isValidRun_GmDocument_(d, run)) {
1799 if ((dir < 0 && bottom_Rect(run->visBounds) < visRangeY.start) || 2089 if ((dir < 0 && bottom_Rect(run->visBounds) < visRangeY.start) ||
@@ -1806,6 +2096,7 @@ const iGmRun *renderProgressive_GmDocument(const iGmDocument *d, const iGmRun *f
1806 render(context, run); 2096 render(context, run);
1807 run += dir; 2097 run += dir;
1808 } 2098 }
2099 setAnsiFlags_Text(allowAll_AnsiFlag);
1809 return isValidRun_GmDocument_(d, run) ? run : NULL; 2100 return isValidRun_GmDocument_(d, run) ? run : NULL;
1810} 2101}
1811 2102
@@ -1877,17 +2168,17 @@ iRangecc findTextBefore_GmDocument(const iGmDocument *d, const iString *text, co
1877} 2168}
1878 2169
1879iGmRunRange findPreformattedRange_GmDocument(const iGmDocument *d, const iGmRun *run) { 2170iGmRunRange findPreformattedRange_GmDocument(const iGmDocument *d, const iGmRun *run) {
1880 iAssert(run->preId); 2171 iAssert(preId_GmRun(run));
1881 iGmRunRange range = { run, run }; 2172 iGmRunRange range = { run, run };
1882 /* Find the beginning. */ 2173 /* Find the beginning. */
1883 while (range.start > (const iGmRun *) constData_Array(&d->layout)) { 2174 while (range.start > (const iGmRun *) constData_Array(&d->layout)) {
1884 const iGmRun *prev = range.start - 1; 2175 const iGmRun *prev = range.start - 1;
1885 if (prev->preId != run->preId) break; 2176 if (preId_GmRun(prev) != preId_GmRun(run)) break;
1886 range.start = prev; 2177 range.start = prev;
1887 } 2178 }
1888 /* Find the ending. */ 2179 /* Find the ending. */
1889 while (range.end < (const iGmRun *) constEnd_Array(&d->layout)) { 2180 while (range.end < (const iGmRun *) constEnd_Array(&d->layout)) {
1890 if (range.end->preId != run->preId) break; 2181 if (preId_GmRun(range.end) != preId_GmRun(run)) break;
1891 range.end++; 2182 range.end++;
1892 } 2183 }
1893 return range; 2184 return range;
@@ -1994,6 +2285,9 @@ iLocalDef iBool isOldSchool_GmLinkScheme(enum iGmLinkScheme d) {
1994 2285
1995enum iColorId linkColor_GmDocument(const iGmDocument *d, iGmLinkId linkId, enum iGmLinkPart part) { 2286enum iColorId linkColor_GmDocument(const iGmDocument *d, iGmLinkId linkId, enum iGmLinkPart part) {
1996 const iGmLink *link = link_GmDocument_(d, linkId); 2287 const iGmLink *link = link_GmDocument_(d, linkId);
2288 if (!link) {
2289 return none_ColorId;
2290 }
1997// const int www_GmLinkFlag = http_GmLinkFlag | mailto_GmLinkFlag; 2291// const int www_GmLinkFlag = http_GmLinkFlag | mailto_GmLinkFlag;
1998// const int gopherOrFinger_GmLinkFlag = gopher_GmLinkFlag | finger_GmLinkFlag; 2292// const int gopherOrFinger_GmLinkFlag = gopher_GmLinkFlag | finger_GmLinkFlag;
1999 const enum iGmLinkScheme scheme = scheme_GmLinkFlag(link->flags); 2293 const enum iGmLinkScheme scheme = scheme_GmLinkFlag(link->flags);
@@ -2063,6 +2357,26 @@ iChar siteIcon_GmDocument(const iGmDocument *d) {
2063 return d->siteIcon; 2357 return d->siteIcon;
2064} 2358}
2065 2359
2360int ansiEscapes_GmDocument(const iGmDocument *d) {
2361 return d->theme.ansiEscapes;
2362}
2363
2364void runBaseAttributes_GmDocument(const iGmDocument *d, const iGmRun *run, int *fontId_out,
2365 int *colorId_out) {
2366 /* Font and color according to the line type. These are needed because each GmRun is
2367 a segment of a paragraph, and if the font or color changes inside the run, each wrapped
2368 segment needs to know both the current font/color and ALSO the base font/color, so
2369 the default attributes can be restored. */
2370 if (run->isLede) {
2371 *fontId_out = firstParagraph_FontId;
2372 *colorId_out = tmFirstParagraph_ColorId;
2373 }
2374 else {
2375 *fontId_out = fontWithSize_Text(d->theme.fonts[run->lineType], run->font % max_FontSize); /* retain size */
2376 *colorId_out = d->theme.colors[run->lineType];
2377 }
2378}
2379
2066iRangecc findLoc_GmRun(const iGmRun *d, iInt2 pos) { 2380iRangecc findLoc_GmRun(const iGmRun *d, iInt2 pos) {
2067 if (pos.y < top_Rect(d->bounds)) { 2381 if (pos.y < top_Rect(d->bounds)) {
2068 return (iRangecc){ d->text.start, d->text.start }; 2382 return (iRangecc){ d->text.start, d->text.start };
@@ -2078,7 +2392,7 @@ iRangecc findLoc_GmRun(const iGmRun *d, iInt2 pos) {
2078 return (iRangecc){ d->text.end, d->text.end }; 2392 return (iRangecc){ d->text.end, d->text.end };
2079 } 2393 }
2080 iRangecc loc; 2394 iRangecc loc;
2081 tryAdvanceNoWrap_Text(d->textParams.font, d->text, x, &loc.start); 2395 tryAdvanceNoWrap_Text(d->font, d->text, x, &loc.start);
2082 loc.end = loc.start; 2396 loc.end = loc.start;
2083 if (!contains_Range(&d->text, loc.start) && loc.start != d->text.end) { 2397 if (!contains_Range(&d->text, loc.start) && loc.start != d->text.end) {
2084 return iNullRange; /* it's some other text */ 2398 return iNullRange; /* it's some other text */