@wdprlib/render 2.1.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/dist/index.cjs +2344 -1668
  2. package/dist/index.d.cts +15 -13
  3. package/dist/index.d.ts +15 -13
  4. package/dist/index.js +2375 -1699
  5. package/package.json +1 -1
  6. package/src/context/attributes.ts +14 -0
  7. package/src/context/bibliography.ts +109 -0
  8. package/src/context/counters.ts +51 -0
  9. package/src/context/image-urls.ts +31 -0
  10. package/src/context/index.ts +285 -0
  11. package/src/context/output.ts +17 -0
  12. package/src/context/page-urls.ts +81 -0
  13. package/src/context/style-slots.ts +29 -0
  14. package/src/context/urls.ts +2 -0
  15. package/src/elements/bibliography/block.ts +27 -0
  16. package/src/elements/bibliography/cite.ts +23 -0
  17. package/src/elements/bibliography/ids.ts +9 -0
  18. package/src/elements/bibliography/index.ts +9 -0
  19. package/src/elements/code/contents.ts +18 -0
  20. package/src/elements/code/index.ts +29 -0
  21. package/src/elements/collapsible/index.ts +31 -0
  22. package/src/elements/collapsible/labels.ts +35 -0
  23. package/src/elements/collapsible/link.ts +11 -0
  24. package/src/elements/collapsible/sections.ts +39 -0
  25. package/src/elements/container/attributes.ts +28 -0
  26. package/src/elements/container/header.ts +27 -0
  27. package/src/elements/container/index.ts +35 -0
  28. package/src/elements/container/string-container.ts +40 -0
  29. package/src/elements/container/string-types.ts +63 -0
  30. package/src/elements/container/wrappers.ts +32 -0
  31. package/src/elements/date/format.ts +20 -0
  32. package/src/elements/{date.ts → date/index.ts} +4 -29
  33. package/src/elements/date/output.ts +6 -0
  34. package/src/elements/embed/iframe.ts +8 -0
  35. package/src/elements/embed/index.ts +28 -0
  36. package/src/elements/embed/providers.ts +43 -0
  37. package/src/elements/embed/validation.ts +15 -0
  38. package/src/elements/embed-block/allowlist.ts +60 -0
  39. package/src/elements/embed-block/boolean-attributes.ts +38 -0
  40. package/src/elements/embed-block/iframe.ts +33 -0
  41. package/src/elements/embed-block/index.ts +31 -0
  42. package/src/elements/embed-block/sanitize-config.ts +22 -0
  43. package/src/elements/embed-block/sanitize.ts +44 -0
  44. package/src/elements/expr/branch.ts +29 -0
  45. package/src/elements/expr/index.ts +63 -0
  46. package/src/elements/expr/result.ts +19 -0
  47. package/src/elements/footnote/body.ts +11 -0
  48. package/src/elements/footnote/index.ts +35 -0
  49. package/src/elements/footnote/ref.ts +16 -0
  50. package/src/elements/html/attributes.ts +24 -0
  51. package/src/elements/html/index.ts +39 -0
  52. package/src/elements/html/url.ts +19 -0
  53. package/src/elements/iframe/attributes.ts +28 -0
  54. package/src/elements/iframe/index.ts +22 -0
  55. package/src/elements/iftags/condition.ts +42 -0
  56. package/src/elements/iftags/index.ts +39 -0
  57. package/src/elements/iftags/style-slot.ts +23 -0
  58. package/src/elements/iftags/tokens.ts +36 -0
  59. package/src/elements/image/alignment.ts +44 -0
  60. package/src/elements/image/attributes.ts +10 -0
  61. package/src/elements/image/img-attributes.ts +26 -0
  62. package/src/elements/image/index.ts +36 -0
  63. package/src/elements/image/link-href.ts +24 -0
  64. package/src/elements/image/link.ts +13 -0
  65. package/src/elements/image/source.ts +16 -0
  66. package/src/elements/{include.ts → include/index.ts} +5 -13
  67. package/src/elements/include/missing.ts +15 -0
  68. package/src/elements/link/anchor-name.ts +6 -0
  69. package/src/elements/link/anchor.ts +27 -0
  70. package/src/elements/link/attributes.ts +47 -0
  71. package/src/elements/link/index.ts +26 -0
  72. package/src/elements/link/label.ts +23 -0
  73. package/src/elements/link/target.ts +20 -0
  74. package/src/elements/list/attributes.ts +19 -0
  75. package/src/elements/list/definition-list.ts +16 -0
  76. package/src/elements/list/index.ts +48 -0
  77. package/src/elements/list/item-rendering.ts +38 -0
  78. package/src/elements/list/items.ts +61 -0
  79. package/src/elements/list/no-marker.ts +53 -0
  80. package/src/elements/list/paragraphs.ts +34 -0
  81. package/src/elements/list/trim.ts +38 -0
  82. package/src/elements/math/block.ts +29 -0
  83. package/src/elements/math/equation-ref.ts +12 -0
  84. package/src/elements/math/index.ts +14 -0
  85. package/src/elements/math/inline.ts +19 -0
  86. package/src/elements/math/latex.ts +27 -0
  87. package/src/elements/math/source.ts +18 -0
  88. package/src/elements/module/backlinks.ts +2 -1
  89. package/src/elements/module/categories.ts +2 -2
  90. package/src/elements/module/empty-container.ts +10 -0
  91. package/src/elements/module/index.ts +2 -4
  92. package/src/elements/module/join-markup.ts +10 -0
  93. package/src/elements/module/join.ts +2 -7
  94. package/src/elements/module/listpages.ts +2 -2
  95. package/src/elements/module/listusers.ts +2 -2
  96. package/src/elements/module/page-tree.ts +2 -2
  97. package/src/elements/module/rate-markup.ts +10 -0
  98. package/src/elements/module/rate.ts +4 -13
  99. package/src/elements/module/unknown.ts +11 -0
  100. package/src/elements/tab-view/ids.ts +16 -0
  101. package/src/elements/tab-view/index.ts +31 -0
  102. package/src/elements/tab-view/navigation.ts +15 -0
  103. package/src/elements/tab-view/panels.ts +16 -0
  104. package/src/elements/table/attributes.ts +23 -0
  105. package/src/elements/table/cell-attributes.ts +62 -0
  106. package/src/elements/table/cell.ts +13 -0
  107. package/src/elements/table/index.ts +27 -0
  108. package/src/elements/text/email.ts +20 -0
  109. package/src/elements/text/index.ts +11 -0
  110. package/src/elements/text/plain.ts +11 -0
  111. package/src/elements/text/raw.ts +20 -0
  112. package/src/elements/toc/body.ts +12 -0
  113. package/src/elements/toc/entries.ts +34 -0
  114. package/src/elements/toc/frame.ts +27 -0
  115. package/src/elements/toc/index.ts +17 -0
  116. package/src/elements/toc/link.ts +26 -0
  117. package/src/elements/user/index.ts +40 -0
  118. package/src/elements/user/markup.ts +34 -0
  119. package/src/elements/user/resolve.ts +6 -0
  120. package/src/escape/attribute-allowlists.ts +101 -0
  121. package/src/escape/attributes.ts +62 -0
  122. package/src/escape/css-color-functions.ts +18 -0
  123. package/src/escape/css-colors.ts +183 -0
  124. package/src/escape/css-danger.ts +22 -0
  125. package/src/escape/css-normalize.ts +54 -0
  126. package/src/escape/css-style.ts +78 -0
  127. package/src/escape/css-urls.ts +76 -0
  128. package/src/escape/css.ts +4 -0
  129. package/src/escape/email.ts +22 -0
  130. package/src/escape/html.ts +68 -0
  131. package/src/escape/index.ts +15 -0
  132. package/src/escape/url.ts +18 -0
  133. package/src/libs/highlighter/engine/end-pattern.ts +26 -0
  134. package/src/libs/highlighter/engine/html.ts +19 -0
  135. package/src/libs/highlighter/engine/index.ts +3 -0
  136. package/src/libs/highlighter/engine/keywords.ts +22 -0
  137. package/src/libs/highlighter/engine/parts.ts +36 -0
  138. package/src/libs/highlighter/engine/preprocess.ts +10 -0
  139. package/src/libs/highlighter/engine/render.ts +31 -0
  140. package/src/libs/highlighter/engine/token.ts +7 -0
  141. package/src/libs/highlighter/engine/tokenizer.ts +266 -0
  142. package/src/libs/highlighter/engine/utils.ts +38 -0
  143. package/src/render/collected-styles.ts +22 -0
  144. package/src/render/dispatch.ts +181 -0
  145. package/src/render/index.ts +28 -0
  146. package/src/render/primitives.ts +17 -0
  147. package/src/render/style-tag.ts +6 -0
  148. package/src/render/style.ts +15 -0
  149. package/src/types.ts +6 -2
  150. package/src/context.ts +0 -422
  151. package/src/elements/bibliography.ts +0 -123
  152. package/src/elements/code.ts +0 -49
  153. package/src/elements/collapsible.ts +0 -105
  154. package/src/elements/container.ts +0 -302
  155. package/src/elements/embed-block.ts +0 -327
  156. package/src/elements/embed.ts +0 -166
  157. package/src/elements/expr.ts +0 -102
  158. package/src/elements/footnote.ts +0 -76
  159. package/src/elements/html.ts +0 -79
  160. package/src/elements/iframe.ts +0 -44
  161. package/src/elements/iftags.ts +0 -118
  162. package/src/elements/image.ts +0 -154
  163. package/src/elements/link.ts +0 -201
  164. package/src/elements/list.ts +0 -241
  165. package/src/elements/math.ts +0 -177
  166. package/src/elements/tab-view.ts +0 -75
  167. package/src/elements/table.ts +0 -101
  168. package/src/elements/text.ts +0 -57
  169. package/src/elements/toc.ts +0 -147
  170. package/src/elements/user.ts +0 -79
  171. package/src/escape.ts +0 -829
  172. package/src/libs/highlighter/engine.ts +0 -352
  173. package/src/render.ts +0 -231
