@ai-matrx/print 0.8.1 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -313,10 +313,24 @@ declare const NOTO_EMOJI_URL = "https://cdn.jsdelivr.net/npm/@fontsource/noto-em
313
313
  declare const NOTO_SYMBOLS_URL = "https://cdn.jsdelivr.net/npm/@fontsource/noto-sans-math@5.3.0/files/noto-sans-math-latin-400-normal.woff";
314
314
  declare const defaultEmojiFontLoader: FontLoader;
315
315
  declare const defaultSymbolFontLoader: FontLoader;
316
+ /** Loads the CJK subset faces covering these code points. */
317
+ type CjkFontLoader = (codepoints: number[]) => Promise<Array<{
318
+ name: string;
319
+ bytes: Uint8Array;
320
+ }>>;
316
321
  interface FallbackFontLoaders {
317
322
  emoji?: FontLoader | undefined;
318
323
  symbols?: FontLoader | undefined;
319
- }
324
+ cjk?: CjkFontLoader | undefined;
325
+ }
326
+ /** Noto Sans JP (SIL OFL) from fontsource: 124 unicode-range subsets; only the needed ones load. */
327
+ declare const NOTO_CJK_BASE = "https://cdn.jsdelivr.net/npm/@fontsource/noto-sans-jp@5.3.0";
328
+ /** Parse a fontsource stylesheet: each face's file stem and its unicode ranges. */
329
+ declare function parseUnicodeRangeCss(css: string): Array<{
330
+ file: string;
331
+ ranges: Array<[number, number]>;
332
+ }>;
333
+ declare const defaultCjkFontLoader: CjkFontLoader;
320
334
  /**
321
335
  * Words pdfmake did not put on a page: a text node with no position was never
322
336
  * placed, and a node measured taller than its page (`__height`) whose lines
@@ -657,4 +671,4 @@ declare function exportDocument(source: string, format: DocumentFormat, options?
657
671
  /** Browser helper: save an export through a download link. */
658
672
  declare function downloadDocumentExport(exp: DocumentExport): void;
659
673
 
660
- export { BUNDLED_STYLE_IDS, type BibliographyEntry, type Block, CSL_STYLE_ALIASES, type Caption, CitationEngine, type CitationStyle, type CslFetcher, type CslItem, type CslName, DEFAULT_FOOTER, DOCUMENT_FORMATS, type DocSection, type DocTree, type DocumentExport, type DocumentFormat, type DocumentNotice, type DocumentSettings, type ExportDocumentOptions, type FallbackFontLoaders, type FontLoader, type ImageLoader, type ImageMap, type Inline, type ListItem, type Margins, type MathSpan, NOTO_EMOJI_URL, NOTO_SYMBOLS_URL, type Orientation, PAGE_SIZES, type PageSizeName, type ParseDocumentOptions, type PrepareDocumentOptions, type ResolvedImage, type RunningTemplate, type TocEntry, type TypesetMath, buildPdfDefinition, countUnplacedWords, cslStyleId, defaultCslFetcher, defaultEmojiFontLoader, defaultImageLoader, defaultSymbolFontLoader, documentCss, documentToDocx, documentToEpub, documentToHtml, documentToPdf, downloadDocumentExport, exportDocument, extractMath, fillTemplate, loadCslLocale, loadCslStyle, mathKey, normalizeReference, parseCitationGroup, parseDocument, parseLength, parseLocator, parseMathML, plainText, prepareDocument, renderBlocksHtml, renderPdfDefinition, resolveDocumentImages, resolveSettings, scanCitationKeys, splitFrontmatter, typesetDocumentMath };
674
+ export { BUNDLED_STYLE_IDS, type BibliographyEntry, type Block, CSL_STYLE_ALIASES, type Caption, CitationEngine, type CitationStyle, type CjkFontLoader, type CslFetcher, type CslItem, type CslName, DEFAULT_FOOTER, DOCUMENT_FORMATS, type DocSection, type DocTree, type DocumentExport, type DocumentFormat, type DocumentNotice, type DocumentSettings, type ExportDocumentOptions, type FallbackFontLoaders, type FontLoader, type ImageLoader, type ImageMap, type Inline, type ListItem, type Margins, type MathSpan, NOTO_CJK_BASE, NOTO_EMOJI_URL, NOTO_SYMBOLS_URL, type Orientation, PAGE_SIZES, type PageSizeName, type ParseDocumentOptions, type PrepareDocumentOptions, type ResolvedImage, type RunningTemplate, type TocEntry, type TypesetMath, buildPdfDefinition, countUnplacedWords, cslStyleId, defaultCjkFontLoader, defaultCslFetcher, defaultEmojiFontLoader, defaultImageLoader, defaultSymbolFontLoader, documentCss, documentToDocx, documentToEpub, documentToHtml, documentToPdf, downloadDocumentExport, exportDocument, extractMath, fillTemplate, loadCslLocale, loadCslStyle, mathKey, normalizeReference, parseCitationGroup, parseDocument, parseLength, parseLocator, parseMathML, parseUnicodeRangeCss, plainText, prepareDocument, renderBlocksHtml, renderPdfDefinition, resolveDocumentImages, resolveSettings, scanCitationKeys, splitFrontmatter, typesetDocumentMath };
@@ -313,10 +313,24 @@ declare const NOTO_EMOJI_URL = "https://cdn.jsdelivr.net/npm/@fontsource/noto-em
313
313
  declare const NOTO_SYMBOLS_URL = "https://cdn.jsdelivr.net/npm/@fontsource/noto-sans-math@5.3.0/files/noto-sans-math-latin-400-normal.woff";
