hyper-html-api 0.1.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/LICENSE +13 -0
- package/README.md +51 -0
- package/dist/hyper-html-api.min.js +2 -0
- package/package.json +68 -0
- package/src/adapters/cheerio.js +93 -0
- package/src/adapters/dom.js +83 -0
- package/src/adapters/index.js +2 -0
- package/src/engine/apply.js +139 -0
- package/src/engine/diff.js +129 -0
- package/src/engine/dom-properties.js +52 -0
- package/src/engine/errors.js +48 -0
- package/src/engine/extract.js +70 -0
- package/src/engine/index.js +6 -0
- package/src/engine/rules-tag.js +18 -0
- package/src/engine/rules.js +245 -0
- package/src/hyper-html-api.js +39 -0
- package/src/upgrade/helper.js +143 -0
- package/src/upgrade/index.js +4 -0
- package/src/upgrade/registry.js +19 -0
- package/src/upgrade/runner.js +92 -0
- package/src/upgrade/shape-match.js +87 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Zero-Clause BSD
|
|
2
|
+
=============
|
|
3
|
+
|
|
4
|
+
Permission to use, copy, modify, and/or distribute this software for
|
|
5
|
+
any purpose with or without fee is hereby granted.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL
|
|
8
|
+
WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
|
|
9
|
+
OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
|
|
10
|
+
FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
|
|
11
|
+
DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
|
|
12
|
+
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
|
|
13
|
+
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# hyper-html-api
|
|
2
|
+
|
|
3
|
+
Self-describing data layer for HTML files — a bidirectional engine that reads structured data out of an HTML page and writes structured data back into it, driven by a single declarative rules tag.
|
|
4
|
+
|
|
5
|
+
Status: **early development** (Phase 0 — repo bootstrap). API shape and behavior are being ported from `hyperclay/server-lib/data-extractor.js`. See `plans/hyperclay/hyper-html-api/` in the workspace for the full specification.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install hyper-html-api
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Rules tag
|
|
14
|
+
|
|
15
|
+
Pages describe their data layer with a single script tag:
|
|
16
|
+
|
|
17
|
+
```html
|
|
18
|
+
<script type="application/hyper-html-api" id="hyper-html-api" data-rules-version="1">
|
|
19
|
+
{
|
|
20
|
+
"title": "h1",
|
|
21
|
+
"user": { "name": ".user-name", "role": ".user-role" },
|
|
22
|
+
"products": [".product", { "name": ".name", "price": ".price" }],
|
|
23
|
+
"tags": ".tag[]"
|
|
24
|
+
}
|
|
25
|
+
</script>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Browser API
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
HyperHtmlApi.engine.extract(root, rules) // → JSON
|
|
32
|
+
HyperHtmlApi.engine.apply(root, rules, data) // mutates the DOM
|
|
33
|
+
HyperHtmlApi.engine.findRulesIn(root) // → { rules, tagNode } | null
|
|
34
|
+
HyperHtmlApi.engine.errors // error class refs
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Node API
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
import { extract, apply, findRulesIn, errors } from 'hyper-html-api/engine'
|
|
41
|
+
import cheerioAdapter from 'hyper-html-api/cheerio'
|
|
42
|
+
import * as cheerio from 'cheerio'
|
|
43
|
+
|
|
44
|
+
const $ = cheerio.load(html)
|
|
45
|
+
const { rules } = findRulesIn(cheerioAdapter, $.root())
|
|
46
|
+
const data = extract(cheerioAdapter, $.root(), rules)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## License
|
|
50
|
+
|
|
51
|
+
0BSD
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var HyperHtmlApi=(()=>{var B=Object.defineProperty;var ce=Object.getOwnPropertyDescriptor;var le=Object.getOwnPropertyNames;var ae=Object.prototype.hasOwnProperty;var ee=(e,t)=>{for(var r in t)B(e,r,{get:t[r],enumerable:!0})},ue=(e,t,r,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of le(t))!ae.call(e,n)&&n!==r&&B(e,n,{get:()=>t[n],enumerable:!(o=ce(t,n))||o.enumerable});return e};var de=e=>ue(B({},"__esModule",{value:!0}),e);var Be={};ee(Be,{cms:()=>ie,default:()=>qe,engine:()=>se,upgrade:()=>fe});var C=["textContent","innerText","innerHTML","outerHTML","value","checked","selected","disabled","readOnly","type","tagName","nodeName","nodeType","nodeValue","childElementCount","id","className","classList","href","src","action","baseURI","offsetWidth","offsetHeight","clientWidth","clientHeight","scrollWidth","scrollHeight","dataset","currentSrc","duration","paused","title","documentURI","contentType"],O=new Set(C);var D={};ee(D,{EmptyListInsert:()=>b,MaxRuleDepthExceeded:()=>E,RulesParseError:()=>x,ShapeMismatch:()=>M,UnknownRulesVersion:()=>T,UpgradeAlreadyRegistered:()=>w});var x=class extends Error{constructor(t,r){super(t),this.name="RulesParseError",this.cause=r}},T=class extends Error{constructor(t){super(`unknown rules version: ${t}. Library supports "1".`),this.name="UnknownRulesVersion",this.version=t}},E=class extends Error{constructor(t){super(`rule depth exceeded 20 at path: ${t.join(".")}`),this.name="MaxRuleDepthExceeded",this.path=t}},M=class extends Error{constructor(t){super(`shape mismatch: ${t.length} field(s) failed validation`),this.name="ShapeMismatch",this.mismatches=t}},b=class extends Error{constructor(t){super(`cannot add items to empty list at "${t.join(".")}" \u2014 no sibling to clone as template. Seed the list with a hidden item first.`),this.name="EmptyListInsert",this.path=t}},w=class extends Error{constructor(){super("upgrade transform already registered; only one registration is allowed per page."),this.name="UpgradeAlreadyRegistered"}};var pe=20;function A(e,t,r){return $(e,t,r,{depth:0,path:[]})}function $(e,t,r,o){if(o.depth>pe)throw new E(o.path);if(typeof r=="string")return he(e,t,r);if(Array.isArray(r)){let[n,s]=r;return e.find(t,n).map((i,f)=>$(e,i,s,{depth:o.depth+1,path:[...o.path,f]}))}if(typeof r=="object"&&r!==null){let n={};for(let[s,c]of Object.entries(r))n[s]=$(e,t,c,{depth:o.depth+1,path:[...o.path,s]});return n}return null}function he(e,t,r){if(r.endsWith("[]")){let n=r.slice(0,-2);return e.find(t,n).map(s=>e.text(s))}if(r.startsWith("@"))return te(e,t,r.slice(1));if(r.includes("@")){let[n,s]=r.split("@"),c=n?e.find(t,n):[t];return c.length===0?null:te(e,c[0],s)}if(r===".")return e.text(t);let o=e.find(t,r);return o.length===0?null:e.text(o[0])}function te(e,t,r){if(O.has(r)){let n=e.prop(t,r);return n==null?null:String(n)}let o=e.attr(t,r);return o||null}var ye=.5;function W(e,t,r,o,n,s,c){let i=e.find(t,r);if(n.length===0){i.forEach(d=>e.remove(d));return}if(n.length>i.length&&i.length===0)throw new b(s.path);let a=i.map(d=>me(e,d,o)),u=null;if(i.length>0){u=e.clone(i[0]);let d=e.stripIds(u);d>0&&console.warn(`[hyper-html-api] stripped ${d} id attribute(s) from cloned template at "${s.path.join(".")||"(root)"}"`)}let p=ge(n,a,o),l=e.parent(i[0]),R=Ae(e,l,i[0]),y=new Set,h=n.map((d,m)=>{let S=p[m];if(S>=0)return y.add(S),i[S];let v=e.clone(u);return e.stripIds(v),v});i.forEach((d,m)=>{y.has(m)||e.remove(d)}),y.forEach(d=>e.remove(i[d])),h.forEach((d,m)=>{e.insertAt(l,d,R+m)}),h.forEach((d,m)=>{if(o===null){let S=n[m];e.text(d,S==null?"":String(S))}else c(e,d,o,n[m],{depth:s.depth+1,path:[...s.path,m]})})}function me(e,t,r){return r===null?e.text(t):A(e,t,r)}function ge(e,t,r){let o=new Array(e.length).fill(-1),n=new Set;return e.forEach((s,c)=>{let i=-1,f=-1;t.forEach((a,u)=>{if(n.has(u))return;let p=Ee(s,a,r),l=p===f&&i>=0?Math.abs(u-c)<Math.abs(i-c):!1;(p>f||l)&&(f=p,i=u)}),f>=ye&&(o[c]=i,n.add(i))}),o}function Ee(e,t,r){if(r===null)return e===t?1:0;let o=Object.keys(r||{});if(o.length===0)return 0;let n=0;for(let s of o)JSON.stringify(e?.[s])===JSON.stringify(t?.[s])&&n++;return n/o.length}function Ae(e,t,r){let o=e.children(t);for(let n=0;n<o.length;n++)if(e.sameNode(o[n],r))return n;return-1}var Re=20,re=new Set(["checked","selected","disabled","readOnly","paused"]);function L(e,t,r,o){let n=[];if(F(r,o,[],n),n.length)throw new M(n);j(e,t,r,o,{depth:0,path:[]})}function j(e,t,r,o,n){if(n.depth>Re)throw new E(n.path);if(o!==void 0){if(typeof r=="string")return Se(e,t,r,o,n);if(Array.isArray(r)){let[s,c]=r;return W(e,t,s,c,o,n,j)}if(typeof r=="object"&&r!==null)for(let[s,c]of Object.entries(r))j(e,t,c,o==null?o:o[s],{depth:n.depth+1,path:[...n.path,s]})}}function Se(e,t,r,o,n){if(r.endsWith("[]")){let c=r.slice(0,-2);return W(e,t,c,null,o,n,j)}if(r.startsWith("@"))return ne(e,t,r.slice(1),o);if(r.includes("@")){let[c,i]=r.split("@"),f=c?e.find(t,c):[t];return f.length===0?void 0:ne(e,f[0],i,o)}if(r==="."){e.text(t,o==null?"":String(o));return}let s=e.find(t,r);s.length!==0&&e.text(s[0],o==null?"":String(o))}function ne(e,t,r,o){O.has(r)?e.prop(t,r,xe(r,o)):e.attr(t,r,o==null?"":String(o))}function xe(e,t){return t==null?re.has(e)?!1:"":re.has(e)?!!t:t}function F(e,t,r,o){if(!(t==null||t==="")){if(typeof e=="string"){if(e.endsWith("[]")){Array.isArray(t)?t.forEach((n,s)=>{typeof n=="object"&&n!==null&&o.push({path:N([...r,s]),expected:"scalar",got:I(n)})}):o.push({path:N(r),expected:"array",got:I(t)});return}typeof t=="object"&&o.push({path:N(r),expected:"scalar",got:I(t)});return}if(Array.isArray(e)){if(!Array.isArray(t)){o.push({path:N(r),expected:"array",got:I(t)});return}let n=e[1];t.forEach((s,c)=>F(n,s,[...r,c],o));return}if(typeof e=="object"&&e!==null){if(Array.isArray(t)||typeof t!="object"){o.push({path:N(r),expected:"object",got:I(t)});return}for(let[n,s]of Object.entries(e))F(s,t[n],[...r,n],o)}}}function I(e){return e===null?"null":Array.isArray(e)?"array":typeof e}function N(e){return e.join(".")}function P(e){try{return JSON.parse(e)}catch(t){throw new x(`hyper-html-api script tags require strict JSON. Relaxed JSON (unquoted keys, single quotes, trailing commas) is only supported in the ?data= URL parameter. Original error: ${t.message}`,t)}}function J(e){try{return JSON.parse(e)}catch{}let t={BRACE_OPEN:"{",BRACE_CLOSE:"}",BRACKET_OPEN:"[",BRACKET_CLOSE:"]",COLON:":",COMMA:",",STRING:"STRING",SELECTOR:"SELECTOR",IDENTIFIER:"IDENTIFIER",NUMBER:"NUMBER",BOOLEAN:"BOOLEAN"};function r(n){let s=[],c=0;for(;c<n.length;){let i=n[c];if(/\s/.test(i)){c++;continue}if("{}".includes(i)){s.push({type:i,value:i}),c++;continue}if(i==="["){let p=!1,l=c+1;for(;l<n.length&&/\s/.test(n[l]);)l++;if(l<n.length&&/[a-zA-Z_]/.test(n[l])&&(p=!0),!p){s.push({type:i,value:i}),c++;continue}}if(i==="]"){s.push({type:i,value:i}),c++;continue}if(i===":"){s.push({type:t.COLON,value:i}),c++;continue}if(i===","){s.push({type:t.COMMA,value:i}),c++;continue}if(i==='"'||i==="'"){let p=i,l=c+1;for(;l<n.length&&n[l]!==p;)n[l]==="\\"&&l++,l++;s.push({type:t.STRING,value:n.substring(c+1,l),quoted:!0}),c=l+1;continue}let f=c,a;for(;f<n.length&&!/[{},]/.test(n[f]);)if(n[f]===":"){let p=[":first",":last",":nth-child",":nth-of-type",":first-child",":last-child",":first-of-type",":last-of-type",":only-child",":only-of-type",":hover",":focus",":active",":visited",":disabled",":enabled",":checked",":empty",":root",":target",":not",":before",":after",":nth-last-child",":nth-last-of-type"],l=!1;for(let R of p){let y=R.substring(1);if(n.substring(f+1,f+1+y.length)===y){l=!0,f+=y.length;break}}if(!l)break}else if(n[f]==="["){for(f++;f<n.length&&n[f]!=="]";){if(n[f]==='"'||n[f]==="'"){let p=n[f];for(f++;f<n.length&&n[f]!==p;)n[f]==="\\"&&f++,f++}f++}f<n.length&&n[f]==="]"&&f++}else f++;a=n.substring(c,f);let u=t.IDENTIFIER;/^-?\d+(\.\d+)?$/.test(a)?u=t.NUMBER:a==="true"||a==="false"||a==="null"?u=t.BOOLEAN:/^[.#@\[]|[.#@\[]| /.test(a)&&(u=t.SELECTOR),s.push({type:u,value:a,quoted:!1}),c=f}return s}function o(n){let s="";for(let c=0;c<n.length;c++){let i=n[c];if("{}".includes(i.type)||"[]".includes(i.type)){s+=i.value;continue}if(i.type===t.COLON){s+=i.value;continue}if(i.type===t.COMMA){s+=i.value;continue}if(i.type===t.STRING&&i.quoted){s+=`"${i.value}"`;continue}if(i.type===t.NUMBER||i.type===t.BOOLEAN){s+=i.value;continue}if(i.type===t.SELECTOR||i.type===t.IDENTIFIER){s+=`"${i.value}"`;continue}s+=`"${i.value}"`}return s}try{let n=r(e),s=o(n);return JSON.parse(s)}catch(n){throw new x("Invalid extraction rules syntax: "+n.message,n)}}var Oe="hyper-html-api",Te="1";function _(e,t){let r=e.find(t,`script#${Oe}`,{includeRulesTag:!0});if(r.length===0)return null;let o=r[0],n=e.attr(o,"data-rules-version");if(n!==Te)throw new T(n);let s=e.text(o);return{rules:P(s),tagNode:o}}var G=null;function V(e){if(typeof e!="function")throw new TypeError("registerUpgrade expects a function (v1Data) => v2Data");if(G)throw new w;G=e}function H(){return G}function U(e,t){let r={carriedOver:0,discarded:0,listItems:0},o=Y(t,e,r);return X(t,e,r),{data:o,summary:r}}function Y(e,t,r){if(typeof e=="string")return e.endsWith("[]")?Array.isArray(t)?(r.listItems+=t.length,r.carriedOver+=t.length,t):void 0:t==null?void 0:(r.carriedOver++,t);if(Array.isArray(e)){let[,o]=e;return Array.isArray(t)?(r.listItems+=t.length,t.map(n=>Y(o,n,r))):void 0}if(typeof e=="object"&&e!==null){let o={};for(let[n,s]of Object.entries(e)){let c=Y(s,t?.[n],r);c!==void 0&&(o[n]=c)}return o}}function X(e,t,r){if(t!=null){if(typeof e=="object"&&e!==null&&!Array.isArray(e)){if(typeof t!="object"||Array.isArray(t))return;let o=new Set(Object.keys(e));for(let n of Object.keys(t))o.has(n)?X(e[n],t[n],r):r.discarded+=k(t[n]);return}if(Array.isArray(e)&&Array.isArray(t)){let o=e[1];t.forEach(n=>X(o,n,r));return}typeof e=="string"&&!e.endsWith("[]")&&typeof t=="object"&&t!==null&&(r.discarded+=k(t))}}function k(e){return e==null?0:Array.isArray(e)?e.reduce((t,r)=>t+k(r),0):typeof e=="object"?Object.values(e).reduce((t,r)=>t+k(r),0):1}async function z({sourceUrl:e,v1Data:t,v1Version:r=null,parent:o=typeof window<"u"?window:null,timeoutMs:n=15e3,hidden:s=!0}={}){if(!o)throw new Error("upgrade.run requires a window context");if(!e)throw new Error("upgrade.run requires sourceUrl");let c=be(e,o.location.origin),i=new URL(c,o.location.href).origin,f=o.document.createElement("iframe");s&&(f.style.cssText="position:absolute;left:-9999px;top:-9999px;width:1px;height:1px;border:0;"),f.setAttribute("aria-hidden","true"),f.src=c,o.document.body.appendChild(f);let a=()=>{};try{return await new Promise((u,p)=>{let l=setTimeout(()=>{p(new Error(`upgrade.run: helper iframe timed out after ${n}ms`))},n),R=y=>{if(y.source!==f.contentWindow||y.origin!==i)return;let h=y.data;if(!(!h||typeof h!="object")){if(h.type==="hha:upgrade-ready"){f.contentWindow.postMessage({type:"hha:upgrade-data",v1Data:t,v1Version:r},i);return}if(h.type==="hha:upgrade-result"){clearTimeout(l),u({html:h.html,summary:h.summary});return}if(h.type==="hha:upgrade-error"){clearTimeout(l);let d=new Error(h.message||"helper error");d.name=h.name||"UpgradeHelperError",p(d);return}}};o.addEventListener("message",R),a=()=>{o.removeEventListener("message",R),clearTimeout(l),f.parentNode&&f.parentNode.removeChild(f)}})}finally{a()}}function be(e,t){let r=new URL(e,typeof location<"u"?location.href:void 0);return r.searchParams.set("_hyperHtmlApi","upgrade-helper"),r.searchParams.set("parentOrigin",t),r.toString()}var we="hyper-html-api";function Ie(e){return e&&e.nodeType===1&&e.id===we&&e.tagName==="SCRIPT"}function Ne(e){return e?(e.nodeType===9||e.nodeType===11,e):null}var Le={find(e,t,r={}){let o=Ne(e);if(!o||!o.querySelectorAll)return[];let n=Array.from(o.querySelectorAll(t));return r.includeRulesTag?n:n.filter(s=>!Ie(s))},parent(e){return e?e.parentElement:null},children(e){return e?Array.from(e.children):[]},text(e,t){if(t===void 0)return(e.textContent||"").trim();e.textContent=t},attr(e,t,r){if(r===void 0)return e.hasAttribute&&e.hasAttribute(t)?e.getAttribute(t):null;e.setAttribute(t,r)},prop(e,t,r){if(r===void 0){let o=e?e[t]:void 0;return o!==void 0?o:null}e[t]=r},clone(e){return e.cloneNode(!0)},insertAt(e,t,r){let o=e.children[r]||null;e.insertBefore(t,o)},remove(e){e&&e.parentNode&&e.parentNode.removeChild(e)},stripIds(e){let t=0;return e.id&&(e.removeAttribute("id"),t++),(e.querySelectorAll?e.querySelectorAll("[id]"):[]).forEach(o=>{o.removeAttribute("id"),t++}),t},sameNode(e,t){return e===t}},g=Le;var Pe="_hyperHtmlApi",_e="upgrade-helper",Ue="parentOrigin";function q(e=typeof location<"u"?location:null){if(!e)return!1;try{return new URLSearchParams(e.search).get(Pe)===_e}catch{return!1}}function oe(e=typeof location<"u"?location:null){return e?new URLSearchParams(e.search).get(Ue):null}function Z({win:e,doc:t,parentOrigin:r}={}){if(e=e||(typeof window<"u"?window:null),t=t||(typeof document<"u"?document:null),!e||!t||(r=r||oe(e.location),!r))return;let o=()=>Ce({win:e,doc:t,parentOrigin:r});t.readyState==="loading"?t.addEventListener("DOMContentLoaded",o,{once:!0}):o()}function Ce({win:e,doc:t,parentOrigin:r}){let o;try{o=_(g,t.body)}catch(f){return K(e,r,f)}if(!o)return K(e,r,new Error("helper-mode: no rules tag in v2 document"));let n=o.rules,s=je(t,"hyper-version"),c=!!H(),i=f=>{if(f.source!==e.parent||f.origin!==r)return;let a=f.data;if(!(!a||a.type!=="hha:upgrade-data")){e.removeEventListener("message",i);try{let u=De({doc:t,rules:n,v1Data:a.v1Data});e.parent.postMessage({type:"hha:upgrade-result",html:u.html,summary:u.summary},r)}catch(u){K(e,r,u)}}};e.addEventListener("message",i),e.parent.postMessage({type:"hha:upgrade-ready",rules:n,version:s,hasTransform:c},r)}function De({doc:e,rules:t,v1Data:r}){let o=H(),n=r,s=!1;o&&(n=o(r),s=!0);let{data:c,summary:i}=U(n,t);L(g,e.body,t,c);let f=A(g,e.body,t);return{html:`<!DOCTYPE html>
|
|
2
|
+
`+e.documentElement.outerHTML,summary:{...i,transformApplied:s,appliedFieldCount:Q(f)}}}function K(e,t,r){e.parent.postMessage({type:"hha:upgrade-error",name:r?.name||"Error",message:r?.message||String(r)},t)}function je(e,t){let r=e.querySelector(`meta[name="${t}"]`);return r?r.getAttribute("content"):null}function Q(e){return e==null?0:Array.isArray(e)?e.reduce((t,r)=>t+Q(r),0):typeof e=="object"?Object.values(e).reduce((t,r)=>t+Q(r),0):1}var se={extract:(e,t)=>A(g,e,t),apply:(e,t,r)=>L(g,e,t,r),findRulesIn:e=>_(g,e),parseStrict:P,parseRelaxed:J,errors:D,DOM_PROPERTIES:C},ie={},fe={registerUpgrade:V,run:z,shapeMatch:U,isHelperMode:q};typeof window<"u"&&q(window.location)&&Z({win:window,doc:document});var ke={engine:se,cms:ie,upgrade:fe},qe=ke;return de(Be);})();
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hyper-html-api",
|
|
3
|
+
"description": "self-describing data layer for HTML files — extract, apply, CMS, upgrade",
|
|
4
|
+
"keywords": [
|
|
5
|
+
"HTML",
|
|
6
|
+
"DOM",
|
|
7
|
+
"data",
|
|
8
|
+
"extract",
|
|
9
|
+
"apply",
|
|
10
|
+
"CMS"
|
|
11
|
+
],
|
|
12
|
+
"version": "0.1.0",
|
|
13
|
+
"homepage": "https://github.com/panphora/hyper-html-api",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/panphora/hyper-html-api/issues"
|
|
16
|
+
},
|
|
17
|
+
"license": "0BSD",
|
|
18
|
+
"files": [
|
|
19
|
+
"LICENSE",
|
|
20
|
+
"README.md",
|
|
21
|
+
"src/**/*.js",
|
|
22
|
+
"dist/*.js"
|
|
23
|
+
],
|
|
24
|
+
"main": "src/hyper-html-api.js",
|
|
25
|
+
"module": "src/hyper-html-api.js",
|
|
26
|
+
"type": "module",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": "./src/hyper-html-api.js",
|
|
29
|
+
"./engine": "./src/engine/index.js",
|
|
30
|
+
"./cheerio": "./src/adapters/cheerio.js",
|
|
31
|
+
"./dom": "./src/adapters/dom.js"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "npx esbuild src/hyper-html-api.js --bundle --minify --format=iife --global-name=HyperHtmlApi --outfile=dist/hyper-html-api.min.js",
|
|
35
|
+
"prepublishOnly": "npm run build",
|
|
36
|
+
"copy-to-hyperclayjs": "npm run build && node scripts/copy-to-hyperclayjs.js",
|
|
37
|
+
"demo": "http-server . -p 5693 -c-1 -o /demo/",
|
|
38
|
+
"test": "web-test-runner",
|
|
39
|
+
"test:debug": "web-test-runner --manual --open",
|
|
40
|
+
"test:chrome": "web-test-runner --playwright --browsers chromium",
|
|
41
|
+
"test:firefox": "web-test-runner --concurrency 1 --playwright --browsers firefox",
|
|
42
|
+
"test:webkit": "web-test-runner --playwright --browsers webkit",
|
|
43
|
+
"test:all": "web-test-runner --concurrency 1 --playwright --browsers chromium firefox webkit",
|
|
44
|
+
"test:node": "node --test 'test-node/**/*.test.js'",
|
|
45
|
+
"test:ci": "npm run test:node && npm run test:all -- --fail-only && node test/lib/ensure-full-coverage.js",
|
|
46
|
+
"test:coverage": "npm run test:chrome && node test/lib/ensure-full-coverage.js",
|
|
47
|
+
"format": "prettier --write .",
|
|
48
|
+
"format:check": "prettier --check ."
|
|
49
|
+
},
|
|
50
|
+
"repository": {
|
|
51
|
+
"type": "git",
|
|
52
|
+
"url": "git+https://github.com/panphora/hyper-html-api.git"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@web/test-runner": "^0.20.0",
|
|
56
|
+
"@web/test-runner-playwright": "^0.11.0",
|
|
57
|
+
"chai": "^4.3.6",
|
|
58
|
+
"chai-dom": "^1.11.0",
|
|
59
|
+
"cheerio": "^1.0.0-rc.12",
|
|
60
|
+
"esbuild": "^0.24.0",
|
|
61
|
+
"http-server": "^14.1.1",
|
|
62
|
+
"jsdom": "^29.0.2",
|
|
63
|
+
"lcov-parse": "^1.0.0",
|
|
64
|
+
"mocha": "^11.0.1",
|
|
65
|
+
"prettier": "^3.4.2",
|
|
66
|
+
"sinon": "^9.2.4"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const RULES_TAG_ID = 'hyper-html-api'
|
|
2
|
+
|
|
3
|
+
function isRulesTag(node) {
|
|
4
|
+
if (!node || !node.attr) return false
|
|
5
|
+
if (node.attr('id') !== RULES_TAG_ID) return false
|
|
6
|
+
const tag = node.prop ? node.prop('tagName') : ''
|
|
7
|
+
return String(tag || '').toUpperCase() === 'SCRIPT'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function toWrappers(cheerioSet) {
|
|
11
|
+
const out = []
|
|
12
|
+
for (let i = 0; i < cheerioSet.length; i++) out.push(cheerioSet.eq(i))
|
|
13
|
+
return out
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const cheerioAdapter = {
|
|
17
|
+
find(ctx, selector, opts = {}) {
|
|
18
|
+
if (!ctx || !ctx.find) return []
|
|
19
|
+
const matches = toWrappers(ctx.find(selector))
|
|
20
|
+
if (opts.includeRulesTag) return matches
|
|
21
|
+
return matches.filter((n) => !isRulesTag(n))
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
parent(node) {
|
|
25
|
+
if (!node || !node.parent) return null
|
|
26
|
+
const p = node.parent()
|
|
27
|
+
return p && p.length ? p : null
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
children(node) {
|
|
31
|
+
if (!node || !node.children) return []
|
|
32
|
+
return toWrappers(node.children())
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
text(node, value) {
|
|
36
|
+
if (value === undefined) return node.text().trim()
|
|
37
|
+
node.text(value)
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
attr(node, name, value) {
|
|
41
|
+
if (value === undefined) {
|
|
42
|
+
const v = node.attr(name)
|
|
43
|
+
return v !== undefined ? v : null
|
|
44
|
+
}
|
|
45
|
+
node.attr(name, value)
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
prop(node, name, value) {
|
|
49
|
+
if (value === undefined) {
|
|
50
|
+
const v = node.prop(name)
|
|
51
|
+
return v !== undefined ? v : null
|
|
52
|
+
}
|
|
53
|
+
node.prop(name, value)
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
clone(node) {
|
|
57
|
+
return node.clone()
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
insertAt(parent, node, index) {
|
|
61
|
+
const siblings = parent.children()
|
|
62
|
+
if (index >= siblings.length) parent.append(node)
|
|
63
|
+
else siblings.eq(index).before(node)
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
remove(node) {
|
|
67
|
+
if (node) node.remove()
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
stripIds(node) {
|
|
71
|
+
let count = 0
|
|
72
|
+
if (node.attr('id')) {
|
|
73
|
+
node.removeAttr('id')
|
|
74
|
+
count++
|
|
75
|
+
}
|
|
76
|
+
node.find('[id]').each(function () {
|
|
77
|
+
if (this.attribs && this.attribs.id !== undefined) {
|
|
78
|
+
delete this.attribs.id
|
|
79
|
+
count++
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
return count
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
sameNode(a, b) {
|
|
86
|
+
if (!a || !b) return false
|
|
87
|
+
const aEl = a[0]
|
|
88
|
+
const bEl = b[0]
|
|
89
|
+
return !!aEl && aEl === bEl
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export default cheerioAdapter
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const RULES_TAG_ID = 'hyper-html-api'
|
|
2
|
+
|
|
3
|
+
function isRulesTag(node) {
|
|
4
|
+
return node && node.nodeType === 1 && node.id === RULES_TAG_ID && node.tagName === 'SCRIPT'
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function resolveSearchRoot(ctx) {
|
|
8
|
+
if (!ctx) return null
|
|
9
|
+
if (ctx.nodeType === 9) return ctx
|
|
10
|
+
if (ctx.nodeType === 11) return ctx
|
|
11
|
+
return ctx
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const dom = {
|
|
15
|
+
find(ctx, selector, opts = {}) {
|
|
16
|
+
const root = resolveSearchRoot(ctx)
|
|
17
|
+
if (!root || !root.querySelectorAll) return []
|
|
18
|
+
const all = Array.from(root.querySelectorAll(selector))
|
|
19
|
+
if (opts.includeRulesTag) return all
|
|
20
|
+
return all.filter((n) => !isRulesTag(n))
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
parent(node) {
|
|
24
|
+
return node ? node.parentElement : null
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
children(node) {
|
|
28
|
+
return node ? Array.from(node.children) : []
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
text(node, value) {
|
|
32
|
+
if (value === undefined) return (node.textContent || '').trim()
|
|
33
|
+
node.textContent = value
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
attr(node, name, value) {
|
|
37
|
+
if (value === undefined) {
|
|
38
|
+
return node.hasAttribute && node.hasAttribute(name) ? node.getAttribute(name) : null
|
|
39
|
+
}
|
|
40
|
+
node.setAttribute(name, value)
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
prop(node, name, value) {
|
|
44
|
+
if (value === undefined) {
|
|
45
|
+
const v = node ? node[name] : undefined
|
|
46
|
+
return v !== undefined ? v : null
|
|
47
|
+
}
|
|
48
|
+
node[name] = value
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
clone(node) {
|
|
52
|
+
return node.cloneNode(true)
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
insertAt(parent, node, index) {
|
|
56
|
+
const ref = parent.children[index] || null
|
|
57
|
+
parent.insertBefore(node, ref)
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
remove(node) {
|
|
61
|
+
if (node && node.parentNode) node.parentNode.removeChild(node)
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
stripIds(node) {
|
|
65
|
+
let count = 0
|
|
66
|
+
if (node.id) {
|
|
67
|
+
node.removeAttribute('id')
|
|
68
|
+
count++
|
|
69
|
+
}
|
|
70
|
+
const inner = node.querySelectorAll ? node.querySelectorAll('[id]') : []
|
|
71
|
+
inner.forEach((el) => {
|
|
72
|
+
el.removeAttribute('id')
|
|
73
|
+
count++
|
|
74
|
+
})
|
|
75
|
+
return count
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
sameNode(a, b) {
|
|
79
|
+
return a === b
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export default dom
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { DOM_PROPERTIES_SET } from './dom-properties.js'
|
|
2
|
+
import { MaxRuleDepthExceeded, ShapeMismatch } from './errors.js'
|
|
3
|
+
import { listDiff } from './diff.js'
|
|
4
|
+
|
|
5
|
+
const MAX_DEPTH = 20
|
|
6
|
+
|
|
7
|
+
const BOOLEAN_PROPS = new Set(['checked', 'selected', 'disabled', 'readOnly', 'paused'])
|
|
8
|
+
|
|
9
|
+
export function apply(adapter, root, rules, data) {
|
|
10
|
+
const mismatches = []
|
|
11
|
+
validateShape(rules, data, [], mismatches)
|
|
12
|
+
if (mismatches.length) throw new ShapeMismatch(mismatches)
|
|
13
|
+
|
|
14
|
+
applyAt(adapter, root, rules, data, { depth: 0, path: [] })
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function applyAt(adapter, ctx, rule, value, trace) {
|
|
18
|
+
if (trace.depth > MAX_DEPTH) throw new MaxRuleDepthExceeded(trace.path)
|
|
19
|
+
if (value === undefined) return
|
|
20
|
+
|
|
21
|
+
if (typeof rule === 'string') return applyScalar(adapter, ctx, rule, value, trace)
|
|
22
|
+
|
|
23
|
+
if (Array.isArray(rule)) {
|
|
24
|
+
const [selector, shape] = rule
|
|
25
|
+
return listDiff(adapter, ctx, selector, shape, value, trace, applyAt)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (typeof rule === 'object' && rule !== null) {
|
|
29
|
+
for (const [key, sub] of Object.entries(rule)) {
|
|
30
|
+
applyAt(
|
|
31
|
+
adapter,
|
|
32
|
+
ctx,
|
|
33
|
+
sub,
|
|
34
|
+
value == null ? value : value[key],
|
|
35
|
+
{ depth: trace.depth + 1, path: [...trace.path, key] },
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function applyScalar(adapter, ctx, rule, value, trace) {
|
|
42
|
+
if (rule.endsWith('[]')) {
|
|
43
|
+
const selector = rule.slice(0, -2)
|
|
44
|
+
return listDiff(adapter, ctx, selector, null, value, trace, applyAt)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (rule.startsWith('@')) {
|
|
48
|
+
return writePropOrAttr(adapter, ctx, rule.slice(1), value)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (rule.includes('@')) {
|
|
52
|
+
const [selector, name] = rule.split('@')
|
|
53
|
+
const matches = selector ? adapter.find(ctx, selector) : [ctx]
|
|
54
|
+
if (matches.length === 0) return
|
|
55
|
+
return writePropOrAttr(adapter, matches[0], name, value)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (rule === '.') {
|
|
59
|
+
adapter.text(ctx, value == null ? '' : String(value))
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const matches = adapter.find(ctx, rule)
|
|
64
|
+
if (matches.length === 0) return
|
|
65
|
+
adapter.text(matches[0], value == null ? '' : String(value))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function writePropOrAttr(adapter, node, name, value) {
|
|
69
|
+
if (DOM_PROPERTIES_SET.has(name)) {
|
|
70
|
+
adapter.prop(node, name, coercePropValue(name, value))
|
|
71
|
+
} else {
|
|
72
|
+
adapter.attr(node, name, value == null ? '' : String(value))
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function coercePropValue(name, value) {
|
|
77
|
+
if (value === null || value === undefined) {
|
|
78
|
+
return BOOLEAN_PROPS.has(name) ? false : ''
|
|
79
|
+
}
|
|
80
|
+
if (BOOLEAN_PROPS.has(name)) return Boolean(value)
|
|
81
|
+
return value
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function validateShape(rule, value, path, mismatches) {
|
|
85
|
+
if (value === undefined || value === null || value === '') return
|
|
86
|
+
|
|
87
|
+
if (typeof rule === 'string') {
|
|
88
|
+
if (rule.endsWith('[]')) {
|
|
89
|
+
if (!Array.isArray(value)) {
|
|
90
|
+
mismatches.push({ path: pathStr(path), expected: 'array', got: typeofX(value) })
|
|
91
|
+
} else {
|
|
92
|
+
value.forEach((v, i) => {
|
|
93
|
+
if (typeof v === 'object' && v !== null) {
|
|
94
|
+
mismatches.push({
|
|
95
|
+
path: pathStr([...path, i]),
|
|
96
|
+
expected: 'scalar',
|
|
97
|
+
got: typeofX(v),
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
if (typeof value === 'object') {
|
|
105
|
+
mismatches.push({ path: pathStr(path), expected: 'scalar', got: typeofX(value) })
|
|
106
|
+
}
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (Array.isArray(rule)) {
|
|
111
|
+
if (!Array.isArray(value)) {
|
|
112
|
+
mismatches.push({ path: pathStr(path), expected: 'array', got: typeofX(value) })
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
const shape = rule[1]
|
|
116
|
+
value.forEach((item, i) => validateShape(shape, item, [...path, i], mismatches))
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (typeof rule === 'object' && rule !== null) {
|
|
121
|
+
if (Array.isArray(value) || typeof value !== 'object') {
|
|
122
|
+
mismatches.push({ path: pathStr(path), expected: 'object', got: typeofX(value) })
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
for (const [k, sub] of Object.entries(rule)) {
|
|
126
|
+
validateShape(sub, value[k], [...path, k], mismatches)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function typeofX(v) {
|
|
132
|
+
if (v === null) return 'null'
|
|
133
|
+
if (Array.isArray(v)) return 'array'
|
|
134
|
+
return typeof v
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function pathStr(path) {
|
|
138
|
+
return path.join('.')
|
|
139
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { EmptyListInsert } from './errors.js'
|
|
2
|
+
import { extract } from './extract.js'
|
|
3
|
+
|
|
4
|
+
const SIMILARITY_THRESHOLD = 0.5
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Reconcile `newItems` (array of incoming items) into the DOM subtree matched
|
|
8
|
+
* by `parentCtx.find(selector)`. `shape` is null for scalar lists (items are
|
|
9
|
+
* strings) or the rule shape for object lists.
|
|
10
|
+
*
|
|
11
|
+
* applyItem is passed in (normally `applyAt` from apply.js) to avoid a
|
|
12
|
+
* circular import; object-list items use it to recursively apply their shape.
|
|
13
|
+
*/
|
|
14
|
+
export function listDiff(adapter, parentCtx, selector, shape, newItems, trace, applyItem) {
|
|
15
|
+
const oldNodes = adapter.find(parentCtx, selector)
|
|
16
|
+
|
|
17
|
+
if (newItems.length === 0) {
|
|
18
|
+
oldNodes.forEach((n) => adapter.remove(n))
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const needsTemplate = newItems.length > oldNodes.length
|
|
23
|
+
if (needsTemplate && oldNodes.length === 0) throw new EmptyListInsert(trace.path)
|
|
24
|
+
|
|
25
|
+
const oldValues = oldNodes.map((n) => extractItem(adapter, n, shape))
|
|
26
|
+
|
|
27
|
+
let template = null
|
|
28
|
+
if (oldNodes.length > 0) {
|
|
29
|
+
template = adapter.clone(oldNodes[0])
|
|
30
|
+
const stripped = adapter.stripIds(template)
|
|
31
|
+
if (stripped > 0) {
|
|
32
|
+
// eslint-disable-next-line no-console
|
|
33
|
+
console.warn(
|
|
34
|
+
`[hyper-html-api] stripped ${stripped} id attribute(s) from cloned template at "${trace.path.join('.') || '(root)'}"`,
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const matches = greedyMatch(newItems, oldValues, shape)
|
|
40
|
+
|
|
41
|
+
const parent = adapter.parent(oldNodes[0])
|
|
42
|
+
const anchorIdx = indexInParent(adapter, parent, oldNodes[0])
|
|
43
|
+
|
|
44
|
+
const used = new Set()
|
|
45
|
+
const finalNodes = newItems.map((_, i) => {
|
|
46
|
+
const oldIdx = matches[i]
|
|
47
|
+
if (oldIdx >= 0) {
|
|
48
|
+
used.add(oldIdx)
|
|
49
|
+
return oldNodes[oldIdx]
|
|
50
|
+
}
|
|
51
|
+
const cloned = adapter.clone(template)
|
|
52
|
+
adapter.stripIds(cloned)
|
|
53
|
+
return cloned
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
oldNodes.forEach((n, i) => {
|
|
57
|
+
if (!used.has(i)) adapter.remove(n)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
used.forEach((i) => adapter.remove(oldNodes[i]))
|
|
61
|
+
|
|
62
|
+
finalNodes.forEach((node, i) => {
|
|
63
|
+
adapter.insertAt(parent, node, anchorIdx + i)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
finalNodes.forEach((node, i) => {
|
|
67
|
+
if (shape === null) {
|
|
68
|
+
const v = newItems[i]
|
|
69
|
+
adapter.text(node, v == null ? '' : String(v))
|
|
70
|
+
} else {
|
|
71
|
+
applyItem(adapter, node, shape, newItems[i], {
|
|
72
|
+
depth: trace.depth + 1,
|
|
73
|
+
path: [...trace.path, i],
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function extractItem(adapter, node, shape) {
|
|
80
|
+
if (shape === null) return adapter.text(node)
|
|
81
|
+
return extract(adapter, node, shape)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function greedyMatch(newItems, oldValues, shape) {
|
|
85
|
+
const matches = new Array(newItems.length).fill(-1)
|
|
86
|
+
const taken = new Set()
|
|
87
|
+
newItems.forEach((newItem, i) => {
|
|
88
|
+
let bestIdx = -1
|
|
89
|
+
let bestScore = -1
|
|
90
|
+
oldValues.forEach((oldVal, j) => {
|
|
91
|
+
if (taken.has(j)) return
|
|
92
|
+
const score = similarity(newItem, oldVal, shape)
|
|
93
|
+
const closerIdx =
|
|
94
|
+
score === bestScore && bestIdx >= 0
|
|
95
|
+
? Math.abs(j - i) < Math.abs(bestIdx - i)
|
|
96
|
+
: false
|
|
97
|
+
if (score > bestScore || closerIdx) {
|
|
98
|
+
bestScore = score
|
|
99
|
+
bestIdx = j
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
if (bestScore >= SIMILARITY_THRESHOLD) {
|
|
103
|
+
matches[i] = bestIdx
|
|
104
|
+
taken.add(bestIdx)
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
return matches
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function similarity(a, b, shape) {
|
|
111
|
+
if (shape === null) return a === b ? 1 : 0
|
|
112
|
+
const fields = Object.keys(shape || {})
|
|
113
|
+
if (fields.length === 0) return 0
|
|
114
|
+
let equal = 0
|
|
115
|
+
for (const f of fields) {
|
|
116
|
+
if (JSON.stringify(a == null ? undefined : a[f]) === JSON.stringify(b == null ? undefined : b[f])) {
|
|
117
|
+
equal++
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return equal / fields.length
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function indexInParent(adapter, parent, targetNode) {
|
|
124
|
+
const siblings = adapter.children(parent)
|
|
125
|
+
for (let i = 0; i < siblings.length; i++) {
|
|
126
|
+
if (adapter.sameNode(siblings[i], targetNode)) return i
|
|
127
|
+
}
|
|
128
|
+
return -1
|
|
129
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Names that are read/written via the DOM/cheerio .prop() interface rather
|
|
3
|
+
* than .attr(). Copied verbatim from hyperclay/server-lib/data-extractor.js
|
|
4
|
+
* so the new engine preserves byte-identical output.
|
|
5
|
+
*/
|
|
6
|
+
export const DOM_PROPERTIES = [
|
|
7
|
+
'textContent',
|
|
8
|
+
'innerText',
|
|
9
|
+
'innerHTML',
|
|
10
|
+
'outerHTML',
|
|
11
|
+
|
|
12
|
+
'value',
|
|
13
|
+
'checked',
|
|
14
|
+
'selected',
|
|
15
|
+
'disabled',
|
|
16
|
+
'readOnly',
|
|
17
|
+
'type',
|
|
18
|
+
|
|
19
|
+
'tagName',
|
|
20
|
+
'nodeName',
|
|
21
|
+
'nodeType',
|
|
22
|
+
'nodeValue',
|
|
23
|
+
'childElementCount',
|
|
24
|
+
|
|
25
|
+
'id',
|
|
26
|
+
'className',
|
|
27
|
+
'classList',
|
|
28
|
+
|
|
29
|
+
'href',
|
|
30
|
+
'src',
|
|
31
|
+
'action',
|
|
32
|
+
'baseURI',
|
|
33
|
+
|
|
34
|
+
'offsetWidth',
|
|
35
|
+
'offsetHeight',
|
|
36
|
+
'clientWidth',
|
|
37
|
+
'clientHeight',
|
|
38
|
+
'scrollWidth',
|
|
39
|
+
'scrollHeight',
|
|
40
|
+
|
|
41
|
+
'dataset',
|
|
42
|
+
|
|
43
|
+
'currentSrc',
|
|
44
|
+
'duration',
|
|
45
|
+
'paused',
|
|
46
|
+
|
|
47
|
+
'title',
|
|
48
|
+
'documentURI',
|
|
49
|
+
'contentType',
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
export const DOM_PROPERTIES_SET = new Set(DOM_PROPERTIES)
|