@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.
@@ -322,14 +322,22 @@ interface FallbackFontLoaders {
322
322
  emoji?: FontLoader | undefined;
323
323
  symbols?: FontLoader | undefined;
324
324
  cjk?: CjkFontLoader | undefined;
325
+ arabic?: FontLoader | undefined;
326
+ hebrew?: FontLoader | undefined;
325
327
  }
326
328
  /** Noto Sans JP (SIL OFL) from fontsource: 124 unicode-range subsets; only the needed ones load. */
327
329
  declare const NOTO_CJK_BASE = "https://cdn.jsdelivr.net/npm/@fontsource/noto-sans-jp@5.3.0";
330
+ /** Noto Sans KR (SIL OFL) — Hangul, same unicode-range scheme. */
331
+ declare const NOTO_KR_BASE = "https://cdn.jsdelivr.net/npm/@fontsource/noto-sans-kr@5.3.0";
332
+ /** Noto Sans Arabic / Hebrew (SIL OFL) — one script subset each. */
333
+ declare const NOTO_ARABIC_URL = "https://cdn.jsdelivr.net/npm/@fontsource/noto-sans-arabic@5.3.0/files/noto-sans-arabic-arabic-400-normal.woff";
334
+ declare const NOTO_HEBREW_URL = "https://cdn.jsdelivr.net/npm/@fontsource/noto-sans-hebrew@5.3.0/files/noto-sans-hebrew-hebrew-400-normal.woff";
328
335
  /** Parse a fontsource stylesheet: each face's file stem and its unicode ranges. */
329
336
  declare function parseUnicodeRangeCss(css: string): Array<{
330
337
  file: string;
331
338
  ranges: Array<[number, number]>;
332
339
  }>;
340
+ /** Hangul from Noto Sans KR, every other CJK character from Noto Sans JP. */
333
341
  declare const defaultCjkFontLoader: CjkFontLoader;
334
342
  /**
335
343
  * Words pdfmake did not put on a page: a text node with no position was never
@@ -416,6 +424,8 @@ declare class CitationEngine {
416
424
  /** A note style: every citation becomes a numbered note. */
417
425
  readonly isNote: boolean;
418
426
  private readonly numeric;
427
+ /** APA 7 §8.11: narrative citations join authors with "and", not "&". */
428
+ private readonly apaNarrativeAnd;
419
429
  constructor(styleXml: string, localeXml: string, items: CslItem[], lang: string);
420
430
  /** Cite a group; `noteIndex` is the note number for note styles (0 = in text). */