@@ -0,0 +1,34 @@
1
+ import type { Element, ListData, ListItem } from "@wdprlib/ast";
2
+ import type { RenderContext } from "../../context";
3
+ import { escapeAttr, escapeHtml } from "../../escape";
4
+ import { extractTocLink, rewriteTocAnchor } from "./link";
5
+
6
+ export function renderTocEntries(ctx: RenderContext, elements: Element[]): void {
7
+ for (const element of elements) {
8
+ if (element.element === "list") {
9
+ renderTocList(ctx, element.data, 1);
10
+ }
11
+ }
12
+ }
13
+
14
+ function renderTocList(ctx: RenderContext, listData: ListData, depth: number): void {
15
+ for (const item of listData.items) {
16
+ renderTocItem(ctx, item, depth);
17
+ }
18
+ }
19
+
20
+ function renderTocItem(ctx: RenderContext, item: ListItem, depth: number): void {
21
+ if (item["item-type"] === "elements") {
22
+ for (const el of item.elements) {
23
+ const link = extractTocLink(el);
24
+ if (link) {
25
+ const href = rewriteTocAnchor(ctx, link.href);
26
+ ctx.push(
27
+ `<div style="margin-left: ${depth}em;"><a href="${escapeAttr(href)}">${escapeHtml(link.text)}</a></div>`,
28
+ );
29
+ }
30
+ }
31
+ } else if (item["item-type"] === "sub-list") {
32
+ renderTocList(ctx, item.data, depth + 1);
33
+ }
34
+ }
@@ -0,0 +1,27 @@
1
+ import type { Alignment } from "@wdprlib/ast";
2
+ import type { RenderContext } from "../../context";
3
+
4
+ export function isFloatingToc(align: Alignment | null): boolean {
5
+ return align === "left" || align === "right";
6
+ }
7
+
8
+ export function openTocFrame(ctx: RenderContext, align: Alignment | null): void {
9
+ if (!isFloatingToc(align)) {
10
+ ctx.push(`<table style="margin:0; padding:0"><tr><td style="margin:0; padding:0">`);
11
+ }
12
+
13
+ if (isFloatingToc(align)) {
14
+ const floatClass = align === "left" ? "floatleft" : "floatright";
15
+ ctx.push(`<div id="toc" class="${floatClass}">`);
16
+ } else {
17
+ ctx.push(`<div id="toc">`);
18
+ }
19
+ }
20
+
21
+ export function closeTocFrame(ctx: RenderContext, align: Alignment | null): void {
22
+ ctx.push("</div>");
23
+
24
+ if (!isFloatingToc(align)) {
25
+ ctx.push(`</td></tr></table>`);
26
+ }
27
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ *
3
+ * Renderer for `[[toc]]` (Table of Contents) elements.
4
+ *
5
+ * @module
6
+ */
7
+
8
+ import type { TableOfContentsData } from "@wdprlib/ast";
9
+ import type { RenderContext } from "../../context";
10
+ import { renderTocBody } from "./body";
11
+ import { closeTocFrame, openTocFrame } from "./frame";
12
+
13
+ export function renderTableOfContents(ctx: RenderContext, data: TableOfContentsData): void {
14
+ openTocFrame(ctx, data.align);
15
+ renderTocBody(ctx);
16
+ closeTocFrame(ctx, data.align);
17
+ }
@@ -0,0 +1,26 @@
1
+ import type { Element } from "@wdprlib/ast";
2
+ import type { RenderContext } from "../../context";
3
+
4
+ export interface TocLink {
5
+ href: string;
6
+ text: string;
7
+ }
8
+
9
+ export function extractTocLink(element: Element): TocLink | null {
10
+ if (element.element !== "link") return null;
11
+
12
+ const label = element.data.label;
13
+ let text = "";
14
+ if (typeof label === "object" && label !== null && "text" in label) {
15
+ text = label.text;
16
+ }
17
+
18
+ const href = typeof element.data.link === "string" ? element.data.link : "";
19
+ return { href, text };
20
+ }
21
+
22
+ export function rewriteTocAnchor(ctx: RenderContext, href: string): string {
23
+ const match = /^#toc(\d+)$/.exec(href);
24
+ if (!match) return href;
25
+ return `#${ctx.generateId("toc", Number(match[1]))}`;
26
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ *
3
+ * Renderer for `[[user username]]` elements.
4
+ *
5
+ * @module
6
+ */
7
+
8
+ import type { UserData } from "@wdprlib/ast";
9
+ import type { RenderContext } from "../../context";
10
+ import { escapeHtml } from "../../escape";
11
+ import { getResolvedUser } from "./resolve";
12
+ import { renderAvatarUser, renderLinkedUser } from "./markup";
13
+
14
+ /**
15
+ * Render a `[[user username]]` element.
16
+ *
17
+ * @param ctx - The current render context.
18
+ * @param data - User element data with username and show-avatar flag.
19
+ */
20
+ export function renderUser(ctx: RenderContext, data: UserData): void {
21
+ const normalized = data.name.toLowerCase().trim();
22
+
23
+ if (normalized === "anonymous") {
24
+ ctx.push("Anonymous");
25
+ return;
26
+ }
27
+
28
+ const resolved = getResolvedUser(ctx, data.name);
29
+ if (resolved === null) {
30
+ ctx.push(escapeHtml(data.name));
31
+ return;
32
+ }
33
+
34
+ const showAvatar = data["show-avatar"] && resolved.url && resolved.avatarUrl;
35
+ if (showAvatar) {
36
+ renderAvatarUser(ctx, data.name, resolved);
37
+ } else {
38
+ renderLinkedUser(ctx, data.name, resolved);
39
+ }
40
+ }
@@ -0,0 +1,34 @@
1
+ import type { RenderContext } from "../../context";
2
+ import type { ResolvedUser } from "../../types";
3
+ import { escapeAttr, escapeHtml } from "../../escape";
4
+
5
+ export function renderLinkedUser(ctx: RenderContext, username: string, user: ResolvedUser): void {
6
+ const displayName = user.name ?? username;
7
+ const hrefAttr = user.url ? ` href="${escapeAttr(user.url)}"` : "";
8
+
9
+ ctx.push(`<span class="printuser">`);
10
+ ctx.push(`<a${hrefAttr}>`);
11
+ ctx.push(escapeHtml(displayName));
12
+ ctx.push("</a>");
13
+ ctx.push("</span>");
14
+ }
15
+
16
+ export function renderAvatarUser(ctx: RenderContext, username: string, user: ResolvedUser): void {
17
+ const displayName = user.name ?? username;
18
+ const hrefAttr = user.url ? ` href="${escapeAttr(user.url)}"` : "";
19
+ const avatarUrl = user.avatarUrl ?? "";
20
+ const styleAttr = user.karmaUrl
21
+ ? ` style="background-image:url(${escapeAttr(user.karmaUrl)})"`
22
+ : "";
23
+
24
+ ctx.push(`<span class="printuser avatarhover">`);
25
+ ctx.push(`<a${hrefAttr}>`);
26
+ ctx.push(
27
+ `<img class="small" src="${escapeAttr(avatarUrl)}" alt="${escapeAttr(displayName)}"${styleAttr} />`,
28
+ );
29
+ ctx.push("</a>");
30
+ ctx.push(`<a${hrefAttr}>`);
31
+ ctx.push(escapeHtml(displayName));
32
+ ctx.push("</a>");
33
+ ctx.push("</span>");
34
+ }
@@ -0,0 +1,6 @@
1
+ import type { RenderContext } from "../../context";
2
+ import type { ResolvedUser } from "../../types";
3
+
4
+ export function getResolvedUser(ctx: RenderContext, username: string): ResolvedUser | null {
5
+ return ctx.options.resolvers?.user?.(username) ?? null;
6
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * HTML attribute names considered safe for rendering.
3
+ */
4
+ export const SAFE_ATTRIBUTES: ReadonlySet<string> = new Set([
5
+ "accept",
6
+ "align",
7
+ "alt",
8
+ "autocapitalize",
9
+ "autoplay",
10
+ "background",
11
+ "bgcolor",
12
+ "border",
13
+ "buffered",
14
+ "checked",
15
+ "cite",
16
+ "class",
17
+ "cols",
18
+ "colspan",
19
+ "contenteditable",
20
+ "controls",
21
+ "coords",
22
+ "datetime",
23
+ "decoding",
24
+ "default",
25
+ "dir",
26
+ "dirname",
27
+ "disabled",
28
+ "download",
29
+ "draggable",
30
+ "for",
31
+ "form",
32
+ "headers",
33
+ "height",
34
+ "hidden",
35
+ "high",
36
+ "href",
37
+ "hreflang",
38
+ "id",
39
+ "inputmode",
40
+ "ismap",
41
+ "itemprop",
42
+ "kind",
43
+ "label",
44
+ "lang",
45
+ "list",
46
+ "loop",
47
+ "low",
48
+ "max",
49
+ "maxlength",
50
+ "min",
51
+ "minlength",
52
+ "multiple",
53
+ "muted",
54
+ "name",
55
+ "optimum",
56
+ "pattern",
57
+ "placeholder",
58
+ "poster",
59
+ "preload",
60
+ "readonly",
61
+ "required",
62
+ "reversed",
63
+ "role",
64
+ "rows",
65
+ "rowspan",
66
+ "scope",
67
+ "selected",
68
+ "shape",
69
+ "size",
70
+ "sizes",
71
+ "span",
72
+ "spellcheck",
73
+ "src",
74
+ "srclang",
75
+ "srcset",
76
+ "start",
77
+ "step",
78
+ "style",
79
+ "tabindex",
80
+ "target",
81
+ "title",
82
+ "translate",
83
+ "type",
84
+ "usemap",
85
+ "value",
86
+ "width",
87
+ "wrap",
88
+ ]);
89
+
90
+ /**
91
+ * Attribute names whose values are interpreted as URLs by browsers.
92
+ */
93
+ export const URL_ATTRIBUTES: ReadonlySet<string> = new Set([
94
+ "href",
95
+ "src",
96
+ "action",
97
+ "formaction",
98
+ "srcset",
99
+ "poster",
100
+ "background",
101
+ ]);
@@ -0,0 +1,62 @@
1
+ import { sanitizeStyleValue } from "./css";
2
+ import { SAFE_ATTRIBUTES, URL_ATTRIBUTES } from "./attribute-allowlists";
3
+ import { isDangerousUrl } from "./url";
4
+
5
+ /**
6
+ * Check whether an HTML attribute name is safe to include in rendered output.
7
+ *
8
+ * The check applies three rules in order:
9
+ * 1. Block all event handlers (`on*` prefix) unconditionally
10
+ * 2. Allow accessibility (`aria-*`) and custom data (`data-*`) attributes
11
+ * 3. Allow only attributes in the `SAFE_ATTRIBUTES` allowlist
12
+ *
13
+ * @param name - The attribute name to validate (case-insensitive).
14
+ * @returns `true` if the attribute is safe to render.
15
+ */
16
+ export function isSafeAttribute(name: string): boolean {
17
+ const lower = name.toLowerCase();
18
+ return isSafeAttributeLower(lower);
19
+ }
20
+
21
+ export function isSafeAttributeLower(lower: string): boolean {
22
+ // Block all event handlers
23
+ if (lower.startsWith("on")) return false;
24
+ // Allow aria-* and data-* prefixes
25
+ if (lower.startsWith("aria-") || lower.startsWith("data-")) return true;
26
+ return SAFE_ATTRIBUTES.has(lower);
27
+ }
28
+
29
+ /**
30
+ * Sanitize a map of HTML attributes, returning a new map containing
31
+ * only entries that pass all safety checks.
32
+ *
33
+ * For each attribute, this function:
34
+ * 1. Drops attributes that fail {@link isSafeAttribute} (event handlers, unknown names)
35
+ * 2. Drops URL-bearing attributes whose values fail {@link isDangerousUrl}
36
+ * 3. Sanitizes `style` values via {@link sanitizeStyleValue}, dropping them entirely
37
+ * if the result is empty
38
+ * 4. Passes all other safe attributes through unchanged
39
+ *
40
+ * @param attributes - The raw attribute name-value map to sanitize.
41
+ * @returns A new map containing only the safe attributes and their (possibly sanitized) values.
42
+ */
43
+ export function sanitizeAttributes(attributes: Record<string, string>): Record<string, string> {
44
+ const result: Record<string, string> = {};
45
+ for (const key in attributes) {
46
+ const value = attributes[key]!;
47
+ const lower = key.toLowerCase();
48
+ if (!isSafeAttributeLower(lower)) continue;
49
+ // Check URL attributes for dangerous schemes
50
+ if (URL_ATTRIBUTES.has(lower) && isDangerousUrl(value)) continue;
51
+ // Sanitize style attribute
52
+ if (lower === "style") {
53
+ const sanitized = sanitizeStyleValue(value);
54
+ if (sanitized) {
55
+ result[key] = sanitized;
56
+ }
57
+ continue;
58
+ }
59
+ result[key] = value;
60
+ }
61
+ return result;
62
+ }
@@ -0,0 +1,18 @@
1
+ export function isValidCssColorFunction(color: string): boolean {
2
+ const fnMatch = color.match(/^(rgba?|hsla?)\(([^)]*)\)$/);
3
+ if (!fnMatch) {
4
+ return false;
5
+ }
6
+
7
+ const fn = fnMatch[1]!;
8
+ const args = fnMatch[2]!
9
+ .split(",")
10
+ .map((s) => s.trim())
11
+ .join(",");
12
+
13
+ if (fn.startsWith("rgb")) {
14
+ return /^\d{1,3},\d{1,3},\d{1,3}(,(0|1|0?\.\d+))?$/.test(args);
15
+ }
16
+
17
+ return /^\d{1,3},\d{1,3}%,\d{1,3}%(,(0|1|0?\.\d+))?$/.test(args);
18
+ }
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Complete set of CSS Level 4 named colors plus CSS-wide keywords
3
+ * (`transparent`, `currentcolor`, `inherit`, `initial`, `unset`).
4
+ */
5
+ import { isValidCssColorFunction } from "./css-color-functions";
6
+
7
+ const CSS_NAMED_COLORS = new Set([
8
+ "aliceblue",
9
+ "antiquewhite",
10
+ "aqua",
11
+ "aquamarine",
12
+ "azure",
13
+ "beige",
14
+ "bisque",
15
+ "black",
16
+ "blanchedalmond",
17
+ "blue",
18
+ "blueviolet",
19
+ "brown",
20
+ "burlywood",
21
+ "cadetblue",
22
+ "chartreuse",
23
+ "chocolate",
24
+ "coral",
25
+ "cornflowerblue",
26
+ "cornsilk",
27
+ "crimson",
28
+ "cyan",
29
+ "darkblue",
30
+ "darkcyan",
31
+ "darkgoldenrod",
32
+ "darkgray",
33
+ "darkgreen",
34
+ "darkgrey",
35
+ "darkkhaki",
36
+ "darkmagenta",
37
+ "darkolivegreen",
38
+ "darkorange",
39
+ "darkorchid",
40
+ "darkred",
41
+ "darksalmon",
42
+ "darkseagreen",
43
+ "darkslateblue",
44
+ "darkslategray",
45
+ "darkslategrey",
46
+ "darkturquoise",
47
+ "darkviolet",
48
+ "deeppink",
49
+ "deepskyblue",
50
+ "dimgray",
51
+ "dimgrey",
52
+ "dodgerblue",
53
+ "firebrick",
54
+ "floralwhite",
55
+ "forestgreen",
56
+ "fuchsia",
57
+ "gainsboro",
58
+ "ghostwhite",
59
+ "gold",
60
+ "goldenrod",
61
+ "gray",
62
+ "green",
63
+ "greenyellow",
64
+ "grey",
65
+ "honeydew",
66
+ "hotpink",
67
+ "indianred",
68
+ "indigo",
69
+ "ivory",
70
+ "khaki",
71
+ "lavender",
72
+ "lavenderblush",
73
+ "lawngreen",
74
+ "lemonchiffon",
75
+ "lightblue",
76
+ "lightcoral",
77
+ "lightcyan",
78
+ "lightgoldenrodyellow",
79
+ "lightgray",
80
+ "lightgreen",
81
+ "lightgrey",
82
+ "lightpink",
83
+ "lightsalmon",
84
+ "lightseagreen",
85
+ "lightskyblue",
86
+ "lightslategray",
87
+ "lightslategrey",
88
+ "lightsteelblue",
89
+ "lightyellow",
90
+ "lime",
91
+ "limegreen",
92
+ "linen",
93
+ "magenta",
94
+ "maroon",
95
+ "mediumaquamarine",
96
+ "mediumblue",
97
+ "mediumorchid",
98
+ "mediumpurple",
99
+ "mediumseagreen",
100
+ "mediumslateblue",
101
+ "mediumspringgreen",
102
+ "mediumturquoise",
103
+ "mediumvioletred",
104
+ "midnightblue",
105
+ "mintcream",
106
+ "mistyrose",
107
+ "moccasin",
108
+ "navajowhite",
109
+ "navy",
110
+ "oldlace",
111
+ "olive",
112
+ "olivedrab",
113
+ "orange",
114
+ "orangered",
115
+ "orchid",
116
+ "palegoldenrod",
117
+ "palegreen",
118
+ "paleturquoise",
119
+ "palevioletred",
120
+ "papayawhip",
121
+ "peachpuff",
122
+ "peru",
123
+ "pink",
124
+ "plum",
125
+ "powderblue",
126
+ "purple",
127
+ "rebeccapurple",
128
+ "red",
129
+ "rosybrown",
130
+ "royalblue",
131
+ "saddlebrown",
132
+ "salmon",
133
+ "sandybrown",
134
+ "seagreen",
135
+ "seashell",
136
+ "sienna",
137
+ "silver",
138
+ "skyblue",
139
+ "slateblue",
140
+ "slategray",
141
+ "slategrey",
142
+ "snow",
143
+ "springgreen",
144
+ "steelblue",
145
+ "tan",
146
+ "teal",
147
+ "thistle",
148
+ "tomato",
149
+ "turquoise",
150
+ "violet",
151
+ "wheat",
152
+ "white",
153
+ "whitesmoke",
154
+ "yellow",
155
+ "yellowgreen",
156
+ "transparent",
157
+ "currentcolor",
158
+ "inherit",
159
+ "initial",
160
+ "unset",
161
+ ]);
162
+
163
+ /**
164
+ * Validate that a string is a safe CSS color value.
165
+ */
166
+ export function isValidCssColor(color: string): boolean {
167
+ const trimmed = color.trim().toLowerCase();
168
+
169
+ if (!trimmed) return false;
170
+ if (CSS_NAMED_COLORS.has(trimmed)) return true;
171
+
172
+ if (/^#[0-9a-f]{3}([0-9a-f])?$/.test(trimmed) || /^#[0-9a-f]{6}([0-9a-f]{2})?$/.test(trimmed)) {
173
+ return true;
174
+ }
175
+
176
+ if (isValidCssColorFunction(trimmed)) return true;
177
+
178
+ return false;
179
+ }
180
+
181
+ export function sanitizeCssColor(color: string, fallback = "inherit"): string {
182
+ return isValidCssColor(color) ? color : fallback;
183
+ }
@@ -0,0 +1,22 @@
1
+ import { normalizeCssValue } from "./css-normalize";
2
+ import { isCssUrlAllowed, iterateCssUrls } from "./css-urls";
3
+
4
+ /**
5
+ * Check whether a CSS property value contains dangerous patterns that
6
+ * could enable script execution or disallowed external resource loading.
7
+ */
8
+ export function isDangerousCssValue(value: string): boolean {
9
+ const normalized = normalizeCssValue(value);
10
+
11
+ for (const { inner, malformed } of iterateCssUrls(normalized)) {
12
+ if (malformed) return true;
13
+ if (!isCssUrlAllowed(inner)) return true;
14
+ }
15
+
16
+ if (normalized.includes("expression(")) return true;
17
+ if (normalized.includes("-moz-binding")) return true;
18
+ if (normalized.includes("behavior:")) return true;
19
+ if (normalized.includes("@import")) return true;
20
+
21
+ return false;
22
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Normalize a CSS value by resolving escape sequences, removing comments,
3
+ * stripping whitespace and control characters, and lowercasing.
4
+ */
5
+ export function normalizeCssValue(value: string): string {
6
+ let result = value;
7
+
8
+ result = stripCssComments(result);
9
+ result = result.replace(/\\(?:\r\n|[\n\r\f])/g, "");
10
+ result = result.replace(/\\([0-9a-f]{1,6})\s?/gi, (_, hex) => {
11
+ const code = Number.parseInt(hex, 16);
12
+ return code > 0 && code <= 0x10ffff ? String.fromCodePoint(code) : "";
13
+ });
14
+ result = result.replace(/\\(.)/g, "$1");
15
+ result = stripControlAndWhitespace(result);
16
+
17
+ return result.toLowerCase();
18
+ }
19
+
20
+ const WHITESPACE = /\s/;
21
+
22
+ function stripCssComments(value: string): string {
23
+ let result = "";
24
+ let cursor = 0;
25
+
26
+ while (cursor < value.length) {
27
+ const start = value.indexOf("/*", cursor);
28
+ if (start === -1) {
29
+ result += value.slice(cursor);
30
+ break;
31
+ }
32
+
33
+ result += value.slice(cursor, start);
34
+ const end = value.indexOf("*/", start + 2);
35
+ if (end === -1) {
36
+ break;
37
+ }
38
+ cursor = end + 2;
39
+ }
40
+
41
+ return result;
42
+ }
43
+
44
+ function stripControlAndWhitespace(value: string): string {
45
+ let result = "";
46
+ for (const char of value) {
47
+ const code = char.charCodeAt(0);
48
+ if (WHITESPACE.test(char) || code <= 0x1f || (code >= 0x7f && code <= 0x9f)) {
49
+ continue;
50
+ }
51
+ result += char;
52
+ }
53
+ return result;
54
+ }