Comparing version
# ultrahtml | ||
## 0.3.0 | ||
### Minor Changes | ||
- 2de70f3: Add `ultrahtml/selector` module which exports `querySelector`, `querySelectorAll`, and `matches` functions. | ||
To use `querySelectorAll`, pass the root `Node` as the first argument and any valid CSS selector as the second argument. Note that if a CSS selector you need is not yet implemented, you are invited to [open an issue](https://github.com/natemoo-re/ultrahtml/issues). | ||
```js | ||
import { parse } from "ultrahtml"; | ||
import { querySelectorAll, matches } from "ultrahtml/selector"; | ||
const doc = parse(` | ||
<html> | ||
<head> | ||
<title>Demo</title> | ||
/head> | ||
<body> | ||
<h1>Hello world!</h1> | ||
</body> | ||
</html> | ||
`); | ||
const h1 = querySelector(doc, "h1"); | ||
const match = matches(h1, "h1"); | ||
``` | ||
## 0.2.1 | ||
@@ -4,0 +30,0 @@ |
@@ -1,387 +0,1 @@ | ||
"use strict"; | ||
export const DOCUMENT_NODE = 0; | ||
export const ELEMENT_NODE = 1; | ||
export const TEXT_NODE = 2; | ||
export const COMMENT_NODE = 3; | ||
export const DOCTYPE_NODE = 4; | ||
const VOID_TAGS = { img: 1, br: 1, hr: 1, meta: 1, link: 1, base: 1, input: 1 }; | ||
const SPLIT_ATTRS_RE = /([\@\.a-z0-9_\:\-]*)\s*?=?\s*?(['"]?)(.*?)\2\s+/gim; | ||
const DOM_PARSER_RE = /(?:<(\/?)([a-zA-Z][a-zA-Z0-9\:-]*)(?:\s([^>]*?))?((?:\s*\/)?)>|(<\!\-\-)([\s\S]*?)(\-\->)|(<\!)([\s\S]*?)(>))/gm; | ||
function splitAttrs(str) { | ||
let obj = {}; | ||
let token; | ||
if (str) { | ||
SPLIT_ATTRS_RE.lastIndex = 0; | ||
str = " " + (str || "") + " "; | ||
while (token = SPLIT_ATTRS_RE.exec(str)) { | ||
if (token[0] === " ") | ||
continue; | ||
obj[token[1]] = token[3]; | ||
} | ||
} | ||
return obj; | ||
} | ||
export function parse(input) { | ||
let str = typeof input === "string" ? input : input.value; | ||
let doc, parent, token, text, i, bStart, bText, bEnd, tag; | ||
const tags = []; | ||
DOM_PARSER_RE.lastIndex = 0; | ||
parent = doc = { | ||
type: DOCUMENT_NODE, | ||
children: [] | ||
}; | ||
let lastIndex = 0; | ||
function commitTextNode() { | ||
text = str.substring(lastIndex, DOM_PARSER_RE.lastIndex - token[0].length); | ||
if (text) { | ||
parent.children.push({ | ||
type: TEXT_NODE, | ||
value: text, | ||
parent | ||
}); | ||
} | ||
} | ||
while (token = DOM_PARSER_RE.exec(str)) { | ||
bStart = token[5] || token[8]; | ||
bText = token[6] || token[9]; | ||
bEnd = token[7] || token[10]; | ||
if (bStart === "<!--") { | ||
i = DOM_PARSER_RE.lastIndex - token[0].length; | ||
tag = { | ||
type: COMMENT_NODE, | ||
value: bText, | ||
parent, | ||
loc: [ | ||
{ | ||
start: i, | ||
end: i + bStart.length | ||
}, | ||
{ | ||
start: DOM_PARSER_RE.lastIndex - bEnd.length, | ||
end: DOM_PARSER_RE.lastIndex | ||
} | ||
] | ||
}; | ||
tags.push(tag); | ||
tag.parent.children.push(tag); | ||
} else if (bStart === "<!") { | ||
i = DOM_PARSER_RE.lastIndex - token[0].length; | ||
tag = { | ||
type: DOCTYPE_NODE, | ||
value: bText, | ||
parent, | ||
loc: [ | ||
{ | ||
start: i, | ||
end: i + bStart.length | ||
}, | ||
{ | ||
start: DOM_PARSER_RE.lastIndex - bEnd.length, | ||
end: DOM_PARSER_RE.lastIndex | ||
} | ||
] | ||
}; | ||
tags.push(tag); | ||
tag.parent.children.push(tag); | ||
} else if (token[1] !== "/") { | ||
commitTextNode(); | ||
tag = { | ||
type: ELEMENT_NODE, | ||
name: token[2] + "", | ||
attributes: splitAttrs(token[3]), | ||
parent, | ||
children: [], | ||
loc: [ | ||
{ | ||
start: DOM_PARSER_RE.lastIndex - token[0].length, | ||
end: DOM_PARSER_RE.lastIndex | ||
} | ||
] | ||
}; | ||
tags.push(tag); | ||
tag.parent.children.push(tag); | ||
if (token[4] && token[4].indexOf("/") > -1 || VOID_TAGS.hasOwnProperty(tag.name)) { | ||
tag.loc[1] = tag.loc[0]; | ||
tag.isSelfClosingTag = true; | ||
} else { | ||
parent = tag; | ||
} | ||
} else { | ||
commitTextNode(); | ||
if (token[2] + "" === parent.name) { | ||
tag = parent; | ||
parent = tag.parent; | ||
tag.loc.push({ | ||
start: DOM_PARSER_RE.lastIndex - token[0].length, | ||
end: DOM_PARSER_RE.lastIndex | ||
}); | ||
text = str.substring(tag.loc[0].end, tag.loc[1].start); | ||
if (tag.children.length === 0) { | ||
tag.children.push({ | ||
type: TEXT_NODE, | ||
value: text, | ||
parent | ||
}); | ||
} | ||
} else if (token[2] + "" === tags[tags.length - 1].name && tags[tags.length - 1].isSelfClosingTag === true) { | ||
tag = tags[tags.length - 1]; | ||
tag.loc.push({ | ||
start: DOM_PARSER_RE.lastIndex - token[0].length, | ||
end: DOM_PARSER_RE.lastIndex | ||
}); | ||
} | ||
} | ||
lastIndex = DOM_PARSER_RE.lastIndex; | ||
} | ||
text = str.slice(lastIndex); | ||
parent.children.push({ | ||
type: TEXT_NODE, | ||
value: text, | ||
parent | ||
}); | ||
return doc; | ||
} | ||
class Walker { | ||
constructor(callback) { | ||
this.callback = callback; | ||
} | ||
async visit(node, parent, index) { | ||
await this.callback(node, parent, index); | ||
if (Array.isArray(node.children)) { | ||
let promises = []; | ||
for (let i = 0; i < node.children.length; i++) { | ||
const child = node.children[i]; | ||
promises.push(this.visit(child, node, i)); | ||
} | ||
await Promise.all(promises); | ||
} | ||
} | ||
} | ||
class WalkerSync { | ||
constructor(callback) { | ||
this.callback = callback; | ||
} | ||
visit(node, parent, index) { | ||
this.callback(node, parent, index); | ||
if (Array.isArray(node.children)) { | ||
for (let i = 0; i < node.children.length; i++) { | ||
const child = node.children[i]; | ||
this.visit(child, node, i); | ||
} | ||
} | ||
} | ||
} | ||
const HTMLString = Symbol("HTMLString"); | ||
const AttrString = Symbol("AttrString"); | ||
function mark(str, tags = [HTMLString]) { | ||
const v = { value: str }; | ||
for (const tag of tags) { | ||
Object.defineProperty(v, tag, { | ||
value: true, | ||
enumerable: false, | ||
writable: false | ||
}); | ||
} | ||
return v; | ||
} | ||
export function __unsafeHTML(str) { | ||
return mark(str); | ||
} | ||
const ESCAPE_CHARS = { | ||
"&": "&", | ||
"<": "<", | ||
">": ">" | ||
}; | ||
function escapeHTML(str) { | ||
return str.replace(/[&<>]/g, (c) => ESCAPE_CHARS[c] || c); | ||
} | ||
export function attrs(attributes) { | ||
let attrStr = ""; | ||
for (const [key, value] of Object.entries(attributes)) { | ||
attrStr += ` ${key}="${value}"`; | ||
} | ||
return mark(attrStr, [HTMLString, AttrString]); | ||
} | ||
export function html(tmpl, ...vals) { | ||
let buf = ""; | ||
for (let i = 0; i < tmpl.length; i++) { | ||
buf += tmpl[i]; | ||
const expr = vals[i]; | ||
if (buf.endsWith("...") && expr && typeof expr === "object") { | ||
buf = buf.slice(0, -3).trimEnd(); | ||
buf += attrs(expr).value; | ||
} else if (expr && expr[AttrString]) { | ||
buf = buf.trimEnd(); | ||
buf += expr.value; | ||
} else if (expr && expr[HTMLString]) { | ||
buf += expr.value; | ||
} else if (typeof expr === "string") { | ||
buf += escapeHTML(expr); | ||
} else if (expr || expr === 0) { | ||
buf += String(expr); | ||
} | ||
} | ||
return mark(buf); | ||
} | ||
export function walk(node, callback) { | ||
const walker = new Walker(callback); | ||
return walker.visit(node); | ||
} | ||
export function walkSync(node, callback) { | ||
const walker = new WalkerSync(callback); | ||
return walker.visit(node); | ||
} | ||
function resolveSantizeOptions({ | ||
components = {}, | ||
sanitize = true | ||
}) { | ||
var _a; | ||
if (sanitize === true) { | ||
return { | ||
allowElements: Object.keys(components), | ||
dropElements: ["script"], | ||
allowComponents: false, | ||
allowCustomElements: false, | ||
allowComments: false | ||
}; | ||
} else if (sanitize === false) { | ||
return { | ||
dropElements: [], | ||
allowComponents: true, | ||
allowCustomElements: true, | ||
allowComments: true | ||
}; | ||
} else { | ||
const dropElements = /* @__PURE__ */ new Set([]); | ||
if (!((_a = sanitize.allowElements) == null ? void 0 : _a.includes("script"))) { | ||
dropElements.add("script"); | ||
} | ||
for (const dropElement of sanitize.dropElements ?? []) { | ||
dropElements.add(dropElement); | ||
} | ||
return { | ||
allowComponents: false, | ||
allowCustomElements: false, | ||
allowComments: false, | ||
...sanitize, | ||
allowElements: [ | ||
...Object.keys(components), | ||
...sanitize.allowElements ?? [] | ||
], | ||
dropElements: Array.from(dropElements) | ||
}; | ||
} | ||
} | ||
function getNodeType(node) { | ||
if (node.name.includes("-")) | ||
return "custom-element"; | ||
if (/[\_\$A-Z]/.test(node.name[0]) || node.name.includes(".")) | ||
return "component"; | ||
return "element"; | ||
} | ||
function getAction(name, type, sanitize) { | ||
var _a, _b, _c; | ||
if (((_a = sanitize.allowElements) == null ? void 0 : _a.length) > 0) { | ||
if (sanitize.allowElements.includes(name)) | ||
return "allow"; | ||
} | ||
if (((_b = sanitize.blockElements) == null ? void 0 : _b.length) > 0) { | ||
if (sanitize.blockElements.includes(name)) | ||
return "block"; | ||
} | ||
if (((_c = sanitize.dropElements) == null ? void 0 : _c.length) > 0) { | ||
if (sanitize.dropElements.find((n) => n === name)) | ||
return "drop"; | ||
} | ||
if (type === "component" && !sanitize.allowComponents) | ||
return "drop"; | ||
if (type === "custom-element" && !sanitize.allowCustomElements) | ||
return "drop"; | ||
return "allow"; | ||
} | ||
function sanitizeAttributes(node, sanitize) { | ||
var _a, _b, _c, _d, _e, _f; | ||
const attrs2 = node.attributes; | ||
for (const key of Object.keys(node.attributes)) { | ||
if (((_a = sanitize.allowAttributes) == null ? void 0 : _a[key]) && ((_b = sanitize.allowAttributes) == null ? void 0 : _b[key].includes(node.name)) || ((_c = sanitize.allowAttributes) == null ? void 0 : _c[key].includes("*"))) { | ||
continue; | ||
} | ||
if (((_d = sanitize.dropAttributes) == null ? void 0 : _d[key]) && ((_e = sanitize.dropAttributes) == null ? void 0 : _e[key].includes(node.name)) || ((_f = sanitize.dropAttributes) == null ? void 0 : _f[key].includes("*"))) { | ||
delete attrs2[key]; | ||
} | ||
} | ||
return attrs2; | ||
} | ||
async function renderElement(node, opts) { | ||
const type = getNodeType(node); | ||
const { name } = node; | ||
const action = getAction( | ||
name, | ||
type, | ||
opts.sanitize | ||
); | ||
if (action === "drop") | ||
return ""; | ||
if (action === "block") | ||
return await Promise.all( | ||
node.children.map((child) => render(child, opts)) | ||
).then((res) => res.join("")); | ||
const component = opts.components[node.name]; | ||
if (typeof component === "string") | ||
return renderElement({ ...node, name: component }, opts); | ||
const attributes = sanitizeAttributes( | ||
node, | ||
opts.sanitize | ||
); | ||
if (typeof component === "function") { | ||
const value = await component( | ||
attributes, | ||
mark( | ||
await Promise.all( | ||
node.children.map((child) => render(child, opts)) | ||
).then((res) => res.join("")) | ||
) | ||
); | ||
if (value && value[HTMLString]) | ||
return value.value; | ||
return escapeHTML(String(value)); | ||
} | ||
if (VOID_TAGS.hasOwnProperty(name)) { | ||
return `<${node.name}${attrs(attributes).value}>`; | ||
} | ||
return `<${node.name}${attrs(attributes).value}>${await Promise.all( | ||
node.children.map((child) => render(child, opts)) | ||
).then((res) => res.join(""))}</${node.name}>`; | ||
} | ||
export async function render(node, opts = {}) { | ||
const sanitize = resolveSantizeOptions(opts); | ||
switch (node.type) { | ||
case DOCUMENT_NODE: { | ||
return Promise.all( | ||
node.children.map((child) => render(child, opts)) | ||
).then((res) => res.join("")); | ||
} | ||
case ELEMENT_NODE: | ||
return renderElement(node, { | ||
components: opts.components ?? {}, | ||
sanitize | ||
}); | ||
case TEXT_NODE: { | ||
return `${node.value}`; | ||
} | ||
case COMMENT_NODE: { | ||
if (sanitize.allowComments) { | ||
return `<!--${node.value}-->`; | ||
} else { | ||
return ""; | ||
} | ||
} | ||
case DOCTYPE_NODE: { | ||
return `<!${node.value}>`; | ||
} | ||
} | ||
return ""; | ||
} | ||
export async function transform(input, opts = {}) { | ||
return render(parse(input), opts); | ||
} | ||
var I=0,P=1,M=2,$=3,j=4,O={img:1,br:1,hr:1,meta:1,link:1,base:1,input:1},w=/([\@\.a-z0-9_\:\-]*)\s*?=?\s*?(['"]?)(.*?)\2\s+/gim,c=/(?:<(\/?)([a-zA-Z][a-zA-Z0-9\:-]*)(?:\s([^>]*?))?((?:\s*\/)?)>|(<\!\-\-)([\s\S]*?)(\-\->)|(<\!)([\s\S]*?)(>))/gm;function S(t){let e={},r;if(t)for(w.lastIndex=0,t=" "+(t||"")+" ";r=w.exec(t);)r[0]!==" "&&(e[r[1]]=r[3]);return e}function A(t){let e=typeof t=="string"?t:t.value,r,l,n,o,a,i,p,u,s,d=[];c.lastIndex=0,l=r={type:0,children:[]};let h=0;function b(){o=e.substring(h,c.lastIndex-n[0].length),o&&l.children.push({type:2,value:o,parent:l})}for(;n=c.exec(e);)i=n[5]||n[8],p=n[6]||n[9],u=n[7]||n[10],i==="<!--"?(a=c.lastIndex-n[0].length,s={type:3,value:p,parent:l,loc:[{start:a,end:a+i.length},{start:c.lastIndex-u.length,end:c.lastIndex}]},d.push(s),s.parent.children.push(s)):i==="<!"?(a=c.lastIndex-n[0].length,s={type:4,value:p,parent:l,loc:[{start:a,end:a+i.length},{start:c.lastIndex-u.length,end:c.lastIndex}]},d.push(s),s.parent.children.push(s)):n[1]!=="/"?(b(),s={type:1,name:n[2]+"",attributes:S(n[3]),parent:l,children:[],loc:[{start:c.lastIndex-n[0].length,end:c.lastIndex}]},d.push(s),s.parent.children.push(s),n[4]&&n[4].indexOf("/")>-1||O.hasOwnProperty(s.name)?(s.loc[1]=s.loc[0],s.isSelfClosingTag=!0):l=s):(b(),n[2]+""===l.name?(s=l,l=s.parent,s.loc.push({start:c.lastIndex-n[0].length,end:c.lastIndex}),o=e.substring(s.loc[0].end,s.loc[1].start),s.children.length===0&&s.children.push({type:2,value:o,parent:l})):n[2]+""===d[d.length-1].name&&d[d.length-1].isSelfClosingTag===!0&&(s=d[d.length-1],s.loc.push({start:c.lastIndex-n[0].length,end:c.lastIndex}))),h=c.lastIndex;return o=e.slice(h),l.children.push({type:2,value:o,parent:l}),r}var E=class{constructor(e){this.callback=e}async visit(e,r,l){if(await this.callback(e,r,l),Array.isArray(e.children)){let n=[];for(let o=0;o<e.children.length;o++){let a=e.children[o];n.push(this.visit(a,e,o))}await Promise.all(n)}}},y=class{constructor(e){this.callback=e}visit(e,r,l){if(this.callback(e,r,l),Array.isArray(e.children))for(let n=0;n<e.children.length;n++){let o=e.children[n];this.visit(o,e,n)}}},f=Symbol("HTMLString"),T=Symbol("AttrString");function g(t,e=[f]){let r={value:t};for(let l of e)Object.defineProperty(r,l,{value:!0,enumerable:!1,writable:!1});return r}function L(t){return g(t)}var C={"&":"&","<":"<",">":">"};function x(t){return t.replace(/[&<>]/g,e=>C[e]||e)}function N(t){let e="";for(let[r,l]of Object.entries(t))e+=` ${r}="${l}"`;return g(e,[f,T])}function V(t,...e){let r="";for(let l=0;l<t.length;l++){r+=t[l];let n=e[l];r.endsWith("...")&&n&&typeof n=="object"?(r=r.slice(0,-3).trimEnd(),r+=N(n).value):n&&n[T]?(r=r.trimEnd(),r+=n.value):n&&n[f]?r+=n.value:typeof n=="string"?r+=x(n):(n||n===0)&&(r+=String(n))}return g(r)}function q(t,e){return new E(e).visit(t)}function H(t,e){return new y(e).visit(t)}function R({components:t={},sanitize:e=!0}){var r;if(e===!0)return{allowElements:Object.keys(t),dropElements:["script"],allowComponents:!1,allowCustomElements:!1,allowComments:!1};if(e===!1)return{dropElements:[],allowComponents:!0,allowCustomElements:!0,allowComments:!0};{let l=new Set([]);(r=e.allowElements)!=null&&r.includes("script")||l.add("script");for(let n of e.dropElements??[])l.add(n);return{allowComponents:!1,allowCustomElements:!1,allowComments:!1,...e,allowElements:[...Object.keys(t),...e.allowElements??[]],dropElements:Array.from(l)}}}function _(t){return t.name.includes("-")?"custom-element":/[\_\$A-Z]/.test(t.name[0])||t.name.includes(".")?"component":"element"}function D(t,e,r){var l,n,o;return((l=r.allowElements)==null?void 0:l.length)>0&&r.allowElements.includes(t)?"allow":((n=r.blockElements)==null?void 0:n.length)>0&&r.blockElements.includes(t)?"block":((o=r.dropElements)==null?void 0:o.length)>0&&r.dropElements.find(a=>a===t)||e==="component"&&!r.allowComponents||e==="custom-element"&&!r.allowCustomElements?"drop":"allow"}function k(t,e){var l,n,o,a,i,p;let r=t.attributes;for(let u of Object.keys(t.attributes))((l=e.allowAttributes)==null?void 0:l[u])&&((n=e.allowAttributes)==null?void 0:n[u].includes(t.name))||((o=e.allowAttributes)==null?void 0:o[u].includes("*"))||(((a=e.dropAttributes)==null?void 0:a[u])&&((i=e.dropAttributes)==null?void 0:i[u].includes(t.name))||((p=e.dropAttributes)==null?void 0:p[u].includes("*")))&&delete r[u];return r}async function v(t,e){let r=_(t),{name:l}=t,n=D(l,r,e.sanitize);if(n==="drop")return"";if(n==="block")return await Promise.all(t.children.map(i=>m(i,e))).then(i=>i.join(""));let o=e.components[t.name];if(typeof o=="string")return v({...t,name:o},e);let a=k(t,e.sanitize);if(typeof o=="function"){let i=await o(a,g(await Promise.all(t.children.map(p=>m(p,e))).then(p=>p.join(""))));return i&&i[f]?i.value:x(String(i))}return O.hasOwnProperty(l)?`<${t.name}${N(a).value}>`:`<${t.name}${N(a).value}>${await Promise.all(t.children.map(i=>m(i,e))).then(i=>i.join(""))}</${t.name}>`}async function m(t,e={}){let r=R(e);switch(t.type){case 0:return Promise.all(t.children.map(l=>m(l,e))).then(l=>l.join(""));case 1:return v(t,{components:e.components??{},sanitize:r});case 2:return`${t.value}`;case 3:return r.allowComments?`<!--${t.value}-->`:"";case 4:return`<!${t.value}>`}return""}async function X(t,e={}){return m(A(t),e)}export{$ as COMMENT_NODE,j as DOCTYPE_NODE,I as DOCUMENT_NODE,P as ELEMENT_NODE,M as TEXT_NODE,L as __unsafeHTML,N as attrs,V as html,A as parse,m as render,X as transform,q as walk,H as walkSync}; |
{ | ||
"name": "ultrahtml", | ||
"type": "module", | ||
"version": "0.2.1", | ||
"version": "0.3.0", | ||
"types": "./dist/index.d.ts", | ||
@@ -15,2 +15,3 @@ "repository": { | ||
"files": [ | ||
"selector.d.ts", | ||
"dist", | ||
@@ -21,2 +22,3 @@ "CHANGELOG.md" | ||
".": "./dist/index.js", | ||
"./selector": "./dist/selector.js", | ||
"./dist/*": "./dist/*", | ||
@@ -40,2 +42,5 @@ "./package.json": "./package.json" | ||
"packageManager": "pnpm@7.6.0", | ||
"dependencies": { | ||
"parsel-js": "^1.0.2" | ||
}, | ||
"devDependencies": { | ||
@@ -50,3 +55,5 @@ "@changesets/cli": "^2.18.1", | ||
"scripts": { | ||
"build": "esbuild src/index.ts --target=node14 --outfile=dist/index.js && tsc -p .", | ||
"build:index": "esbuild src/index.ts --bundle --format=esm --minify --sourcemap=external --target=node14 --outfile=dist/index.js", | ||
"build:selector": "esbuild src/selector.ts --format=esm --minify --sourcemap=external --target=node14 --outfile=dist/selector.js", | ||
"build": "pnpm run build:index && pnpm run build:selector && tsc -p .", | ||
"lint": "prettier \"**/*.{js,ts,md}\"", | ||
@@ -53,0 +60,0 @@ "test": "vitest" |
@@ -12,2 +12,3 @@ # `ultrahtml` | ||
- Handy `html` template utility | ||
- `querySelector` and `querySelectorAll` support using `ultrahtml/selector` | ||
@@ -14,0 +15,0 @@ #### `walk` |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
62656
160.24%11
83.33%98
1.03%1
Infinity%96
-78.18%2
100%1
Infinity%+ Added
+ Added