314
314
  declare const defaultEmojiFontLoader: FontLoader;
315
315
  declare const defaultSymbolFontLoader: FontLoader;
316
+ /** Loads the CJK subset faces covering these code points. */
317
+ type CjkFontLoader = (codepoints: number[]) => Promise<Array<{
318
+ name: string;
319
+ bytes: Uint8Array;
320
+ }>>;
316
321
  interface FallbackFontLoaders {
317
322
  emoji?: FontLoader | undefined;
318
323
  symbols?: FontLoader | undefined;
319
- }
324
+ cjk?: CjkFontLoader | undefined;
325
+ }
326
+ /** Noto Sans JP (SIL OFL) from fontsource: 124 unicode-range subsets; only the needed ones load. */
327
+ declare const NOTO_CJK_BASE = "https://cdn.jsdelivr.net/npm/@fontsource/noto-sans-jp@5.3.0";
328
+ /** Parse a fontsource stylesheet: each face's file stem and its unicode ranges. */
329
+ declare function parseUnicodeRangeCss(css: string): Array<{
330
+ file: string;
331
+ ranges: Array<[number, number]>;
332
+ }>;
333
+ declare const defaultCjkFontLoader: CjkFontLoader;
320
334
  /**
321
335
  * Words pdfmake did not put on a page: a text node with no position was never
322
336
  * placed, and a node measured taller than its page (`__height`) whose lines
@@ -657,4 +671,4 @@ declare function exportDocument(source: string, format: DocumentFormat, options?
657
671
  /** Browser helper: save an export through a download link. */
658
672
  declare function downloadDocumentExport(exp: DocumentExport): void;
659
673
 
