@segnavia/html 0.39.0 → 0.40.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/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { base64ToBytes, bytesToBase64 } from "./base64.js";
2
- import { REFUSED_ELEMENTS, SanitizeOptions, findActiveContent, safeImageSource, sanitize } from "./sanitize.js";
2
+ import { REFUSED_ELEMENTS, SanitizeOptions, UrlPolicy, findActiveContent, safeImageSource, safeMarkdownUrl, sanitize } from "./sanitize.js";
3
3
  import { BINDINGS, FillOptions, Filled, bindingNames, bindingRoot, fill, hasBindings } from "./bindings.js";
4
4
  import { BookBuild, BookDocument, BookDocumentOptions, BookHeading, BookPrintPart, BookSectionInfo, bookPrintPart } from "./book/types.js";
5
5
  import { BOOK_COLUMN_MAX, BOOK_COLUMN_MIN, BOOK_FLOW_COLUMN_MIN_PX, BookPositionKey, parseBookPositionKey } from "./book/book-flow.js";
@@ -16,6 +16,7 @@ import { bookStylesheet, sanitizeBookCss } from "./book-css.js";
16
16
  import { FontCssRefusedError, fontCss, fontFaceCss } from "./font-css.js";
17
17
  import { fontFormat } from "./font-format.js";
18
18
  import { fontReferenceRefusals } from "./font-references.js";
19
+ import { AlertKind, AlertLabels, BodyHtmlOptions, HtmlPage, MarkdownHeading, alertLabels, bodyHtml, isDiagramFence, isGithubAlertBlock, markdownOutline, resolvePageHtml } from "./markdown.js";
19
20
  import { ExpandMotifsOptions, ExpandedMotifs, MOTIF_DEPTH_LIMIT, MotifRefusedError, PlacedMotif, documentLanguage, expandMotifs, hasHostMotifPlacement, hasMotifPlacement, libraryMotifPlacementRefusal, motifPlacementRefusals, renderMotifPlacements } from "./motifs.js";
20
21
  import { MediaAnswer, PLACEMENT_ATTRIBUTES, PlacementPlan, PlacementPlanEntry, PlacementRefusal, PlacementReport, PlacementsOptions, ResolvePlacement, ResolvedPlacement, classifyMediaAnswer, placementCropEdges, placementPlaceholderSource, placementPlan, resolvePlacements } from "./placements.js";
21
22
  import { SlotFillEntry, SlotFillRefusal, SlotFillsConflictError, SlotFillsRefusedError, SlotPrintOptions, assertSingleSlotCopy, assertSlotPrintOptions, collectSlotPrintCopies, isSlotCopyRefusal, slotFillCopyId, slotFillsConflict } from "./slot-print.js";
@@ -27,4 +28,4 @@ export type HtmlKind = 'plain' | 'live-data' | 'scripted' | 'refused';
27
28
  export declare function classify(html: string, prefix: string): HtmlKind;
28
29
  export declare function searchText(html: string): string;
29
30
  //#endregion
