@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.
- package/dist/bindings.d.ts +1 -1
- package/dist/bindings.js +4 -1
- package/dist/book/book-document.js +146 -65
- package/dist/book/book-held.js +182 -0
- package/dist/book/book-view.d.ts +5 -0
- package/dist/book/book-view.js +26 -0
- package/dist/book/furniture.d.ts +14 -0
- package/dist/book/furniture.js +36 -0
- package/dist/book/notes.js +18 -9
- package/dist/book/paginate-book.d.ts +11 -0
- package/dist/book/paginate-book.js +195 -0
- package/dist/book/paginate-columns.d.ts +8 -0
- package/dist/book/paginate-columns.js +119 -0
- package/dist/book/paginate-keeps.d.ts +4 -0
- package/dist/book/paginate-keeps.js +80 -0
- package/dist/book/paginate-marks.d.ts +7 -0
- package/dist/book/paginate-marks.js +34 -0
- package/dist/book/paginate-notes.d.ts +21 -0
- package/dist/book/paginate-notes.js +105 -0
- package/dist/book/paginate-sequence.d.ts +4 -0
- package/dist/book/paginate-sequence.js +267 -0
- package/dist/book/paginate-target.d.ts +13 -0
- package/dist/book/paginate-target.js +36 -0
- package/dist/book/paginate-types.d.ts +170 -0
- package/dist/book/types.d.ts +1 -1
- package/dist/book-css.d.ts +5 -0
- package/dist/book-css.js +14 -0
- package/dist/css-tokens.js +189 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.js +7 -2
- package/dist/motifs.d.ts +1 -0
- package/dist/motifs.js +5 -1
- package/dist/printed-pages.d.ts +18 -4
- package/dist/printed-pages.js +41 -19
- package/dist/sanitize-css.d.ts +6 -0
- package/dist/sanitize-css.js +308 -0
- package/dist/sanitize.d.ts +1 -0
- package/dist/sanitize.js +37 -15
- 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(/ /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
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, {
|
|
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;
|
package/dist/printed-pages.d.ts
CHANGED
|
@@ -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
|
|
28
|
-
|
|
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 };
|
package/dist/printed-pages.js
CHANGED
|
@@ -39,37 +39,59 @@ function planPrintedPages(rules, count, direction, numbering = "arabic") {
|
|
|
39
39
|
};
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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 =
|
|
51
|
-
if (
|
|
78
|
+
const selected = selectBookParent(fact, index, facts.length, direction, names);
|
|
79
|
+
if (selected.role === "blank") return {
|
|
52
80
|
index,
|
|
53
|
-
|
|
54
|
-
parent: BOOK_PARENT_NAMES.blank,
|
|
55
|
-
side: selected.side
|
|
81
|
+
...selected
|
|
56
82
|
};
|
|
57
|
-
if (
|
|
83
|
+
if (selected.role === "opener") return {
|
|
58
84
|
index,
|
|
59
|
-
|
|
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
|
-
|
|
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,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 };
|
package/dist/sanitize.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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)
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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",
|