@ai-matrx/print 0.8.0 → 0.8.2

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,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.2
4
+
5
+ - **PDF font fallback covers Japanese / Chinese / Korean.** Characters the
6
+ body, emoji and symbol faces cannot draw now fall back to Noto Sans JP
7
+ (SIL OFL, fontsource) — loading only the unicode-range subsets the document
8
+ uses (`fonts.cjk` loader; default jsDelivr). Found on a real conversation
9
+ whose citations carried Japanese source titles. A glyph is announced only
10
+ when no face in the chain can draw it.
11
+ - Consumer action: none — refresh to latest.
12
+
13
+ ## 0.8.1
14
+
15
+ - **Fixed (Word):** exporting DOCX laid out a whole PDF to number the table of
16
+ contents even when the document has no table of contents; now only with a
17
+ TOC, and a failed measuring pass never fails the export — the TOC lists every
18
+ heading and Word fills in the page numbers on open (`toc-pages-deferred`
19
+ notice).
20
+ - Consumer action: none — refresh to latest.
21
+
3
22
  ## 0.8.0
4
23
 
5
24
  - **Citations through a real CSL processor.** citeproc-js (the engine Zotero
package/dist/document.cjs CHANGED
@@ -35,12 +35,14 @@ __export(document_exports, {
35
35
  CitationEngine: () => CitationEngine,
36
36
  DEFAULT_FOOTER: () => DEFAULT_FOOTER,
37
37
  DOCUMENT_FORMATS: () => DOCUMENT_FORMATS,
38
+ NOTO_CJK_BASE: () => NOTO_CJK_BASE,
38
39
  NOTO_EMOJI_URL: () => NOTO_EMOJI_URL,
39
40
  NOTO_SYMBOLS_URL: () => NOTO_SYMBOLS_URL,
40
41
  PAGE_SIZES: () => PAGE_SIZES,
41
42
  buildPdfDefinition: () => buildPdfDefinition,
42
43
  countUnplacedWords: () => countUnplacedWords,
43
44
  cslStyleId: () => cslStyleId,
45
+ defaultCjkFontLoader: () => defaultCjkFontLoader,
44
46
  defaultCslFetcher: () => defaultCslFetcher,
45
47
  defaultEmojiFontLoader: () => defaultEmojiFontLoader,
46
48
  defaultImageLoader: () => defaultImageLoader,
@@ -63,6 +65,7 @@ __export(document_exports, {
63
65
  parseLength: () => parseLength,
64
66
  parseLocator: () => parseLocator,
65
67
  parseMathML: () => parseMathML,
68
+ parseUnicodeRangeCss: () => parseUnicodeRangeCss,
66
69
  plainText: () => plainText,
67
70
  prepareDocument: () => prepareDocument,
68
71
  renderBlocksHtml: () => renderBlocksHtml,
@@ -4533,12 +4536,13 @@ async function loadTypography(tree, loaders = {}) {
4533
4536
  const base = await loadFaces(vfs, s.fontSize);
4534
4537
  let needEmoji = false;
4535
4538
  let needSymbols = false;
4539
+ const cjk = /* @__PURE__ */ new Set();
4536
4540
  for (const ch of documentText(tree)) {
4537
4541
  const cp = ch.codePointAt(0);
4538
4542
  if (cp < 32 || cp === 8205 || cp === 65039 || base.body(cp)) continue;
4539
4543
  if (EMOJI_CP.test(ch)) needEmoji = true;
4544
+ else if (isCjk(cp)) cjk.add(cp);
4540
4545
  else needSymbols = true;
4541
- if (needEmoji && needSymbols) break;
4542
4546
  }
4543
4547
  const fallbacks = [];
4544
4548
  if (needEmoji) {
@@ -4549,6 +4553,26 @@ async function loadTypography(tree, loaders = {}) {
4549
4553
  const b = await registerFace("NotoSymbols", "NotoSansMath.woff", loaders.symbols ?? defaultSymbolFontLoader, tree, "symbols");
4550
4554
  if (b) fallbacks.push({ font: "NotoSymbols", bytes: b });
4551
4555
  }
4556
+ if (cjk.size) {
4557
+ let subsets = [];
4558
+ try {
4559
+ subsets = await (loaders.cjk ?? defaultCjkFontLoader)([...cjk]);
4560
+ } catch {
4561
+ subsets = [];
4562
+ }
4563
+ for (const sub of subsets) {
4564
+ const font = `NotoCJK_${sub.name.replace(/[^\w]/g, "_")}`;
4565
+ const b = await registerFace(font, `${sub.name}.woff`, async () => sub.bytes, tree, "Japanese / Chinese");
4566
+ if (b) fallbacks.push({ font, bytes: b });
4567
+ }
4568
+ if (!subsets.length) {
4569
+ tree.notices.push({
4570
+ code: "glyph-substituted",
4571
+ message: "The Japanese / Chinese font could not be loaded, so those characters were left out of the PDF (Word, EPUB and HTML keep them).",
4572
+ remedy: "Check the internet connection (the font comes from cdn.jsdelivr.net) and export again."
4573
+ });
4574
+ }
4575
+ }
4552
4576
  const faces = fallbacks.length ? await loadFaces(vfs, s.fontSize, fallbacks) : base;
4553
4577
  if (!s.hyphenate) return { faces };
4554
4578
  const split = await loadHyphenator(s.lang);
@@ -4571,6 +4595,37 @@ var urlLoader = (url) => async () => {
4571
4595
  };
4572
4596
  var defaultEmojiFontLoader = urlLoader(NOTO_EMOJI_URL);
4573
4597
  var defaultSymbolFontLoader = urlLoader(NOTO_SYMBOLS_URL);
4598
+ var NOTO_CJK_BASE = "https://cdn.jsdelivr.net/npm/@fontsource/noto-sans-jp@5.3.0";
4599
+ function parseUnicodeRangeCss(css) {
4600
+ const out = [];
4601
+ for (const block3 of css.split("@font-face").slice(1)) {
4602
+ const file = /url\(\.\/files\/([\w-]+)\.woff\)/.exec(block3)?.[1];
4603
+ const range = /unicode-range:\s*([^;]+);/.exec(block3)?.[1];
4604
+ if (!file || !range) continue;
4605
+ const ranges = range.split(",").map((r) => {
4606
+ const [a, b] = r.trim().replace(/^U\+/i, "").split("-");
4607
+ const lo = parseInt(a, 16);
4608
+ return [lo, b ? parseInt(b, 16) : lo];
4609
+ });
4610
+ out.push({ file, ranges });
4611
+ }
4612
+ return out;
4613
+ }
4614
+ var defaultCjkFontLoader = async (cps) => {
4615
+ if (typeof fetch !== "function" || cps.length === 0) return [];
4616
+ const res = await fetch(`${NOTO_CJK_BASE}/400.css`);
4617
+ if (!res.ok) return [];
4618
+ const faces = parseUnicodeRangeCss(await res.text()).filter((f) => cps.some((cp) => f.ranges.some(([a, b]) => cp >= a && cp <= b)));
4619
+ const out = [];
4620
+ await Promise.all(
4621
+ faces.map(async (f) => {
4622
+ const r = await fetch(`${NOTO_CJK_BASE}/files/${f.file}.woff`);
4623
+ if (r.ok) out.push({ name: f.file, bytes: new Uint8Array(await r.arrayBuffer()) });
4624
+ })
4625
+ );
4626
+ return out;
4627
+ };
4628
+ 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;
4574
4629
  var EMOJI_CP = /\p{Extended_Pictographic}|\p{Regional_Indicator}|\u20E3/u;
4575
4630
  function documentText(tree) {
4576
4631
  const s = tree.settings;
@@ -5548,7 +5603,21 @@ async function exportDocument(source, format, options = {}) {
5548
5603
  bytes = r.bytes;
5549
5604
  pageCount = r.pageCount;
5550
5605
  } else if (format === "docx") {
5551
- const pages = tree.toc.length ? await headingPages(tree, images, options.fonts) : void 0;
5606
+ const hasToc = tree.sections.some((sec) => sec.blocks.some((blk) => blk.type === "toc"));
5607
+ let pages;
5608
+ if (hasToc) {
5609
+ try {
5610
+ pages = await headingPages(tree, images, options.fonts);
5611
+ } catch (err) {
5612
+ const n = {
5613
+ code: "toc-pages-deferred",
5614
+ message: `The Word file's table of contents lists every heading; its page numbers fill in when the file is opened (they could not be measured here: ${err.message}).`,
5615
+ remedy: "Open the file in Word (or update fields) to show the page numbers."
5616
+ };
5617
+ tree.notices.push(n);
5618
+ options.onNotice?.(n);
5619
+ }
5620
+ }
5552
5621
  bytes = await documentToDocx(tree, { images, headingPages: pages });
5553
5622
  } else bytes = await documentToEpub(tree, { images });
5554
5623
  }