vadivam-preact 0.0.42 → 0.0.44

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/README.md CHANGED
@@ -8,7 +8,7 @@ Tree-shakeable 24px outline SVG icons for Preact.
8
8
  [![downloads](https://img.shields.io/npm/dw/vadivam-preact?style=flat-square&color=666)](https://www.npmjs.com/package/vadivam-preact)
9
9
  [![license](https://img.shields.io/npm/l/vadivam-preact?style=flat-square&color=111)](https://github.com/praveenjuge/vadivam/blob/main/LICENSE)
10
10
 
11
- ![All Vadivam icons](https://raw.githubusercontent.com/praveenjuge/vadivam/main/apps/docs/public/preview.png?v=0.0.42)
11
+ ![All Vadivam icons](https://raw.githubusercontent.com/praveenjuge/vadivam/main/apps/docs/public/preview.png?v=0.0.44)
12
12
 
13
13
  ```sh
14
14
  npm install vadivam-preact
package/dist/Icon.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { h, toChildArray } from "preact";
2
2
  import defaultAttributes from "./defaultAttributes.js";
3
+ import { sanitizeIconNode, sanitizePaint, sanitizeRootAttributes } from "./runtimePolicy.js";
3
4
  import { useVadivamContext } from "./context.js";
4
5
  function classes(...values) { return values.flatMap((value) => typeof value === "string" ? value.split(/\s+/) : []).filter((value, index, all) => value && all.indexOf(value) === index).join(" "); }
5
6
  function hasA11y(props) { return Object.keys(props).some((name) => name.startsWith("aria-") || name === "role" || name === "title"); }
6
- export function Icon({ color, size, strokeWidth, absoluteStrokeWidth, children, iconNode = [], name, title, class: className = "", className: reactClass = "", ...rest }) { const context = useVadivamContext() ?? {}; const resolvedSize = size ?? context.size ?? 24; const resolvedStroke = strokeWidth ?? context.strokeWidth ?? 2; const numericSize = Number(resolvedSize); const numericStroke = Number(resolvedStroke); const absolute = absoluteStrokeWidth ?? context.absoluteStrokeWidth ?? false; const calculated = absolute && Number.isFinite(numericSize) && numericSize !== 0 && Number.isFinite(numericStroke) ? numericStroke * 24 / numericSize : resolvedStroke; const labelled = Boolean(title) || hasA11y(rest); return h("svg", { ...defaultAttributes, width: resolvedSize, height: resolvedSize, stroke: color ?? context.color ?? "currentColor", "stroke-width": calculated, class: classes("vadivam", name && "vadivam-" + name, context.class, className, reactClass), ...(!labelled ? { "aria-hidden": "true" } : {}), ...(title ? { role: "img" } : {}), ...rest }, [title ? h("title", null, title) : null, ...iconNode.map(([tag, attrs]) => h(tag, attrs)), ...toChildArray(children)]); }
7
+ export function Icon({ color, size, strokeWidth, absoluteStrokeWidth, children, iconNode = [], name, title, class: className = "", className: reactClass = "", ...rest }) { const context = useVadivamContext() ?? {}; const resolvedSize = size ?? context.size ?? 24; const resolvedStroke = strokeWidth ?? context.strokeWidth ?? 2; const numericSize = Number(resolvedSize); const numericStroke = Number(resolvedStroke); const absolute = absoluteStrokeWidth ?? context.absoluteStrokeWidth ?? false; const calculated = absolute && Number.isFinite(numericSize) && numericSize !== 0 && Number.isFinite(numericStroke) ? numericStroke * 24 / numericSize : resolvedStroke; const safeRest = sanitizeRootAttributes(rest, true); const labelled = Boolean(title) || hasA11y(safeRest); return h("svg", { ...defaultAttributes, width: resolvedSize, height: resolvedSize, stroke: sanitizePaint(color ?? context.color), "stroke-width": calculated, class: classes("vadivam", name && "vadivam-" + name, context.class, className, reactClass), ...(!labelled ? { "aria-hidden": "true" } : {}), ...(title ? { role: "img" } : {}), ...safeRest }, [title ? h("title", null, title) : null, ...sanitizeIconNode(iconNode).map(([tag, attrs]) => h(tag, attrs)), ...toChildArray(children)]); }
7
8
  export default Icon;
@@ -0,0 +1,4 @@
1
+ import type { IconNode } from "./types.js";
2
+ export declare function sanitizeIconNode(iconNode: unknown): IconNode;
3
+ export declare function sanitizePaint(value: unknown, fallback?: string): string | number;
4
+ export declare function sanitizeRootAttributes(attrs: unknown, allowEventFunctions?: boolean): Record<string, unknown>;
@@ -0,0 +1,141 @@
1
+ const SAFE_ICON_TAGS = new Set(["circle","ellipse","line","path","polygon","polyline","rect"]);
2
+ const GEOMETRY_ATTRIBUTES = {
3
+ circle: new Set(["cx", "cy", "r"]),
4
+ ellipse: new Set(["cx", "cy", "rx", "ry"]),
5
+ line: new Set(["x1", "y1", "x2", "y2"]),
6
+ path: new Set(["d", "pathLength"]),
7
+ polygon: new Set(["points", "pathLength"]),
8
+ polyline: new Set(["points", "pathLength"]),
9
+ rect: new Set(["x", "y", "width", "height", "rx", "ry", "pathLength"]),
10
+ };
11
+ const PRESENTATION_ATTRIBUTES = new Set([
12
+ "class", "className", "id", "role", "key", "opacity", "transform",
13
+ "clip-rule", "clipRule", "fill", "fill-opacity", "fillOpacity", "fill-rule", "fillRule",
14
+ "shape-rendering", "shapeRendering", "stroke", "stroke-dasharray", "strokeDasharray",
15
+ "stroke-dashoffset", "strokeDashoffset", "stroke-linecap", "strokeLinecap",
16
+ "stroke-linejoin", "strokeLinejoin", "stroke-miterlimit", "strokeMiterlimit",
17
+ "stroke-opacity", "strokeOpacity", "stroke-width", "strokeWidth", "vector-effect", "vectorEffect",
18
+ ]);
19
+ const SAFE_ATTRIBUTE_NAME = /^[A-Za-z_][A-Za-z0-9_.-]*$/;
20
+ const SAFE_DATA_ATTRIBUTE = /^data-[A-Za-z0-9_.-]+$/;
21
+ const SAFE_ARIA_ATTRIBUTE = /^aria-[A-Za-z0-9_.-]+$/;
22
+ const SAFE_STYLE_NAME = /^(?:--[A-Za-z0-9_-]+|[A-Za-z_][A-Za-z0-9_-]*)$/;
23
+ const SAFE_EVENT_NAME = /^on(?::[A-Za-z][A-Za-z0-9_.-]*|[A-Za-z][A-Za-z0-9_.-]*)$/;
24
+ const UNSAFE_ROOT_ATTRIBUTE = /^(?:dangerouslySetInnerHTML|innerHTML|innerText|textContent|children|href|xlinkHref|src|srcdoc)$/i;
25
+ const URL_VALUE_ATTRIBUTE = /^(?:fill|stroke|filter|clipPath|clip-path|mask|marker|markerStart|marker-start|markerMid|marker-mid|markerEnd|marker-end|cursor)$/;
26
+ const SCRIPT_SCHEME = /(?:java|vb)script\s*:/i;
27
+ const URL_FUNCTION = /url\s*\(/i;
28
+ const COMPLETE_URL_FUNCTION = /url\s*\(\s*(["']?)([^"')\s]+)\1\s*\)/gi;
29
+ const EXTERNAL_SCHEME = /(?:https?|data|blob|file|ftp)\s*:/i;
30
+ const CSS_ESCAPE = /\\/;
31
+ const CSS_RESOURCE_FUNCTION = /(?:image-set|cross-fade|image|src)\s*\(/i;
32
+ const CSS_IMPORT = /@import\b/i;
33
+ const MAX_ICON_DEPTH = 32;
34
+ const MAX_ICON_NODES = 1024;
35
+
36
+ function isRecord(value) {
37
+ return value !== null && typeof value === "object" && !Array.isArray(value);
38
+ }
39
+
40
+ function isSafeValue(name, value) {
41
+ if (value === undefined || value === null || typeof value === "function") return false;
42
+ if (typeof value === "object") return false;
43
+ if (typeof value === "string" && SCRIPT_SCHEME.test(value)) return false;
44
+ if ((name === "fill" || name === "stroke") && typeof value === "string" && hasExternalUrl(value)) return false;
45
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
46
+ }
47
+
48
+ function hasExternalUrl(value) {
49
+ if (!URL_FUNCTION.test(value)) return false;
50
+ COMPLETE_URL_FUNCTION.lastIndex = 0;
51
+ let matched = false;
52
+ let match;
53
+ while ((match = COMPLETE_URL_FUNCTION.exec(value)) !== null) {
54
+ matched = true;
55
+ if (!match[2].startsWith("#")) return true;
56
+ }
57
+ return !matched;
58
+ }
59
+
60
+ function hasUnsafeStyleValue(value) {
61
+ return SCRIPT_SCHEME.test(value) || EXTERNAL_SCHEME.test(value) || CSS_ESCAPE.test(value) ||
62
+ CSS_RESOURCE_FUNCTION.test(value) || CSS_IMPORT.test(value) || hasExternalUrl(value);
63
+ }
64
+
65
+ function isSafeIconAttribute(tag, name) {
66
+ return SAFE_ATTRIBUTE_NAME.test(name) && (
67
+ GEOMETRY_ATTRIBUTES[tag]?.has(name) ||
68
+ PRESENTATION_ATTRIBUTES.has(name) ||
69
+ SAFE_DATA_ATTRIBUTE.test(name) ||
70
+ SAFE_ARIA_ATTRIBUTE.test(name)
71
+ );
72
+ }
73
+
74
+ function sanitizeStyle(value) {
75
+ if (typeof value === "string") return hasUnsafeStyleValue(value) ? undefined : value;
76
+ if (Array.isArray(value)) {
77
+ const safeValues = value.map(sanitizeStyle).filter((entry) => entry !== undefined);
78
+ return safeValues.length ? safeValues : undefined;
79
+ }
80
+ if (!isRecord(value)) return undefined;
81
+ const safeStyle = {};
82
+ for (const [name, entry] of Object.entries(value)) {
83
+ if (!SAFE_STYLE_NAME.test(name) || (typeof entry !== "string" && typeof entry !== "number")) continue;
84
+ if (typeof entry === "string" && hasUnsafeStyleValue(entry)) continue;
85
+ safeStyle[name] = entry;
86
+ }
87
+ return Object.keys(safeStyle).length ? safeStyle : undefined;
88
+ }
89
+
90
+ function safeRootValue(name, value) {
91
+ if (name === "style") return sanitizeStyle(value);
92
+ if (value === undefined || value === null || typeof value === "object" || typeof value === "function") return undefined;
93
+ if (typeof value === "string" && URL_VALUE_ATTRIBUTE.test(name) && (SCRIPT_SCHEME.test(value) || hasExternalUrl(value))) return undefined;
94
+ return value;
95
+ }
96
+
97
+ export function sanitizePaint(value, fallback = "currentColor") {
98
+ const safeValue = safeRootValue("stroke", value);
99
+ return typeof safeValue === "string" || typeof safeValue === "number" ? safeValue : fallback;
100
+ }
101
+
102
+ function sanitizeNodes(iconNode, activeArrays, state, depth) {
103
+ if (!Array.isArray(iconNode) || depth > MAX_ICON_DEPTH || activeArrays.has(iconNode)) return [];
104
+ activeArrays.add(iconNode);
105
+ const safeNodes = [];
106
+ for (const node of iconNode) {
107
+ state.count += 1;
108
+ if (state.count > MAX_ICON_NODES) break;
109
+ if (!Array.isArray(node) || typeof node[0] !== "string" || !SAFE_ICON_TAGS.has(node[0]) || !isRecord(node[1])) continue;
110
+ const [tag, attrs, children] = node;
111
+ const safeAttrs = {};
112
+ for (const [name, value] of Object.entries(attrs)) {
113
+ if (isSafeIconAttribute(tag, name) && isSafeValue(name, value)) safeAttrs[name] = value;
114
+ }
115
+ safeNodes.push([tag, safeAttrs]);
116
+ }
117
+ activeArrays.delete(iconNode);
118
+ return safeNodes;
119
+ }
120
+
121
+ export function sanitizeIconNode(iconNode) {
122
+ return sanitizeNodes(iconNode, new WeakSet(), { count: 0 }, 0);
123
+ }
124
+
125
+ export function sanitizeRootAttributes(attrs, allowEventFunctions = false) {
126
+ if (!isRecord(attrs)) return {};
127
+ const safeAttrs = {};
128
+ for (const name of Object.keys(attrs)) {
129
+ const value = attrs[name];
130
+ if (/^on/i.test(name)) {
131
+ if (allowEventFunctions && typeof value === "function" && SAFE_EVENT_NAME.test(name)) {
132
+ Object.defineProperty(safeAttrs, name, { enumerable: true, get: () => typeof attrs[name] === "function" ? attrs[name] : undefined });
133
+ }
134
+ continue;
135
+ }
136
+ if (!SAFE_ATTRIBUTE_NAME.test(name) || UNSAFE_ROOT_ATTRIBUTE.test(name) || /^prop/i.test(name)) continue;
137
+ if (safeRootValue(name, value) === undefined) continue;
138
+ Object.defineProperty(safeAttrs, name, { enumerable: true, get: () => safeRootValue(name, attrs[name]) });
139
+ }
140
+ return safeAttrs;
141
+ }
package/dist/types.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { ComponentType, JSX } from "preact";
2
2
  export type SVGElementType = "circle" | "ellipse" | "line" | "path" | "polygon" | "polyline" | "rect";
3
- export type IconNode = readonly [tag: SVGElementType, attrs: Record<string, string>][];
3
+ export type IconNode = readonly [tag: SVGElementType, attrs: Record<string, string | number | boolean>][];
4
4
  export interface VadivamProps extends Omit<JSX.SVGAttributes<SVGSVGElement>, "color" | "size"> { name?: string; size?: string | number; color?: string; strokeWidth?: string | number; absoluteStrokeWidth?: boolean; title?: string; iconNode?: IconNode; }
5
5
  export type VadivamIcon = ComponentType<VadivamProps>;
6
6
  export type IconName = "a-arrow-down" | "a-arrow-up" | "a-large-small" | "accessibility" | "activity" | "air-vent" | "airplay" | "alarm-clock-check" | "alarm-clock-minus" | "alarm-clock-off" | "alarm-clock-plus" | "alarm-clock" | "alarm-smoke" | "album" | "align-center-horizontal" | "align-center-vertical" | "align-end-horizontal" | "align-end-vertical" | "align-horizontal-distribute-center" | "align-horizontal-distribute-end" | "align-horizontal-distribute-start" | "align-horizontal-justify-center" | "align-horizontal-justify-end" | "align-horizontal-justify-start" | "align-horizontal-space-around" | "align-horizontal-space-between" | "align-start-horizontal" | "align-start-vertical" | "align-vertical-distribute-center" | "align-vertical-distribute-end" | "align-vertical-distribute-start" | "align-vertical-justify-center" | "align-vertical-justify-end" | "align-vertical-justify-start" | "align-vertical-space-around" | "align-vertical-space-between" | "ambulance" | "ampersand" | "ampersands" | "amphora" | "anchor" | "antenna" | "anvil" | "aperture" | "app-window-mac" | "app-window" | "apple" | "archive-restore" | "archive-x" | "archive" | "armchair" | "arrow-big-down-dash" | "arrow-big-down" | "arrow-big-left-dash" | "arrow-big-left" | "arrow-big-right-dash" | "arrow-big-right" | "arrow-big-up-dash" | "arrow-big-up" | "arrow-down-0-1" | "arrow-down-1-0" | "arrow-down-a-z" | "arrow-down-from-line" | "arrow-down-left" | "arrow-down-narrow-wide" | "arrow-down-right" | "arrow-down-to-dot" | "arrow-down-to-line" | "arrow-down-up" | "arrow-down-wide-narrow" | "arrow-down-z-a" | "arrow-down" | "arrow-left-from-line" | "arrow-left-right" | "arrow-left-to-line" | "arrow-left" | "arrow-right-from-line" | "arrow-right-left" | "arrow-right-to-line" | "arrow-right" | "arrow-up-0-1" | "arrow-up-1-0" | "arrow-up-a-z" | "arrow-up-down" | "arrow-up-from-dot" | "arrow-up-from-line" | "arrow-up-left" | "arrow-up-narrow-wide" | "arrow-up-right" | "arrow-up-to-line" | "arrow-up-wide-narrow" | "arrow-up-z-a" | "arrow-up" | "arrows-up-from-line" | "asterisk" | "astroid" | "at-sign" | "atom" | "audio-lines" | "audio-waveform" | "award" | "axe" | "axis-3d" | "baby" | "backpack" | "badge-alert" | "badge-cent" | "badge-check" | "badge-dollar-sign" | "badge-euro" | "badge-indian-rupee" | "badge-info" | "badge-japanese-yen" | "badge-minus" | "badge-percent" | "badge-plus" | "badge-pound-sterling" | "badge-question-mark" | "badge-russian-ruble" | "badge-swiss-franc" | "badge-turkish-lira" | "badge-x" | "badge" | "baggage-claim" | "balloon" | "ban" | "banana" | "bandage" | "banknote-arrow-down" | "banknote-arrow-up" | "banknote-x" | "banknote" | "barcode" | "barrel" | "baseline" | "bath" | "battery-charging" | "battery-full" | "battery-low" | "battery-medium" | "battery-plus" | "battery-warning" | "battery" | "beaker" | "bean-off" | "bean" | "bed-double" | "bed-single" | "bed" | "beef-off" | "beef" | "beer-off" | "beer" | "bell-check" | "bell-dot" | "bell-electric" | "bell-minus" | "bell-off" | "bell-plus" | "bell-ring" | "bell" | "between-horizontal-end" | "between-horizontal-start" | "between-vertical-end" | "between-vertical-start" | "blocks" | "bluetooth" | "bold" | "book-open-check" | "book-open-text" | "book-open" | "book" | "bookmark" | "bot" | "box" | "boxes" | "braces" | "brain" | "briefcase-business" | "briefcase" | "bug" | "building-2" | "building" | "calculator" | "calendar-clock" | "calendar-days" | "calendar-minus" | "calendar-plus" | "calendar" | "camera" | "captions" | "car" | "chart-area" | "chart-bar" | "chart-column-big" | "chart-line" | "chart-pie" | "check-check" | "check" | "chevron-down" | "chevron-left" | "chevron-right" | "chevron-up" | "chevrons-left" | "chevrons-right" | "chevrons-up-down" | "circle-alert" | "circle-arrow-down" | "circle-arrow-left" | "circle-arrow-right" | "circle-arrow-up" | "circle-check-big" | "circle-check" | "circle-dashed" | "circle-dot-dashed" | "circle-dot" | "circle-fading-arrow-up" | "circle-fading-plus" | "circle-off" | "circle-plus" | "circle-question-mark" | "circle-stop" | "circle-user-round" | "circle-user" | "circle-x" | "circle" | "clipboard-list" | "clipboard-paste" | "clipboard" | "clock-1" | "clock-10" | "clock-11" | "clock-12" | "clock-2" | "clock-3" | "clock-4" | "clock-5" | "clock-6" | "clock-7" | "clock-8" | "clock-9" | "clock" | "cloud-upload" | "cloud" | "code-xml" | "code" | "coffee" | "cog" | "coins" | "columns-2" | "columns-3" | "columns-4" | "command" | "compass" | "container" | "copy" | "corner-down-left" | "corner-down-right" | "corner-left-down" | "corner-left-up" | "corner-right-down" | "corner-right-up" | "corner-up-left" | "corner-up-right" | "cpu" | "credit-card" | "crown" | "database" | "dollar-sign" | "dot" | "download" | "ellipsis-vertical" | "ellipsis" | "eraser" | "external-link" | "eye-off" | "eye" | "face-angry" | "face-expressionless" | "face-slightly-smiling" | "file-archive" | "file-chart-column-increasing" | "file-chart-column" | "file-code" | "file-down" | "file-exclamation-point" | "file-image" | "file-minus-corner" | "file-minus" | "file-plus-corner" | "file-plus" | "file-search-corner" | "file-search" | "file-text" | "file-up" | "file" | "film" | "flag-triangle-left" | "flag-triangle-right" | "flag" | "flame" | "flask-conical" | "folder-code" | "folder-open" | "folder-plus" | "folder-search-2" | "folder-search" | "folder" | "footprints" | "forward" | "frame" | "fullscreen" | "funnel" | "gallery-horizontal-end" | "gallery-horizontal" | "gallery-thumbnails" | "gallery-vertical-end" | "gallery-vertical" | "gauge" | "gift" | "git-branch-minus" | "git-branch-plus" | "git-branch" | "git-commit-horizontal" | "git-commit-vertical" | "globe" | "grip-horizontal" | "grip-vertical" | "hard-drive" | "hash" | "heading-1" | "heading-2" | "heading-3" | "heading-4" | "heading-5" | "heading-6" | "heading" | "heart" | "hexagon" | "house" | "image" | "inbox" | "info" | "italic" | "key-round" | "key" | "keyboard" | "languages" | "laptop" | "layers" | "layout-dashboard" | "layout-grid" | "library" | "life-buoy" | "lightbulb" | "link-2" | "link" | "list-checks" | "list-filter" | "list-ordered" | "list-todo" | "list" | "loader-circle" | "loader" | "lock-keyhole" | "lock" | "log-in" | "log-out" | "mail-check" | "mail" | "map-pin" | "map" | "maximize-2" | "maximize" | "menu" | "message-circle-dashed" | "message-circle-question-mark" | "message-circle" | "message-square" | "mic" | "minimize-2" | "minimize" | "minus" | "monitor" | "moon" | "mouse-pointer-2" | "mouse-pointer-click" | "mouse-pointer" | "move-right" | "music" | "network" | "octagon-x" | "package" | "paintbrush" | "palette" | "panel-left-close" | "panel-left-open" | "panel-left" | "panel-right-close" | "panel-right-open" | "panel-right" | "panels-top-left" | "paperclip" | "pause" | "pencil-line" | "pencil" | "phone" | "pin" | "play" | "plug" | "plus" | "popcorn" | "power" | "presentation" | "puzzle" | "qr-code" | "quote" | "radar" | "radio" | "redo" | "refresh-ccw" | "refresh-cw" | "repeat" | "rocket" | "rotate-ccw-clock" | "rotate-ccw" | "rotate-cw" | "save" | "scale" | "scissors" | "scroll-text" | "scroll" | "search" | "send" | "server" | "settings-2" | "settings" | "share-2" | "share" | "shield-alert" | "shield-check" | "shield" | "shopping-bag" | "shopping-cart" | "shuffle" | "skip-forward" | "slash" | "sliders-horizontal" | "smartphone" | "sparkles" | "square-centerline-dashed-horizontal" | "square-centerline-dashed-vertical" | "square-dashed" | "square-pen" | "square-terminal" | "square" | "star-off" | "star" | "strikethrough" | "sun" | "table" | "tablet" | "tag" | "target" | "telescope" | "terminal" | "text-align-center" | "text-align-end" | "text-align-start" | "thermometer" | "thumbs-down" | "thumbs-up" | "timer-off" | "timer" | "trash-2" | "trash" | "trending-down" | "trending-up" | "triangle-alert" | "trophy" | "tv" | "type" | "underline" | "undo-2" | "undo" | "upload" | "user-check" | "user-plus" | "user-round-x" | "user-round" | "user" | "users" | "video" | "volume-1" | "volume-2" | "volume-off" | "volume-x" | "volume" | "wallet" | "waves-horizontal" | "wifi-off" | "wifi" | "workflow" | "wrench" | "x" | "zap" | "zoom-in" | "zoom-out";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vadivam-preact",
3
- "version": "0.0.42",
3
+ "version": "0.0.44",
4
4
  "description": "Pixel-perfect 24px outline icon components for Preact with tree-shakeable imports.",
5
5
  "license": "MIT",
6
6
  "author": "Praveen Juge",