30
- export { ABSOLUTE_PX, ABSOLUTE_UNITS, BINDINGS, BOOK_COLUMN_MAX, BOOK_COLUMN_MIN, BOOK_FLOW_COLUMN_MIN_PX, type BookBuild, type BookDocument, type BookDocumentOptions, type BookFurnitureValues, type BookHeading, BookPageFacts, type BookPositionKey, type BookPrintPart, BookPrintedPagePlan, BookPrintedPageRole, type BookSectionInfo, type BookSectionParents, type ExpandMotifsOptions, type ExpandedMotifs, type FillOptions, type Filled, FontCssRefusedError, MAX_SLOT_DECLARATIONS, MOTIF_DEPTH_LIMIT, type MediaAnswer, MotifRefusedError, PAGE_REFERENCE_ATTRIBUTE, PAGE_REFERENCE_SELECTOR, PAGE_REFERENCE_VALUE, PAGINATE_FUNCTIONS, PLACEMENT_ATTRIBUTES, type PaginateBlock, type PaginateBookInput, type PaginateBookPage, type PaginateBookRefusal, type PaginateBookResult, type PaginateFlow, type PaginateFlowParents, type PaginateLine, type PaginateMark, type PaginateNote, type PaginateNoteRef, type PaginateNoteSeriesGeometry, type PaginateOpens, type PaginatePageMarks, type PaginatePageNote, type PaginatePageRole, type PaginateSequence, type PaginateTarget, type PaginateTargetPage, type PaginateWindow, type PlacedMotif, type PlacementPlan, type PlacementPlanEntry, type PlacementRefusal, type PlacementReport, type PlacementsOptions, type PreparedSlots, PrintedPagePlan, PrintedPageSide, REFUSED_ELEMENTS, RequiredSlotsRefusedError, type ResolvePlacement, type ResolvedPlacement, 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, 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, selectBookParent, slotFillCopyId, slotFillRefusal, slotFillsConflict, unmeasuredSlotRefusal, windowPageIndex };
31
+ export { ABSOLUTE_PX, ABSOLUTE_UNITS, type AlertKind, type AlertLabels, BINDINGS, BOOK_COLUMN_MAX, BOOK_COLUMN_MIN, BOOK_FLOW_COLUMN_MIN_PX, type BodyHtmlOptions, type BookBuild, type BookDocument, type BookDocumentOptions, type BookFurnitureValues, type BookHeading, BookPageFacts, type BookPositionKey, type BookPrintPart, BookPrintedPagePlan, BookPrintedPageRole, type BookSectionInfo, type BookSectionParents, type ExpandMotifsOptions, type ExpandedMotifs, type FillOptions, type Filled, FontCssRefusedError, type HtmlPage, MAX_SLOT_DECLARATIONS, MOTIF_DEPTH_LIMIT, type MarkdownHeading, type MediaAnswer, MotifRefusedError, PAGE_REFERENCE_ATTRIBUTE, PAGE_REFERENCE_SELECTOR, PAGE_REFERENCE_VALUE, PAGINATE_FUNCTIONS, PLACEMENT_ATTRIBUTES, type PaginateBlock, type PaginateBookInput, type PaginateBookPage, type PaginateBookRefusal, type PaginateBookResult, type PaginateFlow, type PaginateFlowParents, type PaginateLine, type PaginateMark, type PaginateNote, type PaginateNoteRef, type PaginateNoteSeriesGeometry, type PaginateOpens, type PaginatePageMarks, type PaginatePageNote, type PaginatePageRole, type PaginateSequence, type PaginateTarget, type PaginateTargetPage, type PaginateWindow, type PlacedMotif, type PlacementPlan, type PlacementPlanEntry, type PlacementRefusal, type PlacementReport, type PlacementsOptions, type PreparedSlots, PrintedPagePlan, PrintedPageSide, REFUSED_ELEMENTS, RequiredSlotsRefusedError, type ResolvePlacement, type ResolvedPlacement, type SanitizeOptions, type SlotFillEntry, type SlotFillRefusal, SlotFillRefusedError, SlotFillsConflictError, SlotFillsRefusedError, type SlotPrintOptions, UNICODE_RANGE_CLEARANCE, type UnicodeRangeParse, UnmeasuredSlotsRefusedError, type UrlPolicy, alertLabels, assertSingleSlotCopy, assertSlotPrintOptions, base64ToBytes, bindingNames, bindingRoot, bodyHtml, bookDocument, bookPrintPart, bookStylesheet, bookView, bookViewFromHtml, bytesToBase64, classifyMediaAnswer, collectSlotPrintCopies, documentLanguage, embedFunction, expandMotifs, fill, fillBookFurniture, findActiveContent, fitRegionSize, fontCss, fontFaceCss, fontFormat, fontReferenceRefusals, furnitureScript, hasBindings, hasHostMotifPlacement, hasMotifPlacement, hasSlotWork, heldPageNumber, isDiagramFence, isGithubAlertBlock, isSlotCopyRefusal, lengthPx, libraryMotifPlacementRefusal, markdownOutline, mergeSlotFitResults, motifPlacementRefusals, openerPageIndex, pageIndexForTarget, pageReferenceTarget, paginateBook, paginateScript, parseBookPositionKey, parseCssLength, parseUnicodeRange, placementCropEdges, placementPlaceholderSource, placementPlan, planBookPages, planPrintedPages, prepareSlotCollection, prepareSlots, printedPageAt, renderMotifPlacements, requiredSlotRefusal, resolvePageHtml, resolvePlacements, safeImageSource, safeMarkdownUrl, sanitize, sanitizeBookCss, selectBookParent, slotFillCopyId, slotFillRefusal, slotFillsConflict, unmeasuredSlotRefusal, windowPageIndex };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { REFUSED_ELEMENTS, elements, findActiveContent, parse, safeImageSource, sanitize } from "./sanitize.js";
1
+ import { REFUSED_ELEMENTS, elements, findActiveContent, parse, safeImageSource, safeMarkdownUrl, 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";
@@ -16,6 +16,7 @@ import { UNICODE_RANGE_CLEARANCE, parseUnicodeRange } from "./unicode-range.js";
16
16
  import { FontCssRefusedError, fontCss, fontFaceCss } from "./font-css.js";
17
17
  import { fontFormat } from "./font-format.js";
18
18
  import { fontReferenceRefusals } from "./font-references.js";
19
+ import { alertLabels, bodyHtml, isDiagramFence, isGithubAlertBlock, markdownOutline, resolvePageHtml } from "./markdown.js";
19
20
  import { MOTIF_DEPTH_LIMIT, MotifRefusedError, documentLanguage, expandMotifs, hasHostMotifPlacement, hasMotifPlacement, libraryMotifPlacementRefusal, motifPlacementRefusals, renderMotifPlacements } from "./motifs.js";
20
21
  import { PLACEMENT_ATTRIBUTES, classifyMediaAnswer, placementCropEdges, placementPlaceholderSource, placementPlan, resolvePlacements } from "./placements.js";
21
22
  import { RequiredSlotsRefusedError, UnmeasuredSlotsRefusedError, mergeSlotFitResults, requiredSlotRefusal, unmeasuredSlotRefusal } from "./slot-policy.js";
@@ -33,4 +34,4 @@ function searchText(html) {
33
34
  return html.replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1\s*>/gi, " ").replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/\s+/g, " ").trim();
