@vobs/vite-plugin 1.1.0 → 1.2.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/html-component.cjs +138 -0
- package/dist/html-component.cjs.map +1 -0
- package/dist/html-component.d.cts +7 -0
- package/dist/html-component.d.ts +7 -0
- package/dist/html-component.js +136 -0
- package/dist/html-component.js.map +1 -0
- package/dist/index.cjs +1366 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +15 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +1359 -0
- package/dist/index.js.map +1 -0
- package/package.json +21 -6
- package/src/index.ts +5 -5
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var parse5 = require('parse5');
|
|
4
|
+
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
7
|
+
var blockedTags = /* @__PURE__ */ new Set(["script", "iframe", "object", "embed", "base", "meta", "link", "style"]);
|
|
8
|
+
var blockedAttributes = /^(?:on[a-z]+|srcdoc|style)$/iu;
|
|
9
|
+
var urlAttributes = /* @__PURE__ */ new Set(["href", "src", "action", "formaction", "poster", "xlink:href"]);
|
|
10
|
+
var safeProtocols = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:"]);
|
|
11
|
+
var directivePattern = /^data-vobs-(text|slot|on|bind|prop)(?:-(.+))?$/iu;
|
|
12
|
+
var safePropertyNames = /* @__PURE__ */ new Set([
|
|
13
|
+
"value",
|
|
14
|
+
"checked",
|
|
15
|
+
"selected",
|
|
16
|
+
"disabled",
|
|
17
|
+
"multiple",
|
|
18
|
+
"readonly",
|
|
19
|
+
"required",
|
|
20
|
+
"autofocus",
|
|
21
|
+
"hidden",
|
|
22
|
+
"tabindex"
|
|
23
|
+
]);
|
|
24
|
+
function compileHtmlComponent(source, options = {}) {
|
|
25
|
+
const fragment = parse5.parseFragment(source);
|
|
26
|
+
const body = compileChildren(fragment, options.filename ?? "component.html");
|
|
27
|
+
const result = body.length === 1 ? body[0] : `createFragment((parent, anchor) => {${body.map((code) => `insertBefore(parent, ${code}, anchor);`).join("")}})`;
|
|
28
|
+
return `import { addEventListener, bindAttribute, bindProperty, bindText, createElement, createFragment, createText, insertBefore, insertDynamic, setAttribute } from '@vobs/vobs';
|
|
29
|
+
function resolveHtmlSlot(value) { const resolved = typeof value === 'function' ? value() : value; if (resolved === undefined || resolved === null || resolved === false) return null; if (typeof resolved === 'string' || typeof resolved === 'number') return createText(String(resolved)); if (Array.isArray(resolved)) return createFragment((parent, anchor) => { for (const item of resolved) { const child = resolveHtmlSlot(item); if (child) insertBefore(parent, child, anchor); } }); return resolved; }
|
|
30
|
+
function sanitizeHtmlAttribute(name, value) { const stringValue = String(value ?? ''); if (!['href', 'src', 'action', 'formaction', 'poster', 'xlink:href'].includes(name.toLowerCase())) return stringValue; const normalized = stringValue.trim().toLowerCase(); if (normalized.startsWith('#') || normalized.startsWith('/') || normalized.startsWith('./') || normalized.startsWith('../')) return stringValue; try { const protocol = new URL(normalized, 'https://vobs.invalid/').protocol; return ['http:', 'https:', 'mailto:', 'tel:'].includes(protocol) ? stringValue : ''; } catch { return ''; } }
|
|
31
|
+
export default function HtmlComponent(props = {}) { return ${result ?? "createFragment(() => {})"}; }`;
|
|
32
|
+
}
|
|
33
|
+
__name(compileHtmlComponent, "compileHtmlComponent");
|
|
34
|
+
function compileChildren(parent, filename) {
|
|
35
|
+
return parent.childNodes.flatMap((node) => compileNode(node, filename));
|
|
36
|
+
}
|
|
37
|
+
__name(compileChildren, "compileChildren");
|
|
38
|
+
function compileNode(node, filename) {
|
|
39
|
+
if (node.nodeName === "#text") {
|
|
40
|
+
const value = node.value;
|
|
41
|
+
return value ? [`createText(${JSON.stringify(value)})`] : [];
|
|
42
|
+
}
|
|
43
|
+
if (node.nodeName === "#comment") return [];
|
|
44
|
+
if (node.nodeName !== "#document-fragment" && !isElement(node)) return [];
|
|
45
|
+
if (isElement(node)) {
|
|
46
|
+
const tag = node.tagName.toLowerCase();
|
|
47
|
+
if (blockedTags.has(tag)) throw new Error(`Vobs HTML component: \u7981\u6B62\u4F7F\u7528 <${tag}> (${filename})`);
|
|
48
|
+
const children = compileChildren(node, filename);
|
|
49
|
+
const statements = [`const element = createElement(${JSON.stringify(tag)})`];
|
|
50
|
+
const dynamicText = [];
|
|
51
|
+
for (const attribute of node.attrs) {
|
|
52
|
+
const name = attribute.name.toLowerCase();
|
|
53
|
+
const value = attribute.value;
|
|
54
|
+
const directive = parseDirective(name, value, filename);
|
|
55
|
+
if (directive) {
|
|
56
|
+
if (directive.kind === "text") {
|
|
57
|
+
dynamicText.push(directive.prop);
|
|
58
|
+
} else if (directive.kind === "slot") {
|
|
59
|
+
statements.push(`insertDynamic(element, null, () => resolveHtmlSlot(props[${JSON.stringify(directive.prop)}]))`);
|
|
60
|
+
} else if (directive.kind === "event") {
|
|
61
|
+
statements.push(`addEventListener(element, ${JSON.stringify(directive.name)}, (event) => { const handler = props[${JSON.stringify(directive.prop)}]; if (typeof handler === 'function') handler(event); })`);
|
|
62
|
+
} else if (directive.kind === "attribute") {
|
|
63
|
+
statements.push(`bindAttribute(element, ${JSON.stringify(directive.name)}, () => sanitizeHtmlAttribute(${JSON.stringify(directive.name)}, props[${JSON.stringify(directive.prop)}] ?? ''))`);
|
|
64
|
+
} else if (directive.kind === "property") {
|
|
65
|
+
statements.push(`bindProperty(element, ${JSON.stringify(directive.name)}, () => props[${JSON.stringify(directive.prop)}])`);
|
|
66
|
+
}
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (blockedAttributes.test(name)) throw new Error(`Vobs HTML component: \u7981\u6B62\u4F7F\u7528\u5371\u9669\u5C5E\u6027 ${attribute.name} (${filename})`);
|
|
70
|
+
if (urlAttributes.has(name) && !isSafeUrl(value)) {
|
|
71
|
+
throw new Error(`Vobs HTML component: \u7981\u6B62\u4F7F\u7528\u5371\u9669 URL \u5C5E\u6027 ${attribute.name} (${filename})`);
|
|
72
|
+
}
|
|
73
|
+
statements.push(`setAttribute(element, ${JSON.stringify(attribute.name)}, ${JSON.stringify(value)})`);
|
|
74
|
+
}
|
|
75
|
+
if (dynamicText.length > 1) throw new Error(`Vobs HTML component: \u4E00\u4E2A\u5143\u7D20\u53EA\u80FD\u4F7F\u7528\u4E00\u4E2A data-vobs-text \u6307\u4EE4 (${filename})`);
|
|
76
|
+
if (dynamicText.length === 1) {
|
|
77
|
+
const text = 'createText("")';
|
|
78
|
+
statements.push(`const text = ${text}`);
|
|
79
|
+
statements.push("insertBefore(element, text, null)");
|
|
80
|
+
statements.push(`bindText(text, () => props[${JSON.stringify(dynamicText[0])}] ?? '')`);
|
|
81
|
+
} else {
|
|
82
|
+
for (const child of children) statements.push(`insertBefore(element, ${child}, null)`);
|
|
83
|
+
}
|
|
84
|
+
statements.push("return element");
|
|
85
|
+
return [`(() => {${statements.join(";")};})()`];
|
|
86
|
+
}
|
|
87
|
+
return compileChildren(node, filename);
|
|
88
|
+
}
|
|
89
|
+
__name(compileNode, "compileNode");
|
|
90
|
+
function isElement(node) {
|
|
91
|
+
return !node.nodeName.startsWith("#");
|
|
92
|
+
}
|
|
93
|
+
__name(isElement, "isElement");
|
|
94
|
+
function isSafeUrl(value) {
|
|
95
|
+
const normalized = value.trim().toLowerCase();
|
|
96
|
+
if (normalized.startsWith("#") || normalized.startsWith("/") || normalized.startsWith("./") || normalized.startsWith("../")) return true;
|
|
97
|
+
try {
|
|
98
|
+
return safeProtocols.has(new URL(normalized, "https://vobs.invalid/").protocol);
|
|
99
|
+
} catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
__name(isSafeUrl, "isSafeUrl");
|
|
104
|
+
function parseDirective(name, value, filename) {
|
|
105
|
+
const match = directivePattern.exec(name);
|
|
106
|
+
if (!match) return null;
|
|
107
|
+
const kind = match[1].toLowerCase();
|
|
108
|
+
const namePart = match[2];
|
|
109
|
+
const prop = value.trim();
|
|
110
|
+
if (!prop || !/^[A-Za-z_$][\w$]*$/u.test(prop)) {
|
|
111
|
+
throw new Error(`Vobs HTML component: ${name} \u5FC5\u987B\u5F15\u7528\u6709\u6548\u7684 props \u540D\u79F0 (${filename})`);
|
|
112
|
+
}
|
|
113
|
+
if (kind === "text" || kind === "slot") {
|
|
114
|
+
if (namePart) throw new Error(`Vobs HTML component: ${name} \u4E0D\u63A5\u53D7\u989D\u5916\u540D\u79F0 (${filename})`);
|
|
115
|
+
return { kind, prop };
|
|
116
|
+
}
|
|
117
|
+
if (!namePart || !/^[a-z][a-z0-9:-]*$/iu.test(namePart)) {
|
|
118
|
+
throw new Error(`Vobs HTML component: ${name} \u5FC5\u987B\u5305\u542B\u6709\u6548\u540D\u79F0 (${filename})`);
|
|
119
|
+
}
|
|
120
|
+
if (kind === "on") return { kind: "event", name: namePart.toLowerCase(), prop };
|
|
121
|
+
if (kind === "bind") {
|
|
122
|
+
if (namePart.toLowerCase() === "style") throw new Error(`Vobs HTML component: \u4E0D\u5141\u8BB8\u52A8\u6001\u7ED1\u5B9A style (${filename})`);
|
|
123
|
+
return { kind: "attribute", name: namePart, prop };
|
|
124
|
+
}
|
|
125
|
+
if (!safePropertyNames.has(namePart.toLowerCase())) {
|
|
126
|
+
throw new Error(`Vobs HTML component: \u4E0D\u5141\u8BB8\u52A8\u6001\u7ED1\u5B9A property ${namePart} (${filename})`);
|
|
127
|
+
}
|
|
128
|
+
return { kind: "property", name: normalizePropertyName(namePart), prop };
|
|
129
|
+
}
|
|
130
|
+
__name(parseDirective, "parseDirective");
|
|
131
|
+
function normalizePropertyName(name) {
|
|
132
|
+
return name.toLowerCase() === "readonly" ? "readOnly" : name.toLowerCase() === "tabindex" ? "tabIndex" : name;
|
|
133
|
+
}
|
|
134
|
+
__name(normalizePropertyName, "normalizePropertyName");
|
|
135
|
+
|
|
136
|
+
exports.compileHtmlComponent = compileHtmlComponent;
|
|
137
|
+
//# sourceMappingURL=html-component.cjs.map
|
|
138
|
+
//# sourceMappingURL=html-component.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/html-component.ts"],"names":["parseFragment"],"mappings":"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","file":"html-component.cjs","sourcesContent":["import { parseFragment, type DefaultTreeAdapterMap, type DefaultTreeAdapterTypes } from 'parse5'\n\ntype HtmlNode = DefaultTreeAdapterTypes.Node\ntype HtmlDocumentFragment = DefaultTreeAdapterTypes.DocumentFragment\ntype HtmlElement = DefaultTreeAdapterTypes.Element\n\nconst blockedTags = new Set(['script', 'iframe', 'object', 'embed', 'base', 'meta', 'link', 'style'])\nconst blockedAttributes = /^(?:on[a-z]+|srcdoc|style)$/iu\nconst urlAttributes = new Set(['href', 'src', 'action', 'formaction', 'poster', 'xlink:href'])\nconst safeProtocols = new Set(['http:', 'https:', 'mailto:', 'tel:'])\nconst directivePattern = /^data-vobs-(text|slot|on|bind|prop)(?:-(.+))?$/iu\nconst safePropertyNames = new Set([\n 'value', 'checked', 'selected', 'disabled', 'multiple', 'readonly', 'required',\n 'autofocus', 'hidden', 'tabindex'\n])\n\nexport interface HtmlComponentOptions {\n readonly filename?: string\n}\n\n/** Converts a trusted static HTML fragment to Vobs runtime node creation code. */\nexport function compileHtmlComponent(source: string, options: HtmlComponentOptions = {}): string {\n const fragment = parseFragment(source)\n const body = compileChildren(fragment, options.filename ?? 'component.html')\n const result = body.length === 1 ? body[0] : `createFragment((parent, anchor) => {${body.map(code => `insertBefore(parent, ${code}, anchor);`).join('')}})`\n\n return `import { addEventListener, bindAttribute, bindProperty, bindText, createElement, createFragment, createText, insertBefore, insertDynamic, setAttribute } from '@vobs/vobs';\\nfunction resolveHtmlSlot(value) { const resolved = typeof value === 'function' ? value() : value; if (resolved === undefined || resolved === null || resolved === false) return null; if (typeof resolved === 'string' || typeof resolved === 'number') return createText(String(resolved)); if (Array.isArray(resolved)) return createFragment((parent, anchor) => { for (const item of resolved) { const child = resolveHtmlSlot(item); if (child) insertBefore(parent, child, anchor); } }); return resolved; }\\nfunction sanitizeHtmlAttribute(name, value) { const stringValue = String(value ?? ''); if (!['href', 'src', 'action', 'formaction', 'poster', 'xlink:href'].includes(name.toLowerCase())) return stringValue; const normalized = stringValue.trim().toLowerCase(); if (normalized.startsWith('#') || normalized.startsWith('/') || normalized.startsWith('./') || normalized.startsWith('../')) return stringValue; try { const protocol = new URL(normalized, 'https://vobs.invalid/').protocol; return ['http:', 'https:', 'mailto:', 'tel:'].includes(protocol) ? stringValue : ''; } catch { return ''; } }\\nexport default function HtmlComponent(props = {}) { return ${result ?? \"createFragment(() => {})\"}; }`\n}\n\nfunction compileChildren(parent: HtmlDocumentFragment | HtmlElement, filename: string): string[] {\n return parent.childNodes.flatMap(node => compileNode(node, filename))\n}\n\nfunction compileNode(node: HtmlNode, filename: string): string[] {\n if (node.nodeName === '#text') {\n const value = (node as DefaultTreeAdapterMap['textNode']).value\n return value ? [`createText(${JSON.stringify(value)})`] : []\n }\n\n if (node.nodeName === '#comment') return []\n if (node.nodeName !== '#document-fragment' && !isElement(node)) return []\n\n if (isElement(node)) {\n const tag = node.tagName.toLowerCase()\n if (blockedTags.has(tag)) throw new Error(`Vobs HTML component: 禁止使用 <${tag}> (${filename})`)\n\n const children = compileChildren(node, filename)\n const statements = [`const element = createElement(${JSON.stringify(tag)})`]\n const dynamicText = [] as string[]\n for (const attribute of node.attrs) {\n const name = attribute.name.toLowerCase()\n const value = attribute.value\n const directive = parseDirective(name, value, filename)\n if (directive) {\n if (directive.kind === 'text') {\n dynamicText.push(directive.prop)\n } else if (directive.kind === 'slot') {\n statements.push(`insertDynamic(element, null, () => resolveHtmlSlot(props[${JSON.stringify(directive.prop)}]))`)\n } else if (directive.kind === 'event') {\n statements.push(`addEventListener(element, ${JSON.stringify(directive.name)}, (event) => { const handler = props[${JSON.stringify(directive.prop)}]; if (typeof handler === 'function') handler(event); })`)\n } else if (directive.kind === 'attribute') {\n statements.push(`bindAttribute(element, ${JSON.stringify(directive.name)}, () => sanitizeHtmlAttribute(${JSON.stringify(directive.name)}, props[${JSON.stringify(directive.prop)}] ?? ''))`)\n } else if (directive.kind === 'property') {\n statements.push(`bindProperty(element, ${JSON.stringify(directive.name)}, () => props[${JSON.stringify(directive.prop)}])`)\n }\n continue\n }\n if (blockedAttributes.test(name)) throw new Error(`Vobs HTML component: 禁止使用危险属性 ${attribute.name} (${filename})`)\n if (urlAttributes.has(name) && !isSafeUrl(value)) {\n throw new Error(`Vobs HTML component: 禁止使用危险 URL 属性 ${attribute.name} (${filename})`)\n }\n statements.push(`setAttribute(element, ${JSON.stringify(attribute.name)}, ${JSON.stringify(value)})`)\n }\n if (dynamicText.length > 1) throw new Error(`Vobs HTML component: 一个元素只能使用一个 data-vobs-text 指令 (${filename})`)\n if (dynamicText.length === 1) {\n const text = 'createText(\"\")'\n statements.push(`const text = ${text}`)\n statements.push('insertBefore(element, text, null)')\n statements.push(`bindText(text, () => props[${JSON.stringify(dynamicText[0])}] ?? '')`)\n } else {\n for (const child of children) statements.push(`insertBefore(element, ${child}, null)`)\n }\n statements.push('return element')\n return [`(() => {${statements.join(';')};})()`]\n }\n\n return compileChildren(node as HtmlDocumentFragment, filename)\n}\n\nfunction isElement(node: HtmlNode): node is HtmlElement {\n return !node.nodeName.startsWith('#')\n}\n\nfunction isSafeUrl(value: string): boolean {\n const normalized = value.trim().toLowerCase()\n if (normalized.startsWith('#') || normalized.startsWith('/') || normalized.startsWith('./') || normalized.startsWith('../')) return true\n try {\n return safeProtocols.has(new URL(normalized, 'https://vobs.invalid/').protocol)\n } catch {\n return false\n }\n}\n\ntype HtmlDirective =\n | { readonly kind: 'text' | 'slot'; readonly prop: string }\n | { readonly kind: 'event' | 'attribute' | 'property'; readonly name: string; readonly prop: string }\n\nfunction parseDirective(name: string, value: string, filename: string): HtmlDirective | null {\n const match = directivePattern.exec(name)\n if (!match) return null\n const kind = match[1].toLowerCase()\n const namePart = match[2]\n const prop = value.trim()\n if (!prop || !/^[A-Za-z_$][\\w$]*$/u.test(prop)) {\n throw new Error(`Vobs HTML component: ${name} 必须引用有效的 props 名称 (${filename})`)\n }\n if (kind === 'text' || kind === 'slot') {\n if (namePart) throw new Error(`Vobs HTML component: ${name} 不接受额外名称 (${filename})`)\n return { kind, prop }\n }\n if (!namePart || !/^[a-z][a-z0-9:-]*$/iu.test(namePart)) {\n throw new Error(`Vobs HTML component: ${name} 必须包含有效名称 (${filename})`)\n }\n if (kind === 'on') return { kind: 'event', name: namePart.toLowerCase(), prop }\n if (kind === 'bind') {\n if (namePart.toLowerCase() === 'style') throw new Error(`Vobs HTML component: 不允许动态绑定 style (${filename})`)\n return { kind: 'attribute', name: namePart, prop }\n }\n if (!safePropertyNames.has(namePart.toLowerCase())) {\n throw new Error(`Vobs HTML component: 不允许动态绑定 property ${namePart} (${filename})`)\n }\n return { kind: 'property', name: normalizePropertyName(namePart), prop }\n}\n\nfunction normalizePropertyName(name: string): string {\n return name.toLowerCase() === 'readonly' ? 'readOnly' : name.toLowerCase() === 'tabindex' ? 'tabIndex' : name\n}\n"]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
interface HtmlComponentOptions {
|
|
2
|
+
readonly filename?: string;
|
|
3
|
+
}
|
|
4
|
+
/** Converts a trusted static HTML fragment to Vobs runtime node creation code. */
|
|
5
|
+
declare function compileHtmlComponent(source: string, options?: HtmlComponentOptions): string;
|
|
6
|
+
|
|
7
|
+
export { type HtmlComponentOptions, compileHtmlComponent };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
interface HtmlComponentOptions {
|
|
2
|
+
readonly filename?: string;
|
|
3
|
+
}
|
|
4
|
+
/** Converts a trusted static HTML fragment to Vobs runtime node creation code. */
|
|
5
|
+
declare function compileHtmlComponent(source: string, options?: HtmlComponentOptions): string;
|
|
6
|
+
|
|
7
|
+
export { type HtmlComponentOptions, compileHtmlComponent };
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { parseFragment } from 'parse5';
|
|
2
|
+
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
5
|
+
var blockedTags = /* @__PURE__ */ new Set(["script", "iframe", "object", "embed", "base", "meta", "link", "style"]);
|
|
6
|
+
var blockedAttributes = /^(?:on[a-z]+|srcdoc|style)$/iu;
|
|
7
|
+
var urlAttributes = /* @__PURE__ */ new Set(["href", "src", "action", "formaction", "poster", "xlink:href"]);
|
|
8
|
+
var safeProtocols = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:"]);
|
|
9
|
+
var directivePattern = /^data-vobs-(text|slot|on|bind|prop)(?:-(.+))?$/iu;
|
|
10
|
+
var safePropertyNames = /* @__PURE__ */ new Set([
|
|
11
|
+
"value",
|
|
12
|
+
"checked",
|
|
13
|
+
"selected",
|
|
14
|
+
"disabled",
|
|
15
|
+
"multiple",
|
|
16
|
+
"readonly",
|
|
17
|
+
"required",
|
|
18
|
+
"autofocus",
|
|
19
|
+
"hidden",
|
|
20
|
+
"tabindex"
|
|
21
|
+
]);
|
|
22
|
+
function compileHtmlComponent(source, options = {}) {
|
|
23
|
+
const fragment = parseFragment(source);
|
|
24
|
+
const body = compileChildren(fragment, options.filename ?? "component.html");
|
|
25
|
+
const result = body.length === 1 ? body[0] : `createFragment((parent, anchor) => {${body.map((code) => `insertBefore(parent, ${code}, anchor);`).join("")}})`;
|
|
26
|
+
return `import { addEventListener, bindAttribute, bindProperty, bindText, createElement, createFragment, createText, insertBefore, insertDynamic, setAttribute } from '@vobs/vobs';
|
|
27
|
+
function resolveHtmlSlot(value) { const resolved = typeof value === 'function' ? value() : value; if (resolved === undefined || resolved === null || resolved === false) return null; if (typeof resolved === 'string' || typeof resolved === 'number') return createText(String(resolved)); if (Array.isArray(resolved)) return createFragment((parent, anchor) => { for (const item of resolved) { const child = resolveHtmlSlot(item); if (child) insertBefore(parent, child, anchor); } }); return resolved; }
|
|
28
|
+
function sanitizeHtmlAttribute(name, value) { const stringValue = String(value ?? ''); if (!['href', 'src', 'action', 'formaction', 'poster', 'xlink:href'].includes(name.toLowerCase())) return stringValue; const normalized = stringValue.trim().toLowerCase(); if (normalized.startsWith('#') || normalized.startsWith('/') || normalized.startsWith('./') || normalized.startsWith('../')) return stringValue; try { const protocol = new URL(normalized, 'https://vobs.invalid/').protocol; return ['http:', 'https:', 'mailto:', 'tel:'].includes(protocol) ? stringValue : ''; } catch { return ''; } }
|
|
29
|
+
export default function HtmlComponent(props = {}) { return ${result ?? "createFragment(() => {})"}; }`;
|
|
30
|
+
}
|
|
31
|
+
__name(compileHtmlComponent, "compileHtmlComponent");
|
|
32
|
+
function compileChildren(parent, filename) {
|
|
33
|
+
return parent.childNodes.flatMap((node) => compileNode(node, filename));
|
|
34
|
+
}
|
|
35
|
+
__name(compileChildren, "compileChildren");
|
|
36
|
+
function compileNode(node, filename) {
|
|
37
|
+
if (node.nodeName === "#text") {
|
|
38
|
+
const value = node.value;
|
|
39
|
+
return value ? [`createText(${JSON.stringify(value)})`] : [];
|
|
40
|
+
}
|
|
41
|
+
if (node.nodeName === "#comment") return [];
|
|
42
|
+
if (node.nodeName !== "#document-fragment" && !isElement(node)) return [];
|
|
43
|
+
if (isElement(node)) {
|
|
44
|
+
const tag = node.tagName.toLowerCase();
|
|
45
|
+
if (blockedTags.has(tag)) throw new Error(`Vobs HTML component: \u7981\u6B62\u4F7F\u7528 <${tag}> (${filename})`);
|
|
46
|
+
const children = compileChildren(node, filename);
|
|
47
|
+
const statements = [`const element = createElement(${JSON.stringify(tag)})`];
|
|
48
|
+
const dynamicText = [];
|
|
49
|
+
for (const attribute of node.attrs) {
|
|
50
|
+
const name = attribute.name.toLowerCase();
|
|
51
|
+
const value = attribute.value;
|
|
52
|
+
const directive = parseDirective(name, value, filename);
|
|
53
|
+
if (directive) {
|
|
54
|
+
if (directive.kind === "text") {
|
|
55
|
+
dynamicText.push(directive.prop);
|
|
56
|
+
} else if (directive.kind === "slot") {
|
|
57
|
+
statements.push(`insertDynamic(element, null, () => resolveHtmlSlot(props[${JSON.stringify(directive.prop)}]))`);
|
|
58
|
+
} else if (directive.kind === "event") {
|
|
59
|
+
statements.push(`addEventListener(element, ${JSON.stringify(directive.name)}, (event) => { const handler = props[${JSON.stringify(directive.prop)}]; if (typeof handler === 'function') handler(event); })`);
|
|
60
|
+
} else if (directive.kind === "attribute") {
|
|
61
|
+
statements.push(`bindAttribute(element, ${JSON.stringify(directive.name)}, () => sanitizeHtmlAttribute(${JSON.stringify(directive.name)}, props[${JSON.stringify(directive.prop)}] ?? ''))`);
|
|
62
|
+
} else if (directive.kind === "property") {
|
|
63
|
+
statements.push(`bindProperty(element, ${JSON.stringify(directive.name)}, () => props[${JSON.stringify(directive.prop)}])`);
|
|
64
|
+
}
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (blockedAttributes.test(name)) throw new Error(`Vobs HTML component: \u7981\u6B62\u4F7F\u7528\u5371\u9669\u5C5E\u6027 ${attribute.name} (${filename})`);
|
|
68
|
+
if (urlAttributes.has(name) && !isSafeUrl(value)) {
|
|
69
|
+
throw new Error(`Vobs HTML component: \u7981\u6B62\u4F7F\u7528\u5371\u9669 URL \u5C5E\u6027 ${attribute.name} (${filename})`);
|
|
70
|
+
}
|
|
71
|
+
statements.push(`setAttribute(element, ${JSON.stringify(attribute.name)}, ${JSON.stringify(value)})`);
|
|
72
|
+
}
|
|
73
|
+
if (dynamicText.length > 1) throw new Error(`Vobs HTML component: \u4E00\u4E2A\u5143\u7D20\u53EA\u80FD\u4F7F\u7528\u4E00\u4E2A data-vobs-text \u6307\u4EE4 (${filename})`);
|
|
74
|
+
if (dynamicText.length === 1) {
|
|
75
|
+
const text = 'createText("")';
|
|
76
|
+
statements.push(`const text = ${text}`);
|
|
77
|
+
statements.push("insertBefore(element, text, null)");
|
|
78
|
+
statements.push(`bindText(text, () => props[${JSON.stringify(dynamicText[0])}] ?? '')`);
|
|
79
|
+
} else {
|
|
80
|
+
for (const child of children) statements.push(`insertBefore(element, ${child}, null)`);
|
|
81
|
+
}
|
|
82
|
+
statements.push("return element");
|
|
83
|
+
return [`(() => {${statements.join(";")};})()`];
|
|
84
|
+
}
|
|
85
|
+
return compileChildren(node, filename);
|
|
86
|
+
}
|
|
87
|
+
__name(compileNode, "compileNode");
|
|
88
|
+
function isElement(node) {
|
|
89
|
+
return !node.nodeName.startsWith("#");
|
|
90
|
+
}
|
|
91
|
+
__name(isElement, "isElement");
|
|
92
|
+
function isSafeUrl(value) {
|
|
93
|
+
const normalized = value.trim().toLowerCase();
|
|
94
|
+
if (normalized.startsWith("#") || normalized.startsWith("/") || normalized.startsWith("./") || normalized.startsWith("../")) return true;
|
|
95
|
+
try {
|
|
96
|
+
return safeProtocols.has(new URL(normalized, "https://vobs.invalid/").protocol);
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
__name(isSafeUrl, "isSafeUrl");
|
|
102
|
+
function parseDirective(name, value, filename) {
|
|
103
|
+
const match = directivePattern.exec(name);
|
|
104
|
+
if (!match) return null;
|
|
105
|
+
const kind = match[1].toLowerCase();
|
|
106
|
+
const namePart = match[2];
|
|
107
|
+
const prop = value.trim();
|
|
108
|
+
if (!prop || !/^[A-Za-z_$][\w$]*$/u.test(prop)) {
|
|
109
|
+
throw new Error(`Vobs HTML component: ${name} \u5FC5\u987B\u5F15\u7528\u6709\u6548\u7684 props \u540D\u79F0 (${filename})`);
|
|
110
|
+
}
|
|
111
|
+
if (kind === "text" || kind === "slot") {
|
|
112
|
+
if (namePart) throw new Error(`Vobs HTML component: ${name} \u4E0D\u63A5\u53D7\u989D\u5916\u540D\u79F0 (${filename})`);
|
|
113
|
+
return { kind, prop };
|
|
114
|
+
}
|
|
115
|
+
if (!namePart || !/^[a-z][a-z0-9:-]*$/iu.test(namePart)) {
|
|
116
|
+
throw new Error(`Vobs HTML component: ${name} \u5FC5\u987B\u5305\u542B\u6709\u6548\u540D\u79F0 (${filename})`);
|
|
117
|
+
}
|
|
118
|
+
if (kind === "on") return { kind: "event", name: namePart.toLowerCase(), prop };
|
|
119
|
+
if (kind === "bind") {
|
|
120
|
+
if (namePart.toLowerCase() === "style") throw new Error(`Vobs HTML component: \u4E0D\u5141\u8BB8\u52A8\u6001\u7ED1\u5B9A style (${filename})`);
|
|
121
|
+
return { kind: "attribute", name: namePart, prop };
|
|
122
|
+
}
|
|
123
|
+
if (!safePropertyNames.has(namePart.toLowerCase())) {
|
|
124
|
+
throw new Error(`Vobs HTML component: \u4E0D\u5141\u8BB8\u52A8\u6001\u7ED1\u5B9A property ${namePart} (${filename})`);
|
|
125
|
+
}
|
|
126
|
+
return { kind: "property", name: normalizePropertyName(namePart), prop };
|
|
127
|
+
}
|
|
128
|
+
__name(parseDirective, "parseDirective");
|
|
129
|
+
function normalizePropertyName(name) {
|
|
130
|
+
return name.toLowerCase() === "readonly" ? "readOnly" : name.toLowerCase() === "tabindex" ? "tabIndex" : name;
|
|
131
|
+
}
|
|
132
|
+
__name(normalizePropertyName, "normalizePropertyName");
|
|
133
|
+
|
|
134
|
+
export { compileHtmlComponent };
|
|
135
|
+
//# sourceMappingURL=html-component.js.map
|
|
136
|
+
//# sourceMappingURL=html-component.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/html-component.ts"],"names":[],"mappings":"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","file":"html-component.js","sourcesContent":["import { parseFragment, type DefaultTreeAdapterMap, type DefaultTreeAdapterTypes } from 'parse5'\n\ntype HtmlNode = DefaultTreeAdapterTypes.Node\ntype HtmlDocumentFragment = DefaultTreeAdapterTypes.DocumentFragment\ntype HtmlElement = DefaultTreeAdapterTypes.Element\n\nconst blockedTags = new Set(['script', 'iframe', 'object', 'embed', 'base', 'meta', 'link', 'style'])\nconst blockedAttributes = /^(?:on[a-z]+|srcdoc|style)$/iu\nconst urlAttributes = new Set(['href', 'src', 'action', 'formaction', 'poster', 'xlink:href'])\nconst safeProtocols = new Set(['http:', 'https:', 'mailto:', 'tel:'])\nconst directivePattern = /^data-vobs-(text|slot|on|bind|prop)(?:-(.+))?$/iu\nconst safePropertyNames = new Set([\n 'value', 'checked', 'selected', 'disabled', 'multiple', 'readonly', 'required',\n 'autofocus', 'hidden', 'tabindex'\n])\n\nexport interface HtmlComponentOptions {\n readonly filename?: string\n}\n\n/** Converts a trusted static HTML fragment to Vobs runtime node creation code. */\nexport function compileHtmlComponent(source: string, options: HtmlComponentOptions = {}): string {\n const fragment = parseFragment(source)\n const body = compileChildren(fragment, options.filename ?? 'component.html')\n const result = body.length === 1 ? body[0] : `createFragment((parent, anchor) => {${body.map(code => `insertBefore(parent, ${code}, anchor);`).join('')}})`\n\n return `import { addEventListener, bindAttribute, bindProperty, bindText, createElement, createFragment, createText, insertBefore, insertDynamic, setAttribute } from '@vobs/vobs';\\nfunction resolveHtmlSlot(value) { const resolved = typeof value === 'function' ? value() : value; if (resolved === undefined || resolved === null || resolved === false) return null; if (typeof resolved === 'string' || typeof resolved === 'number') return createText(String(resolved)); if (Array.isArray(resolved)) return createFragment((parent, anchor) => { for (const item of resolved) { const child = resolveHtmlSlot(item); if (child) insertBefore(parent, child, anchor); } }); return resolved; }\\nfunction sanitizeHtmlAttribute(name, value) { const stringValue = String(value ?? ''); if (!['href', 'src', 'action', 'formaction', 'poster', 'xlink:href'].includes(name.toLowerCase())) return stringValue; const normalized = stringValue.trim().toLowerCase(); if (normalized.startsWith('#') || normalized.startsWith('/') || normalized.startsWith('./') || normalized.startsWith('../')) return stringValue; try { const protocol = new URL(normalized, 'https://vobs.invalid/').protocol; return ['http:', 'https:', 'mailto:', 'tel:'].includes(protocol) ? stringValue : ''; } catch { return ''; } }\\nexport default function HtmlComponent(props = {}) { return ${result ?? \"createFragment(() => {})\"}; }`\n}\n\nfunction compileChildren(parent: HtmlDocumentFragment | HtmlElement, filename: string): string[] {\n return parent.childNodes.flatMap(node => compileNode(node, filename))\n}\n\nfunction compileNode(node: HtmlNode, filename: string): string[] {\n if (node.nodeName === '#text') {\n const value = (node as DefaultTreeAdapterMap['textNode']).value\n return value ? [`createText(${JSON.stringify(value)})`] : []\n }\n\n if (node.nodeName === '#comment') return []\n if (node.nodeName !== '#document-fragment' && !isElement(node)) return []\n\n if (isElement(node)) {\n const tag = node.tagName.toLowerCase()\n if (blockedTags.has(tag)) throw new Error(`Vobs HTML component: 禁止使用 <${tag}> (${filename})`)\n\n const children = compileChildren(node, filename)\n const statements = [`const element = createElement(${JSON.stringify(tag)})`]\n const dynamicText = [] as string[]\n for (const attribute of node.attrs) {\n const name = attribute.name.toLowerCase()\n const value = attribute.value\n const directive = parseDirective(name, value, filename)\n if (directive) {\n if (directive.kind === 'text') {\n dynamicText.push(directive.prop)\n } else if (directive.kind === 'slot') {\n statements.push(`insertDynamic(element, null, () => resolveHtmlSlot(props[${JSON.stringify(directive.prop)}]))`)\n } else if (directive.kind === 'event') {\n statements.push(`addEventListener(element, ${JSON.stringify(directive.name)}, (event) => { const handler = props[${JSON.stringify(directive.prop)}]; if (typeof handler === 'function') handler(event); })`)\n } else if (directive.kind === 'attribute') {\n statements.push(`bindAttribute(element, ${JSON.stringify(directive.name)}, () => sanitizeHtmlAttribute(${JSON.stringify(directive.name)}, props[${JSON.stringify(directive.prop)}] ?? ''))`)\n } else if (directive.kind === 'property') {\n statements.push(`bindProperty(element, ${JSON.stringify(directive.name)}, () => props[${JSON.stringify(directive.prop)}])`)\n }\n continue\n }\n if (blockedAttributes.test(name)) throw new Error(`Vobs HTML component: 禁止使用危险属性 ${attribute.name} (${filename})`)\n if (urlAttributes.has(name) && !isSafeUrl(value)) {\n throw new Error(`Vobs HTML component: 禁止使用危险 URL 属性 ${attribute.name} (${filename})`)\n }\n statements.push(`setAttribute(element, ${JSON.stringify(attribute.name)}, ${JSON.stringify(value)})`)\n }\n if (dynamicText.length > 1) throw new Error(`Vobs HTML component: 一个元素只能使用一个 data-vobs-text 指令 (${filename})`)\n if (dynamicText.length === 1) {\n const text = 'createText(\"\")'\n statements.push(`const text = ${text}`)\n statements.push('insertBefore(element, text, null)')\n statements.push(`bindText(text, () => props[${JSON.stringify(dynamicText[0])}] ?? '')`)\n } else {\n for (const child of children) statements.push(`insertBefore(element, ${child}, null)`)\n }\n statements.push('return element')\n return [`(() => {${statements.join(';')};})()`]\n }\n\n return compileChildren(node as HtmlDocumentFragment, filename)\n}\n\nfunction isElement(node: HtmlNode): node is HtmlElement {\n return !node.nodeName.startsWith('#')\n}\n\nfunction isSafeUrl(value: string): boolean {\n const normalized = value.trim().toLowerCase()\n if (normalized.startsWith('#') || normalized.startsWith('/') || normalized.startsWith('./') || normalized.startsWith('../')) return true\n try {\n return safeProtocols.has(new URL(normalized, 'https://vobs.invalid/').protocol)\n } catch {\n return false\n }\n}\n\ntype HtmlDirective =\n | { readonly kind: 'text' | 'slot'; readonly prop: string }\n | { readonly kind: 'event' | 'attribute' | 'property'; readonly name: string; readonly prop: string }\n\nfunction parseDirective(name: string, value: string, filename: string): HtmlDirective | null {\n const match = directivePattern.exec(name)\n if (!match) return null\n const kind = match[1].toLowerCase()\n const namePart = match[2]\n const prop = value.trim()\n if (!prop || !/^[A-Za-z_$][\\w$]*$/u.test(prop)) {\n throw new Error(`Vobs HTML component: ${name} 必须引用有效的 props 名称 (${filename})`)\n }\n if (kind === 'text' || kind === 'slot') {\n if (namePart) throw new Error(`Vobs HTML component: ${name} 不接受额外名称 (${filename})`)\n return { kind, prop }\n }\n if (!namePart || !/^[a-z][a-z0-9:-]*$/iu.test(namePart)) {\n throw new Error(`Vobs HTML component: ${name} 必须包含有效名称 (${filename})`)\n }\n if (kind === 'on') return { kind: 'event', name: namePart.toLowerCase(), prop }\n if (kind === 'bind') {\n if (namePart.toLowerCase() === 'style') throw new Error(`Vobs HTML component: 不允许动态绑定 style (${filename})`)\n return { kind: 'attribute', name: namePart, prop }\n }\n if (!safePropertyNames.has(namePart.toLowerCase())) {\n throw new Error(`Vobs HTML component: 不允许动态绑定 property ${namePart} (${filename})`)\n }\n return { kind: 'property', name: normalizePropertyName(namePart), prop }\n}\n\nfunction normalizePropertyName(name: string): string {\n return name.toLowerCase() === 'readonly' ? 'readOnly' : name.toLowerCase() === 'tabindex' ? 'tabIndex' : name\n}\n"]}
|