postext 1.24.0 → 1.25.0
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/dist/__tests__/bundle/cjkRulesPin.test.d.ts +2 -0
- package/dist/__tests__/bundle/cjkRulesPin.test.d.ts.map +1 -0
- package/dist/__tests__/bundle/cjkRulesPin.test.js +78 -0
- package/dist/__tests__/bundle/cjkRulesPin.test.js.map +1 -0
- package/dist/__tests__/bundle/designOverflowPin.test.js +4 -2
- package/dist/__tests__/bundle/designOverflowPin.test.js.map +1 -1
- package/dist/__tests__/bundle/flowColumnsPin.test.d.ts +2 -0
- package/dist/__tests__/bundle/flowColumnsPin.test.d.ts.map +1 -0
- package/dist/__tests__/bundle/flowColumnsPin.test.js +40 -0
- package/dist/__tests__/bundle/flowColumnsPin.test.js.map +1 -0
- package/dist/__tests__/bundle/gridBalancingPin.test.d.ts +2 -0
- package/dist/__tests__/bundle/gridBalancingPin.test.d.ts.map +1 -0
- package/dist/__tests__/bundle/gridBalancingPin.test.js +53 -0
- package/dist/__tests__/bundle/gridBalancingPin.test.js.map +1 -0
- package/dist/__tests__/bundle/inlineTableSplitPin.test.d.ts +2 -0
- package/dist/__tests__/bundle/inlineTableSplitPin.test.d.ts.map +1 -0
- package/dist/__tests__/bundle/inlineTableSplitPin.test.js +43 -0
- package/dist/__tests__/bundle/inlineTableSplitPin.test.js.map +1 -0
- package/dist/__tests__/bundle/openerHeadFloatsPin.test.d.ts +2 -0
- package/dist/__tests__/bundle/openerHeadFloatsPin.test.d.ts.map +1 -0
- package/dist/__tests__/bundle/openerHeadFloatsPin.test.js +50 -0
- package/dist/__tests__/bundle/openerHeadFloatsPin.test.js.map +1 -0
- package/dist/__tests__/bundle.test.js +11 -1
- package/dist/__tests__/bundle.test.js.map +1 -1
- package/dist/__tests__/cjk/annotationsRender.test.js +5 -2
- package/dist/__tests__/cjk/annotationsRender.test.js.map +1 -1
- package/dist/__tests__/cjk/circledNumbers.test.d.ts +2 -0
- package/dist/__tests__/cjk/circledNumbers.test.d.ts.map +1 -0
- package/dist/__tests__/cjk/circledNumbers.test.js +83 -0
- package/dist/__tests__/cjk/circledNumbers.test.js.map +1 -0
- package/dist/__tests__/cjk/gridBalancing.test.d.ts +2 -0
- package/dist/__tests__/cjk/gridBalancing.test.d.ts.map +1 -0
- package/dist/__tests__/cjk/gridBalancing.test.js +227 -0
- package/dist/__tests__/cjk/gridBalancing.test.js.map +1 -0
- package/dist/__tests__/cjk/justifyRender.test.js +2 -1
- package/dist/__tests__/cjk/justifyRender.test.js.map +1 -1
- package/dist/__tests__/cjk/titleBreaks.test.d.ts +2 -0
- package/dist/__tests__/cjk/titleBreaks.test.d.ts.map +1 -0
- package/dist/__tests__/cjk/titleBreaks.test.js +76 -0
- package/dist/__tests__/cjk/titleBreaks.test.js.map +1 -0
- package/dist/__tests__/cjk/wordJoiner.test.js +13 -4
- package/dist/__tests__/cjk/wordJoiner.test.js.map +1 -1
- package/dist/__tests__/codeBlocks.test.js +1 -1
- package/dist/__tests__/design/cjkDesignText.test.d.ts +2 -0
- package/dist/__tests__/design/cjkDesignText.test.d.ts.map +1 -0
- package/dist/__tests__/design/cjkDesignText.test.js +130 -0
- package/dist/__tests__/design/cjkDesignText.test.js.map +1 -0
- package/dist/__tests__/design/cjkDesignTextRender.test.d.ts +2 -0
- package/dist/__tests__/design/cjkDesignTextRender.test.d.ts.map +1 -0
- package/dist/__tests__/design/cjkDesignTextRender.test.js +100 -0
- package/dist/__tests__/design/cjkDesignTextRender.test.js.map +1 -0
- package/dist/__tests__/exports.test.js +49 -0
- package/dist/__tests__/exports.test.js.map +1 -1
- package/dist/__tests__/hardBreaks.test.js +1 -1
- package/dist/__tests__/headingInlineMarks.test.js +1 -1
- package/dist/__tests__/headingNumberSpans.test.d.ts +2 -0
- package/dist/__tests__/headingNumberSpans.test.d.ts.map +1 -0
- package/dist/__tests__/headingNumberSpans.test.js +96 -0
- package/dist/__tests__/headingNumberSpans.test.js.map +1 -0
- package/dist/__tests__/mathSizeMigration.test.js +2 -2
- package/dist/__tests__/multipleColumns.test.js +207 -0
- package/dist/__tests__/multipleColumns.test.js.map +1 -1
- package/dist/__tests__/parse/citations.test.js +29 -0
- package/dist/__tests__/parse/citations.test.js.map +1 -1
- package/dist/__tests__/pipeline/calloutInlineResourceGap.test.js +10 -6
- package/dist/__tests__/pipeline/calloutInlineResourceGap.test.js.map +1 -1
- package/dist/__tests__/pipeline/calloutOverflow.test.js +4 -2
- package/dist/__tests__/pipeline/calloutOverflow.test.js.map +1 -1
- package/dist/__tests__/pipeline/calloutSplitMinLinesChild.test.js +1 -1
- package/dist/__tests__/pipeline/calloutTallKeepTogether.test.js +6 -5
- package/dist/__tests__/pipeline/calloutTallKeepTogether.test.js.map +1 -1
- package/dist/__tests__/pipeline/colonListRoom.test.js +1 -1
- package/dist/__tests__/pipeline/compoundBreaks.test.js +1 -1
- package/dist/__tests__/pipeline/dashBreaks.test.js +1 -1
- package/dist/__tests__/pipeline/floatCitingPage.test.d.ts +2 -0
- package/dist/__tests__/pipeline/floatCitingPage.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/floatCitingPage.test.js +241 -0
- package/dist/__tests__/pipeline/floatCitingPage.test.js.map +1 -0
- package/dist/__tests__/pipeline/floatOverInlineResource.test.js +18 -6
- package/dist/__tests__/pipeline/floatOverInlineResource.test.js.map +1 -1
- package/dist/__tests__/pipeline/floatPlacement.test.js +3 -3
- package/dist/__tests__/pipeline/floatPlacement.test.js.map +1 -1
- package/dist/__tests__/pipeline/flowColumns.test.d.ts +2 -0
- package/dist/__tests__/pipeline/flowColumns.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/flowColumns.test.js +261 -0
- package/dist/__tests__/pipeline/flowColumns.test.js.map +1 -0
- package/dist/__tests__/pipeline/headingKeepFitEps.test.d.ts +2 -0
- package/dist/__tests__/pipeline/headingKeepFitEps.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/headingKeepFitEps.test.js +83 -0
- package/dist/__tests__/pipeline/headingKeepFitEps.test.js.map +1 -0
- package/dist/__tests__/pipeline/headingSplitOrphans.test.js +1 -1
- package/dist/__tests__/pipeline/inlineFigureSpace.test.js +1 -1
- package/dist/__tests__/pipeline/inlineTableSplit.test.d.ts +2 -0
- package/dist/__tests__/pipeline/inlineTableSplit.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/inlineTableSplit.test.js +238 -0
- package/dist/__tests__/pipeline/inlineTableSplit.test.js.map +1 -0
- package/dist/__tests__/pipeline/paragraphContainerTail.test.js +1 -1
- package/dist/__tests__/pipeline/raggedOptimal.test.js +1 -1
- package/dist/__tests__/pipeline/resolvedPaletteCopy.test.js +3 -2
- package/dist/__tests__/pipeline/resolvedPaletteCopy.test.js.map +1 -1
- package/dist/__tests__/pipeline/sideBoxAfterText.test.d.ts +2 -0
- package/dist/__tests__/pipeline/sideBoxAfterText.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/sideBoxAfterText.test.js +163 -0
- package/dist/__tests__/pipeline/sideBoxAfterText.test.js.map +1 -0
- package/dist/__tests__/prepareFonts.test.d.ts +2 -0
- package/dist/__tests__/prepareFonts.test.d.ts.map +1 -0
- package/dist/__tests__/prepareFonts.test.js +358 -0
- package/dist/__tests__/prepareFonts.test.js.map +1 -0
- package/dist/__tests__/svgFonts.test.d.ts +2 -0
- package/dist/__tests__/svgFonts.test.d.ts.map +1 -0
- package/dist/__tests__/svgFonts.test.js +286 -0
- package/dist/__tests__/svgFonts.test.js.map +1 -0
- package/dist/__tests__/verseLines.test.js +1 -1
- package/dist/__tests__/vertical/measurePaint.test.js +16 -0
- package/dist/__tests__/vertical/measurePaint.test.js.map +1 -1
- package/dist/bundle/adapters.d.ts +31 -7
- package/dist/bundle/adapters.d.ts.map +1 -1
- package/dist/bundle/adapters.js +113 -34
- package/dist/bundle/adapters.js.map +1 -1
- package/dist/bundle/codec.d.ts.map +1 -1
- package/dist/bundle/codec.js +1 -0
- package/dist/bundle/codec.js.map +1 -1
- package/dist/bundle/configVersion.d.ts +127 -10
- package/dist/bundle/configVersion.d.ts.map +1 -1
- package/dist/bundle/configVersion.js +260 -9
- package/dist/bundle/configVersion.js.map +1 -1
- package/dist/bundle/index.d.ts +3 -3
- package/dist/bundle/index.d.ts.map +1 -1
- package/dist/bundle/index.js +2 -2
- package/dist/bundle/index.js.map +1 -1
- package/dist/bundle/manifest.d.ts.map +1 -1
- package/dist/bundle/manifest.js +3 -2
- package/dist/bundle/manifest.js.map +1 -1
- package/dist/bundle/types.d.ts +4 -0
- package/dist/bundle/types.d.ts.map +1 -1
- package/dist/canvas-backend/headerFooter.d.ts.map +1 -1
- package/dist/canvas-backend/headerFooter.js +3 -1
- package/dist/canvas-backend/headerFooter.js.map +1 -1
- package/dist/columnRule.d.ts.map +1 -1
- package/dist/columnRule.js +5 -2
- package/dist/columnRule.js.map +1 -1
- package/dist/configWarnings.js +1 -1
- package/dist/configWarnings.js.map +1 -1
- package/dist/defaults/cjk.d.ts +2 -1
- package/dist/defaults/cjk.d.ts.map +1 -1
- package/dist/defaults/cjk.js +17 -1
- package/dist/defaults/cjk.js.map +1 -1
- package/dist/defaults/diagramStyle.d.ts.map +1 -1
- package/dist/defaults/diagramStyle.js +6 -0
- package/dist/defaults/diagramStyle.js.map +1 -1
- package/dist/defaults/headings.d.ts.map +1 -1
- package/dist/defaults/headings.js +3 -0
- package/dist/defaults/headings.js.map +1 -1
- package/dist/defaults/index.d.ts +1 -1
- package/dist/defaults/index.d.ts.map +1 -1
- package/dist/defaults/index.js +1 -1
- package/dist/defaults/index.js.map +1 -1
- package/dist/defaults/layout.d.ts +5 -0
- package/dist/defaults/layout.d.ts.map +1 -1
- package/dist/defaults/layout.js +32 -0
- package/dist/defaults/layout.js.map +1 -1
- package/dist/defaults/tableStyle.d.ts.map +1 -1
- package/dist/defaults/tableStyle.js +6 -0
- package/dist/defaults/tableStyle.js.map +1 -1
- package/dist/design/cjkText.d.ts +34 -0
- package/dist/design/cjkText.d.ts.map +1 -0
- package/dist/design/cjkText.js +82 -0
- package/dist/design/cjkText.js.map +1 -0
- package/dist/design/layout.d.ts.map +1 -1
- package/dist/design/layout.js +103 -5
- package/dist/design/layout.js.map +1 -1
- package/dist/design/richText.d.ts +24 -0
- package/dist/design/richText.d.ts.map +1 -1
- package/dist/design/richText.js +37 -1
- package/dist/design/richText.js.map +1 -1
- package/dist/fonts/buildWithFonts.d.ts +19 -0
- package/dist/fonts/buildWithFonts.d.ts.map +1 -0
- package/dist/fonts/buildWithFonts.js +16 -0
- package/dist/fonts/buildWithFonts.js.map +1 -0
- package/dist/fonts/faces.d.ts +88 -0
- package/dist/fonts/faces.d.ts.map +1 -0
- package/dist/fonts/faces.js +264 -0
- package/dist/fonts/faces.js.map +1 -0
- package/dist/fonts/fontSet.d.ts +34 -0
- package/dist/fonts/fontSet.d.ts.map +1 -0
- package/dist/fonts/fontSet.js +173 -0
- package/dist/fonts/fontSet.js.map +1 -0
- package/dist/fonts/prepare.d.ts +120 -0
- package/dist/fonts/prepare.d.ts.map +1 -0
- package/dist/fonts/prepare.js +375 -0
- package/dist/fonts/prepare.js.map +1 -0
- package/dist/html-backend.d.ts +17 -0
- package/dist/html-backend.d.ts.map +1 -1
- package/dist/html-backend.js +87 -3
- package/dist/html-backend.js.map +1 -1
- package/dist/index.d.ts +25 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -2
- package/dist/index.js.map +1 -1
- package/dist/measure/cache.d.ts.map +1 -1
- package/dist/measure/cache.js +32 -2
- package/dist/measure/cache.js.map +1 -1
- package/dist/measure/canvas.d.ts +15 -0
- package/dist/measure/canvas.d.ts.map +1 -1
- package/dist/measure/canvas.js +68 -4
- package/dist/measure/canvas.js.map +1 -1
- package/dist/measure/cjk.d.ts.map +1 -1
- package/dist/measure/cjk.js +4 -1
- package/dist/measure/cjk.js.map +1 -1
- package/dist/measure/cjkClasses.d.ts +17 -0
- package/dist/measure/cjkClasses.d.ts.map +1 -1
- package/dist/measure/cjkClasses.js +41 -1
- package/dist/measure/cjkClasses.js.map +1 -1
- package/dist/measure/cjkCompose.d.ts.map +1 -1
- package/dist/measure/cjkCompose.js +119 -7
- package/dist/measure/cjkCompose.js.map +1 -1
- package/dist/measure/cjkPunctuation.d.ts +18 -1
- package/dist/measure/cjkPunctuation.d.ts.map +1 -1
- package/dist/measure/cjkPunctuation.js +13 -1
- package/dist/measure/cjkPunctuation.js.map +1 -1
- package/dist/measure/font.d.ts +8 -0
- package/dist/measure/font.d.ts.map +1 -1
- package/dist/measure/font.js +13 -2
- package/dist/measure/font.js.map +1 -1
- package/dist/measure/fontString.d.ts +14 -0
- package/dist/measure/fontString.d.ts.map +1 -0
- package/dist/measure/fontString.js +55 -0
- package/dist/measure/fontString.js.map +1 -0
- package/dist/measure/index.d.ts +4 -1
- package/dist/measure/index.d.ts.map +1 -1
- package/dist/measure/index.js +3 -1
- package/dist/measure/index.js.map +1 -1
- package/dist/measure/types.d.ts +4 -0
- package/dist/measure/types.d.ts.map +1 -1
- package/dist/measure/types.js.map +1 -1
- package/dist/parse/citations.d.ts.map +1 -1
- package/dist/parse/citations.js +7 -3
- package/dist/parse/citations.js.map +1 -1
- package/dist/pipeline/build.d.ts +36 -0
- package/dist/pipeline/build.d.ts.map +1 -1
- package/dist/pipeline/build.js +1065 -193
- package/dist/pipeline/build.js.map +1 -1
- package/dist/pipeline/buildBlockKind.d.ts.map +1 -1
- package/dist/pipeline/buildBlockKind.js +20 -6
- package/dist/pipeline/buildBlockKind.js.map +1 -1
- package/dist/pipeline/buildMeasurement.d.ts +6 -0
- package/dist/pipeline/buildMeasurement.d.ts.map +1 -1
- package/dist/pipeline/buildMeasurement.js +3 -2
- package/dist/pipeline/buildMeasurement.js.map +1 -1
- package/dist/pipeline/calloutLayout.d.ts +59 -0
- package/dist/pipeline/calloutLayout.d.ts.map +1 -1
- package/dist/pipeline/calloutLayout.js +287 -65
- package/dist/pipeline/calloutLayout.js.map +1 -1
- package/dist/pipeline/columnBalancing.d.ts +20 -0
- package/dist/pipeline/columnBalancing.d.ts.map +1 -1
- package/dist/pipeline/columnBalancing.js +22 -4
- package/dist/pipeline/columnBalancing.js.map +1 -1
- package/dist/pipeline/columnsGroup.d.ts +61 -0
- package/dist/pipeline/columnsGroup.d.ts.map +1 -0
- package/dist/pipeline/columnsGroup.js +88 -0
- package/dist/pipeline/columnsGroup.js.map +1 -0
- package/dist/pipeline/config.d.ts +28 -0
- package/dist/pipeline/config.d.ts.map +1 -1
- package/dist/pipeline/config.js +67 -8
- package/dist/pipeline/config.js.map +1 -1
- package/dist/pipeline/contentWarnings.d.ts.map +1 -1
- package/dist/pipeline/contentWarnings.js +28 -0
- package/dist/pipeline/contentWarnings.js.map +1 -1
- package/dist/pipeline/floatPlacement.d.ts +16 -2
- package/dist/pipeline/floatPlacement.d.ts.map +1 -1
- package/dist/pipeline/floatPlacement.js +12 -4
- package/dist/pipeline/floatPlacement.js.map +1 -1
- package/dist/pipeline/floatSlots.d.ts +9 -3
- package/dist/pipeline/floatSlots.d.ts.map +1 -1
- package/dist/pipeline/floatSlots.js +15 -6
- package/dist/pipeline/floatSlots.js.map +1 -1
- package/dist/pipeline/flowColumns.d.ts +41 -0
- package/dist/pipeline/flowColumns.d.ts.map +1 -0
- package/dist/pipeline/flowColumns.js +116 -0
- package/dist/pipeline/flowColumns.js.map +1 -0
- package/dist/pipeline/measureContentBlock.d.ts +13 -1
- package/dist/pipeline/measureContentBlock.d.ts.map +1 -1
- package/dist/pipeline/measureContentBlock.js +20 -5
- package/dist/pipeline/measureContentBlock.js.map +1 -1
- package/dist/pipeline/partPalette.d.ts +3 -0
- package/dist/pipeline/partPalette.d.ts.map +1 -1
- package/dist/pipeline/partPalette.js +5 -0
- package/dist/pipeline/partPalette.js.map +1 -1
- package/dist/pipeline/placement.d.ts +19 -0
- package/dist/pipeline/placement.d.ts.map +1 -1
- package/dist/pipeline/placement.js +5 -2
- package/dist/pipeline/placement.js.map +1 -1
- package/dist/pipeline/resourceLayout.d.ts +26 -0
- package/dist/pipeline/resourceLayout.d.ts.map +1 -1
- package/dist/pipeline/resourceLayout.js +53 -3
- package/dist/pipeline/resourceLayout.js.map +1 -1
- package/dist/pipeline/toc.d.ts.map +1 -1
- package/dist/pipeline/toc.js +6 -1
- package/dist/pipeline/toc.js.map +1 -1
- package/dist/svg/fontRegistry.d.ts +66 -0
- package/dist/svg/fontRegistry.d.ts.map +1 -0
- package/dist/svg/fontRegistry.js +294 -0
- package/dist/svg/fontRegistry.js.map +1 -0
- package/dist/svg/fonts.d.ts +140 -0
- package/dist/svg/fonts.d.ts.map +1 -0
- package/dist/svg/fonts.js +642 -0
- package/dist/svg/fonts.js.map +1 -0
- package/dist/svg/image.d.ts +44 -0
- package/dist/svg/image.d.ts.map +1 -0
- package/dist/svg/image.js +67 -0
- package/dist/svg/image.js.map +1 -0
- package/dist/types.d.ts +138 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/util/stableHash.d.ts +20 -0
- package/dist/util/stableHash.d.ts.map +1 -0
- package/dist/util/stableHash.js +59 -0
- package/dist/util/stableHash.js.map +1 -0
- package/dist/vdt.d.ts +118 -3
- package/dist/vdt.d.ts.map +1 -1
- package/dist/vdt.js.map +1 -1
- package/dist/worker/client.d.ts +8 -0
- package/dist/worker/client.d.ts.map +1 -1
- package/dist/worker/client.js +22 -0
- package/dist/worker/client.js.map +1 -1
- package/dist/worker/fonts.d.ts +1 -13
- package/dist/worker/fonts.d.ts.map +1 -1
- package/dist/worker/fonts.js +3 -61
- package/dist/worker/fonts.js.map +1 -1
- package/dist/worker/layout.worker.js +23 -16
- package/dist/worker/layout.worker.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
// The document's fonts inside SVG pictures (#630).
|
|
2
|
+
//
|
|
3
|
+
// An SVG is shown through an image (`<img>`, a canvas `drawImage`, an EPUB
|
|
4
|
+
// picture), and an image document cannot see the page's web fonts, so
|
|
5
|
+
// `<text font-family="IBM Plex Sans">` falls back to a system face. Before
|
|
6
|
+
// an SVG is decoded or handed out as a URL, the faces its text runs ask for
|
|
7
|
+
// are embedded in it: one `@font-face` per font file, its bytes as a data
|
|
8
|
+
// URI, in a `<style>` right after the root start tag.
|
|
9
|
+
//
|
|
10
|
+
// svgFontRequests(svg) the (families, weight, style)
|
|
11
|
+
// each run asks for, with its
|
|
12
|
+
// characters
|
|
13
|
+
// inlineSvgFonts(svg, provider, opts) embed the faces a provider
|
|
14
|
+
// answers (async)
|
|
15
|
+
// inlineSvgFontsSync(svg, provider, opts) the same from bytes in memory
|
|
16
|
+
//
|
|
17
|
+
// The provider has the contract of postext-pdf's `PdfFontProvider`, so one
|
|
18
|
+
// provider serves both: it is told the characters each face sets and may
|
|
19
|
+
// answer with only the unicode-range slices that hold them (Fontsource and
|
|
20
|
+
// Google Fonts serve a family as several such files). A family the SVG
|
|
21
|
+
// declares itself with `@font-face` is left alone. Inlining happens at
|
|
22
|
+
// decode or export time only, never into the stored resource bytes, and a
|
|
23
|
+
// family `withhold` names (not redistributable) stays out of anything that
|
|
24
|
+
// leaves the app.
|
|
25
|
+
//
|
|
26
|
+
// Single ink recolours the markup first (applySingleInkToSvg), then the
|
|
27
|
+
// faces are inlined: recolour → inline.
|
|
28
|
+
/** Default {@link InlineSvgFontsOptions.maxBytes}: 2 MiB of font files. */
|
|
29
|
+
export const DEFAULT_SVG_FONT_MAX_BYTES = 2 * 1024 * 1024;
|
|
30
|
+
const ALL_FORMATS = ['woff2', 'woff', 'ttf', 'otf'];
|
|
31
|
+
/** Generic CSS families, which never name a loadable face. */
|
|
32
|
+
const GENERIC_FAMILIES = new Set([
|
|
33
|
+
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', 'system-ui', 'ui-serif', 'ui-sans-serif',
|
|
34
|
+
'ui-monospace', 'ui-rounded', 'emoji', 'math', 'fangsong', 'inherit', 'initial', 'unset', 'revert', 'default',
|
|
35
|
+
]);
|
|
36
|
+
/** Elements whose character data is never drawn as text. */
|
|
37
|
+
const NON_TEXT = new Set(['title', 'desc', 'metadata', 'script', 'style']);
|
|
38
|
+
// ------------------------------------------------------------ parsing
|
|
39
|
+
const ENTITIES = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: '\u00a0' };
|
|
40
|
+
function charOf(cp) {
|
|
41
|
+
return Number.isInteger(cp) && cp >= 0 && cp <= 0x10ffff ? String.fromCodePoint(cp) : '';
|
|
42
|
+
}
|
|
43
|
+
function decodeEntities(s) {
|
|
44
|
+
if (!s.includes('&'))
|
|
45
|
+
return s;
|
|
46
|
+
return s.replace(/&(#x[0-9a-fA-F]+|#\d+|[a-zA-Z]+);/g, (m, body) => {
|
|
47
|
+
if (body[0] === '#')
|
|
48
|
+
return charOf(body[1] === 'x' || body[1] === 'X' ? parseInt(body.slice(2), 16) : parseInt(body.slice(1), 10));
|
|
49
|
+
return ENTITIES[body] ?? m;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function localName(name) {
|
|
53
|
+
const i = name.indexOf(':');
|
|
54
|
+
return (i < 0 ? name : name.slice(i + 1)).toLowerCase();
|
|
55
|
+
}
|
|
56
|
+
/** A `font-family` list as names, quotes stripped, generic families and
|
|
57
|
+
* CSS-wide keywords left out. */
|
|
58
|
+
export function parseFontFamilyList(value) {
|
|
59
|
+
const out = [];
|
|
60
|
+
for (const raw of value.split(',')) {
|
|
61
|
+
const name = raw.trim().replace(/^(['"])(.*)\1$/, '$2').trim();
|
|
62
|
+
if (name && !GENERIC_FAMILIES.has(name.toLowerCase()) && !out.includes(name))
|
|
63
|
+
out.push(name);
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
function parseWeight(value, inherited) {
|
|
68
|
+
const v = value.trim().toLowerCase();
|
|
69
|
+
if (v === 'normal')
|
|
70
|
+
return 400;
|
|
71
|
+
if (v === 'bold')
|
|
72
|
+
return 700;
|
|
73
|
+
if (v === 'bolder')
|
|
74
|
+
return inherited < 400 ? 400 : inherited < 600 ? 700 : 900;
|
|
75
|
+
if (v === 'lighter')
|
|
76
|
+
return inherited < 600 ? 100 : inherited < 800 ? 400 : 700;
|
|
77
|
+
const n = Number(v);
|
|
78
|
+
return Number.isFinite(n) && n >= 1 && n <= 1000 ? n : null;
|
|
79
|
+
}
|
|
80
|
+
function parseStyle(value) {
|
|
81
|
+
const v = value.trim().toLowerCase();
|
|
82
|
+
if (v === 'normal')
|
|
83
|
+
return 'normal';
|
|
84
|
+
if (v.startsWith('italic') || v.startsWith('oblique'))
|
|
85
|
+
return 'italic';
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
/** CSS declarations of a `style` attribute or rule body, lower-cased names. */
|
|
89
|
+
function declarations(css) {
|
|
90
|
+
const out = [];
|
|
91
|
+
for (const decl of css.split(';')) {
|
|
92
|
+
const i = decl.indexOf(':');
|
|
93
|
+
if (i < 0)
|
|
94
|
+
continue;
|
|
95
|
+
const name = decl.slice(0, i).trim().toLowerCase();
|
|
96
|
+
const value = decl.slice(i + 1).replace(/!important\s*$/i, '').trim();
|
|
97
|
+
if (name)
|
|
98
|
+
out.push([name, value]);
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
/** The `font` shorthand: `[style] [variant] [weight] size[/line-height]
|
|
103
|
+
* family-list`. */
|
|
104
|
+
function parseFontShorthand(value, ctx) {
|
|
105
|
+
const m = /^(.*?)(?:^|\s)(?:[\d.]+(?:px|pt|em|rem|%|ex|ch|mm|cm|in|pc|q|vh|vw)|0|xx-small|x-small|small|medium|large|x-large|xx-large|smaller|larger)(?:\s*\/\s*\S+)?\s+(.+)$/i.exec(value.trim());
|
|
106
|
+
if (!m)
|
|
107
|
+
return null;
|
|
108
|
+
const out = { families: parseFontFamilyList(m[2]), weight: 400, style: 'normal' };
|
|
109
|
+
for (const word of m[1].trim().split(/\s+/)) {
|
|
110
|
+
if (!word)
|
|
111
|
+
continue;
|
|
112
|
+
const style = parseStyle(word);
|
|
113
|
+
if (style && word.toLowerCase() !== 'normal')
|
|
114
|
+
out.style = style;
|
|
115
|
+
const weight = parseWeight(word, ctx.weight);
|
|
116
|
+
if (weight !== null && word.toLowerCase() !== 'normal')
|
|
117
|
+
out.weight = weight;
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
/** Apply font properties (attributes, then `style`, which wins) to an
|
|
122
|
+
* inherited context. */
|
|
123
|
+
function applyFontProps(ctx, props) {
|
|
124
|
+
let next = ctx;
|
|
125
|
+
for (const [name, value] of props) {
|
|
126
|
+
if (name === 'font-family') {
|
|
127
|
+
next = { ...next, families: parseFontFamilyList(value) };
|
|
128
|
+
}
|
|
129
|
+
else if (name === 'font-weight') {
|
|
130
|
+
const w = parseWeight(value, next.weight);
|
|
131
|
+
if (w !== null)
|
|
132
|
+
next = { ...next, weight: w };
|
|
133
|
+
}
|
|
134
|
+
else if (name === 'font-style') {
|
|
135
|
+
const s = parseStyle(value);
|
|
136
|
+
if (s)
|
|
137
|
+
next = { ...next, style: s };
|
|
138
|
+
}
|
|
139
|
+
else if (name === 'font') {
|
|
140
|
+
const f = parseFontShorthand(value, next);
|
|
141
|
+
if (f)
|
|
142
|
+
next = { ...next, ...f };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return next;
|
|
146
|
+
}
|
|
147
|
+
/** A start tag's attributes. */
|
|
148
|
+
function parseAttributes(tag) {
|
|
149
|
+
const attrs = new Map();
|
|
150
|
+
const re = /([^\s=/>]+)\s*=\s*("([^"]*)"|'([^']*)')/g;
|
|
151
|
+
for (const m of tag.matchAll(re))
|
|
152
|
+
attrs.set(m[1].toLowerCase(), decodeEntities(m[3] ?? m[4] ?? ''));
|
|
153
|
+
return attrs;
|
|
154
|
+
}
|
|
155
|
+
/** Strip CSS comments. */
|
|
156
|
+
function stripCssComments(css) {
|
|
157
|
+
return css.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
158
|
+
}
|
|
159
|
+
/** The rules of a style sheet, `@font-face` ones apart: top-level
|
|
160
|
+
* `prelude { body }` pairs (an at-rule block such as `@media` is opened
|
|
161
|
+
* and its rules read in turn). */
|
|
162
|
+
function cssRules(css) {
|
|
163
|
+
const out = [];
|
|
164
|
+
const text = stripCssComments(css);
|
|
165
|
+
let i = 0;
|
|
166
|
+
while (i < text.length) {
|
|
167
|
+
const open = text.indexOf('{', i);
|
|
168
|
+
if (open < 0)
|
|
169
|
+
break;
|
|
170
|
+
const prelude = text.slice(i, open).trim();
|
|
171
|
+
// The matching brace, counting nested blocks.
|
|
172
|
+
let depth = 1;
|
|
173
|
+
let j = open + 1;
|
|
174
|
+
for (; j < text.length && depth > 0; j++) {
|
|
175
|
+
if (text[j] === '{')
|
|
176
|
+
depth++;
|
|
177
|
+
else if (text[j] === '}')
|
|
178
|
+
depth--;
|
|
179
|
+
}
|
|
180
|
+
const body = text.slice(open + 1, j - 1);
|
|
181
|
+
if (/^@(media|supports|layer|container)\b/i.test(prelude))
|
|
182
|
+
out.push(...cssRules(body));
|
|
183
|
+
else
|
|
184
|
+
out.push({ prelude, body });
|
|
185
|
+
i = j;
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
/** Whether a style sheet holds nothing but `@font-face` rules (comments
|
|
190
|
+
* and whitespace aside): the faces an author embedded, which change no
|
|
191
|
+
* geometry. */
|
|
192
|
+
export function isFontFaceOnlyStyleSheet(css) {
|
|
193
|
+
const text = stripCssComments(css.replace(/<!\[CDATA\[|\]\]>/g, ''));
|
|
194
|
+
if (text.trim() === '')
|
|
195
|
+
return true;
|
|
196
|
+
const rules = cssRules(text);
|
|
197
|
+
if (rules.length === 0)
|
|
198
|
+
return false;
|
|
199
|
+
// Whatever lies outside the rule blocks must be blank.
|
|
200
|
+
const outside = text.replace(/\{[^{}]*\}/g, '').replace(/@font-face/gi, '');
|
|
201
|
+
return outside.trim() === '' && rules.every((r) => /^@font-face$/i.test(r.prelude));
|
|
202
|
+
}
|
|
203
|
+
function scanSvgFonts(svgText) {
|
|
204
|
+
const byKey = new Map();
|
|
205
|
+
const order = [];
|
|
206
|
+
const declared = new Set();
|
|
207
|
+
const sheets = [];
|
|
208
|
+
const allCodePoints = new Set([0x20]);
|
|
209
|
+
const note = (ctx, text) => {
|
|
210
|
+
if (ctx.families.length === 0)
|
|
211
|
+
return;
|
|
212
|
+
const key = `${ctx.families.join('\u0000')}|${ctx.weight}|${ctx.style}`;
|
|
213
|
+
let req = byKey.get(key);
|
|
214
|
+
if (!req) {
|
|
215
|
+
req = { families: [...ctx.families], weight: ctx.weight, style: ctx.style, codePoints: new Set([0x20]) };
|
|
216
|
+
byKey.set(key, req);
|
|
217
|
+
order.push(req);
|
|
218
|
+
}
|
|
219
|
+
for (const ch of text) {
|
|
220
|
+
const cp = ch.codePointAt(0);
|
|
221
|
+
if (cp > 0x20)
|
|
222
|
+
req.codePoints.add(cp);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
const root = { families: [], weight: 400, style: 'normal' };
|
|
226
|
+
const stack = [];
|
|
227
|
+
const top = () => stack[stack.length - 1];
|
|
228
|
+
const onText = (raw, isCdata) => {
|
|
229
|
+
const t = top();
|
|
230
|
+
if (!t)
|
|
231
|
+
return;
|
|
232
|
+
const text = isCdata ? raw : decodeEntities(raw);
|
|
233
|
+
if (t.name === 'style') {
|
|
234
|
+
sheets.push(text);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (t.nonText || !t.inText)
|
|
238
|
+
return;
|
|
239
|
+
for (const ch of text) {
|
|
240
|
+
const cp = ch.codePointAt(0);
|
|
241
|
+
if (cp > 0x20)
|
|
242
|
+
allCodePoints.add(cp);
|
|
243
|
+
}
|
|
244
|
+
note(t.ctx, text);
|
|
245
|
+
};
|
|
246
|
+
const n = svgText.length;
|
|
247
|
+
let i = 0;
|
|
248
|
+
while (i < n) {
|
|
249
|
+
const lt = svgText.indexOf('<', i);
|
|
250
|
+
if (lt < 0) {
|
|
251
|
+
onText(svgText.slice(i), false);
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
if (lt > i)
|
|
255
|
+
onText(svgText.slice(i, lt), false);
|
|
256
|
+
i = lt;
|
|
257
|
+
if (svgText.startsWith('<!--', i)) {
|
|
258
|
+
const end = svgText.indexOf('-->', i + 4);
|
|
259
|
+
i = end < 0 ? n : end + 3;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (svgText.startsWith('<![CDATA[', i)) {
|
|
263
|
+
const end = svgText.indexOf(']]>', i + 9);
|
|
264
|
+
onText(svgText.slice(i + 9, end < 0 ? n : end), true);
|
|
265
|
+
i = end < 0 ? n : end + 3;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (svgText.startsWith('<?', i)) {
|
|
269
|
+
const end = svgText.indexOf('?>', i + 2);
|
|
270
|
+
i = end < 0 ? n : end + 2;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (svgText.startsWith('<!', i)) {
|
|
274
|
+
let depth = 0;
|
|
275
|
+
let j = i + 2;
|
|
276
|
+
for (; j < n; j++) {
|
|
277
|
+
const ch = svgText[j];
|
|
278
|
+
if (ch === '[')
|
|
279
|
+
depth++;
|
|
280
|
+
else if (ch === ']')
|
|
281
|
+
depth--;
|
|
282
|
+
else if (ch === '>' && depth === 0)
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
i = j + 1;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (svgText.startsWith('</', i)) {
|
|
289
|
+
const end = svgText.indexOf('>', i + 2);
|
|
290
|
+
const name = localName(svgText.slice(i + 2, end < 0 ? n : end).trim());
|
|
291
|
+
// Pop to the matching element (tolerates sloppy markup).
|
|
292
|
+
for (let k = stack.length - 1; k >= 0; k--) {
|
|
293
|
+
if (stack[k].name === name) {
|
|
294
|
+
stack.length = k;
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
i = end < 0 ? n : end + 1;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
// A start tag: find its end outside quoted attribute values.
|
|
302
|
+
let j = i + 1;
|
|
303
|
+
let quote = null;
|
|
304
|
+
for (; j < n; j++) {
|
|
305
|
+
const ch = svgText[j];
|
|
306
|
+
if (quote) {
|
|
307
|
+
if (ch === quote)
|
|
308
|
+
quote = null;
|
|
309
|
+
}
|
|
310
|
+
else if (ch === '"' || ch === "'") {
|
|
311
|
+
quote = ch;
|
|
312
|
+
}
|
|
313
|
+
else if (ch === '>') {
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const tag = svgText.slice(i + 1, j);
|
|
318
|
+
i = j + 1;
|
|
319
|
+
const selfClosing = tag.endsWith('/');
|
|
320
|
+
const nameMatch = /^([^\s/>]+)/.exec(tag);
|
|
321
|
+
if (!nameMatch)
|
|
322
|
+
continue;
|
|
323
|
+
const name = localName(nameMatch[1]);
|
|
324
|
+
const parent = top();
|
|
325
|
+
const attrs = parseAttributes(tag);
|
|
326
|
+
const props = [];
|
|
327
|
+
for (const key of ['font-family', 'font-weight', 'font-style']) {
|
|
328
|
+
const v = attrs.get(key);
|
|
329
|
+
if (v !== undefined)
|
|
330
|
+
props.push([key, v]);
|
|
331
|
+
}
|
|
332
|
+
const style = attrs.get('style');
|
|
333
|
+
if (style)
|
|
334
|
+
props.push(...declarations(style));
|
|
335
|
+
const ctx = applyFontProps(parent?.ctx ?? root, props);
|
|
336
|
+
const entry = {
|
|
337
|
+
name,
|
|
338
|
+
ctx,
|
|
339
|
+
inText: (parent?.inText ?? false) || name === 'text',
|
|
340
|
+
nonText: (parent?.nonText ?? false) || NON_TEXT.has(name),
|
|
341
|
+
};
|
|
342
|
+
if (!selfClosing)
|
|
343
|
+
stack.push(entry);
|
|
344
|
+
}
|
|
345
|
+
// Style sheets: `@font-face` rules declare families; a rule that names a
|
|
346
|
+
// family is taken to style the SVG's text (its selector is not matched).
|
|
347
|
+
for (const sheet of sheets) {
|
|
348
|
+
for (const rule of cssRules(sheet)) {
|
|
349
|
+
const decls = declarations(rule.body);
|
|
350
|
+
if (/^@font-face$/i.test(rule.prelude)) {
|
|
351
|
+
for (const [name, value] of decls) {
|
|
352
|
+
if (name === 'font-family')
|
|
353
|
+
for (const f of parseFontFamilyList(value))
|
|
354
|
+
declared.add(f.toLowerCase());
|
|
355
|
+
}
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
if (rule.prelude.startsWith('@'))
|
|
359
|
+
continue;
|
|
360
|
+
const ctx = applyFontProps(root, decls);
|
|
361
|
+
if (ctx.families.length > 0 && decls.some(([k]) => k === 'font-family' || k === 'font')) {
|
|
362
|
+
note(ctx, String.fromCodePoint(...allCodePoints));
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return { requests: order, declared };
|
|
367
|
+
}
|
|
368
|
+
/** Every `(families, weight, style)` the SVG's text runs ask for, with
|
|
369
|
+
* the characters each sets, from `font-family`, `font-weight`,
|
|
370
|
+
* `font-style` and `font` in attributes, `style` attributes and inherited
|
|
371
|
+
* from enclosing elements. A `<style>` rule that names a family counts
|
|
372
|
+
* too (with all the SVG's characters: its selector is not matched).
|
|
373
|
+
* Generic families are ignored, and so is text in `<title>`, `<desc>`
|
|
374
|
+
* and the like. Pure. */
|
|
375
|
+
export function svgFontRequests(svgText) {
|
|
376
|
+
return scanSvgFonts(svgText).requests;
|
|
377
|
+
}
|
|
378
|
+
/** Families the SVG declares in its own `@font-face` rules, as written. */
|
|
379
|
+
export function svgDeclaredFontFamilies(svgText) {
|
|
380
|
+
return [...scanSvgFonts(svgText).declared];
|
|
381
|
+
}
|
|
382
|
+
// ------------------------------------------------------------ files
|
|
383
|
+
/** The format of a font file from its first bytes; null for one an
|
|
384
|
+
* `@font-face` cannot carry (a collection, an unknown file). */
|
|
385
|
+
export function sniffFontFormat(bytes) {
|
|
386
|
+
if (bytes.length < 4)
|
|
387
|
+
return null;
|
|
388
|
+
const tag = String.fromCharCode(bytes[0], bytes[1], bytes[2], bytes[3]);
|
|
389
|
+
if (tag === 'wOF2')
|
|
390
|
+
return 'woff2';
|
|
391
|
+
if (tag === 'wOFF')
|
|
392
|
+
return 'woff';
|
|
393
|
+
if (tag === 'OTTO')
|
|
394
|
+
return 'otf';
|
|
395
|
+
if (tag === 'true' || (bytes[0] === 0 && bytes[1] === 1 && bytes[2] === 0 && bytes[3] === 0))
|
|
396
|
+
return 'ttf';
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
const MIME = { woff2: 'font/woff2', woff: 'font/woff', ttf: 'font/ttf', otf: 'font/otf' };
|
|
400
|
+
const CSS_FORMAT = { woff2: 'woff2', woff: 'woff', ttf: 'truetype', otf: 'opentype' };
|
|
401
|
+
const base64Cache = new WeakMap();
|
|
402
|
+
function toBase64(bytes) {
|
|
403
|
+
const cached = base64Cache.get(bytes);
|
|
404
|
+
if (cached !== undefined)
|
|
405
|
+
return cached;
|
|
406
|
+
let binary = '';
|
|
407
|
+
for (let i = 0; i < bytes.length; i += 0x8000) {
|
|
408
|
+
binary += String.fromCharCode.apply(null, Array.from(bytes.subarray(i, i + 0x8000)));
|
|
409
|
+
}
|
|
410
|
+
const out = btoa(binary);
|
|
411
|
+
base64Cache.set(bytes, out);
|
|
412
|
+
return out;
|
|
413
|
+
}
|
|
414
|
+
/** One `@font-face` rule with the file as a data URI. */
|
|
415
|
+
export function fontFaceRule(family, weight, style, bytes, format = sniffFontFormat(bytes) ?? 'ttf') {
|
|
416
|
+
const name = family.replace(/["\\]/g, '');
|
|
417
|
+
return `@font-face{font-family:"${name}";font-weight:${weight};font-style:${style};`
|
|
418
|
+
+ `src:url(data:${MIME[format]};base64,${toBase64(bytes)}) format("${CSS_FORMAT[format]}")}`;
|
|
419
|
+
}
|
|
420
|
+
/** Insert `css` as a `<style>` right after the root `<svg …>` start tag.
|
|
421
|
+
* Pure; the markup is returned unchanged when there is no root tag (or
|
|
422
|
+
* an empty self-closing one, which has no text). */
|
|
423
|
+
export function injectSvgStyle(svgText, css) {
|
|
424
|
+
if (!css)
|
|
425
|
+
return svgText;
|
|
426
|
+
const m = /<(?:[a-zA-Z_][\w.-]*:)?svg\b(?:[^>"']|"[^"]*"|'[^']*')*?(\/?)>/.exec(svgText);
|
|
427
|
+
if (!m || m[1] === '/')
|
|
428
|
+
return svgText;
|
|
429
|
+
const at = m.index + m[0].length;
|
|
430
|
+
const cdata = `<![CDATA[${css.replace(/]]>/g, ']]]]><![CDATA[>')}]]>`;
|
|
431
|
+
return `${svgText.slice(0, at)}<style type="text/css">${cdata}</style>${svgText.slice(at)}`;
|
|
432
|
+
}
|
|
433
|
+
const faceKey = (family, weight, style) => `${family.toLowerCase()}|${weight}|${style}`;
|
|
434
|
+
/** The plan of an inlining: the faces to look up, then (given their
|
|
435
|
+
* files) the CSS and the report. Shared by the async and sync paths. */
|
|
436
|
+
function planInlining(svgText) {
|
|
437
|
+
const scan = scanSvgFonts(svgText);
|
|
438
|
+
const jobs = new Map();
|
|
439
|
+
for (const req of scan.requests) {
|
|
440
|
+
if (req.families.some((f) => scan.declared.has(f.toLowerCase())))
|
|
441
|
+
continue;
|
|
442
|
+
for (const family of req.families) {
|
|
443
|
+
const key = faceKey(family, req.weight, req.style);
|
|
444
|
+
let job = jobs.get(key);
|
|
445
|
+
if (!job) {
|
|
446
|
+
job = { family, weight: req.weight, style: req.style, codePoints: new Set() };
|
|
447
|
+
jobs.set(key, job);
|
|
448
|
+
}
|
|
449
|
+
for (const cp of req.codePoints)
|
|
450
|
+
job.codePoints.add(cp);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return { scan, jobs };
|
|
454
|
+
}
|
|
455
|
+
function asFiles(answer, formats) {
|
|
456
|
+
const list = Array.isArray(answer) ? answer : answer ? [answer] : [];
|
|
457
|
+
const out = [];
|
|
458
|
+
for (const bytes of list) {
|
|
459
|
+
if (!(bytes instanceof Uint8Array) || bytes.length === 0)
|
|
460
|
+
continue;
|
|
461
|
+
const format = sniffFontFormat(bytes);
|
|
462
|
+
if (format && formats.includes(format))
|
|
463
|
+
out.push({ bytes, format });
|
|
464
|
+
}
|
|
465
|
+
return out;
|
|
466
|
+
}
|
|
467
|
+
function finishInlining(svgText, plan, files, withheldFamilies, options) {
|
|
468
|
+
const maxBytes = options?.maxBytes ?? DEFAULT_SVG_FONT_MAX_BYTES;
|
|
469
|
+
const faces = [];
|
|
470
|
+
const reported = new Set();
|
|
471
|
+
const report = (r) => {
|
|
472
|
+
const key = faceKey(r.family, r.weight, r.style);
|
|
473
|
+
if (reported.has(key))
|
|
474
|
+
return;
|
|
475
|
+
reported.add(key);
|
|
476
|
+
faces.push(r);
|
|
477
|
+
};
|
|
478
|
+
/** Faces chosen to embed, in order. */
|
|
479
|
+
const chosen = [];
|
|
480
|
+
const chosenKeys = new Set();
|
|
481
|
+
const unavailable = [];
|
|
482
|
+
for (const req of plan.scan.requests) {
|
|
483
|
+
const declared = req.families.find((f) => plan.scan.declared.has(f.toLowerCase()));
|
|
484
|
+
if (declared) {
|
|
485
|
+
report({ family: declared, weight: req.weight, style: req.style, status: 'declared' });
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
let resolved = false;
|
|
489
|
+
for (const family of req.families) {
|
|
490
|
+
if (withheldFamilies.has(family.toLowerCase())) {
|
|
491
|
+
report({ family, weight: req.weight, style: req.style, status: 'withheld' });
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
const key = faceKey(family, req.weight, req.style);
|
|
495
|
+
const found = files.get(key);
|
|
496
|
+
if (!found || found.length === 0)
|
|
497
|
+
continue;
|
|
498
|
+
if (!chosenKeys.has(key)) {
|
|
499
|
+
chosenKeys.add(key);
|
|
500
|
+
chosen.push({ family, weight: req.weight, style: req.style, files: found });
|
|
501
|
+
report({ family, weight: req.weight, style: req.style, status: 'inlined' });
|
|
502
|
+
}
|
|
503
|
+
resolved = true;
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
if (!resolved && !req.families.some((f) => withheldFamilies.has(f.toLowerCase()))) {
|
|
507
|
+
const first = req.families[0];
|
|
508
|
+
const key = faceKey(first, req.weight, req.style);
|
|
509
|
+
if (!reported.has(key))
|
|
510
|
+
unavailable.push({ family: first, weight: req.weight, style: req.style, status: 'unavailable' });
|
|
511
|
+
report({ family: first, weight: req.weight, style: req.style, status: 'unavailable' });
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
// One rule per distinct file of a family: a file the provider answered
|
|
515
|
+
// for several weights (the nearest it has) is embedded once, declared
|
|
516
|
+
// for the range of weights it stands for.
|
|
517
|
+
const rules = [];
|
|
518
|
+
for (const face of chosen) {
|
|
519
|
+
for (const file of face.files) {
|
|
520
|
+
const same = rules.find((r) => r.bytes === file.bytes && r.style === face.style && r.family.toLowerCase() === face.family.toLowerCase());
|
|
521
|
+
if (same) {
|
|
522
|
+
if (!same.weights.includes(face.weight))
|
|
523
|
+
same.weights.push(face.weight);
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
rules.push({ family: face.family, weights: [face.weight], style: face.style, bytes: file.bytes, format: file.format });
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
const total = rules.reduce((sum, r) => sum + r.bytes.length, 0);
|
|
531
|
+
const sizeOf = (face) => ({
|
|
532
|
+
bytes: face.files.reduce((s, f) => s + f.bytes.length, 0),
|
|
533
|
+
files: face.files.length,
|
|
534
|
+
});
|
|
535
|
+
for (const w of unavailable)
|
|
536
|
+
options?.onWarning?.({ kind: 'svgFontUnavailable', family: w.family, weight: w.weight, style: w.style });
|
|
537
|
+
const tooLarge = total > maxBytes;
|
|
538
|
+
for (const face of chosen) {
|
|
539
|
+
const r = faces.find((f) => f.status === 'inlined' && faceKey(f.family, f.weight, f.style) === faceKey(face.family, face.weight, face.style));
|
|
540
|
+
if (r)
|
|
541
|
+
Object.assign(r, { status: tooLarge ? 'tooLarge' : 'inlined', ...sizeOf(face) });
|
|
542
|
+
}
|
|
543
|
+
if (tooLarge) {
|
|
544
|
+
options?.onWarning?.({ kind: 'svgFontsTooLarge', bytes: total, maxBytes });
|
|
545
|
+
return { svg: svgText, faces, bytes: 0 };
|
|
546
|
+
}
|
|
547
|
+
if (rules.length === 0)
|
|
548
|
+
return { svg: svgText, faces, bytes: 0 };
|
|
549
|
+
const css = rules.map((r) => {
|
|
550
|
+
const lo = Math.min(...r.weights);
|
|
551
|
+
const hi = Math.max(...r.weights);
|
|
552
|
+
return fontFaceRule(r.family, lo === hi ? lo : `${lo} ${hi}`, r.style, r.bytes, r.format);
|
|
553
|
+
}).join('');
|
|
554
|
+
return { svg: injectSvgStyle(svgText, css), faces, bytes: total };
|
|
555
|
+
}
|
|
556
|
+
function withheldOf(jobs, options) {
|
|
557
|
+
const out = new Set();
|
|
558
|
+
if (!options?.withhold)
|
|
559
|
+
return out;
|
|
560
|
+
for (const job of jobs.values()) {
|
|
561
|
+
const lower = job.family.toLowerCase();
|
|
562
|
+
if (out.has(lower) || !options.withhold(job.family))
|
|
563
|
+
continue;
|
|
564
|
+
out.add(lower);
|
|
565
|
+
options.onWithheld?.(job.family);
|
|
566
|
+
}
|
|
567
|
+
return out;
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Embed the faces an SVG's text asks for (see the module comment), with
|
|
571
|
+
* a report of each face. The provider is asked once per face, with the
|
|
572
|
+
* characters the SVG sets in it; a face it rejects falls through the
|
|
573
|
+
* run's `font-family` list, and a run none of whose families has a face
|
|
574
|
+
* is reported as `svgFontUnavailable`.
|
|
575
|
+
*/
|
|
576
|
+
export async function inlineSvgFontsDetailed(svgText, provider, options) {
|
|
577
|
+
const plan = planInlining(svgText);
|
|
578
|
+
if (plan.scan.requests.length === 0)
|
|
579
|
+
return { svg: svgText, faces: [], bytes: 0 };
|
|
580
|
+
const withheld = withheldOf(plan.jobs, options);
|
|
581
|
+
const formats = options?.formats ?? ALL_FORMATS;
|
|
582
|
+
const files = new Map();
|
|
583
|
+
await Promise.all([...plan.jobs].map(async ([key, job]) => {
|
|
584
|
+
if (withheld.has(job.family.toLowerCase()))
|
|
585
|
+
return;
|
|
586
|
+
const answer = await provider(job.family, job.weight, job.style, { codePoints: job.codePoints }).catch(() => null);
|
|
587
|
+
files.set(key, asFiles(answer, formats));
|
|
588
|
+
}));
|
|
589
|
+
return finishInlining(svgText, plan, files, withheld, options);
|
|
590
|
+
}
|
|
591
|
+
/** {@link inlineSvgFontsDetailed} from a provider that answers from
|
|
592
|
+
* memory, for hosts that build their URLs synchronously
|
|
593
|
+
* (`bundleImageUrl`, `renderToHtml`). */
|
|
594
|
+
export function inlineSvgFontsDetailedSync(svgText, provider, options) {
|
|
595
|
+
const plan = planInlining(svgText);
|
|
596
|
+
if (plan.scan.requests.length === 0)
|
|
597
|
+
return { svg: svgText, faces: [], bytes: 0 };
|
|
598
|
+
const withheld = withheldOf(plan.jobs, options);
|
|
599
|
+
const formats = options?.formats ?? ALL_FORMATS;
|
|
600
|
+
const files = new Map();
|
|
601
|
+
for (const [key, job] of plan.jobs) {
|
|
602
|
+
if (withheld.has(job.family.toLowerCase()))
|
|
603
|
+
continue;
|
|
604
|
+
let answer = null;
|
|
605
|
+
try {
|
|
606
|
+
answer = provider(job.family, job.weight, job.style, { codePoints: job.codePoints });
|
|
607
|
+
}
|
|
608
|
+
catch {
|
|
609
|
+
answer = null;
|
|
610
|
+
}
|
|
611
|
+
files.set(key, asFiles(answer, formats));
|
|
612
|
+
}
|
|
613
|
+
return finishInlining(svgText, plan, files, withheld, options);
|
|
614
|
+
}
|
|
615
|
+
/** Embed the faces an SVG's text asks for as `@font-face` data URIs and
|
|
616
|
+
* return the markup (see {@link inlineSvgFontsDetailed}). */
|
|
617
|
+
export async function inlineSvgFonts(svgText, provider, options) {
|
|
618
|
+
return (await inlineSvgFontsDetailed(svgText, provider, options)).svg;
|
|
619
|
+
}
|
|
620
|
+
/** {@link inlineSvgFonts} from a provider that answers from memory. */
|
|
621
|
+
export function inlineSvgFontsSync(svgText, provider, options) {
|
|
622
|
+
return inlineSvgFontsDetailedSync(svgText, provider, options).svg;
|
|
623
|
+
}
|
|
624
|
+
/** Providers tried in order: the first that answers wins. */
|
|
625
|
+
export function chainSvgFontProviders(...providers) {
|
|
626
|
+
const list = providers.filter((p) => !!p);
|
|
627
|
+
return async (family, weight, style, request) => {
|
|
628
|
+
let last = new Error(`No face for "${family}"`);
|
|
629
|
+
for (const p of list) {
|
|
630
|
+
try {
|
|
631
|
+
const answer = await p(family, weight, style, request);
|
|
632
|
+
if (Array.isArray(answer) ? answer.length > 0 : answer && answer.length > 0)
|
|
633
|
+
return answer;
|
|
634
|
+
}
|
|
635
|
+
catch (err) {
|
|
636
|
+
last = err;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
throw last;
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
//# sourceMappingURL=fonts.js.map
|