HS: extend parser/runtime + new node test runner; ignore test-results/
- Parser: `--` line comments, `|` op, `result` → `the-result`, query-scoped `<sel> in <expr>`, `is a/an <type>` predicate, multi-`as` chaining with `|`, `match`/`precede` keyword aliases, `[attr]` add/toggle, between attr forms - Runtime: per-element listener registry + hs-deactivate!, attr toggle variants, set-inner-html boots subtree, hs-append polymorphic on string/list/element, default? / array-set! / query-all-in / list-set via take+drop, hs-script idempotence guard - Integration: skip reserved (me/it/event/you/yourself) when collecting vars - Tokenizer: emit `--` comments and `|` op - Test framework + conformance runner updates; new tests/hs-run-filtered.js (single-process Node runner using OCaml VM step-limit to bound infinite loops); generate-sx-conformance-dev.py improvements - mcp_tree.ml + run_tests.ml: harness extensions - .gitignore: top-level test-results/ (Playwright artifacts) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -155,6 +155,192 @@ let register_mcp_jit_hook () =
|
||||
| None -> None))
|
||||
| _ -> None)
|
||||
|
||||
(* ------------------------------------------------------------------ *)
|
||||
(* Native tree-tools helpers — avoid CEK overhead on big trees. *)
|
||||
(* Mirror the SX semantics from lib/tree-tools.sx but run as direct *)
|
||||
(* OCaml recursion. Used by read-subtree / validate / find-all / *)
|
||||
(* find-across / comp-usage handlers. *)
|
||||
(* ------------------------------------------------------------------ *)
|
||||
|
||||
let native_path_str path =
|
||||
"[" ^ String.concat "," (List.map string_of_int path) ^ "]"
|
||||
|
||||
let rec native_node_display (node : value) : string =
|
||||
match node with
|
||||
| Nil -> "nil"
|
||||
| Symbol s -> s
|
||||
| Keyword k -> ":" ^ k
|
||||
| String s -> "\"" ^ s ^ "\""
|
||||
| Number n ->
|
||||
if Float.is_integer n then string_of_int (int_of_float n)
|
||||
else Printf.sprintf "%g" n
|
||||
| Bool true -> "true"
|
||||
| Bool false -> "false"
|
||||
| List [] | ListRef { contents = [] } -> "()"
|
||||
| List (h :: t) | ListRef { contents = h :: t } ->
|
||||
if t = [] then "(" ^ native_node_display h ^ ")"
|
||||
else "(" ^ native_node_display h ^ " ...)"
|
||||
| Dict _ -> "{...}"
|
||||
| v -> Sx_runtime.value_to_str v
|
||||
|
||||
(* node-summary-short: "(head)" for singletons, "(head second ...)" for >3,
|
||||
"(all node-displays joined)" for small lists. Mirrors lib/tree-tools.sx. *)
|
||||
let native_node_summary_short node =
|
||||
match node with
|
||||
| List [] | ListRef { contents = [] } -> "()"
|
||||
| List items | ListRef { contents = items } ->
|
||||
let n = List.length items in
|
||||
if n > 3 then
|
||||
let head = native_node_display (List.hd items) in
|
||||
let second = native_node_display (List.nth items 1) in
|
||||
Printf.sprintf "(%s %s ...)" head second
|
||||
else
|
||||
"(" ^ String.concat " " (List.map native_node_display items) ^ ")"
|
||||
| _ -> native_node_display node
|
||||
|
||||
let rec native_node_matches node pattern =
|
||||
match node with
|
||||
| Symbol s | String s ->
|
||||
(try ignore (Str.search_forward (Str.regexp_string pattern) s 0); true
|
||||
with Not_found -> false)
|
||||
| List items | ListRef { contents = items } ->
|
||||
List.exists (fun c -> native_node_matches c pattern) items
|
||||
| _ -> false
|
||||
|
||||
(* Raw find: returns reversed list of (path, summary) pairs — caller reverses. *)
|
||||
let native_find_all_raw exprs pattern =
|
||||
let nodes = match exprs with
|
||||
| List xs | ListRef { contents = xs } -> xs
|
||||
| x -> [x] in
|
||||
let acc = ref [] in
|
||||
let rec go node path =
|
||||
if native_node_matches node pattern then
|
||||
acc := (List.rev path, native_node_summary_short node) :: !acc;
|
||||
match node with
|
||||
| List items | ListRef { contents = items } ->
|
||||
List.iteri (fun i child -> go child (i :: path)) items
|
||||
| _ -> ()
|
||||
in
|
||||
List.iteri (fun i node -> go node [i]) nodes;
|
||||
List.rev !acc
|
||||
|
||||
(* SX-value-returning wrapper — kept for any SX code that still calls find-all. *)
|
||||
let native_find_all_sx exprs pattern =
|
||||
let pairs = native_find_all_raw exprs pattern in
|
||||
List (List.map (fun (path, summary) ->
|
||||
List [List (List.map (fun i -> Number (float_of_int i)) path);
|
||||
String summary]
|
||||
) pairs)
|
||||
|
||||
(* navigate: walk a tree by a list of indices. Wraps exprs into a list on entry
|
||||
(mirrors SX navigate). Returns Nil if any index is out of range. *)
|
||||
let native_navigate exprs path =
|
||||
let init = match exprs with
|
||||
| List _ | ListRef _ -> exprs
|
||||
| x -> List [x] in
|
||||
let rec step current = function
|
||||
| [] -> current
|
||||
| i :: rest ->
|
||||
(match current with
|
||||
| List items when i >= 0 && i < List.length items ->
|
||||
step (List.nth items i) rest
|
||||
| ListRef { contents = items } when i >= 0 && i < List.length items ->
|
||||
step (List.nth items i) rest
|
||||
| _ -> Nil) in
|
||||
step init path
|
||||
|
||||
(* annotate-tree: render a list of exprs as an indented path-annotated string.
|
||||
Mirrors the `annotate-node` dispatch from lib/tree-tools.sx:
|
||||
- small lists (len<=4, no list children) render inline
|
||||
- others render head on first line, children indented, closing ")"
|
||||
Path annotations use [i,j,k,...] form. *)
|
||||
let native_annotate_tree exprs =
|
||||
let nodes = match exprs with
|
||||
| List xs | ListRef { contents = xs } -> xs
|
||||
| x -> [x] in
|
||||
let out = Buffer.create 1024 in
|
||||
let first = ref true in
|
||||
let emit s =
|
||||
if !first then first := false else Buffer.add_char out '\n';
|
||||
Buffer.add_string out s in
|
||||
let rec ann node path depth =
|
||||
let indent = String.make (depth * 2) ' ' in
|
||||
let label = native_path_str (List.rev path) in
|
||||
match node with
|
||||
| List [] | ListRef { contents = [] } ->
|
||||
emit (indent ^ label ^ " ()")
|
||||
| List items | ListRef { contents = items } ->
|
||||
let n = List.length items in
|
||||
let rest = List.tl items in
|
||||
let any_child_list = List.exists
|
||||
(fun c -> match c with List _ | ListRef _ -> true | _ -> false) rest in
|
||||
if n <= 4 && not any_child_list then
|
||||
emit (indent ^ label ^ " (" ^
|
||||
String.concat " " (List.map native_node_display items) ^ ")")
|
||||
else begin
|
||||
let head_str = native_node_display (List.hd items) in
|
||||
emit (indent ^ label ^ " (" ^ head_str);
|
||||
List.iteri (fun i child ->
|
||||
if i > 0 then ann child (i :: path) (depth + 1)
|
||||
) items;
|
||||
emit (indent ^ " )")
|
||||
end
|
||||
| _ ->
|
||||
emit (indent ^ label ^ " " ^ native_node_display node)
|
||||
in
|
||||
List.iteri (fun i node -> ann node [i] 0) nodes;
|
||||
Buffer.contents out
|
||||
|
||||
let native_read_subtree exprs path =
|
||||
let node = native_navigate exprs path in
|
||||
match node with
|
||||
| Nil -> "Error: path " ^ native_path_str path ^ " not found"
|
||||
| _ -> native_annotate_tree (List [node])
|
||||
|
||||
(* validate: walk the tree, emit WARNING for malformed letrec bindings and
|
||||
ERROR for defisland/defcomp with fewer than 3 args. *)
|
||||
let native_validate exprs =
|
||||
let errors = ref [] in
|
||||
let emit s = errors := s :: !errors in
|
||||
let rec go node path =
|
||||
(match node with
|
||||
| List items | ListRef { contents = items } ->
|
||||
(match items with
|
||||
| [] -> ()
|
||||
| head :: _ ->
|
||||
let head_name = match head with Symbol s -> Some s | _ -> None in
|
||||
(match head_name with
|
||||
| Some "letrec" when List.length items >= 2 ->
|
||||
let bindings = List.nth items 1 in
|
||||
(match bindings with
|
||||
| List pairs | ListRef { contents = pairs } ->
|
||||
List.iteri (fun i pair ->
|
||||
let ok = match pair with
|
||||
| List (Symbol _ :: _ :: _) -> true
|
||||
| ListRef { contents = Symbol _ :: _ :: _ } -> true
|
||||
| _ -> false in
|
||||
if not ok then
|
||||
emit (Printf.sprintf
|
||||
"WARNING %s: letrec binding %d is not a (name value) pair: %s"
|
||||
(native_path_str (List.rev (i :: 1 :: path)))
|
||||
i
|
||||
(native_node_display pair))
|
||||
) pairs
|
||||
| _ -> ())
|
||||
| Some (("defisland" | "defcomp") as nm) when List.length items < 4 ->
|
||||
emit (Printf.sprintf
|
||||
"ERROR %s: %s has fewer than 3 args (name params b..."
|
||||
(native_path_str (List.rev path)) nm)
|
||||
| _ -> ()));
|
||||
List.iteri (fun i child -> go child (i :: path)) items
|
||||
| _ -> ())
|
||||
in
|
||||
let nodes = match exprs with
|
||||
| List xs | ListRef { contents = xs } -> xs
|
||||
| x -> [x] in
|
||||
List.iteri (fun i node -> go node [i]) nodes;
|
||||
if !errors = [] then "OK" else String.concat "\n" (List.rev !errors)
|
||||
|
||||
let setup_env () =
|
||||
let e = make_env () in
|
||||
(* Primitives are auto-registered at module init *)
|
||||
@@ -439,6 +625,30 @@ let setup_env () =
|
||||
try load_sx_file e (Filename.concat lib_dir "compiler.sx");
|
||||
register_mcp_jit_hook ()
|
||||
with exn -> Printf.eprintf "[mcp] Warning: compiler.sx load failed (JIT disabled): %s\n%!" (Printexc.to_string exn));
|
||||
|
||||
(* Native-impl the hot tree-tools ops — replaces SX versions to avoid CEK
|
||||
overhead on big trees. See native_* helpers defined above setup_env. *)
|
||||
ignore (Sx_types.env_bind e "find-all" (NativeFn ("find-all", fun args ->
|
||||
match args with
|
||||
| [exprs; String pattern] -> native_find_all_sx exprs pattern
|
||||
| _ -> List [])));
|
||||
ignore (Sx_types.env_bind e "read-subtree" (NativeFn ("read-subtree", fun args ->
|
||||
match args with
|
||||
| [exprs; List path] | [exprs; ListRef { contents = path }] ->
|
||||
let ints = List.map (fun v -> match v with Number n -> int_of_float n | _ -> 0) path in
|
||||
String (native_read_subtree exprs ints)
|
||||
| _ -> String "")));
|
||||
ignore (Sx_types.env_bind e "validate" (NativeFn ("validate", fun args ->
|
||||
match args with
|
||||
| [exprs] -> String (native_validate exprs)
|
||||
| _ -> String "")));
|
||||
ignore (Sx_types.env_bind e "path-str" (NativeFn ("path-str", fun args ->
|
||||
match args with
|
||||
| [List path] | [ListRef { contents = path }] ->
|
||||
let ints = List.map (fun v -> match v with Number n -> int_of_float n | _ -> 0) path in
|
||||
String (native_path_str ints)
|
||||
| _ -> String "[]")));
|
||||
|
||||
Printf.eprintf "[mcp] Ready in %.0fms\n%!" ((Unix.gettimeofday () -. t0) *. 1000.0);
|
||||
env := e
|
||||
|
||||
@@ -804,7 +1014,11 @@ let handle_sx_read_subtree args =
|
||||
let open Yojson.Safe.Util in
|
||||
let tree = parse_file (require_file args "file") in
|
||||
let path = resolve_path tree (args |> member "path" |> to_string) in
|
||||
text_result (value_to_string (call_sx "read-subtree" [tree; path]))
|
||||
let ints = match path with
|
||||
| List xs | ListRef { contents = xs } ->
|
||||
List.map (fun v -> match v with Number n -> int_of_float n | _ -> 0) xs
|
||||
| _ -> [] in
|
||||
text_result (native_read_subtree tree ints)
|
||||
|
||||
let handle_sx_get_context args =
|
||||
let open Yojson.Safe.Util in
|
||||
@@ -816,17 +1030,8 @@ let handle_sx_find_all args =
|
||||
let open Yojson.Safe.Util in
|
||||
let tree = parse_file (require_file args "file") in
|
||||
let pattern = args |> member "pattern" |> to_string in
|
||||
let results = call_sx "find-all" [tree; String pattern] in
|
||||
let lines = match results with
|
||||
| List items | ListRef { contents = items } ->
|
||||
List.map (fun item ->
|
||||
match item with
|
||||
| List [p; s] | ListRef { contents = [p; s] } ->
|
||||
value_to_string (call_sx "path-str" [p]) ^ " " ^ value_to_string s
|
||||
| _ -> value_to_string item
|
||||
) items
|
||||
| _ -> [value_to_string results]
|
||||
in
|
||||
let results = native_find_all_raw tree pattern in
|
||||
let lines = List.map (fun (p, s) -> native_path_str p ^ " " ^ s) results in
|
||||
text_result (String.concat "\n" lines)
|
||||
|
||||
let handle_sx_get_siblings args =
|
||||
@@ -843,7 +1048,7 @@ let handle_sx_get_siblings args =
|
||||
|
||||
let handle_sx_validate args =
|
||||
let tree = parse_file (require_file args "file") in
|
||||
text_result (value_to_string (call_sx "validate" [tree]))
|
||||
text_result (native_validate tree)
|
||||
|
||||
let handle_sx_replace_node args =
|
||||
let open Yojson.Safe.Util in
|
||||
@@ -1912,16 +2117,8 @@ let handle_sx_find_across args =
|
||||
let rel = relative_path ~base:dir path in
|
||||
try
|
||||
let tree = parse_file path in
|
||||
let results = call_sx "find-all" [tree; String pattern] in
|
||||
(match results with
|
||||
| List items | ListRef { contents = items } ->
|
||||
List.map (fun item ->
|
||||
match item with
|
||||
| List [p; s] | ListRef { contents = [p; s] } ->
|
||||
rel ^ " " ^ value_to_string (call_sx "path-str" [p]) ^ " " ^ value_to_string s
|
||||
| _ -> rel ^ " " ^ value_to_string item
|
||||
) items
|
||||
| _ -> [])
|
||||
let results = native_find_all_raw tree pattern in
|
||||
List.map (fun (p, s) -> rel ^ " " ^ native_path_str p ^ " " ^ s) results
|
||||
with _ -> []
|
||||
) files in
|
||||
if all_lines = [] then text_result "(no matches)"
|
||||
@@ -1948,16 +2145,8 @@ let handle_sx_comp_usage args =
|
||||
let rel = relative_path ~base:dir path in
|
||||
try
|
||||
let tree = parse_file path in
|
||||
let results = call_sx "find-all" [tree; String name] in
|
||||
(match results with
|
||||
| List items | ListRef { contents = items } ->
|
||||
List.map (fun item ->
|
||||
match item with
|
||||
| List [p; s] | ListRef { contents = [p; s] } ->
|
||||
rel ^ " " ^ value_to_string (call_sx "path-str" [p]) ^ " " ^ value_to_string s
|
||||
| _ -> rel ^ " " ^ value_to_string item
|
||||
) items
|
||||
| _ -> [])
|
||||
let results = native_find_all_raw tree name in
|
||||
List.map (fun (p, s) -> rel ^ " " ^ native_path_str p ^ " " ^ s) results
|
||||
with _ -> []
|
||||
) files in
|
||||
if all_lines = [] then text_result "(no usages found)"
|
||||
|
||||
@@ -25,6 +25,9 @@ open Sx_ref
|
||||
let pass_count = ref 0
|
||||
let fail_count = ref 0
|
||||
let suite_stack : string list ref = ref []
|
||||
(* Test filter: when Some, only run tests (suite, name) in the set.
|
||||
Populated by --only-failing=FILE from lines like "FAIL: suite > name: error". *)
|
||||
let suite_filter : (string * string, unit) Hashtbl.t option ref = ref None
|
||||
|
||||
(* ====================================================================== *)
|
||||
(* Deep equality — SX structural comparison *)
|
||||
@@ -176,6 +179,17 @@ let make_test_env () =
|
||||
suite_stack := (match !suite_stack with _ :: t -> t | [] -> []);
|
||||
Nil);
|
||||
|
||||
bind "test-allowed?" (fun args ->
|
||||
match !suite_filter with
|
||||
| None -> Bool true
|
||||
| Some filter ->
|
||||
let name = match args with
|
||||
| [String s] -> s
|
||||
| [v] -> Sx_types.value_to_string v
|
||||
| _ -> "" in
|
||||
let suite = match !suite_stack with [] -> "" | s :: _ -> s in
|
||||
Bool (Hashtbl.mem filter (suite, name)));
|
||||
|
||||
(* --- Test helpers --- *)
|
||||
|
||||
bind "sx-parse" (fun args ->
|
||||
@@ -1563,6 +1577,131 @@ let run_spec_tests env test_files =
|
||||
| _ -> child
|
||||
in
|
||||
|
||||
(* Minimal HTML parser for test mock.
|
||||
Parses an HTML string into mock child elements and appends them to `parent`.
|
||||
Handles: <tag attr="v" attr='v' attr=v attr>content</tag>, nested elements,
|
||||
self-closing tags, text content. No comments, CDATA, DOCTYPE, or entities. *)
|
||||
let parse_html_into parent_d html =
|
||||
let len = String.length html in
|
||||
let pos = ref 0 in
|
||||
let is_name_char c =
|
||||
(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
||||
(c >= '0' && c <= '9') || c = '-' || c = '_' || c = ':'
|
||||
in
|
||||
let skip_ws () =
|
||||
while !pos < len && (let c = html.[!pos] in c = ' ' || c = '\n' || c = '\t' || c = '\r') do
|
||||
incr pos
|
||||
done
|
||||
in
|
||||
let parse_name () =
|
||||
let start = !pos in
|
||||
while !pos < len && is_name_char html.[!pos] do incr pos done;
|
||||
String.sub html start (!pos - start)
|
||||
in
|
||||
let parse_attr_value () =
|
||||
if !pos >= len then ""
|
||||
else if html.[!pos] = '"' then begin
|
||||
incr pos;
|
||||
let start = !pos in
|
||||
while !pos < len && html.[!pos] <> '"' do incr pos done;
|
||||
let v = String.sub html start (!pos - start) in
|
||||
if !pos < len then incr pos;
|
||||
v
|
||||
end
|
||||
else if html.[!pos] = '\'' then begin
|
||||
incr pos;
|
||||
let start = !pos in
|
||||
while !pos < len && html.[!pos] <> '\'' do incr pos done;
|
||||
let v = String.sub html start (!pos - start) in
|
||||
if !pos < len then incr pos;
|
||||
v
|
||||
end
|
||||
else begin
|
||||
let start = !pos in
|
||||
while !pos < len && (let c = html.[!pos] in
|
||||
c <> ' ' && c <> '\t' && c <> '\n' && c <> '\r'
|
||||
&& c <> '>' && c <> '/') do
|
||||
incr pos
|
||||
done;
|
||||
String.sub html start (!pos - start)
|
||||
end
|
||||
in
|
||||
let parse_attrs (elem : (string, Sx_types.value) Hashtbl.t) =
|
||||
skip_ws ();
|
||||
while !pos < len && html.[!pos] <> '>' && html.[!pos] <> '/' do
|
||||
let name = parse_name () in
|
||||
if name = "" then begin
|
||||
(* Avoid infinite loop on unexpected char *)
|
||||
if !pos < len then incr pos
|
||||
end else begin
|
||||
let value =
|
||||
if !pos < len && html.[!pos] = '=' then begin
|
||||
incr pos; parse_attr_value ()
|
||||
end else ""
|
||||
in
|
||||
let attrs = match Hashtbl.find_opt elem "attributes" with
|
||||
| Some (Dict a) -> a
|
||||
| _ -> let a = Hashtbl.create 4 in Hashtbl.replace elem "attributes" (Dict a); a in
|
||||
Hashtbl.replace attrs name (String value);
|
||||
if name = "id" then Hashtbl.replace elem "id" (String value);
|
||||
if name = "class" then Hashtbl.replace elem "className" (String value);
|
||||
if name = "value" then Hashtbl.replace elem "value" (String value);
|
||||
skip_ws ()
|
||||
end
|
||||
done
|
||||
in
|
||||
let void_tags = ["br"; "hr"; "img"; "input"; "meta"; "link"; "area";
|
||||
"base"; "col"; "embed"; "source"; "track"; "wbr"] in
|
||||
let rec parse_children parent_elem =
|
||||
while !pos < len && not (!pos + 1 < len && html.[!pos] = '<' && html.[!pos + 1] = '/') do
|
||||
if !pos < len && html.[!pos] = '<' && !pos + 1 < len && is_name_char html.[!pos + 1] then
|
||||
parse_element parent_elem
|
||||
else if !pos < len && html.[!pos] = '<' then begin
|
||||
(* Unknown/comment — skip to next '>' *)
|
||||
while !pos < len && html.[!pos] <> '>' do incr pos done;
|
||||
if !pos < len then incr pos
|
||||
end
|
||||
else begin
|
||||
let start = !pos in
|
||||
while !pos < len && html.[!pos] <> '<' do incr pos done;
|
||||
let text = String.sub html start (!pos - start) in
|
||||
if String.trim text <> "" then begin
|
||||
let cur = match Hashtbl.find_opt parent_elem "textContent" with
|
||||
| Some (String s) -> s | _ -> "" in
|
||||
Hashtbl.replace parent_elem "textContent" (String (cur ^ text))
|
||||
end
|
||||
end
|
||||
done
|
||||
and parse_element parent_elem =
|
||||
incr pos; (* skip '<' *)
|
||||
let tag = parse_name () in
|
||||
if tag = "" then () else begin
|
||||
let el = make_mock_element tag in
|
||||
let eld = match el with Dict d -> d | _ -> Hashtbl.create 0 in
|
||||
parse_attrs eld;
|
||||
skip_ws ();
|
||||
let self_closing =
|
||||
if !pos < len && html.[!pos] = '/' then begin incr pos; true end else false
|
||||
in
|
||||
if !pos < len && html.[!pos] = '>' then incr pos;
|
||||
let is_void = List.mem (String.lowercase_ascii tag) void_tags in
|
||||
if not self_closing && not is_void then begin
|
||||
parse_children eld;
|
||||
if !pos + 1 < len && html.[!pos] = '<' && html.[!pos + 1] = '/' then begin
|
||||
pos := !pos + 2;
|
||||
let _ = parse_name () in
|
||||
skip_ws ();
|
||||
if !pos < len && html.[!pos] = '>' then incr pos
|
||||
end
|
||||
end;
|
||||
ignore (mock_append_child (Dict parent_elem) el)
|
||||
end
|
||||
in
|
||||
pos := 0;
|
||||
parse_children parent_d
|
||||
in
|
||||
let _ = parse_html_into in
|
||||
|
||||
(* Helper: remove child from parent *)
|
||||
let mock_remove_child parent child =
|
||||
match parent, child with
|
||||
@@ -1578,11 +1717,21 @@ let run_spec_tests env test_files =
|
||||
in
|
||||
|
||||
(* Helper: querySelector - find element matching selector in tree *)
|
||||
let mock_matches el sel =
|
||||
let rec mock_matches el sel =
|
||||
match el with
|
||||
| Dict d ->
|
||||
let sel = String.trim sel in
|
||||
if String.length sel > 0 && sel.[0] = '#' then
|
||||
(* Compound selector: tag[attr=value] or tag.class or tag#id — split into parts *)
|
||||
if String.length sel > 1 &&
|
||||
((sel.[0] >= 'a' && sel.[0] <= 'z') || (sel.[0] >= 'A' && sel.[0] <= 'Z')) &&
|
||||
(String.contains sel '[' || String.contains sel '.' || String.contains sel '#') then
|
||||
let i = ref 0 in
|
||||
let n = String.length sel in
|
||||
while !i < n && ((sel.[!i] >= 'a' && sel.[!i] <= 'z') || (sel.[!i] >= 'A' && sel.[!i] <= 'Z') || (sel.[!i] >= '0' && sel.[!i] <= '9') || sel.[!i] = '-') do incr i done;
|
||||
let tag_part = String.sub sel 0 !i in
|
||||
let rest_part = String.sub sel !i (n - !i) in
|
||||
(mock_matches el tag_part) && (mock_matches el rest_part)
|
||||
else if String.length sel > 0 && sel.[0] = '#' then
|
||||
let id = String.sub sel 1 (String.length sel - 1) in
|
||||
(match Hashtbl.find_opt d "id" with Some (String i) -> i = id | _ -> false)
|
||||
else if String.length sel > 0 && sel.[0] = '.' then
|
||||
@@ -1590,7 +1739,8 @@ let run_spec_tests env test_files =
|
||||
List.mem cls (get_classes d)
|
||||
else if String.length sel > 0 && sel.[0] = '[' then
|
||||
(* [attr] or [attr="value"] *)
|
||||
let inner = String.sub sel 1 (String.length sel - 2) in
|
||||
let end_bracket = try String.index sel ']' with Not_found -> String.length sel - 1 in
|
||||
let inner = String.sub sel 1 (end_bracket - 1) in
|
||||
(match String.split_on_char '=' inner with
|
||||
| [attr] ->
|
||||
let attrs = match Hashtbl.find_opt d "attributes" with Some (Dict a) -> a | _ -> Hashtbl.create 0 in
|
||||
@@ -1621,19 +1771,51 @@ let run_spec_tests env test_files =
|
||||
| found -> mock_query_selector found (String.concat " " rest))
|
||||
| [] -> Nil
|
||||
and mock_query_selector_single el sel =
|
||||
match el with
|
||||
| Dict d ->
|
||||
let kids = match Hashtbl.find_opt d "children" with Some (List l) -> l | _ -> [] in
|
||||
let rec search = function
|
||||
| [] -> Nil
|
||||
| child :: rest ->
|
||||
if mock_matches child sel then child
|
||||
else match mock_query_selector_single child sel with
|
||||
| Nil -> search rest
|
||||
| found -> found
|
||||
in
|
||||
search kids
|
||||
| _ -> Nil
|
||||
(* Handle tag:nth-of-type(N): find Nth child of same tag under parent *)
|
||||
let nth_match = try
|
||||
let idx = String.index sel ':' in
|
||||
let tag = String.sub sel 0 idx in
|
||||
let rest = String.sub sel idx (String.length sel - idx) in
|
||||
if String.length rest > String.length ":nth-of-type(" &&
|
||||
String.sub rest 0 (String.length ":nth-of-type(") = ":nth-of-type(" &&
|
||||
rest.[String.length rest - 1] = ')'
|
||||
then
|
||||
let n_str = String.sub rest (String.length ":nth-of-type(")
|
||||
(String.length rest - String.length ":nth-of-type(" - 1) in
|
||||
(try Some (tag, int_of_string (String.trim n_str)) with _ -> None)
|
||||
else None
|
||||
with Not_found -> None in
|
||||
(match nth_match with
|
||||
| Some (tag, n) ->
|
||||
(* Walk tree; collect matching-tag elements in document order; return nth *)
|
||||
let found = ref [] in
|
||||
let rec walk node =
|
||||
match node with
|
||||
| Dict d ->
|
||||
let kids = match Hashtbl.find_opt d "children" with Some (List l) -> l | _ -> [] in
|
||||
List.iter (fun child ->
|
||||
if mock_matches child tag then found := child :: !found;
|
||||
walk child
|
||||
) kids
|
||||
| _ -> ()
|
||||
in
|
||||
walk el;
|
||||
let matches = List.rev !found in
|
||||
(try List.nth matches (n - 1) with _ -> Nil)
|
||||
| None ->
|
||||
(match el with
|
||||
| Dict d ->
|
||||
let kids = match Hashtbl.find_opt d "children" with Some (List l) -> l | _ -> [] in
|
||||
let rec search = function
|
||||
| [] -> Nil
|
||||
| child :: rest ->
|
||||
if mock_matches child sel then child
|
||||
else match mock_query_selector_single child sel with
|
||||
| Nil -> search rest
|
||||
| found -> found
|
||||
in
|
||||
search kids
|
||||
| _ -> Nil))
|
||||
in
|
||||
|
||||
let rec mock_query_all el sel =
|
||||
@@ -1742,7 +1924,7 @@ let run_spec_tests env test_files =
|
||||
| "addEventListener" | "removeEventListener" | "dispatchEvent"
|
||||
| "appendChild" | "removeChild" | "insertBefore" | "replaceChild"
|
||||
| "querySelector" | "querySelectorAll" | "closest" | "matches"
|
||||
| "contains" | "cloneNode" | "remove" | "focus" | "blur" | "click"
|
||||
| "contains" | "compareDocumentPosition" | "cloneNode" | "remove" | "focus" | "blur" | "click"
|
||||
| "insertAdjacentHTML" | "prepend" | "showModal" | "show" | "close"
|
||||
| "getBoundingClientRect" | "getAnimations" | "scrollIntoView"
|
||||
| "scrollTo" | "scroll" | "reset" -> Bool true
|
||||
@@ -1841,16 +2023,19 @@ let run_spec_tests env test_files =
|
||||
| Some (Dict _cl) -> () (* classes live in className *)
|
||||
| _ -> ())
|
||||
| "innerHTML" ->
|
||||
(* Setting innerHTML clears children and syncs textContent (like a browser) *)
|
||||
(* Setting innerHTML clears existing children, parses the HTML, and
|
||||
creates new mock child elements (approximating browser behavior). *)
|
||||
let kids = match Hashtbl.find_opt d "children" with Some (List l) -> l | _ -> [] in
|
||||
List.iter (fun c -> match c with Dict cd ->
|
||||
Hashtbl.replace cd "parentElement" Nil;
|
||||
Hashtbl.replace cd "parentNode" Nil | _ -> ()) kids;
|
||||
Hashtbl.replace d "children" (List []);
|
||||
Hashtbl.replace d "childNodes" (List []);
|
||||
(* Approximate textContent: strip HTML tags from innerHTML *)
|
||||
Hashtbl.replace d "textContent" (String "");
|
||||
(match stored with
|
||||
| String s ->
|
||||
| String s when String.contains s '<' ->
|
||||
parse_html_into d s;
|
||||
(* Strip tags for a best-effort textContent *)
|
||||
let buf = Buffer.create (String.length s) in
|
||||
let in_tag = ref false in
|
||||
String.iter (fun c ->
|
||||
@@ -1859,6 +2044,7 @@ let run_spec_tests env test_files =
|
||||
else if not !in_tag then Buffer.add_char buf c
|
||||
) s;
|
||||
Hashtbl.replace d "textContent" (String (Buffer.contents buf))
|
||||
| String s -> Hashtbl.replace d "textContent" (String s)
|
||||
| _ -> Hashtbl.replace d "textContent" (String ""))
|
||||
| "textContent" ->
|
||||
(* Setting textContent clears children *)
|
||||
@@ -1887,6 +2073,8 @@ let run_spec_tests env test_files =
|
||||
| "setTimeout" -> (match rest with fn :: _ -> ignore (Sx_ref.cek_call fn (List [])); Nil | _ -> Nil)
|
||||
| "clearTimeout" -> Nil
|
||||
| _ -> Nil)
|
||||
| Dict d :: String "hasOwnProperty" :: [String k] ->
|
||||
Bool (Hashtbl.mem d k)
|
||||
| Dict d :: String m :: rest ->
|
||||
let mt = match Hashtbl.find_opt d "__mock_type" with Some (String t) -> t | _ -> "" in
|
||||
|
||||
@@ -2139,6 +2327,33 @@ let run_spec_tests env test_files =
|
||||
| _ -> Nil
|
||||
in up (Dict d)
|
||||
| _ -> Nil)
|
||||
| "compareDocumentPosition" ->
|
||||
(match rest with
|
||||
| [other] ->
|
||||
let self = Dict d in
|
||||
let body = Dict mock_body in
|
||||
let found_self = ref false in
|
||||
let found_other = ref false in
|
||||
let self_first = ref false in
|
||||
let rec walk node =
|
||||
if !found_self && !found_other then ()
|
||||
else begin
|
||||
if mock_el_eq node self then begin
|
||||
if not !found_other then self_first := true;
|
||||
found_self := true
|
||||
end;
|
||||
if mock_el_eq node other then found_other := true;
|
||||
(match node with
|
||||
| Dict dd -> let kids = match Hashtbl.find_opt dd "children" with Some (List l) -> l | _ -> [] in
|
||||
List.iter walk kids
|
||||
| _ -> ())
|
||||
end
|
||||
in
|
||||
walk body;
|
||||
if !found_self && !found_other then
|
||||
Number (if !self_first then 4.0 else 2.0)
|
||||
else Number 0.0
|
||||
| _ -> Number 0.0)
|
||||
| "matches" ->
|
||||
(match rest with [String sel] -> Bool (mock_matches (Dict d) sel) | _ -> Bool false)
|
||||
| "contains" ->
|
||||
@@ -2213,25 +2428,62 @@ let run_spec_tests env test_files =
|
||||
Hashtbl.replace r "right" (Number 100.0); Hashtbl.replace r "bottom" (Number 100.0);
|
||||
Dict r
|
||||
| "insertAdjacentHTML" ->
|
||||
(* Position-aware insertion, coerce value to string *)
|
||||
(* Position-aware insertion. Parse the new HTML into a scratch
|
||||
container, then splice the resulting children into the target
|
||||
position WITHOUT disturbing sibling nodes. *)
|
||||
(match rest with
|
||||
| [String pos; value] ->
|
||||
| [String pos_kind; value] ->
|
||||
let html = match dom_stringify value with String s -> s | _ -> "" in
|
||||
let cur = match Hashtbl.find_opt d "innerHTML" with Some (String s) -> s | _ -> "" in
|
||||
let new_html = match pos with
|
||||
| "afterbegin" -> html ^ cur (* prepend *)
|
||||
| _ -> cur ^ html (* beforeend / default: append *)
|
||||
(* Parse new HTML into scratch container to get new child list.
|
||||
For pure-text content, wrap into the target's innerHTML path. *)
|
||||
let scratch = make_mock_element "div" in
|
||||
let scratch_d = match scratch with Dict sd -> sd | _ -> Hashtbl.create 0 in
|
||||
if String.contains html '<' then parse_html_into scratch_d html;
|
||||
let new_kids = match Hashtbl.find_opt scratch_d "children" with Some (List l) -> l | _ -> [] in
|
||||
let prepend = pos_kind = "beforebegin" || pos_kind = "afterbegin" in
|
||||
let insert_into container_d index =
|
||||
List.iter (fun c -> match c with
|
||||
| Dict cd ->
|
||||
Hashtbl.replace cd "parentElement" (Dict container_d);
|
||||
Hashtbl.replace cd "parentNode" (Dict container_d)
|
||||
| _ -> ()) new_kids;
|
||||
let kids = match Hashtbl.find_opt container_d "children" with Some (List l) -> l | _ -> [] in
|
||||
let before = List.filteri (fun i _ -> i < index) kids in
|
||||
let after = List.filteri (fun i _ -> i >= index) kids in
|
||||
let all = before @ new_kids @ after in
|
||||
Hashtbl.replace container_d "children" (List all);
|
||||
Hashtbl.replace container_d "childNodes" (List all);
|
||||
(* Update container innerHTML based on position kind, not index *)
|
||||
let cur = match Hashtbl.find_opt container_d "innerHTML" with Some (String s) -> s | _ -> "" in
|
||||
let new_html = if prepend then html ^ cur else cur ^ html in
|
||||
Hashtbl.replace container_d "innerHTML" (String new_html);
|
||||
let buf = Buffer.create (String.length new_html) in
|
||||
let in_tag = ref false in
|
||||
String.iter (fun c ->
|
||||
if c = '<' then in_tag := true
|
||||
else if c = '>' then in_tag := false
|
||||
else if not !in_tag then Buffer.add_char buf c
|
||||
) new_html;
|
||||
Hashtbl.replace container_d "textContent" (String (Buffer.contents buf))
|
||||
in
|
||||
Hashtbl.replace d "innerHTML" (String new_html);
|
||||
(* Sync textContent *)
|
||||
let buf = Buffer.create (String.length new_html) in
|
||||
let in_tag = ref false in
|
||||
String.iter (fun c ->
|
||||
if c = '<' then in_tag := true
|
||||
else if c = '>' then in_tag := false
|
||||
else if not !in_tag then Buffer.add_char buf c
|
||||
) new_html;
|
||||
Hashtbl.replace d "textContent" (String (Buffer.contents buf));
|
||||
(match pos_kind with
|
||||
| "beforebegin" | "afterend" ->
|
||||
(match Hashtbl.find_opt d "parentElement" with
|
||||
| Some (Dict pd) ->
|
||||
let siblings = match Hashtbl.find_opt pd "children" with Some (List l) -> l | _ -> [] in
|
||||
let rec find_idx i = function
|
||||
| [] -> List.length siblings
|
||||
| x :: _ when mock_el_eq x (Dict d) -> i
|
||||
| _ :: rest -> find_idx (i+1) rest
|
||||
in
|
||||
let self_idx = find_idx 0 siblings in
|
||||
let insert_idx = if pos_kind = "beforebegin" then self_idx else self_idx + 1 in
|
||||
insert_into pd insert_idx
|
||||
| _ -> ())
|
||||
| "afterbegin" -> insert_into d 0
|
||||
| _ (* "beforeend" *) ->
|
||||
let kids_len = match Hashtbl.find_opt d "children" with Some (List l) -> List.length l | _ -> 0 in
|
||||
insert_into d kids_len);
|
||||
Nil
|
||||
| _ -> Nil)
|
||||
| "showModal" | "show" ->
|
||||
@@ -2341,6 +2593,8 @@ let run_spec_tests env test_files =
|
||||
Hashtbl.replace ev "_stopped" (Bool false);
|
||||
Hashtbl.replace ev "_stopImmediate" (Bool false);
|
||||
Dict ev
|
||||
| [String "Object"] ->
|
||||
Dict (Hashtbl.create 4)
|
||||
| _ -> Nil);
|
||||
|
||||
reg "host-callback" (fun args ->
|
||||
@@ -2823,7 +3077,39 @@ let () =
|
||||
let args = Array.to_list Sys.argv |> List.tl in
|
||||
let foundation_only = List.mem "--foundation" args in
|
||||
let jit_enabled = List.mem "--jit" args in
|
||||
let test_files = List.filter (fun a -> not (String.length a > 0 && a.[0] = '-')) args in
|
||||
(* --only-failing=PATH : read lines of form "FAIL: suite > name: ..." and
|
||||
restrict test runs to those (suite, name) pairs. *)
|
||||
List.iter (fun a ->
|
||||
let prefix = "--only-failing=" in
|
||||
if String.length a > String.length prefix
|
||||
&& String.sub a 0 (String.length prefix) = prefix then begin
|
||||
let path = String.sub a (String.length prefix) (String.length a - String.length prefix) in
|
||||
let filter = Hashtbl.create 64 in
|
||||
let ic = open_in path in
|
||||
(try while true do
|
||||
let line = input_line ic in
|
||||
(* Match " FAIL: <suite> > <name>: <err>" or "FAIL: <suite> > <name>: <err>" *)
|
||||
let line = String.trim line in
|
||||
if String.length line > 6 && String.sub line 0 6 = "FAIL: " then begin
|
||||
let rest = String.sub line 6 (String.length line - 6) in
|
||||
match String.index_opt rest '>' with
|
||||
| Some gt ->
|
||||
let suite = String.trim (String.sub rest 0 gt) in
|
||||
let after = String.sub rest (gt + 1) (String.length rest - gt - 1) in
|
||||
(match String.index_opt after ':' with
|
||||
| Some colon ->
|
||||
let name = String.trim (String.sub after 0 colon) in
|
||||
Hashtbl.replace filter (suite, name) ()
|
||||
| None -> ())
|
||||
| None -> ()
|
||||
end
|
||||
done with End_of_file -> ());
|
||||
close_in ic;
|
||||
Printf.eprintf "[filter] %d tests loaded from %s\n%!" (Hashtbl.length filter) path;
|
||||
suite_filter := Some filter
|
||||
end) args;
|
||||
let test_files = List.filter (fun a ->
|
||||
not (String.length a > 0 && a.[0] = '-')) args in
|
||||
|
||||
(* Always run foundation tests *)
|
||||
run_foundation_tests ();
|
||||
|
||||
Reference in New Issue
Block a user