@bendyline/squisq-formats 2.7.1 → 2.7.3
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 +192 -0
- package/dist/{chunk-BHWIHYUJ.js → chunk-52RXDH24.js} +118 -46
- package/dist/{chunk-3NKXJF4C.js → chunk-ETK2SKVQ.js} +55 -7
- package/dist/{chunk-ERT4EVR2.js → chunk-GHHIYBUT.js} +1 -1
- package/dist/{chunk-YERFQ2SB.js → chunk-GRIARAMQ.js} +214 -80
- package/dist/{chunk-TX2HE34S.js → chunk-PKFWF4FX.js} +12 -9
- package/dist/chunk-VCZUXQUL.js +90 -0
- package/dist/{chunk-A6N6IN3I.js → chunk-XVBQB2SL.js} +9 -0
- package/dist/{chunk-5ACTORJI.js → chunk-YITBSNPM.js} +82 -15
- package/dist/diagrams/index.d.ts +15 -4
- package/dist/diagrams/index.js +33 -6
- package/dist/docx/index.d.ts +11 -0
- package/dist/docx/index.js +3 -2
- package/dist/epub/index.d.ts +1 -1
- package/dist/epub/index.js +2 -1
- package/dist/html/index.js +2 -1
- package/dist/index.js +9 -8
- package/dist/outside-in/index.js +2 -2
- package/dist/pdf/index.d.ts +38 -0
- package/dist/pdf/index.js +1 -1
- package/dist/pptx/index.js +3 -2
- package/dist/registry/index.js +1 -1
- package/dist/svgIslandPoster-RYA6PDX7.js +73 -0
- package/dist/unicodeFontData.generated-YY6T2K5S.js +22 -0
- package/package.json +4 -3
- /package/dist/{chunk-HNGS7A5Z.js → chunk-CUQ7YITW.js} +0 -0
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
|
|
20
20
|
// src/pdf/export.ts
|
|
21
21
|
import { PDFDocument, StandardFonts, rgb, PDFString } from "pdf-lib";
|
|
22
|
-
import
|
|
22
|
+
import fontkit2 from "@pdf-lib/fontkit";
|
|
23
23
|
import { contrastRatio } from "@bendyline/squisq/schemas";
|
|
24
24
|
import { docToMarkdown, resolveThemeForDoc } from "@bendyline/squisq/doc";
|
|
25
25
|
import { readFrontmatterThemeId, sanitizeUrl } from "@bendyline/squisq/markdown";
|
|
@@ -269,6 +269,10 @@ var WHITESPACE_LIKE = /* @__PURE__ */ new Set([
|
|
|
269
269
|
8288,
|
|
270
270
|
65279
|
|
271
271
|
]);
|
|
272
|
+
var INVISIBLE_OR_SPACE = /^[\p{Cc}\p{Cf}\p{Zl}\p{Zp}\p{Zs}]$/u;
|
|
273
|
+
function isInvisibleOrSpace(codePoint) {
|
|
274
|
+
return WHITESPACE_LIKE.has(codePoint) || INVISIBLE_OR_SPACE.test(String.fromCodePoint(codePoint));
|
|
275
|
+
}
|
|
272
276
|
var MAX_SAMPLES = 5;
|
|
273
277
|
var ASCII_ONLY = /^[\x20-\x7e]*$/;
|
|
274
278
|
var COMBINING_MARKS = /[̀-ͯ]/g;
|
|
@@ -320,13 +324,116 @@ var WinAnsiTracker = class {
|
|
|
320
324
|
get substitutions() {
|
|
321
325
|
return this.count;
|
|
322
326
|
}
|
|
323
|
-
/**
|
|
324
|
-
|
|
327
|
+
/**
|
|
328
|
+
* One-shot summary for the export, or undefined when nothing was lost.
|
|
329
|
+
* `unicodeFont` says an embedded Unicode font already drew what it could, so
|
|
330
|
+
* what remains is beyond it too (CJK and emoji, with the bundled faces).
|
|
331
|
+
*/
|
|
332
|
+
warning(options = {}) {
|
|
325
333
|
if (this.count === 0) return void 0;
|
|
334
|
+
if (options.unicodeFont) {
|
|
335
|
+
return `PDF export replaced ${this.count} character(s) that its fonts cannot draw (${this.samples.join(", ")}). Scripts such as CJK and emoji are not available in PDF export.`;
|
|
336
|
+
}
|
|
326
337
|
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.`;
|
|
327
338
|
}
|
|
328
339
|
};
|
|
329
340
|
|
|
341
|
+
// src/pdf/unicodeText.ts
|
|
342
|
+
import fontkit from "@pdf-lib/fontkit";
|
|
343
|
+
async function loadBundledUnicodeFonts() {
|
|
344
|
+
const { dejaVuFontData } = await import("./unicodeFontData.generated-YY6T2K5S.js");
|
|
345
|
+
return dejaVuFontData();
|
|
346
|
+
}
|
|
347
|
+
function hasUnencodableText(value) {
|
|
348
|
+
for (const ch of value) {
|
|
349
|
+
const codePoint = ch.codePointAt(0);
|
|
350
|
+
if (!isWinAnsiEncodable(codePoint) && !isInvisibleOrSpace(codePoint)) return true;
|
|
351
|
+
}
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
function needsUnicodeFont(root) {
|
|
355
|
+
const pending = [root];
|
|
356
|
+
while (pending.length > 0) {
|
|
357
|
+
const value = pending.pop();
|
|
358
|
+
if (typeof value === "string") {
|
|
359
|
+
if (hasUnencodableText(value)) return true;
|
|
360
|
+
} else if (Array.isArray(value)) {
|
|
361
|
+
pending.push(...value);
|
|
362
|
+
} else if (value && typeof value === "object") {
|
|
363
|
+
pending.push(...Object.values(value));
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
function asBytes(data) {
|
|
369
|
+
return data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
370
|
+
}
|
|
371
|
+
async function embedUnicodeFaces(pdfDoc, fonts, standard) {
|
|
372
|
+
pdfDoc.registerFontkit(fontkit);
|
|
373
|
+
const faces = /* @__PURE__ */ new Map();
|
|
374
|
+
const faceFor = (data) => {
|
|
375
|
+
let face = faces.get(data);
|
|
376
|
+
if (!face) {
|
|
377
|
+
const bytes = asBytes(data);
|
|
378
|
+
const metrics = fontkit.create(bytes);
|
|
379
|
+
face = pdfDoc.embedFont(bytes, { subset: true }).then((font) => ({
|
|
380
|
+
font,
|
|
381
|
+
covers: (codePoint) => metrics.hasGlyphForCodePoint(codePoint)
|
|
382
|
+
}));
|
|
383
|
+
faces.set(data, face);
|
|
384
|
+
}
|
|
385
|
+
return face;
|
|
386
|
+
};
|
|
387
|
+
const [regular, bold, mono] = await Promise.all([
|
|
388
|
+
faceFor(fonts.regular),
|
|
389
|
+
faceFor(fonts.bold ?? fonts.regular),
|
|
390
|
+
faceFor(fonts.mono ?? fonts.regular)
|
|
391
|
+
]);
|
|
392
|
+
return /* @__PURE__ */ new Map([
|
|
393
|
+
[standard.regular, regular],
|
|
394
|
+
[standard.italic, regular],
|
|
395
|
+
[standard.bold, bold],
|
|
396
|
+
[standard.boldItalic, bold],
|
|
397
|
+
[standard.mono, mono],
|
|
398
|
+
[standard.monoBold, mono]
|
|
399
|
+
]);
|
|
400
|
+
}
|
|
401
|
+
var PdfTextRuns = class {
|
|
402
|
+
constructor(tracker, faces = /* @__PURE__ */ new Map()) {
|
|
403
|
+
this.tracker = tracker;
|
|
404
|
+
this.faces = faces;
|
|
405
|
+
}
|
|
406
|
+
split(text, font) {
|
|
407
|
+
const face = this.faces.get(font);
|
|
408
|
+
if (!face || !hasUnencodableText(text)) return [{ text: this.tracker.clean(text), font }];
|
|
409
|
+
const runs = [];
|
|
410
|
+
const push = (value, runFont) => {
|
|
411
|
+
if (value.length === 0) return;
|
|
412
|
+
const last = runs[runs.length - 1];
|
|
413
|
+
if (last && last.font === runFont) last.text += value;
|
|
414
|
+
else runs.push({ text: value, font: runFont });
|
|
415
|
+
};
|
|
416
|
+
const drawable = (codePoint) => !isInvisibleOrSpace(codePoint) && face.covers(codePoint);
|
|
417
|
+
for (const word of text.normalize("NFC").split(/(\s+)/u)) {
|
|
418
|
+
if (word.length === 0) continue;
|
|
419
|
+
const codePoints = Array.from(word, (ch) => ch.codePointAt(0));
|
|
420
|
+
if (codePoints.every(isWinAnsiEncodable)) {
|
|
421
|
+
push(word, font);
|
|
422
|
+
} else if (codePoints.every(drawable)) {
|
|
423
|
+
push(word, face.font);
|
|
424
|
+
} else {
|
|
425
|
+
for (const ch of word) {
|
|
426
|
+
const codePoint = ch.codePointAt(0);
|
|
427
|
+
if (isWinAnsiEncodable(codePoint)) push(ch, font);
|
|
428
|
+
else if (drawable(codePoint)) push(ch, face.font);
|
|
429
|
+
else push(this.tracker.clean(ch), font);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return runs.length > 0 ? runs : [{ text: "", font }];
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
|
|
330
437
|
// src/pdf/styles.ts
|
|
331
438
|
var PAGE_WIDTH_LETTER = 612;
|
|
332
439
|
var PAGE_HEIGHT_LETTER = 792;
|
|
@@ -401,7 +508,7 @@ async function markdownDocToPdf(doc, options = {}) {
|
|
|
401
508
|
const ctx = await createExportContext(pdfDoc, options, doc);
|
|
402
509
|
ctx.images = await embedDocumentImages(pdfDoc, doc, options.images);
|
|
403
510
|
renderBlocks(doc.children, ctx, 0);
|
|
404
|
-
const warning = ctx.text.warning();
|
|
511
|
+
const warning = ctx.text.warning({ unicodeFont: ctx.unicodeFont });
|
|
405
512
|
if (warning) {
|
|
406
513
|
if (options.onWarning) options.onWarning(warning);
|
|
407
514
|
else console.warn(warning);
|
|
@@ -455,10 +562,22 @@ async function createExportContext(pdfDoc, options, doc) {
|
|
|
455
562
|
pdfDoc.embedFont(StandardFonts.Courier),
|
|
456
563
|
pdfDoc.embedFont(StandardFonts.CourierBold)
|
|
457
564
|
]);
|
|
565
|
+
let unicodeFaces = /* @__PURE__ */ new Map();
|
|
566
|
+
if (options.unicodeFonts !== false && needsUnicodeFont(doc)) {
|
|
567
|
+
const faces = options.unicodeFonts ?? await loadBundledUnicodeFonts();
|
|
568
|
+
unicodeFaces = await embedUnicodeFaces(pdfDoc, faces, {
|
|
569
|
+
regular,
|
|
570
|
+
bold,
|
|
571
|
+
italic,
|
|
572
|
+
boldItalic,
|
|
573
|
+
mono,
|
|
574
|
+
monoBold
|
|
575
|
+
});
|
|
576
|
+
}
|
|
458
577
|
const icons = {};
|
|
459
578
|
const iconFamilies = collectInlineIconFamilies(doc);
|
|
460
579
|
if (iconFamilies.size > 0) {
|
|
461
|
-
pdfDoc.registerFontkit(
|
|
580
|
+
pdfDoc.registerFontkit(fontkit2);
|
|
462
581
|
await Promise.all(
|
|
463
582
|
fontAwesomeFaces(iconFamilies).map(async (face) => {
|
|
464
583
|
icons[face.family] = await pdfDoc.embedFont(face.data, { subset: true });
|
|
@@ -483,6 +602,7 @@ async function createExportContext(pdfDoc, options, doc) {
|
|
|
483
602
|
colorLink = paperInk(theme.colors.highlight || theme.colors.secondary, COLOR_LINK);
|
|
484
603
|
}
|
|
485
604
|
}
|
|
605
|
+
const text = new WinAnsiTracker();
|
|
486
606
|
return {
|
|
487
607
|
pdfDoc,
|
|
488
608
|
fonts: { regular, bold, italic, boldItalic, mono, monoBold, icons },
|
|
@@ -496,7 +616,9 @@ async function createExportContext(pdfDoc, options, doc) {
|
|
|
496
616
|
bottomY: margin,
|
|
497
617
|
colors: { text: colorText, heading: colorHeading, link: colorLink },
|
|
498
618
|
footnotes: new FootnoteIndex(doc),
|
|
499
|
-
text
|
|
619
|
+
text,
|
|
620
|
+
runs: new PdfTextRuns(text, unicodeFaces),
|
|
621
|
+
unicodeFont: unicodeFaces.size > 0,
|
|
500
622
|
signal: options.signal,
|
|
501
623
|
images: /* @__PURE__ */ new Map()
|
|
502
624
|
};
|
|
@@ -522,15 +644,15 @@ function flattenInlines(nodes, ctx, state) {
|
|
|
522
644
|
case "text": {
|
|
523
645
|
const font = state.code ? state.bold ? ctx.fonts.monoBold : ctx.fonts.mono : pickFont(ctx, state.bold, state.italic);
|
|
524
646
|
const baseSize = state.code ? CODE_FONT_SIZE : ctx.fontSize;
|
|
525
|
-
spans.push(
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
647
|
+
spans.push(
|
|
648
|
+
...textSpans(ctx, node.value, font, {
|
|
649
|
+
fontSize: state.vertAlign ? baseSize * VERT_ALIGN_SCALE : baseSize,
|
|
650
|
+
color: state.code ? COLOR_CODE_TEXT : state.color ?? ctx.colors.text,
|
|
651
|
+
link: state.link,
|
|
652
|
+
strikethrough: state.strikethrough,
|
|
653
|
+
...state.vertAlign ? { baselineShift: vertAlignShift(state.vertAlign, baseSize) } : {}
|
|
654
|
+
})
|
|
655
|
+
);
|
|
534
656
|
break;
|
|
535
657
|
}
|
|
536
658
|
case "strong":
|
|
@@ -561,13 +683,13 @@ function flattenInlines(nodes, ctx, state) {
|
|
|
561
683
|
);
|
|
562
684
|
break;
|
|
563
685
|
case "inlineCode": {
|
|
564
|
-
spans.push(
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
686
|
+
spans.push(
|
|
687
|
+
...textSpans(ctx, node.value, ctx.fonts.mono, {
|
|
688
|
+
fontSize: CODE_FONT_SIZE,
|
|
689
|
+
color: COLOR_CODE_TEXT,
|
|
690
|
+
link: state.link
|
|
691
|
+
})
|
|
692
|
+
);
|
|
571
693
|
break;
|
|
572
694
|
}
|
|
573
695
|
case "link": {
|
|
@@ -584,12 +706,12 @@ function flattenInlines(nodes, ctx, state) {
|
|
|
584
706
|
}
|
|
585
707
|
case "image": {
|
|
586
708
|
const imgNode = node;
|
|
587
|
-
spans.push(
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
709
|
+
spans.push(
|
|
710
|
+
...textSpans(ctx, imgNode.alt ? `[Image: ${imgNode.alt}]` : "[Image]", ctx.fonts.italic, {
|
|
711
|
+
fontSize: ctx.fontSize,
|
|
712
|
+
color: COLOR_BLOCKQUOTE_TEXT
|
|
713
|
+
})
|
|
714
|
+
);
|
|
593
715
|
break;
|
|
594
716
|
}
|
|
595
717
|
case "inlineIcon": {
|
|
@@ -606,14 +728,14 @@ function flattenInlines(nodes, ctx, state) {
|
|
|
606
728
|
strikethrough: state.strikethrough
|
|
607
729
|
});
|
|
608
730
|
} else {
|
|
609
|
-
spans.push(
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
731
|
+
spans.push(
|
|
732
|
+
...textSpans(ctx, `{[${iconNode.token}]}`, pickFont(ctx, state.bold, state.italic), {
|
|
733
|
+
fontSize: ctx.fontSize,
|
|
734
|
+
color: state.color ?? ctx.colors.text,
|
|
735
|
+
link: state.link,
|
|
736
|
+
strikethrough: state.strikethrough
|
|
737
|
+
})
|
|
738
|
+
);
|
|
617
739
|
}
|
|
618
740
|
break;
|
|
619
741
|
}
|
|
@@ -628,33 +750,33 @@ function flattenInlines(nodes, ctx, state) {
|
|
|
628
750
|
case "htmlInline": {
|
|
629
751
|
const html = node.rawHtml;
|
|
630
752
|
if (html) {
|
|
631
|
-
spans.push(
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
753
|
+
spans.push(
|
|
754
|
+
...textSpans(ctx, html, ctx.fonts.mono, {
|
|
755
|
+
fontSize: CODE_FONT_SIZE,
|
|
756
|
+
color: COLOR_CODE_TEXT
|
|
757
|
+
})
|
|
758
|
+
);
|
|
637
759
|
}
|
|
638
760
|
break;
|
|
639
761
|
}
|
|
640
762
|
case "inlineMath":
|
|
641
|
-
spans.push(
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
763
|
+
spans.push(
|
|
764
|
+
...textSpans(ctx, node.value, ctx.fonts.mono, {
|
|
765
|
+
fontSize: CODE_FONT_SIZE,
|
|
766
|
+
color: COLOR_CODE_TEXT
|
|
767
|
+
})
|
|
768
|
+
);
|
|
647
769
|
break;
|
|
648
770
|
case "footnoteReference": {
|
|
649
771
|
const ref = node;
|
|
650
772
|
const marker = ctx.footnotes ? String(ctx.footnotes.cite(ref.identifier).number) : ref.label ?? ref.identifier;
|
|
651
|
-
spans.push(
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
773
|
+
spans.push(
|
|
774
|
+
...textSpans(ctx, marker, ctx.fonts.regular, {
|
|
775
|
+
fontSize: ctx.fontSize * VERT_ALIGN_SCALE,
|
|
776
|
+
color: ctx.colors.link,
|
|
777
|
+
baselineShift: vertAlignShift("superscript", ctx.fontSize)
|
|
778
|
+
})
|
|
779
|
+
);
|
|
658
780
|
break;
|
|
659
781
|
}
|
|
660
782
|
// linkReference, imageReference, textDirective — render children or identifier
|
|
@@ -663,12 +785,12 @@ function flattenInlines(nodes, ctx, state) {
|
|
|
663
785
|
if (fallback.children) {
|
|
664
786
|
spans.push(...flattenInlines(fallback.children, ctx, state));
|
|
665
787
|
} else if (fallback.value) {
|
|
666
|
-
spans.push(
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
788
|
+
spans.push(
|
|
789
|
+
...textSpans(ctx, fallback.value, pickFont(ctx, state.bold, state.italic), {
|
|
790
|
+
fontSize: ctx.fontSize,
|
|
791
|
+
color: state.color ?? ctx.colors.text
|
|
792
|
+
})
|
|
793
|
+
);
|
|
672
794
|
}
|
|
673
795
|
break;
|
|
674
796
|
}
|
|
@@ -676,6 +798,23 @@ function flattenInlines(nodes, ctx, state) {
|
|
|
676
798
|
}
|
|
677
799
|
return spans;
|
|
678
800
|
}
|
|
801
|
+
function textSpans(ctx, text, font, style) {
|
|
802
|
+
return ctx.runs.split(text, font).map((run) => ({ ...style, text: run.text, font: run.font }));
|
|
803
|
+
}
|
|
804
|
+
function drawTextLine(ctx, text, font, at) {
|
|
805
|
+
let x = at.x;
|
|
806
|
+
for (const run of ctx.runs.split(text, font)) {
|
|
807
|
+
if (run.text.length === 0) continue;
|
|
808
|
+
ctx.page.drawText(run.text, {
|
|
809
|
+
x,
|
|
810
|
+
y: at.y,
|
|
811
|
+
size: at.size,
|
|
812
|
+
font: run.font,
|
|
813
|
+
color: rgb(at.color.r, at.color.g, at.color.b)
|
|
814
|
+
});
|
|
815
|
+
x += run.font.widthOfTextAtSize(run.text, at.size);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
679
818
|
function pickFont(ctx, bold, italic) {
|
|
680
819
|
if (bold && italic) return ctx.fonts.boldItalic;
|
|
681
820
|
if (bold) return ctx.fonts.bold;
|
|
@@ -967,7 +1106,7 @@ function renderListItem(item, ctx, extraIndent, depth, ordered, counter) {
|
|
|
967
1106
|
function renderCodeBlock(node, ctx, extraIndent) {
|
|
968
1107
|
const x0 = ctx.margin + extraIndent;
|
|
969
1108
|
const _w = ctx.contentWidth - extraIndent;
|
|
970
|
-
const lines = node.value.split("\n")
|
|
1109
|
+
const lines = node.value.split("\n");
|
|
971
1110
|
const lineH = CODE_FONT_SIZE * LINE_HEIGHT_FACTOR;
|
|
972
1111
|
const totalHeight = lines.length * lineH + 12;
|
|
973
1112
|
ensureSpace(ctx, Math.min(totalHeight, ctx.y - ctx.bottomY));
|
|
@@ -977,12 +1116,11 @@ function renderCodeBlock(node, ctx, extraIndent) {
|
|
|
977
1116
|
for (const line of lines) {
|
|
978
1117
|
ensureSpace(ctx, lineH);
|
|
979
1118
|
if (line.length > 0) {
|
|
980
|
-
ctx.
|
|
1119
|
+
drawTextLine(ctx, line, ctx.fonts.mono, {
|
|
981
1120
|
x: x0 + 8,
|
|
982
1121
|
y: ctx.y - CODE_FONT_SIZE,
|
|
983
1122
|
size: CODE_FONT_SIZE,
|
|
984
|
-
|
|
985
|
-
color: rgb(COLOR_CODE_TEXT.r, COLOR_CODE_TEXT.g, COLOR_CODE_TEXT.b)
|
|
1123
|
+
color: COLOR_CODE_TEXT
|
|
986
1124
|
});
|
|
987
1125
|
}
|
|
988
1126
|
ctx.y -= lineH;
|
|
@@ -1083,36 +1221,34 @@ function renderThematicBreak(ctx, extraIndent) {
|
|
|
1083
1221
|
}
|
|
1084
1222
|
function renderHtmlBlock(node, ctx, extraIndent) {
|
|
1085
1223
|
if (!node.rawHtml) return;
|
|
1086
|
-
const lines = node.rawHtml.split("\n")
|
|
1224
|
+
const lines = node.rawHtml.split("\n");
|
|
1087
1225
|
const x0 = ctx.margin + extraIndent;
|
|
1088
1226
|
for (const line of lines) {
|
|
1089
1227
|
if (line.trim().length === 0) continue;
|
|
1090
1228
|
const lineH = CODE_FONT_SIZE * LINE_HEIGHT_FACTOR;
|
|
1091
1229
|
ensureSpace(ctx, lineH);
|
|
1092
|
-
ctx.
|
|
1230
|
+
drawTextLine(ctx, line, ctx.fonts.mono, {
|
|
1093
1231
|
x: x0,
|
|
1094
1232
|
y: ctx.y - CODE_FONT_SIZE,
|
|
1095
1233
|
size: CODE_FONT_SIZE,
|
|
1096
|
-
|
|
1097
|
-
color: rgb(COLOR_CODE_TEXT.r, COLOR_CODE_TEXT.g, COLOR_CODE_TEXT.b)
|
|
1234
|
+
color: COLOR_CODE_TEXT
|
|
1098
1235
|
});
|
|
1099
1236
|
ctx.y -= lineH;
|
|
1100
1237
|
}
|
|
1101
1238
|
ctx.y -= PARAGRAPH_SPACING;
|
|
1102
1239
|
}
|
|
1103
1240
|
function renderMathBlock(node, ctx, extraIndent) {
|
|
1104
|
-
const lines = node.value.split("\n")
|
|
1241
|
+
const lines = node.value.split("\n");
|
|
1105
1242
|
const x0 = ctx.margin + extraIndent;
|
|
1106
1243
|
for (const line of lines) {
|
|
1107
1244
|
const lineH = CODE_FONT_SIZE * LINE_HEIGHT_FACTOR;
|
|
1108
1245
|
ensureSpace(ctx, lineH);
|
|
1109
1246
|
if (line.length > 0) {
|
|
1110
|
-
ctx.
|
|
1247
|
+
drawTextLine(ctx, line, ctx.fonts.mono, {
|
|
1111
1248
|
x: x0,
|
|
1112
1249
|
y: ctx.y - CODE_FONT_SIZE,
|
|
1113
1250
|
size: CODE_FONT_SIZE,
|
|
1114
|
-
|
|
1115
|
-
color: rgb(COLOR_CODE_TEXT.r, COLOR_CODE_TEXT.g, COLOR_CODE_TEXT.b)
|
|
1251
|
+
color: COLOR_CODE_TEXT
|
|
1116
1252
|
});
|
|
1117
1253
|
}
|
|
1118
1254
|
ctx.y -= lineH;
|
|
@@ -1121,15 +1257,14 @@ function renderMathBlock(node, ctx, extraIndent) {
|
|
|
1121
1257
|
}
|
|
1122
1258
|
function renderFootnoteDefinition(node, ctx, extraIndent) {
|
|
1123
1259
|
const number = ctx.footnotes?.numberFor(node.identifier);
|
|
1124
|
-
const label =
|
|
1260
|
+
const label = number !== void 0 ? `${number}.` : `[${node.identifier}]`;
|
|
1125
1261
|
const lineH = ctx.fontSize * LINE_HEIGHT_FACTOR;
|
|
1126
1262
|
ensureSpace(ctx, lineH);
|
|
1127
|
-
ctx.
|
|
1263
|
+
drawTextLine(ctx, label, ctx.fonts.bold, {
|
|
1128
1264
|
x: ctx.margin + extraIndent,
|
|
1129
1265
|
y: ctx.y - ctx.fontSize * 0.75,
|
|
1130
1266
|
size: ctx.fontSize * 0.75,
|
|
1131
|
-
|
|
1132
|
-
color: rgb(ctx.colors.link.r, ctx.colors.link.g, ctx.colors.link.b)
|
|
1267
|
+
color: ctx.colors.link
|
|
1133
1268
|
});
|
|
1134
1269
|
renderBlocks(node.children, ctx, extraIndent + LIST_INDENT);
|
|
1135
1270
|
}
|
|
@@ -1163,12 +1298,11 @@ function renderFallbackBlock(node, ctx, extraIndent) {
|
|
|
1163
1298
|
} else if (fallback.value && typeof fallback.value === "string") {
|
|
1164
1299
|
const lineH = ctx.fontSize * LINE_HEIGHT_FACTOR;
|
|
1165
1300
|
ensureSpace(ctx, lineH);
|
|
1166
|
-
|
|
1301
|
+
drawTextLine(ctx, fallback.value, ctx.fonts.regular, {
|
|
1167
1302
|
x: ctx.margin + extraIndent,
|
|
1168
1303
|
y: ctx.y - ctx.fontSize,
|
|
1169
1304
|
size: ctx.fontSize,
|
|
1170
|
-
|
|
1171
|
-
color: rgb(ctx.colors.text.r, ctx.colors.text.g, ctx.colors.text.b)
|
|
1305
|
+
color: ctx.colors.text
|
|
1172
1306
|
});
|
|
1173
1307
|
ctx.y -= lineH + PARAGRAPH_SPACING;
|
|
1174
1308
|
}
|
|
@@ -2,6 +2,10 @@ import {
|
|
|
2
2
|
extractFilename,
|
|
3
3
|
inferMimeType
|
|
4
4
|
} from "./chunk-YVCUNZB5.js";
|
|
5
|
+
import {
|
|
6
|
+
exportImageBytes,
|
|
7
|
+
svgIslandsAsImages
|
|
8
|
+
} from "./chunk-VCZUXQUL.js";
|
|
5
9
|
import {
|
|
6
10
|
extractPlainText
|
|
7
11
|
} from "./chunk-AVOZAKGP.js";
|
|
@@ -23,8 +27,11 @@ import JSZip from "jszip";
|
|
|
23
27
|
import { resolveFontFamily } from "@bendyline/squisq/schemas";
|
|
24
28
|
import { resolveThemeForDoc } from "@bendyline/squisq/doc";
|
|
25
29
|
import { readFrontmatterThemeId, sanitizeUrl } from "@bendyline/squisq/markdown";
|
|
26
|
-
async function markdownDocToEpub(
|
|
30
|
+
async function markdownDocToEpub(sourceDoc, options = {}) {
|
|
27
31
|
options.signal?.throwIfAborted();
|
|
32
|
+
const themeId = options.themeId ?? readFrontmatterThemeId(sourceDoc.frontmatter);
|
|
33
|
+
const islandColor = themeId ? resolveThemeForDoc(sourceDoc, themeId, options.themeRegistry).colors.text : void 0;
|
|
34
|
+
const { doc, images: islandImages } = svgIslandsAsImages(sourceDoc, { color: islandColor });
|
|
28
35
|
const fmTitle = doc.frontmatter?.title;
|
|
29
36
|
const fmAuthor = doc.frontmatter?.author;
|
|
30
37
|
const title = options.title ?? (typeof fmTitle === "string" ? fmTitle : "Untitled");
|
|
@@ -42,11 +49,12 @@ async function markdownDocToEpub(doc, options = {}) {
|
|
|
42
49
|
const imageEntries = collectDocImages(doc.children);
|
|
43
50
|
const resolvedImages = /* @__PURE__ */ new Map();
|
|
44
51
|
const usedImageNames = /* @__PURE__ */ new Set();
|
|
45
|
-
if (options.images) {
|
|
52
|
+
if (options.images || islandImages.size > 0) {
|
|
46
53
|
let imageIndex = 0;
|
|
47
54
|
for (const src of imageEntries) {
|
|
48
55
|
if ((imageIndex++ & 63) === 0) options.signal?.throwIfAborted();
|
|
49
|
-
const
|
|
56
|
+
const attached = options.images?.get(src);
|
|
57
|
+
const data = islandImages.get(src)?.data ?? (attached && exportImageBytes(src, attached));
|
|
50
58
|
if (data) {
|
|
51
59
|
const filename = uniqueFilename(safeArchiveBasename(src, "image"), usedImageNames);
|
|
52
60
|
usedImageNames.add(filename);
|
|
@@ -54,12 +62,7 @@ async function markdownDocToEpub(doc, options = {}) {
|
|
|
54
62
|
}
|
|
55
63
|
}
|
|
56
64
|
}
|
|
57
|
-
const css = generateStylesheet(
|
|
58
|
-
options.themeId ?? readFrontmatterThemeId(doc.frontmatter),
|
|
59
|
-
doc,
|
|
60
|
-
options.themeRegistry,
|
|
61
|
-
embeddedIconFonts
|
|
62
|
-
);
|
|
65
|
+
const css = generateStylesheet(themeId, doc, options.themeRegistry, embeddedIconFonts);
|
|
63
66
|
const zip = new JSZip();
|
|
64
67
|
zip.file("mimetype", "application/epub+zip", { compression: "STORE" });
|
|
65
68
|
zip.file("META-INF/container.xml", CONTAINER_XML);
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// src/shared/svgIslands.ts
|
|
2
|
+
import {
|
|
3
|
+
isSvgImageSource,
|
|
4
|
+
isSvgIslandLang,
|
|
5
|
+
parseSvgIsland,
|
|
6
|
+
svgIslandMarkup,
|
|
7
|
+
svgSourceFromBytes
|
|
8
|
+
} from "@bendyline/squisq/markdown";
|
|
9
|
+
var SVG_ISLAND_FALLBACK_ALT = "Illustration";
|
|
10
|
+
function ownBuffer(encoded) {
|
|
11
|
+
const bytes = new Uint8Array(encoded.length);
|
|
12
|
+
bytes.set(encoded);
|
|
13
|
+
return bytes.buffer;
|
|
14
|
+
}
|
|
15
|
+
function svgIslandImage(source, options = {}) {
|
|
16
|
+
const result = parseSvgIsland(source);
|
|
17
|
+
if (!result.ok) return null;
|
|
18
|
+
const { island } = result;
|
|
19
|
+
const data = ownBuffer(
|
|
20
|
+
new TextEncoder().encode(
|
|
21
|
+
svgIslandMarkup(island, { color: options.color, replaceCurrentColor: true })
|
|
22
|
+
)
|
|
23
|
+
);
|
|
24
|
+
return {
|
|
25
|
+
data,
|
|
26
|
+
contentType: "image/svg+xml",
|
|
27
|
+
width: island.width,
|
|
28
|
+
height: island.height,
|
|
29
|
+
alt: island.title ?? SVG_ISLAND_FALLBACK_ALT
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function docHasSvgIslands(doc) {
|
|
33
|
+
const visit = (node) => {
|
|
34
|
+
if (node.type === "code") return isSvgIslandLang(node.lang);
|
|
35
|
+
const nested = node.children;
|
|
36
|
+
return Array.isArray(nested) && nested.some(visit);
|
|
37
|
+
};
|
|
38
|
+
return visit(doc);
|
|
39
|
+
}
|
|
40
|
+
function svgIslandsAsImages(doc, options = {}) {
|
|
41
|
+
const prefix = options.prefix ?? "squisq-svg-island";
|
|
42
|
+
const images = /* @__PURE__ */ new Map();
|
|
43
|
+
const replace = (nodes) => {
|
|
44
|
+
let changed = false;
|
|
45
|
+
const next = nodes.map((node) => {
|
|
46
|
+
if (node.type === "code" && isSvgIslandLang(node.lang)) {
|
|
47
|
+
const image = svgIslandImage(node.value, options);
|
|
48
|
+
if (!image) return node;
|
|
49
|
+
const url = `${prefix}-${images.size + 1}.svg`;
|
|
50
|
+
images.set(url, image);
|
|
51
|
+
changed = true;
|
|
52
|
+
const picture = { type: "image", url, alt: image.alt, title: null };
|
|
53
|
+
const paragraph = { type: "paragraph", children: [picture] };
|
|
54
|
+
return paragraph;
|
|
55
|
+
}
|
|
56
|
+
const nested = node.children;
|
|
57
|
+
if (Array.isArray(nested)) {
|
|
58
|
+
const replaced = replace(nested);
|
|
59
|
+
if (replaced !== nested) {
|
|
60
|
+
changed = true;
|
|
61
|
+
return { ...node, children: replaced };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return node;
|
|
65
|
+
});
|
|
66
|
+
return changed ? next : nodes;
|
|
67
|
+
};
|
|
68
|
+
if (!docHasSvgIslands(doc)) return { doc, images };
|
|
69
|
+
const children = replace(doc.children);
|
|
70
|
+
return { doc: children === doc.children ? doc : { ...doc, children }, images };
|
|
71
|
+
}
|
|
72
|
+
function isSvgImageEntry(path, contentType) {
|
|
73
|
+
return isSvgImageSource(path) || /^image\/svg\+xml\b/i.test(contentType ?? "");
|
|
74
|
+
}
|
|
75
|
+
function svgImageFromBytes(data) {
|
|
76
|
+
return svgIslandImage(svgSourceFromBytes(data));
|
|
77
|
+
}
|
|
78
|
+
function exportImageBytes(path, data, contentType) {
|
|
79
|
+
if (!isSvgImageEntry(path, contentType)) return data;
|
|
80
|
+
return svgImageFromBytes(data)?.data ?? null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export {
|
|
84
|
+
svgIslandImage,
|
|
85
|
+
docHasSvgIslands,
|
|
86
|
+
svgIslandsAsImages,
|
|
87
|
+
isSvgImageEntry,
|
|
88
|
+
svgImageFromBytes,
|
|
89
|
+
exportImageBytes
|
|
90
|
+
};
|
|
@@ -117,11 +117,20 @@ function inlineNodeToRuns(node, handlers, format) {
|
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
// src/shared/officeSvgBlip.ts
|
|
121
|
+
var SVG_BLIP_EXT_URI = "{96DAC541-7B7A-43D3-8B79-37D633B846F1}";
|
|
122
|
+
var NS_ASVG = "http://schemas.microsoft.com/office/drawing/2016/SVG/main";
|
|
123
|
+
function blipXml(rasterRelId, svgRelId) {
|
|
124
|
+
if (!svgRelId) return `<a:blip r:embed="${rasterRelId}"/>`;
|
|
125
|
+
return `<a:blip r:embed="${rasterRelId}"><a:extLst><a:ext uri="${SVG_BLIP_EXT_URI}"><asvg:svgBlip xmlns:asvg="${NS_ASVG}" r:embed="${svgRelId}"/></a:ext></a:extLst></a:blip>`;
|
|
126
|
+
}
|
|
127
|
+
|
|
120
128
|
export {
|
|
121
129
|
RelIdAllocator,
|
|
122
130
|
toOoxmlHex,
|
|
123
131
|
normalizeOoxmlHex,
|
|
124
132
|
sanitizeOfficeHyperlink,
|
|
133
|
+
blipXml,
|
|
125
134
|
inlineNodesToRuns,
|
|
126
135
|
inlineNodeToRuns
|
|
127
136
|
};
|