@bendyline/squisq-formats 2.8.15 → 2.8.16
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/README.md +1 -1
- package/THIRD_PARTY_LICENSES.txt +369 -0
- package/dist/{chunk-BBB7FCK7.js → chunk-KZUP6TIV.js} +1 -1
- package/dist/{chunk-NQNIFIDP.js → chunk-NNNHOTVM.js} +174 -53
- package/dist/{chunk-S5VXWSRR.js → chunk-SWLDHEZI.js} +13 -7
- package/dist/cjkFontData.generated-LJKMYRHP.js +16 -0
- package/dist/emojiFontData.generated-EPQOUGVZ.js +16 -0
- package/dist/hangulFontData.generated-CVLPZH2N.js +16 -0
- package/dist/index.js +4 -4
- package/dist/outside-in/index.js +2 -2
- package/dist/pdf/index.d.ts +37 -17
- package/dist/pdf/index.js +1 -1
- package/dist/pptx/index.js +1 -1
- package/dist/registry/index.js +1 -1
- package/package.json +6 -3
- /package/dist/{chunk-KHRX22VW.js → chunk-LSUMEIQ7.js} +0 -0
|
@@ -21,7 +21,6 @@ import {
|
|
|
21
21
|
|
|
22
22
|
// src/pdf/export.ts
|
|
23
23
|
import { PDFDocument, StandardFonts, rgb, PDFString } from "pdf-lib";
|
|
24
|
-
import fontkit2 from "@pdf-lib/fontkit";
|
|
25
24
|
import { contrastRatio } from "@bendyline/squisq/schemas";
|
|
26
25
|
import { docToMarkdown, resolveThemeForDoc } from "@bendyline/squisq/doc";
|
|
27
26
|
import { isHtmlLineBreak, readFrontmatterThemeId, sanitizeUrl } from "@bendyline/squisq/markdown";
|
|
@@ -328,56 +327,168 @@ var WinAnsiTracker = class {
|
|
|
328
327
|
}
|
|
329
328
|
/**
|
|
330
329
|
* One-shot summary for the export, or undefined when nothing was lost.
|
|
331
|
-
* `unicodeFont` says
|
|
332
|
-
* what remains is beyond
|
|
330
|
+
* `unicodeFont` says embedded Unicode fonts already drew what they could, so
|
|
331
|
+
* what remains is beyond them too (Thai and the Indic scripts, with the
|
|
332
|
+
* bundled faces).
|
|
333
333
|
*/
|
|
334
334
|
warning(options = {}) {
|
|
335
335
|
if (this.count === 0) return void 0;
|
|
336
336
|
if (options.unicodeFont) {
|
|
337
|
-
return `PDF export replaced ${this.count} character(s) that its fonts cannot draw (${this.samples.join(", ")}). The PDF fonts do not cover
|
|
337
|
+
return `PDF export replaced ${this.count} character(s) that its fonts cannot draw (${this.samples.join(", ")}). The PDF fonts do not cover every script, such as Thai or Devanagari.`;
|
|
338
338
|
}
|
|
339
339
|
return `PDF export replaced ${this.count} character(s) that the standard PDF fonts cannot encode (${this.samples.join(", ")}). Scripts such as emoji, CJK and Cyrillic require an embedded Unicode font.`;
|
|
340
340
|
}
|
|
341
341
|
};
|
|
342
342
|
|
|
343
|
-
// src/pdf/
|
|
343
|
+
// src/pdf/fontkitSubset.ts
|
|
344
344
|
import fontkit from "@pdf-lib/fontkit";
|
|
345
|
-
|
|
346
|
-
const
|
|
347
|
-
|
|
345
|
+
function padGlyphsToEvenLength(subset) {
|
|
346
|
+
const target = subset;
|
|
347
|
+
const addGlyph = target._addGlyph;
|
|
348
|
+
if (typeof addGlyph !== "function") return;
|
|
349
|
+
target._addGlyph = function(glyphId) {
|
|
350
|
+
const index = addGlyph.call(this, glyphId);
|
|
351
|
+
const data = this.glyf[index];
|
|
352
|
+
if (data && data.length % 2 === 1) {
|
|
353
|
+
const Buffer = data.constructor;
|
|
354
|
+
this.glyf[index] = Buffer.concat([data, Buffer.alloc(1)]);
|
|
355
|
+
this.offset += 1;
|
|
356
|
+
}
|
|
357
|
+
return index;
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
var subsetSafeFontkit = {
|
|
361
|
+
create(buffer, postscriptName) {
|
|
362
|
+
const font = fontkit.create(buffer, postscriptName);
|
|
363
|
+
if (typeof font.createSubset === "function") {
|
|
364
|
+
const createSubset = font.createSubset.bind(font);
|
|
365
|
+
font.createSubset = () => {
|
|
366
|
+
const subset = createSubset();
|
|
367
|
+
padGlyphsToEvenLength(subset);
|
|
368
|
+
return subset;
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
return font;
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// src/pdf/lineBreaks.ts
|
|
376
|
+
var BREAKS_BETWEEN = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\u3000-\u303f\uff00-\uffef]/u;
|
|
377
|
+
var NO_LINE_START = /[.,!?;:)\]}、。,.:;!?)]}」』】〉》〕・ー々ぁぃぅぇぉっゃゅょゎァィゥェォッャュョヮヵヶ]/u;
|
|
378
|
+
var NO_LINE_END = /[([{「『【〈《〔]/u;
|
|
379
|
+
function breakBetweenCjk(word) {
|
|
380
|
+
if (!BREAKS_BETWEEN.test(word)) return [word];
|
|
381
|
+
const pieces = [];
|
|
382
|
+
let piece = "";
|
|
383
|
+
let previousBreaks = false;
|
|
384
|
+
let previousOpens = false;
|
|
385
|
+
for (const ch of word) {
|
|
386
|
+
const breaks = BREAKS_BETWEEN.test(ch);
|
|
387
|
+
if (piece && (breaks || previousBreaks) && !previousOpens && !NO_LINE_START.test(ch)) {
|
|
388
|
+
pieces.push(piece);
|
|
389
|
+
piece = "";
|
|
390
|
+
}
|
|
391
|
+
piece += ch;
|
|
392
|
+
previousBreaks = breaks;
|
|
393
|
+
previousOpens = NO_LINE_END.test(ch);
|
|
394
|
+
}
|
|
395
|
+
if (piece) pieces.push(piece);
|
|
396
|
+
return pieces;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// src/pdf/unicodeText.ts
|
|
400
|
+
import fontkit2 from "@pdf-lib/fontkit";
|
|
401
|
+
var EMOJI_TEXT = /[\p{Extended_Pictographic}\p{Regional_Indicator}]/u;
|
|
402
|
+
var HAN_OR_KANA_TEXT = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Bopomofo}\u3000-\u303f\uff00-\uffef]/u;
|
|
403
|
+
var HANGUL_TEXT = /\p{Script=Hangul}/u;
|
|
404
|
+
async function loadBundledUnicodeFonts(root) {
|
|
405
|
+
const text = root === void 0 ? "" : unencodableCharacters(root);
|
|
406
|
+
const [{ dejaVuFontData }, emoji, cjk, hangul] = await Promise.all([
|
|
407
|
+
import("./unicodeFontData.generated-YY6T2K5S.js"),
|
|
408
|
+
EMOJI_TEXT.test(text) ? import("./emojiFontData.generated-EPQOUGVZ.js").then((m) => m.notoEmojiFontData()) : void 0,
|
|
409
|
+
HAN_OR_KANA_TEXT.test(text) ? import("./cjkFontData.generated-LJKMYRHP.js").then((m) => m.notoSansScFontData()) : void 0,
|
|
410
|
+
HANGUL_TEXT.test(text) ? import("./hangulFontData.generated-CVLPZH2N.js").then((m) => m.notoSansKrFontData()) : void 0
|
|
411
|
+
]);
|
|
412
|
+
return {
|
|
413
|
+
...dejaVuFontData(),
|
|
414
|
+
emoji,
|
|
415
|
+
fallbacks: [cjk, hangul].filter((face) => face !== void 0)
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
function isUnencodable(codePoint) {
|
|
419
|
+
return !isWinAnsiEncodable(codePoint) && !isInvisibleOrSpace(codePoint);
|
|
348
420
|
}
|
|
349
421
|
function hasUnencodableText(value) {
|
|
350
422
|
for (const ch of value) {
|
|
351
|
-
|
|
352
|
-
if (!isWinAnsiEncodable(codePoint) && !isInvisibleOrSpace(codePoint)) return true;
|
|
423
|
+
if (isUnencodable(ch.codePointAt(0))) return true;
|
|
353
424
|
}
|
|
354
425
|
return false;
|
|
355
426
|
}
|
|
356
|
-
function
|
|
427
|
+
function forEachString(root, visit) {
|
|
357
428
|
const pending = [root];
|
|
358
429
|
while (pending.length > 0) {
|
|
359
430
|
const value = pending.pop();
|
|
360
431
|
if (typeof value === "string") {
|
|
361
|
-
if (
|
|
432
|
+
if (visit(value) === true) return;
|
|
362
433
|
} else if (Array.isArray(value)) {
|
|
363
434
|
pending.push(...value);
|
|
364
435
|
} else if (value && typeof value === "object") {
|
|
365
436
|
pending.push(...Object.values(value));
|
|
366
437
|
}
|
|
367
438
|
}
|
|
368
|
-
return false;
|
|
369
439
|
}
|
|
440
|
+
function needsUnicodeFont(root) {
|
|
441
|
+
let found = false;
|
|
442
|
+
forEachString(root, (value) => found = hasUnencodableText(value));
|
|
443
|
+
return found;
|
|
444
|
+
}
|
|
445
|
+
function unencodableCharacters(root) {
|
|
446
|
+
const characters = /* @__PURE__ */ new Set();
|
|
447
|
+
forEachString(root, (value) => {
|
|
448
|
+
for (const ch of value) {
|
|
449
|
+
if (isUnencodable(ch.codePointAt(0))) characters.add(ch);
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
return [...characters].join("");
|
|
453
|
+
}
|
|
454
|
+
function continuesCluster(ch, codePoint) {
|
|
455
|
+
return /\p{M}/u.test(ch) || // combining marks, variation selectors, the keycap
|
|
456
|
+
codePoint === 8205 || // zero-width joiner
|
|
457
|
+
codePoint >= 127995 && codePoint <= 127999 || // emoji skin tones
|
|
458
|
+
codePoint >= 917536 && codePoint <= 917631;
|
|
459
|
+
}
|
|
460
|
+
function isRegionalIndicator(codePoint) {
|
|
461
|
+
return codePoint >= 127462 && codePoint <= 127487;
|
|
462
|
+
}
|
|
463
|
+
function characterClusters(text) {
|
|
464
|
+
const clusters = [];
|
|
465
|
+
let loneRegionalIndicator = false;
|
|
466
|
+
for (const ch of text) {
|
|
467
|
+
const codePoint = ch.codePointAt(0);
|
|
468
|
+
const last = clusters.length - 1;
|
|
469
|
+
const joins = last >= 0 && (continuesCluster(ch, codePoint) || clusters[last].endsWith("\u200D") || loneRegionalIndicator && isRegionalIndicator(codePoint));
|
|
470
|
+
if (joins) {
|
|
471
|
+
clusters[last] += ch;
|
|
472
|
+
loneRegionalIndicator = false;
|
|
473
|
+
} else {
|
|
474
|
+
clusters.push(ch);
|
|
475
|
+
loneRegionalIndicator = isRegionalIndicator(codePoint);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return clusters;
|
|
479
|
+
}
|
|
480
|
+
var EMOJI_STYLE = /[\p{Emoji_Presentation}\p{Emoji_Modifier}\p{Regional_Indicator}\u{fe0f}\u{20e3}\u{e0020}-\u{e007f}]/u;
|
|
370
481
|
function asBytes(data) {
|
|
371
482
|
return data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
372
483
|
}
|
|
373
484
|
async function embedUnicodeFaces(pdfDoc, fonts, standard) {
|
|
374
|
-
pdfDoc.registerFontkit(
|
|
485
|
+
pdfDoc.registerFontkit(subsetSafeFontkit);
|
|
375
486
|
const faces = /* @__PURE__ */ new Map();
|
|
376
487
|
const faceFor = (data) => {
|
|
377
488
|
let face = faces.get(data);
|
|
378
489
|
if (!face) {
|
|
379
490
|
const bytes = asBytes(data);
|
|
380
|
-
const metrics =
|
|
491
|
+
const metrics = fontkit2.create(bytes);
|
|
381
492
|
face = pdfDoc.embedFont(bytes, { subset: true }).then((font) => ({
|
|
382
493
|
font,
|
|
383
494
|
covers: (codePoint) => metrics.hasGlyphForCodePoint(codePoint)
|
|
@@ -386,18 +497,21 @@ async function embedUnicodeFaces(pdfDoc, fonts, standard) {
|
|
|
386
497
|
}
|
|
387
498
|
return face;
|
|
388
499
|
};
|
|
389
|
-
const [regular, bold, mono] = await Promise.all([
|
|
500
|
+
const [regular, bold, mono, emoji, fallbacks] = await Promise.all([
|
|
390
501
|
faceFor(fonts.regular),
|
|
391
502
|
faceFor(fonts.bold ?? fonts.regular),
|
|
392
|
-
faceFor(fonts.mono ?? fonts.regular)
|
|
503
|
+
faceFor(fonts.mono ?? fonts.regular),
|
|
504
|
+
fonts.emoji ? faceFor(fonts.emoji) : void 0,
|
|
505
|
+
Promise.all((fonts.fallbacks ?? []).map(faceFor))
|
|
393
506
|
]);
|
|
507
|
+
const withPrimary = (primary) => ({ primary, fallbacks, emoji });
|
|
394
508
|
return /* @__PURE__ */ new Map([
|
|
395
|
-
[standard.regular, regular],
|
|
396
|
-
[standard.italic, regular],
|
|
397
|
-
[standard.bold, bold],
|
|
398
|
-
[standard.boldItalic, bold],
|
|
399
|
-
[standard.mono, mono],
|
|
400
|
-
[standard.monoBold, mono]
|
|
509
|
+
[standard.regular, withPrimary(regular)],
|
|
510
|
+
[standard.italic, withPrimary(regular)],
|
|
511
|
+
[standard.bold, withPrimary(bold)],
|
|
512
|
+
[standard.boldItalic, withPrimary(bold)],
|
|
513
|
+
[standard.mono, withPrimary(mono)],
|
|
514
|
+
[standard.monoBold, withPrimary(mono)]
|
|
401
515
|
]);
|
|
402
516
|
}
|
|
403
517
|
var PdfTextRuns = class {
|
|
@@ -406,8 +520,8 @@ var PdfTextRuns = class {
|
|
|
406
520
|
this.faces = faces;
|
|
407
521
|
}
|
|
408
522
|
split(text, font) {
|
|
409
|
-
const
|
|
410
|
-
if (!
|
|
523
|
+
const faces = this.faces.get(font);
|
|
524
|
+
if (!faces || !hasUnencodableText(text)) return [{ text: this.tracker.clean(text), font }];
|
|
411
525
|
const runs = [];
|
|
412
526
|
const push = (value, runFont) => {
|
|
413
527
|
if (value.length === 0) return;
|
|
@@ -415,25 +529,48 @@ var PdfTextRuns = class {
|
|
|
415
529
|
if (last && last.font === runFont) last.text += value;
|
|
416
530
|
else runs.push({ text: value, font: runFont });
|
|
417
531
|
};
|
|
418
|
-
const
|
|
532
|
+
const { primary } = faces;
|
|
533
|
+
const drawable = (codePoint) => !isInvisibleOrSpace(codePoint) && primary.covers(codePoint);
|
|
419
534
|
for (const word of text.normalize("NFC").split(/(\s+)/u)) {
|
|
420
535
|
if (word.length === 0) continue;
|
|
421
536
|
const codePoints = Array.from(word, (ch) => ch.codePointAt(0));
|
|
422
537
|
if (codePoints.every(isWinAnsiEncodable)) {
|
|
423
538
|
push(word, font);
|
|
424
|
-
} else if (codePoints.every(drawable)) {
|
|
425
|
-
push(word,
|
|
539
|
+
} else if (!EMOJI_STYLE.test(word) && codePoints.every(drawable)) {
|
|
540
|
+
push(word, primary.font);
|
|
426
541
|
} else {
|
|
427
|
-
for (const
|
|
428
|
-
|
|
429
|
-
if (isWinAnsiEncodable(codePoint)) push(ch, font);
|
|
430
|
-
else if (drawable(codePoint)) push(ch, face.font);
|
|
431
|
-
else push(this.tracker.clean(ch), font);
|
|
432
|
-
}
|
|
542
|
+
for (const cluster of characterClusters(word))
|
|
543
|
+
this.splitCluster(cluster, font, faces, push);
|
|
433
544
|
}
|
|
434
545
|
}
|
|
435
546
|
return runs.length > 0 ? runs : [{ text: "", font }];
|
|
436
547
|
}
|
|
548
|
+
/** One reader-visible character: drawn whole by one face where one can. */
|
|
549
|
+
splitCluster(cluster, font, faces, push) {
|
|
550
|
+
const codePoints = Array.from(cluster, (ch) => ch.codePointAt(0));
|
|
551
|
+
if (codePoints.every(isWinAnsiEncodable)) {
|
|
552
|
+
push(cluster, font);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
const order = EMOJI_STYLE.test(cluster) ? [faces.emoji, faces.primary, ...faces.fallbacks] : [faces.primary, ...faces.fallbacks, faces.emoji];
|
|
556
|
+
const candidates = order.filter((face) => face !== void 0);
|
|
557
|
+
const visible = codePoints.filter((codePoint) => !isInvisibleOrSpace(codePoint));
|
|
558
|
+
const whole = visible.length > 0 ? candidates.find((face) => visible.every((codePoint) => face.covers(codePoint))) : void 0;
|
|
559
|
+
if (whole) {
|
|
560
|
+
push(
|
|
561
|
+
Array.from(cluster).filter((ch) => whole.covers(ch.codePointAt(0))).join(""),
|
|
562
|
+
whole.font
|
|
563
|
+
);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
for (const ch of cluster) {
|
|
567
|
+
const codePoint = ch.codePointAt(0);
|
|
568
|
+
const face = isInvisibleOrSpace(codePoint) ? void 0 : candidates.find((candidate) => candidate.covers(codePoint));
|
|
569
|
+
if (isWinAnsiEncodable(codePoint)) push(ch, font);
|
|
570
|
+
else if (face) push(ch, face.font);
|
|
571
|
+
else push(this.tracker.clean(ch), font);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
437
574
|
};
|
|
438
575
|
|
|
439
576
|
// src/pdf/styles.ts
|
|
@@ -567,7 +704,7 @@ async function createExportContext(pdfDoc, options, doc) {
|
|
|
567
704
|
]);
|
|
568
705
|
let unicodeFaces = /* @__PURE__ */ new Map();
|
|
569
706
|
if (options.unicodeFonts !== false && needsUnicodeFont(doc)) {
|
|
570
|
-
const faces = options.unicodeFonts ?? await loadBundledUnicodeFonts();
|
|
707
|
+
const faces = options.unicodeFonts ?? await loadBundledUnicodeFonts(doc);
|
|
571
708
|
unicodeFaces = await embedUnicodeFaces(pdfDoc, faces, {
|
|
572
709
|
regular,
|
|
573
710
|
bold,
|
|
@@ -580,7 +717,7 @@ async function createExportContext(pdfDoc, options, doc) {
|
|
|
580
717
|
const icons = {};
|
|
581
718
|
const iconFamilies = collectInlineIconFamilies(doc);
|
|
582
719
|
if (iconFamilies.size > 0) {
|
|
583
|
-
pdfDoc.registerFontkit(
|
|
720
|
+
pdfDoc.registerFontkit(subsetSafeFontkit);
|
|
584
721
|
await Promise.all(
|
|
585
722
|
fontAwesomeFaces(iconFamilies).map(async (face) => {
|
|
586
723
|
icons[face.family] = await pdfDoc.embedFont(face.data, { subset: true });
|
|
@@ -938,22 +1075,6 @@ function breakLongWord(word, span, maxWidth) {
|
|
|
938
1075
|
if (piece) pieces.push(piece);
|
|
939
1076
|
return pieces;
|
|
940
1077
|
}
|
|
941
|
-
function continuesCluster(ch) {
|
|
942
|
-
const codePoint = ch.codePointAt(0);
|
|
943
|
-
return /\p{M}/u.test(ch) || codePoint === 8205 || codePoint === 65038 || codePoint === 65039 || codePoint >= 127995 && codePoint <= 127999;
|
|
944
|
-
}
|
|
945
|
-
function characterClusters(text) {
|
|
946
|
-
const clusters = [];
|
|
947
|
-
for (const ch of text) {
|
|
948
|
-
const last = clusters.length - 1;
|
|
949
|
-
if (last >= 0 && (continuesCluster(ch) || clusters[last].endsWith("\u200D"))) {
|
|
950
|
-
clusters[last] += ch;
|
|
951
|
-
} else {
|
|
952
|
-
clusters.push(ch);
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
return clusters;
|
|
956
|
-
}
|
|
957
1078
|
function splitIntoWords(text) {
|
|
958
1079
|
const tokens = [];
|
|
959
1080
|
let current = "";
|
|
@@ -968,7 +1089,7 @@ function splitIntoWords(text) {
|
|
|
968
1089
|
inSpace = isSpace;
|
|
969
1090
|
}
|
|
970
1091
|
if (current.length > 0) tokens.push(current);
|
|
971
|
-
return tokens;
|
|
1092
|
+
return tokens.flatMap(breakBetweenCjk);
|
|
972
1093
|
}
|
|
973
1094
|
function getLineHeight(line) {
|
|
974
1095
|
let maxSize = 0;
|
|
@@ -70,6 +70,7 @@ import {
|
|
|
70
70
|
import { VIEWPORT_PRESETS } from "@bendyline/squisq/schemas";
|
|
71
71
|
import {
|
|
72
72
|
buildPreviewDoc,
|
|
73
|
+
coverRepeatsLeadingBlock,
|
|
73
74
|
createTemplateContext,
|
|
74
75
|
docToMarkdown,
|
|
75
76
|
expandCoverBlock,
|
|
@@ -1366,13 +1367,18 @@ async function docToPptx(doc, options = {}) {
|
|
|
1366
1367
|
Math.max(1, expanded.length + 1),
|
|
1367
1368
|
VIEWPORT_PRESETS.landscape
|
|
1368
1369
|
);
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1370
|
+
const coverLayers = expandCoverBlock(doc.startBlock, coverContext);
|
|
1371
|
+
if (coverRepeatsLeadingBlock(doc) && expanded[0] && expanded[0].id === doc.blocks[0]?.id) {
|
|
1372
|
+
expanded[0] = { ...expanded[0], layers: coverLayers };
|
|
1373
|
+
} else {
|
|
1374
|
+
slideBlocks.push({
|
|
1375
|
+
id: "cover-block",
|
|
1376
|
+
startTime: -1,
|
|
1377
|
+
duration: 0,
|
|
1378
|
+
audioSegment: -1,
|
|
1379
|
+
layers: coverLayers
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1376
1382
|
}
|
|
1377
1383
|
slideBlocks.push(...expanded);
|
|
1378
1384
|
if (slideBlocks.length === 0) {
|