@segnavia/html 0.27.0 → 0.28.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.
Files changed (39) hide show
  1. package/dist/bindings.d.ts +1 -1
  2. package/dist/bindings.js +4 -1
  3. package/dist/book/book-document.js +146 -65
  4. package/dist/book/book-held.js +182 -0
  5. package/dist/book/book-view.d.ts +5 -0
  6. package/dist/book/book-view.js +26 -0
  7. package/dist/book/furniture.d.ts +14 -0
  8. package/dist/book/furniture.js +36 -0
  9. package/dist/book/notes.js +18 -9
  10. package/dist/book/paginate-book.d.ts +11 -0
  11. package/dist/book/paginate-book.js +195 -0
  12. package/dist/book/paginate-columns.d.ts +8 -0
  13. package/dist/book/paginate-columns.js +119 -0
  14. package/dist/book/paginate-keeps.d.ts +4 -0
  15. package/dist/book/paginate-keeps.js +80 -0
  16. package/dist/book/paginate-marks.d.ts +7 -0
  17. package/dist/book/paginate-marks.js +34 -0
  18. package/dist/book/paginate-notes.d.ts +21 -0
  19. package/dist/book/paginate-notes.js +105 -0
  20. package/dist/book/paginate-sequence.d.ts +4 -0
  21. package/dist/book/paginate-sequence.js +267 -0
  22. package/dist/book/paginate-target.d.ts +13 -0
  23. package/dist/book/paginate-target.js +36 -0
  24. package/dist/book/paginate-types.d.ts +170 -0
  25. package/dist/book/types.d.ts +1 -1
  26. package/dist/book-css.d.ts +5 -0
  27. package/dist/book-css.js +14 -0
  28. package/dist/css-tokens.js +189 -0
  29. package/dist/index.d.ts +8 -2
  30. package/dist/index.js +7 -2
  31. package/dist/motifs.d.ts +1 -0
  32. package/dist/motifs.js +5 -1
  33. package/dist/printed-pages.d.ts +18 -4
  34. package/dist/printed-pages.js +41 -19
  35. package/dist/sanitize-css.d.ts +6 -0
  36. package/dist/sanitize-css.js +308 -0
  37. package/dist/sanitize.d.ts +1 -0
  38. package/dist/sanitize.js +37 -15
  39. package/package.json +2 -2
package/dist/index.d.ts CHANGED
@@ -3,11 +3,17 @@ import { REFUSED_ELEMENTS, SanitizeOptions, findActiveContent, sanitize } from "
3
3
  import { BINDINGS, FillOptions, Filled, bindingNames, bindingRoot, fill, hasBindings } from "./bindings.js";
4
4
  import { BookDocument, BookDocumentOptions, BookHeading, BookPrintPart, BookSectionInfo, bookPrintPart } from "./book/types.js";
5
5
  import { bookDocument } from "./book/book-document.js";
6
+ import { bookView, bookViewFromHtml } from "./book/book-view.js";
7
+ import { PaginateBlock, PaginateBookInput, PaginateBookPage, PaginateBookRefusal, PaginateBookResult, PaginateFlow, PaginateFlowParents, PaginateLine, PaginateMark, PaginateNote, PaginateNoteRef, PaginateOpens, PaginatePageMarks, PaginatePageNote, PaginatePageRole, PaginateSequence, PaginateWindow } from "./book/paginate-types.js";
8
+ import { BookFurnitureValues, embedFunction, fillBookFurniture, furnitureScript } from "./book/furniture.js";
9
+ import { PAGINATE_FUNCTIONS, paginateBook, paginateScript } from "./book/paginate-book.js";
10
+ import { PaginateTarget, PaginateTargetPage, heldPageNumber, openerPageIndex, pageIndexForTarget, windowPageIndex } from "./book/paginate-target.js";
11
+ import { bookStylesheet, sanitizeBookCss } from "./book-css.js";
6
12
  import { FontCssRefusedError, fontCss, fontFaceCss } from "./font-css.js";
7
13
  import { fontFormat } from "./font-format.js";
8
14
  import { fontReferenceRefusals } from "./font-references.js";
9
15
  import { ExpandMotifsOptions, ExpandedMotifs, MOTIF_DEPTH_LIMIT, MotifRefusedError, PlacedMotif, expandMotifs, hasMotifPlacement, libraryMotifPlacementRefusal, motifPlacementRefusals, renderMotifPlacements } from "./motifs.js";
10
- import { BookPageFacts, BookPrintedPagePlan, BookPrintedPageRole, PrintedPagePlan, PrintedPageSide, planBookPages, planPrintedPages, printedPageAt } from "./printed-pages.js";
16
+ import { BookPageFacts, BookPrintedPagePlan, BookPrintedPageRole, BookSectionParents, PrintedPagePlan, PrintedPageSide, planBookPages, planPrintedPages, printedPageAt, selectBookParent } from "./printed-pages.js";
11
17
  import { SlotFillEntry, SlotFillRefusal, SlotFillsConflictError, SlotFillsRefusedError, SlotPrintOptions, assertSingleSlotCopy, assertSlotPrintOptions, collectSlotPrintCopies, isSlotCopyRefusal, slotFillCopyId, slotFillsConflict } from "./slot-print.js";
12
18
  import { RequiredSlotsRefusedError, UnmeasuredSlotsRefusedError, mergeSlotFitResults, requiredSlotRefusal, unmeasuredSlotRefusal } from "./slot-policy.js";
13
19
  import { MAX_SLOT_DECLARATIONS, PreparedSlots, SlotFillRefusedError, hasSlotWork, prepareSlotCollection, prepareSlots, slotFillRefusal } from "./slots.js";
@@ -17,4 +23,4 @@ export type HtmlKind = 'plain' | 'live-data' | 'scripted' | 'refused';
17
23
  export declare function classify(html: string, prefix: string): HtmlKind;
