@ai-matrx/print 0.8.2 → 0.8.4

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.4
4
+
5
+ - **Right-to-left scripts in PDF (Arabic, Hebrew).** Lines are put in visual
6
+ order by the Unicode Bidirectional Algorithm (bidi-js; words reversed at RTL
7
+ levels, numbers kept left-to-right, brackets mirrored), RTL paragraphs,
8
+ headings, list items and cells are right-aligned, and letters are shaped by
9
+ the font (Arabic joining forms). New fallback faces Noto Sans Arabic and
10
+ Noto Sans Hebrew (`fonts.arabic` / `fonts.hebrew`). HTML/EPUB blocks carry
11
+ `dir="auto"`; Word marks RTL paragraphs `bidi` and RTL runs `rtl`.
12
+ - **Korean** in PDF through Noto Sans KR (unicode-range subsets on demand,
13
+ same `fonts.cjk` loader; Hangul → KR, other CJK → JP).
14
+ - **Table cells are hyphenated** to their own column width in PDF; a word
15
+ that fits a line whole is never hyphenated.
16
+ - **APA 7 narrative citations use "and"** (`Kim and Alvarez (2021)`),
17
+ parenthetical keep "&"; `@key [p. 4]` gives a narrative citation with a
18
+ locator (pandoc syntax).
19
+ - Consumer action: none — refresh to latest.
20
+
21
+ ## 0.8.3
22
+
23
+ - **Fixed:** an `<artifact>` envelope (how chat answers store tables and code)
24
+ printed as raw pipes in documents. The document pipeline now unwraps it with
25
+ the same rules the print skins use (`unwrapArtifactsToMarkdown`, exported
26
+ from `/markdown`): a table stays a table, code stays code.
27
+ - **Fixed (EPUB):** a `$$…$$` formula inside a sentence rendered as a `<div>`
28
+ inside `<p>` (epubcheck RSC-005); it is now a block-styled span.
29
+ - **Word:** East-Asian text carries a CJK font hint, so Word/LibreOffice pick
30
+ an installed Japanese/Chinese face.
31
+ - Consumer action: none — refresh to latest.
32
+
3
33
  ## 0.8.2
4
34
 
5
35
  - **PDF font fallback covers Japanese / Chinese / Korean.** Characters the