421
431
  cite(refs: CiteRef[], opts?: {
@@ -671,4 +681,4 @@ declare function exportDocument(source: string, format: DocumentFormat, options?
671
681
  /** Browser helper: save an export through a download link. */
672
682
  declare function downloadDocumentExport(exp: DocumentExport): void;
673
683
 
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 };
684
+ 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_ARABIC_URL, NOTO_CJK_BASE, NOTO_EMOJI_URL, NOTO_HEBREW_URL, NOTO_KR_BASE, 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 };
@@ -322,14 +322,22 @@ interface FallbackFontLoaders {
322
322
  emoji?: FontLoader | undefined;
323
323
  symbols?: FontLoader | undefined;
324
324
  cjk?: CjkFontLoader | undefined;
325
+ arabic?: FontLoader | undefined;
326
+ hebrew?: FontLoader | undefined;
325
327
  }
326
328
  /** Noto Sans JP (SIL OFL) from fontsource: 124 unicode-range subsets; only the needed ones load. */
327
329
  declare const NOTO_CJK_BASE = "https://cdn.jsdelivr.net/npm/@fontsource/noto-sans-jp@5.3.0";
330
+ /** Noto Sans KR (SIL OFL) — Hangul, same unicode-range scheme. */
331
+ declare const NOTO_KR_BASE = "https://cdn.jsdelivr.net/npm/@fontsource/noto-sans-kr@5.3.0";
332
+ /** Noto Sans Arabic / Hebrew (SIL OFL) — one script subset each. */
333
+ declare const NOTO_ARABIC_URL = "https://cdn.jsdelivr.net/npm/@fontsource/noto-sans-arabic@5.3.0/files/noto-sans-arabic-arabic-400-normal.woff";
334
+ declare const NOTO_HEBREW_URL = "https://cdn.jsdelivr.net/npm/@fontsource/noto-sans-hebrew@5.3.0/files/noto-sans-hebrew-hebrew-400-normal.woff";
328
335
  /** Parse a fontsource stylesheet: each face's file stem and its unicode ranges. */
329
336
  declare function parseUnicodeRangeCss(css: string): Array<{
330
337
  file: string;
331
338
  ranges: Array<[number, number]>;
332
339
  }>;
340
+ /** Hangul from Noto Sans KR, every other CJK character from Noto Sans JP. */
333
341
  declare const defaultCjkFontLoader: CjkFontLoader;
334
342
  /**
335
343
  * Words pdfmake did not put on a page: a text node with no position was never
@@ -416,6 +424,8 @@ declare class CitationEngine {
416
424
  /** A note style: every citation becomes a numbered note. */
417
425
  readonly isNote: boolean;
418
426
  private readonly numeric;
427
+ /** APA 7 §8.11: narrative citations join authors with "and", not "&". */
428
+ private readonly apaNarrativeAnd;
419
429
  constructor(styleXml: string, localeXml: string, items: CslItem[], lang: string);
420
430
  /** Cite a group; `noteIndex` is the note number for note styles (0 = in text). */
421
431
  cite(refs: CiteRef[], opts?: {
@@ -671,4 +681,4 @@ declare function exportDocument(source: string, format: DocumentFormat, options?
671
681
  /** Browser helper: save an export through a download link. */
672
682
  declare function downloadDocumentExport(exp: DocumentExport): void;
673
683
 
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 };
684
+ 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_ARABIC_URL, NOTO_CJK_BASE, NOTO_EMOJI_URL, NOTO_HEBREW_URL, NOTO_KR_BASE, 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
@@ -1,3 +1,68 @@
1
+ // src/document/bidi.ts
2
+ import bidiFactory from "bidi-js";
3
+ var BIDI = /* @__PURE__ */ Symbol.for("ai-matrx.print.document.bidi");
4
+ function bidi() {
5
+ const g = globalThis;
6
+ if (!g[BIDI]) g[BIDI] = bidiFactory();
7
+ return g[BIDI];
8
+ }
9
+ var RTL_RE = /[֐-ࣿיִ-﷿ﹰ-]/;
10
+ function isRtlParagraph(text) {
11
+ if (!RTL_RE.test(text)) return false;
12
+ return (bidi().getEmbeddingLevels(text).paragraphs[0]?.level ?? 0) % 2 === 1;
13
+ }
14
+ function visualOrder(input, rtl) {
15
+ const b = bidi();
16
+ const runs3 = input.map((r) => typeof r === "string" ? { text: r } : { ...r, text: String(r.text ?? "") });
17
+ const lines = [[]];
18
+ runs3.forEach((r, i) => {
19
+ for (const m of r.text.matchAll(/\n|[^\S\n]+|[\u0590-\u08FF\uFB1D-\uFDFF\uFE70-\uFEFF]+|[^\s\u0590-\u08FF\uFB1D-\uFDFF\uFE70-\uFEFF]+/g)) {
20
+ if (m[0] === "\n") lines.push([]);
21
+ else lines[lines.length - 1].push({ run: i, text: m[0] });
22
+ }
23
+ });
24
+ const out = [];
25
+ lines.forEach((line, li) => {
26
+ if (li > 0) out.push({ ...runs3[0], text: "\n" });
27
+ const text = line.map((t) => t.text).join("");
28
+ if (!text) return;
29
+ const { levels } = b.getEmbeddingLevels(text, rtl ? "rtl" : "ltr");
30
+ let at = 0;
31
+ const tokens = line.map((t) => {
32
+ const level = levels[at] ?? 0;
33
+ let s = t.text;
34
+ if (level % 2 === 1) {
35
+ s = [...s].map((ch) => b.getMirroredCharacter(ch) ?? ch).join("");
36
+ if (!RTL_RE.test(s) && !/^\d/.test(s)) s = [...s].reverse().join("");
37
+ }
38
+ at += t.text.length;
39
+ return { ...t, text: s, level };
40
+ });
41
+ const max = Math.max(...tokens.map((t) => t.level));
42
+ const minOdd = Math.min(...tokens.map((t) => t.level % 2 === 1 ? t.level : Infinity));
43
+ for (let lvl = max; lvl >= minOdd && Number.isFinite(minOdd); lvl--) {
44
+ for (let i = 0; i < tokens.length; ) {
45
+ if (tokens[i].level < lvl) {
46
+ i++;
47
+ continue;
48
+ }
49
+ let j = i;
50
+ while (j < tokens.length && tokens[j].level >= lvl) j++;
51
+ tokens.splice(i, j - i, ...tokens.slice(i, j).reverse());
52
+ i = j;
53
+ }
54
+ }
55
+ for (const t of tokens) {
56
+ const last = out[out.length - 1];
57
+ const src = runs3[t.run];
58
+ if (last && last.text !== "\n" && last.__run === t.run) last.text += t.text;
59
+ else out.push({ ...src, text: t.text, __run: t.run });
60
+ }
61
+ });
62
+ for (const r of out) delete r.__run;
63
+ return out;
64
+ }
65
+
1
66
  // src/document/math.ts
2
67
  import { pairDisplayMath, singleDollarMathEnd } from "@ai-matrx/content-ir/source";
3
68
  var MATH_OPEN = "\uE100";
@@ -2045,6 +2110,8 @@ var CitationEngine = class {
2045
2110
  /** A note style: every citation becomes a numbered note. */
2046
2111
  isNote;
2047
2112
  numeric;
2113
+ /** APA 7 §8.11: narrative citations join authors with "and", not "&". */
2114
+ apaNarrativeAnd;
2048
2115
  constructor(styleXml, localeXml, items, lang) {
2049
2116
  const byId = new Map(items.map((i) => [i.id, toCslJson(i)]));
2050
2117
  const sys = {
@@ -2055,6 +2122,7 @@ var CitationEngine = class {
2055
2122
  this.engine = new Engine(sys, styleXml, cslLocaleTag(lang), true);
2056
2123
  this.engine.setOutputFormat("html");
2057
2124
  this.isNote = this.engine.opt.xclass === "note";
2125
+ this.apaNarrativeAnd = /<title>[^<]*(?:\bAPA\b|American Psychological Association)/i.test(styleXml) || /<link[^>]*href="[^"]*\/styles\/apa(?:-[\w-]*)?"[^>]*rel="(?:self|template|independent-parent)"/i.test(styleXml);
2058
2126
  this.numeric = /citation-number/.test(styleXml.slice(styleXml.indexOf("<citation"), styleXml.indexOf("</citation>")));
2059
2127
  }
2060
2128
  /** Cite a group; `noteIndex` is the note number for note styles (0 = in text). */
@@ -2091,7 +2159,8 @@ var CitationEngine = class {
2091
2159
  if (opts.narrative && refs.length === 1 && !this.isNote && !this.numeric) {
2092
2160
  const author = make(items);
2093
2161
  const rest = make(refs.map((r, i) => ({ ...items[i], "author-only": void 0, "suppress-author": true, id: r.key })));
2094
- const join = () => `${author.label}${rest.label ? ` ${/^\(/.test(rest.label) ? rest.label : `(${rest.label})`}` : ""}`;
2162
+ const who = () => this.apaNarrativeAnd ? author.label.replace(/,? & /g, (m) => m.startsWith(",") ? ", and " : " and ") : author.label;
2163
+ const join = () => `${who()}${rest.label ? ` ${/^\(/.test(rest.label) ? rest.label : `(${rest.label})`}` : ""}`;
2095
2164
  const listeners = [];
2096
2165
  author.listeners.push(() => listeners.forEach((l) => l(join())));
2097
2166
  rest.listeners.push(() => listeners.forEach((l) => l(join())));
@@ -2570,7 +2639,7 @@ function runs(nodes, c, st = {}) {
2570
2639
  for (const n of nodes) {
2571
2640
  switch (n.type) {
2572
2641
  case "text":
2573
- out.push(tr(n.value));
2642
+ out.push(RTL_RE.test(n.value) ? tr(n.value, { rightToLeft: true }) : tr(n.value));
2574
2643
  break;
2575
2644
  case "strong":
2576
2645
  out.push(...runs(n.children, c, { ...st, bold: true }));
@@ -2722,6 +2791,10 @@ function numberingRef(c, ordered, start) {
2722
2791
  }
2723
2792
  return ref;
2724
2793
  }
2794
+ function bidiPara(nodes) {
2795
+ const text = nodes.map((n) => "value" in n ? n.value : "").join("");
2796
+ return isRtlParagraph(text) ? { bidirectional: true } : {};
2797
+ }
2725
2798
  function blocks(list, c, o = {}) {
2726
2799
  const out = [];
2727
2800
  for (const b of list) out.push(...block(b, c, o));
@@ -2746,6 +2819,7 @@ function block(b, c, o) {
2746
2819
  const kids = [...b.number ? [new d.TextRun({ text: `${b.number} `, color: HEX.muted })] : [], ...runs(b.children, c)];
2747
2820
  return [
2748
2821
  new d.Paragraph({
2822
+ ...bidiPara(b.children),
2749
2823
  heading,
2750
2824
  keepNext: true,
2751
2825
  keepLines: true,
@@ -2755,7 +2829,7 @@ function block(b, c, o) {
2755
2829
  ];
2756
2830
  }
2757
2831
  case "paragraph":
2758
- return [para(c, o, { children: runs(b.children, c), widowControl: true })];
2832
+ return [para(c, o, { children: runs(b.children, c), widowControl: true, ...bidiPara(b.children) })];
2759
2833
  case "list": {
2760
2834
  const level = o.listLevel ?? 0;
2761
2835
  const ref = numberingRef(c, b.ordered, b.start);
@@ -2772,7 +2846,14 @@ function block(b, c, o) {
2772
2846
  })
2773
2847
  );
2774
2848
  } else {
2775
- out.push(new d.Paragraph({ numbering: { reference: ref, level, instance }, children: firstRuns, widowControl: true }));
2849
+ out.push(
2850
+ new d.Paragraph({
2851
+ numbering: { reference: ref, level, instance },
2852
+ children: firstRuns,
2853
+ widowControl: true,
2854
+ ...first?.type === "paragraph" ? bidiPara(first.children) : {}
2855
+ })
2856
+ );
2776
2857
  }
2777
2858
  const tail = first && first.type !== "paragraph" ? [first, ...rest] : rest;
2778
2859
  for (const child of tail) {
@@ -2814,6 +2895,7 @@ function block(b, c, o) {
2814
2895
  borders: { top: none, left: none, right: none, bottom: head ? { ...border, size: 10, color: HEX.heading } : border },
2815
2896
  children: [
2816
2897
  new d.Paragraph({
2898
+ ...bidiPara(cells[i] ?? []),
2817
2899
  alignment: alignOf(i),
2818
2900
  keepNext: keep && !last,
2819
2901
  children: runs(cells[i] ?? [], c, head ? { bold: true } : {})
@@ -3029,7 +3111,7 @@ async function documentToDocx(tree, options = {}) {
3029
3111
  default: {
3030
3112
  // East-Asian text gets a CJK face (Word and LibreOffice substitute an installed one).
3031
3113
  document: {
3032
- run: { font: { ascii: "Cambria", hAnsi: "Cambria", cs: "Cambria", eastAsia: "Yu Gothic" }, size, color: HEX.text },
3114
+ run: { font: { ascii: "Cambria", hAnsi: "Cambria", cs: "Arial", eastAsia: "Yu Gothic" }, size, sizeComplexScript: size, color: HEX.text },
3033
3115
  paragraph: { spacing: { after: 140, line: 300 } }
3034
3116
  }
3035
3117
  },
@@ -3217,10 +3299,10 @@ function renderBlockHtml(b, x) {
3217
3299
  switch (b.type) {
3218
3300
  case "heading": {
3219
3301
  const num = b.number ? `<span class="doc-heading-number">${b.number}</span> ` : "";
3220
- return `<h${b.level} id="${escapeHtml(b.id)}">${num}${renderInlineHtml(b.children, x)}</h${b.level}>`;
3302
+ return `<h${b.level} id="${escapeHtml(b.id)}" dir="auto">${num}${renderInlineHtml(b.children, x)}</h${b.level}>`;
3221
3303
  }
3222
3304
  case "paragraph":
3223
- return `<p>${renderInlineHtml(b.children, x)}</p>`;
3305
+ return `<p dir="auto">${renderInlineHtml(b.children, x)}</p>`;
3224
3306
  case "list": {
3225
3307
  const tag = b.ordered ? "ol" : "ul";
3226
3308
  const start = b.ordered && b.start !== 1 ? ` start="${b.start}"` : "";
@@ -3228,12 +3310,12 @@ function renderBlockHtml(b, x) {
3228
3310
  const items = b.items.map((i) => {
3229
3311
  const box = i.checked === null ? "" : `<span class="doc-task-box${i.checked ? " is-checked" : ""}" role="img" aria-label="${i.checked ? "done" : "not done"}"></span>`;
3230
3312
  const inner = i.children.length === 1 && i.children[0]?.type === "paragraph" ? renderInlineHtml(i.children[0].children, x) : renderBlocksHtml(i.children, x);
3231
- return `<li${i.checked !== null ? ' class="doc-task"' : ""}>${box}${inner}</li>`;
3313
+ return `<li dir="auto"${i.checked !== null ? ' class="doc-task"' : ""}>${box}${inner}</li>`;
3232
3314
  }).join("");
3233
3315
  return `<${tag}${start}${task ? ' class="doc-task-list"' : ""}>${items}</${tag}>`;
3234
3316
  }
3235
3317
  case "blockquote":
3236
- return `<blockquote>${renderBlocksHtml(b.children, x)}</blockquote>`;
3318
+ return `<blockquote dir="auto">${renderBlocksHtml(b.children, x)}</blockquote>`;
3237
3319
  case "code":
3238
3320
  return `<pre class="doc-code"${b.lang ? ` data-language="${escapeHtml(b.lang)}"` : ""}><code>${escapeHtml(
3239
3321
  b.value
@@ -3243,8 +3325,8 @@ function renderBlockHtml(b, x) {
3243
3325
  const a = b.align[i];
3244
3326
  return a ? ` style="text-align:${a}"` : "";
3245
3327
  };
3246
- const head = `<thead><tr>${b.header.map((c, i) => `<th${al(i)}>${renderInlineHtml(c, x)}</th>`).join("")}</tr></thead>`;
3247
- const body = `<tbody>${b.rows.map((r) => `<tr>${r.map((c, i) => `<td${al(i)}>${renderInlineHtml(c, x)}</td>`).join("")}</tr>`).join("")}</tbody>`;
3328
+ const head = `<thead><tr>${b.header.map((c, i) => `<th dir="auto"${al(i)}>${renderInlineHtml(c, x)}</th>`).join("")}</tr></thead>`;
3329
+ 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>`;
3248
3330
  const id = b.caption?.id ? ` id="tbl:${escapeHtml(b.caption.id)}"` : "";
3249
3331
  return `<div class="doc-table-wrap"${id}><table>${b.caption ? captionHtml(b.caption, "caption", x) : ""}${head}${body}</table></div>`;
3250
3332
  }
@@ -3714,7 +3796,7 @@ function breakRuns(input, width, t) {
3714
3796
  for (let guard = 0; guard < 50 && atoms.length; guard++) {
3715
3797
  const gw = atoms.reduce((n, a) => n + w(a), 0);
3716
3798
  const sw = pendingSpace && line > 0 ? w(pendingSpace) : 0;
3717
- if (line + sw + gw <= W || line === 0 && atoms.length !== 1) {
3799
+ if (line + sw + gw <= W || line === 0 && (atoms.length !== 1 || gw <= width)) {
3718
3800
  if (sw && pendingSpace) out.push(pendingSpace);
3719
3801
  out.push(...atoms);
3720
3802
  line += sw + gw;
@@ -3838,11 +3920,14 @@ function withFallbackFonts(nodes, c) {
3838
3920
  const fb = fallbackFor(cp);
3839
3921
  if (fb) {
3840
3922
  flush();
3841
- out.push({ ...n, text: ch, font: fb, bold: false, italics: false });
3923
+ const prev = out[out.length - 1];
3924
+ if (prev && typeof prev === "object" && prev.font === fb && prev.__fb) prev.text = `${prev.text}${ch}`;
3925
+ else out.push({ ...n, text: ch, font: fb, bold: false, italics: false, __fb: true });
3842
3926
  } else dropped = true;
3843
3927
  }
3844
3928
  flush();
3845
3929
  }
3930
+ for (const o of out) if (typeof o === "object") delete o.__fb;
3846
3931
  if (dropped && !c.glyphs.announced) {
3847
3932
  c.glyphs.announced = true;
3848
3933
  c.tree.notices.push({
@@ -3869,8 +3954,13 @@ function drawable(text, c) {
3869
3954
  }
3870
3955
  function bodyRuns(nodes, c, width = c.width) {
3871
3956
  const r = runs2(nodes, c);
3872
- return c.typo ? breakRuns(r, width, c.typo) : r;
3957
+ const text = plain(nodes);
3958
+ if (!RTL_RE.test(text)) return c.typo ? breakRuns(r, width, c.typo) : r;
3959
+ const typo = c.typo ?? (c.faces ? { split: (w) => [w], measure: c.faces.measure } : void 0);
3960
+ const lines = typo ? breakRuns(r, width, typo) : r;
3961
+ return visualOrder(lines, isRtlParagraph(text));
3873
3962
  }
3963
+ var alignFor = (nodes) => isRtlParagraph(plain(nodes)) ? { alignment: "right" } : {};
3874
3964
  function runs2(nodes, c, st = {}) {
3875
3965
  const out = [];
3876
3966
  const base = (raw) => {
@@ -4062,6 +4152,7 @@ function estimateLines(text, c, size = c.s.fontSize) {
4062
4152
  function columnWidths(b, n, size, c) {
4063
4153
  const measure = (text, bold) => c.faces ? c.faces.measure(text, { bold, fontSize: size }) : text.length * size * 0.5;
4064
4154
  const pad = 10;
4155
+ const slack = 1;
4065
4156
  const min = new Array(n).fill(0);
4066
4157
  const max = new Array(n).fill(0);
4067
4158
  const rows = [[b.header, true], ...b.rows.map((r) => [r, false])];
@@ -4069,8 +4160,8 @@ function columnWidths(b, n, size, c) {
4069
4160
  for (let i = 0; i < n; i++) {
4070
4161
  const t = plain(r[i] ?? []);
4071
4162
  const longest = t.split(/\s+/).reduce((m, w) => Math.max(m, measure(w, head)), 0);
4072
- min[i] = Math.max(min[i], Math.min(longest, c.width * 0.35) + pad);
4073
- max[i] = Math.max(max[i], measure(t, head) + pad);
4163
+ min[i] = Math.max(min[i], Math.min(longest * slack, c.width * 0.35) + pad);
4164
+ max[i] = Math.max(max[i], measure(t, head) * slack + pad);
4074
4165
  }
4075
4166
  }
4076
4167
  const avail = c.width;
@@ -4134,8 +4225,11 @@ function block2(b, c) {
4134
4225
  switch (b.type) {
4135
4226
  case "heading": {
4136
4227
  const rel = Math.min(6, b.level - c.minLevel + 1);
4228
+ const rtlHeading = RTL_RE.test(b.plain);
4229
+ const headRuns = rtlHeading ? visualOrder(runs2(b.children, c), isRtlParagraph(b.plain)) : runs2(b.children, c);
4137
4230
  const node = {
4138
- text: [...b.number ? [{ text: `${b.number} `, color: COLOR.muted }] : [], ...runs2(b.children, c)],
4231
+ ...rtlHeading ? alignFor(b.children) : {},
4232
+ text: [...b.number ? [{ text: `${b.number} `, color: COLOR.muted }] : [], ...headRuns],
4139
4233
  style: `h${Math.min(6, b.level)}`,
4140
4234
  headlineLevel: b.level,
4141
4235
  id: b.id
@@ -4149,12 +4243,12 @@ function block2(b, c) {
4149
4243
  }
4150
4244
  case "paragraph": {
4151
4245
  const text = bodyRuns(b.children, c);
4152
- const p = { text, style: "p" };
4246
+ const p = { text, style: "p", ...alignFor(b.children) };
4153
4247
  return estimateLines(plain(b.children), c) <= 5 ? { stack: [p], unbreakable: true } : p;
4154
4248
  }
4155
4249
  case "list": {
4156
4250
  const items = b.items.map((it) => {
4157
- 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 });
4251
+ 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 });
4158
4252
  if (it.checked !== null) return { ...taskItem(it.checked, content, c), listType: "none" };
4159
4253
  return content.length === 1 ? content[0] : { stack: content };
4160
4254
  });
@@ -4207,15 +4301,24 @@ function block2(b, c) {
4207
4301
  case "table": {
4208
4302
  const n = Math.max(b.header.length, ...b.rows.map((r) => r.length), 1);
4209
4303
  const size = c.s.fontSize * 0.92;
4304
+ const widths = columnWidths(b, n, size, c);
4305
+ const cellRuns = (inl, i, head) => {
4306
+ const r = runs2(inl, c).map((x) => typeof x === "object" ? { ...x, fontSize: x.fontSize ?? size, ...head ? { bold: true } : {} } : x);
4307
+ const w = (widths[i] ?? c.width) - 10;
4308
+ if (RTL_RE.test(plain(inl))) {
4309
+ const typo = c.typo ?? (c.faces ? { split: (x) => [x], measure: c.faces.measure } : void 0);
4310
+ return visualOrder(typo ? breakRuns(r, w, typo) : r, isRtlParagraph(plain(inl)));
4311
+ }
4312
+ return c.typo && !head ? breakRuns(r, w, c.typo) : r;
4313
+ };
4210
4314
  const cell = (inl, i, head) => ({
4211
- text: runs2(inl ?? [], c),
4315
+ text: cellRuns(inl ?? [], i, head),
4212
4316
  bold: head,
4213
- alignment: b.align[i] ?? "left",
4317
+ alignment: b.align[i] ?? (isRtlParagraph(plain(inl ?? [])) ? "right" : "left"),
4214
4318
  fontSize: size,
4215
4319
  color: head ? COLOR.heading : COLOR.text
4216
4320
  });
4217
4321
  const row = (r, head) => Array.from({ length: n }, (_, i) => cell(r[i], i, head));
4218
- const widths = columnWidths(b, n, size, c);
4219
4322
  const lineH = size * 1.3;
4220
4323
  const pageH = c.pageHeight;
4221
4324
  const rowHeight = (r) => Math.max(...r.map((inl, i) => estimateLines(plain(inl), { ...c, width: (widths[i] ?? c.width) - 10 }, size) * lineH)) + 8;
@@ -4461,12 +4564,16 @@ async function loadTypography(tree, loaders = {}) {
4461
4564
  const base = await loadFaces(vfs, s.fontSize);
4462
4565
  let needEmoji = false;
4463
4566
  let needSymbols = false;
4567
+ let needArabic = false;
4568
+ let needHebrew = false;
4464
4569
  const cjk = /* @__PURE__ */ new Set();
4465
4570
  for (const ch of documentText(tree)) {
4466
4571
  const cp = ch.codePointAt(0);
4467
4572
  if (cp < 32 || cp === 8205 || cp === 65039 || base.body(cp)) continue;
4468
4573
  if (EMOJI_CP.test(ch)) needEmoji = true;
4469
- else if (isCjk(cp)) cjk.add(cp);
4574
+ else if (isCjk(cp) || isHangul(cp)) cjk.add(cp);
4575
+ else if (isArabic(cp)) needArabic = true;
4576
+ else if (isHebrew(cp)) needHebrew = true;
4470
4577
  else needSymbols = true;
4471
4578
  }
4472
4579
  const fallbacks = [];
@@ -4478,6 +4585,14 @@ async function loadTypography(tree, loaders = {}) {
4478
4585
  const b = await registerFace("NotoSymbols", "NotoSansMath.woff", loaders.symbols ?? defaultSymbolFontLoader, tree, "symbols");
4479
4586
  if (b) fallbacks.push({ font: "NotoSymbols", bytes: b });
4480
4587
  }
4588
+ if (needArabic) {
4589
+ const b = await registerFace("NotoArabic", "NotoSansArabic.woff", loaders.arabic ?? urlLoader(NOTO_ARABIC_URL), tree, "Arabic");
4590
+ if (b) fallbacks.push({ font: "NotoArabic", bytes: b });
4591
+ }
4592
+ if (needHebrew) {
4593
+ const b = await registerFace("NotoHebrew", "NotoSansHebrew.woff", loaders.hebrew ?? urlLoader(NOTO_HEBREW_URL), tree, "Hebrew");
4594
+ if (b) fallbacks.push({ font: "NotoHebrew", bytes: b });
4595
+ }
4481
4596
  if (cjk.size) {
4482
4597
  let subsets = [];
4483
4598
  try {
@@ -4493,7 +4608,7 @@ async function loadTypography(tree, loaders = {}) {
4493
4608
  if (!subsets.length) {
4494
4609
  tree.notices.push({
4495
4610
  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).",
4611
+ 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).",
4497
4612
  remedy: "Check the internet connection (the font comes from cdn.jsdelivr.net) and export again."
4498
4613
  });
4499
4614
  }
@@ -4521,6 +4636,12 @@ var urlLoader = (url) => async () => {
4521
4636
  var defaultEmojiFontLoader = urlLoader(NOTO_EMOJI_URL);
4522
4637
  var defaultSymbolFontLoader = urlLoader(NOTO_SYMBOLS_URL);
4523
4638
  var NOTO_CJK_BASE = "https://cdn.jsdelivr.net/npm/@fontsource/noto-sans-jp@5.3.0";
4639
+ var NOTO_KR_BASE = "https://cdn.jsdelivr.net/npm/@fontsource/noto-sans-kr@5.3.0";
4640
+ var NOTO_ARABIC_URL = "https://cdn.jsdelivr.net/npm/@fontsource/noto-sans-arabic@5.3.0/files/noto-sans-arabic-arabic-400-normal.woff";
4641
+ var NOTO_HEBREW_URL = "https://cdn.jsdelivr.net/npm/@fontsource/noto-sans-hebrew@5.3.0/files/noto-sans-hebrew-hebrew-400-normal.woff";
4642
+ var isHangul = (cp) => cp >= 44032 && cp <= 55215 || cp >= 4352 && cp <= 4607 || cp >= 12592 && cp <= 12687;
4643
+ var isArabic = (cp) => cp >= 1536 && cp <= 1791 || cp >= 1872 && cp <= 1919 || cp >= 2208 && cp <= 2303 || cp >= 64336 && cp <= 65023 || cp >= 65136 && cp <= 65279;
4644
+ var isHebrew = (cp) => cp >= 1424 && cp <= 1535 || cp >= 64285 && cp <= 64335;
4524
4645
  function parseUnicodeRangeCss(css) {
4525
4646
  const out = [];
4526
4647
  for (const block3 of css.split("@font-face").slice(1)) {
@@ -4536,19 +4657,25 @@ function parseUnicodeRangeCss(css) {
4536
4657
  }
4537
4658
  return out;
4538
4659
  }
4539
- var defaultCjkFontLoader = async (cps) => {
4660
+ async function unicodeRangeFaces(base, cps) {
4540
4661
  if (typeof fetch !== "function" || cps.length === 0) return [];
4541
- const res = await fetch(`${NOTO_CJK_BASE}/400.css`);
4662
+ const res = await fetch(`${base}/400.css`);
4542
4663
  if (!res.ok) return [];
4543
4664
  const faces = parseUnicodeRangeCss(await res.text()).filter((f) => cps.some((cp) => f.ranges.some(([a, b]) => cp >= a && cp <= b)));
4544
4665
  const out = [];
4545
4666
  await Promise.all(
4546
4667
  faces.map(async (f) => {
4547
- const r = await fetch(`${NOTO_CJK_BASE}/files/${f.file}.woff`);
4668
+ const r = await fetch(`${base}/files/${f.file}.woff`);
4548
4669
  if (r.ok) out.push({ name: f.file, bytes: new Uint8Array(await r.arrayBuffer()) });
4549
4670
  })
4550
4671
  );
4551
4672
  return out;
4673
+ }
4674
+ var defaultCjkFontLoader = async (cps) => {
4675
+ const kr = cps.filter(isHangul);
4676
+ const jp = cps.filter((cp) => !isHangul(cp));
4677
+ const [a, b] = await Promise.all([unicodeRangeFaces(NOTO_KR_BASE, kr), unicodeRangeFaces(NOTO_CJK_BASE, jp)]);
4678
+ return [...a, ...b];
4552
4679
  };
4553
4680
  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;
4554
4681
  var EMOJI_CP = /\p{Extended_Pictographic}|\p{Regional_Indicator}|\u20E3/u;
@@ -4867,7 +4994,7 @@ function uniqueId(ctx, base) {
4867
4994
  return id;
4868
4995
  }
4869
4996
  var CITE_GROUP_RE = /\[(-?@[^[\]]+)\]/g;
4870
- var BARE_RE = /(^|[^\w@.\\])@((?:fig|tbl|sec|eq):[\w-]+|[A-Za-z0-9_](?:[\w:.#$%&+?<>~/-]*\w)?)/g;
4997
+ var BARE_RE = /(^|[^\w@.\\])@((?:fig|tbl|sec|eq):[\w-]+|[A-Za-z0-9_](?:[\w:.#$%&+?<>~/-]*\w)?)(?: \[([^\[\]@]+)\])?/g;
4871
4998
  function scanText(value, ctx) {
4872
4999
  const out = [];
4873
5000
  const pushText = (s) => {
@@ -4888,8 +5015,8 @@ function scanText(value, ctx) {
4888
5015
  last2 = start + key.length + 1;
4889
5016
  } else if (ctx.refs.has(key)) {
4890
5017
  pushText(s.slice(last2, start));
4891
- out.push(cite([{ key, suppressAuthor: false }], ctx, true));
4892
- last2 = start + key.length + 1;
5018
+ out.push(cite([{ key, locator: m[3]?.trim(), suppressAuthor: false }], ctx, true));
5019
+ last2 = start + key.length + 1 + (m[3] !== void 0 ? m[3].length + 3 : 0);
4893
5020
  }
4894
5021
  }
4895
5022
  pushText(s.slice(last2));
@@ -5630,8 +5757,11 @@ export {
5630
5757
  CitationEngine,
5631
5758
  DEFAULT_FOOTER,
5632
5759
  DOCUMENT_FORMATS,
5760
+ NOTO_ARABIC_URL,
5633
5761
  NOTO_CJK_BASE,
5634
5762
  NOTO_EMOJI_URL,
5763
+ NOTO_HEBREW_URL,
5764
+ NOTO_KR_BASE,
5635
5765
  NOTO_SYMBOLS_URL,
5636
5766
  PAGE_SIZES,
5637
5767
  buildPdfDefinition,