660
- export { BUNDLED_STYLE_IDS, type BibliographyEntry, type Block, CSL_STYLE_ALIASES, type Caption, CitationEngine, type CitationStyle, type CslFetcher, type CslItem, type CslName, DEFAULT_FOOTER, DOCUMENT_FORMATS, type DocSection, type DocTree, type DocumentExport, type DocumentFormat, type DocumentNotice, type DocumentSettings, type ExportDocumentOptions, type FallbackFontLoaders, type FontLoader, type ImageLoader, type ImageMap, type Inline, type ListItem, type Margins, type MathSpan, NOTO_EMOJI_URL, NOTO_SYMBOLS_URL, type Orientation, PAGE_SIZES, type PageSizeName, type ParseDocumentOptions, type PrepareDocumentOptions, type ResolvedImage, type RunningTemplate, type TocEntry, type TypesetMath, buildPdfDefinition, countUnplacedWords, cslStyleId, defaultCslFetcher, defaultEmojiFontLoader, defaultImageLoader, defaultSymbolFontLoader, documentCss, documentToDocx, documentToEpub, documentToHtml, documentToPdf, downloadDocumentExport, exportDocument, extractMath, fillTemplate, loadCslLocale, loadCslStyle, mathKey, normalizeReference, parseCitationGroup, parseDocument, parseLength, parseLocator, parseMathML, plainText, prepareDocument, renderBlocksHtml, renderPdfDefinition, resolveDocumentImages, resolveSettings, scanCitationKeys, splitFrontmatter, typesetDocumentMath };
674
+ export { BUNDLED_STYLE_IDS, type BibliographyEntry, type Block, CSL_STYLE_ALIASES, type Caption, CitationEngine, type CitationStyle, type CjkFontLoader, type CslFetcher, type CslItem, type CslName, DEFAULT_FOOTER, DOCUMENT_FORMATS, type DocSection, type DocTree, type DocumentExport, type DocumentFormat, type DocumentNotice, type DocumentSettings, type ExportDocumentOptions, type FallbackFontLoaders, type FontLoader, type ImageLoader, type ImageMap, type Inline, type ListItem, type Margins, type MathSpan, NOTO_CJK_BASE, NOTO_EMOJI_URL, NOTO_SYMBOLS_URL, type Orientation, PAGE_SIZES, type PageSizeName, type ParseDocumentOptions, type PrepareDocumentOptions, type ResolvedImage, type RunningTemplate, type TocEntry, type TypesetMath, buildPdfDefinition, countUnplacedWords, cslStyleId, defaultCjkFontLoader, defaultCslFetcher, defaultEmojiFontLoader, defaultImageLoader, defaultSymbolFontLoader, documentCss, documentToDocx, documentToEpub, documentToHtml, documentToPdf, downloadDocumentExport, exportDocument, extractMath, fillTemplate, loadCslLocale, loadCslStyle, mathKey, normalizeReference, parseCitationGroup, parseDocument, parseLength, parseLocator, parseMathML, parseUnicodeRangeCss, plainText, prepareDocument, renderBlocksHtml, renderPdfDefinition, resolveDocumentImages, resolveSettings, scanCitationKeys, splitFrontmatter, typesetDocumentMath };
package/dist/document.js CHANGED
@@ -3027,7 +3027,11 @@ async function documentToDocx(tree, options = {}) {
3027
3027
  numbering: { config: [...c.numbering.entries()].map(([reference, levels2]) => ({ reference, levels: levels2 })) },
3028
3028
  styles: {
3029
3029
  default: {
3030
- document: { run: { font: "Cambria", size, color: HEX.text }, paragraph: { spacing: { after: 140, line: 300 } } }
3030
+ // East-Asian text gets a CJK face (Word and LibreOffice substitute an installed one).
3031
+ document: {
3032
+ run: { font: { ascii: "Cambria", hAnsi: "Cambria", cs: "Cambria", eastAsia: "Yu Gothic" }, size, color: HEX.text },
3033
+ paragraph: { spacing: { after: 140, line: 300 } }
3034
+ }
3031
3035
  },
3032
3036
  paragraphStyles: [
3033
3037
  heading("Heading1", "Heading 1", s.fontSize * 1.8, 0),
@@ -3170,7 +3174,7 @@ function renderInlineHtml(nodes, x) {
3170
3174
  case "cite":
3171
3175
  return n.resolved ? `<a class="doc-cite" href="${escapeHtml(href("ref-" + (n.keys[0] ?? ""), x))}">${escapeHtml(n.label)}</a>` : `<span class="doc-cite doc-cite-missing">${escapeHtml(n.label)}</span>`;
3172
3176
  case "math":
3173
- return mathHtml(n.tex, n.display, x);
3177
+ return mathHtml(n.tex, n.display, x, true);
3174
3178
  case "noteRef":
3175
3179
  return `<sup class="doc-note-ref"><a href="${escapeHtml(href(`note-${n.n}`, x))}" id="noteref-${n.n}">${n.n}</a></sup>`;
3176
3180
  default:
@@ -3178,10 +3182,11 @@ function renderInlineHtml(nodes, x) {
3178
3182
  }
3179
3183
  }).join("");
3180
3184
  }
3181
- function mathHtml(tex, display, x) {
3185
+ function mathHtml(tex, display, x, inText = false) {
3182
3186
  const m = x.tree.math.get(mathKey(tex, display));
3183
3187
  if (m?.mathml && !m.error) {
3184
- return display ? `<div class="doc-math-display">${m.mathml}</div>` : `<span class="doc-math">${m.mathml}</span>`;
3188
+ if (!display) return `<span class="doc-math">${m.mathml}</span>`;
3189
+ return inText ? `<span class="doc-math-display">${m.mathml}</span>` : `<div class="doc-math-display">${m.mathml}</div>`;
3185
3190
  }
3186
3191
  return `<code class="doc-math-tex">${escapeHtml(display ? `$$${tex}$$` : `$${tex}$`)}</code>`;
3187
3192
  }
@@ -3343,7 +3348,7 @@ table caption{caption-side:top;text-align:left;margin-bottom:4pt;}
3343
3348
  .doc-figure figcaption{margin-top:4pt;}
3344
3349
  .doc-image-missing{border:1px dashed var(--matrx-print-rule,#d1d5db);padding:16pt;color:var(--matrx-print-muted,#6b7280);}
3345
3350
  .doc-inline-img{max-height:1.2em;vertical-align:middle;}
3346
- .doc-math-display{margin:0.6em 0 0.9em;text-align:center;break-inside:avoid;overflow-x:auto;}
3351
+ .doc-math-display{display:block;margin:0.6em 0 0.9em;text-align:center;break-inside:avoid;overflow-x:auto;}
3347
3352
  .doc-math-display math{font-size:1.15em;}
3348
3353
  .doc-math-tex{color:var(--matrx-print-danger,#b91c1c);}
3349
3354
  .doc-task-list{list-style:none;padding-left:0.2em;}
@@ -3422,7 +3427,7 @@ blockquote{margin:0 0 0.8em;padding-left:1em;border-left:3px solid #d1d5db;color
3422
3427
  .doc-cover-logo{max-height:5em;}
3423
3428
  .doc-cover-title{font-size:2em;}
3424
3429
  .matrx-page-break{page-break-after:always;break-after:page;}
3425
- .doc-math-display{margin:0.6em 0;text-align:center;}
3430
+ .doc-math-display{display:block;margin:0.6em 0;text-align:center;}
3426
3431
  `.trim();
3427
3432
  function extOf(mime) {
3428
3433
  return mime === "image/png" ? "png" : mime === "image/gif" ? "gif" : "jpg";
@@ -4456,12 +4461,13 @@ async function loadTypography(tree, loaders = {}) {
4456
4461
  const base = await loadFaces(vfs, s.fontSize);
4457
4462
  let needEmoji = false;
4458
4463
  let needSymbols = false;
4464
+ const cjk = /* @__PURE__ */ new Set();
4459
4465
  for (const ch of documentText(tree)) {
4460
4466
  const cp = ch.codePointAt(0);
4461
4467
  if (cp < 32 || cp === 8205 || cp === 65039 || base.body(cp)) continue;
4462
4468
  if (EMOJI_CP.test(ch)) needEmoji = true;
4469
+ else if (isCjk(cp)) cjk.add(cp);
4463
4470
  else needSymbols = true;
4464
- if (needEmoji && needSymbols) break;
4465
4471
  }
4466
4472
  const fallbacks = [];
4467
4473
  if (needEmoji) {
@@ -4472,6 +4478,26 @@ async function loadTypography(tree, loaders = {}) {
4472
4478
  const b = await registerFace("NotoSymbols", "NotoSansMath.woff", loaders.symbols ?? defaultSymbolFontLoader, tree, "symbols");
4473
4479
  if (b) fallbacks.push({ font: "NotoSymbols", bytes: b });
4474
4480
  }
4481
+ if (cjk.size) {
4482
+ let subsets = [];
4483
+ try {
4484
+ subsets = await (loaders.cjk ?? defaultCjkFontLoader)([...cjk]);
4485
+ } catch {
4486
+ subsets = [];
4487
+ }
4488
+ for (const sub of subsets) {
4489
+ const font = `NotoCJK_${sub.name.replace(/[^\w]/g, "_")}`;
4490
+ const b = await registerFace(font, `${sub.name}.woff`, async () => sub.bytes, tree, "Japanese / Chinese");
4491
+ if (b) fallbacks.push({ font, bytes: b });
4492
+ }
4493
+ if (!subsets.length) {
4494
+ tree.notices.push({
4495
+ code: "glyph-substituted",
4496
+ message: "The Japanese / Chinese font could not be loaded, so those characters were left out of the PDF (Word, EPUB and HTML keep them).",
4497
+ remedy: "Check the internet connection (the font comes from cdn.jsdelivr.net) and export again."
4498
+ });
4499
+ }
4500
+ }
4475
4501
  const faces = fallbacks.length ? await loadFaces(vfs, s.fontSize, fallbacks) : base;
4476
4502
  if (!s.hyphenate) return { faces };
4477
4503
  const split = await loadHyphenator(s.lang);
@@ -4494,6 +4520,37 @@ var urlLoader = (url) => async () => {
4494
4520
  };
4495
4521
  var defaultEmojiFontLoader = urlLoader(NOTO_EMOJI_URL);
4496
4522
  var defaultSymbolFontLoader = urlLoader(NOTO_SYMBOLS_URL);
4523
+ var NOTO_CJK_BASE = "https://cdn.jsdelivr.net/npm/@fontsource/noto-sans-jp@5.3.0";
4524
+ function parseUnicodeRangeCss(css) {
4525
+ const out = [];
4526
+ for (const block3 of css.split("@font-face").slice(1)) {
4527
+ const file = /url\(\.\/files\/([\w-]+)\.woff\)/.exec(block3)?.[1];
4528
+ const range = /unicode-range:\s*([^;]+);/.exec(block3)?.[1];
4529
+ if (!file || !range) continue;
4530
+ const ranges = range.split(",").map((r) => {
4531
+ const [a, b] = r.trim().replace(/^U\+/i, "").split("-");
4532
+ const lo = parseInt(a, 16);
4533
+ return [lo, b ? parseInt(b, 16) : lo];
4534
+ });
4535
+ out.push({ file, ranges });
4536
+ }
4537
+ return out;
4538
+ }
4539
+ var defaultCjkFontLoader = async (cps) => {
4540
+ if (typeof fetch !== "function" || cps.length === 0) return [];
4541
+ const res = await fetch(`${NOTO_CJK_BASE}/400.css`);
4542
+ if (!res.ok) return [];
4543
+ const faces = parseUnicodeRangeCss(await res.text()).filter((f) => cps.some((cp) => f.ranges.some(([a, b]) => cp >= a && cp <= b)));
4544
+ const out = [];
4545
+ await Promise.all(
4546
+ faces.map(async (f) => {
4547
+ const r = await fetch(`${NOTO_CJK_BASE}/files/${f.file}.woff`);
4548
+ if (r.ok) out.push({ name: f.file, bytes: new Uint8Array(await r.arrayBuffer()) });
4549
+ })
4550
+ );
4551
+ return out;
4552
+ };
4553
+ var isCjk = (cp) => cp >= 11904 && cp <= 40959 || cp >= 44032 && cp <= 55215 || cp >= 63744 && cp <= 64255 || cp >= 65072 && cp <= 65103 || cp >= 65280 && cp <= 65519 || cp >= 131072 && cp <= 201551;
4497
4554
  var EMOJI_CP = /\p{Extended_Pictographic}|\p{Regional_Indicator}|\u20E3/u;
4498
4555
  function documentText(tree) {
4499
4556
  const s = tree.settings;
@@ -4660,6 +4717,70 @@ function removeThinkingContent(content) {
4660
4717
  return result.trim();
4661
4718
  }
4662
4719
  var PAGE_BREAK_HTML = `<div class="${PAGE_BREAK_CLASS}" role="separator" aria-label="${PAGE_BREAK_LABEL}" data-label="${PAGE_BREAK_LABEL}"></div>`;
4720
+ var CODE_ARTIFACT_LANGUAGE = {
4721
+ html: "html",
4722
+ react: "tsx",
4723
+ svg: "svg",
4724
+ mermaid: "mermaid",
4725
+ diff: "diff",
4726
+ iframe: "html",
4727
+ code: ""
4728
+ };
4729
+ function parseTagAttributes(tag) {
4730
+ const attrs = {};
4731
+ for (const m of tag.matchAll(/([a-zA-Z_][\w-]*)\s*=\s*(?:"([^"]*)"|'([^']*)')/g)) {
4732
+ attrs[m[1].toLowerCase()] = m[2] ?? m[3] ?? "";
4733
+ }
4734
+ return attrs;
4735
+ }
4736
+ function unwrapArtifactsToMarkdown(markdown) {
4737
+ if (!/<artifact\b/i.test(markdown)) return markdown;
4738
+ return markdown.split(/(^[ \t]*(?:`{3,}|~{3,})[\s\S]*?^[ \t]*(?:`{3,}|~{3,})[ \t]*$)/m).map(
4739
+ (part, i) => i % 2 === 1 ? part : part.replace(/<artifact\b((?:[^>"']|"[^"]*"|'[^']*')*?)>([\s\S]*?)<\/artifact>/gi, (_m, tag, body) => {
4740
+ const attrs = parseTagAttributes(tag);
4741
+ const type = (attrs.type ?? "").toLowerCase();
4742
+ const trimmed = body.replace(/^\n+|\n+$/g, "");
4743
+ if (!trimmed.trim()) return "";
4744
+ if (/^\s*(`{3,}|~{3,})/.test(trimmed)) return `
4745
+
4746
+ ${trimmed}
4747
+
4748
+ `;
4749
+ if (type in CODE_ARTIFACT_LANGUAGE) {
4750
+ const lang = CODE_ARTIFACT_LANGUAGE[type] || attrs.language || attrs.lang || "";
4751
+ return `
4752
+
4753
+ \`\`\`\`${lang}
4754
+ ${trimmed}
4755
+ \`\`\`\`
4756
+
4757
+ `;
4758
+ }
4759
+ if (looksLikeJson(trimmed)) return `
4760
+
4761
+ \`\`\`\`json
4762
+ ${JSON.stringify(JSON.parse(trimmed), null, 2)}
4763
+ \`\`\`\`
4764
+
4765
+ `;
4766
+ return `
4767
+
4768
+ ${trimmed}
4769
+
4770
+ `;
4771
+ })
4772
+ ).join("");
4773
+ }
4774
+ function looksLikeJson(body) {
4775
+ const t = body.trim();
4776
+ if (!/^[[{]/.test(t)) return false;
4777
+ try {
4778
+ JSON.parse(t);
4779
+ return true;
4780
+ } catch {
4781
+ return false;
4782
+ }
4783
+ }
4663
4784
  var LINK_URL = String.raw`((?:[^()\s]|\([^()\s]*\))+)(?:\s+"[^"]*")?`;
4664
4785
  var IMAGE_RE = new RegExp(String.raw`!\[([^\]]*)\]\(` + LINK_URL + String.raw`\)`, "g");
4665
4786
  var LINK_RE = new RegExp(String.raw`\[([^\]]+)\]\(` + LINK_URL + String.raw`\)`, "g");
@@ -5114,7 +5235,7 @@ function parseDocument(source, options = {}) {
5114
5235
  const settings = resolveSettings(fm.data, sink, options);
5115
5236
  for (const n of sink) notify(n);
5116
5237
  const body = removeThinkingContent(fm.body);
5117
- const lifted = extractMath(body);
5238
+ const lifted = extractMath(unwrapArtifactsToMarkdown(body));
5118
5239
  const root = fromMarkdown(prepass(lifted.text), {
5119
5240
  extensions: [gfm()],
5120
5241
  mdastExtensions: [gfmFromMarkdown()]
@@ -5509,12 +5630,14 @@ export {
5509
5630
  CitationEngine,
5510
5631
  DEFAULT_FOOTER,
5511
5632
  DOCUMENT_FORMATS,
5633
+ NOTO_CJK_BASE,
5512
5634
  NOTO_EMOJI_URL,
5513
5635
  NOTO_SYMBOLS_URL,
5514
5636
  PAGE_SIZES,
5515
5637
  buildPdfDefinition,
5516
5638
  countUnplacedWords,
5517
5639
  cslStyleId,
5640
+ defaultCjkFontLoader,
5518
5641
  defaultCslFetcher,
5519
5642
  defaultEmojiFontLoader,
5520
5643
  defaultImageLoader,
@@ -5537,6 +5660,7 @@ export {
5537
5660
  parseLength,
5538
5661
  parseLocator,
5539
5662
  parseMathML,
5663
+ parseUnicodeRangeCss,
5540
5664
  plainText,
5541
5665
  prepareDocument,
5542
5666
  renderBlocksHtml,