diff options
Diffstat (limited to 'src/gmdocument.c')
-rw-r--r-- | src/gmdocument.c | 734 |
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 | ||
78 | iDeclareType(GmTheme) | ||
79 | |||
80 | struct Impl_GmTheme { | ||
81 | int ansiEscapes; | ||
82 | int colors[max_GmLineType]; | ||
83 | int fonts[max_GmLineType]; | ||
84 | }; | ||
85 | |||
86 | /*----------------------------------------------------------------------------------------------*/ | ||
87 | |||
77 | struct Impl_GmDocument { | 88 | struct 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 | ||
101 | iDefineObjectConstruction(GmDocument) | 114 | iDefineObjectConstruction(GmDocument) |
102 | 115 | ||
116 | static 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 | |||
128 | static 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 | |||
103 | static enum iGmLineType lineType_GmDocument_(const iGmDocument *d, const iRangecc line) { | 161 | static 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 | ||
317 | static 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 | |||
329 | static void linkContentWasLaidOut_GmDocument_(iGmDocument *d, const iGmMediaInfo *mediaInfo, | 386 | static 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 | ||
357 | static void alignDecoration_GmRun_(iGmRun *run, iBool isCentered) { | 414 | static 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 | ||
404 | static void init_RunTypesetter_(iRunTypesetter *d) { | 462 | static void init_RunTypesetter_(iRunTypesetter *d) { |
@@ -421,39 +479,53 @@ static void commit_RunTypesetter_(iRunTypesetter *d, iGmDocument *doc) { | |||
421 | 479 | ||
422 | static const int maxLedeLines_ = 10; | 480 | static const int maxLedeLines_ = 10; |
423 | 481 | ||
424 | static const int colors[max_GmLineType] = { | 482 | static 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 | ||
435 | static iBool typesetOneLine_RunTypesetter_(iWrapText *wrap, iRangecc wrapRange, int origin, | 501 | static 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 | ||
1052 | void 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 | |||
1066 | static void setDerivedThemeColors_(enum iGmDocumentTheme theme) { | 1117 | static 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 | |||
1792 | static 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 | |||
1830 | iDeclareType(PendingLink) | ||
1831 | struct Impl_PendingLink { | ||
1832 | iString *url; | ||
1833 | iString *title; | ||
1834 | }; | ||
1835 | |||
1836 | static 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 | |||
1843 | static 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 | |||
1850 | static 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 | |||
1877 | static 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, " ", "\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 | ||
1717 | void setSource_GmDocument(iGmDocument *d, const iString *source, int width, int outsideMargin, | 1985 | void 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) { | |||
1766 | void render_GmDocument(const iGmDocument *d, iRangei visRangeY, iGmDocumentRenderFunc render, | 2053 | void 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 | ||
1785 | static iBool isValidRun_GmDocument_(const iGmDocument *d, const iGmRun *run) { | 2074 | static 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 | ||
1879 | iGmRunRange findPreformattedRange_GmDocument(const iGmDocument *d, const iGmRun *run) { | 2170 | iGmRunRange 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 | ||
1995 | enum iColorId linkColor_GmDocument(const iGmDocument *d, iGmLinkId linkId, enum iGmLinkPart part) { | 2286 | enum 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 | ||
2360 | int ansiEscapes_GmDocument(const iGmDocument *d) { | ||
2361 | return d->theme.ansiEscapes; | ||
2362 | } | ||
2363 | |||
2364 | void 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 | |||
2066 | iRangecc findLoc_GmRun(const iGmRun *d, iInt2 pos) { | 2380 | iRangecc 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 */ |