@segnavia/html 0.37.0 → 0.39.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/book/book-contents.js +33 -0
- package/dist/book/book-document-nodes.js +37 -0
- package/dist/book/book-document.d.ts +2 -0
- package/dist/book/book-document.js +178 -155
- package/dist/book/book-flow.js +23 -7
- package/dist/book/book-held.d.ts +4 -0
- package/dist/book/book-held.js +60 -7
- package/dist/book/book-issues.js +42 -0
- package/dist/book/book-page-reference.d.ts +7 -0
- package/dist/book/book-page-reference.js +19 -0
- package/dist/book/book-part-build.js +50 -0
- package/dist/book/dom.d.ts +1 -0
- package/dist/book/notes.d.ts +2 -0
- package/dist/book/notes.js +117 -51
- package/dist/book/paginate-book.d.ts +2 -1
- package/dist/book/paginate-book.js +34 -26
- package/dist/book/paginate-notes.js +19 -7
- package/dist/book/paginate-sequence.d.ts +1 -1
- package/dist/book/paginate-sequence.js +60 -28
- package/dist/book/paginate-types.d.ts +18 -5
- package/dist/book/region-fit.d.ts +16 -0
- package/dist/book/region-fit.js +111 -0
- package/dist/book/types.d.ts +5 -2
- package/dist/book/types.js +2 -2
- package/dist/font-css.js +13 -2
- package/dist/font-format.js +2 -6
- package/dist/index.d.ts +9 -6
- package/dist/index.js +7 -4
- package/dist/motifs.d.ts +2 -0
- package/dist/motifs.js +5 -2
- package/dist/placements.d.ts +89 -0
- package/dist/placements.js +189 -0
- package/dist/printed-pages.d.ts +2 -7
- package/dist/printed-pages.js +1 -1
- package/dist/sanitize.d.ts +2 -0
- package/dist/sanitize.js +82 -44
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import { REFUSED_ELEMENTS, elements, findActiveContent, parse, sanitize } from "./sanitize.js";
|
|
1
|
+
import { REFUSED_ELEMENTS, elements, findActiveContent, parse, safeImageSource, sanitize } from "./sanitize.js";
|
|
2
2
|
import { BINDINGS, bindingNames, bindingRoot, fill, hasBindings } from "./bindings.js";
|
|
3
3
|
import { base64ToBytes, bytesToBase64 } from "./base64.js";
|
|
4
4
|
import { BOOK_COLUMN_MAX, BOOK_COLUMN_MIN, BOOK_FLOW_COLUMN_MIN_PX, parseBookPositionKey } from "./book/book-flow.js";
|
|
5
|
+
import { ABSOLUTE_PX, ABSOLUTE_UNITS, fitRegionSize, lengthPx, parseCssLength } from "./book/region-fit.js";
|
|
6
|
+
import { PAGE_REFERENCE_ATTRIBUTE, PAGE_REFERENCE_SELECTOR, PAGE_REFERENCE_VALUE, pageReferenceTarget } from "./book/book-page-reference.js";
|
|
5
7
|
import { bookPrintPart } from "./book/types.js";
|
|
6
8
|
import { bookDocument } from "./book/book-document.js";
|
|
7
9
|
import { bookView, bookViewFromHtml } from "./book/book-view.js";
|
|
8
10
|
import { embedFunction, fillBookFurniture, furnitureScript } from "./book/furniture.js";
|
|
11
|
+
import { planBookPages, planPrintedPages, printedPageAt, selectBookParent } from "./printed-pages.js";
|
|
9
12
|
import { PAGINATE_FUNCTIONS, paginateBook, paginateScript } from "./book/paginate-book.js";
|
|
10
13
|
import { heldPageNumber, openerPageIndex, pageIndexForTarget, windowPageIndex } from "./book/paginate-target.js";
|
|
11
14
|
import { bookStylesheet, sanitizeBookCss } from "./book-css.js";
|
|
@@ -13,8 +16,8 @@ import { UNICODE_RANGE_CLEARANCE, parseUnicodeRange } from "./unicode-range.js";
|
|
|
13
16
|
import { FontCssRefusedError, fontCss, fontFaceCss } from "./font-css.js";
|
|
14
17
|
import { fontFormat } from "./font-format.js";
|
|
15
18
|
import { fontReferenceRefusals } from "./font-references.js";
|
|
16
|
-
import { MOTIF_DEPTH_LIMIT, MotifRefusedError, documentLanguage, expandMotifs, hasMotifPlacement, libraryMotifPlacementRefusal, motifPlacementRefusals, renderMotifPlacements } from "./motifs.js";
|
|
17
|
-
import {
|
|
19
|
+
import { MOTIF_DEPTH_LIMIT, MotifRefusedError, documentLanguage, expandMotifs, hasHostMotifPlacement, hasMotifPlacement, libraryMotifPlacementRefusal, motifPlacementRefusals, renderMotifPlacements } from "./motifs.js";
|
|
20
|
+
import { PLACEMENT_ATTRIBUTES, classifyMediaAnswer, placementCropEdges, placementPlaceholderSource, placementPlan, resolvePlacements } from "./placements.js";
|
|
18
21
|
import { RequiredSlotsRefusedError, UnmeasuredSlotsRefusedError, mergeSlotFitResults, requiredSlotRefusal, unmeasuredSlotRefusal } from "./slot-policy.js";
|
|
19
22
|
import { MAX_SLOT_DECLARATIONS, SlotFillRefusedError, hasSlotWork, prepareSlotCollection, prepareSlots, slotFillRefusal } from "./slots.js";
|
|
20
23
|
import { SlotFillsConflictError, SlotFillsRefusedError, assertSingleSlotCopy, assertSlotPrintOptions, collectSlotPrintCopies, isSlotCopyRefusal, slotFillCopyId, slotFillsConflict } from "./slot-print.js";
|
|
@@ -30,4 +33,4 @@ function searchText(html) {
|
|
|
30
33
|
return html.replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1\s*>/gi, " ").replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/\s+/g, " ").trim();
|
|
31
34
|
}
|
|
32
35
|
//#endregion
|
|
33
|
-
export { BINDINGS, BOOK_COLUMN_MAX, BOOK_COLUMN_MIN, BOOK_FLOW_COLUMN_MIN_PX, 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, documentLanguage, embedFunction, expandMotifs, fill, fillBookFurniture, findActiveContent, fontCss, fontFaceCss, fontFormat, fontReferenceRefusals, furnitureScript, hasBindings, hasMotifPlacement, hasSlotWork, heldPageNumber, isSlotCopyRefusal, libraryMotifPlacementRefusal, mergeSlotFitResults, motifPlacementRefusals, openerPageIndex, pageIndexForTarget, paginateBook, paginateScript, parseBookPositionKey, parseUnicodeRange, planBookPages, planPrintedPages, prepareSlotCollection, prepareSlots, printedPageAt, renderMotifPlacements, requiredSlotRefusal, sanitize, sanitizeBookCss, searchText, selectBookParent, slotFillCopyId, slotFillRefusal, slotFillsConflict, unmeasuredSlotRefusal, windowPageIndex };
|
|
36
|
+
export { ABSOLUTE_PX, ABSOLUTE_UNITS, BINDINGS, BOOK_COLUMN_MAX, BOOK_COLUMN_MIN, BOOK_FLOW_COLUMN_MIN_PX, FontCssRefusedError, MAX_SLOT_DECLARATIONS, MOTIF_DEPTH_LIMIT, MotifRefusedError, PAGE_REFERENCE_ATTRIBUTE, PAGE_REFERENCE_SELECTOR, PAGE_REFERENCE_VALUE, PAGINATE_FUNCTIONS, PLACEMENT_ATTRIBUTES, REFUSED_ELEMENTS, RequiredSlotsRefusedError, SlotFillRefusedError, SlotFillsConflictError, SlotFillsRefusedError, UNICODE_RANGE_CLEARANCE, UnmeasuredSlotsRefusedError, assertSingleSlotCopy, assertSlotPrintOptions, base64ToBytes, bindingNames, bindingRoot, bookDocument, bookPrintPart, bookStylesheet, bookView, bookViewFromHtml, bytesToBase64, classify, classifyMediaAnswer, collectSlotPrintCopies, documentLanguage, embedFunction, expandMotifs, fill, fillBookFurniture, findActiveContent, fitRegionSize, fontCss, fontFaceCss, fontFormat, fontReferenceRefusals, furnitureScript, hasBindings, hasHostMotifPlacement, hasMotifPlacement, hasSlotWork, heldPageNumber, isSlotCopyRefusal, lengthPx, libraryMotifPlacementRefusal, mergeSlotFitResults, motifPlacementRefusals, openerPageIndex, pageIndexForTarget, pageReferenceTarget, paginateBook, paginateScript, parseBookPositionKey, parseCssLength, parseUnicodeRange, placementCropEdges, placementPlaceholderSource, placementPlan, planBookPages, planPrintedPages, prepareSlotCollection, prepareSlots, printedPageAt, renderMotifPlacements, requiredSlotRefusal, resolvePlacements, safeImageSource, sanitize, sanitizeBookCss, searchText, selectBookParent, slotFillCopyId, slotFillRefusal, slotFillsConflict, unmeasuredSlotRefusal, windowPageIndex };
|
package/dist/motifs.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface ExpandMotifsOptions {
|
|
|
11
11
|
dataFonts?: boolean;
|
|
12
12
|
signal: AbortSignal;
|
|
13
13
|
deferPaths?: Iterable<string>;
|
|
14
|
+
deferLibrary?: boolean;
|
|
14
15
|
}
|
|
15
16
|
export interface PlacedMotif extends MotifPlacement {
|
|
16
17
|
version: number;
|
|
@@ -26,6 +27,7 @@ export declare class MotifRefusedError extends Error {
|
|
|
26
27
|
}
|
|
27
28
|
export declare function libraryMotifPlacementRefusal(html: string): string | undefined;
|
|
28
29
|
export declare function hasMotifPlacement(html: string): boolean;
|
|
30
|
+
export declare function hasHostMotifPlacement(html: string): boolean;
|
|
29
31
|
export declare function renderMotifPlacements(html: string, replacement: (key: string) => string | undefined): string;
|
|
30
32
|
export declare function documentLanguage(html: string): string | undefined;
|
|
31
33
|
export declare function expandMotifs(html: string, options: ExpandMotifsOptions): Promise<ExpandedMotifs>;
|
package/dist/motifs.js
CHANGED
|
@@ -26,6 +26,9 @@ function libraryMotifPlacementRefusal(html) {
|
|
|
26
26
|
function hasMotifPlacement(html) {
|
|
27
27
|
return motifElements(html).length > 0;
|
|
28
28
|
}
|
|
29
|
+
function hasHostMotifPlacement(html) {
|
|
30
|
+
return motifElements(html).some((element) => !(element.attribs[MOTIF_ATTRIBUTE] ?? "").startsWith(LIBRARY_MOTIF_PREFIX));
|
|
31
|
+
}
|
|
29
32
|
function renderMotifPlacements(html, replacement) {
|
|
30
33
|
const document = parse(html);
|
|
31
34
|
const visit = (nodes) => {
|
|
@@ -207,7 +210,7 @@ async function expandNodes(nodes, inheritedLang, stack, state) {
|
|
|
207
210
|
for (const node of [...nodes]) {
|
|
208
211
|
state.options.signal.throwIfAborted();
|
|
209
212
|
if (!isTag(node)) continue;
|
|
210
|
-
if (Object.hasOwn(node.attribs, MOTIF_ATTRIBUTE)) await expandPlacement(node, inheritedLang, stack, state);
|
|
213
|
+
if (Object.hasOwn(node.attribs, MOTIF_ATTRIBUTE) && !(state.options.deferLibrary && (node.attribs[MOTIF_ATTRIBUTE] ?? "").startsWith(LIBRARY_MOTIF_PREFIX))) await expandPlacement(node, inheritedLang, stack, state);
|
|
211
214
|
else await expandNodes(node.children, inheritedLang, stack, state);
|
|
212
215
|
}
|
|
213
216
|
}
|
|
@@ -246,4 +249,4 @@ async function motifPlacementRefusals(html, options) {
|
|
|
246
249
|
return (await run(html, options, true)).state.refusals;
|
|
247
250
|
}
|
|
248
251
|
//#endregion
|
|
249
|
-
export { MOTIF_DEPTH_LIMIT, MotifRefusedError, documentLanguage, expandMotifs, hasMotifPlacement, libraryMotifPlacementRefusal, motifPlacementRefusals, renderMotifPlacements };
|
|
252
|
+
export { MOTIF_DEPTH_LIMIT, MotifRefusedError, documentLanguage, expandMotifs, hasHostMotifPlacement, hasMotifPlacement, libraryMotifPlacementRefusal, motifPlacementRefusals, renderMotifPlacements };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Media, Page, Placement } from "@segnavia/format";
|
|
2
|
+
//#region src/placements.d.ts
|
|
3
|
+
export declare const PLACEMENT_ATTRIBUTES: readonly ["data-segnavia-media", "data-segnavia-page", "data-segnavia-box", "data-segnavia-crop"];
|
|
4
|
+
export type ResolvedPlacement = {
|
|
5
|
+
kind: 'media';
|
|
6
|
+
media: Media;
|
|
7
|
+
src: string;
|
|
8
|
+
} | {
|
|
9
|
+
kind: 'page';
|
|
10
|
+
page: Page;
|
|
11
|
+
};
|
|
12
|
+
export type ResolvePlacement<R extends {
|
|
13
|
+
kind: string;
|
|
14
|
+
} = ResolvedPlacement> = (placement: Placement, context: {
|
|
15
|
+
purpose: 'screen' | 'print';
|
|
16
|
+
width?: number;
|
|
17
|
+
signal: AbortSignal;
|
|
18
|
+
}) => Promise<R | undefined>;
|
|
19
|
+
export interface PlacementPlanEntry {
|
|
20
|
+
index: number;
|
|
21
|
+
placement: Placement;
|
|
22
|
+
alt?: string;
|
|
23
|
+
lang?: string;
|
|
24
|
+
dir?: string;
|
|
25
|
+
width?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface PlacementRefusal {
|
|
28
|
+
index: number;
|
|
29
|
+
address?: string;
|
|
30
|
+
page?: string | number;
|
|
31
|
+
message: string;
|
|
32
|
+
}
|
|
33
|
+
export interface PlacementPlan {
|
|
34
|
+
entries: PlacementPlanEntry[];
|
|
35
|
+
refusals: PlacementRefusal[];
|
|
36
|
+
}
|
|
37
|
+
export type PlacementReport = (entry: {
|
|
38
|
+
address: string | undefined;
|
|
39
|
+
page: string | number | undefined;
|
|
40
|
+
reason: 'missing' | 'refused-src' | 'invalid' | 'failed';
|
|
41
|
+
message?: string;
|
|
42
|
+
}) => void;
|
|
43
|
+
export interface PlacementsOptions<R extends {
|
|
44
|
+
kind: string;
|
|
45
|
+
} = ResolvedPlacement> {
|
|
46
|
+
resolve: ResolvePlacement<R>;
|
|
47
|
+
report?: PlacementReport;
|
|
48
|
+
}
|
|
49
|
+
type ResolvedMedia = Extract<ResolvedPlacement, {
|
|
50
|
+
kind: 'media';
|
|
51
|
+
}>;
|
|
52
|
+
export type MediaAnswer = {
|
|
53
|
+
status: 'ok';
|
|
54
|
+
value: ResolvedMedia;
|
|
55
|
+
} | {
|
|
56
|
+
status: 'missing' | 'refused-src';
|
|
57
|
+
message?: undefined;
|
|
58
|
+
} | {
|
|
59
|
+
status: 'invalid';
|
|
60
|
+
message: string;
|
|
61
|
+
} | {
|
|
62
|
+
status: 'failed';
|
|
63
|
+
message: string;
|
|
64
|
+
};
|
|
65
|
+
export declare function classifyMediaAnswer(input: {
|
|
66
|
+
answer: {
|
|
67
|
+
kind: string;
|
|
68
|
+
} | undefined;
|
|
69
|
+
}): Exclude<MediaAnswer, {
|
|
70
|
+
status: 'failed';
|
|
71
|
+
}>;
|
|
72
|
+
export declare function classifyMediaAnswer(input: {
|
|
73
|
+
error: unknown;
|
|
74
|
+
}): Extract<MediaAnswer, {
|
|
75
|
+
status: 'failed';
|
|
76
|
+
}>;
|
|
77
|
+
export declare function placementCropEdges(crop: Placement['crop']): string | undefined;
|
|
78
|
+
export declare function placementPlan(html: string): PlacementPlan;
|
|
79
|
+
export declare function placementPlaceholderSource(width?: number, height?: number): string;
|
|
80
|
+
export declare function resolvePlacements<R extends {
|
|
81
|
+
kind: string;
|
|
82
|
+
}>(html: string, options: {
|
|
83
|
+
resolvePlacement: ResolvePlacement<R>;
|
|
84
|
+
purpose: 'screen' | 'print';
|
|
85
|
+
width: number;
|
|
86
|
+
signal: AbortSignal;
|
|
87
|
+
report?: PlacementReport;
|
|
88
|
+
}): Promise<string>;
|
|
89
|
+
//#endregion
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { elements, parse, safeImageSource, serialize } from "./sanitize.js";
|
|
2
|
+
import { Placement, mediaTextDirection } from "@segnavia/format";
|
|
3
|
+
//#region src/placements.ts
|
|
4
|
+
const PLACEMENT_ATTRIBUTES = [
|
|
5
|
+
"data-segnavia-media",
|
|
6
|
+
"data-segnavia-page",
|
|
7
|
+
"data-segnavia-box",
|
|
8
|
+
"data-segnavia-crop"
|
|
9
|
+
];
|
|
10
|
+
const [MEDIA_ATTRIBUTE, PAGE_ATTRIBUTE, BOX_ATTRIBUTE, CROP_ATTRIBUTE] = PLACEMENT_ATTRIBUTES;
|
|
11
|
+
function classifyMediaAnswer(input) {
|
|
12
|
+
if ("error" in input) return {
|
|
13
|
+
status: "failed",
|
|
14
|
+
message: input.error instanceof Error ? input.error.message : String(input.error)
|
|
15
|
+
};
|
|
16
|
+
const answer = input.answer;
|
|
17
|
+
if (!answer) return { status: "missing" };
|
|
18
|
+
if (answer.kind !== "media" || !("src" in answer) || !("media" in answer)) return {
|
|
19
|
+
status: "invalid",
|
|
20
|
+
message: `resolved ${answer.kind}, not media`
|
|
21
|
+
};
|
|
22
|
+
const media = answer;
|
|
23
|
+
return safeImageSource(media.src) ? {
|
|
24
|
+
status: "ok",
|
|
25
|
+
value: media
|
|
26
|
+
} : { status: "refused-src" };
|
|
27
|
+
}
|
|
28
|
+
function placementCropEdges(crop) {
|
|
29
|
+
if (!crop) return void 0;
|
|
30
|
+
return `${crop.top ?? 0} ${crop.right ?? 0} ${crop.bottom ?? 0} ${crop.left ?? 0}`;
|
|
31
|
+
}
|
|
32
|
+
function positiveInteger(value) {
|
|
33
|
+
return value !== void 0 && /^[1-9][0-9]*$/.test(value) ? Number(value) : void 0;
|
|
34
|
+
}
|
|
35
|
+
function pageSelector(value) {
|
|
36
|
+
if (value === void 0) return void 0;
|
|
37
|
+
return /^[1-9][0-9]*$/.test(value) ? Number(value) : value;
|
|
38
|
+
}
|
|
39
|
+
function cropValue(value) {
|
|
40
|
+
if (value === void 0) return void 0;
|
|
41
|
+
const edges = value.trim().split(/\s+/);
|
|
42
|
+
if (edges.length < 1 || edges.length > 4) return { top: value };
|
|
43
|
+
const [top, second, third, fourth] = edges;
|
|
44
|
+
return {
|
|
45
|
+
top,
|
|
46
|
+
right: second ?? top,
|
|
47
|
+
bottom: third ?? top,
|
|
48
|
+
left: fourth ?? second ?? top
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function plannedElement(element, index) {
|
|
52
|
+
const address = element.attribs[MEDIA_ATTRIBUTE];
|
|
53
|
+
const page = pageSelector(element.attribs[PAGE_ATTRIBUTE]);
|
|
54
|
+
try {
|
|
55
|
+
return {
|
|
56
|
+
index,
|
|
57
|
+
placement: Placement.parse({
|
|
58
|
+
kind: "media",
|
|
59
|
+
address,
|
|
60
|
+
...page === void 0 ? {} : { page },
|
|
61
|
+
...element.attribs[BOX_ATTRIBUTE] === void 0 ? {} : { box: element.attribs[BOX_ATTRIBUTE] },
|
|
62
|
+
...element.attribs[CROP_ATTRIBUTE] === void 0 ? {} : { crop: cropValue(element.attribs[CROP_ATTRIBUTE]) }
|
|
63
|
+
}, "page"),
|
|
64
|
+
..."alt" in element.attribs ? { alt: element.attribs.alt } : {},
|
|
65
|
+
..."lang" in element.attribs ? { lang: element.attribs.lang } : {},
|
|
66
|
+
..."dir" in element.attribs ? { dir: element.attribs.dir } : {},
|
|
67
|
+
...positiveInteger(element.attribs.width) === void 0 ? {} : { width: positiveInteger(element.attribs.width) }
|
|
68
|
+
};
|
|
69
|
+
} catch (error) {
|
|
70
|
+
return {
|
|
71
|
+
index,
|
|
72
|
+
address,
|
|
73
|
+
page,
|
|
74
|
+
message: error instanceof Error ? error.message : String(error)
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function planElements(found) {
|
|
79
|
+
const planned = found.flatMap((element, index) => element.name.toLowerCase() === "img" && MEDIA_ATTRIBUTE in element.attribs ? [plannedElement(element, index)] : []);
|
|
80
|
+
return {
|
|
81
|
+
entries: planned.filter((entry) => "placement" in entry),
|
|
82
|
+
refusals: planned.filter((entry) => !("placement" in entry))
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function placementPlan(html) {
|
|
86
|
+
return planElements(elements(parse(html).children));
|
|
87
|
+
}
|
|
88
|
+
function placementPlaceholderSource(width = 400, height = 300) {
|
|
89
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><rect width="100%" height="100%" fill="#eee"/><rect x="0.5" y="0.5" width="${width - 1}" height="${height - 1}" fill="none" stroke="#aaa"/></svg>`;
|
|
90
|
+
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
|
|
91
|
+
}
|
|
92
|
+
function placeholder(element) {
|
|
93
|
+
const authoredWidth = positiveInteger(element.attribs.width);
|
|
94
|
+
const authoredHeight = positiveInteger(element.attribs.height);
|
|
95
|
+
const width = authoredWidth ?? (authoredHeight === void 0 ? 400 : Math.round(authoredHeight * 4 / 3));
|
|
96
|
+
const height = authoredHeight ?? (authoredWidth === void 0 ? 300 : Math.round(authoredWidth * 3 / 4));
|
|
97
|
+
element.attribs.src = placementPlaceholderSource(width, height);
|
|
98
|
+
element.attribs["data-segnavia-placement"] = "missing";
|
|
99
|
+
if (!("alt" in element.attribs)) element.attribs.alt = "";
|
|
100
|
+
for (const attribute of PLACEMENT_ATTRIBUTES) delete element.attribs[attribute];
|
|
101
|
+
}
|
|
102
|
+
function applyResolved(element, entry, resolved) {
|
|
103
|
+
element.attribs.src = resolved.src;
|
|
104
|
+
if (entry.alt === void 0 && resolved.media.alt) {
|
|
105
|
+
element.attribs.alt = resolved.media.alt.text;
|
|
106
|
+
if (entry.lang === void 0 && entry.dir === void 0) {
|
|
107
|
+
if (resolved.media.alt.lang) element.attribs.lang = resolved.media.alt.lang;
|
|
108
|
+
element.attribs.dir = mediaTextDirection(resolved.media.alt);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const crop = placementCropEdges(entry.placement.crop);
|
|
112
|
+
if (crop) {
|
|
113
|
+
const declaration = `object-view-box: inset(${crop})`;
|
|
114
|
+
const authored = element.attribs.style?.trim();
|
|
115
|
+
element.attribs.style = authored ? `${authored.replace(/;?$/, ";")} ${declaration}` : declaration;
|
|
116
|
+
}
|
|
117
|
+
for (const attribute of PLACEMENT_ATTRIBUTES) delete element.attribs[attribute];
|
|
118
|
+
}
|
|
119
|
+
function abortable(work, signal) {
|
|
120
|
+
return new Promise((resolve, reject) => {
|
|
121
|
+
if (signal.aborted) {
|
|
122
|
+
reject(signal.reason);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const aborted = () => reject(signal.reason);
|
|
126
|
+
signal.addEventListener("abort", aborted, { once: true });
|
|
127
|
+
work().then((value) => {
|
|
128
|
+
signal.removeEventListener("abort", aborted);
|
|
129
|
+
resolve(value);
|
|
130
|
+
}, (error) => {
|
|
131
|
+
signal.removeEventListener("abort", aborted);
|
|
132
|
+
reject(error);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
async function resolvePlacements(html, options) {
|
|
137
|
+
const document = parse(html);
|
|
138
|
+
const found = elements(document.children);
|
|
139
|
+
const plan = planElements(found);
|
|
140
|
+
if (plan.entries.length === 0 && plan.refusals.length === 0) return html;
|
|
141
|
+
options.signal.throwIfAborted();
|
|
142
|
+
for (const refusal of plan.refusals) {
|
|
143
|
+
const element = found[refusal.index];
|
|
144
|
+
if (element) placeholder(element);
|
|
145
|
+
options.report?.({
|
|
146
|
+
address: refusal.address,
|
|
147
|
+
page: refusal.page,
|
|
148
|
+
reason: "invalid",
|
|
149
|
+
message: refusal.message
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
await Promise.all(plan.entries.map(async (entry) => {
|
|
153
|
+
const element = found[entry.index];
|
|
154
|
+
if (!element) return;
|
|
155
|
+
try {
|
|
156
|
+
const resolved = await abortable(() => options.resolvePlacement(entry.placement, {
|
|
157
|
+
purpose: options.purpose,
|
|
158
|
+
width: entry.width ?? options.width,
|
|
159
|
+
signal: options.signal
|
|
160
|
+
}), options.signal);
|
|
161
|
+
options.signal.throwIfAborted();
|
|
162
|
+
const classified = classifyMediaAnswer({ answer: resolved });
|
|
163
|
+
if (classified.status === "ok") applyResolved(element, entry, classified.value);
|
|
164
|
+
else {
|
|
165
|
+
placeholder(element);
|
|
166
|
+
options.report?.({
|
|
167
|
+
address: entry.placement.address,
|
|
168
|
+
page: entry.placement.page,
|
|
169
|
+
reason: classified.status,
|
|
170
|
+
...classified.message === void 0 ? {} : { message: classified.message }
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (options.signal.aborted) throw options.signal.reason;
|
|
175
|
+
const classified = classifyMediaAnswer({ error });
|
|
176
|
+
placeholder(element);
|
|
177
|
+
options.report?.({
|
|
178
|
+
address: entry.placement.address,
|
|
179
|
+
page: entry.placement.page,
|
|
180
|
+
reason: classified.status,
|
|
181
|
+
message: classified.message
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}));
|
|
185
|
+
options.signal.throwIfAborted();
|
|
186
|
+
return serialize(document);
|
|
187
|
+
}
|
|
188
|
+
//#endregion
|
|
189
|
+
export { PLACEMENT_ATTRIBUTES, classifyMediaAnswer, placementCropEdges, placementPlaceholderSource, placementPlan, resolvePlacements };
|
package/dist/printed-pages.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BookSectionParents as BookSectionParents$1, NumberSystem, PrintedPageRole, PrintedPageRules } from "@segnavia/format";
|
|
1
|
+
import { BOOK_PARENT_NAMES, 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 {
|
|
@@ -26,12 +26,7 @@ export interface BookPrintedPagePlan extends Omit<BookPageFacts, 'blank' | 'sect
|
|
|
26
26
|
}
|
|
27
27
|
export declare function printedPageAt(rules: PrintedPageRules, index: number, count: number, direction: 'ltr' | 'rtl'): Pick<PrintedPagePlan, 'role' | 'parent' | 'side'>;
|
|
28
28
|
export declare function planPrintedPages(rules: PrintedPageRules, count: number, direction: 'ltr' | 'rtl', numbering?: NumberSystem): PrintedPagePlan[];
|
|
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'>;
|
|
29
|
+
export declare function selectBookParent(fact: BookPageFacts, index: number, count: number, direction: 'ltr' | 'rtl', names: typeof BOOK_PARENT_NAMES): Pick<BookPrintedPagePlan, 'role' | 'parent' | 'side'>;
|
|
35
30
|
export declare function planBookPages(facts: BookPageFacts[], direction: 'ltr' | 'rtl', names?: {
|
|
36
31
|
readonly recto: "segnavia:recto";
|
|
37
32
|
readonly verso: "segnavia:verso";
|
package/dist/printed-pages.js
CHANGED
|
@@ -39,7 +39,7 @@ function planPrintedPages(rules, count, direction, numbering = "arabic") {
|
|
|
39
39
|
};
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
|
-
function selectBookParent(fact, index, count, direction, names
|
|
42
|
+
function selectBookParent(fact, index, count, direction, names) {
|
|
43
43
|
const physical = printedPageAt({
|
|
44
44
|
every: names.recto,
|
|
45
45
|
recto: names.recto,
|
package/dist/sanitize.d.ts
CHANGED
|
@@ -4,9 +4,11 @@ export declare const REFUSED_ELEMENTS: Set<string>;
|
|
|
4
4
|
export interface SanitizeOptions {
|
|
5
5
|
linkOrigins?: Iterable<string>;
|
|
6
6
|
keepScripts?: boolean;
|
|
7
|
+
dependencyOrigins?: Iterable<string>;
|
|
7
8
|
dataFonts?: boolean;
|
|
8
9
|
refuseElements?: Iterable<string>;
|
|
9
10
|
}
|
|
11
|
+
export declare function safeImageSource(value: string): boolean;
|
|
10
12
|
export declare function findActiveContent(html: string, options?: SanitizeOptions): string[];
|
|
11
13
|
export declare function sanitize(html: string, options?: SanitizeOptions): string;
|
|
12
14
|
//#endregion
|
package/dist/sanitize.js
CHANGED
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
import { sanitizeCss } from "./sanitize-css.js";
|
|
2
2
|
import { isTag } from "domhandler";
|
|
3
|
+
import { dependencyDecision, dependencyOriginSet, originSet } from "@segnavia/format";
|
|
3
4
|
import render from "dom-serializer";
|
|
4
5
|
import { parseDocument } from "htmlparser2";
|
|
5
6
|
//#region src/sanitize.ts
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
7
|
+
const REFUSED = {
|
|
8
|
+
script: "always",
|
|
9
|
+
iframe: "always",
|
|
10
|
+
frame: "always",
|
|
11
|
+
frameset: "always",
|
|
12
|
+
object: "always",
|
|
13
|
+
embed: "always",
|
|
14
|
+
applet: "always",
|
|
15
|
+
base: "always",
|
|
16
|
+
portal: "always",
|
|
17
|
+
link: "always",
|
|
18
|
+
form: "unless-scripts",
|
|
19
|
+
input: "unless-scripts",
|
|
20
|
+
button: "unless-scripts",
|
|
21
|
+
select: "unless-scripts",
|
|
22
|
+
textarea: "unless-scripts"
|
|
23
|
+
};
|
|
24
|
+
const REFUSED_ELEMENTS = new Set(Object.keys(REFUSED));
|
|
23
25
|
const REFUSED_SVG_ELEMENTS = /* @__PURE__ */ new Set([
|
|
24
26
|
"script",
|
|
25
27
|
"animate",
|
|
@@ -78,13 +80,8 @@ function insideSvg(element) {
|
|
|
78
80
|
function normalizeUrl(value) {
|
|
79
81
|
return [...value].filter((c) => c.charCodeAt(0) > 32 && c.charCodeAt(0) !== 127).join("").toLowerCase();
|
|
80
82
|
}
|
|
81
|
-
function
|
|
82
|
-
|
|
83
|
-
for (const value of values ?? []) try {
|
|
84
|
-
const url = new URL(value);
|
|
85
|
-
if (url.protocol === "https:") set.add(url.origin.toLowerCase());
|
|
86
|
-
} catch {}
|
|
87
|
-
return set;
|
|
83
|
+
function safeImageSource(value) {
|
|
84
|
+
return DATA_IMAGE.test(normalizeUrl(value));
|
|
88
85
|
}
|
|
89
86
|
function safeUrl(attribute, value, allowed) {
|
|
90
87
|
const url = normalizeUrl(value);
|
|
@@ -98,74 +95,115 @@ function safeUrl(attribute, value, allowed) {
|
|
|
98
95
|
}
|
|
99
96
|
}
|
|
100
97
|
if (attribute === "srcset") return DATA_IMAGE_SRCSET.test(url);
|
|
101
|
-
if (attribute === "src" || attribute === "poster") return
|
|
98
|
+
if (attribute === "src" || attribute === "poster") return safeImageSource(value);
|
|
102
99
|
return false;
|
|
103
100
|
}
|
|
104
101
|
function refusedElement(element, refused, keepScripts) {
|
|
105
102
|
const name = element.name.toLowerCase();
|
|
106
103
|
if (name === "script") return !keepScripts;
|
|
104
|
+
if (name === "link" && keepScripts && element.attribs.rel?.toLowerCase() === "stylesheet") return false;
|
|
107
105
|
if (refused.has(name)) return true;
|
|
108
106
|
if (insideSvg(element) && REFUSED_SVG_ELEMENTS.has(name)) return true;
|
|
109
107
|
return name === "meta" && Object.keys(element.attribs).some((attribute) => attribute.toLowerCase() === "http-equiv");
|
|
110
108
|
}
|
|
111
|
-
function
|
|
109
|
+
function dependencyAttribute(element, attribute, value, options, dependencies) {
|
|
110
|
+
if (!options.keepScripts) return void 0;
|
|
111
|
+
const tag = element.name.toLowerCase();
|
|
112
|
+
if (!(tag === "script" && attribute === "src" || tag === "link" && element.attribs.rel?.toLowerCase() === "stylesheet" && attribute === "href")) return void 0;
|
|
113
|
+
const decision = dependencyDecision(value, dependencies);
|
|
114
|
+
return decision.allowed ? "" : decision.reason;
|
|
115
|
+
}
|
|
116
|
+
function refusedAttribute(element, name, value, allowed, options, dependencies) {
|
|
112
117
|
const attribute = name.toLowerCase();
|
|
113
|
-
if (attribute.startsWith("on")) return
|
|
114
|
-
|
|
118
|
+
if (attribute.startsWith("on")) return options.keepScripts ? void 0 : "";
|
|
119
|
+
const dependency = dependencyAttribute(element, attribute, value, options, dependencies);
|
|
120
|
+
if (dependency !== void 0) return dependency || void 0;
|
|
121
|
+
return REFUSED_ATTRIBUTES.has(attribute) || URL_ATTRIBUTES.has(attribute) && !safeUrl(attribute, value, allowed) ? "" : void 0;
|
|
115
122
|
}
|
|
116
123
|
function refusedSet(options) {
|
|
117
|
-
|
|
124
|
+
const defaults = options.keepScripts ? Object.entries(REFUSED).filter(([, rule]) => rule === "always").map(([name]) => name) : REFUSED_ELEMENTS;
|
|
125
|
+
return new Set([...defaults, ...options.refuseElements ?? []].map((name) => name.toLowerCase()));
|
|
118
126
|
}
|
|
119
127
|
function styleText(element) {
|
|
120
128
|
return element.children.map((child) => "data" in child ? child.data : "").join("");
|
|
121
129
|
}
|
|
122
|
-
function
|
|
130
|
+
function dependencyCss(source, declarations, options, dependencies) {
|
|
131
|
+
if (!options.keepScripts) return sanitizeCss(source, declarations, options.dataFonts ?? false);
|
|
132
|
+
const restores = [];
|
|
133
|
+
const hold = (url) => {
|
|
134
|
+
if (!dependencyDecision(url, dependencies).allowed) return url;
|
|
135
|
+
return `data:image/segnavia-dependency-${restores.push(url) - 1},x`;
|
|
136
|
+
};
|
|
137
|
+
let marker = "segnavia-held-import";
|
|
138
|
+
while (source.includes(marker)) marker += "-x";
|
|
139
|
+
const imports = [];
|
|
140
|
+
let protectedCss = source.replace(/@import\s+(?:url\(\s*)?["']?(https:\/\/[^\s"')]+)["']?\s*\)?[^;]*;/gi, (rule, url) => {
|
|
141
|
+
if (hold(url) === url) return rule;
|
|
142
|
+
const index = imports.push(rule) - 1;
|
|
143
|
+
return `.${marker}-${index}{}`;
|
|
144
|
+
});
|
|
145
|
+
protectedCss = protectedCss.replace(/@font-face\s*\{[^}]*\}/gi, (block) => block.replace(/https:\/\/[^\s"')]+/gi, hold));
|
|
146
|
+
const result = sanitizeCss(protectedCss, declarations, options.dataFonts ?? false);
|
|
147
|
+
const restore = (value) => value.replace(/data:image\/segnavia-dependency-(\d+),x/g, (_, index) => restores[Number(index)] ?? "");
|
|
148
|
+
let css = restore(result.css);
|
|
149
|
+
for (const [index, rule] of imports.entries()) css = css.replace(`.${marker}-${index}{}`, rule);
|
|
150
|
+
return {
|
|
151
|
+
css,
|
|
152
|
+
removed: result.removed.map(restore)
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function styleFindings(element, options, dependencies) {
|
|
123
156
|
const found = [];
|
|
124
|
-
if (element.name.toLowerCase() === "style") for (const construct of
|
|
157
|
+
if (element.name.toLowerCase() === "style") for (const construct of dependencyCss(styleText(element), false, options, dependencies).removed) found.push(`<style> ${construct}`);
|
|
125
158
|
for (const [name, value] of Object.entries(element.attribs)) {
|
|
126
159
|
if (name.toLowerCase() !== "style") continue;
|
|
127
|
-
for (const construct of
|
|
160
|
+
for (const construct of dependencyCss(value, true, options, dependencies).removed) found.push(`${element.name}[${name}] ${construct}`);
|
|
128
161
|
}
|
|
129
162
|
return found;
|
|
130
163
|
}
|
|
131
164
|
function findActiveContent(html, options = {}) {
|
|
132
|
-
const allowed =
|
|
165
|
+
const allowed = originSet(options.linkOrigins);
|
|
166
|
+
const dependencies = dependencyOriginSet(options.dependencyOrigins);
|
|
133
167
|
const refused = refusedSet(options);
|
|
134
168
|
const keepScripts = options.keepScripts ?? false;
|
|
135
169
|
const found = [];
|
|
136
170
|
for (const element of elements(parse(html).children)) {
|
|
137
171
|
if (refusedElement(element, refused, keepScripts)) found.push(`<${element.name}>`);
|
|
138
|
-
found.push(...styleFindings(element, options
|
|
139
|
-
for (const [name, value] of Object.entries(element.attribs))
|
|
172
|
+
found.push(...styleFindings(element, options, dependencies));
|
|
173
|
+
for (const [name, value] of Object.entries(element.attribs)) {
|
|
174
|
+
const reason = refusedAttribute(element, name, value, allowed, options, dependencies);
|
|
175
|
+
if (reason !== void 0) found.push(`${element.name}[${name}]${reason ? ` ${reason}` : ""}`);
|
|
176
|
+
}
|
|
140
177
|
}
|
|
141
178
|
return found;
|
|
142
179
|
}
|
|
143
|
-
function cleanStyleElement(element,
|
|
180
|
+
function cleanStyleElement(element, options, dependencies) {
|
|
144
181
|
if (element.name.toLowerCase() !== "style") return;
|
|
145
182
|
const children = element.children.filter((candidate) => "data" in candidate);
|
|
146
183
|
const child = children[0];
|
|
147
184
|
if (!child || !("data" in child)) return;
|
|
148
|
-
child.data =
|
|
185
|
+
child.data = dependencyCss(styleText(element), false, options, dependencies).css;
|
|
149
186
|
for (const extra of children.slice(1)) if ("data" in extra) extra.data = "";
|
|
150
187
|
}
|
|
151
|
-
function cleanAttributes(element, allowed,
|
|
152
|
-
for (const [name, value] of Object.entries(element.attribs)) if (refusedAttribute(name, value, allowed,
|
|
188
|
+
function cleanAttributes(element, allowed, options, dependencies) {
|
|
189
|
+
for (const [name, value] of Object.entries(element.attribs)) if (refusedAttribute(element, name, value, allowed, options, dependencies) !== void 0) delete element.attribs[name];
|
|
153
190
|
else if (name.toLowerCase() === "style") {
|
|
154
|
-
const css =
|
|
191
|
+
const css = dependencyCss(value, true, options, dependencies).css.trim();
|
|
155
192
|
if (css) element.attribs[name] = css;
|
|
156
193
|
else delete element.attribs[name];
|
|
157
194
|
}
|
|
158
195
|
}
|
|
159
196
|
function sanitize(html, options = {}) {
|
|
160
|
-
const allowed =
|
|
197
|
+
const allowed = originSet(options.linkOrigins);
|
|
198
|
+
const dependencies = dependencyOriginSet(options.dependencyOrigins);
|
|
161
199
|
const refused = refusedSet(options);
|
|
162
200
|
const keepScripts = options.keepScripts ?? false;
|
|
163
201
|
const document = parse(html);
|
|
164
202
|
const prune = (nodes) => nodes.filter((node) => {
|
|
165
203
|
if (!isTag(node)) return true;
|
|
166
204
|
if (refusedElement(node, refused, keepScripts)) return false;
|
|
167
|
-
cleanStyleElement(node, options
|
|
168
|
-
cleanAttributes(node, allowed,
|
|
205
|
+
cleanStyleElement(node, options, dependencies);
|
|
206
|
+
cleanAttributes(node, allowed, options, dependencies);
|
|
169
207
|
node.children = prune(node.children);
|
|
170
208
|
return true;
|
|
171
209
|
});
|
|
@@ -173,4 +211,4 @@ function sanitize(html, options = {}) {
|
|
|
173
211
|
return serialize(document);
|
|
174
212
|
}
|
|
175
213
|
//#endregion
|
|
176
|
-
export { REFUSED_ELEMENTS, elements, findActiveContent, parse, sanitize, serialize };
|
|
214
|
+
export { REFUSED_ELEMENTS, elements, findActiveContent, parse, safeImageSource, sanitize, serialize };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@segnavia/html",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.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.39.0",
|
|
30
30
|
"dom-serializer": "^2.0.0",
|
|
31
31
|
"domhandler": "^5.0.3",
|
|
32
32
|
"htmlparser2": "^10.0.0",
|