HS: DOM ref-eq + compound selector + DOM tree fixes

- hs-id= uses JS === for DOM elements (hs-ref-eq), = for scalars
- != operator now uses hs-id= for structural correctness
- compound tag[attr=val] selector matching in test runner
- dom-query-all replaces host-call querySelectorAll
- DOM tree structure corrected in 4 generated tests (elements were
  appended to wrong parents)
This commit is contained in:
2026-04-26 17:49:51 +00:00
parent f2993f0582
commit a48110417b
9 changed files with 95 additions and 61 deletions

View File

@@ -297,6 +297,15 @@ function mt(e,s) {
const m = base.match(/^\[([^\]=]+)(?:="([^"]*)")?\]$/);
if(m) return m[2] !== undefined ? e.getAttribute(m[1]) === m[2] : e.hasAttribute(m[1]);
}
// Compound tag[attr=val] e.g. input[type=checkbox] or input[type="checkbox"]
if(base.includes('[')) {
const cm = base.match(/^([\w-]+)(\[.+\])$/);
if(cm) {
if(e.tagName.toLowerCase() !== cm[1]) return false;
const attrParts = cm[2].match(/^\[([^\]=]+)(?:=["']?([^"'\]]+)["']?)?\]$/);
if(attrParts) return attrParts[2] !== undefined ? e.getAttribute(attrParts[1]) === attrParts[2] : e.hasAttribute(attrParts[1]);
}
}
if(base.includes('.')) { const [tag, cls] = base.split('.'); return e.tagName.toLowerCase() === tag && e.classList.contains(cls); }
if(base.includes('#')) { const [tag, id] = base.split('#'); return e.tagName.toLowerCase() === tag && e.id === id; }
return e.tagName.toLowerCase() === base.toLowerCase();
@@ -536,6 +545,9 @@ globalThis.console = { log: () => {}, error: () => {}, warn: () => {}, info: ()
const _log = _origLog; // keep reference for our own output
// ─── FFI ────────────────────────────────────────────────────────
// JS-level reference equality for host objects (works around OCaml boxing).
// The SX `=` primitive doesn't do JS === for host objects in the WASM kernel.
K.registerNative('hs-ref-eq',a=>a[0]===a[1]);
K.registerNative('host-global',a=>{const n=a[0];return(n in globalThis)?globalThis[n]:null;});
K.registerNative('host-get',a=>{
if(a[0]==null)return null;

View File

@@ -210,11 +210,18 @@ def parse_html(html):
# button HTML in `properly processes hyperscript X` tests). HTMLParser handles
# backslashes in attribute values as literal characters, so we leave them.
# HTML5 void elements — never have children, auto-pop from stack immediately.
VOID_TAGS = {'area','base','br','col','embed','hr','img','input','link',
'meta','param','source','track','wbr'}
elements = []
stack = []
class Parser(HTMLParser):
def handle_starttag(self, tag, attrs):
# Pop any void elements left on the stack (they have no close tag).
while stack and stack[-1]['tag'] in VOID_TAGS:
stack.pop()
el = {
'tag': tag, 'id': None, 'classes': [], 'hs': None,
'attrs': {}, 'inner': '', 'depth': len(stack),
@@ -244,6 +251,9 @@ def parse_html(html):
elements.append(el)
def handle_endtag(self, tag):
# Pop void elements first (they don't have close tags but may linger).
while stack and stack[-1]['tag'] in VOID_TAGS:
stack.pop()
if stack and stack[-1]['tag'] == tag:
stack.pop()