Comparing version 0.2.1 to 0.3.0
# 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
62656
11
98
1
96
1
+ Addedparsel-js@^1.0.2
+ Addedparsel-js@1.2.1(transitive)