18
24
  export declare function searchText(html: string): string;
19
25
  //#endregion
20
- export { BINDINGS, type BookDocument, type BookDocumentOptions, type BookHeading, BookPageFacts, type BookPrintPart, BookPrintedPagePlan, BookPrintedPageRole, type BookSectionInfo, type ExpandMotifsOptions, type ExpandedMotifs, type FillOptions, type Filled, FontCssRefusedError, MAX_SLOT_DECLARATIONS, MOTIF_DEPTH_LIMIT, MotifRefusedError, type PlacedMotif, type PreparedSlots, PrintedPagePlan, PrintedPageSide, REFUSED_ELEMENTS, RequiredSlotsRefusedError, type SanitizeOptions, type SlotFillEntry, type SlotFillRefusal, SlotFillRefusedError, SlotFillsConflictError, SlotFillsRefusedError, type SlotPrintOptions, UNICODE_RANGE_CLEARANCE, type UnicodeRangeParse, UnmeasuredSlotsRefusedError, assertSingleSlotCopy, assertSlotPrintOptions, base64ToBytes, bindingNames, bindingRoot, bookDocument, bookPrintPart, bytesToBase64, collectSlotPrintCopies, expandMotifs, fill, findActiveContent, fontCss, fontFaceCss, fontFormat, fontReferenceRefusals, hasBindings, hasMotifPlacement, hasSlotWork, isSlotCopyRefusal, libraryMotifPlacementRefusal, mergeSlotFitResults, motifPlacementRefusals, parseUnicodeRange, planBookPages, planPrintedPages, prepareSlotCollection, prepareSlots, printedPageAt, renderMotifPlacements, requiredSlotRefusal, sanitize, slotFillCopyId, slotFillRefusal, slotFillsConflict, unmeasuredSlotRefusal };
26
+ export { BINDINGS, type BookDocument, type BookDocumentOptions, type BookFurnitureValues, type BookHeading, BookPageFacts, type BookPrintPart, BookPrintedPagePlan, BookPrintedPageRole, type BookSectionInfo, type BookSectionParents, type ExpandMotifsOptions, type ExpandedMotifs, type FillOptions, type Filled, FontCssRefusedError, MAX_SLOT_DECLARATIONS, MOTIF_DEPTH_LIMIT, MotifRefusedError, PAGINATE_FUNCTIONS, type PaginateBlock, type PaginateBookInput, type PaginateBookPage, type PaginateBookRefusal, type PaginateBookResult, type PaginateFlow, type PaginateFlowParents, type PaginateLine, type PaginateMark, type PaginateNote, type PaginateNoteRef, type PaginateOpens, type PaginatePageMarks, type PaginatePageNote, type PaginatePageRole, type PaginateSequence, type PaginateTarget, type PaginateTargetPage, type PaginateWindow, type PlacedMotif, type PreparedSlots, PrintedPagePlan, PrintedPageSide, REFUSED_ELEMENTS, RequiredSlotsRefusedError, type SanitizeOptions, type SlotFillEntry, type SlotFillRefusal, SlotFillRefusedError, SlotFillsConflictError, SlotFillsRefusedError, type SlotPrintOptions, UNICODE_RANGE_CLEARANCE, type UnicodeRangeParse, UnmeasuredSlotsRefusedError, assertSingleSlotCopy, assertSlotPrintOptions, base64ToBytes, bindingNames, bindingRoot, bookDocument, bookPrintPart, bookStylesheet, bookView, bookViewFromHtml, bytesToBase64, collectSlotPrintCopies, embedFunction, expandMotifs, fill, fillBookFurniture, findActiveContent, fontCss, fontFaceCss, fontFormat, fontReferenceRefusals, furnitureScript, hasBindings, hasMotifPlacement, hasSlotWork, heldPageNumber, isSlotCopyRefusal, libraryMotifPlacementRefusal, mergeSlotFitResults, motifPlacementRefusals, openerPageIndex, pageIndexForTarget, paginateBook, paginateScript, parseUnicodeRange, planBookPages, planPrintedPages, prepareSlotCollection, prepareSlots, printedPageAt, renderMotifPlacements, requiredSlotRefusal, sanitize, sanitizeBookCss, selectBookParent, slotFillCopyId, slotFillRefusal, slotFillsConflict, unmeasuredSlotRefusal, windowPageIndex };
package/dist/index.js CHANGED
@@ -3,12 +3,17 @@ import { BINDINGS, bindingNames, bindingRoot, fill, hasBindings } from "./bindin
3
3
  import { base64ToBytes, bytesToBase64 } from "./base64.js";
4
4
  import { bookPrintPart } from "./book/types.js";
5
5
  import { bookDocument } from "./book/book-document.js";
6
+ import { bookView, bookViewFromHtml } from "./book/book-view.js";
7
+ import { embedFunction, fillBookFurniture, furnitureScript } from "./book/furniture.js";
8
+ import { PAGINATE_FUNCTIONS, paginateBook, paginateScript } from "./book/paginate-book.js";
9
+ import { heldPageNumber, openerPageIndex, pageIndexForTarget, windowPageIndex } from "./book/paginate-target.js";
10
+ import { bookStylesheet, sanitizeBookCss } from "./book-css.js";
6
11
  import { UNICODE_RANGE_CLEARANCE, parseUnicodeRange } from "./unicode-range.js";
7
12
  import { FontCssRefusedError, fontCss, fontFaceCss } from "./font-css.js";
8
13
  import { fontFormat } from "./font-format.js";
9
14
  import { fontReferenceRefusals } from "./font-references.js";
10
15
  import { MOTIF_DEPTH_LIMIT, MotifRefusedError, expandMotifs, hasMotifPlacement, libraryMotifPlacementRefusal, motifPlacementRefusals, renderMotifPlacements } from "./motifs.js";
