@vobs/vite-plugin 1.2.1 → 1.3.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 +31 -8
- package/dist/html-component.cjs.map +1 -1
- package/dist/html-component.js +6 -5
- package/dist/html-component.js.map +1 -1
- package/dist/index.cjs +78 -1161
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +38 -1148
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/index.test.ts +48 -0
- package/src/index.ts +38 -4
package/dist/html-component.cjs
CHANGED
|
@@ -1,9 +1,31 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
var parse5 = require('parse5');
|
|
4
|
-
|
|
1
|
+
var __VOBS_CJS_FILE_URL = require("url").pathToFileURL(__filename).href;
|
|
2
|
+
"use strict";
|
|
5
3
|
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
7
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
21
|
+
|
|
22
|
+
// packages/vite-plugin/src/html-component.ts
|
|
23
|
+
var html_component_exports = {};
|
|
24
|
+
__export(html_component_exports, {
|
|
25
|
+
compileHtmlComponent: () => compileHtmlComponent
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(html_component_exports);
|
|
28
|
+
var import_parse5 = require("parse5");
|
|
7
29
|
var blockedTags = /* @__PURE__ */ new Set(["script", "iframe", "object", "embed", "base", "meta", "link", "style"]);
|
|
8
30
|
var blockedAttributes = /^(?:on[a-z]+|srcdoc|style)$/iu;
|
|
9
31
|
var urlAttributes = /* @__PURE__ */ new Set(["href", "src", "action", "formaction", "poster", "xlink:href"]);
|
|
@@ -22,7 +44,7 @@ var safePropertyNames = /* @__PURE__ */ new Set([
|
|
|
22
44
|
"tabindex"
|
|
23
45
|
]);
|
|
24
46
|
function compileHtmlComponent(source, options = {}) {
|
|
25
|
-
const fragment =
|
|
47
|
+
const fragment = (0, import_parse5.parseFragment)(source);
|
|
26
48
|
const body = compileChildren(fragment, options.filename ?? "component.html");
|
|
27
49
|
const result = body.length === 1 ? body[0] : `createFragment((parent, anchor) => {${body.map((code) => `insertBefore(parent, ${code}, anchor);`).join("")}})`;
|
|
28
50
|
return `import { addEventListener, bindAttribute, bindProperty, bindText, createElement, createFragment, createText, insertBefore, insertDynamic, setAttribute } from '@vobs/vobs';
|
|
@@ -132,7 +154,8 @@ function normalizePropertyName(name) {
|
|
|
132
154
|
return name.toLowerCase() === "readonly" ? "readOnly" : name.toLowerCase() === "tabindex" ? "tabIndex" : name;
|
|
133
155
|
}
|
|
134
156
|
__name(normalizePropertyName, "normalizePropertyName");
|
|
135
|
-
|
|
136
|
-
exports
|
|
137
|
-
|
|
157
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
158
|
+
0 && (module.exports = {
|
|
159
|
+
compileHtmlComponent
|
|
160
|
+
});
|
|
138
161
|
//# sourceMappingURL=html-component.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/html-component.ts"],"names":["parseFragment"],"mappings":";;;;;;AAMA,IAAM,WAAA,mBAAc,IAAI,GAAA,CAAI,CAAC,QAAA,EAAU,QAAA,EAAU,QAAA,EAAU,OAAA,EAAS,MAAA,EAAQ,MAAA,EAAQ,MAAA,EAAQ,OAAO,CAAC,CAAA;AACpG,IAAM,iBAAA,GAAoB,+BAAA;AAC1B,IAAM,aAAA,mBAAgB,IAAI,GAAA,CAAI,CAAC,MAAA,EAAQ,OAAO,QAAA,EAAU,YAAA,EAAc,QAAA,EAAU,YAAY,CAAC,CAAA;AAC7F,IAAM,aAAA,uBAAoB,GAAA,CAAI,CAAC,SAAS,QAAA,EAAU,SAAA,EAAW,MAAM,CAAC,CAAA;AACpE,IAAM,gBAAA,GAAmB,kDAAA;AACzB,IAAM,iBAAA,uBAAwB,GAAA,CAAI;AAAA,EAChC,OAAA;AAAA,EAAS,SAAA;AAAA,EAAW,UAAA;AAAA,EAAY,UAAA;AAAA,EAAY,UAAA;AAAA,EAAY,UAAA;AAAA,EAAY,UAAA;AAAA,EACpE,WAAA;AAAA,EAAa,QAAA;AAAA,EAAU;AACzB,CAAC,CAAA;AAOM,SAAS,oBAAA,CAAqB,MAAA,EAAgB,OAAA,GAAgC,EAAC,EAAW;AAC/F,EAAA,MAAM,QAAA,GAAWA,qBAAc,MAAM,CAAA;AACrC,EAAA,MAAM,IAAA,GAAO,eAAA,CAAgB,QAAA,EAAU,OAAA,CAAQ,YAAY,gBAAgB,CAAA;AAC3E,EAAA,MAAM,SAAS,IAAA,CAAK,MAAA,KAAW,CAAA,GAAI,IAAA,CAAK,CAAC,CAAA,GAAI,CAAA,oCAAA,EAAuC,IAAA,CAAK,GAAA,CAAI,UAAQ,CAAA,qBAAA,EAAwB,IAAI,YAAY,CAAA,CAAE,IAAA,CAAK,EAAE,CAAC,CAAA,EAAA,CAAA;AAEvJ,EAAA,OAAO,CAAA;AAAA;AAAA;AAAA,2DAAA,EAAgzC,UAAU,0BAA0B,CAAA,GAAA,CAAA;AAC71C;AANgB,MAAA,CAAA,oBAAA,EAAA,sBAAA,CAAA;AAQhB,SAAS,eAAA,CAAgB,QAA4C,QAAA,EAA4B;AAC/F,EAAA,OAAO,OAAO,UAAA,CAAW,OAAA,CAAQ,UAAQ,WAAA,CAAY,IAAA,EAAM,QAAQ,CAAC,CAAA;AACtE;AAFS,MAAA,CAAA,eAAA,EAAA,iBAAA,CAAA;AAIT,SAAS,WAAA,CAAY,MAAgB,QAAA,EAA4B;AAC/D,EAAA,IAAI,IAAA,CAAK,aAAa,OAAA,EAAS;AAC7B,IAAA,MAAM,QAAS,IAAA,CAA2C,KAAA;AAC1D,IAAA,OAAO,KAAA,GAAQ,CAAC,CAAA,WAAA,EAAc,IAAA,CAAK,UAAU,KAAK,CAAC,CAAA,CAAA,CAAG,CAAA,GAAI,EAAC;AAAA,EAC7D;AAEA,EAAA,IAAI,IAAA,CAAK,QAAA,KAAa,UAAA,EAAY,OAAO,EAAC;AAC1C,EAAA,IAAI,IAAA,CAAK,aAAa,oBAAA,IAAwB,CAAC,UAAU,IAAI,CAAA,SAAU,EAAC;AAExE,EAAA,IAAI,SAAA,CAAU,IAAI,CAAA,EAAG;AACnB,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,OAAA,CAAQ,WAAA,EAAY;AACrC,IAAA,IAAI,WAAA,CAAY,GAAA,CAAI,GAAG,CAAA,EAAG,MAAM,IAAI,KAAA,CAAM,CAAA,+CAAA,EAA8B,GAAG,CAAA,GAAA,EAAM,QAAQ,CAAA,CAAA,CAAG,CAAA;AAE5F,IAAA,MAAM,QAAA,GAAW,eAAA,CAAgB,IAAA,EAAM,QAAQ,CAAA;AAC/C,IAAA,MAAM,aAAa,CAAC,CAAA,8BAAA,EAAiC,KAAK,SAAA,CAAU,GAAG,CAAC,CAAA,CAAA,CAAG,CAAA;AAC3E,IAAA,MAAM,cAAc,EAAC;AACrB,IAAA,KAAA,MAAW,SAAA,IAAa,KAAK,KAAA,EAAO;AAClC,MAAA,MAAM,IAAA,GAAO,SAAA,CAAU,IAAA,CAAK,WAAA,EAAY;AACxC,MAAA,MAAM,QAAQ,SAAA,CAAU,KAAA;AACxB,MAAA,MAAM,SAAA,GAAY,cAAA,CAAe,IAAA,EAAM,KAAA,EAAO,QAAQ,CAAA;AACtD,MAAA,IAAI,SAAA,EAAW;AACb,QAAA,IAAI,SAAA,CAAU,SAAS,MAAA,EAAQ;AAC7B,UAAA,WAAA,CAAY,IAAA,CAAK,UAAU,IAAI,CAAA;AAAA,QACjC,CAAA,MAAA,IAAW,SAAA,CAAU,IAAA,KAAS,MAAA,EAAQ;AACpC,UAAA,UAAA,CAAW,KAAK,CAAA,yDAAA,EAA4D,IAAA,CAAK,UAAU,SAAA,CAAU,IAAI,CAAC,CAAA,GAAA,CAAK,CAAA;AAAA,QACjH,CAAA,MAAA,IAAW,SAAA,CAAU,IAAA,KAAS,OAAA,EAAS;AACrC,UAAA,UAAA,CAAW,IAAA,CAAK,CAAA,0BAAA,EAA6B,IAAA,CAAK,SAAA,CAAU,SAAA,CAAU,IAAI,CAAC,CAAA,qCAAA,EAAwC,IAAA,CAAK,SAAA,CAAU,SAAA,CAAU,IAAI,CAAC,CAAA,wDAAA,CAA0D,CAAA;AAAA,QAC7M,CAAA,MAAA,IAAW,SAAA,CAAU,IAAA,KAAS,WAAA,EAAa;AACzC,UAAA,UAAA,CAAW,KAAK,CAAA,uBAAA,EAA0B,IAAA,CAAK,UAAU,SAAA,CAAU,IAAI,CAAC,CAAA,8BAAA,EAAiC,IAAA,CAAK,UAAU,SAAA,CAAU,IAAI,CAAC,CAAA,QAAA,EAAW,IAAA,CAAK,UAAU,SAAA,CAAU,IAAI,CAAC,CAAA,SAAA,CAAW,CAAA;AAAA,QAC7L,CAAA,MAAA,IAAW,SAAA,CAAU,IAAA,KAAS,UAAA,EAAY;AACxC,UAAA,UAAA,CAAW,IAAA,CAAK,CAAA,sBAAA,EAAyB,IAAA,CAAK,SAAA,CAAU,SAAA,CAAU,IAAI,CAAC,CAAA,cAAA,EAAiB,IAAA,CAAK,SAAA,CAAU,SAAA,CAAU,IAAI,CAAC,CAAA,EAAA,CAAI,CAAA;AAAA,QAC5H;AACA,QAAA;AAAA,MACF;AACA,MAAA,IAAI,iBAAA,CAAkB,IAAA,CAAK,IAAI,CAAA,EAAG,MAAM,IAAI,KAAA,CAAM,CAAA,sEAAA,EAAiC,SAAA,CAAU,IAAI,CAAA,EAAA,EAAK,QAAQ,CAAA,CAAA,CAAG,CAAA;AACjH,MAAA,IAAI,cAAc,GAAA,CAAI,IAAI,KAAK,CAAC,SAAA,CAAU,KAAK,CAAA,EAAG;AAChD,QAAA,MAAM,IAAI,KAAA,CAAM,CAAA,2EAAA,EAAsC,UAAU,IAAI,CAAA,EAAA,EAAK,QAAQ,CAAA,CAAA,CAAG,CAAA;AAAA,MACtF;AACA,MAAA,UAAA,CAAW,IAAA,CAAK,CAAA,sBAAA,EAAyB,IAAA,CAAK,SAAA,CAAU,SAAA,CAAU,IAAI,CAAC,CAAA,EAAA,EAAK,IAAA,CAAK,SAAA,CAAU,KAAK,CAAC,CAAA,CAAA,CAAG,CAAA;AAAA,IACtG;AACA,IAAA,IAAI,WAAA,CAAY,SAAS,CAAA,EAAG,MAAM,IAAI,KAAA,CAAM,CAAA,+GAAA,EAAsD,QAAQ,CAAA,CAAA,CAAG,CAAA;AAC7G,IAAA,IAAI,WAAA,CAAY,WAAW,CAAA,EAAG;AAC5B,MAAA,MAAM,IAAA,GAAO,gBAAA;AACb,MAAA,UAAA,CAAW,IAAA,CAAK,CAAA,aAAA,EAAgB,IAAI,CAAA,CAAE,CAAA;AACtC,MAAA,UAAA,CAAW,KAAK,mCAAmC,CAAA;AACnD,MAAA,UAAA,CAAW,IAAA,CAAK,8BAA8B,IAAA,CAAK,SAAA,CAAU,YAAY,CAAC,CAAC,CAAC,CAAA,QAAA,CAAU,CAAA;AAAA,IACxF,CAAA,MAAO;AACL,MAAA,KAAA,MAAW,SAAS,QAAA,EAAU,UAAA,CAAW,IAAA,CAAK,CAAA,sBAAA,EAAyB,KAAK,CAAA,OAAA,CAAS,CAAA;AAAA,IACvF;AACA,IAAA,UAAA,CAAW,KAAK,gBAAgB,CAAA;AAChC,IAAA,OAAO,CAAC,CAAA,QAAA,EAAW,UAAA,CAAW,IAAA,CAAK,GAAG,CAAC,CAAA,KAAA,CAAO,CAAA;AAAA,EAChD;AAEA,EAAA,OAAO,eAAA,CAAgB,MAA8B,QAAQ,CAAA;AAC/D;AAtDS,MAAA,CAAA,WAAA,EAAA,aAAA,CAAA;AAwDT,SAAS,UAAU,IAAA,EAAqC;AACtD,EAAA,OAAO,CAAC,IAAA,CAAK,QAAA,CAAS,UAAA,CAAW,GAAG,CAAA;AACtC;AAFS,MAAA,CAAA,SAAA,EAAA,WAAA,CAAA;AAIT,SAAS,UAAU,KAAA,EAAwB;AACzC,EAAA,MAAM,UAAA,GAAa,KAAA,CAAM,IAAA,EAAK,CAAE,WAAA,EAAY;AAC5C,EAAA,IAAI,WAAW,UAAA,CAAW,GAAG,CAAA,IAAK,UAAA,CAAW,WAAW,GAAG,CAAA,IAAK,UAAA,CAAW,UAAA,CAAW,IAAI,CAAA,IAAK,UAAA,CAAW,UAAA,CAAW,KAAK,GAAG,OAAO,IAAA;AACpI,EAAA,IAAI;AACF,IAAA,OAAO,cAAc,GAAA,CAAI,IAAI,IAAI,UAAA,EAAY,uBAAuB,EAAE,QAAQ,CAAA;AAAA,EAChF,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACF;AARS,MAAA,CAAA,SAAA,EAAA,WAAA,CAAA;AAcT,SAAS,cAAA,CAAe,IAAA,EAAc,KAAA,EAAe,QAAA,EAAwC;AAC3F,EAAA,MAAM,KAAA,GAAQ,gBAAA,CAAiB,IAAA,CAAK,IAAI,CAAA;AACxC,EAAA,IAAI,CAAC,OAAO,OAAO,IAAA;AACnB,EAAA,MAAM,IAAA,GAAO,KAAA,CAAM,CAAC,CAAA,CAAE,WAAA,EAAY;AAClC,EAAA,MAAM,QAAA,GAAW,MAAM,CAAC,CAAA;AACxB,EAAA,MAAM,IAAA,GAAO,MAAM,IAAA,EAAK;AACxB,EAAA,IAAI,CAAC,IAAA,IAAQ,CAAC,qBAAA,CAAsB,IAAA,CAAK,IAAI,CAAA,EAAG;AAC9C,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,qBAAA,EAAwB,IAAI,CAAA,gEAAA,EAAsB,QAAQ,CAAA,CAAA,CAAG,CAAA;AAAA,EAC/E;AACA,EAAA,IAAI,IAAA,KAAS,MAAA,IAAU,IAAA,KAAS,MAAA,EAAQ;AACtC,IAAA,IAAI,QAAA,QAAgB,IAAI,KAAA,CAAM,wBAAwB,IAAI,CAAA,6CAAA,EAAa,QAAQ,CAAA,CAAA,CAAG,CAAA;AAClF,IAAA,OAAO,EAAE,MAAM,IAAA,EAAK;AAAA,EACtB;AACA,EAAA,IAAI,CAAC,QAAA,IAAY,CAAC,sBAAA,CAAuB,IAAA,CAAK,QAAQ,CAAA,EAAG;AACvD,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,qBAAA,EAAwB,IAAI,CAAA,mDAAA,EAAc,QAAQ,CAAA,CAAA,CAAG,CAAA;AAAA,EACvE;AACA,EAAA,IAAI,IAAA,KAAS,IAAA,EAAM,OAAO,EAAE,IAAA,EAAM,SAAS,IAAA,EAAM,QAAA,CAAS,WAAA,EAAY,EAAG,IAAA,EAAK;AAC9E,EAAA,IAAI,SAAS,MAAA,EAAQ;AACnB,IAAA,IAAI,QAAA,CAAS,aAAY,KAAM,OAAA,QAAe,IAAI,KAAA,CAAM,CAAA,uEAAA,EAAuC,QAAQ,CAAA,CAAA,CAAG,CAAA;AAC1G,IAAA,OAAO,EAAE,IAAA,EAAM,WAAA,EAAa,IAAA,EAAM,UAAU,IAAA,EAAK;AAAA,EACnD;AACA,EAAA,IAAI,CAAC,iBAAA,CAAkB,GAAA,CAAI,QAAA,CAAS,WAAA,EAAa,CAAA,EAAG;AAClD,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,yEAAA,EAAyC,QAAQ,CAAA,EAAA,EAAK,QAAQ,CAAA,CAAA,CAAG,CAAA;AAAA,EACnF;AACA,EAAA,OAAO,EAAE,IAAA,EAAM,UAAA,EAAY,MAAM,qBAAA,CAAsB,QAAQ,GAAG,IAAA,EAAK;AACzE;AAzBS,MAAA,CAAA,cAAA,EAAA,gBAAA,CAAA;AA2BT,SAAS,sBAAsB,IAAA,EAAsB;AACnD,EAAA,OAAO,IAAA,CAAK,aAAY,KAAM,UAAA,GAAa,aAAa,IAAA,CAAK,WAAA,EAAY,KAAM,UAAA,GAAa,UAAA,GAAa,IAAA;AAC3G;AAFS,MAAA,CAAA,qBAAA,EAAA,uBAAA,CAAA","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"]}
|
|
1
|
+
{"version":3,"sources":["../src/html-component.ts"],"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"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAAwF;AAMxF,IAAM,cAAc,oBAAI,IAAI,CAAC,UAAU,UAAU,UAAU,SAAS,QAAQ,QAAQ,QAAQ,OAAO,CAAC;AACpG,IAAM,oBAAoB;AAC1B,IAAM,gBAAgB,oBAAI,IAAI,CAAC,QAAQ,OAAO,UAAU,cAAc,UAAU,YAAY,CAAC;AAC7F,IAAM,gBAAgB,oBAAI,IAAI,CAAC,SAAS,UAAU,WAAW,MAAM,CAAC;AACpE,IAAM,mBAAmB;AACzB,IAAM,oBAAoB,oBAAI,IAAI;AAAA,EAChC;AAAA,EAAS;AAAA,EAAW;AAAA,EAAY;AAAA,EAAY;AAAA,EAAY;AAAA,EAAY;AAAA,EACpE;AAAA,EAAa;AAAA,EAAU;AACzB,CAAC;AAOM,SAAS,qBAAqB,QAAgB,UAAgC,CAAC,GAAW;AAC/F,QAAM,eAAW,6BAAc,MAAM;AACrC,QAAM,OAAO,gBAAgB,UAAU,QAAQ,YAAY,gBAAgB;AAC3E,QAAM,SAAS,KAAK,WAAW,IAAI,KAAK,CAAC,IAAI,uCAAuC,KAAK,IAAI,UAAQ,wBAAwB,IAAI,YAAY,EAAE,KAAK,EAAE,CAAC;AAEvJ,SAAO;AAAA;AAAA;AAAA,6DAAgzC,UAAU,0BAA0B;AAC71C;AANgB;AAQhB,SAAS,gBAAgB,QAA4C,UAA4B;AAC/F,SAAO,OAAO,WAAW,QAAQ,UAAQ,YAAY,MAAM,QAAQ,CAAC;AACtE;AAFS;AAIT,SAAS,YAAY,MAAgB,UAA4B;AAC/D,MAAI,KAAK,aAAa,SAAS;AAC7B,UAAM,QAAS,KAA2C;AAC1D,WAAO,QAAQ,CAAC,cAAc,KAAK,UAAU,KAAK,CAAC,GAAG,IAAI,CAAC;AAAA,EAC7D;AAEA,MAAI,KAAK,aAAa,WAAY,QAAO,CAAC;AAC1C,MAAI,KAAK,aAAa,wBAAwB,CAAC,UAAU,IAAI,EAAG,QAAO,CAAC;AAExE,MAAI,UAAU,IAAI,GAAG;AACnB,UAAM,MAAM,KAAK,QAAQ,YAAY;AACrC,QAAI,YAAY,IAAI,GAAG,EAAG,OAAM,IAAI,MAAM,kDAA8B,GAAG,MAAM,QAAQ,GAAG;AAE5F,UAAM,WAAW,gBAAgB,MAAM,QAAQ;AAC/C,UAAM,aAAa,CAAC,iCAAiC,KAAK,UAAU,GAAG,CAAC,GAAG;AAC3E,UAAM,cAAc,CAAC;AACrB,eAAW,aAAa,KAAK,OAAO;AAClC,YAAM,OAAO,UAAU,KAAK,YAAY;AACxC,YAAM,QAAQ,UAAU;AACxB,YAAM,YAAY,eAAe,MAAM,OAAO,QAAQ;AACtD,UAAI,WAAW;AACb,YAAI,UAAU,SAAS,QAAQ;AAC7B,sBAAY,KAAK,UAAU,IAAI;AAAA,QACjC,WAAW,UAAU,SAAS,QAAQ;AACpC,qBAAW,KAAK,4DAA4D,KAAK,UAAU,UAAU,IAAI,CAAC,KAAK;AAAA,QACjH,WAAW,UAAU,SAAS,SAAS;AACrC,qBAAW,KAAK,6BAA6B,KAAK,UAAU,UAAU,IAAI,CAAC,wCAAwC,KAAK,UAAU,UAAU,IAAI,CAAC,0DAA0D;AAAA,QAC7M,WAAW,UAAU,SAAS,aAAa;AACzC,qBAAW,KAAK,0BAA0B,KAAK,UAAU,UAAU,IAAI,CAAC,iCAAiC,KAAK,UAAU,UAAU,IAAI,CAAC,WAAW,KAAK,UAAU,UAAU,IAAI,CAAC,WAAW;AAAA,QAC7L,WAAW,UAAU,SAAS,YAAY;AACxC,qBAAW,KAAK,yBAAyB,KAAK,UAAU,UAAU,IAAI,CAAC,iBAAiB,KAAK,UAAU,UAAU,IAAI,CAAC,IAAI;AAAA,QAC5H;AACA;AAAA,MACF;AACA,UAAI,kBAAkB,KAAK,IAAI,EAAG,OAAM,IAAI,MAAM,yEAAiC,UAAU,IAAI,KAAK,QAAQ,GAAG;AACjH,UAAI,cAAc,IAAI,IAAI,KAAK,CAAC,UAAU,KAAK,GAAG;AAChD,cAAM,IAAI,MAAM,8EAAsC,UAAU,IAAI,KAAK,QAAQ,GAAG;AAAA,MACtF;AACA,iBAAW,KAAK,yBAAyB,KAAK,UAAU,UAAU,IAAI,CAAC,KAAK,KAAK,UAAU,KAAK,CAAC,GAAG;AAAA,IACtG;AACA,QAAI,YAAY,SAAS,EAAG,OAAM,IAAI,MAAM,kHAAsD,QAAQ,GAAG;AAC7G,QAAI,YAAY,WAAW,GAAG;AAC5B,YAAM,OAAO;AACb,iBAAW,KAAK,gBAAgB,IAAI,EAAE;AACtC,iBAAW,KAAK,mCAAmC;AACnD,iBAAW,KAAK,8BAA8B,KAAK,UAAU,YAAY,CAAC,CAAC,CAAC,UAAU;AAAA,IACxF,OAAO;AACL,iBAAW,SAAS,SAAU,YAAW,KAAK,yBAAyB,KAAK,SAAS;AAAA,IACvF;AACA,eAAW,KAAK,gBAAgB;AAChC,WAAO,CAAC,WAAW,WAAW,KAAK,GAAG,CAAC,OAAO;AAAA,EAChD;AAEA,SAAO,gBAAgB,MAA8B,QAAQ;AAC/D;AAtDS;AAwDT,SAAS,UAAU,MAAqC;AACtD,SAAO,CAAC,KAAK,SAAS,WAAW,GAAG;AACtC;AAFS;AAIT,SAAS,UAAU,OAAwB;AACzC,QAAM,aAAa,MAAM,KAAK,EAAE,YAAY;AAC5C,MAAI,WAAW,WAAW,GAAG,KAAK,WAAW,WAAW,GAAG,KAAK,WAAW,WAAW,IAAI,KAAK,WAAW,WAAW,KAAK,EAAG,QAAO;AACpI,MAAI;AACF,WAAO,cAAc,IAAI,IAAI,IAAI,YAAY,uBAAuB,EAAE,QAAQ;AAAA,EAChF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AARS;AAcT,SAAS,eAAe,MAAc,OAAe,UAAwC;AAC3F,QAAM,QAAQ,iBAAiB,KAAK,IAAI;AACxC,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,OAAO,MAAM,CAAC,EAAE,YAAY;AAClC,QAAM,WAAW,MAAM,CAAC;AACxB,QAAM,OAAO,MAAM,KAAK;AACxB,MAAI,CAAC,QAAQ,CAAC,sBAAsB,KAAK,IAAI,GAAG;AAC9C,UAAM,IAAI,MAAM,wBAAwB,IAAI,mEAAsB,QAAQ,GAAG;AAAA,EAC/E;AACA,MAAI,SAAS,UAAU,SAAS,QAAQ;AACtC,QAAI,SAAU,OAAM,IAAI,MAAM,wBAAwB,IAAI,gDAAa,QAAQ,GAAG;AAClF,WAAO,EAAE,MAAM,KAAK;AAAA,EACtB;AACA,MAAI,CAAC,YAAY,CAAC,uBAAuB,KAAK,QAAQ,GAAG;AACvD,UAAM,IAAI,MAAM,wBAAwB,IAAI,sDAAc,QAAQ,GAAG;AAAA,EACvE;AACA,MAAI,SAAS,KAAM,QAAO,EAAE,MAAM,SAAS,MAAM,SAAS,YAAY,GAAG,KAAK;AAC9E,MAAI,SAAS,QAAQ;AACnB,QAAI,SAAS,YAAY,MAAM,QAAS,OAAM,IAAI,MAAM,0EAAuC,QAAQ,GAAG;AAC1G,WAAO,EAAE,MAAM,aAAa,MAAM,UAAU,KAAK;AAAA,EACnD;AACA,MAAI,CAAC,kBAAkB,IAAI,SAAS,YAAY,CAAC,GAAG;AAClD,UAAM,IAAI,MAAM,4EAAyC,QAAQ,KAAK,QAAQ,GAAG;AAAA,EACnF;AACA,SAAO,EAAE,MAAM,YAAY,MAAM,sBAAsB,QAAQ,GAAG,KAAK;AACzE;AAzBS;AA2BT,SAAS,sBAAsB,MAAsB;AACnD,SAAO,KAAK,YAAY,MAAM,aAAa,aAAa,KAAK,YAAY,MAAM,aAAa,aAAa;AAC3G;AAFS;","names":[]}
|
package/dist/html-component.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { parseFragment } from 'parse5';
|
|
2
|
-
|
|
3
1
|
var __defProp = Object.defineProperty;
|
|
4
2
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
|
|
4
|
+
// packages/vite-plugin/src/html-component.ts
|
|
5
|
+
import { parseFragment } from "parse5";
|
|
5
6
|
var blockedTags = /* @__PURE__ */ new Set(["script", "iframe", "object", "embed", "base", "meta", "link", "style"]);
|
|
6
7
|
var blockedAttributes = /^(?:on[a-z]+|srcdoc|style)$/iu;
|
|
7
8
|
var urlAttributes = /* @__PURE__ */ new Set(["href", "src", "action", "formaction", "poster", "xlink:href"]);
|
|
@@ -130,7 +131,7 @@ function normalizePropertyName(name) {
|
|
|
130
131
|
return name.toLowerCase() === "readonly" ? "readOnly" : name.toLowerCase() === "tabindex" ? "tabIndex" : name;
|
|
131
132
|
}
|
|
132
133
|
__name(normalizePropertyName, "normalizePropertyName");
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
134
|
+
export {
|
|
135
|
+
compileHtmlComponent
|
|
136
|
+
};
|
|
136
137
|
//# sourceMappingURL=html-component.js.map
|
|
@@ -1 +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"]}
|
|
1
|
+
{"version":3,"sources":["../src/html-component.ts"],"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"],"mappings":"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","names":[]}
|