package/dist/document.cjs CHANGED
@@ -35,8 +35,11 @@ __export(document_exports, {
35
35
  CitationEngine: () => CitationEngine,
36
36
  DEFAULT_FOOTER: () => DEFAULT_FOOTER,
37
37
  DOCUMENT_FORMATS: () => DOCUMENT_FORMATS,
38
+ NOTO_ARABIC_URL: () => NOTO_ARABIC_URL,
38
39
  NOTO_CJK_BASE: () => NOTO_CJK_BASE,
39
40
  NOTO_EMOJI_URL: () => NOTO_EMOJI_URL,
41
+ NOTO_HEBREW_URL: () => NOTO_HEBREW_URL,
42
+ NOTO_KR_BASE: () => NOTO_KR_BASE,
40
43
  NOTO_SYMBOLS_URL: () => NOTO_SYMBOLS_URL,
41
44
  PAGE_SIZES: () => PAGE_SIZES,
42
45
  buildPdfDefinition: () => buildPdfDefinition,
@@ -78,6 +81,71 @@ __export(document_exports, {
78
81
  });
79
82
  module.exports = __toCommonJS(document_exports);
80
83
 
84
+ // src/document/bidi.ts
85
+ var import_bidi_js = __toESM(require("bidi-js"), 1);
86
+ var BIDI = /* @__PURE__ */ Symbol.for("ai-matrx.print.document.bidi");
87
+ function bidi() {
88
+ const g = globalThis;
89
+ if (!g[BIDI]) g[BIDI] = (0, import_bidi_js.default)();
90
+ return g[BIDI];
91
+ }
92
+ var RTL_RE = /[֐-ࣿיִ-﷿ﹰ-]/;
93
+ function isRtlParagraph(text) {
94
+ if (!RTL_RE.test(text)) return false;
95
+ return (bidi().getEmbeddingLevels(text).paragraphs[0]?.level ?? 0) % 2 === 1;
96
+ }
97
+ function visualOrder(input, rtl) {
98
+ const b = bidi();
99
+ const runs3 = input.map((r) => typeof r === "string" ? { text: r } : { ...r, text: String(r.text ?? "") });
100
+ const lines = [[]];
101
+ runs3.forEach((r, i) => {
102
+ for (const m of r.text.matchAll(/\n|[^\S\n]+|[\u0590-\u08FF\uFB1D-\uFDFF\uFE70-\uFEFF]+|[^\s\u0590-\u08FF\uFB1D-\uFDFF\uFE70-\uFEFF]+/g)) {
103
+ if (m[0] === "\n") lines.push([]);
104
+ else lines[lines.length - 1].push({ run: i, text: m[0] });
105
+ }
106
+ });
107
+ const out = [];
108
+ lines.forEach((line, li) => {
109
+ if (li > 0) out.push({ ...runs3[0], text: "\n" });
110
+ const text = line.map((t) => t.text).join("");
111
+ if (!text) return;
112
+ const { levels } = b.getEmbeddingLevels(text, rtl ? "rtl" : "ltr");
113
+ let at = 0;
114
+ const tokens = line.map((t) => {
115
+ const level = levels[at] ?? 0;
116
+ let s = t.text;
117
+ if (level % 2 === 1) {
118
+ s = [...s].map((ch) => b.getMirroredCharacter(ch) ?? ch).join("");
119
+ if (!RTL_RE.test(s) && !/^\d/.test(s)) s = [...s].reverse().join("");
120
+ }
121
+ at += t.text.length;
122
+ return { ...t, text: s, level };
123
+ });
124
+ const max = Math.max(...tokens.map((t) => t.level));
125
+ const minOdd = Math.min(...tokens.map((t) => t.level % 2 === 1 ? t.level : Infinity));
126
+ for (let lvl = max; lvl >= minOdd && Number.isFinite(minOdd); lvl--) {
127
+ for (let i = 0; i < tokens.length; ) {
128
+ if (tokens[i].level < lvl) {
129
+ i++;
130
+ continue;
131
+ }
132
+ let j = i;
133
+ while (j < tokens.length && tokens[j].level >= lvl) j++;
134
+ tokens.splice(i, j - i, ...tokens.slice(i, j).reverse());
135
+ i = j;
136
+ }
137
+ }
138
+ for (const t of tokens) {
139
+ const last = out[out.length - 1];
140
+ const src = runs3[t.run];
141
+ if (last && last.text !== "\n" && last.__run === t.run) last.text += t.text;
142
+ else out.push({ ...src, text: t.text, __run: t.run });
143
+ }
144
+ });
145
+ for (const r of out) delete r.__run;
146
+ return out;
147
+ }
148
+
81
149
  // src/document/math.ts
82
150
  var import_source = require("@ai-matrx/content-ir/source");
83
151
  var MATH_OPEN = "\uE100";
@@ -2125,6 +2193,8 @@ var CitationEngine = class {
2125
2193
  /** A note style: every citation becomes a numbered note. */
2126
2194
  isNote;
2127
2195
  numeric;
2196
+ /** APA 7 §8.11: narrative citations join authors with "and", not "&". */
2197
+ apaNarrativeAnd;
2128
2198
  constructor(styleXml, localeXml, items, lang) {
2129
2199
  const byId = new Map(items.map((i) => [i.id, toCslJson(i)]));
2130
2200
  const sys = {
@@ -2135,6 +2205,7 @@ var CitationEngine = class {
2135
2205
  this.engine = new Engine(sys, styleXml, cslLocaleTag(lang), true);
2136
2206
  this.engine.setOutputFormat("html");
2137
2207
  this.isNote = this.engine.opt.xclass === "note";
2208
+ this.apaNarrativeAnd = /<title>[^<]*(?:\bAPA\b|American Psychological Association)/i.test(styleXml) || /<link[^>]*href="[^"]*\/styles\/apa(?:-[\w-]*)?"[^>]*rel="(?:self|template|independent-parent)"/i.test(styleXml);
2138
2209
  this.numeric = /citation-number/.test(styleXml.slice(styleXml.indexOf("<citation"), styleXml.indexOf("</citation>")));
2139
2210
  }
2140
2211
  /** Cite a group; `noteIndex` is the note number for note styles (0 = in text). */
@@ -2171,7 +2242,8 @@ var CitationEngine = class {
2171
2242
  if (opts.narrative && refs.length === 1 && !this.isNote && !this.numeric) {
2172
2243
  const author = make(items);
2173
2244
  const rest = make(refs.map((r, i) => ({ ...items[i], "author-only": void 0, "suppress-author": true, id: r.key })));
2174
- const join = () => `${author.label}${rest.label ? ` ${/^\(/.test(rest.label) ? rest.label : `(${rest.label})`}` : ""}`;
2245
+ const who = () => this.apaNarrativeAnd ? author.label.replace(/,? & /g, (m) => m.startsWith(",") ? ", and " : " and ") : author.label;
2246
+ const join = () => `${who()}${rest.label ? ` ${/^\(/.test(rest.label) ? rest.label : `(${rest.label})`}` : ""}`;
2175
2247
  const listeners = [];
2176
2248
  author.listeners.push(() => listeners.forEach((l) => l(join())));
2177
2249
  rest.listeners.push(() => listeners.forEach((l) => l(join())));
@@ -2650,7 +2722,7 @@ function runs(nodes, c, st = {}) {
2650
2722
  for (const n of nodes) {
2651
2723
  switch (n.type) {
2652
2724
  case "text":
2653
- out.push(tr(n.value));
2725
+ out.push(RTL_RE.test(n.value) ? tr(n.value, { rightToLeft: true }) : tr(n.value));
2654
2726
  break;
2655
2727
  case "strong":
2656
2728
  out.push(...runs(n.children, c, { ...st, bold: true }));
@@ -2802,6 +2874,10 @@ function numberingRef(c, ordered, start) {
2802
2874
  }
2803
2875
  return ref;
2804
2876
  }
2877
+ function bidiPara(nodes) {
2878
+ const text = nodes.map((n) => "value" in n ? n.value : "").join("");
2879
+ return isRtlParagraph(text) ? { bidirectional: true } : {};
2880
+ }
2805
2881
  function blocks(list, c, o = {}) {
2806
2882
  const out = [];
2807
2883
  for (const b of list) out.push(...block(b, c, o));
@@ -2826,6 +2902,7 @@ function block(b, c, o) {
2826
2902
  const kids = [...b.number ? [new d.TextRun({ text: `${b.number} `, color: HEX.muted })] : [], ...runs(b.children, c)];
2827
2903
  return [
2828
2904
  new d.Paragraph({
2905
+ ...bidiPara(b.children),
2829
2906
  heading,
2830
2907
  keepNext: true,
2831
2908
  keepLines: true,
@@ -2835,7 +2912,7 @@ function block(b, c, o) {
2835
2912
  ];
2836
2913
  }
2837
2914
  case "paragraph":
2838
- return [para(c, o, { children: runs(b.children, c), widowControl: true })];
2915
+ return [para(c, o, { children: runs(b.children, c), widowControl: true, ...bidiPara(b.children) })];
2839
2916
  case "list": {
2840
2917
  const level = o.listLevel ?? 0;
2841
2918
  const ref = numberingRef(c, b.ordered, b.start);
@@ -2852,7 +2929,14 @@ function block(b, c, o) {
2852
2929
  })
2853
2930
  );
2854
2931
  } else {
2855
- out.push(new d.Paragraph({ numbering: { reference: ref, level, instance }, children: firstRuns, widowControl: true }));
2932
+ out.push(
2933
+ new d.Paragraph({
2934
+ numbering: { reference: ref, level, instance },
2935
+ children: firstRuns,
2936
+ widowControl: true,
2937
+ ...first?.type === "paragraph" ? bidiPara(first.children) : {}
2938
+ })
2939
+ );
2856
2940
  }
2857
2941
  const tail = first && first.type !== "paragraph" ? [first, ...rest] : rest;
2858
2942
  for (const child of tail) {
@@ -2894,6 +2978,7 @@ function block(b, c, o) {
2894
2978
  borders: { top: none, left: none, right: none, bottom: head ? { ...border, size: 10, color: HEX.heading } : border },
2895
2979
  children: [
2896
2980
  new d.Paragraph({
2981
+ ...bidiPara(cells[i] ?? []),
2897
2982
  alignment: alignOf(i),
2898
2983
  keepNext: keep && !last,
2899
2984
  children: runs(cells[i] ?? [], c, head ? { bold: true } : {})
@@ -3107,7 +3192,11 @@ async function documentToDocx(tree, options = {}) {
3107
3192
  numbering: { config: [...c.numbering.entries()].map(([reference, levels2]) => ({ reference, levels: levels2 })) },
3108
3193
  styles: {
3109
3194
  default: {
3110
- document: { run: { font: "Cambria", size, color: HEX.text }, paragraph: { spacing: { after: 140, line: 300 } } }
3195
+ // East-Asian text gets a CJK face (Word and LibreOffice substitute an installed one).
3196
+ document: {
3197
+ run: { font: { ascii: "Cambria", hAnsi: "Cambria", cs: "Arial", eastAsia: "Yu Gothic" }, size, sizeComplexScript: size, color: HEX.text },
3198
+ paragraph: { spacing: { after: 140, line: 300 } }
3199
+ }
3111
3200
  },
3112
3201
  paragraphStyles: [
3113
3202
  heading("Heading1", "Heading 1", s.fontSize * 1.8, 0),
@@ -3250,7 +3339,7 @@ function renderInlineHtml(nodes, x) {
3250
3339
  case "cite":
3251
3340
  return n.resolved ? `<a class="doc-cite" href="${(0, import_html_escape.escapeHtml)(href("ref-" + (n.keys[0] ?? ""), x))}">${(0, import_html_escape.escapeHtml)(n.label)}</a>` : `<span class="doc-cite doc-cite-missing">${(0, import_html_escape.escapeHtml)(n.label)}</span>`;
3252
3341
  case "math":
3253
- return mathHtml(n.tex, n.display, x);
3342
+ return mathHtml(n.tex, n.display, x, true);
3254
3343
  case "noteRef":
3255
3344
  return `<sup class="doc-note-ref"><a href="${(0, import_html_escape.escapeHtml)(href(`note-${n.n}`, x))}" id="noteref-${n.n}">${n.n}</a></sup>`;
3256
3345
  default:
@@ -3258,10 +3347,11 @@ function renderInlineHtml(nodes, x) {
3258
3347
  }
3259
3348
  }).join("");
3260
3349
  }
3261
- function mathHtml(tex, display, x) {
3350
+ function mathHtml(tex, display, x, inText = false) {
3262
3351
  const m = x.tree.math.get(mathKey(tex, display));
3263
3352
  if (m?.mathml && !m.error) {
3264
- return display ? `<div class="doc-math-display">${m.mathml}</div>` : `<span class="doc-math">${m.mathml}</span>`;
3353
+ if (!display) return `<span class="doc-math">${m.mathml}</span>`;
3354
+ return inText ? `<span class="doc-math-display">${m.mathml}</span>` : `<div class="doc-math-display">${m.mathml}</div>`;
3265
3355
  }
3266
3356
  return `<code class="doc-math-tex">${(0, import_html_escape.escapeHtml)(display ? `$$${tex}$$` : `$${tex}$`)}</code>`;
3267
3357
  }
@@ -3292,10 +3382,10 @@ function renderBlockHtml(b, x) {
3292
3382
  switch (b.type) {
3293
3383
  case "heading": {
3294
3384
  const num = b.number ? `<span class="doc-heading-number">${b.number}</span> ` : "";
3295
- return `<h${b.level} id="${(0, import_html_escape.escapeHtml)(b.id)}">${num}${renderInlineHtml(b.children, x)}</h${b.level}>`;
3385
+ return `<h${b.level} id="${(0, import_html_escape.escapeHtml)(b.id)}" dir="auto">${num}${renderInlineHtml(b.children, x)}</h${b.level}>`;
3296
3386
  }
3297
3387
  case "paragraph":
3298
- return `<p>${renderInlineHtml(b.children, x)}</p>`;
3388
+ return `<p dir="auto">${renderInlineHtml(b.children, x)}</p>`;
3299
3389
  case "list": {
3300
3390
  const tag = b.ordered ? "ol" : "ul";
3301
3391
  const start = b.ordered && b.start !== 1 ? ` start="${b.start}"` : "";
@@ -3303,12 +3393,12 @@ function renderBlockHtml(b, x) {
3303
3393
  const items = b.items.map((i) => {
3304
3394
  const box = i.checked === null ? "" : `<span class="doc-task-box${i.checked ? " is-checked" : ""}" role="img" aria-label="${i.checked ? "done" : "not done"}"></span>`;
3305
3395
  const inner = i.children.length === 1 && i.children[0]?.type === "paragraph" ? renderInlineHtml(i.children[0].children, x) : renderBlocksHtml(i.children, x);
3306
- return `<li${i.checked !== null ? ' class="doc-task"' : ""}>${box}${inner}</li>`;
3396
+ return `<li dir="auto"${i.checked !== null ? ' class="doc-task"' : ""}>${box}${inner}</li>`;
3307
3397
  }).join("");
3308
3398
  return `<${tag}${start}${task ? ' class="doc-task-list"' : ""}>${items}</${tag}>`;
3309
3399
  }
3310
3400
  case "blockquote":
3311
- return `<blockquote>${renderBlocksHtml(b.children, x)}</blockquote>`;
3401
+ return `<blockquote dir="auto">${renderBlocksHtml(b.children, x)}</blockquote>`;
3312
3402
  case "code":
3313
3403
  return `<pre class="doc-code"${b.lang ? ` data-language="${(0, import_html_escape.escapeHtml)(b.lang)}"` : ""}><code>${(0, import_html_escape.escapeHtml)(
3314
3404
  b.value
@@ -3318,8 +3408,8 @@ function renderBlockHtml(b, x) {
3318
3408
  const a = b.align[i];
3319
3409
  return a ? ` style="text-align:${a}"` : "";
3320
3410
  };
3321
- const head = `<thead><tr>${b.header.map((c, i) => `<th${al(i)}>${renderInlineHtml(c, x)}</th>`).join("")}</tr></thead>`;
3322
- const body = `<tbody>${b.rows.map((r) => `<tr>${r.map((c, i) => `<td${al(i)}>${renderInlineHtml(c, x)}</td>`).join("")}</tr>`).join("")}</tbody>`;
3411
+ const head = `<thead><tr>${b.header.map((c, i) => `<th dir="auto"${al(i)}>${renderInlineHtml(c, x)}</th>`).join("")}</tr></thead>`;
3412
+ const body = `<tbody>${b.rows.map((r) => `<tr>${r.map((c, i) => `<td dir="auto"${al(i)}>${renderInlineHtml(c, x)}</td>`).join("")}</tr>`).join("")}</tbody>`;
3323
3413
  const id = b.caption?.id ? ` id="tbl:${(0, import_html_escape.escapeHtml)(b.caption.id)}"` : "";
3324
3414
  return `<div class="doc-table-wrap"${id}><table>${b.caption ? captionHtml(b.caption, "caption", x) : ""}${head}${body}</table></div>`;
3325
3415
  }
@@ -3423,7 +3513,7 @@ table caption{caption-side:top;text-align:left;margin-bottom:4pt;}
3423
3513
  .doc-figure figcaption{margin-top:4pt;}
3424
3514
  .doc-image-missing{border:1px dashed var(--matrx-print-rule,#d1d5db);padding:16pt;color:var(--matrx-print-muted,#6b7280);}
3425
3515
  .doc-inline-img{max-height:1.2em;vertical-align:middle;}
3426
- .doc-math-display{margin:0.6em 0 0.9em;text-align:center;break-inside:avoid;overflow-x:auto;}
3516
+ .doc-math-display{display:block;margin:0.6em 0 0.9em;text-align:center;break-inside:avoid;overflow-x:auto;}
3427
3517
  .doc-math-display math{font-size:1.15em;}
3428
3518
  .doc-math-tex{color:var(--matrx-print-danger,#b91c1c);}
3429
3519
  .doc-task-list{list-style:none;padding-left:0.2em;}
@@ -3502,7 +3592,7 @@ blockquote{margin:0 0 0.8em;padding-left:1em;border-left:3px solid #d1d5db;color
3502
3592
  .doc-cover-logo{max-height:5em;}
3503
3593
  .doc-cover-title{font-size:2em;}
3504
3594
  .matrx-page-break{page-break-after:always;break-after:page;}
3505
- .doc-math-display{margin:0.6em 0;text-align:center;}
3595
+ .doc-math-display{display:block;margin:0.6em 0;text-align:center;}
3506
3596
  `.trim();
3507
3597
  function extOf(mime) {
3508
3598
  return mime === "image/png" ? "png" : mime === "image/gif" ? "gif" : "jpg";
@@ -3789,7 +3879,7 @@ function breakRuns(input, width, t) {
3789
3879
  for (let guard = 0; guard < 50 && atoms.length; guard++) {
3790
3880
  const gw = atoms.reduce((n, a) => n + w(a), 0);
3791
3881
  const sw = pendingSpace && line > 0 ? w(pendingSpace) : 0;
3792
- if (line + sw + gw <= W || line === 0 && atoms.length !== 1) {
3882
+ if (line + sw + gw <= W || line === 0 && (atoms.length !== 1 || gw <= width)) {
3793
3883
  if (sw && pendingSpace) out.push(pendingSpace);
3794
3884
  out.push(...atoms);
3795
3885
  line += sw + gw;
@@ -3913,11 +4003,14 @@ function withFallbackFonts(nodes, c) {
3913
4003
  const fb = fallbackFor(cp);
3914
4004
  if (fb) {
3915
4005
  flush();
3916
- out.push({ ...n, text: ch, font: fb, bold: false, italics: false });
4006
+ const prev = out[out.length - 1];
4007
+ if (prev && typeof prev === "object" && prev.font === fb && prev.__fb) prev.text = `${prev.text}${ch}`;
4008
+ else out.push({ ...n, text: ch, font: fb, bold: false, italics: false, __fb: true });
3917
4009
  } else dropped = true;
3918
4010
  }
3919
4011
  flush();
3920
4012
  }
4013
+ for (const o of out) if (typeof o === "object") delete o.__fb;
3921
4014
  if (dropped && !c.glyphs.announced) {
3922
4015
  c.glyphs.announced = true;
3923
4016
  c.tree.notices.push({
@@ -3944,8 +4037,13 @@ function drawable(text, c) {
3944
4037
  }
3945
4038
  function bodyRuns(nodes, c, width = c.width) {
3946
4039
  const r = runs2(nodes, c);
3947
- return c.typo ? breakRuns(r, width, c.typo) : r;
4040
+ const text = plain(nodes);
4041
+ if (!RTL_RE.test(text)) return c.typo ? breakRuns(r, width, c.typo) : r;
4042
+ const typo = c.typo ?? (c.faces ? { split: (w) => [w], measure: c.faces.measure } : void 0);
4043
+ const lines = typo ? breakRuns(r, width, typo) : r;
4044
+ return visualOrder(lines, isRtlParagraph(text));
3948
4045
  }
4046
+ var alignFor = (nodes) => isRtlParagraph(plain(nodes)) ? { alignment: "right" } : {};
3949
4047
  function runs2(nodes, c, st = {}) {
3950
4048
  const out = [];
3951
4049
  const base = (raw) => {
@@ -4137,6 +4235,7 @@ function estimateLines(text, c, size = c.s.fontSize) {
4137
4235
  function columnWidths(b, n, size, c) {
4138
4236
  const measure = (text, bold) => c.faces ? c.faces.measure(text, { bold, fontSize: size }) : text.length * size * 0.5;
4139
4237
  const pad = 10;
4238
+ const slack = 1;
4140
4239
  const min = new Array(n).fill(0);
4141
4240
  const max = new Array(n).fill(0);
4142
4241
  const rows = [[b.header, true], ...b.rows.map((r) => [r, false])];
@@ -4144,8 +4243,8 @@ function columnWidths(b, n, size, c) {
4144
4243
  for (let i = 0; i < n; i++) {
4145
4244
  const t = plain(r[i] ?? []);
4146
4245
  const longest = t.split(/\s+/).reduce((m, w) => Math.max(m, measure(w, head)), 0);
4147
- min[i] = Math.max(min[i], Math.min(longest, c.width * 0.35) + pad);
4148
- max[i] = Math.max(max[i], measure(t, head) + pad);
4246
+ min[i] = Math.max(min[i], Math.min(longest * slack, c.width * 0.35) + pad);
4247
+ max[i] = Math.max(max[i], measure(t, head) * slack + pad);
4149
4248
  }
4150
4249
  }
4151
4250
  const avail = c.width;
@@ -4209,8 +4308,11 @@ function block2(b, c) {
4209
4308
  switch (b.type) {
4210
4309
  case "heading": {
4211
4310
  const rel = Math.min(6, b.level - c.minLevel + 1);
4311
+ const rtlHeading = RTL_RE.test(b.plain);
4312
+ const headRuns = rtlHeading ? visualOrder(runs2(b.children, c), isRtlParagraph(b.plain)) : runs2(b.children, c);
4212
4313
  const node = {
4213
- text: [...b.number ? [{ text: `${b.number} `, color: COLOR.muted }] : [], ...runs2(b.children, c)],
4314
+ ...rtlHeading ? alignFor(b.children) : {},
4315
+ text: [...b.number ? [{ text: `${b.number} `, color: COLOR.muted }] : [], ...headRuns],
4214
4316
  style: `h${Math.min(6, b.level)}`,
4215
4317
  headlineLevel: b.level,
4216
4318
  id: b.id
@@ -4224,12 +4326,12 @@ function block2(b, c) {
4224
4326
  }
4225
4327
  case "paragraph": {
4226
4328
  const text = bodyRuns(b.children, c);
4227
- const p = { text, style: "p" };
4329
+ const p = { text, style: "p", ...alignFor(b.children) };
4228
4330
  return estimateLines(plain(b.children), c) <= 5 ? { stack: [p], unbreakable: true } : p;
4229
4331
  }
4230
4332
  case "list": {
4231
4333
  const items = b.items.map((it) => {
4232
- const content = it.children.length === 1 && it.children[0]?.type === "paragraph" ? [{ text: bodyRuns(it.children[0].children, c, c.width - 20 - (it.checked !== null ? 8 : 0)), margin: [0, 0, 0, 2] }] : blocks2(it.children, { ...c, width: c.width - 18 });
4334
+ const content = it.children.length === 1 && it.children[0]?.type === "paragraph" ? [{ text: bodyRuns(it.children[0].children, c, c.width - 20 - (it.checked !== null ? 8 : 0)), margin: [0, 0, 0, 2], ...alignFor(it.children[0].children) }] : blocks2(it.children, { ...c, width: c.width - 18 });
4233
4335
  if (it.checked !== null) return { ...taskItem(it.checked, content, c), listType: "none" };
4234
4336
  return content.length === 1 ? content[0] : { stack: content };
4235
4337
  });
@@ -4282,15 +4384,24 @@ function block2(b, c) {
4282
4384
  case "table": {
4283
4385
  const n = Math.max(b.header.length, ...b.rows.map((r) => r.length), 1);
4284
4386
  const size = c.s.fontSize * 0.92;
4387
+ const widths = columnWidths(b, n, size, c);
4388
+ const cellRuns = (inl, i, head) => {
4389
+ const r = runs2(inl, c).map((x) => typeof x === "object" ? { ...x, fontSize: x.fontSize ?? size, ...head ? { bold: true } : {} } : x);
4390
+ const w = (widths[i] ?? c.width) - 10;
4391
+ if (RTL_RE.test(plain(inl))) {
4392
+ const typo = c.typo ?? (c.faces ? { split: (x) => [x], measure: c.faces.measure } : void 0);
4393
+ return visualOrder(typo ? breakRuns(r, w, typo) : r, isRtlParagraph(plain(inl)));
4394
+ }
4395
+ return c.typo && !head ? breakRuns(r, w, c.typo) : r;
4396
+ };
4285
4397
  const cell = (inl, i, head) => ({
4286
- text: runs2(inl ?? [], c),
4398
+ text: cellRuns(inl ?? [], i, head),
4287
4399
  bold: head,
4288
- alignment: b.align[i] ?? "left",
4400
+ alignment: b.align[i] ?? (isRtlParagraph(plain(inl ?? [])) ? "right" : "left"),
4289
4401
  fontSize: size,
4290
4402
  color: head ? COLOR.heading : COLOR.text
4291
4403
  });
4292
4404
  const row = (r, head) => Array.from({ length: n }, (_, i) => cell(r[i], i, head));
4293
- const widths = columnWidths(b, n, size, c);
4294
4405
  const lineH = size * 1.3;
4295
4406
  const pageH = c.pageHeight;
4296
4407
  const rowHeight = (r) => Math.max(...r.map((inl, i) => estimateLines(plain(inl), { ...c, width: (widths[i] ?? c.width) - 10 }, size) * lineH)) + 8;
@@ -4536,12 +4647,16 @@ async function loadTypography(tree, loaders = {}) {
4536
4647
  const base = await loadFaces(vfs, s.fontSize);
4537
4648
  let needEmoji = false;
4538
4649
  let needSymbols = false;
4650
+ let needArabic = false;
4651
+ let needHebrew = false;
4539
4652
  const cjk = /* @__PURE__ */ new Set();
4540
4653
  for (const ch of documentText(tree)) {
4541
4654
  const cp = ch.codePointAt(0);
4542
4655
  if (cp < 32 || cp === 8205 || cp === 65039 || base.body(cp)) continue;
4543
4656
  if (EMOJI_CP.test(ch)) needEmoji = true;
4544
- else if (isCjk(cp)) cjk.add(cp);
4657
+ else if (isCjk(cp) || isHangul(cp)) cjk.add(cp);
4658
+ else if (isArabic(cp)) needArabic = true;
4659
+ else if (isHebrew(cp)) needHebrew = true;
4545
4660
  else needSymbols = true;
4546
4661
  }
4547
4662
  const fallbacks = [];
@@ -4553,6 +4668,14 @@ async function loadTypography(tree, loaders = {}) {
4553
4668
  const b = await registerFace("NotoSymbols", "NotoSansMath.woff", loaders.symbols ?? defaultSymbolFontLoader, tree, "symbols");
4554
4669
  if (b) fallbacks.push({ font: "NotoSymbols", bytes: b });
4555
4670
  }
4671
+ if (needArabic) {
4672
+ const b = await registerFace("NotoArabic", "NotoSansArabic.woff", loaders.arabic ?? urlLoader(NOTO_ARABIC_URL), tree, "Arabic");
4673
+ if (b) fallbacks.push({ font: "NotoArabic", bytes: b });
4674
+ }
4675
+ if (needHebrew) {
4676
+ const b = await registerFace("NotoHebrew", "NotoSansHebrew.woff", loaders.hebrew ?? urlLoader(NOTO_HEBREW_URL), tree, "Hebrew");
4677
+ if (b) fallbacks.push({ font: "NotoHebrew", bytes: b });
4678
+ }
4556
4679
  if (cjk.size) {
4557
4680
  let subsets = [];
4558
4681
  try {
@@ -4568,7 +4691,7 @@ async function loadTypography(tree, loaders = {}) {
4568
4691
  if (!subsets.length) {
4569
4692
  tree.notices.push({
4570
4693
  code: "glyph-substituted",
4571
- message: "The Japanese / Chinese font could not be loaded, so those characters were left out of the PDF (Word, EPUB and HTML keep them).",
4694
+ message: "The Chinese / Japanese / Korean font could not be loaded, so those characters were left out of the PDF (Word, EPUB and HTML keep them).",
4572
4695
  remedy: "Check the internet connection (the font comes from cdn.jsdelivr.net) and export again."
4573
4696
  });
4574
4697
  }
@@ -4596,6 +4719,12 @@ var urlLoader = (url) => async () => {
4596
4719
  var defaultEmojiFontLoader = urlLoader(NOTO_EMOJI_URL);
4597
4720
  var defaultSymbolFontLoader = urlLoader(NOTO_SYMBOLS_URL);
4598
4721
  var NOTO_CJK_BASE = "https://cdn.jsdelivr.net/npm/@fontsource/noto-sans-jp@5.3.0";
4722
+ var NOTO_KR_BASE = "https://cdn.jsdelivr.net/npm/@fontsource/noto-sans-kr@5.3.0";
4723
+ var NOTO_ARABIC_URL = "https://cdn.jsdelivr.net/npm/@fontsource/noto-sans-arabic@5.3.0/files/noto-sans-arabic-arabic-400-normal.woff";
4724
+ var NOTO_HEBREW_URL = "https://cdn.jsdelivr.net/npm/@fontsource/noto-sans-hebrew@5.3.0/files/noto-sans-hebrew-hebrew-400-normal.woff";
4725
+ var isHangul = (cp) => cp >= 44032 && cp <= 55215 || cp >= 4352 && cp <= 4607 || cp >= 12592 && cp <= 12687;
4726
+ var isArabic = (cp) => cp >= 1536 && cp <= 1791 || cp >= 1872 && cp <= 1919 || cp >= 2208 && cp <= 2303 || cp >= 64336 && cp <= 65023 || cp >= 65136 && cp <= 65279;
4727
+ var isHebrew = (cp) => cp >= 1424 && cp <= 1535 || cp >= 64285 && cp <= 64335;
4599
4728
  function parseUnicodeRangeCss(css) {
4600
4729
  const out = [];
4601
4730
  for (const block3 of css.split("@font-face").slice(1)) {
@@ -4611,19 +4740,25 @@ function parseUnicodeRangeCss(css) {
4611
4740
  }
4612
4741
  return out;
4613
4742
  }
4614
- var defaultCjkFontLoader = async (cps) => {
4743
+ async function unicodeRangeFaces(base, cps) {
4615
4744
  if (typeof fetch !== "function" || cps.length === 0) return [];
4616
- const res = await fetch(`${NOTO_CJK_BASE}/400.css`);
4745
+ const res = await fetch(`${base}/400.css`);
4617
4746
  if (!res.ok) return [];
4618
4747
  const faces = parseUnicodeRangeCss(await res.text()).filter((f) => cps.some((cp) => f.ranges.some(([a, b]) => cp >= a && cp <= b)));
4619
4748
  const out = [];
4620
4749
  await Promise.all(
4621
4750
  faces.map(async (f) => {
4622
- const r = await fetch(`${NOTO_CJK_BASE}/files/${f.file}.woff`);
4751
+ const r = await fetch(`${base}/files/${f.file}.woff`);
4623
4752
  if (r.ok) out.push({ name: f.file, bytes: new Uint8Array(await r.arrayBuffer()) });
4624
4753
  })
4625
4754
  );
4626
4755
  return out;
4756
+ }
4757
+ var defaultCjkFontLoader = async (cps) => {
4758
+ const kr = cps.filter(isHangul);
4759
+ const jp = cps.filter((cp) => !isHangul(cp));
4760
+ const [a, b] = await Promise.all([unicodeRangeFaces(NOTO_KR_BASE, kr), unicodeRangeFaces(NOTO_CJK_BASE, jp)]);
4761
+ return [...a, ...b];
4627
4762
  };
4628
4763
  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;
4629
4764
  var EMOJI_CP = /\p{Extended_Pictographic}|\p{Regional_Indicator}|\u20E3/u;
@@ -4792,6 +4927,70 @@ function removeThinkingContent(content) {
4792
4927
  return result.trim();
4793
4928
  }
4794
4929
  var PAGE_BREAK_HTML = `<div class="${PAGE_BREAK_CLASS}" role="separator" aria-label="${PAGE_BREAK_LABEL}" data-label="${PAGE_BREAK_LABEL}"></div>`;
4930
+ var CODE_ARTIFACT_LANGUAGE = {
4931
+ html: "html",
4932
+ react: "tsx",
4933
+ svg: "svg",
4934
+ mermaid: "mermaid",
4935
+ diff: "diff",
4936
+ iframe: "html",
4937
+ code: ""
4938
+ };
4939
+ function parseTagAttributes(tag) {
4940
+ const attrs = {};
4941
+ for (const m of tag.matchAll(/([a-zA-Z_][\w-]*)\s*=\s*(?:"([^"]*)"|'([^']*)')/g)) {
4942
+ attrs[m[1].toLowerCase()] = m[2] ?? m[3] ?? "";
4943
+ }
4944
+ return attrs;
4945
+ }
4946
+ function unwrapArtifactsToMarkdown(markdown) {
4947
+ if (!/<artifact\b/i.test(markdown)) return markdown;
4948
+ return markdown.split(/(^[ \t]*(?:`{3,}|~{3,})[\s\S]*?^[ \t]*(?:`{3,}|~{3,})[ \t]*$)/m).map(
4949
+ (part, i) => i % 2 === 1 ? part : part.replace(/<artifact\b((?:[^>"']|"[^"]*"|'[^']*')*?)>([\s\S]*?)<\/artifact>/gi, (_m, tag, body) => {
4950
+ const attrs = parseTagAttributes(tag);
4951
+ const type = (attrs.type ?? "").toLowerCase();
4952
+ const trimmed = body.replace(/^\n+|\n+$/g, "");
4953
+ if (!trimmed.trim()) return "";
4954
+ if (/^\s*(`{3,}|~{3,})/.test(trimmed)) return `
4955
+
4956
+ ${trimmed}
4957
+
4958
+ `;
4959
+ if (type in CODE_ARTIFACT_LANGUAGE) {
4960
+ const lang = CODE_ARTIFACT_LANGUAGE[type] || attrs.language || attrs.lang || "";
4961
+ return `
4962
+
4963
+ \`\`\`\`${lang}
4964
+ ${trimmed}
4965
+ \`\`\`\`
4966
+
4967
+ `;
4968
+ }
4969
+ if (looksLikeJson(trimmed)) return `
4970
+
4971
+ \`\`\`\`json
4972
+ ${JSON.stringify(JSON.parse(trimmed), null, 2)}
4973
+ \`\`\`\`
4974
+
4975
+ `;
4976
+ return `
4977
+
4978
+ ${trimmed}
4979
+
4980
+ `;
4981
+ })
4982
+ ).join("");
4983
+ }
4984
+ function looksLikeJson(body) {
4985
+ const t = body.trim();
4986
+ if (!/^[[{]/.test(t)) return false;
4987
+ try {
4988
+ JSON.parse(t);
4989
+ return true;
4990
+ } catch {
4991
+ return false;
4992
+ }
4993
+ }
4795
4994
  var LINK_URL = String.raw`((?:[^()\s]|\([^()\s]*\))+)(?:\s+"[^"]*")?`;
4796
4995
  var IMAGE_RE = new RegExp(String.raw`!\[([^\]]*)\]\(` + LINK_URL + String.raw`\)`, "g");
4797
4996
  var LINK_RE = new RegExp(String.raw`\[([^\]]+)\]\(` + LINK_URL + String.raw`\)`, "g");
@@ -4878,7 +5077,7 @@ function uniqueId(ctx, base) {
4878
5077
  return id;
4879
5078
  }
4880
5079
  var CITE_GROUP_RE = /\[(-?@[^[\]]+)\]/g;
4881
- var BARE_RE = /(^|[^\w@.\\])@((?:fig|tbl|sec|eq):[\w-]+|[A-Za-z0-9_](?:[\w:.#$%&+?<>~/-]*\w)?)/g;
5080
+ var BARE_RE = /(^|[^\w@.\\])@((?:fig|tbl|sec|eq):[\w-]+|[A-Za-z0-9_](?:[\w:.#$%&+?<>~/-]*\w)?)(?: \[([^\[\]@]+)\])?/g;
4882
5081
  function scanText(value, ctx) {
4883
5082
  const out = [];
4884
5083
  const pushText = (s) => {
@@ -4899,8 +5098,8 @@ function scanText(value, ctx) {
4899
5098
  last2 = start + key.length + 1;
4900
5099
  } else if (ctx.refs.has(key)) {
4901
5100
  pushText(s.slice(last2, start));
4902
- out.push(cite([{ key, suppressAuthor: false }], ctx, true));
4903
- last2 = start + key.length + 1;
5101
+ out.push(cite([{ key, locator: m[3]?.trim(), suppressAuthor: false }], ctx, true));
5102
+ last2 = start + key.length + 1 + (m[3] !== void 0 ? m[3].length + 3 : 0);
4904
5103
  }
4905
5104
  }
4906
5105
  pushText(s.slice(last2));
@@ -5246,7 +5445,7 @@ function parseDocument(source, options = {}) {
5246
5445
  const settings = resolveSettings(fm.data, sink, options);
5247
5446
  for (const n of sink) notify(n);
5248
5447
  const body = removeThinkingContent(fm.body);
5249
- const lifted = extractMath(body);
5448
+ const lifted = extractMath(unwrapArtifactsToMarkdown(body));
5250
5449
  const root = (0, import_mdast_util_from_markdown.fromMarkdown)(prepass(lifted.text), {
5251
5450
  extensions: [(0, import_micromark_extension_gfm.gfm)()],
5252
5451
  mdastExtensions: [(0, import_mdast_util_gfm.gfmFromMarkdown)()]