34
35
  }
35
36
  //#endregion
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 };
37
+ 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, alertLabels, assertSingleSlotCopy, assertSlotPrintOptions, base64ToBytes, bindingNames, bindingRoot, bodyHtml, 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, isDiagramFence, isGithubAlertBlock, isSlotCopyRefusal, lengthPx, libraryMotifPlacementRefusal, markdownOutline, mergeSlotFitResults, motifPlacementRefusals, openerPageIndex, pageIndexForTarget, pageReferenceTarget, paginateBook, paginateScript, parseBookPositionKey, parseCssLength, parseUnicodeRange, placementCropEdges, placementPlaceholderSource, placementPlan, planBookPages, planPrintedPages, prepareSlotCollection, prepareSlots, printedPageAt, renderMotifPlacements, requiredSlotRefusal, resolvePageHtml, resolvePlacements, safeImageSource, safeMarkdownUrl, sanitize, sanitizeBookCss, searchText, selectBookParent, slotFillCopyId, slotFillRefusal, slotFillsConflict, unmeasuredSlotRefusal, windowPageIndex };
@@ -0,0 +1,20 @@
1
+ //#region src/markdown-policy.ts
2
+ const MARKDOWN_CONVERTER_POLICY = {
3
+ urls: {
4
+ linkProtocols: [
5
+ "https:",
6
+ "http:",
7
+ "mailto:"
8
+ ],
9
+ imageProtocols: ["https:", "http:"],
10
+ linkFragments: true,
11
+ dataImages: true
12
+ },
13
+ inertControls: [{
14
+ element: "input",
15
+ type: "checkbox",
16
+ attribute: "disabled"
17
+ }]
18
+ };
19
+ //#endregion
20
+ export { MARKDOWN_CONVERTER_POLICY };
@@ -0,0 +1,29 @@
1
+ import { Page, PageBody } from "@segnavia/format";
2
+ //#region src/markdown.d.ts
3
+ export type MarkdownHeading = {
4
+ id: string;
5
+ level: number;
6
+ title: string;
7
+ };
8
+ export type AlertKind = 'note' | 'tip' | 'important' | 'warning' | 'caution';
9
+ export type AlertLabels = Record<AlertKind, string>;
10
+ export declare const alertLabels: Readonly<Record<'en' | 'he', AlertLabels>>;
11
+ /** Whether the reader recognizes a blockquote source as a GitHub alert. */
12
+ export declare function isGithubAlertBlock(raw: string): boolean;
13
+ /** Whether the reader turns a fenced language into a diagram placeholder. */
14
+ export declare function isDiagramFence(language: string | undefined): boolean;
15
+ export interface BodyHtmlOptions {
16
+ alertLabels?: Partial<AlertLabels>;
17
+ dir?: 'ltr' | 'rtl' | 'auto';
18
+ lang?: string;
19
+ }
20
+ export declare function bodyHtml(body: PageBody, options?: BodyHtmlOptions): string;
21
+ export declare function markdownOutline(markdown: string | Extract<PageBody, {
22
+ markdown: string;
23
+ }>): MarkdownHeading[];
24
+ export type HtmlPage = Omit<Page, 'body'> & {
25
+ html: string;
26
+ };
27
+ export declare function resolvePageHtml(page: Page, options: Pick<BodyHtmlOptions, 'alertLabels'>): HtmlPage;
28
+ export declare function resolvePageHtml(page: Page): HtmlPage;
29
+ //#endregion
@@ -0,0 +1,278 @@
1
+ import { MARKDOWN_CONVERTER_POLICY } from "./markdown-policy.js";
2
+ import { sanitize } from "./sanitize.js";
3
+ import { resolveDirection } from "@segnavia/format";
4
+ import rehypeStringify from "rehype-stringify";
5
+ import remarkDirective from "remark-directive";
6
+ import remarkFrontmatter from "remark-frontmatter";
7
+ import remarkGfm from "remark-gfm";
8
+ import remarkParse from "remark-parse";
9
+ import remarkRehype from "remark-rehype";
10
+ import { unified } from "unified";
11
+ import { parse } from "yaml";
12
+ //#region src/markdown.ts
13
+ const alertLabels = {
14
+ en: {
15
+ note: "Note",
16
+ tip: "Tip",
17
+ important: "Important",
18
+ warning: "Warning",
19
+ caution: "Caution"
20
+ },
21
+ he: {
22
+ note: "הערה",
23
+ tip: "עצה",
24
+ important: "חשוב",
25
+ warning: "אזהרה",
26
+ caution: "זהירות"
27
+ }
28
+ };
29
+ function alertMarker(value) {
30
+ const match = /^\[!([A-Za-z]+)\](?:\r?\n|\s*)/.exec(value);
31
+ const kind = match?.[1]?.toLowerCase();
32
+ return kind && match && Object.hasOwn(alertLabels.en, kind) ? {
33
+ kind,
34
+ length: match[0].length
35
+ } : void 0;
36
+ }
37
+ /** Whether the reader recognizes a blockquote source as a GitHub alert. */
38
+ function isGithubAlertBlock(raw) {
39
+ const content = raw.split("\n").map((line) => /^ {0,3}>[ \t]?(.*)$/.exec(line)?.[1] ?? "").find((line) => line.trim() !== "");
40
+ return Boolean(content && alertMarker(content));
41
+ }
42
+ /** Whether the reader turns a fenced language into a diagram placeholder. */
43
+ function isDiagramFence(language) {
44
+ return (language ?? "").trim().split(/[ \t]/, 1)[0]?.toLowerCase() === "mermaid";
45
+ }
46
+ const GEOMETRY_STYLE = "<style>:where([data-segnavia-overflow]){max-inline-size:100%;overflow-x:auto}:where([data-segnavia-overflow]>table){inline-size:max-content;min-inline-size:100%}@media print{:where([data-segnavia-overflow]){overflow-x:visible}:where([data-segnavia-overflow]>table){inline-size:100%;max-inline-size:100%;min-inline-size:0}:where([data-segnavia-overflow]>pre){white-space:pre-wrap}}</style>";
47
+ const memo = /* @__PURE__ */ new WeakMap();
48
+ function textNode(value) {
49
+ return {
50
+ type: "text",
51
+ value
52
+ };
53
+ }
54
+ function element(tagName, children, properties) {
55
+ return {
56
+ type: "element",
57
+ tagName,
58
+ properties,
59
+ children
60
+ };
61
+ }
62
+ function propertyValue(value, ancestors) {
63
+ if (typeof value === "boolean") return [textNode(value ? "yes" : "no")];
64
+ if (Array.isArray(value)) {
65
+ if (ancestors.has(value)) return [textNode("(cycle)")];
66
+ const next = new Set(ancestors).add(value);
67
+ return value.flatMap((item) => [element("span", propertyValue(item, next))]);
68
+ }
69
+ if (value !== null && typeof value === "object") {
70
+ if (ancestors.has(value)) return [textNode("(cycle)")];
71
+ return [propertyList(value, new Set(ancestors).add(value))];
72
+ }
73
+ return [textNode(value == null ? "" : String(value))];
74
+ }
75
+ function propertyList(properties, ancestors) {
76
+ return element("dl", Object.entries(properties).flatMap(([key, value]) => [element("dt", [textNode(key)]), element("dd", propertyValue(value, ancestors))]));
77
+ }
78
+ function unreadableFrontmatter(source) {
79
+ return [element("p", [textNode("Frontmatter could not be read.")]), element("pre", [element("code", [textNode(`---\n${source}\n---`)])])];
80
+ }
81
+ function frontmatter(source) {
82
+ let value;
83
+ try {
84
+ value = parse(source);
85
+ } catch {
86
+ return unreadableFrontmatter(source);
87
+ }
88
+ if (value == null) return [];
89
+ if (typeof value !== "object" || Array.isArray(value)) return unreadableFrontmatter(source);
90
+ const properties = value;
91
+ return Object.keys(properties).length === 0 ? [] : propertyList(properties, /* @__PURE__ */ new Set([properties])).children ?? [];
92
+ }
93
+ function visibleText(node) {
94
+ if (node.type === "text" || node.type === "inlineCode") return node.value ?? "";
95
+ return (node.children ?? []).map(visibleText).join("");
96
+ }
97
+ function slug(title) {
98
+ return title.trim().toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "") || "section";
99
+ }
100
+ function directiveSource(node, source) {
101
+ const start = node.position?.start.offset;
102
+ const end = node.position && "end" in node.position ? node.position.end?.offset : void 0;
103
+ return start === void 0 || end === void 0 ? `:${node.name ?? ""}` : source.slice(start, end);
104
+ }
105
+ function applyFrontmatter(node) {
106
+ if (node.type !== "yaml" || typeof node.value !== "string") return;
107
+ node.type = "paragraph";
108
+ node.children = [];
109
+ node.data = {
110
+ hName: "dl",
111
+ hProperties: { dataSegnaviaFrontmatter: "" },
112
+ hChildren: frontmatter(node.value)
113
+ };
114
+ }
115
+ function applyHeading(node, seen, outline) {
116
+ if (node.type !== "heading" || !node.depth) return;
117
+ const title = visibleText(node);
118
+ const base = slug(title);
119
+ const count = seen.get(base) ?? 0;
120
+ seen.set(base, count + 1);
121
+ const id = count === 0 ? base : `${base}-${count + 1}`;
122
+ node.data = {
123
+ ...node.data,
124
+ hProperties: {
125
+ ...node.data?.hProperties,
126
+ id
127
+ }
128
+ };
129
+ outline.push({
130
+ id,
131
+ level: node.depth,
132
+ title
133
+ });
134
+ }
135
+ function applyDirective(node) {
136
+ if (node.type !== "containerDirective") return;
137
+ const attributes = Object.fromEntries(Object.entries(node.attributes ?? {}).map(([name, value]) => [`dataSegnaviaDirective${name.charAt(0).toUpperCase()}${name.slice(1).replace(/[^a-zA-Z0-9]+(.)/g, (_, next) => next.toUpperCase())}`, value ?? ""]));
138
+ node.data = {
139
+ hName: "div",
140
+ hProperties: {
141
+ dataSegnaviaDirective: node.name ?? "",
142
+ ...attributes
143
+ }
144
+ };
145
+ }
146
+ function applyAlert(node, labels) {
147
+ if (node.type !== "blockquote") return;
148
+ const first = node.children?.[0]?.children?.find((child) => child.type === "text");
149
+ if (!first) return;
150
+ const marker = alertMarker(first.value ?? "");
151
+ if (!marker) return;
152
+ const { kind } = marker;
153
+ first.value = (first.value ?? "").slice(marker.length);
154
+ node.data = {
155
+ hName: "div",
156
+ hProperties: { dataSegnaviaAlert: kind }
157
+ };
158
+ node.children = [{
159
+ type: "paragraph",
160
+ children: [{
161
+ type: "text",
162
+ value: labels[kind]
163
+ }],
164
+ data: { hName: "bdi" }
165
+ }, ...node.children ?? []];
166
+ }
167
+ function literalDirectives(node, source) {
168
+ if (!node.children) return;
169
+ node.children = node.children.map((child) => child.type === "textDirective" || child.type === "leafDirective" ? {
170
+ type: "text",
171
+ value: directiveSource(child, source)
172
+ } : child);
173
+ }
174
+ function markdownPlugin(outline, labels) {
175
+ return (tree, file) => {
176
+ const source = String(file.value);
177
+ const seen = /* @__PURE__ */ new Map();
178
+ const visit = (node) => {
179
+ applyFrontmatter(node);
180
+ applyHeading(node, seen, outline);
181
+ applyDirective(node);
182
+ applyAlert(node, labels);
183
+ literalDirectives(node, source);
184
+ for (const child of node.children ?? []) visit(child);
185
+ };
186
+ visit(tree);
187
+ };
188
+ }
189
+ function alertLanguage(markdown, options) {
190
+ const primary = options.lang?.trim().toLowerCase().split(/[-_]/)[0];
191
+ if (primary === "en" || primary === "he") return primary;
192
+ const text = markdown.replace(/^>\s*\[![A-Za-z]+\]\s*$/gm, "");
193
+ return resolveDirection(options.dir, options.lang, text) === "rtl" ? "he" : "en";
194
+ }
195
+ function hastPlugin() {
196
+ return (tree) => {
197
+ const wrap = (node) => {
198
+ const copy = {
199
+ ...node,
200
+ children: node.children
201
+ };
202
+ node.tagName = "div";
203
+ node.properties = { dataSegnaviaOverflow: "" };
204
+ node.children = [copy];
205
+ };
206
+ const transform = (node) => {
207
+ if (node.type !== "element") return;
208
+ if (node.tagName === "table") wrap(node);
209
+ if (node.tagName !== "pre") return;
210
+ const code = node.children?.[0];
211
+ if ((Array.isArray(code?.properties?.className) ? code.properties.className.map(String) : []).some((name) => name.startsWith("language-") && isDiagramFence(name.slice(9)))) node.properties = {
212
+ ...node.properties,
213
+ dataSegnaviaDiagram: "mermaid"
214
+ };
215
+ else wrap(node);
216
+ };
217
+ const visit = (node) => {
218
+ for (const child of node.children ?? []) visit(child);
219
+ transform(node);
220
+ };
221
+ visit(tree);
222
+ };
223
+ }
224
+ function convert(markdown, options) {
225
+ const outline = [];
226
+ const language = alertLanguage(markdown, options);
227
+ const labels = {
228
+ ...alertLabels[language],
229
+ ...options.alertLabels
230
+ };
231
+ const result = unified().use(remarkParse).use(remarkGfm).use(remarkFrontmatter).use(remarkDirective).use(markdownPlugin, outline, labels).use(remarkRehype).use(hastPlugin).use(rehypeStringify).processSync(markdown);
232
+ return {
233
+ html: `${GEOMETRY_STYLE}${sanitize(String(result), { converterPolicy: MARKDOWN_CONVERTER_POLICY })}`,
234
+ outline
235
+ };
236
+ }
237
+ function optionKey(options) {
238
+ return JSON.stringify([
239
+ options.dir,
240
+ options.lang,
241
+ options.alertLabels
242
+ ]);
243
+ }
244
+ function markdownResult(body, options) {
245
+ let entries = memo.get(body);
246
+ if (!entries) {
247
+ entries = /* @__PURE__ */ new Map();
248
+ memo.set(body, entries);
249
+ }
250
+ const key = optionKey(options);
251
+ const found = entries.get(key);
252
+ if (found !== void 0) return found;
253
+ const result = convert(body.markdown, options);
254
+ entries.set(key, result);
255
+ return result;
256
+ }
257
+ function outlinedMarkdownResult(body) {
258
+ return memo.get(body)?.values().next().value ?? markdownResult(body, {});
259
+ }
260
+ function bodyHtml(body, options = {}) {
261
+ return "html" in body ? body.html : markdownResult(body, options).html;
262
+ }
263
+ function markdownOutline(markdown) {
264
+ return outlinedMarkdownResult(typeof markdown === "string" ? { markdown } : markdown).outline;
265
+ }
266
+ function resolvePageHtml(page, options = {}) {
267
+ const { body, ...fields } = page;
268
+ return {
269
+ ...fields,
270
+ html: bodyHtml(body, {
271
+ dir: page.dir,
272
+ lang: page.lang,
273
+ alertLabels: options.alertLabels
274
+ })
275
+ };
276
+ }
277
+ //#endregion
278
+ export { alertLabels, bodyHtml, isDiagramFence, isGithubAlertBlock, markdownOutline, resolvePageHtml };
@@ -7,8 +7,25 @@ export interface SanitizeOptions {
7
7
  dependencyOrigins?: Iterable<string>;
8
8
  dataFonts?: boolean;
9
9
  refuseElements?: Iterable<string>;
10
+ converterPolicy?: ConverterPolicy;
11
+ }
12
+ export interface UrlPolicy {
13
+ linkProtocols: readonly string[];
14
+ imageProtocols: readonly string[];
15
+ linkFragments: boolean;
16
+ dataImages: boolean;
17
+ }
18
+ export interface ConverterPolicy {
19
+ urls: UrlPolicy;
20
+ inertControls: readonly {
21
+ element: 'input';
22
+ type: 'checkbox';
23
+ attribute: 'disabled';
24
+ }[];
10
25
  }
