@ai-matrx/print 0.8.3 → 0.8.5

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,31 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.5
4
+
5
+ - **Word tables use content-aware column widths** (the same rule as the PDF,
6
+ now shared in `src/document/layout.ts`; fixed table layout). A 10-column
7
+ landscape table no longer splits its width equally.
8
+ - A `$$...$$` placeholder in prose prints as text (via content-ir's rule).
9
+ - Consumer action: none — refresh to latest.
10
+
11
+ ## 0.8.4
12
+
13
+ - **Right-to-left scripts in PDF (Arabic, Hebrew).** Lines are put in visual
14
+ order by the Unicode Bidirectional Algorithm (bidi-js; words reversed at RTL
15
+ levels, numbers kept left-to-right, brackets mirrored), RTL paragraphs,
16
+ headings, list items and cells are right-aligned, and letters are shaped by
17
+ the font (Arabic joining forms). New fallback faces Noto Sans Arabic and
18
+ Noto Sans Hebrew (`fonts.arabic` / `fonts.hebrew`). HTML/EPUB blocks carry
19
+ `dir="auto"`; Word marks RTL paragraphs `bidi` and RTL runs `rtl`.
20
+ - **Korean** in PDF through Noto Sans KR (unicode-range subsets on demand,
21
+ same `fonts.cjk` loader; Hangul → KR, other CJK → JP).
22
+ - **Table cells are hyphenated** to their own column width in PDF; a word
23
+ that fits a line whole is never hyphenated.
24
+ - **APA 7 narrative citations use "and"** (`Kim and Alvarez (2021)`),
25
+ parenthetical keep "&"; `@key [p. 4]` gives a narrative citation with a
26
+ locator (pandoc syntax).
27
+ - Consumer action: none — refresh to latest.
28
+
3
29
  ## 0.8.3
4
30
 
5
31
  - **Fixed:** an `<artifact>` envelope (how chat answers store tables and code)
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,65 +81,119 @@ __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
+
149
+ // src/document/layout.ts
150
+ var plainInline = (nodes) => nodes.map((n) => "value" in n ? n.value : "label" in n ? n.label : "tex" in n ? n.tex : "children" in n ? plainInline(n.children) : "").join("");
151
+ function contentColumnWidths(b, n, avail, measure, pad = 10) {
152
+ const min = new Array(n).fill(0);
153
+ const max = new Array(n).fill(0);
154
+ const rows = [[b.header, true], ...b.rows.map((r) => [r, false])];
155
+ for (const [r, head] of rows) {
156
+ for (let i = 0; i < n; i++) {
157
+ const t = plainInline(r[i] ?? []);
158
+ const longest = t.split(/\s+/).reduce((m, w) => Math.max(m, measure(w, head)), 0);
159
+ min[i] = Math.max(min[i], Math.min(longest, avail * 0.35) + pad);
160
+ max[i] = Math.max(max[i], measure(t, head) + pad);
161
+ }
162
+ }
163
+ const sumMax = max.reduce((a, x) => a + x, 0);
164
+ const sumMin = min.reduce((a, x) => a + x, 0);
165
+ if (sumMax <= avail) return max.map((m) => m + (avail - sumMax) / n);
166
+ if (sumMin >= avail) return min.map((m) => m / sumMin * avail);
167
+ const extra = avail - sumMin;
168
+ return min.map((m, i) => m + (max[i] - m) * (extra / (sumMax - sumMin)));
169
+ }
170
+
81
171
  // src/document/math.ts
82
172
  var import_source = require("@ai-matrx/content-ir/source");
83
173
  var MATH_OPEN = "\uE100";
84
174
  var MATH_CLOSE = "\uE101";
85
175
  var MATH_TOKEN_RE = /\uE100(\d+)\uE101/g;
86
- function codeRanges(text) {
87
- const ranges = [];
88
- for (const re of [/```[\s\S]*?(?:```|$)/g, /~~~[\s\S]*?(?:~~~|$)/g, /`[^`\n]*`/g]) {
89
- let m;
90
- while ((m = re.exec(text)) !== null) {
91
- ranges.push([m.index, m.index + m[0].length]);
92
- if (m[0].length === 0) re.lastIndex++;
93
- }
94
- }
95
- return ranges.sort((a, b) => a[0] - b[0]);
96
- }
176
+ var DELIMITERS = {
177
+ $$: { open: 2, close: 2, display: true },
178
+ "\\[": { open: 2, close: 2, display: true },
179
+ $: { open: 1, close: 1, display: false },
180
+ "\\(": { open: 2, close: 2, display: false }
181
+ };
97
182
  function extractMath(body) {
98
183
  const spans = [];
99
184
  if (!/[$\\]/.test(body)) return { text: body, spans };
100
- const code = codeRanges(body);
101
- const inCode = (i2) => code.some(([a, b]) => i2 >= a && i2 < b);
102
- const display = (0, import_source.pairDisplayMath)(body);
185
+ const islands = (0, import_source.listIslands)((0, import_source.tokenizeSource)(body)).filter((isl) => isl.complete && (isl.islandType === "math_block" || isl.islandType === "math_inline")).sort((a, b) => a.start - b.start);
103
186
  let out = "";
104
- let i = 0;
105
- const token = (tex, isDisplay) => {
106
- spans.push({ tex: tex.trim(), display: isDisplay });
107
- return `${MATH_OPEN}${spans.length - 1}${MATH_CLOSE}`;
108
- };
109
- while (i < body.length) {
110
- const ch = body[i];
111
- if (ch === "$" && !inCode(i)) {
112
- const close = display.get(i);
113
- if (close !== void 0) {
114
- out += token(body.slice(i + 2, close), true);
115
- i = close + 2;
116
- continue;
117
- }
118
- if (body[i + 1] !== "$" && body[i - 1] !== "$" && body[i - 1] !== "\\") {
119
- const end = (0, import_source.singleDollarMathEnd)(body, i);
120
- if (end > 0) {
121
- out += token(body.slice(i + 1, end - 1), false);
122
- i = end;
123
- continue;
124
- }
125
- }
126
- }
127
- if (ch === "\\" && !inCode(i) && (body[i + 1] === "[" || body[i + 1] === "(")) {
128
- const closer = body[i + 1] === "[" ? "\\]" : "\\)";
129
- const end = body.indexOf(closer, i + 2);
130
- if (end > i + 2 && end - i < 2e3 && (body[i + 1] === "[" || !body.slice(i, end).includes("\n\n"))) {
131
- out += token(body.slice(i + 2, end), body[i + 1] === "[");
132
- i = end + 2;
133
- continue;
134
- }
135
- }
136
- out += ch;
137
- i++;
187
+ let at = 0;
188
+ for (const isl of islands) {
189
+ const d = DELIMITERS[String(isl.meta.delimiter ?? "")];
190
+ if (!d || isl.start < at) continue;
191
+ out += body.slice(at, isl.start);
192
+ spans.push({ tex: body.slice(isl.start + d.open, isl.end - d.close).trim(), display: d.display });
193
+ out += `${MATH_OPEN}${spans.length - 1}${MATH_CLOSE}`;
194
+ at = isl.end;
138
195
  }
139
- return { text: out, spans };
196
+ return { text: out + body.slice(at), spans };
140
197
  }
141
198
  var mathKey = (tex, display) => `${display ? "D" : "I"}:${tex}`;
142
199
  var MATHJAX = /* @__PURE__ */ Symbol.for("ai-matrx.print.document.mathjax");
@@ -2125,6 +2182,8 @@ var CitationEngine = class {
2125
2182
  /** A note style: every citation becomes a numbered note. */
2126
2183
  isNote;
2127
2184
  numeric;
2185
+ /** APA 7 §8.11: narrative citations join authors with "and", not "&". */
2186
+ apaNarrativeAnd;
2128
2187
  constructor(styleXml, localeXml, items, lang) {
2129
2188
  const byId = new Map(items.map((i) => [i.id, toCslJson(i)]));
2130
2189
  const sys = {
@@ -2135,6 +2194,7 @@ var CitationEngine = class {
2135
2194
  this.engine = new Engine(sys, styleXml, cslLocaleTag(lang), true);
2136
2195
  this.engine.setOutputFormat("html");
2137
2196
  this.isNote = this.engine.opt.xclass === "note";
2197
+ 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
2198
  this.numeric = /citation-number/.test(styleXml.slice(styleXml.indexOf("<citation"), styleXml.indexOf("</citation>")));
2139
2199
  }
2140
2200
  /** Cite a group; `noteIndex` is the note number for note styles (0 = in text). */
@@ -2171,7 +2231,8 @@ var CitationEngine = class {
2171
2231
  if (opts.narrative && refs.length === 1 && !this.isNote && !this.numeric) {
2172
2232
  const author = make(items);
2173
2233
  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})`}` : ""}`;
2234
+ const who = () => this.apaNarrativeAnd ? author.label.replace(/,? & /g, (m) => m.startsWith(",") ? ", and " : " and ") : author.label;
2235
+ const join = () => `${who()}${rest.label ? ` ${/^\(/.test(rest.label) ? rest.label : `(${rest.label})`}` : ""}`;
2175
2236
  const listeners = [];
2176
2237
  author.listeners.push(() => listeners.forEach((l) => l(join())));
2177
2238
  rest.listeners.push(() => listeners.forEach((l) => l(join())));
@@ -2650,7 +2711,7 @@ function runs(nodes, c, st = {}) {
2650
2711
  for (const n of nodes) {
2651
2712
  switch (n.type) {
2652
2713
  case "text":
2653
- out.push(tr(n.value));
2714
+ out.push(RTL_RE.test(n.value) ? tr(n.value, { rightToLeft: true }) : tr(n.value));
2654
2715
  break;
2655
2716
  case "strong":
2656
2717
  out.push(...runs(n.children, c, { ...st, bold: true }));
@@ -2802,6 +2863,10 @@ function numberingRef(c, ordered, start) {
2802
2863
  }
2803
2864
  return ref;
2804
2865
  }
2866
+ function bidiPara(nodes) {
2867
+ const text = nodes.map((n) => "value" in n ? n.value : "").join("");
2868
+ return isRtlParagraph(text) ? { bidirectional: true } : {};
2869
+ }
2805
2870
  function blocks(list, c, o = {}) {
2806
2871
  const out = [];
2807
2872
  for (const b of list) out.push(...block(b, c, o));
@@ -2826,6 +2891,7 @@ function block(b, c, o) {
2826
2891
  const kids = [...b.number ? [new d.TextRun({ text: `${b.number} `, color: HEX.muted })] : [], ...runs(b.children, c)];
2827
2892
  return [
2828
2893
  new d.Paragraph({
2894
+ ...bidiPara(b.children),
2829
2895
  heading,
2830
2896
  keepNext: true,
2831
2897
  keepLines: true,
@@ -2835,7 +2901,7 @@ function block(b, c, o) {
2835
2901
  ];
2836
2902
  }
2837
2903
  case "paragraph":
2838
- return [para(c, o, { children: runs(b.children, c), widowControl: true })];
2904
+ return [para(c, o, { children: runs(b.children, c), widowControl: true, ...bidiPara(b.children) })];
2839
2905
  case "list": {
2840
2906
  const level = o.listLevel ?? 0;
2841
2907
  const ref = numberingRef(c, b.ordered, b.start);
@@ -2852,7 +2918,14 @@ function block(b, c, o) {
2852
2918
  })
2853
2919
  );
2854
2920
  } else {
2855
- out.push(new d.Paragraph({ numbering: { reference: ref, level, instance }, children: firstRuns, widowControl: true }));
2921
+ out.push(
2922
+ new d.Paragraph({
2923
+ numbering: { reference: ref, level, instance },
2924
+ children: firstRuns,
2925
+ widowControl: true,
2926
+ ...first?.type === "paragraph" ? bidiPara(first.children) : {}
2927
+ })
2928
+ );
2856
2929
  }
2857
2930
  const tail = first && first.type !== "paragraph" ? [first, ...rest] : rest;
2858
2931
  for (const child of tail) {
@@ -2880,6 +2953,8 @@ function block(b, c, o) {
2880
2953
  case "table": {
2881
2954
  const n = Math.max(b.header.length, ...b.rows.map((r) => r.length), 1);
2882
2955
  const keep = b.rows.length <= 20;
2956
+ const cellPt = c.s.fontSize;
2957
+ const colTw = contentColumnWidths(b, n, c.width, (t, bold) => t.length * cellPt * (bold ? 0.55 : 0.5), 10).map((w) => Math.round(w * TW));
2883
2958
  const alignOf = (i) => b.align[i] === "center" ? d.AlignmentType.CENTER : b.align[i] === "right" ? d.AlignmentType.RIGHT : d.AlignmentType.LEFT;
2884
2959
  const border = { style: d.BorderStyle.SINGLE, size: 4, color: "E5E7EB" };
2885
2960
  const none = { style: d.BorderStyle.NONE, size: 0, color: "FFFFFF" };
@@ -2889,11 +2964,13 @@ function block(b, c, o) {
2889
2964
  children: Array.from(
2890
2965
  { length: n },
2891
2966
  (_, i) => new d.TableCell({
2967
+ width: { size: colTw[i] ?? 0, type: d.WidthType.DXA },
2892
2968
  ...head ? { shading: { type: d.ShadingType.CLEAR, fill: "F3F4F6", color: "auto" } } : {},
2893
2969
  margins: { top: 60, bottom: 60, left: 100, right: 100 },
2894
2970
  borders: { top: none, left: none, right: none, bottom: head ? { ...border, size: 10, color: HEX.heading } : border },
2895
2971
  children: [
2896
2972
  new d.Paragraph({
2973
+ ...bidiPara(cells[i] ?? []),
2897
2974
  alignment: alignOf(i),
2898
2975
  keepNext: keep && !last,
2899
2976
  children: runs(cells[i] ?? [], c, head ? { bold: true } : {})
@@ -2906,7 +2983,9 @@ function block(b, c, o) {
2906
2983
  if (b.caption) out.push(captionParagraph(b.caption, c, true));
2907
2984
  out.push(
2908
2985
  new d.Table({
2909
- width: { size: 100, type: d.WidthType.PERCENTAGE },
2986
+ width: { size: Math.round(c.width * TW), type: d.WidthType.DXA },
2987
+ columnWidths: colTw,
2988
+ layout: d.TableLayoutType.FIXED,
2910
2989
  rows: [row(b.header, true, b.rows.length === 0), ...b.rows.map((r, i) => row(r, false, i === b.rows.length - 1))]
2911
2990
  })
2912
2991
  );
@@ -3109,7 +3188,7 @@ async function documentToDocx(tree, options = {}) {
3109
3188
  default: {
3110
3189
  // East-Asian text gets a CJK face (Word and LibreOffice substitute an installed one).
3111
3190
  document: {
3112
- run: { font: { ascii: "Cambria", hAnsi: "Cambria", cs: "Cambria", eastAsia: "Yu Gothic" }, size, color: HEX.text },
3191
+ run: { font: { ascii: "Cambria", hAnsi: "Cambria", cs: "Arial", eastAsia: "Yu Gothic" }, size, sizeComplexScript: size, color: HEX.text },
3113
3192
  paragraph: { spacing: { after: 140, line: 300 } }
3114
3193
  }
3115
3194
  },
@@ -3297,10 +3376,10 @@ function renderBlockHtml(b, x) {
3297
3376
  switch (b.type) {
3298
3377
  case "heading": {
3299
3378
  const num = b.number ? `<span class="doc-heading-number">${b.number}</span> ` : "";
3300
- return `<h${b.level} id="${(0, import_html_escape.escapeHtml)(b.id)}">${num}${renderInlineHtml(b.children, x)}</h${b.level}>`;
3379
+ return `<h${b.level} id="${(0, import_html_escape.escapeHtml)(b.id)}" dir="auto">${num}${renderInlineHtml(b.children, x)}</h${b.level}>`;
3301
3380
  }
3302
3381
  case "paragraph":
3303
- return `<p>${renderInlineHtml(b.children, x)}</p>`;
3382
+ return `<p dir="auto">${renderInlineHtml(b.children, x)}</p>`;
3304
3383
  case "list": {
3305
3384
  const tag = b.ordered ? "ol" : "ul";
3306
3385
  const start = b.ordered && b.start !== 1 ? ` start="${b.start}"` : "";
@@ -3308,12 +3387,12 @@ function renderBlockHtml(b, x) {
3308
3387
  const items = b.items.map((i) => {
3309
3388
  const box = i.checked === null ? "" : `<span class="doc-task-box${i.checked ? " is-checked" : ""}" role="img" aria-label="${i.checked ? "done" : "not done"}"></span>`;
3310
3389
  const inner = i.children.length === 1 && i.children[0]?.type === "paragraph" ? renderInlineHtml(i.children[0].children, x) : renderBlocksHtml(i.children, x);
3311
- return `<li${i.checked !== null ? ' class="doc-task"' : ""}>${box}${inner}</li>`;
3390
+ return `<li dir="auto"${i.checked !== null ? ' class="doc-task"' : ""}>${box}${inner}</li>`;
3312
3391
  }).join("");
3313
3392
  return `<${tag}${start}${task ? ' class="doc-task-list"' : ""}>${items}</${tag}>`;
3314
3393
  }
3315
3394
  case "blockquote":
3316
- return `<blockquote>${renderBlocksHtml(b.children, x)}</blockquote>`;
3395
+ return `<blockquote dir="auto">${renderBlocksHtml(b.children, x)}</blockquote>`;
3317
3396
  case "code":
3318
3397
  return `<pre class="doc-code"${b.lang ? ` data-language="${(0, import_html_escape.escapeHtml)(b.lang)}"` : ""}><code>${(0, import_html_escape.escapeHtml)(
3319
3398
  b.value
@@ -3323,8 +3402,8 @@ function renderBlockHtml(b, x) {
3323
3402
  const a = b.align[i];
3324
3403
  return a ? ` style="text-align:${a}"` : "";
3325
3404
  };
3326
- const head = `<thead><tr>${b.header.map((c, i) => `<th${al(i)}>${renderInlineHtml(c, x)}</th>`).join("")}</tr></thead>`;
3327
- const body = `<tbody>${b.rows.map((r) => `<tr>${r.map((c, i) => `<td${al(i)}>${renderInlineHtml(c, x)}</td>`).join("")}</tr>`).join("")}</tbody>`;
3405
+ const head = `<thead><tr>${b.header.map((c, i) => `<th dir="auto"${al(i)}>${renderInlineHtml(c, x)}</th>`).join("")}</tr></thead>`;
3406
+ 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>`;
3328
3407
  const id = b.caption?.id ? ` id="tbl:${(0, import_html_escape.escapeHtml)(b.caption.id)}"` : "";
3329
3408
  return `<div class="doc-table-wrap"${id}><table>${b.caption ? captionHtml(b.caption, "caption", x) : ""}${head}${body}</table></div>`;
3330
3409
  }
@@ -3794,7 +3873,7 @@ function breakRuns(input, width, t) {
3794
3873
  for (let guard = 0; guard < 50 && atoms.length; guard++) {
3795
3874
  const gw = atoms.reduce((n, a) => n + w(a), 0);
3796
3875
  const sw = pendingSpace && line > 0 ? w(pendingSpace) : 0;
3797
- if (line + sw + gw <= W || line === 0 && atoms.length !== 1) {
3876
+ if (line + sw + gw <= W || line === 0 && (atoms.length !== 1 || gw <= width)) {
3798
3877
  if (sw && pendingSpace) out.push(pendingSpace);
3799
3878
  out.push(...atoms);
3800
3879
  line += sw + gw;
@@ -3918,11 +3997,14 @@ function withFallbackFonts(nodes, c) {
3918
3997
  const fb = fallbackFor(cp);
3919
3998
  if (fb) {
3920
3999
  flush();
3921
- out.push({ ...n, text: ch, font: fb, bold: false, italics: false });
4000
+ const prev = out[out.length - 1];
4001
+ if (prev && typeof prev === "object" && prev.font === fb && prev.__fb) prev.text = `${prev.text}${ch}`;
4002
+ else out.push({ ...n, text: ch, font: fb, bold: false, italics: false, __fb: true });
3922
4003
  } else dropped = true;
3923
4004
  }
3924
4005
  flush();
3925
4006
  }
4007
+ for (const o of out) if (typeof o === "object") delete o.__fb;
3926
4008
  if (dropped && !c.glyphs.announced) {
3927
4009
  c.glyphs.announced = true;
3928
4010
  c.tree.notices.push({
@@ -3949,8 +4031,13 @@ function drawable(text, c) {
3949
4031
  }
3950
4032
  function bodyRuns(nodes, c, width = c.width) {
3951
4033
  const r = runs2(nodes, c);
3952
- return c.typo ? breakRuns(r, width, c.typo) : r;
4034
+ const text = plain(nodes);
4035
+ if (!RTL_RE.test(text)) return c.typo ? breakRuns(r, width, c.typo) : r;
4036
+ const typo = c.typo ?? (c.faces ? { split: (w) => [w], measure: c.faces.measure } : void 0);
4037
+ const lines = typo ? breakRuns(r, width, typo) : r;
4038
+ return visualOrder(lines, isRtlParagraph(text));
3953
4039
  }
4040
+ var alignFor = (nodes) => isRtlParagraph(plain(nodes)) ? { alignment: "right" } : {};
3954
4041
  function runs2(nodes, c, st = {}) {
3955
4042
  const out = [];
3956
4043
  const base = (raw) => {
@@ -4141,25 +4228,7 @@ function estimateLines(text, c, size = c.s.fontSize) {
4141
4228
  }
4142
4229
  function columnWidths(b, n, size, c) {
4143
4230
  const measure = (text, bold) => c.faces ? c.faces.measure(text, { bold, fontSize: size }) : text.length * size * 0.5;
4144
- const pad = 10;
4145
- const min = new Array(n).fill(0);
4146
- const max = new Array(n).fill(0);
4147
- const rows = [[b.header, true], ...b.rows.map((r) => [r, false])];
4148
- for (const [r, head] of rows) {
4149
- for (let i = 0; i < n; i++) {
4150
- const t = plain(r[i] ?? []);
4151
- const longest = t.split(/\s+/).reduce((m, w) => Math.max(m, measure(w, head)), 0);
4152
- min[i] = Math.max(min[i], Math.min(longest, c.width * 0.35) + pad);
4153
- max[i] = Math.max(max[i], measure(t, head) + pad);
4154
- }
4155
- }
4156
- const avail = c.width;
4157
- const sumMax = max.reduce((a, x) => a + x, 0);
4158
- const sumMin = min.reduce((a, x) => a + x, 0);
4159
- if (sumMax <= avail) return max.map((m) => m + (avail - sumMax) / n);
4160
- if (sumMin >= avail) return min.map((m) => m / sumMin * avail);
4161
- const extra = avail - sumMin;
4162
- return min.map((m, i) => m + (max[i] - m) * (extra / (sumMax - sumMin)));
4231
+ return contentColumnWidths(b, n, c.width, measure);
4163
4232
  }
4164
4233
  var plain = (nodes) => nodes.map((n) => "value" in n ? n.value : "label" in n ? n.label : "children" in n ? plain(n.children) : "").join("");
4165
4234
  function taskItem(checked, content, c) {
@@ -4214,8 +4283,11 @@ function block2(b, c) {
4214
4283
  switch (b.type) {
4215
4284
  case "heading": {
4216
4285
  const rel = Math.min(6, b.level - c.minLevel + 1);
4286
+ const rtlHeading = RTL_RE.test(b.plain);
4287
+ const headRuns = rtlHeading ? visualOrder(runs2(b.children, c), isRtlParagraph(b.plain)) : runs2(b.children, c);
4217
4288
  const node = {
4218
- text: [...b.number ? [{ text: `${b.number} `, color: COLOR.muted }] : [], ...runs2(b.children, c)],
4289
+ ...rtlHeading ? alignFor(b.children) : {},
4290
+ text: [...b.number ? [{ text: `${b.number} `, color: COLOR.muted }] : [], ...headRuns],
4219
4291
  style: `h${Math.min(6, b.level)}`,
4220
4292
  headlineLevel: b.level,
4221
4293
  id: b.id
@@ -4229,12 +4301,12 @@ function block2(b, c) {
4229
4301
  }
4230
4302
  case "paragraph": {
4231
4303
  const text = bodyRuns(b.children, c);
4232
- const p = { text, style: "p" };
4304
+ const p = { text, style: "p", ...alignFor(b.children) };
4233
4305
  return estimateLines(plain(b.children), c) <= 5 ? { stack: [p], unbreakable: true } : p;
4234
4306
  }
4235
4307
  case "list": {
4236
4308
  const items = b.items.map((it) => {
4237
- 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 });
4309
+ 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 });
4238
4310
  if (it.checked !== null) return { ...taskItem(it.checked, content, c), listType: "none" };
4239
4311
  return content.length === 1 ? content[0] : { stack: content };
4240
4312
  });
@@ -4287,15 +4359,24 @@ function block2(b, c) {
4287
4359
  case "table": {
4288
4360
  const n = Math.max(b.header.length, ...b.rows.map((r) => r.length), 1);
4289
4361
  const size = c.s.fontSize * 0.92;
4362
+ const widths = columnWidths(b, n, size, c);
4363
+ const cellRuns = (inl, i, head) => {
4364
+ const r = runs2(inl, c).map((x) => typeof x === "object" ? { ...x, fontSize: x.fontSize ?? size, ...head ? { bold: true } : {} } : x);
4365
+ const w = (widths[i] ?? c.width) - 10;
4366
+ if (RTL_RE.test(plain(inl))) {
4367
+ const typo = c.typo ?? (c.faces ? { split: (x) => [x], measure: c.faces.measure } : void 0);
4368
+ return visualOrder(typo ? breakRuns(r, w, typo) : r, isRtlParagraph(plain(inl)));
4369
+ }
4370
+ return c.typo && !head ? breakRuns(r, w, c.typo) : r;
4371
+ };
4290
4372
  const cell = (inl, i, head) => ({
4291
- text: runs2(inl ?? [], c),
4373
+ text: cellRuns(inl ?? [], i, head),
4292
4374
  bold: head,
4293
- alignment: b.align[i] ?? "left",
4375
+ alignment: b.align[i] ?? (isRtlParagraph(plain(inl ?? [])) ? "right" : "left"),
4294
4376
  fontSize: size,
4295
4377
  color: head ? COLOR.heading : COLOR.text
4296
4378
  });
4297
4379
  const row = (r, head) => Array.from({ length: n }, (_, i) => cell(r[i], i, head));
4298
- const widths = columnWidths(b, n, size, c);
4299
4380
  const lineH = size * 1.3;
4300
4381
  const pageH = c.pageHeight;
4301
4382
  const rowHeight = (r) => Math.max(...r.map((inl, i) => estimateLines(plain(inl), { ...c, width: (widths[i] ?? c.width) - 10 }, size) * lineH)) + 8;
@@ -4541,12 +4622,16 @@ async function loadTypography(tree, loaders = {}) {
4541
4622
  const base = await loadFaces(vfs, s.fontSize);
4542
4623
  let needEmoji = false;
4543
4624
  let needSymbols = false;
4625
+ let needArabic = false;
4626
+ let needHebrew = false;
4544
4627
  const cjk = /* @__PURE__ */ new Set();
4545
4628
  for (const ch of documentText(tree)) {
4546
4629
  const cp = ch.codePointAt(0);
4547
4630
  if (cp < 32 || cp === 8205 || cp === 65039 || base.body(cp)) continue;
4548
4631
  if (EMOJI_CP.test(ch)) needEmoji = true;
4549
- else if (isCjk(cp)) cjk.add(cp);
4632
+ else if (isCjk(cp) || isHangul(cp)) cjk.add(cp);
4633
+ else if (isArabic(cp)) needArabic = true;
4634
+ else if (isHebrew(cp)) needHebrew = true;
4550
4635
  else needSymbols = true;
4551
4636
  }
4552
4637
  const fallbacks = [];
@@ -4558,6 +4643,14 @@ async function loadTypography(tree, loaders = {}) {
4558
4643
  const b = await registerFace("NotoSymbols", "NotoSansMath.woff", loaders.symbols ?? defaultSymbolFontLoader, tree, "symbols");
4559
4644
  if (b) fallbacks.push({ font: "NotoSymbols", bytes: b });
4560
4645
  }
4646
+ if (needArabic) {
4647
+ const b = await registerFace("NotoArabic", "NotoSansArabic.woff", loaders.arabic ?? urlLoader(NOTO_ARABIC_URL), tree, "Arabic");
4648
+ if (b) fallbacks.push({ font: "NotoArabic", bytes: b });
4649
+ }
4650
+ if (needHebrew) {
4651
+ const b = await registerFace("NotoHebrew", "NotoSansHebrew.woff", loaders.hebrew ?? urlLoader(NOTO_HEBREW_URL), tree, "Hebrew");
4652
+ if (b) fallbacks.push({ font: "NotoHebrew", bytes: b });
4653
+ }
4561
4654
  if (cjk.size) {
4562
4655
  let subsets = [];
4563
4656
  try {
@@ -4573,7 +4666,7 @@ async function loadTypography(tree, loaders = {}) {
4573
4666
  if (!subsets.length) {
4574
4667
  tree.notices.push({
4575
4668
  code: "glyph-substituted",
4576
- message: "The Japanese / Chinese font could not be loaded, so those characters were left out of the PDF (Word, EPUB and HTML keep them).",
4669
+ 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).",
4577
4670
  remedy: "Check the internet connection (the font comes from cdn.jsdelivr.net) and export again."
4578
4671
  });
4579
4672
  }
@@ -4601,6 +4694,12 @@ var urlLoader = (url) => async () => {
4601
4694
  var defaultEmojiFontLoader = urlLoader(NOTO_EMOJI_URL);
4602
4695
  var defaultSymbolFontLoader = urlLoader(NOTO_SYMBOLS_URL);
4603
4696
  var NOTO_CJK_BASE = "https://cdn.jsdelivr.net/npm/@fontsource/noto-sans-jp@5.3.0";
4697
+ var NOTO_KR_BASE = "https://cdn.jsdelivr.net/npm/@fontsource/noto-sans-kr@5.3.0";
4698
+ var NOTO_ARABIC_URL = "https://cdn.jsdelivr.net/npm/@fontsource/noto-sans-arabic@5.3.0/files/noto-sans-arabic-arabic-400-normal.woff";
4699
+ var NOTO_HEBREW_URL = "https://cdn.jsdelivr.net/npm/@fontsource/noto-sans-hebrew@5.3.0/files/noto-sans-hebrew-hebrew-400-normal.woff";
4700
+ var isHangul = (cp) => cp >= 44032 && cp <= 55215 || cp >= 4352 && cp <= 4607 || cp >= 12592 && cp <= 12687;
4701
+ var isArabic = (cp) => cp >= 1536 && cp <= 1791 || cp >= 1872 && cp <= 1919 || cp >= 2208 && cp <= 2303 || cp >= 64336 && cp <= 65023 || cp >= 65136 && cp <= 65279;
4702
+ var isHebrew = (cp) => cp >= 1424 && cp <= 1535 || cp >= 64285 && cp <= 64335;
4604
4703
  function parseUnicodeRangeCss(css) {
4605
4704
  const out = [];
4606
4705
  for (const block3 of css.split("@font-face").slice(1)) {
@@ -4616,19 +4715,25 @@ function parseUnicodeRangeCss(css) {
4616
4715
  }
4617
4716
  return out;
4618
4717
  }
4619
- var defaultCjkFontLoader = async (cps) => {
4718
+ async function unicodeRangeFaces(base, cps) {
4620
4719
  if (typeof fetch !== "function" || cps.length === 0) return [];
4621
- const res = await fetch(`${NOTO_CJK_BASE}/400.css`);
4720
+ const res = await fetch(`${base}/400.css`);
4622
4721
  if (!res.ok) return [];
4623
4722
  const faces = parseUnicodeRangeCss(await res.text()).filter((f) => cps.some((cp) => f.ranges.some(([a, b]) => cp >= a && cp <= b)));
4624
4723
  const out = [];
4625
4724
  await Promise.all(
4626
4725
  faces.map(async (f) => {
4627
- const r = await fetch(`${NOTO_CJK_BASE}/files/${f.file}.woff`);
4726
+ const r = await fetch(`${base}/files/${f.file}.woff`);
4628
4727
  if (r.ok) out.push({ name: f.file, bytes: new Uint8Array(await r.arrayBuffer()) });
4629
4728
  })
4630
4729
  );
4631
4730
  return out;
4731
+ }
4732
+ var defaultCjkFontLoader = async (cps) => {
4733
+ const kr = cps.filter(isHangul);
4734
+ const jp = cps.filter((cp) => !isHangul(cp));
4735
+ const [a, b] = await Promise.all([unicodeRangeFaces(NOTO_KR_BASE, kr), unicodeRangeFaces(NOTO_CJK_BASE, jp)]);
4736
+ return [...a, ...b];
4632
4737
  };
4633
4738
  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;
4634
4739
  var EMOJI_CP = /\p{Extended_Pictographic}|\p{Regional_Indicator}|\u20E3/u;
@@ -4947,7 +5052,7 @@ function uniqueId(ctx, base) {
4947
5052
  return id;
4948
5053
  }
4949
5054
  var CITE_GROUP_RE = /\[(-?@[^[\]]+)\]/g;
4950
- var BARE_RE = /(^|[^\w@.\\])@((?:fig|tbl|sec|eq):[\w-]+|[A-Za-z0-9_](?:[\w:.#$%&+?<>~/-]*\w)?)/g;
5055
+ var BARE_RE = /(^|[^\w@.\\])@((?:fig|tbl|sec|eq):[\w-]+|[A-Za-z0-9_](?:[\w:.#$%&+?<>~/-]*\w)?)(?: \[([^\[\]@]+)\])?/g;
4951
5056
  function scanText(value, ctx) {
4952
5057
  const out = [];
4953
5058
  const pushText = (s) => {
@@ -4968,8 +5073,8 @@ function scanText(value, ctx) {
4968
5073
  last2 = start + key.length + 1;
4969
5074
  } else if (ctx.refs.has(key)) {
4970
5075
  pushText(s.slice(last2, start));
4971
- out.push(cite([{ key, suppressAuthor: false }], ctx, true));
4972
- last2 = start + key.length + 1;
5076
+ out.push(cite([{ key, locator: m[3]?.trim(), suppressAuthor: false }], ctx, true));
5077
+ last2 = start + key.length + 1 + (m[3] !== void 0 ? m[3].length + 3 : 0);
4973
5078
  }
4974
5079
  }
4975
5080
  pushText(s.slice(last2));