11
- import { planBookPages, planPrintedPages, printedPageAt } from "./printed-pages.js";
16
+ import { planBookPages, planPrintedPages, printedPageAt, selectBookParent } from "./printed-pages.js";
12
17
  import { RequiredSlotsRefusedError, UnmeasuredSlotsRefusedError, mergeSlotFitResults, requiredSlotRefusal, unmeasuredSlotRefusal } from "./slot-policy.js";
13
18
  import { MAX_SLOT_DECLARATIONS, SlotFillRefusedError, hasSlotWork, prepareSlotCollection, prepareSlots, slotFillRefusal } from "./slots.js";
14
19
  import { SlotFillsConflictError, SlotFillsRefusedError, assertSingleSlotCopy, assertSlotPrintOptions, collectSlotPrintCopies, isSlotCopyRefusal, slotFillCopyId, slotFillsConflict } from "./slot-print.js";
@@ -24,4 +29,4 @@ function searchText(html) {
24
29
  return html.replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1\s*>/gi, " ").replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/\s+/g, " ").trim();
25
30
  }
26
31
  //#endregion
27
- export { BINDINGS, FontCssRefusedError, MAX_SLOT_DECLARATIONS, MOTIF_DEPTH_LIMIT, MotifRefusedError, REFUSED_ELEMENTS, RequiredSlotsRefusedError, SlotFillRefusedError, SlotFillsConflictError, SlotFillsRefusedError, UNICODE_RANGE_CLEARANCE, UnmeasuredSlotsRefusedError, assertSingleSlotCopy, assertSlotPrintOptions, base64ToBytes, bindingNames, bindingRoot, bookDocument, bookPrintPart, bytesToBase64, classify, collectSlotPrintCopies, expandMotifs, fill, findActiveContent, fontCss, fontFaceCss, fontFormat, fontReferenceRefusals, hasBindings, hasMotifPlacement, hasSlotWork, isSlotCopyRefusal, libraryMotifPlacementRefusal, mergeSlotFitResults, motifPlacementRefusals, parseUnicodeRange, planBookPages, planPrintedPages, prepareSlotCollection, prepareSlots, printedPageAt, renderMotifPlacements, requiredSlotRefusal, sanitize, searchText, slotFillCopyId, slotFillRefusal, slotFillsConflict, unmeasuredSlotRefusal };
32
+ export { BINDINGS, FontCssRefusedError, MAX_SLOT_DECLARATIONS, MOTIF_DEPTH_LIMIT, MotifRefusedError, PAGINATE_FUNCTIONS, REFUSED_ELEMENTS, RequiredSlotsRefusedError, SlotFillRefusedError, SlotFillsConflictError, SlotFillsRefusedError, UNICODE_RANGE_CLEARANCE, UnmeasuredSlotsRefusedError, assertSingleSlotCopy, assertSlotPrintOptions, base64ToBytes, bindingNames, bindingRoot, bookDocument, bookPrintPart, bookStylesheet, bookView, bookViewFromHtml, bytesToBase64, classify, collectSlotPrintCopies, embedFunction, expandMotifs, fill, fillBookFurniture, findActiveContent, fontCss, fontFaceCss, fontFormat, fontReferenceRefusals, furnitureScript, hasBindings, hasMotifPlacement, hasSlotWork, heldPageNumber, isSlotCopyRefusal, libraryMotifPlacementRefusal, mergeSlotFitResults, motifPlacementRefusals, openerPageIndex, pageIndexForTarget, paginateBook, paginateScript, parseUnicodeRange, planBookPages, planPrintedPages, prepareSlotCollection, prepareSlots, printedPageAt, renderMotifPlacements, requiredSlotRefusal, sanitize, sanitizeBookCss, searchText, selectBookParent, slotFillCopyId, slotFillRefusal, slotFillsConflict, unmeasuredSlotRefusal, windowPageIndex };
package/dist/motifs.d.ts CHANGED
@@ -8,6 +8,7 @@ export interface ExpandMotifsOptions {
8
8
  scripts: Scripts;
9
9
  prefix: string;
10
10
  linkOrigins?: Iterable<string>;
11
+ dataFonts?: boolean;
11
12
  signal: AbortSignal;
12
13
  deferPaths?: Iterable<string>;
13
14
  }
package/dist/motifs.js CHANGED
@@ -102,6 +102,7 @@ async function fillVersion(html, placement, element, state) {
102
102
  prefix: state.options.prefix,
103
103
  data: data ?? {},
104
104
  linkOrigins: state.options.linkOrigins,
105
+ dataFonts: state.options.dataFonts,
105
106
  registerHrefOrigins: true,
106
107
  deferPaths: state.options.deferPaths
107
108
  });
@@ -181,7 +182,10 @@ async function expandPlacement(element, inheritedLang, stack, state) {
181
182
  lang,
182
183
  fields
183
184
  };