11
26
  export declare function safeImageSource(value: string): boolean;
27
+ /** The URL decision used when schema-based authoring admits a link or image. */
28
+ export declare function safeMarkdownUrl(attribute: 'href' | 'src', value: string, linkOrigins?: Iterable<string>): boolean;
12
29
  export declare function findActiveContent(html: string, options?: SanitizeOptions): string[];
13
30
  export declare function sanitize(html: string, options?: SanitizeOptions): string;
14
31
  //#endregion
package/dist/sanitize.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { MARKDOWN_CONVERTER_POLICY } from "./markdown-policy.js";
1
2
  import { sanitizeCss } from "./sanitize-css.js";
2
3
  import { isTag } from "domhandler";
3
4
  import { dependencyDecision, dependencyOriginSet, originSet } from "@segnavia/format";
@@ -83,25 +84,47 @@ function normalizeUrl(value) {
83
84
  function safeImageSource(value) {
84
85
  return DATA_IMAGE.test(normalizeUrl(value));
85
86
  }
86
- function safeUrl(attribute, value, allowed) {
87
+ function safeUrl(attribute, value, allowed, policy) {
87
88
  const url = normalizeUrl(value);
88
89
  if (attribute === "href" || attribute === "xlink:href") {
89
- if (url.startsWith("#")) return true;
90
+ if (url.startsWith("#")) return policy?.linkFragments ?? true;
90
91
  try {
91
92
  const parsed = new URL(url);
93
+ if (policy) return policy.linkProtocols.includes(parsed.protocol);
92
94
  return parsed.protocol === "https:" && allowed.has(parsed.origin);
93
95
  } catch {
94
96
  return false;
95
97
  }
96
98
  }
97
99
  if (attribute === "srcset") return DATA_IMAGE_SRCSET.test(url);
98
- if (attribute === "src" || attribute === "poster") return safeImageSource(value);
100
+ if (attribute === "src" || attribute === "poster") {
101
+ if (policy?.dataImages && safeImageSource(value)) return true;
102
+ if (!policy) return safeImageSource(value);
103
+ try {
104
+ return policy.imageProtocols.includes(new URL(url).protocol);
105
+ } catch {
106
+ return false;
107
+ }
108
+ }
99
109
  return false;
100
110
  }
101
- function refusedElement(element, refused, keepScripts) {
111
+ /** The URL decision used when schema-based authoring admits a link or image. */
112
+ function safeMarkdownUrl(attribute, value, linkOrigins) {
113
+ if (!safeUrl(attribute, value, /* @__PURE__ */ new Set(), MARKDOWN_CONVERTER_POLICY.urls)) return false;
114
+ if (attribute === "src" || linkOrigins === void 0 || value.startsWith("#")) return true;
115
+ let parsed;
116
+ try {
117
+ parsed = new URL(value);
118
+ } catch {
119
+ return true;
120
+ }
121
+ return parsed.protocol !== "http:" && parsed.protocol !== "https:" || originSet(linkOrigins).has(parsed.origin.toLowerCase());
122
+ }
123
+ function refusedElement(element, refused, options, keepScripts) {
102
124
  const name = element.name.toLowerCase();
103
125
  if (name === "script") return !keepScripts;
104
126
  if (name === "link" && keepScripts && element.attribs.rel?.toLowerCase() === "stylesheet") return false;
127
+ if (options.converterPolicy?.inertControls.some((control) => control.element === name && element.attribs.type?.toLowerCase() === control.type && control.attribute in element.attribs)) return false;
105
128
  if (refused.has(name)) return true;
106
129
  if (insideSvg(element) && REFUSED_SVG_ELEMENTS.has(name)) return true;
107
130
  return name === "meta" && Object.keys(element.attribs).some((attribute) => attribute.toLowerCase() === "http-equiv");
@@ -118,7 +141,7 @@ function refusedAttribute(element, name, value, allowed, options, dependencies)
118
141
  if (attribute.startsWith("on")) return options.keepScripts ? void 0 : "";
119
142
  const dependency = dependencyAttribute(element, attribute, value, options, dependencies);
120
143
  if (dependency !== void 0) return dependency || void 0;
121
- return REFUSED_ATTRIBUTES.has(attribute) || URL_ATTRIBUTES.has(attribute) && !safeUrl(attribute, value, allowed) ? "" : void 0;
144
+ return REFUSED_ATTRIBUTES.has(attribute) || URL_ATTRIBUTES.has(attribute) && !safeUrl(attribute, value, allowed, options.converterPolicy?.urls) ? "" : void 0;
122
145
  }
123
146
  function refusedSet(options) {
124
147
  const defaults = options.keepScripts ? Object.entries(REFUSED).filter(([, rule]) => rule === "always").map(([name]) => name) : REFUSED_ELEMENTS;
@@ -168,7 +191,7 @@ function findActiveContent(html, options = {}) {
168
191
  const keepScripts = options.keepScripts ?? false;
169
192
  const found = [];
170
193
  for (const element of elements(parse(html).children)) {
171
- if (refusedElement(element, refused, keepScripts)) found.push(`<${element.name}>`);
194
+ if (refusedElement(element, refused, options, keepScripts)) found.push(`<${element.name}>`);
172
195
  found.push(...styleFindings(element, options, dependencies));
173
196
  for (const [name, value] of Object.entries(element.attribs)) {
174
197
  const reason = refusedAttribute(element, name, value, allowed, options, dependencies);
@@ -201,7 +224,7 @@ function sanitize(html, options = {}) {
201
224
  const document = parse(html);
202
225
  const prune = (nodes) => nodes.filter((node) => {
203
226
  if (!isTag(node)) return true;
204
- if (refusedElement(node, refused, keepScripts)) return false;
227
+ if (refusedElement(node, refused, options, keepScripts)) return false;
205
228
  cleanStyleElement(node, options, dependencies);
206
229
  cleanAttributes(node, allowed, options, dependencies);
207
230
  node.children = prune(node.children);
@@ -211,4 +234,4 @@ function sanitize(html, options = {}) {
211
234
  return serialize(document);
212
235
  }
213
236
  //#endregion
214
- export { REFUSED_ELEMENTS, elements, findActiveContent, parse, safeImageSource, sanitize, serialize };
237
+ export { REFUSED_ELEMENTS, elements, findActiveContent, parse, safeImageSource, safeMarkdownUrl, sanitize, serialize };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@segnavia/html",
3
- "version": "0.39.0",
3
+ "version": "0.40.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -26,10 +26,18 @@
26
26
  }
27
27
  },
28
28
  "dependencies": {
29
- "@segnavia/format": "0.39.0",
29
+ "@segnavia/format": "0.40.0",
30
30
  "dom-serializer": "^2.0.0",
31
31
  "domhandler": "^5.0.3",
32
32
  "htmlparser2": "^10.0.0",
33
- "qrcode-generator": "2.0.4"
33
+ "qrcode-generator": "2.0.4",
34
+ "rehype-stringify": "10.0.1",
35
+ "remark-directive": "4.0.0",
36
+ "remark-frontmatter": "5.0.0",
37
+ "remark-gfm": "4.0.1",
38
+ "remark-parse": "11.0.0",
39
+ "remark-rehype": "11.1.2",
40
+ "unified": "11.0.5",
41
+ "yaml": "2.9.1"
34
42
  }
35
43
  }