@ai-matrx/print 0.8.3 → 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,23 @@
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
+
3
21
  ## 0.8.3
4
22
 
5
23
  - **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,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 } : {})
@@ -3109,7 +3194,7 @@ async function documentToDocx(tree, options = {}) {
3109
3194
  default: {
3110
3195
  // East-Asian text gets a CJK face (Word and LibreOffice substitute an installed one).
3111
3196
  document: {
3112
- run: { font: { ascii: "Cambria", hAnsi: "Cambria", cs: "Cambria", eastAsia: "Yu Gothic" }, size, color: HEX.text },
3197
+ run: { font: { ascii: "Cambria", hAnsi: "Cambria", cs: "Arial", eastAsia: "Yu Gothic" }, size, sizeComplexScript: size, color: HEX.text },
3113
3198
  paragraph: { spacing: { after: 140, line: 300 } }
3114
3199
  }
3115
3200
  },
@@ -3297,10 +3382,10 @@ function renderBlockHtml(b, x) {
3297
3382
  switch (b.type) {
3298
3383
  case "heading": {
3299
3384
  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}>`;
3385
+ return `<h${b.level} id="${(0, import_html_escape.escapeHtml)(b.id)}" dir="auto">${num}${renderInlineHtml(b.children, x)}</h${b.level}>`;
3301
3386
  }
3302
3387
  case "paragraph":
3303
- return `<p>${renderInlineHtml(b.children, x)}</p>`;
3388
+ return `<p dir="auto">${renderInlineHtml(b.children, x)}</p>`;
3304
3389
  case "list": {
3305
3390
  const tag = b.ordered ? "ol" : "ul";
3306
3391
  const start = b.ordered && b.start !== 1 ? ` start="${b.start}"` : "";
@@ -3308,12 +3393,12 @@ function renderBlockHtml(b, x) {
3308
3393
  const items = b.items.map((i) => {
3309
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>`;
3310
3395
  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>`;
3396
+ return `<li dir="auto"${i.checked !== null ? ' class="doc-task"' : ""}>${box}${inner}</li>`;
3312
3397
  }).join("");
3313
3398
  return `<${tag}${start}${task ? ' class="doc-task-list"' : ""}>${items}</${tag}>`;
3314
3399
  }
3315
3400
  case "blockquote":
3316
- return `<blockquote>${renderBlocksHtml(b.children, x)}</blockquote>`;
3401
+ return `<blockquote dir="auto">${renderBlocksHtml(b.children, x)}</blockquote>`;
3317
3402
  case "code":
3318
3403
  return `<pre class="doc-code"${b.lang ? ` data-language="${(0, import_html_escape.escapeHtml)(b.lang)}"` : ""}><code>${(0, import_html_escape.escapeHtml)(
3319
3404
  b.value
@@ -3323,8 +3408,8 @@ function renderBlockHtml(b, x) {
3323
3408
  const a = b.align[i];
3324
3409
  return a ? ` style="text-align:${a}"` : "";
3325
3410
  };
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>`;
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>`;
3328
3413
  const id = b.caption?.id ? ` id="tbl:${(0, import_html_escape.escapeHtml)(b.caption.id)}"` : "";
3329
3414
  return `<div class="doc-table-wrap"${id}><table>${b.caption ? captionHtml(b.caption, "caption", x) : ""}${head}${body}</table></div>`;
3330
3415
  }
@@ -3794,7 +3879,7 @@ function breakRuns(input, width, t) {
3794
3879
  for (let guard = 0; guard < 50 && atoms.length; guard++) {
3795
3880
  const gw = atoms.reduce((n, a) => n + w(a), 0);
3796
3881
  const sw = pendingSpace && line > 0 ? w(pendingSpace) : 0;
3797
- if (line + sw + gw <= W || line === 0 && atoms.length !== 1) {
3882
+ if (line + sw + gw <= W || line === 0 && (atoms.length !== 1 || gw <= width)) {
3798
3883
  if (sw && pendingSpace) out.push(pendingSpace);
3799
3884
  out.push(...atoms);
3800
3885
  line += sw + gw;
@@ -3918,11 +4003,14 @@ function withFallbackFonts(nodes, c) {
3918
4003
  const fb = fallbackFor(cp);
3919
4004
  if (fb) {
3920
4005
  flush();
3921
- 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 });
3922
4009
  } else dropped = true;
3923
4010
  }
3924
4011
  flush();
3925
4012
  }
4013
+ for (const o of out) if (typeof o === "object") delete o.__fb;
3926
4014
  if (dropped && !c.glyphs.announced) {
3927
4015
  c.glyphs.announced = true;
3928
4016
  c.tree.notices.push({
@@ -3949,8 +4037,13 @@ function drawable(text, c) {
3949
4037
  }
3950
4038
  function bodyRuns(nodes, c, width = c.width) {
3951
4039
  const r = runs2(nodes, c);
3952
- 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));
3953
4045
  }
4046
+ var alignFor = (nodes) => isRtlParagraph(plain(nodes)) ? { alignment: "right" } : {};
3954
4047
  function runs2(nodes, c, st = {}) {
3955
4048
  const out = [];
3956
4049
  const base = (raw) => {
@@ -4142,6 +4235,7 @@ function estimateLines(text, c, size = c.s.fontSize) {
4142
4235
  function columnWidths(b, n, size, c) {
4143
4236
  const measure = (text, bold) => c.faces ? c.faces.measure(text, { bold, fontSize: size }) : text.length * size * 0.5;
4144
4237
  const pad = 10;
4238
+ const slack = 1;
4145
4239
  const min = new Array(n).fill(0);
4146
4240
  const max = new Array(n).fill(0);
4147
4241
  const rows = [[b.header, true], ...b.rows.map((r) => [r, false])];
@@ -4149,8 +4243,8 @@ function columnWidths(b, n, size, c) {
4149
4243
  for (let i = 0; i < n; i++) {
4150
4244
  const t = plain(r[i] ?? []);
4151
4245
  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);
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);
4154
4248
  }
4155
4249
  }
4156
4250
  const avail = c.width;
@@ -4214,8 +4308,11 @@ function block2(b, c) {
4214
4308
  switch (b.type) {
4215
4309
  case "heading": {
4216
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);
4217
4313
  const node = {
4218
- 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],
4219
4316
  style: `h${Math.min(6, b.level)}`,
4220
4317
  headlineLevel: b.level,
4221
4318
  id: b.id
@@ -4229,12 +4326,12 @@ function block2(b, c) {
4229
4326
  }
4230
4327
  case "paragraph": {
4231
4328
  const text = bodyRuns(b.children, c);
4232
- const p = { text, style: "p" };
4329
+ const p = { text, style: "p", ...alignFor(b.children) };
4233
4330
  return estimateLines(plain(b.children), c) <= 5 ? { stack: [p], unbreakable: true } : p;
4234
4331
  }
4235
4332
  case "list": {
4236
4333
  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 });
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 });
4238
4335
  if (it.checked !== null) return { ...taskItem(it.checked, content, c), listType: "none" };
4239
4336
  return content.length === 1 ? content[0] : { stack: content };
4240
4337
  });
@@ -4287,15 +4384,24 @@ function block2(b, c) {
4287
4384
  case "table": {
4288
4385
  const n = Math.max(b.header.length, ...b.rows.map((r) => r.length), 1);
4289
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
+ };
4290
4397
  const cell = (inl, i, head) => ({
4291
- text: runs2(inl ?? [], c),
4398
+ text: cellRuns(inl ?? [], i, head),
4292
4399
  bold: head,
4293
- alignment: b.align[i] ?? "left",
4400
+ alignment: b.align[i] ?? (isRtlParagraph(plain(inl ?? [])) ? "right" : "left"),
4294
4401
  fontSize: size,
4295
4402
  color: head ? COLOR.heading : COLOR.text
4296
4403
  });
4297
4404
  const row = (r, head) => Array.from({ length: n }, (_, i) => cell(r[i], i, head));
4298
- const widths = columnWidths(b, n, size, c);
4299
4405
  const lineH = size * 1.3;
4300
4406
  const pageH = c.pageHeight;
4301
4407
  const rowHeight = (r) => Math.max(...r.map((inl, i) => estimateLines(plain(inl), { ...c, width: (widths[i] ?? c.width) - 10 }, size) * lineH)) + 8;
@@ -4541,12 +4647,16 @@ async function loadTypography(tree, loaders = {}) {
4541
4647
  const base = await loadFaces(vfs, s.fontSize);
4542
4648
  let needEmoji = false;
4543
4649
  let needSymbols = false;
4650
+ let needArabic = false;
4651
+ let needHebrew = false;
4544
4652
  const cjk = /* @__PURE__ */ new Set();
4545
4653
  for (const ch of documentText(tree)) {
4546
4654
  const cp = ch.codePointAt(0);
4547
4655
  if (cp < 32 || cp === 8205 || cp === 65039 || base.body(cp)) continue;
4548
4656
  if (EMOJI_CP.test(ch)) needEmoji = true;
4549
- 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;
4550
4660
  else needSymbols = true;
4551
4661
  }
4552
4662
  const fallbacks = [];
@@ -4558,6 +4668,14 @@ async function loadTypography(tree, loaders = {}) {
4558
4668
  const b = await registerFace("NotoSymbols", "NotoSansMath.woff", loaders.symbols ?? defaultSymbolFontLoader, tree, "symbols");
4559
4669
  if (b) fallbacks.push({ font: "NotoSymbols", bytes: b });
4560
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
+ }
4561
4679
  if (cjk.size) {
4562
4680
  let subsets = [];
4563
4681
  try {
@@ -4573,7 +4691,7 @@ async function loadTypography(tree, loaders = {}) {
4573
4691
  if (!subsets.length) {
4574
4692
  tree.notices.push({
4575
4693
  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).",
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).",
4577
4695
  remedy: "Check the internet connection (the font comes from cdn.jsdelivr.net) and export again."
4578
4696
  });
4579
4697
  }
@@ -4601,6 +4719,12 @@ var urlLoader = (url) => async () => {
4601
4719
  var defaultEmojiFontLoader = urlLoader(NOTO_EMOJI_URL);
4602
4720
  var defaultSymbolFontLoader = urlLoader(NOTO_SYMBOLS_URL);
4603
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;
4604
4728
  function parseUnicodeRangeCss(css) {
4605
4729
  const out = [];
4606
4730
  for (const block3 of css.split("@font-face").slice(1)) {
@@ -4616,19 +4740,25 @@ function parseUnicodeRangeCss(css) {
4616
4740
  }
4617
4741
  return out;
4618
4742
  }
4619
- var defaultCjkFontLoader = async (cps) => {
4743
+ async function unicodeRangeFaces(base, cps) {
4620
4744
  if (typeof fetch !== "function" || cps.length === 0) return [];
4621
- const res = await fetch(`${NOTO_CJK_BASE}/400.css`);
4745
+ const res = await fetch(`${base}/400.css`);
4622
4746
  if (!res.ok) return [];
4623
4747
  const faces = parseUnicodeRangeCss(await res.text()).filter((f) => cps.some((cp) => f.ranges.some(([a, b]) => cp >= a && cp <= b)));
4624
4748
  const out = [];
4625
4749
  await Promise.all(
4626
4750
  faces.map(async (f) => {
4627
- const r = await fetch(`${NOTO_CJK_BASE}/files/${f.file}.woff`);
4751
+ const r = await fetch(`${base}/files/${f.file}.woff`);
4628
4752
  if (r.ok) out.push({ name: f.file, bytes: new Uint8Array(await r.arrayBuffer()) });
4629
4753
  })
4630
4754
  );
4631
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];
4632
4762
  };
4633
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;
4634
4764
  var EMOJI_CP = /\p{Extended_Pictographic}|\p{Regional_Indicator}|\u20E3/u;
@@ -4947,7 +5077,7 @@ function uniqueId(ctx, base) {
4947
5077
  return id;
4948
5078
  }
4949
5079
  var CITE_GROUP_RE = /\[(-?@[^[\]]+)\]/g;
4950
- 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;
4951
5081
  function scanText(value, ctx) {
4952
5082
  const out = [];
4953
5083
  const pushText = (s) => {
@@ -4968,8 +5098,8 @@ function scanText(value, ctx) {
4968
5098
  last2 = start + key.length + 1;
4969
5099
  } else if (ctx.refs.has(key)) {
4970
5100
  pushText(s.slice(last2, start));
4971
- out.push(cite([{ key, suppressAuthor: false }], ctx, true));
4972
- 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);
4973
5103
  }
4974
5104
  }
4975
5105
  pushText(s.slice(last2));