184
- let versionHtml = sanitize(choice.version.html, { linkOrigins: state.options.linkOrigins });
185
+ let versionHtml = sanitize(choice.version.html, {
186
+ linkOrigins: state.options.linkOrigins,
187
+ dataFonts: state.options.dataFonts
188
+ });
185
189
  if (bound && !state.validateOnly) {
186
190
  const filled = await fillVersion(versionHtml, placement, element, state);
187
191
  if (filled === void 0) return;
@@ -1,4 +1,4 @@
1
- import { NumberSystem, PrintedPageRole, PrintedPageRules } from "@segnavia/format";
1
+ import { BookSectionParents as BookSectionParents$1, NumberSystem, PrintedPageRole, PrintedPageRules } from "@segnavia/format";
2
2
  //#region src/printed-pages.d.ts
3
3
  export type PrintedPageSide = 'left' | 'right';
4
4
  export interface PrintedPagePlan {
@@ -9,13 +9,15 @@ export interface PrintedPagePlan {
9
9
  number: string;
10
10
  total: string;
11
11
  }
12
+ export type BookPrintedPageRole = 'recto' | 'verso' | 'opener' | 'blank' | 'fixed';
12
13
  export interface BookPageFacts {
13
14
  blank: boolean;
14
15
  sectionOpening: boolean;
15
16
  folio?: string;
16
17
  runningHead?: string;
18
+ role?: BookPrintedPageRole | 'body';
19
+ parents?: BookSectionParents$1;
17
20
  }
18
- export type BookPrintedPageRole = 'recto' | 'verso' | 'opener' | 'blank';
19
21
  export interface BookPrintedPagePlan extends Omit<BookPageFacts, 'blank' | 'sectionOpening'> {
20
22
  index: number;
21
23
  role: BookPrintedPageRole;
@@ -24,5 +26,17 @@ export interface BookPrintedPagePlan extends Omit<BookPageFacts, 'blank' | 'sect
24
26
  }
25
27
  export declare function printedPageAt(rules: PrintedPageRules, index: number, count: number, direction: 'ltr' | 'rtl'): Pick<PrintedPagePlan, 'role' | 'parent' | 'side'>;
26
28
  export declare function planPrintedPages(rules: PrintedPageRules, count: number, direction: 'ltr' | 'rtl', numbering?: NumberSystem): PrintedPagePlan[];
27
- export declare function planBookPages(facts: BookPageFacts[], direction: 'ltr' | 'rtl'): BookPrintedPagePlan[];
28
- //#endregion
29
+ export declare function selectBookParent(fact: BookPageFacts, index: number, count: number, direction: 'ltr' | 'rtl', names?: {
30
+ readonly recto: "segnavia:recto";
31
+ readonly verso: "segnavia:verso";
32
+ readonly opener: "segnavia:opener";
33
+ readonly blank: "segnavia:blank";
34
+ }): Pick<BookPrintedPagePlan, 'role' | 'parent' | 'side'>;
35
+ export declare function planBookPages(facts: BookPageFacts[], direction: 'ltr' | 'rtl', names?: {
36
+ readonly recto: "segnavia:recto";
37
+ readonly verso: "segnavia:verso";
38
+ readonly opener: "segnavia:opener";
39
+ readonly blank: "segnavia:blank";
40
+ }): BookPrintedPagePlan[];
41
+ //#endregion
42
+ export type { BookSectionParents$1 as BookSectionParents };
@@ -39,37 +39,59 @@ function planPrintedPages(rules, count, direction, numbering = "arabic") {
39
39
  };
40
40
  });
41
41
  }
42
- const BOOK_RULES = {
43
- every: BOOK_PARENT_NAMES.recto,
44
- recto: BOOK_PARENT_NAMES.recto,
45
- verso: BOOK_PARENT_NAMES.verso
46
- };
47
- function planBookPages(facts, direction) {
42
+ function selectBookParent(fact, index, count, direction, names = BOOK_PARENT_NAMES) {
43
+ const physical = printedPageAt({
44
+ every: names.recto,
45
+ recto: names.recto,
46
+ verso: names.verso
47
+ }, index, count, direction);
48
+ const named = fact.parents;
49
+ if (fact.blank) return {
50
+ role: "blank",
51
+ parent: named?.blank ?? names.blank,
52
+ side: physical.side
53
+ };
54
+ if (fact.role === "fixed") return {
55
+ role: "fixed",
56
+ parent: named?.fixed ?? names.blank,
57
+ side: physical.side
58
+ };
59
+ if (fact.sectionOpening && fact.role !== "body") return {
60
+ role: "opener",
61
+ parent: named?.opener ?? names.opener,
62
+ side: physical.side
63
+ };
64
+ if (physical.role === "verso") return {
65
+ role: "verso",
66
+ parent: named?.verso ?? names.verso,
67
+ side: physical.side
68
+ };
69
+ return {
70
+ role: "recto",
71
+ parent: named?.recto ?? names.recto,
72
+ side: physical.side
73
+ };
74
+ }
75
+ function planBookPages(facts, direction, names = BOOK_PARENT_NAMES) {
48
76
  return facts.map((fact, offset) => {
49
77
  const index = offset + 1;
50
- const selected = printedPageAt(BOOK_RULES, index, facts.length, direction);
51
- if (fact.blank) return {
78
+ const selected = selectBookParent(fact, index, facts.length, direction, names);
79
+ if (selected.role === "blank") return {
52
80
  index,
53
- role: "blank",
54
- parent: BOOK_PARENT_NAMES.blank,
55
- side: selected.side
81
+ ...selected
56
82
  };
57
- if (fact.sectionOpening) return {
83
+ if (selected.role === "opener") return {
58
84
  index,
59
- role: "opener",
60
- parent: BOOK_PARENT_NAMES.opener,
61
- side: selected.side,
85
+ ...selected,
62
86
  ...fact.folio === void 0 ? {} : { folio: fact.folio }
63
87
  };
64
88
  return {
65
89
  index,
66
- role: selected.role === "verso" ? "verso" : "recto",
67
- parent: selected.parent,
68
- side: selected.side,
90
+ ...selected,
69
91
  ...fact.folio === void 0 ? {} : { folio: fact.folio },
70
92
  ...fact.runningHead === void 0 ? {} : { runningHead: fact.runningHead }
71
93
  };
72
94
  });
73
95
  }
74
96
  //#endregion
75
- export { planBookPages, planPrintedPages, printedPageAt };
97
+ export { planBookPages, planPrintedPages, printedPageAt, selectBookParent };
@@ -0,0 +1,6 @@
1
+ //#region src/sanitize-css.d.ts
2
+ export interface CssSanitizeResult {
3
+ css: string;
4
+ removed: string[];
5
+ }
6
+ //#endregion
@@ -0,0 +1,308 @@
1
+ import { cssEscape, cssTokens } from "./css-tokens.js";
2
+ //#region src/sanitize-css.ts
3
+ const DATA_IMAGE = /^data:image\//i;
4
+ const DATA_FONT = /^data:(?:font\/[^;,]+|application\/(?:x-)?font-[^;,]+)(?:[;,])/i;
5
+ const DECLARATION_AT_RULES = /* @__PURE__ */ new Set([
6
+ "counter-style",
7
+ "font-face",
8
+ "font-palette-values",
9
+ "page",
10
+ "property"
11
+ ]);
12
+ const FETCH_FUNCTIONS = [
13
+ "url",
14
+ "src",
15
+ "image-set",
16
+ "-webkit-image-set"
17
+ ];
18
+ const FETCH_AT_KEYWORDS = ["import"];
19
+ const FETCH_CONSTRUCT = new RegExp(`@(?:${FETCH_AT_KEYWORDS.join("|")})|(?:${FETCH_FUNCTIONS.join("|")})\\(`, "gi");
20
+ const FETCH_FUNCTION_SET = new Set(FETCH_FUNCTIONS);
21
+ const FETCH_AT_KEYWORD_SET = new Set(FETCH_AT_KEYWORDS);
22
+ function normalizedUrl(value, decodeEscapes = true) {
23
+ let normalized = "";
24
+ for (let at = 0; at < value.length;) if (value.startsWith("/*", at)) {
25
+ const close = value.indexOf("*/", at + 2);
26
+ at = close < 0 ? value.length : close + 2;
27
+ } else if (decodeEscapes && value[at] === "\\") {
28
+ const escaped = cssEscape(value, at);
29
+ normalized += escaped[0];
30
+ at = escaped[1];
31
+ } else normalized += value[at++] ?? "";
32
+ normalized = normalized.trim();
33
+ if ((normalized[0] === "\"" || normalized[0] === "'") && normalized.at(-1) === normalized[0]) normalized = normalized.slice(1, -1);
34
+ return [...normalized].filter((character) => character.charCodeAt(0) > 32 && character.charCodeAt(0) !== 127).join("").toLowerCase();
35
+ }
36
+ function decodedCss(source) {
37
+ let decoded = "";
38
+ for (let at = 0; at < source.length;) if (source[at] === "\\") {
39
+ const escaped = cssEscape(source, at);
40
+ decoded += escaped[0];
41
+ at = escaped[1];
42
+ } else decoded += source[at++] ?? "";
43
+ return decoded.replace(/\/\*[\s\S]*?(?:\*\/|$)/g, "").replace(/\s+/g, " ").toLowerCase();
44
+ }
45
+ function functionArgument(value) {
46
+ let depth = 1;
47
+ let quote = "";
48
+ for (let at = 0; at < value.length; at++) {
49
+ const character = value[at] ?? "";
50
+ if (quote) {
51
+ if (character === quote) quote = "";
52
+ continue;
53
+ }
54
+ if (character === "\"" || character === "'") quote = character;
55
+ else if (character === "(") depth++;
56
+ else if (character === ")" && --depth === 0) return value.slice(0, at);
57
+ }
58
+ return value;
59
+ }
60
+ function imageSetItems(value) {
61
+ const items = [];
62
+ let start = 0;
63
+ let depth = 0;
64
+ let quote = "";
65
+ for (let at = 0; at < value.length; at++) {
66
+ const character = value[at] ?? "";
67
+ if (quote) {
68
+ if (character === quote) quote = "";
69
+ continue;
70
+ }
71
+ if (character === "\"" || character === "'") quote = character;
72
+ else if (character === "(") depth++;
73
+ else if (character === ")") depth--;
74
+ else if (character === "," && depth === 0) {
75
+ items.push(value.slice(start, at));
76
+ start = at + 1;
77
+ }
78
+ }
79
+ items.push(value.slice(start));
80
+ return items;
81
+ }
82
+ function allowedAuditData(value, dataFonts) {
83
+ let argument = value.trimStart();
84
+ if (/^url\(/i.test(argument)) argument = functionArgument(argument.slice(argument.indexOf("(") + 1)).trimStart();
85
+ argument = argument.replace(/^["']/, "");
86
+ return DATA_IMAGE.test(argument) || dataFonts && DATA_FONT.test(argument);
87
+ }
88
+ function lexicalFindings(source, dataFonts) {
89
+ const text = decodedCss(source);
90
+ const found = [];
91
+ for (const match of text.matchAll(FETCH_CONSTRUCT)) {
92
+ const construct = match[0].toLowerCase();
93
+ const after = text.slice((match.index ?? 0) + construct.length);
94
+ if (construct.startsWith("@")) {
95
+ if (!allowedAuditData(after, dataFonts)) found.push(construct);
96
+ continue;
97
+ }
98
+ const name = construct.slice(0, -1);
99
+ const argument = functionArgument(after);
100
+ if (!(name === "image-set" || name === "-webkit-image-set" ? imageSetItems(argument).every((item) => allowedAuditData(item, dataFonts)) : allowedAuditData(argument, dataFonts))) found.push(`${name === "-webkit-image-set" ? "image-set" : name}()`);
101
+ }
102
+ return [...new Set(found)];
103
+ }
104
+ function matchingClose(list, open, limit = list.length) {
105
+ let depth = 1;
106
+ for (let at = open + 1; at < limit; at++) {
107
+ if (list[at]?.kind === "open" || list[at]?.kind === "function") depth++;
108
+ if (list[at]?.kind === "close" && --depth === 0) return at;
109
+ }
110
+ return limit;
111
+ }
112
+ function unsafeImageSet(list, start, close) {
113
+ let depth = 0;
114
+ for (let at = start + 1; at < close; at++) {
115
+ const token = list[at];
116
+ if (token?.kind === "function" || token?.kind === "open") depth++;
117
+ else if (token?.kind === "close") depth--;
118
+ else if (token?.kind === "string" && depth === 0 && !DATA_IMAGE.test(normalizedUrl(token.value, false))) return true;
119
+ }
120
+ return false;
121
+ }
122
+ function urlFinding(value, dataFonts, fontFace, finding = "url()") {
123
+ if (DATA_IMAGE.test(value)) return void 0;
124
+ if (fontFace && DATA_FONT.test(value)) return dataFonts ? void 0 : "data font";
125
+ return finding;
126
+ }
127
+ function functionFinding(source, list, at, end, dataFonts, fontFace) {
128
+ const token = list[at];
129
+ if (token?.kind === "bad-url") return "url()";
130
+ if (token?.kind === "url") return urlFinding(token.value, dataFonts, fontFace);
131
+ if (token?.kind !== "function") return void 0;
132
+ const close = matchingClose(list, at, end);
133
+ if (!FETCH_FUNCTION_SET.has(token.value)) return void 0;
134
+ if (token.value === "image-set" || token.value === "-webkit-image-set") return unsafeImageSet(list, at, close) ? "image-set()" : void 0;
135
+ return urlFinding(normalizedUrl(source.slice(token.end, close < end ? list[close]?.start : source.length)), dataFonts, fontFace, `${token.value}()`);
136
+ }
137
+ function findings(source, list, start, end, dataFonts, fontFace) {
138
+ const found = [];
139
+ for (let at = start; at < end; at++) {
140
+ const token = list[at];
141
+ if (token?.kind === "at" && FETCH_AT_KEYWORD_SET.has(token.value)) found.push(`@${token.value}`);
142
+ const finding = functionFinding(source, list, at, end, dataFonts, fontFace);
143
+ if (finding) found.push(finding);
144
+ }
145
+ return [...new Set(found)];
146
+ }
147
+ function boundary(list, start, end) {
148
+ let depth = 0;
149
+ for (let at = start; at < end; at++) {
150
+ const token = list[at];
151
+ if (!token) continue;
152
+ if (depth === 0 && (token.kind === "semicolon" || token.kind === "open" && token.value === "{")) return at;
153
+ if (token.kind === "function" || token.kind === "open") depth++;
154
+ else if (token.kind === "close") depth--;
155
+ }
156
+ return end;
157
+ }
158
+ function cleanStatement(source, list, start, edge, dataFonts, removed) {
159
+ const first = list[start];
160
+ const delimiter = list[edge];
161
+ if (!first || !delimiter) return {
162
+ character: source.length,
163
+ next: edge,
164
+ text: ""
165
+ };
166
+ const unsafe = first.kind === "at" && first.value === "import" ? ["@import"] : findings(source, list, start, edge + 1, dataFonts, false);
167
+ if (unsafe.length > 0) removed.push(...unsafe);
168
+ return {
169
+ character: delimiter.end,
170
+ next: edge + 1,
171
+ text: unsafe.length === 0 ? source.slice(first.start, delimiter.end) : ""
172
+ };
173
+ }
174
+ function cleanBlockRule(source, list, start, edge, end, dataFonts, removed) {
175
+ const first = list[start];
176
+ const delimiter = list[edge];
177
+ if (!first || !delimiter) return {
178
+ character: source.length,
179
+ next: end,
180
+ text: ""
181
+ };
182
+ const close = matchingClose(list, edge, end);
183
+ const ruleName = first.kind === "at" ? first.value : "";
184
+ const fontFace = ruleName === "font-face";
185
+ const unsafe = findings(source, list, start, edge, dataFonts, false);
186
+ if (fontFace) unsafe.push(...findings(source, list, edge + 1, close, dataFonts, true));
187
+ if (unsafe.length > 0) removed.push(...unsafe);
188
+ let text = "";
189
+ if (unsafe.length === 0) {
190
+ const body = first.kind !== "at" || DECLARATION_AT_RULES.has(ruleName) ? cleanDeclarations(source, list, edge + 1, close, dataFonts, removed, fontFace) : cleanRules(source, list, edge + 1, close, dataFonts, removed);
191
+ text = source.slice(first.start, delimiter.end) + body;
192
+ if (close < end) text += source.slice(list[close]?.start, list[close]?.end);
193
+ }
194
+ return {
195
+ character: close < end ? list[close]?.end ?? delimiter.end : source.length,
196
+ next: close < end ? close + 1 : end,
197
+ text
198
+ };
199
+ }
200
+ function cleanRuleUnit(source, list, start, end, dataFonts, removed) {
201
+ const first = list[start];
202
+ if (!first) return {
203
+ character: source.length,
204
+ next: end,
205
+ text: ""
206
+ };
207
+ const edge = boundary(list, start, end);
208
+ const delimiter = list[edge];
209
+ if (edge >= end && delimiter?.kind === "close") {
210
+ const unsafe = findings(source, list, start, end, dataFonts, false);
211
+ if (unsafe.length > 0) removed.push(...unsafe);
212
+ return {
213
+ character: delimiter.start,
214
+ next: end,
215
+ text: unsafe.length === 0 ? source.slice(first.start, delimiter.start) : ""
216
+ };
217
+ }
218
+ if (!delimiter) {
219
+ const unsafe = findings(source, list, start, end, dataFonts, false);
220
+ if (unsafe.length > 0) removed.push(...unsafe);
221
+ return {
222
+ character: source.length,
223
+ next: end,
224
+ text: unsafe.length === 0 ? source.slice(first.start) : ""
225
+ };
226
+ }
227
+ return delimiter.kind === "semicolon" ? cleanStatement(source, list, start, edge, dataFonts, removed) : cleanBlockRule(source, list, start, edge, end, dataFonts, removed);
228
+ }
229
+ function cleanRules(source, list, start, end, dataFonts, removed) {
230
+ let output = "";
231
+ let character = start === 0 ? 0 : list[start - 1]?.end ?? source.length;
232
+ let at = start;
233
+ while (at < end) {
234
+ const first = list[at];
235
+ if (!first) break;
236
+ output += source.slice(character, first.start);
237
+ const unit = cleanRuleUnit(source, list, at, end, dataFonts, removed);
238
+ output += unit.text;
239
+ character = unit.character;
240
+ at = unit.next;
241
+ }
242
+ const rangeEnd = end < list.length ? list[end]?.start ?? source.length : source.length;
243
+ return output + source.slice(character, rangeEnd);
244
+ }
245
+ function cleanDeclarationSegment(source, list, start, end, character, dataFonts, fontFace, removed) {
246
+ const delimiter = list[end];
247
+ const hasSemicolon = delimiter?.kind === "semicolon";
248
+ const segmentEnd = hasSemicolon ? end + 1 : end;
249
+ const finish = hasSemicolon ? delimiter.end : end < list.length ? delimiter?.start ?? source.length : source.length;
250
+ const unsafe = findings(source, list, start, segmentEnd, dataFonts, fontFace);
251
+ if (unsafe.length === 0) return [source.slice(character, finish), finish];
252
+ removed.push(...unsafe);
253
+ return [source.slice(character, list[start]?.start ?? finish), finish];
254
+ }
255
+ function declarationEnd(list, start, end) {
256
+ let depth = 0;
257
+ for (let at = start; at < end; at++) {
258
+ const token = list[at];
259
+ if (depth === 0 && token?.kind === "semicolon") return at;
260
+ if (token?.kind === "function" || token?.kind === "open") depth++;
261
+ else if (token?.kind === "close") depth--;
262
+ }
263
+ return end;
264
+ }
265
+ function isDeclaration(list, start) {
266
+ return list[start]?.value.startsWith("--") === true && list[start + 1]?.value === ":";
267
+ }
268
+ function cleanDeclarations(source, list, start, end, dataFonts, removed, fontFace = false) {
269
+ let output = "";
270
+ let character = start === 0 ? 0 : list[start - 1]?.end ?? source.length;
271
+ let at = start;
272
+ while (at < end) {
273
+ const first = list[at];
274
+ if (!first) break;
275
+ output += source.slice(character, first.start);
276
+ const edge = boundary(list, at, end);
277
+ if (list[edge]?.kind === "open" && !isDeclaration(list, at)) {
278
+ const unit = cleanBlockRule(source, list, at, edge, end, dataFonts, removed);
279
+ output += unit.text;
280
+ character = unit.character;
281
+ at = unit.next;
282
+ } else {
283
+ const finish = declarationEnd(list, at, end);
284
+ const cleaned = cleanDeclarationSegment(source, list, at, finish, first.start, dataFonts, fontFace, removed);
285
+ output += cleaned[0];
286
+ character = cleaned[1];
287
+ at = finish < end ? finish + 1 : end;
288
+ }
289
+ }
290
+ const rangeEnd = end < list.length ? list[end]?.start ?? source.length : source.length;
291
+ return output + source.slice(character, rangeEnd);
292
+ }
293
+ function sanitizeCss(source, declarations, dataFonts) {
294
+ const list = cssTokens(source);
295
+ const removed = [];
296
+ const css = declarations ? cleanDeclarations(source, list, 0, list.length, dataFonts, removed) : cleanRules(source, list, 0, list.length, dataFonts, removed);
297
+ const auditRemoved = lexicalFindings(css, dataFonts);
298
+ if (auditRemoved.length > 0) return {
299
+ css: "",
300
+ removed: [.../* @__PURE__ */ new Set([...removed, ...auditRemoved])]
301
+ };
302
+ return {
303
+ css,
304
+ removed: [...new Set(removed)]
305
+ };
306
+ }
307
+ //#endregion
308
+ export { lexicalFindings, sanitizeCss };
@@ -4,6 +4,7 @@ export declare const REFUSED_ELEMENTS: Set<string>;
4
4
  export interface SanitizeOptions {
5
5
  linkOrigins?: Iterable<string>;
6
6
  keepScripts?: boolean;
7
+ dataFonts?: boolean;
7
8
  refuseElements?: Iterable<string>;
8
9
  }
9
10
  export declare function findActiveContent(html: string, options?: SanitizeOptions): string[];
package/dist/sanitize.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { sanitizeCss } from "./sanitize-css.js";
1
2
  import { isTag } from "domhandler";
2
3
  import render from "dom-serializer";
3
4
  import { parseDocument } from "htmlparser2";
@@ -44,7 +45,9 @@ const URL_ATTRIBUTES = /* @__PURE__ */ new Set([
44
45
  "longdesc",
45
46
  "ping"
46
47
  ]);
47
- const DATA_IMAGE = /^data:image\/(?:png|jpeg|gif|webp|svg\+xml)(?:[;,])/i;
48
+ const DATA_IMAGE_TYPES = String.raw`(?:png|jpeg|gif|webp|svg\+xml)`;
49
+ const DATA_IMAGE = new RegExp(`^data:image/${DATA_IMAGE_TYPES}(?:[;,])`, "i");
50
+ const DATA_IMAGE_SRCSET = new RegExp(`^data:image/${DATA_IMAGE_TYPES}(?:;[^,]*)?,[^,]*$`, "i");
48
51
  function parse(html) {
49
52
  return parseDocument(html, {
50
53
  lowerCaseAttributeNames: true,
@@ -94,24 +97,13 @@ function safeUrl(attribute, value, allowed) {
94
97
  return false;
95
98
  }
96
99
  }
97
- if (attribute === "srcset") return /^data:image\/(?:png|jpeg|gif|webp|svg\+xml)(?:;[^,]*)?,[^,]*$/i.test(url);
100
+ if (attribute === "srcset") return DATA_IMAGE_SRCSET.test(url);
98
101
  if (attribute === "src" || attribute === "poster") return DATA_IMAGE.test(url);
99
102
  return false;
100
103
  }
101
- function safeStyle(value) {
102
- const matches = [...value.matchAll(/url\(\s*(["']?)(.*?)\1\s*\)/gi)];
103
- if ((value.match(/url\(/gi)?.length ?? 0) !== matches.length) return false;
104
- return matches.every((match) => normalizeUrl(match[2] ?? "").startsWith("data:image/"));
105
- }
106
- function refusedStyleElement(element) {
107
- if (element.name.toLowerCase() !== "style") return false;
108
- const text = element.children.map((child) => "data" in child ? child.data : "").join("");
109
- return /@import/i.test(text) || !safeStyle(text);
110
- }
111
104
  function refusedElement(element, refused, keepScripts) {
112
105
  const name = element.name.toLowerCase();
113
106
  if (name === "script") return !keepScripts;
114
- if (refusedStyleElement(element)) return true;
115
107
  if (refused.has(name)) return true;
116
108
  if (insideSvg(element) && REFUSED_SVG_ELEMENTS.has(name)) return true;
117
109
  return name === "meta" && Object.keys(element.attribs).some((attribute) => attribute.toLowerCase() === "http-equiv");
@@ -119,11 +111,23 @@ function refusedElement(element, refused, keepScripts) {
119
111
  function refusedAttribute(name, value, allowed, keepScripts) {
120
112
  const attribute = name.toLowerCase();
121
113
  if (attribute.startsWith("on")) return !keepScripts;
122
- return REFUSED_ATTRIBUTES.has(attribute) || URL_ATTRIBUTES.has(attribute) && !safeUrl(attribute, value, allowed) || attribute === "style" && !safeStyle(value);
114
+ return REFUSED_ATTRIBUTES.has(attribute) || URL_ATTRIBUTES.has(attribute) && !safeUrl(attribute, value, allowed);
123
115
  }
124
116
  function refusedSet(options) {
125
117
  return new Set([...REFUSED_ELEMENTS, ...options.refuseElements ?? []].map((name) => name.toLowerCase()));
126
118
  }
119
+ function styleText(element) {
120
+ return element.children.map((child) => "data" in child ? child.data : "").join("");
121
+ }
122
+ function styleFindings(element, dataFonts) {
123
+ const found = [];
124
+ if (element.name.toLowerCase() === "style") for (const construct of sanitizeCss(styleText(element), false, dataFonts).removed) found.push(`<style> ${construct}`);
125
+ for (const [name, value] of Object.entries(element.attribs)) {
126
+ if (name.toLowerCase() !== "style") continue;
127
+ for (const construct of sanitizeCss(value, true, dataFonts).removed) found.push(`${element.name}[${name}] ${construct}`);
128
+ }
129
+ return found;
130
+ }
127
131
  function findActiveContent(html, options = {}) {
128
132
  const allowed = origins(options.linkOrigins);
129
133
  const refused = refusedSet(options);
@@ -131,10 +135,27 @@ function findActiveContent(html, options = {}) {
131
135
  const found = [];
132
136
  for (const element of elements(parse(html).children)) {
133
137
  if (refusedElement(element, refused, keepScripts)) found.push(`<${element.name}>`);
138
+ found.push(...styleFindings(element, options.dataFonts ?? false));
134
139
  for (const [name, value] of Object.entries(element.attribs)) if (refusedAttribute(name, value, allowed, keepScripts)) found.push(`${element.name}[${name}]`);
135
140
  }
136
141
  return found;
137
142
  }
143
+ function cleanStyleElement(element, dataFonts) {
144
+ if (element.name.toLowerCase() !== "style") return;
145
+ const children = element.children.filter((candidate) => "data" in candidate);
146
+ const child = children[0];
147
+ if (!child || !("data" in child)) return;
148
+ child.data = sanitizeCss(styleText(element), false, dataFonts).css;
149
+ for (const extra of children.slice(1)) if ("data" in extra) extra.data = "";
150
+ }
151
+ function cleanAttributes(element, allowed, keepScripts, dataFonts) {
152
+ for (const [name, value] of Object.entries(element.attribs)) if (refusedAttribute(name, value, allowed, keepScripts)) delete element.attribs[name];
153
+ else if (name.toLowerCase() === "style") {
154
+ const css = sanitizeCss(value, true, dataFonts).css.trim();
155
+ if (css) element.attribs[name] = css;
156
+ else delete element.attribs[name];
157
+ }
158
+ }
138
159
  function sanitize(html, options = {}) {
139
160
  const allowed = origins(options.linkOrigins);
140
161
  const refused = refusedSet(options);
@@ -143,7 +164,8 @@ function sanitize(html, options = {}) {
143
164
  const prune = (nodes) => nodes.filter((node) => {
144
165
  if (!isTag(node)) return true;
145
166
  if (refusedElement(node, refused, keepScripts)) return false;
146
- for (const [name, value] of Object.entries(node.attribs)) if (refusedAttribute(name, value, allowed, keepScripts)) delete node.attribs[name];
167
+ cleanStyleElement(node, options.dataFonts ?? false);
168
+ cleanAttributes(node, allowed, keepScripts, options.dataFonts ?? false);
147
169
  node.children = prune(node.children);
148
170
  return true;
149
171
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@segnavia/html",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -26,7 +26,7 @@
26
26
  }
27
27
  },
28
28
  "dependencies": {
29
- "@segnavia/format": "0.27.0",
29
+ "@segnavia/format": "0.28.0",
30
30
  "dom-serializer": "^2.0.0",
31
31
  "domhandler": "^5.0.3",
32
32
  "htmlparser2": "^10.0.0",