{"id":50745846,"url":"https://github.com/ndrean/zexplorer","last_synced_at":"2026-06-10T20:32:25.731Z","repository":{"id":306844158,"uuid":"1027389952","full_name":"ndrean/zexplorer","owner":"ndrean","description":"HTML processor engine on steroids. Give eyes to your LLM","archived":false,"fork":false,"pushed_at":"2026-03-06T18:25:33.000Z","size":112113,"stargazers_count":5,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-03-06T21:47:46.544Z","etag":null,"topics":["css-parser","css-sanitization","html-parser","javascript-tools","lexbor","mcp-server","quickjs-ng","sanitize-html","sqlite","thorvg","yoga","zig","zig-package"],"latest_commit_sha":null,"homepage":"https://ndrean.github.io/zexplorer","language":"Zig","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ndrean.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-07-27T23:29:03.000Z","updated_at":"2026-03-04T06:43:17.000Z","dependencies_parsed_at":"2025-08-13T01:11:15.934Z","dependency_job_id":"3b5d5bcd-f46d-4ae6-ac03-b843b38ec66e","html_url":"https://github.com/ndrean/zexplorer","commit_stats":null,"previous_names":["ndrean/z-html","ndrean/zexplorer"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ndrean/zexplorer","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ndrean%2Fzexplorer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ndrean%2Fzexplorer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ndrean%2Fzexplorer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ndrean%2Fzexplorer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ndrean","download_url":"https://codeload.github.com/ndrean/zexplorer/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ndrean%2Fzexplorer/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34170162,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-10T02:00:07.152Z","response_time":89,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["css-parser","css-sanitization","html-parser","javascript-tools","lexbor","mcp-server","quickjs-ng","sanitize-html","sqlite","thorvg","yoga","zig","zig-package"],"created_at":"2026-06-10T20:32:22.651Z","updated_at":"2026-06-10T20:32:25.716Z","avatar_url":"https://github.com/ndrean.png","language":"Zig","funding_links":[],"categories":[],"sub_categories":[],"readme":"# zexplorer (`zxp`)\n\n![Zig support](https://img.shields.io/badge/Zig-0.15.2-color?logo=zig\u0026color=%23f3ab20)\n\n`zexplorer` is a fast, zero-dependency HTML+JS engine. Think `ffmpeg` for the web.\nYou can use it as a _command-line tool_, an _HTTP dev-server_ or an _MCP server_ for LLM agents — no browser, no Node.js, no Python, no runtime.\n\nThe MCP service gives your LLM agent eyes and persistent local storage, zero infra.\n\n\u003cp align=\"center\"\u003e\n\u003cimg src=\"https://github.com/ndrean/zexplorer/blob/main/images/zexplorer.png\" alt=\"logo\" width=\"700\" height=\"700\" /\u003e\n\u003c/p\u003e\n\n\u003cbr\u003e\n\n**TL;DR**:\n\n- Cold start: ~3ms\n- Memory: ~12MB\n- Zero dependencies. Single statically-compiled binary.\n- Stateless by default, Stateful on demand with a zero-config embedded SQLite storage for local persistence.\n- Pipelines: Native support for parsing Markdown, CSV, and SVG.\n- Outputs: Return raw data (JSON, strings, binary arrays), Markdown or render layouts (**Flexbox**) to PNG, JPEG, WEBP, and PDF.\n- MCP service - the token saver - let the LLM run scripts server-side, such as scrape, transform or render and get back the result.\n- Usage: Composable CLI tool or a high-concurrency HTTP rendering service.\n\n---\n\n## What can it do?\n\nIt can:\n\n- **Scrape** — fetch a URL, hydrate React, render Vue/Svelte/Lit/SolidJS, WebComponents, extract data. No headless browser.\n- **Stream** — consume LLM output via SSE (currently local Ollama, extendable to  any OpenAI-compatible endpoint); receive HTML chunks and rebuild a live DOM incrementally.\n- **Expose** — serve as an MCP server so LLM agents (Claude Desktop, Gemini CLI…) can `run_script` using the custom API, or use the shortcuts `render_html`, `render_markdown`, `render_url`, and receive data or screenshots directly in the conversation.\n- **Render** — `Flexbox` based only, all static:  HTML+JS+SVG such as D3, Chart.js, Leaflet, ECharts. Basic support for Canvas API, output PNG/JPEG/WEBP/PDF.\n- **Generate** — design an SVG in Figma, plug in data, batch-produce OG images or PDF reports.\n- **Sanitize** — DOM+CSS-aware HTML sanitization (stylesheets, inline styles, XSS/mXSS). Built-in.\n- **Run JS** — execute ES2020 scripts against a real DOM with fetch, timers, workers, and an event loop.\n- **Store \u0026 Persist** - drop text, blobs, images in the local storage, no ceremony.\n\n**Limitations**:\n\n- no TypeScript support. JSX is supported via \"tagged templates\" (using `htm`).\n- cannot scrape arbitrary bot protected public websites,\n- cannot paint complex CSS using grid-2d nor position:fixed, no CSS functions or variables nor complex canvas nor media queries...\n\n## Security\n\nIf you use your own trusted code, you can skip sanitization entirely. For untrusted content:\n\n\u003e [!WARNING]\n\u003e All layers are _best-effort_ — see [SECURITY.md](https://github.com/ndrean/zexplorer/blob/main/SECURITY.md) for full details.\n\u003e\n\u003e - **Content sanitization** — DOM+CSS-aware: stylesheets, inline styles, iframes, SVG/MathML, DOM clobbering, URI schemas, XSS/mXSS. Tested against [H5SC](https://github.com/cure53/H5SC), [OWASP](https://cheatsheetseries.owasp.org/cheatsheets/DOM_based_XSS_Prevention_Cheat_Sheet.html), [PortSwigger](https://portswigger.net/web-security/cross-site-scripting/cheat-sheet), and [DOMPurify](https://github.com/cure53/DOMPurify).\n\u003e - **Filesystem sandbox** — kernel-enforced `openat()` with symlink blocking, traversal rejection, cross-device check.\n\u003e - **Network hardening** — timeouts, redirect/size limits, SSRF pre-flight filtering, HTTPS-only remote imports.\n\u003e - **Resource limits** — worker fan-out caps, busy-loop interrupts, max stack/GC/memory, wall-clock deadlines.\n\n## Examples\n\n| Example | What it shows | Output | CLI | Server |\n| ------- | ------------- | ------ | :---: | :---: |\n| [MCP server](#mcp-server) | Give Claude Desktop / Gemini visual eyes | PNG | – | ✓ |\n| [LLM generative UI](#generative-template) | Ollama/OpenAI SSE → DOM → image | WEBP | ✓ | ✓ |\n| [Dynamic HTML card](#use-dynamic-html-with-htm-and-paint) | `htm` tagged templates → paintDOM | PNG | ✓ | ✓ |\n| [CSS grid / flexbox layout](#render-an-html-file-in-the-terminal) | grid-1D + flexbox → terminal image | PNG | ✓ | ✓ |\n| [Scrape Hacker News](#scrape-hacker-news) | fetch → DOM query → structured data | JSON | ✓ | ✓ |\n| [Vercel SPA scrape](#scrape-a-vercel-site-in-less-than-1s) | Next.js hydration → `waitForSelector` | JSON | ✓ | ✓ |\n| [Vercel site snapshot](#render-the-vercel-side) | SSR page → inlined images → render | WEBP | ✓ | ✓ |\n| [Echarts](#echarts) | Echarts SVG -\u003e rasterize | WEBP | ✓ | ✓ |\n| [Leaflet map PDF](#generate-a-leaflet-map-pdf-report) | GeoJSON route → OSM tiles → SVG → PDF | PDF | – | ✓ |\n\n---\n\n### MCP server\n\nStart the server (the `.` sets the sandbox root for file access and the SQLite store):\n\n```sh\n./zig-out/bin/zxp serve .\n```\n\n**Connect Claude Desktop** — add to `~/Library/Application Support/Claude/claude_desktop_config.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"zexplorer\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"mcp-remote\", \"http://localhost:9984/mcp\"]\n    }\n  }\n}\n```\n\n**Available tools:**\n\n| Tool | What it does |\n| ---- | ----------- |\n| `render_html` | Render an HTML string → PNG/WEBP/JPEG (base64 image in MCP response) |\n| `render_markdown` | Render GFM Markdown → image |\n| `render_url` | Fetch a URL, run its scripts, render → image |\n| `run_script` | Execute arbitrary JavaScript in the headless DOM+JS engine; returns text, JSON, or an image |\n| `get_zxp_docs` | Return API docs and worked examples — call this before writing a `run_script` |\n| `store_save` | Persist text or binary data (e.g. a rendered PNG) to a local SQLite store |\n| `store_get` | Retrieve a stored entry by name; `data` is an ArrayBuffer |\n| `store_list` | List store entries (metadata only) |\n| `store_delete` | Delete a store entry by name |\n\nThe typical LLM workflow is: call `get_zxp_docs` to learn the `zxp.*` API, then call `run_script` with composed JavaScript to scrape, render, or process data. `store_*` lets the LLM persist intermediate results across stateless tool calls.\n\nYour local storage is just:\n\n```js\n// zexplorer runs this instantly. No DB connection setup needed.\nconst pageTitle = document.querySelector('title').textContent;\nzxp.store.save(\"last_scraped_title\", pageTitle); // Saved instantly to SQLite\nzxp.store.get(\"last_scraped_title\");\n```\n\n**Smoke-test with curl:**\n\n```sh\n# Text result\ncurl -s -X POST http://localhost:9984/mcp \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"run_script\",\"arguments\":{\"script\":\"const a=10,b=32; `The answer is ${a+b}`\"}}}'\n# → {\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"content\":[{\"type\":\"text\",\"text\":\"The answer is 42\"}]}}\n\n# Image result\ncurl -s -X POST http://localhost:9984/mcp \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"render_html\",\"arguments\":{\"html\":\"\u003ch1 style=\\\"color:red\\\"\u003eHello MCP!\u003c/h1\u003e\",\"width\":400}}}'\n# → {\"jsonrpc\":\"2.0\",\"id\":2,\"result\":{\"content\":[{\"type\":\"image\",\"data\":\"iVBORw0KGgo....\",\"mimeType\":\"image/png\"}]}}\n```\n\n**Use `run_script` to build a D3 chart from CSV data** — the LLM composes the JS and gets an image back:\n\nSource: \u003chttps://github.com/ndrean/zexplorer/blob/main/src/examples/d3_chart/example_d3.js\u003e\n\n```sh\ncurl -s -X POST http://localhost:9984/run --data-binary @src/examples/d3_chart/output_chart.webp\n```\n\n\u003cimg src=\"https://github.com/ndrean/zexplorer/blob/main/src/examples/d3_chart/output_chart.webp\" alt=\"output_chart\" width=\"500\"\u003e\n\n\u003cbr\u003e\n\n### Generative template\n\nYou want to use an LLM to generate some HTML with CSS for us as the engine has builtin support for SSE' text/event-stream' content-type support.\n\nWe showcase the local provider `ollama`. We used the 4.7G model \"qwen2.5-coder:7b\". This can be extended to any provider (OpenAI, Anthropic, Gemini) if you adapt the LLM response parsing.\n\n- Our local LLM `ollama` is up and running: `curl -s http://localhost:11434/api/tags | head -c 200` returns _{\"models\":[{\"name\":\"qwen2.5-coder:7b\",....}_.\n- The dev-server is up and running: `./zig-out.bin/zxp server .`\n\n**First example**: render a generative `\u003cimg\u003e` component.\n\n```html\n\u003cimg src=\"http://localhost:9984/render_llm?prompt=3+metric+cards+Revenue+%2412k+Users+340+MRR+%244.2k\u0026width=600\u0026format=png\"\u003e\n```\n\nLet's \"live-serve\" this component in a browser. The browser will send a GET request to the dev-servern which. in turn will reach the LLM. Depending upon the mood of the LLML, you can get this image:\n\n\u003cimg src=\"https://github.com/ndrean/zexplorer/blob/main/src/examples/generative/img_embedded.png\" alt=\"generative template\" width=\"400\"\u003e\n\n**Second example**: interactive generative form\n\nThe HTML below is a HTML form where we select a more elaborated prompt. On submission, a JavaScript snippet will POST the prompt to the dev-server \"/render_llm\" endpoint.\n\nSource: \u003chttps://github.com/ndrean/zexplorer/blob/main/src/examples/generative/render_llm_demo.html\u003e\n\n\u003cdetails\u003e\u003csummary\u003ea FORM textarea INPUT populated by four buttons with a submit button\u003c/summary\u003e\n\n```html\n\u003csection\u003e\n  \u003ch2\u003eInteractive prompt (POST → base64)\u003c/h2\u003e\n\n  \u003cdiv class=\"quick-prompts\"\u003e\n    \u003cbutton type=\"button\" data-prompt=\"A responsive table with 3 columns: Name, Status, Amount. 5 realistic sample rows, blue header.\"\u003eTable\u003c/button\u003e\n    \u003cbutton type=\"button\" data-prompt=\"A dashboard card grid: 4 KPI cards (Revenue $42k ↑12%, Users 3.4k ↑5%, Churn 2.1% ↓0.3%, MRR $8.2k ↑18%). Clean white cards, subtle shadows.\"\u003eKPI cards\u003c/button\u003e\n    \u003cbutton type=\"button\" data-prompt=\"A horizontal progress tracker with 4 steps: Ordered, Processing, Shipped, Delivered. Step 2 is active in blue.\"\u003eProgress steps\u003c/button\u003e\n    \u003cbutton type=\"button\" data-prompt=\"A minimal invoice: logo placeholder, billed-to block, line-item table (Qty, Description, Unit Price, Total), grand total row.\"\u003eInvoice\u003c/button\u003e\n  \u003c/div\u003e\n\n  \u003cform id=\"gen-form\"\u003e\n    \u003ctextarea\n      name=\"prompt\"\n      placeholder=\"Describe a UI component to generate…\"\n    \u003eA responsive table with 3 columns: Name, Status, Amount. Include 5 realistic sample rows. Use a blue header.\u003c/textarea\u003e\n\n    \u003cdiv class=\"options\"\u003e\n      \u003clabel\u003eWidth \u003cinput name=\"width\" type=\"number\" value=\"800\" min=\"200\" max=\"2000\" step=\"100\"\u003e\u003c/label\u003e\n      \u003clabel\u003eFormat\n        \u003cselect name=\"format\"\u003e\n          \u003coption value=\"png\"\u003ePNG\u003c/option\u003e\n          \u003coption value=\"webp\"\u003eWebP\u003c/option\u003e\n          \u003coption value=\"jpeg\"\u003eJPEG\u003c/option\u003e\n        \u003c/select\u003e\n      \u003c/label\u003e\n      \u003clabel\u003eModel \u003cinput name=\"model\" type=\"text\" value=\"qwen2.5-coder:7b\"\u003e\u003c/label\u003e\n      \u003clabel\u003eOllama URL \u003cinput name=\"base_url\" type=\"text\" value=\"http://localhost:11434\" style=\"width:16rem\"\u003e\u003c/label\u003e\n    \u003c/div\u003e\n\n    \u003cbutton type=\"submit\" id=\"gen-btn\"\u003eGenerate\u003c/button\u003e\n  \u003c/form\u003e\n\n  \u003cdiv class=\"status\" id=\"status\"\u003e\u003c/div\u003e\n  \u003cimg class=\"result-img\" id=\"result\" src=\"\" alt=\"Generated result\"\u003e\n\u003c/section\u003e\n\n\u003cscript\u003e\n  const form   = document.getElementById('gen-form');\n  const btn    = document.getElementById('gen-btn');\n  const status = document.getElementById('status');\n  const result = document.getElementById('result');\n\n  // Quick-prompt buttons fill the textarea.\n  document.querySelectorAll('.quick-prompts button').forEach(b =\u003e {\n    b.addEventListener('click', () =\u003e {\n      form.prompt.value = b.dataset.prompt;\n    });\n  });\n\n  form.addEventListener('submit', async e =\u003e {\n    e.preventDefault();\n\n    const prompt = form.prompt.value.trim();\n    if (!prompt) return;\n\n    btn.disabled = true;\n    status.className = 'status';\n    status.textContent = 'Generating… (this may take a few seconds)';\n    result.style.opacity = '0.4';\n\n    try {\n      const res = await fetch('http://localhost:9984/render_llm', {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify({\n          prompt,\n          model:    form.model.value,\n          base_url: form.base_url.value,\n          width:    parseInt(form.width.value, 10) || 800,\n          format:   form.format.value,\n        }),\n      });\n\n      if (!res.ok) {\n        const text = await res.text();\n        throw new Error(`HTTP ${res.status}: ${text}`);\n      }\n\n      const { data, mime } = await res.json();\n      result.src = `data:${mime};base64,${data}`;\n      result.style.opacity = '1';\n      status.textContent = '';\n    } catch (err) {\n      status.className = 'status error';\n      status.textContent = `Error: ${err.message}`;\n      result.style.opacity = '1';\n    } finally {\n      btn.disabled = false;\n    }\n  });\n\u003c/script\u003e\n```\n\n\u003c/details\u003e\n\u003cbr\u003e\n\nWe have selected to render a table (this is a POST request to \"/render_llm\"). You can get for example:\n\n\u003cimg src=\"https://github.com/ndrean/zexplorer/blob/main/src/examples/generative/browser_render_llm_demo.png\" alt=\"browser screenshot\" width=\"600\"\u003e\n\n\u003cbr/\u003e\n\n#### Note about SSE format\n\n|Provider\t|Content path\t|End signal|\n|--|--|--|\n|OpenAI / groq / mistral / together / ollama_v1\t|.choices[0].delta.content\t|data: [DONE]|\n|Anthropic\t|.delta.text (on content_block_delta events)\t|event: message_stop|\n|Gemini\t|.candidates[0].content.parts[0].text\t|.finishReason == \"STOP\"|\n|Ollama\t|.message.content\t|no SSE — raw NDJSON|\n\n```txt\nif openai || groq || mistral || together || ollama_v1 → .choices[0].delta.content\nif anthropic                                           → .delta.text\nif gemini                                             → .candidates[0].content.parts[0].text\n```\n\nDue to our limitations in the CSS that we are able to render, and because we are using a small model, we had to set hard and explicit constraints in our prompt.\n\n\u003cdetails\u003e\u003csummary\u003eThe general system prompt that we use\u003c/summary\u003e\n\n```zig\npub const default_system =\n    \"You are a UI generator. Output ONLY raw HTML — no markdown, no code fences, no backticks, no explanation. \" ++\n    \"Start your response directly with an HTML tag. \" ++\n    \"Use ONLY \u003cdiv\u003e and \u003cspan\u003e elements — NEVER \u003ctable\u003e, \u003ctr\u003e, \u003ctd\u003e, \u003cth\u003e, \u003cthead\u003e, \u003ctbody\u003e. \" ++\n    \"Use ONLY inline styles with these CSS properties: display (flex or block), flex-direction, \" ++\n    \"justify-content, align-items, flex-wrap, gap, padding, margin, color, background, \" ++\n    \"font-size, font-weight, border-radius, width, height, border, text-align, white-space. \" ++\n    \"No external fonts. No CSS variables. No animations. No \u003cscript\u003e tags. \" ++\n    \"CRITICAL: Every opened \u003cdiv\u003e MUST be explicitly closed with \u003c/div\u003e before opening the next sibling \u003cdiv\u003e. \" ++\n    \"CARD PATTERN — row of sibling cards, each card has stacked label+value (NEVER nest cards): \" ++\n    \"\u003cdiv style=\\\"display:flex;flex-direction:row;gap:16px;padding:16px\\\"\u003e\" ++\n    \"\u003cdiv style=\\\"width:30%;background:#fff;border-radius:8px;padding:16px\\\"\u003e\" ++\n    \"\u003cdiv style=\\\"font-size:13px;color:#666\\\"\u003eLabel A\u003c/div\u003e\" ++\n    \"\u003cdiv style=\\\"font-size:24px;font-weight:bold\\\"\u003eValue A\u003c/div\u003e\" ++\n    \"\u003c/div\u003e\" ++\n    \"\u003cdiv style=\\\"width:30%;background:#fff;border-radius:8px;padding:16px\\\"\u003e\" ++\n    \"\u003cdiv style=\\\"font-size:13px;color:#666\\\"\u003eLabel B\u003c/div\u003e\" ++\n    \"\u003cdiv style=\\\"font-size:24px;font-weight:bold\\\"\u003eValue B\u003c/div\u003e\" ++\n    \"\u003c/div\u003e\" ++\n    \"\u003c/div\u003e \" ++\n    \"TABLE PATTERN — outer column container, SIBLING row divs inside it (NEVER nest rows): \" ++\n    \"\u003cdiv style=\\\"display:flex;flex-direction:column\\\"\u003e\" ++\n    \"\u003cdiv style=\\\"display:flex;flex-direction:row\\\"\u003e\" ++\n    \"\u003cdiv style=\\\"width:40%\\\"\u003eHeader A\u003c/div\u003e\u003cdiv style=\\\"width:60%\\\"\u003eHeader B\u003c/div\u003e\" ++\n    \"\u003c/div\u003e\" ++\n    \"\u003cdiv style=\\\"display:flex;flex-direction:row\\\"\u003e\" ++\n    \"\u003cdiv style=\\\"width:40%\\\"\u003eCell A1\u003c/div\u003e\u003cdiv style=\\\"width:60%\\\"\u003eCell B1\u003c/div\u003e\" ++\n    \"\u003c/div\u003e\" ++\n    \"\u003c/div\u003e\";\n```\n\n\u003c/details\u003e\n\n### ECharts\n\nAn example that shows how to collect public data from a CSV source and build a [D3.js](https://github.com/d3/d3) chart.\n\nWe use the following functions: `zxp.loadHTML()` , `zxp.runScripts()`, `new XMLSerializer()` (serialize the SVG), `zxp.paintSVG()` and `zxp.encode()` (generate WEBP encoded binary) and `zxp.fs.writeFileSync()` to save it locally.\n\nSource: \u003chttps://github.com/ndrean/zexplorer/blob/main/src/examples/echarts/echarts_svg.html\u003e\n\nThe dev-server is up and running. We send a POST request to the endpoint where the payload is the snippet:\n\n```sh\ncurl -s -X POST http://localhost:9984/run --data-binary @src/examples/echarts/run_svg.js\n```\n\n\u003cimg src=\"https://github.com/ndrean/zexplorer/blob/main/src/examples/echarts/echarts_svg.png\" alt=\"D3 chart from CSV\" width=\"400\"\u003e\n\n\u003cbr\u003e\n\n\n\n### Use dynamic HTML with `htm` and paint\n\n\u003cdetails\u003e\u003csummary\u003eWe use htm to build dynamic HTML and render it as an image\u003c/summary\u003e\n\n[Source](https://github.com/ndrean/zexplorer/blob/main/src/examples/frameworks/htm/teset_html.html\")\n\n```html\n\u003chtml\u003e\n  \u003chead\u003e\n    \u003cscript\u003e\n      const { html } = zxp; // embedded in the code\n      const name = \"Zexplorer\";\n      const version = \"0.1.0\";\n      const features = [\"Lexbor DOM\", \"QuickJS\", \"Yoga Layout\", \"ThorVG\"];\n\n      const card = html`\n        \u003cdiv style=${{\n          background: \"#1a1a2e\",\n          color: \"#e0e0e0\",\n          padding: \"20px\",\n        }}\u003e\n          \u003cdiv style=${{\n            background: \"#16213e\",\n            padding: \"10px\",\n            color: \"#f7a41d\",\n          }}\u003e\n            ${name} v${version}\n          \u003c/div\u003e\n          \u003cul style=${{ padding: \"10px\" }}\u003e\n            ${features.map(\n              (f) =\u003e html`\n                \u003cli style=${{\n                  background: \"#0f3460\",\n                  padding: \"5px\",\n                  margin: \"4px\",\n                  color: \"#e94560\",\n                }}\u003e${f}\u003c/li\u003e\n              `\n            )}\n          \u003c/ul\u003e\n        \u003c/div\u003e\n      `;\n\n      document.body.appendChild(card);\n    \u003c/script\u003e\n  \u003c/head\u003e\n  \u003cbody\u003e\u003c/body\u003e\n\u003c/html\u003e\n```\n\n\u003c/details\u003e\n\nWe can use either the dev-server or the CLI.\n  \n**dev-server**: the following JavaScript snippet is our payload. We use the custom methods `zxp.loadHTML`, `zxp.paintDOM` and `zxp.save`.\n\nThe dev-server is up and running. We send a POST request with the the JavaScript snippet:\n\n```js\n// src/examples/frameworks/htm/run.js\nasync function run(input, output) {    \n    const res = await fetch(input);\n    const html = await res.text();\n    zxp.loadHTML(html);\n    await zxp.runScripts();\n    zxp.save(zxp.paintDOM(document.body), output)\n\n}\n\nconst input = \"file://src/examples/frameworks/htm/test_htm.html\"\nconst output = \"src/examples/frameworks/htm/paint.png\"\nrun(input, output);\n```\n\nWe POST the request to the dev-server:\n\n```sh\n  curl -s -X POST http://localhost:9984/run --data-binary @src/examples/frameworks/htm/run.js\n```\n\nor alternatively with the CLI (`-o` to output into the format you want, PNG, JPEG, WEBP or PDF):\n\n```sh\n./zig-out/bin/zxp convert src/examples/frameworks/htm/test_htm.html -o src/examples/frameworks/htm/paint.jpeg\n```\n\n\u003cimg src=\"https://github.com/ndrean/zexplorer/blob/main/src/examples/frameworks/htm/paint.png\" alt=\"demo htm\" width=\"500\" height=\"400\"\u003e\n\n---\n\n### Render an HTML file in the terminal\n\nWe want to render this HTML:\n\n\u003cdetails\u003e\u003csummary\u003eHTML with CSS grid-1D and Flexbox\u003c/summary\u003e\n\n\u003chttps://github.com/ndrean/zexplorer/blob/main/src/examples/render_grid_1d/grid_1d.html\u003e\n\n```html\n\u003chtml\u003e\n  \u003chead\u003e\n    \u003cstyle\u003e\n      body {\n        background: #1e1e2e;\n        padding: 24px;\n        display: flex;\n        flex-direction: column;\n        gap: 20px;\n        width: 760px;\n      }\n\n      h3 {\n        color: #cba6f7;\n        font-size: 13px;\n      }\n\n      /* Test 1: 3 equal columns, wraps to 2 rows */\n      .grid-3col {\n        display: grid;\n        grid-template-columns: repeat(3, 1fr);\n        gap: 12px;\n      }\n      .grid-3col .cell {\n        background: #313244;\n        border: 1px solid #585b70;\n        border-radius: 6px;\n        padding: 16px;\n        color: #a6e3a1;\n        font-size: 14px;\n      }\n\n      /* Test 2: 4 equal columns with gap */\n      .grid-4col {\n        display: grid;\n        grid-template-columns: repeat(4, 1fr);\n        gap: 8px;\n      }\n      .grid-4col .cell {\n        background: #45475a;\n        border: 1px solid #6c7086;\n        border-radius: 4px;\n        padding: 12px;\n        color: #89dceb;\n        font-size: 13px;\n      }\n\n      /* Test 3: grid-auto-flow: column — items flow horizontally */\n      .grid-autoflow {\n        display: grid;\n        grid-auto-flow: column;\n        gap: 10px;\n      }\n      .grid-autoflow .badge {\n        background: #f38ba8;\n        border-radius: 4px;\n        padding: 8px 14px;\n        color: #1e1e2e;\n        font-size: 13px;\n      }\n\n      /* Test 4: place-items: center — single centred item */\n      .grid-center {\n        display: grid;\n        place-items: center;\n        background: #181825;\n        border: 1px solid #cba6f7;\n        border-radius: 8px;\n        height: 100px;\n      }\n      .grid-center .label {\n        background: fuchsia;\n        border-radius: 4px;\n        padding: 10px 24px;\n        color: #1e1e2e;\n        font-size: 14px;\n      }\n    \u003c/style\u003e\n  \u003c/head\u003e\n  \u003cbody\u003e\n    \u003c!-- Test 1: 3-column equal-fr, 6 items → 2 rows --\u003e\n    \u003cdiv\u003e\n      \u003ch3\u003erepeat(3, 1fr) — 6 items, wraps to 2 rows\u003c/h3\u003e\n      \u003cdiv class=\"grid-3col\"\u003e\n        \u003cdiv class=\"cell\"\u003eColumn A\u003c/div\u003e\n        \u003cdiv class=\"cell\"\u003eColumn B\u003c/div\u003e\n        \u003cdiv class=\"cell\"\u003eColumn C\u003c/div\u003e\n        \u003cdiv class=\"cell\"\u003eRow 2 — A\u003c/div\u003e\n        \u003cdiv class=\"cell\"\u003eRow 2 — B\u003c/div\u003e\n        \u003cdiv class=\"cell\"\u003eRow 2 — C\u003c/div\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e\n\n    \u003c!-- Test 2: 4-column grid with gap, 8 items → 2 rows --\u003e\n    \u003cdiv\u003e\n      \u003ch3\u003erepeat(4, 1fr) + gap: 8px — 8 items\u003c/h3\u003e\n      \u003cdiv class=\"grid-4col\"\u003e\n        \u003cdiv class=\"cell\"\u003eJan\u003c/div\u003e\n        \u003cdiv class=\"cell\"\u003eFeb\u003c/div\u003e\n        \u003cdiv class=\"cell\"\u003eMar\u003c/div\u003e\n        \u003cdiv class=\"cell\"\u003eApr\u003c/div\u003e\n        \u003cdiv class=\"cell\"\u003eMay\u003c/div\u003e\n        \u003cdiv class=\"cell\"\u003eJun\u003c/div\u003e\n        \u003cdiv class=\"cell\"\u003eJul\u003c/div\u003e\n        \u003cdiv class=\"cell\"\u003eAug\u003c/div\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e\n\n    \u003c!-- Test 3: grid-auto-flow: column → horizontal flex row --\u003e\n    \u003cdiv\u003e\n      \u003ch3\u003egrid-auto-flow: column — horizontal strip\u003c/h3\u003e\n      \u003cdiv class=\"grid-autoflow\"\u003e\n        \u003cdiv class=\"badge\"\u003eAlpha\u003c/div\u003e\n        \u003cdiv class=\"badge\"\u003eBeta\u003c/div\u003e\n        \u003cdiv class=\"badge\"\u003eGamma\u003c/div\u003e\n        \u003cdiv class=\"badge\"\u003eDelta\u003c/div\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e\n\n    \u003c!-- Test 4: place-items: center → centered item in box --\u003e\n    \u003cdiv\u003e\n      \u003ch3\u003eplace-items: center\u003c/h3\u003e\n      \u003cdiv class=\"grid-center\"\u003e\n        \u003cdiv class=\"label\" style=\"color: yellow\"\u003eCentered content\u003c/div\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e\n  \u003c/body\u003e\n\u003c/html\u003e\n\n```\n\n\u003c/details\u003e\n\nWe will use again the two methods: the long running dev-server and the CLI\n\n**dev-server**: the JS snippet below uses the custom methods `zxp.loadHTML` and `zxp.paintDOM` and `zxp.encode`. This last method encodes the ImageData into the format given by the MIME type.\n\n```js\nconst file = \"file://src/examples/render_grid_1d/grid_1d.html\";\n\nasync function render() {\n  const file_data = await fetch(file);\n  const html = await file_data.text();\n  zxp.loadHTML(html);\n  const img = zxp.paintDOM(document.body);\n  return zxp.encode(img, \"webp\");\n}\n\nrender();\n```\n\nWe start the dev-server with:\n\n```sh\n./zig-out/bin/zxp server\n```\n\nand send an HTTP request with the content of the JavaScript snippet and pipe to display an image in the terminal:\n\n```sh\ncurl -s -X POST http://localhost:9984/run  \\\n--data-binary @src/examples/render_grid_1d/grid_1d_kitty.js \\\n| kitty +kitten icat\n```\n\nand get in your terminal the image:\n\n\u003cimg src=\"https://github.com/ndrean/zexplorer/blob/main/src/examples/render_grid_1d/grid_1d.webp\" alt=\"grid_1d.png\" width=\"500\" height=\"400\"\u003e\n\n\u003cbr\u003e\n\n**CLI**: we pipe `cat` with  `zxp render` to run the snippet and express the desired format with the `-f` flag (defaults to PNG)\n\n```sh\ncat src/examples/render_grid_1d/grid_1d.js | ./zig-out/bin/zxp render - -f webp | kitty +kitten icat\n```\n\n### Scrape Hacker news\n\nLet's fetch the newest posts from the \"hacker news\" website.\n\nWe start the dev-server with:\n\n```sh\n./zig-out/bin/zxp serve\n```\n\n\u003cdetails\u003e\u003csummary\u003eWe run this JavaScript snippet:\u003c/summary\u003e\n\n\u003chttps://github.com/ndrean/zexplorer/blob/main/src/examples/combinator/hacker.js\u003e\n\n```js\nasync function scrape() {\n  url = \"https://news.ycombinator.com/newest\";\n  await zxp.goto(url);\n  return Array.from(document.querySelectorAll(\".titleline a\")).map((a) =\u003e [\n    a.textContent,\n    a.href,\n  ]);\n}\n\nscrape();\n```\n\n\u003c/details\u003e\n\nWe send a POST request where the payload is the JavaScript code:\n\n```sh\ncurl -s -X POST http://localhost:9984/run  --data-binary @src/examples/combinator/hacker.js\n```\n\nWe get back:\n\n```txt\n[\n  [\"Show HN: One-line x402 pay-per-request protection for ElysiaJS APIs\",\"https://github.com/codingstark-dev/x402-elysia\"],\n  [\"github.com/codingstark-dev\",\"from?site=github.com/codingstark-dev\"],\n  ...\n]\n```\n\nIf you use the CLI, we use the verb `scrape` and pass a JavaScript snippet to be executed, using the `-e` flag:\n\n```sh\n./zig-out/bin/zxp scrape https://news.ycombinator.com -e \"Array.from(document.querySelectorAll('.titleline a')).map(a=\u003e([a.textContent,a.href]))\" --pretty\n```\n\nWe can take a snapshot of the website. The rendering is very dependant upon the CSS and we currently support only Flexbox based CSS with no grid-2d nor media-querries no CSS functions and variables.\n\nUsing the CLI, you can pipe the verb `scrape` with the verb `convert`. Note that you must supply the `--base` entry as the prefix of the static assets that will be fetched. You also use the flag `-o` to direct into which format you want to save the snapshot (PNG, WEBP, JPEG, PDF).\n\n```sh\n./zig-out/bin/zxp scrape https://news.ycombinator.com | ./zig-out/bin/zxp convert - --base https://news.ycombinator.com  -o src/examples/combinator/hacker.webp\n```\n\n\u003cimg src=\"https://github.com/ndrean/zexplorer/blob/main/src/examples/combinator/hacker.webp\" alt=\"hacer news\" width=\"400\" height=\"300\"\u003e\n\n\u003cbr\u003e\n\n---\n\n### Scrape a Vercel site in less than 1s\n\nScrape \u003chttps://demo.vercel.store\u003e and get structured data extracted:\n\n**Dev-server**: You prepare a JavaScript snippet to reach the website and pass the selector of your choice. We mimic `puppeteer`'s API with `await zxp.goto()` ot fetch, execute all the received JS chunks and CSS files and parse and sync the DOM and CSS to be able to `await zxp.waitForSelector()` which extracts the data using _querySelector_ against the DOM.\n\n```js\n//  src/examples/vercel-demo/inline-select.js \nasync function select() {\n  await zxp.goto(\"https://demo.vercel.store\");\n  await zxp.waitForSelector(\"a[href^='/product/']\");\n\n  const links = document.querySelectorAll(\"a[href^='/product/']\");\n  const unique = [\n    ...new Set(Array.from(links).map((el) =\u003e el.getAttribute(\"href\"))),\n  ];\n  const items = unique.map((href) =\u003e {\n    const el = document.querySelector(`a[href='${href}']`);\n    return el.textContent.trim();\n  });\n  return items;\n}\n\nselect();\n```\n\nYour server is still running:\n\n```sh\n./zig-out/bin/zxp server\n```\n\nYou send a POST request:\n\n```sh\ncurl -s -X POST http://localhost:9984/run  \\\n--data-binary @src/examples/vercel-demo/inline-select.js\n```\n\nYou receive in your terminal:\n\n```txt\n[\"Acme Circles T-Shirt$20.00USD\", \"Acme Drawstring Bag$12.00USD\", \"Acme Cup$15.00USD\",...]\n```\n\n**CLI**: You use the verb `run`\n\n```sh\nzxp run src/examples/vercel-demo/inline-select.js --pretty\n```\n\n\n---\n\n### Render the Vercel side\n\nIf you want to visualize the website, you can do:\n\n- using the CLI: we use the verb `scrape` and run an additional snippet:\n\n\u003chttps://github.com/ndrean/zexplorer/blob/main/src/examples/vercel-demo/inline-images.js\u003e\n\nThe reason is that Next.js uses `srcset` to serve multiple formats, and we transform it into a `src =\"data:image/webp;base64, ...\"` to render using the Liveserver in VSCode.\n\nWe use two custom native methods `zxp.fetchAll([urls], [headers])` and `zxp.arrayBufferToBase64DataUri(buffer, type)`.\n\n```sh\nzxp scrape https://demo.vercel.store src/examples/inline-images.js -o src/examples/vercel-demo/vercel.html\n```\n\n\u003cdetails\u003e\u003csummary\u003einline-image.js helper\u003c/summary\u003e\n\n```js\n// Resolves Next.js srcset URLs; removes srcset after inlining.\nasync function fetchImages() {\n  const base = \"https://demo.vercel.store\";\n  const imgs = Array.from(document.querySelectorAll(\"img\"));\n\n  // Collect one URL per image (first srcset entry preferred over src).\n  const urls = imgs.map((img) =\u003e {\n    const srcset = img.getAttribute(\"srcset\");\n    if (srcset) {\n      const first = srcset.split(\",\")[0].trim().split(/\\s+/)[0];\n      if (first) return first.startsWith(\"http\") ? first : base + first;\n    }\n    const src = img.getAttribute(\"src\");\n    if (src \u0026\u0026 !src.startsWith(\"data:\")) {\n      return src.startsWith(\"http\") ? src : base + src;\n    }\n    return null;\n  });\n\n  // Fetch all in parallel via libcurl multi.\n  const results = fetchAll(\n    urls.filter((u) =\u003e u !== null),\n    {\n      Accept: \"image/png,image/jpeg,image/webp,*/*;q=0.5\",\n      Referer: base + \"/\",\n    }\n  );\n\n  // Map results back to images (skip nulls).\n  let ri = 0;\n  for (let i = 0; i \u003c imgs.length; i++) {\n    if (urls[i] === null) continue;\n    const r = results[ri++];\n    if (!r || !r.ok) continue;\n    imgs[i].setAttribute(\"src\", arrayBufferToBase64DataUri(r.data, r.type));\n    imgs[i].removeAttribute(\"srcset\");\n  }\n\n  return document.documentElement.outerHTML;\n}\nfetchImages();\n```\n\n\u003c/details\u003e\n\n\u003cimg src=\"https://github.com/ndrean/zexplorer/blob/main/images/demo.vercel.store.png\" alt=\"vercel demo\" width=\"600\" height=\"400\"\u003e\n\n\u003cbr\u003e\n\nWhen using the server, we then use this function along with `zxp.goto()`:\n\n```js\nasync function scrape() {\n  url = \"https://demo.vercel.store\";\n  await zxp.goto(url);\n  return await fetchImages(url); // \u003c-- returns the serailized DOM\n  // return zxp.fs.WriteFileSync(\"src/examples/demo-vercel/vercel.html) \u003c- save disk\n}\nscrape();\n\n```\n\nYou can serve this HTML via the LiveServer to have a snapshot of the Vercel demo website.\n\n\u003chttps://github.com/ndrean/zexplorer/blob/main/demo-vercel/vercel.html\u003e\n\n---\n\n### Generate a Leaflet map PDF report\n\nLoad Leaflet, draw a GeoJSON route on OpenStreetMap tiles, composite the map with an SVG template, and output a multi-layered PDF — all in one shot:\n\n\u003chttps://github.com/ndrean/zexplorer/blob/main/images/RouteReport.pdf\u003e\n\nSee the [full Leaflet-to-PDF example](#embed-leaflet-geojson-path-map-in-an-svg-and-output-a-pdf) below.\n\n---\n\n### CSV parsing\n\nA quick example that uses the CLI: we will execute a JavaScript snippet (`run -e \"...\"`) that reads from stdin (`zxp.stdin.read()`), parses the CSV data (`zxp.csv.parse()`) and conversely stringify it back (`zxp.csv.stringify()`).\n\n**Smoke Test**\n\nWe send a CSV, parse and stringify:\n\n```sh\necho \"name,age\\nAlice,30\\nBob,25\\n\" | \\\n./zig-out/bin/zxp run -e \\\n\"const csv = zxp.stdin.read();\nconst rows = zxp.csv.parse(csv);\nconsole.log(rows); \nconst back = zxp.csv.stringify(rows); \nconsole.log(back);\"\n\n# -\u003e [{\"name\": \"Alice\", \"age\": \"30\"},{\"name\": \"Bob\",\"age\": \"25\"}]\n\n#\"name\",\"age\"\n#Alice,30\n#Bob,25\n```\n\n### Markdown\n\n**Smoke test: HTML -\u003e MD**\n\n```sh\necho \"\u003chtml\u003e\u003cbody\u003e\n  \u003ch2\u003eDOM to Markdown\u003c/h2\u003e\n  \u003cp\u003eA paragraph with \u003cstrong\u003ebold\u003c/strong\u003e and \u003cem\u003eitalic\u003c/em\u003e text.\u003c/p\u003e\n  \u003cul\u003e\u003cli\u003eItem A\u003c/li\u003e\u003cli\u003eItem B\u003c/li\u003e\u003c/ul\u003e\n\u003c/body\u003e\u003c/html\u003e\" \\\n| ./zig-out/bin/zxp run -e \\\n\"zxp.loadHTML(zxp.stdin.read()); \nconst mdFromDOM = zxp.toMarkdown(document.body);\nconsole.error('toMarkdown output:\\n' + mdFromDOM);\"\n\n\n# -\u003e toMarkdown output:\n# ## DOM to Markdown\n\n# A paragraph with **bold** and _italic_ text.\n\n# *   Item A\n#*   Item B\n```\n\n**Smoke Test: MD -\u003e HTML**\n\n```sh\n```md\nconst md = `# Hello from Markdown\n\nThis is **GFM** rendered natively via md4c.\n\n| Column A | Column B |\n|----------|----------|\n| foo      | bar      |\n| baz      | qux      |\n\n- [x] md4c parses GFM Markdown\n- [x] lexbor renders the resulting HTML\n- [ ] profit\n\n~~Strikethrough~~, https://example.com autolink, and \u003cspan style=\"color:red\"\u003eraw HTML\u003c/span\u003e all pass through.\n```\n\n```sh\necho \"# Hello from Markdown\n\nThis is **GFM** rendered natively via md4c.\n\n| Column A | Column B |\n|----------|----------|\n| foo      | bar      |\n| baz      | qux      |\n\n- [x] md4c parses GFM Markdown\n- [x] lexbor renders the resulting HTML\n- [ ] profit\n\n~~Strikethrough~~, https://example.com autolink, and \u003cspan style=\"color:red\"\u003eraw HTML\u003c/span\u003e all pass through.\" | ./zig-out/bin/zxp run -e \"zxp.markdownToHTML(zxp.stdin.read())\"\n```\n\ngives:\n\n```html\n\u003ch1\u003eHello from Markdown\u003c/h1\u003e\n  \u003cp\u003eThis is \u003cstrong\u003eGFM\u003c/strong\u003e rendered natively via md4c.\u003c/p\u003e\n  \u003ctable\u003e\n    \u003cthead\u003e\n      \u003ctr\u003e\n        \u003cth\u003eColumn A\u003c/th\u003e\n        \u003cth\u003eColumn B\u003c/th\u003e\n      \u003c/tr\u003e\n    \u003c/thead\u003e\n    \u003ctbody\u003e\n      \u003ctr\u003e\n        \u003ctd\u003efoo\u003c/td\u003e\n        \u003ctd\u003ebar\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n        \u003ctd\u003ebaz\u003c/td\u003e\n        \u003ctd\u003equx\u003c/td\u003e\n      \u003c/tr\u003e\n    \u003c/tbody\u003e\n  \u003c/table\u003e\n  \u003cul\u003e\n    \u003cli class=\"task-list-item\"\u003e\u003cinput type=\"checkbox\" class=\"task-list-item-checkbox\" disabled checked\u003emd4c parses GFM Markdown\u003c/li\u003e\n    \u003cli class=\"task-list-item\"\u003e\u003cinput type=\"checkbox\" class=\"task-list-item-checkbox\" disabled checked\u003elexbor renders the resulting HTML\u003c/li\u003e\n    \u003cli class=\"task-list-item\"\u003e\u003cinput type=\"checkbox\" class=\"task-list-item-checkbox\" disabled\u003eprofit\u003c/li\u003e\n  \u003c/ul\u003e\n\u003cp\u003e\u003cdel\u003eStrikethrough\u003c/del\u003e, \u003ca href=\"https://example.com\"\u003ehttps://example.com\u003c/a\u003e autolink, and \u003cspan style=color:red\u003eraw HTML\u003c/span\u003e all pass through.\u003c/p\u003e\n```\n\n\n## Library quick start\n\n### Hello world\n\n**[Dual primitives]** When you use `zexplorer` as a `Zig` library, you have DOM primitives accessible in the Zig code. Since these primitives are ported into the JavaScript runtime, you can access them as well in the runtime.\n\n**[First example]** You have a simple HTML file, _examples/hello_world.html_.\n\n```html\n\u003cdiv\u003e\n  \u003cp\u003eHello world\u003c/p\u003e\n\u003c/div\u003e\n```\n\nWe will parse it with `z.parseHTML()` and print the DOM into the console with `prettyPrint()`. We will build and execute the following _src/ex1.zig_ file where we respect the careful and explicit Zig memory allocation ceremony:\n\n```zig\nvar debug_allocator: std.heap.DebugAllocator(.{}) = .init;\n\npub fn main() !void {\n  const gpa = debug_allocator.allocator();\n  defer { \n    _= .ok == debug_allocator.deinit();\n  }\n\n  const html = @embedFile(\"examples/hello_world.html\");\n\n  const doc = try z.parseHTML(allocator, html);\n  defer z.destroyDocument(doc);\n\n  try z.prettyPrint(gpa, z.documentRoot(doc).?);\n}\n\nconst std = @import(\"std\");\nconst z = @import(\"zexplorer\");\n```\n\nThe executable is named \"zxp\". We build the _main.zig_ file (defined for you in _build.zig_ asa the \"run\" step), and execute it by using its name:\n\n```sh\n$\u003e zig build run\n$\u003e ./zig-out/bin/zxp\n```\n\nIn the terminal, you see:\n\n```txt\n\u003cdiv\u003e\n  \u003cp\u003e\n    \"Hello world\"\n  \u003c/p\u003e\n\u003c/div\u003e\n```\n\n---\n\n**[Example of dual primitives]** We create a DOM and query it in pur Zig and then using embedded JavaScript:\n\nIn this example, we first parse the HTML file with `DOMParser.parseFromString()` and then we can query the \"VDOM\" in Zig with `z.querySelector()` and get the content with `textContent_zc()`.\n\n\u003cdetails\u003e\u003csummary\u003eUse parser.parseFromString \u003c/summary\u003e\n\n```zig\nvar debug_allocator: std.heap.DebugAllocator(.{}) = .init;\n\npub fn main() !void {\n  const gpa = debug_allocator.allocator();\n  defer {\n    _ =  .ok == debug_allocator.deinit();\n  }\n\n  // alternative: using DOMParser alternative instead of `parseHTML()`\n  var parser = try z.DOMParser.init(gpa);\n  defer parser.deinit();\n\n  const html = @embedFile(\"examples/hello_world.html\");\n  const doc = try parser.parseFromString(html);\n  defer z.destroyDocument(doc);\n\n  const p_elt = try z.querySelector(gpa, z.bodyNode(doc).?, \"p\");\n  const p_node = z.elementToNode(p_elt.?);\n  const inner_text = z.textContent_zc(p_node); // no allocation\n\n  std.debug.print(\"[Zig] {s}\\n\", .{inner_text});\n}\n\nconst std = @import(\"std\");\nconst z = @import(\"zexplorer\");\n```\n\n\u003c/details\u003e\n\nThen, we run a JavaScript snippet that knows about the \"vDOM\". Indeed, zexplorer brings in a default `document` to which the JavaScript code accesses via a globalThis `document`. We use the engine `z.ScriptEngine`  and the `loadHTML()` and `evalModule()` methods.\n\n\u003cdetails\u003e\u003csummary\u003eWith a JavaScript snippet\u003c/summary\u003e\n\n```zig\nvar debug_allocator: std.heap.DebugAllocator(.{}) = .init;\n\npub fn main() !void {\n  const gpa = debug_allocator.allocator();\n  defer _ = debug_allocator.deinit();\n\n  const sandbox_root = try std.fs.cwd().realpathAlloc(gpa, \".\");\n  defer gpa.free(sandbox_root);\n\n  var engine = try z.ScriptEngine.init(gpa, sandbox_root);\n  defer engine.deinit();\n\n  const js =\n    \\\\const innerText = document.querySelector(\"p\").textContent;\n    \\\\console.log(\"[JS]\", innerText);\n    ;\n  const html = @embedFile(\"examples/hello_world.html\");\n\n  try engine.loadHTML(html);\n  try engine.evalModule(js, \"\u003cscript\u003e\");\n}\n\nconst std = @import(\"std\");\nconst z = @import(\"zexplorer\");\n```\n\n\u003c/details\u003e\n\nYou build and execute the _main.zig_ file via the \"run\" step:\n\n```sh\n$\u003e zig build run\n$\u003e ./zig-out/bin/zxp-ex\n```\n\nand get in the terminal:\n\n```txt\n[Zig] Hello world\n[JS] Hello world\n```\n\n---\n\n**[Run JavaScript]** Let's run a JavaScript snippet that builds the same DOM programmatically and adds a `\u003cscript\u003e` to it:\n\n```js\n// src/examples/hello_world.js\nconst div = document.createElement(\"div\");\nconst p = document.createElement(\"p\");\np.textContent = \"Hello zexplorer\";\ndiv.appendChild(p);\ndocument.body.appendChild(div);\n\nconst script = document.createElement(\"script\");\nscript.textContent = \"const hello = document.querySelector('p').textContent; console.log(\"[JS]\", hello);\";\ndocument.head.appendChild(script);\n```\n\nIn the _main.zig_ file, we use the `z.ScriptEngine` to load the JS code `engine.evalModule()` and then execute it with `engine.executeScripts()`. We take care of all the memory allocations:\n\n\u003cdetails\u003e\u003csummary\u003eUsing engine.evalModule() and engine.executeScripts()\u003c/summary\u003e\n\n```zig\nvar debug_allocator: std.heap.DebugAllocator(.{}) = .init;\n\npub fn main() !void {\n    const gpa = debug_allocator.allocator();\n    defer _ = debug_allocator.deinit();\n    const sandbox_root = try std.fs.cwd().realpathAlloc(gpa, \".\");\n    defer gpa.free(sandbox_root);\n\n    var engine = try z.ScriptEngine.init(gpa, sandbox_root);\n    defer engine.deinit();\n\n    const script = @embedFile(\"examples/hello_world.js\");\n\n    const val = try engine.evalModule(script, \"\u003cmy-script\u003e\");\n    defer engine.ctx.freeValue(val);\n\n    try engine.executeScripts(gpa, \".\");\n\n    // Print the DOM to stdout\n    try z.prettyPrint(gpa, z.documentRoot(engine.dom.doc).?);\n}\n\nconst std = @import(\"std\");\nconst z = @import(\"zexplorer\");\n```\n\n\u003c/details\u003e\n\nYou build and execute the _main.zig_ file via the \"run\" step:\n\n\n```sh\n$\u003e zig build run\n$\u003e ./zig-out/bin/zxp-ex\n```\n\nThe output in the terminal:\n\n```txt\n[JS] Hello zexplorer  \u003c-- zexplorer executed the script\n\n\u003chtml\u003e                \u003c-- zexplorer \"pretty-printed\" the DOM to stdout\n  \u003chead\u003e\n    \u003cscript\u003e\n      \"const hello = document.querySelector('p').textContent; console.log('[JS]', hello);\"\n    \u003c/script\u003e\n  \u003c/head\u003e\n  \u003cbody\u003e\n    \u003cdiv\u003e\n      \u003cp\u003e\n        \"Hello zexplorer\"\n      \u003c/p\u003e\n    \u003c/div\u003e\n  \u003c/body\u003e\n\u003c/html\u003e\n```\n\n---\n\n**[HTML with script]** You have an HTML file (_examples/html-script.html_) with a script. We use the `z.ScriptEngine` and use a higher level primitive `loadPage()` that parses the HTML and CSS and syncs it, and reads and evaluates the found `\u003cscript\u003e`'s elements.\n\n```html\n\u003cbody\u003e\n  \u003cp\u003eHello Zig\u003c/p\u003e\n  \u003cscript\u003e\n    const p = document.querySelector(\"p\");\n    console.log(p.textContent);\n  \u003c/script\u003e\n\u003c/body\u003e\n```\n\nYour _main.zig_  file contains:\n\n\u003cdetails\u003e\u003csummary\u003eUsing ScriptEngine.loadPage()\u003c/summary\u003e\n\n```zig\nvar debug_allocator: std.heap.DebugAllocator(.{}) = .init;\n\npub fn main() !void {\n    const gpa = debug_allocator.allocator();\n    defer _ = debug_allocator.deinit();\n    const sandbox_root = try std.fs.cwd().realpathAlloc(gpa, \".\");\n    defer gpa.free(sandbox_root);\n\n    var engine = try z.ScriptEngine.init(gpa, sandbox_root);\n    defer engine.deinit();\n\n    const html = @embedFile(\"html-script.html\");\n    try engine.loadPage(html, .{});\n\n    try z.prettyPrint(gpa, z.documentRoot(engine.dom.doc).?);\n}\n\nconst std = @import(\"std\");\nconst z = @import(\"zexplorer\");\n```\n\n\u003c/details\u003e\n\nYou build and execute _main.zig_:\n\n```sh\n$\u003e zig build run\n$\u003e ./zig-out/bin/zxp-ex\n```\n\nThe output is as expected:\n\n```txt\n[JS] Hello Zig\n\n\u003chtml\u003e\n  \u003chead\u003e\n    ...\n```\n\n### Scrape a Vercel site\n\nWe scrape \u003chttps://demo.vercel.store\u003e. It makes 12 HTTP requests and runs 42 scripts in order to hydrate the first SSR rendered page.\n\nYou can scrap the Vercel website with this JavaScript snippet that mimics `Puppeteer`'s API.\n\n```js\n// vercel.js\n\nasync function testVercel() {\n  try {\n    await zexplorer.goto(\"https://demo.vercel.store\");\n\n    await zexplorer.waitForSelector(\"a[href^='/product/']\");\n\n    const links = document.querySelectorAll(\"a[href^='/product/']\");\n    const unique = [...new Set(Array.from(links).map(el =\u003e el.getAttribute('href')))];\n    const items = unique.map(href =\u003e {\n      const el = document.querySelector(`a[href='${href}']`);\n      return el.textContent.trim();\n    });\n\n    console.log(items);\n    return items; // \u003c-- return to the engine to marshall the array\n  } catch (err) {\n    console.error(err);\n  }\n}\n```\n\nYou pass it to the engine:\n\n\u003cdetails\u003e\u003csummary\u003eUsing engine.Eval() and engine.evalAsyncAs()\u003c/summary\u003e\n\n```zig\nvar debug_allocator: std.heap.DebugAllocator(.{}) = .init;\n\npub fn main() !void {\n    const gpa = debug_allocator.allocator();\n    defer _ = debug_allocator.deinit();\n    const sandbox_root = try std.fs.cwd().realpathAlloc(gpa, \".\");\n    defer gpa.free(sandbox_root);\n\n    var engine = try z.ScriptEngine.init(gpa, sandbox_root);\n    defer engine.deinit();\n\n    const script = @embedFile(\"vercel.js\");\n    const val = try engine.eval(script, \"test_vercel.js\", .global);\n    defer engine.ctx.freeValue(val);\n    \n    // output is an Array of strings\n    const items = try engine.evalAsyncAs(\n        allocator,\n        []const []const u8,\n        \"testVercel()\",\n        \"\u003cvercel\u003e\",\n    );\n    defer {\n        for (items) |item| allocator.free(item);\n        allocator.free(items);\n    }\n\n    // output : toOwnedSlice or file\n    var buf: std.ArrayList(u8) = .empty;\n    defer buf.deinit(allocator);\n    for (items) |item| {\n        try buf.appendSlice(allocator, item);\n        try buf.append(allocator, '\\n');\n    }\n\n    try std.fs.cwd().writeFile(\n        .{\n            .sub_path = \"vercel_data.txt\",\n            .data = buf.items,\n        },\n    );\n}\n```\n\n\u003c/details\u003e\n\nand you get your data back in 1s:\n\n```txt\n0.17s user 0.14s system 37% cpu 0.835 total\n\n[\n  \"Acme Circles T-Shirt$20.00USD\",\n  \"Acme Drawstring Bag$12.00USD\",\n  \"Acme Cup$15.00USD\",\n  \"Acme Mug$15.00USD\",\n  \"Acme Hoodie$50.00USD\",\n  \"Acme Baby Onesie$10.00USD\",\n  \"Acme Baby Cap$10.00USD\"\n]\n```\n\nTODO\n\n```sh\nTODO\n```\n\n---\n\n## Sanitize HTML \u0026 CSS\n\nFour CSS threat vectors are sanitized in a single pass — covering every way untrusted CSS can reach a document:\n\n| #   | Vector                                                                            | Example threat                                        |\n| --- | --------------------------------------------------------------------------------- | ----------------------------------------------------- |\n| 1   | External stylesheet (`\u003clink\u003e`)                                                    | `background-image: url(\"evil.com\")`                   |\n| 2   | `\u003cstyle\u003e` block                                                                   | `background: url(javascript:alert(\"xss\"))`            |\n| 3   | Inline `style=\"\"` attribute                                                       | `behavior: url(evil.htc)`                             |\n| 4   | JS DOM mutation (`innerHTML`, `outerHTML`, `insertAdjacentHTML`, `createElement`) | `background-image: url(evil.com)` injected at runtime |\n\nVectors 1–3 are static (present in the HTML source). Vector 4 is dynamic — injected by JavaScript after parse.\n\n---\n\n### Vectors 1–3: static CSS (`\u003clink\u003e`, `\u003cstyle\u003e`, inline)\n\n\u003cdetails\u003e\u003csummary\u003esrc/examples/test_example.css\u003c/summary\u003e\n\n```css\n.untrusted {\n  color: red;\n  background-color: #ffe0e0;\n  font-size: 18px;\n  padding: 8px;\n  background-image: url(\"evil.com\");  /* ← threat: exfiltration via image load */\n}\n.trusted {\n  color: darkgreen;\n  background-color: #e0ffe0;\n  font-size: 14px;\n  padding: 6px;\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003esrc/examples/test_example.html\u003c/summary\u003e\n\n```html\n\u003chtml\u003e\n  \u003chead\u003e\n    \u003clink rel=\"stylesheet\" href=\"test_example.css\" /\u003e  \u003c!-- vector 1 --\u003e\n    \u003cstyle\u003e\n      body { margin: 10px; padding: 5px;\n             background: url(javascript:alert(\"xss\")); } /* vector 2: threat */\n      .trusted { color: green; }\n    \u003c/style\u003e\n    \u003cscript src=\"test_example.js\"\u003e\u003c/script\u003e\n  \u003c/head\u003e\n  \u003cbody\u003e\n    \u003cdiv class=\"untrusted\" onclick=\"alert(1)\"           \u003c!-- onclick: threat --\u003e\n         style=\"font-size: 16px\"\u003e                       \u003c!-- vector 3 --\u003e\n      red + pink bg from \u0026lt;link\u0026gt; | font-size 16px from inline | onclick removed\n    \u003c/div\u003e\n    \u003cp class=\"untrusted\" style=\"font-size: 12px\"\u003e\n      red + pink bg from \u0026lt;link\u0026gt; | font-size 12px from inline | bg-image threat stripped\n    \u003c/p\u003e\n    \u003cp class=\"trusted\" style=\"padding: 20px\"\u003e\n      darkgreen + green bg from \u0026lt;link\u0026gt; | color green from \u0026lt;style\u0026gt; block | padding from inline\n    \u003c/p\u003e\n  \u003c/body\u003e\n\u003c/html\u003e\n```\n\n\u003c/details\u003e\n\n**Sanitize and render:**\n\n```sh\nzxp sanitize src/examples/test_example.html \u003e sanitized.html\nzxp convert sanitized.html -o test_example_sanitized.png\n```\n\n\u003c!-- generated image: test_example_sanitized.png --\u003e\n\n\u003cdetails\u003e\u003csummary\u003esanitized.html — annotated output\u003c/summary\u003e\n\n```html\n\u003chtml\u003e\u003chead\u003e\n    \u003c!-- ✓ vector 1: \u003clink\u003e replaced by inline \u003cstyle\u003e; background-image stripped --\u003e\n    \u003cstyle\u003e.untrusted { color: red; background-color: #ffe0e0; font-size: 18px; padding: 8px }\n.trusted { color: darkgreen; background-color: #e0ffe0; font-size: 14px; padding: 6px }\n\u003c/style\u003e\n    \u003c!-- ✓ vector 2: background: url(javascript:...) stripped from \u003cstyle\u003e block --\u003e\n    \u003cstyle\u003ebody { margin: 10px; padding: 5px }\n.trusted { color: green }\n\u003c/style\u003e\n    \u003c!-- ✓ \u003cscript\u003e removed (default) --\u003e\n  \u003c/head\u003e\n  \u003cbody\u003e\n    \u003c!-- ✓ onclick removed; vector 3 inline style preserved (font-size is safe) --\u003e\n    \u003cdiv class=\"untrusted\" style=\"font-size: 16px\"\u003e\n      red + pink bg from \u0026lt;link\u0026gt; | font-size 16px from inline | onclick removed\n    \u003c/div\u003e\n    \u003cp class=\"untrusted\" style=\"font-size: 12px\"\u003e\n      red + pink bg from \u0026lt;link\u0026gt; | font-size 12px from inline | bg-image threat stripped\n    \u003c/p\u003e\n    \u003cp class=\"trusted\" style=\"padding: 20px\"\u003e\n      darkgreen + green bg from \u0026lt;link\u0026gt; | color green from \u0026lt;style\u0026gt; block | padding from inline\n    \u003c/p\u003e\n  \u003c/body\u003e\n\u003c/html\u003e\n```\n\n\u003c/details\u003e\n\n**Programmatic verification** — re-parse the sanitized output cold (no network, no external files) and confirm via `getComputedStyle`:\n\n\u003cdetails\u003e\u003csummary\u003eZig verification (src/examples/test_example.zig)\u003c/summary\u003e\n\n```zig\ntry engine.loadPage(@embedFile(\"test_example.html\"), .{\n    .sanitize = true,\n    .base_dir = \"src/examples\",\n    .execute_scripts = true,\n    .load_stylesheets = true,\n    .sanitizer_options = .{ .remove_scripts = false },\n});\n\nconst doc = engine.dom.doc;\n\n// vector 1 — external stylesheet: safe props survive, threat stripped\nif (z.querySelector(doc, \".untrusted\")) |el| {\n    _ = try z.getComputedStyle(ta, el, \"color\");             // \"red\"      ✓ safe\n    _ = try z.getComputedStyle(ta, el, \"background-color\");  // \"#ffe0e0\"  ✓ safe\n    _ = try z.getComputedStyle(ta, el, \"background-image\");  // null       ✓ stripped\n}\n\n// vector 2 — \u003cstyle\u003e block: safe props survive, js: url stripped\nif (z.querySelector(doc, \"body\")) |el| {\n    _ = try z.getComputedStyle(ta, el, \"margin\");            // \"10px\"     ✓ safe\n    _ = try z.getComputedStyle(ta, el, \"background\");        // null       ✓ stripped\n}\n\n// vector 3 — inline style: safe font-size survives\nif (z.querySelector(doc, \"div.untrusted\")) |el| {\n    _ = try z.getComputedStyle(ta, el, \"font-size\");         // \"16px\"     ✓ safe\n    // onclick attribute removed:\n    std.debug.assert(z.getAttribute_zc(el, \"onclick\") == null);\n}\n```\n\n\u003c/details\u003e\n\n---\n\n### Vector 4: JS DOM mutation\n\nJavaScript can inject HTML at runtime via `innerHTML`, `outerHTML`, `insertAdjacentHTML`, or `createElement` + `setAttribute`. Each mutation is intercepted and sanitized at the point of injection when `sanitize = true`.\n\n\u003cdetails\u003e\u003csummary\u003esrc/examples/test_sanitize_injection.html\u003c/summary\u003e\n\n```html\n\u003chtml\u003e\n  \u003cbody\u003e\n    \u003cdiv id=\"t1\"\u003e\u003c/div\u003e\n    \u003cdiv id=\"t2-placeholder\"\u003e\u003c/div\u003e\n    \u003cdiv id=\"t3\"\u003e\u003c/div\u003e\n    \u003cdiv id=\"t4-host\"\u003e\u003c/div\u003e\n    \u003cscript\u003e\n      // Each injection carries a safe property (color:red) and a threat (background-image)\n      document.getElementById(\"t1\").innerHTML =\n        '\u003cp id=\"r1\" style=\"color:red; background-image:url(evil.com)\"\u003einnerHTML | red from inline | bg-image stripped\u003c/p\u003e';\n\n      document.getElementById(\"t2-placeholder\").outerHTML =\n        '\u003cp id=\"r2\" style=\"color:red; background-image:url(evil2.com)\"\u003eouterHTML | red from inline | bg-image stripped\u003c/p\u003e';\n\n      document.getElementById(\"t3\").insertAdjacentHTML(\"beforeend\",\n        '\u003cp id=\"r3\" style=\"color:red; background-image:url(evil3.com)\"\u003einsertAdjacentHTML | red from inline | bg-image stripped\u003c/p\u003e');\n\n      const p4 = document.createElement(\"p\");\n      p4.id = \"r4\";\n      p4.setAttribute(\"style\", \"color:red; background-image:url(evil4.com)\");\n      p4.textContent = \"createElement + setAttribute | red from inline | bg-image stripped\";\n      document.getElementById(\"t4-host\").appendChild(p4);\n    \u003c/script\u003e\n  \u003c/body\u003e\n\u003c/html\u003e\n```\n\n\u003c/details\u003e\n\n**Run (Zig API — sanitize + render):**\n\n```sh\nzig build example -Dname=test_sanitize_injection\n```\n\n\u003c!-- generated image: test_sanitize_injection.png --\u003e\n\n**Programmatic verification** (from `src/examples/test_sanitize_injection.zig`):\n\n```txt\n  [innerHTML]              style=\"color: red\"   color ✓   bg-image (stripped) ✓   PASS\n  [outerHTML]              style=\"color: red\"   color ✓   bg-image (stripped) ✓   PASS\n  [insertAdjacentHTML]     style=\"color: red\"   color ✓   bg-image (stripped) ✓   PASS\n  [createElement+setAttribute]  style=\"color: red\"   color ✓   bg-image (stripped) ✓   PASS\n```\n\nIn all four cases: the safe property (`color: red`) reaches the computed style, the threat (`background-image: url(...)`) is stripped before the DOM is committed. The rendered image shows red text on a white background — no external image loads occurred.\n\n---\n\n### Generate OG images from SVG templates\n\nWe display two examples that show that `zexplorer` overlaps partially [node-canvas](https://github.com/Automattic/node-canvas) and [Vercel/satori](https://github.com/vercel/satori)  (no JSX but `React` can be loaded) but is very lightweight and limited.\n\nGiven this SVG (designed in Figma):\n\n\u003cdetails\u003e\u003csummary\u003eSVG template\u003c/summary\u003e\n\n```html\n\u003csvg width=\"1200\" height=\"630\" viewBox=\"0 0 1200 630\" xmlns=\"http://www.w3.org/2000/svg\"\u003e\n  \u003cdefs\u003e\n    \u003cmask id=\"hole\"\u003e\n        \u003crect width=\"1200\" height=\"630\" fill=\"white\" /\u003e\n        \u003ccircle cx=\"175\" cy=\"175\" r=\"75\" fill=\"black\" /\u003e\n    \u003c/mask\u003e\n  \u003c/defs\u003e\n  \u003crect width=\"1200\" height=\"630\" fill=\"#0f172a\" mask=\"url(#hole)\"/\u003e\n        \n  \u003ctext x=\"100\" y=\"380\" font-family=\"Arial\" font-size=\"80\" fill=\"#ffffff\" font-weight=\"bold\"\u003e\n    {{TITLE}}\n  \u003c/text\u003e\n        \n  \u003ctext x=\"100\" y=\"480\" font-family=\"Arial\" font-size=\"40\" fill=\"#94a3b8\"\u003e\n    Written by {{AUTHOR}}\n  \u003c/text\u003e\n \u003c/svg\u003e\n```\n\n\u003c/details\u003e\n\nThe following \"standard\" JavaScript snippet makes a layered composition of a \"fetched\" image and the interpolated SVG template inside a Canvas.\n\nYou extract the data and return an ArrayBuffer that Zig will marshall.\n\n\u003cdetails\u003e\u003csummary\u003eJS code\u003c/summary\u003e\n\n```js\nasync function loadImage(url) {\n  return await new Promise((resolve, reject) =\u003e {\n    try {\n      const img = new Image();\n      img.onload = () =\u003e resolve(img);\n      img.onerror = (e) =\u003e reject(new Error(`Image failed to load: ${url}`));\n      img.src = url;\n    } catch (e) {\n      reject(new Error(`Failed to fetch image: ${e.message}`));\n    }\n  });\n}\n\nasync function generateOGImage({ title, author, avatarUrl }) {\n  console.log(`Generating OG Image for:  ${title}`);\n  const avatarImg = await loadImage(avatarUrl);\n\n  const templ_res = await fetch(\n    \"file://src/examples/test_og_generator_template_v2.svg\",\n  );\n\n  const rawSvgTemplate = await templ_res.text();\n  const finalSvgText = rawSvgTemplate\n    .replace(\"{{TITLE}}\", title)\n    .replace(\"{{AUTHOR}}\", author);\n  const svgBlob = new Blob([finalSvgText], { type: \"image/svg+xml\" });\n  const imgSVG = await createImageBitmap(svgBlob);\n\n  const canvas = document.createElement(\"canvas\");\n  canvas.width = 1200;\n  canvas.height = 630;\n  const ctx = canvas.getContext(\"2d\");\n  ctx.drawImage(avatarImg, 100, 100, 150, 150); // in the hole\n  ctx.drawImage(imgSVG, 0, 0);\n\n  const pngBlob = await canvas.toBlob();\n  return await pngBlob.arrayBuffer();\n}\n\nasync function renderTemplate() {\n  try {\n    console.log(\"Called\");\n    const pngBytes = await generateOGImage({\n      title: \"Headless Browser in Zig\",\n      author: \"N. Drean\",\n      avatarUrl: \"https://github.com/torvalds.png\",\n    });\n\n    return pngBytes; // return data to Zig to save in a file\n  } catch (e) {\n    console.error(\"Failed to generate OG image:\", e);\n  }\n}\n```\n\n\u003c/details\u003e\n\nThe Zig code to run this is quite simple:\n\n\n\u003cdetails\u003e\u003csummary\u003eZig runner\u003c/summary\u003e\n\n```zig\npub fn main() !void {\n    const allocator = std.testing.allocator;\n\n    const sandbox_root = try std.fs.cwd().realpathAlloc(gpa, \".\");\n    defer gpa.free(sandbox_root);\n\n    try run_test(gpa, sandbox_root);\n}\n\nfn run_test(allocator: std.mem.Allocator, sbx: []const u8) !void {\n    var zxp_rt = z.ZxpRuntime.init(allocator, sbx);\n    defer zxp_rt.deinit();\n    var engine = try z.ScriptEngine.init(allocator, zxp_rt);\n    defer engine.deinit();\n\n    const script = @embedFile(\"test_og_generator.js\");\n\n    const val = try engine.eval(script, \"\u003cscript\u003e\", .global);\n    defer engine.ctx.freeValue(val);\n\n    const png_bytes = try engine.evalAsyncAs(allocator, []const u8, \"renderTemplate()\", \"\u003csvg-template\u003e\");\n    defer allocator.free(png_bytes);\n\n    try std.fs.cwd().writeFile(.{.sub_path = \"templated.png\", .data = png_bytes});\n}\n\nconst std = @import(\"std\");\nconst z = @import(\"zexplorer\");\nconst ScriptEngine = z.ScriptEngine;\nconst js_canvas = z.js_canvas;\n```\n\n\u003c/details\u003e\n\nThe result is:\n\n\u003cimg src=\"https://github.com/ndrean/zexplorer/blob/main/template_generated.png\" alt=\"OG image\" width=\"600\" height=\"300\"\u003e\n\n\n---\n\n### Embed Leaflet geoJSON path map in an SVG and output a PDF\n\n\u003cdetails\u003e\u003csummary\u003eThe HTML file that draws a Leaflet map into an SVG template and renders a PDF\u003c/summary\u003e\n\n```html\n\u003c!doctype html\u003e\n\u003chtml\u003e\n  \u003chead\u003e\n    \u003clink\n      rel=\"stylesheet\"\n      href=\"https://unpkg.com/leaflet@1.9.4/dist/leaflet.css\"\n    /\u003e\n    \u003cscript src=\"https://unpkg.com/leaflet@1.9.4/dist/leaflet.js\"\u003e\u003c/script\u003e\n  \u003c/head\u003e\n  \u003cbody\u003e\n    \u003cdiv id=\"map\" style=\"width: 800px; height: 600px\"\u003e\u003c/div\u003e\n\n    \u003cscript\u003e\n      async function runDrawGeoJSONRoute(deliveryData) {\n        console.log(\"[Test] Booting Leaflet GeoJSON Engine...\");\n\n        const map = L.map(\"map\", {\n          zoomControl: false,\n          attributionControl: false,\n        }).setView([51.505, -0.09], 13);\n\n        L.tileLayer(\"https://tile.openstreetmap.org/{z}/{x}/{y}.png\").addTo(\n          map,\n        );\n\n        // Draw a path from Hyde Park to the Tower of London!\n        const route = {\n          type: \"LineString\",\n          coordinates: [\n            [-0.15, 51.505],\n            [-0.12, 51.51],\n            [-0.076, 51.508],\n          ],\n        };\n        L.geoJSON(route, {\n          style: { color: \"red\", weight: 6, opacity: 0.8 },\n        }).addTo(map);\n\n        // 1. Extract the Tiles\n        const tiles = Array.from(\n          document.querySelectorAll(\"img.leaflet-tile\"),\n        ).map((img) =\u003e ({\n          url: img.src,\n          x: parseInt(img.style.left || 0, 10),\n          y: parseInt(img.style.top || 0, 10),\n        }));\n\n        // 2. Extract the SVG Vector Data!\n        const svgElement = document.querySelector(\".leaflet-overlay-pane svg\");\n        const svgString = svgElement ? svgElement.outerHTML : \"\";\n\n        console.log(`✅ Extracted ${tiles.length} tiles and SVG overlay!`);\n\n        const readyTiles = [];\n        for (const t of tiles) {\n          try {\n            const res = await fetch(t.url);\n            const buffer = await res.arrayBuffer();\n            readyTiles.push({\n              buffer: buffer,\n              x: t.x,\n              y: t.y,\n              w: t.w,\n              h: t.h,\n            });\n            console.log(`  + Fetched tile at (${t.x}, ${t.y})`);\n          } catch (e) {\n            console.log(`  - Failed to fetch ${t.url}`);\n          }\n        }\n\n        // 3. Send to Zig Compositor\n        // CRITICAL: Dimensions must match the CSS width/height of the map div!\n        const mapBuffer = zexplorer.generateRoutePng(\n          readyTiles,\n          svgString,\n          null, // No file output, keep in memory\n          800, // Map DOM width\n          600, // Map DOM height\n        );\n        console.log(\"Composited Map loaded:\", mapBuffer.byteLength, \"bytes\");\n\n        // 4. Load the UI Background Template\n        const templateRes = await fetch(\n          \"file://src/examples/test_route_report_template.svg\",\n        );\n        const svgText = await templateRes.text();\n        const svgBlob = new Blob([svgText], { type: \"image/svg+xml\" });\n        const bgBitmap = await createImageBitmap(svgBlob);\n\n        // 5. Assemble the PDF Layout (Top-to-Bottom)\n        const pdf = new PDFDocument();\n        pdf.addPage(); // Zig backend handles A4 sizing natively\n\n        // Layer 1: Background\n        pdf.drawImage(bgBitmap, 0, 0, 595, 842);\n\n        // Calculate responsive dimensions\n        const margin = 40;\n        const pdfPageWidth = 595;\n        const maxImageWidth = pdfPageWidth - margin * 2;\n        const imgWidth = maxImageWidth;\n        const imgHeight = (600 / 800) * imgWidth; // 4:3 Aspect Ratio scaling\n        const mapStartY = 160;\n\n        // Layer 2: Title Text at the top\n        pdf.fillStyle = \"#0f172a\";\n        pdf.setFont(\"Roboto-Bold\", 24);\n        pdf.fillText(\"Delivery Route Summary\", margin, margin + 20);\n\n        // Layer 3: The Map (Placed below the title)\n        pdf.drawImageFromBuffer(\n          mapBuffer,\n          margin,\n          mapStartY,\n          imgWidth,\n          imgHeight,\n        );\n\n        // Layer 4: Report Data (Placed dynamically below the map)\n        const textStartY = mapStartY + imgHeight + 40; // Properly declared and calculated\n        pdf.setFont(\"Roboto\", 12);\n        pdf.fillStyle = \"#475569\";\n\n        // Left Column\n        pdf.fillText(`Date: ${deliveryData.date}`, margin + 15, textStartY);\n        pdf.fillText(\n          `Driver: ${deliveryData.driverName}`,\n          margin + 15,\n          textStartY + 25,\n        );\n\n        // Right Column\n        const rightColX = pdfPageWidth - margin - 180;\n        pdf.fillText(`Est Time: ${deliveryData.eta}`, rightColX, textStartY);\n        pdf.fillText(\n          `Total Distance: ${deliveryData.distance}`,\n          rightColX,\n          textStartY + 25,\n        );\n\n        // Layer 5: Decorative Border around the text\n        pdf.setLineWidth(2);\n        pdf.setDrawColor(\"#cbd5e1\");\n        pdf.strokeRect(margin, textStartY - 25, maxImageWidth, 65);\n\n        pdf.save(`RouteReport_${deliveryData.id}.pdf`);\n        console.log(\"🟢 Route Report generated\");\n      }\n\n      runDrawGeoJSONRoute({\n        id: \"LDN-8492\",\n        driverName: \"Auto-Pilot\",\n        date: \"Feb 18, 2026\",\n        distance: \"6.4 km\",\n        eta: \"18 mins\",\n      }).catch((e) =\u003e console.error(\"Error:\", e.message, e.stack));\n    \u003c/script\u003e\n  \u003c/body\u003e\n\u003c/html\u003e\n```\n\n\u003c/details\u003e\n\n\n\u003cdetails\u003e\u003csummary\u003eZig runner\u003c/summary\u003e\n\n```zig\nvar debug_allocator: std.heap.DebugAllocator(.{}) = .init;\n\npub fn main() !void {\n    const gpa = debug_allocator.allocator(),\n    defer {\n      _ = .ok == debug_allocator.deinit();\n    };\n\n    const sandbox_root = try std.fs.cwd().realpathAlloc(gpa, \".\");\n    defer gpa.free(sandbox_root);\n\n    var engine = try ScriptEngine.init(allocator, sbx);\n    defer engine.deinit();\n\n    const html = @embedFile(\"test_route_report.html\");\n    try engine.loadPage(html, .{});\n    try engine.run();\n}\n\nconst std = @import(\"std\");\nconst builtin = @import(\"builtin\");\nconst z = @import(\"zexplorer\");\nconst ScriptEngine = z.ScriptEngine;\n```\n\n\u003c/details\u003e\n\nThe result is:\n\n\u003chttps://github.com/ndrean/zexplorer/blob/main/images/RouteReport.pdf\u003e\n\n---\n\n### Render D3.js to PNG\n\n\u003cdetails\u003e\u003csummary\u003eHTML \u0026 JavaScript snippet to render a pie chart\u003c/summary\u003e\n\n```html\n\u003c!doctype html\u003e\n\u003chtml\u003e\n  \u003chead\u003e\n    \u003cscript src=\"https://d3js.org/d3.v7.min.js\"\u003e\u003c/script\u003e\n  \u003c/head\u003e\n  \u003cbody\u003e\n    \u003cdiv id=\"chart\" style=\"width: 800px; height: 600px\"\u003e\u003c/div\u003e\n\n    \u003cscript\u003e\n      async function runDrawD3Chart() {\n        console.log(\"[Test] Booting D3.js Engine...\");\n\n        const width = 800;\n        const height = 600;\n        const margin = 40;\n        const radius = Math.min(width, height) / 2 - margin;\n\n        // Setup the SVG canvas\n        const svg = d3\n          .select(\"#chart\")\n          .append(\"svg\")\n          .attr(\"width\", width)\n          .attr(\"height\", height)\n          // Crucial for ThorVG!\n          .attr(\"xmlns\", \"http://www.w3.org/2000/svg\")\n          .append(\"g\")\n          .attr(\"transform\", `translate(${width / 2},${height / 2})`);\n\n        const data = {\n          \"In Transit\": 45,\n          Delivered: 120,\n          Delayed: 15,\n          Maintenance: 5,\n        };\n\n        // Color scale\n        const color = d3\n          .scaleOrdinal()\n          .domain(Object.keys(data))\n          .range([\"#3b82f6\", \"#22c55e\", \"#ef4444\", \"#f59e0b\"]);\n\n        // the pie slices\n        const pie = d3.pie().value((d) =\u003e d[1]);\n        const data_ready = pie(Object.entries(data));\n\n        // Shape generator for the arcs (Donut chart)\n        const arcGenerator = d3\n          .arc()\n          .innerRadius(radius * 0.5) // This makes it a donut!\n          .outerRadius(radius);\n\n        // Build the SVG DOM elements!\n        svg\n          .selectAll(\"path\")\n          .data(data_ready)\n          .join(\"path\")\n          .attr(\"d\", arcGenerator)\n          .attr(\"fill\", (d) =\u003e color(d.data[0]))\n          .attr(\"stroke\", \"white\")\n          .style(\"stroke-width\", \"4px\");\n\n        // text labels\n        svg\n          .selectAll(\"text\")\n          .data(data_ready)\n          .join(\"text\")\n          .text((d) =\u003e d.data[0])\n          .attr(\"transform\", (d) =\u003e `translate(${arcGenerator.centroid(d)})`)\n          .style(\"text-anchor\", \"middle\")\n          .style(\"font-family\", \"sans-serif\")\n          .style(\"font-size\", \"16px\")\n          .style(\"fill\", \"#ffffff\")\n          .style(\"font-weight\", \"bold\");\n\n        // Extract the SVG string\n        const svgElement = document.querySelector(\"#chart svg\");\n        const svgString = svgElement ? svgElement.outerHTML : \"\";\n\n        // Send to Zig Compositor (just the SVG saved as PNG)\n        zexplorer.generateRoutePng(\n          [], // Empty tiles array\n          svgString,\n          \"D3_Chart_report.png\",\n          width,\n          height,\n        );\n\n        console.log(\"🟢 Chart Report generated\");\n      }\n\n      runDrawD3Chart().catch((e) =\u003e\n        console.error(\"Error:\", e.message, e.stack),\n      );\n    \u003c/script\u003e\n  \u003c/body\u003e\n\u003c/html\u003e\n```\n\n\u003c/details\u003e\n\n\u003cimg src=\"https://github.com/ndrean/zexplorer/blob/main/images/D3_Chart_report.png\" alt=\"d3 char\" with=\"600\" height=\"400\"\u003e\n\n---\n\n### Render Chart.js to PNG\n\nYou can run [Chart.js](https://www.chartjs.org/) server-side and export the result as a PNG file. \n\n❗️ The Chart.js bundle is _pre-built_ with `Bun` and embedded at compile time.\n\n\u003cdetails\u003e\u003csummary\u003eChartJS example\u003c/summary\u003e\n\n```sh\ncd src/examples/zexp-frams \u0026\u0026 bun run build_chartjs.js\nzig build example -Dname=test_canvas -Doptimize=ReleaseFast\n```\n\nThe HTML file defines the chart configuration and uses the `Chart` constructor:\n\n```html\n\u003c!-- src/examples/test_canvas.html --\u003e\n\u003chtml\u003e\n  \u003cbody\u003e\n    \u003cscript\u003e\n      globalThis.window = globalThis;\n      if (typeof Intl === \"undefined\") {\n        globalThis.Intl = {\n          NumberFormat: function(locale, opts) {\n            return { format: function(n) { return String(n); } };\n          }\n        };\n      }\n      window.devicePixelRatio = 1;\n      window.requestAnimationFrame = (cb) =\u003e setTimeout(cb, 0);\n\n      const canvas = document.createElement(\"canvas\");\n      canvas.width = 800;\n      canvas.height = 600;\n      document.body.insertAdjacentElement(\"afterbegin\", canvas);\n\n      async function render() {\n        const config = {\n          type: \"bar\",\n          data: {\n            labels: [\"Zig\", \"Rust\", \"C++\", \"Go\", \"Python\"],\n            datasets: [{\n              label: \"Performance (Imaginary Units)\",\n              data: [150, 145, 140, 120, 80],\n              backgroundColor: [\n                \"rgba(255, 99, 132, 0.8)\", \"rgba(54, 162, 235, 0.8)\",\n                \"rgba(255, 206, 86, 0.8)\", \"rgba(75, 192, 192, 0.8)\",\n                \"rgba(153, 102, 255, 0.8)\",\n              ],\n              borderColor: \"black\",\n              borderWidth: 2,\n            }],\n          },\n          options: {\n            animation: false,  // CRITICAL: disable animation for SSR\n            responsive: false,\n            plugins: {\n              title: { display: true, text: \"Language Speed Test\", font: { size: 30 } },\n            },\n          },\n        };\n\n        new globalThis.Chart(canvas, config);\n\n        // Since animation is false, it renders synchronously!\n        const blob = await canvas.toBlob();\n        return await blob.arrayBuffer();\n      }\n      render();\n    \u003c/script\u003e\n  \u003c/body\u003e\n\u003c/html\u003e\n```\n\nThe Zig runner loads the `Chart.js` bundle, then evaluates the HTML with its inline script:\n\n```zig\nfn chartJS(allocator: std.mem.Allocator, sbx: []const u8) !void {\n    var engine = try ScriptEngine.init(allocator, sbx);\n    defer engine.deinit();\n\n    // Load Chart.js bundle (pre-built IIFE, embedded at compile time)\n\n    const chartjs = @embedFile(\"vendor/chart.js\");\n    const chartjs_val = try engine.eval(chartjs, \"\u003cchartjs\u003e\", .global);\n    engine.ctx.freeValue(chartjs_val);\n\n    // Load the HTML with chart config, extract and run the \u003cscript\u003e\n\n    const html = @embedFile(\"test_canvas.html\");\n    try engine.loadHTML(html);\n\n    // access the DOM with Zig\n\n    const body = z.bodyNode(engine.dom.doc);\n    const script_elt = z.getElementByTag(body.?, .script).?;\n    const script = z.textContent_zc(z.elementToNode(script_elt));\n\n    const png_bytes = try engine.evalAsyncAs(allocator, []const u8, script, \"\u003cchart\u003e\");\n    defer allocator.free(png_bytes);\n\n    try std.fs.cwd().writeFile(.{ .sub_path = \"canvas_chartjs.png\", .data = png_bytes });\n}\n```\n\n\u003c/details\u003e\n\n\u003cimg src=\"https://github.com/ndrean/zexplorer/blob/main/canvas_graphJS_test.png\" alt=\"chartjs example\" width=\"600\" height=\"400\"\u003e\n\n---\n\n### Run React bundled code\n\nYou can run bundled JSX code in zexplorer. It is a two-step process.\n\n\u003cdetails\u003e\u003csummary\u003eReact code + test\u003c/summary\u003e\n\n```js\n\nimport React, { useState, useMemo, useEffect } from 'react';\nimport { createRoot } from 'react-dom/client';\n\nconst Item = ({ value }) =\u003e {\n  return \u003cli className=\"item\"\u003eValue: \u003cstrong\u003e{value}\u003c/strong\u003e\u003c/li\u003e;\n};\n\nconst List = ({ onlyEven }) =\u003e {\n  const allNumbers = [1, 2, 3, 4, 5, 6, 7];\n\n  // useMemo ensures we only filter when 'onlyEven' changes\n  const displayedNumbers = useMemo(() =\u003e {\n    console.log(`[React] Calculating filter (Even: ${onlyEven})`);\n    if (onlyEven) {\n      return allNumbers.filter(n =\u003e n % 2 === 0);\n    }\n    return allNumbers;\n  }, [onlyEven]);\n\n  return (\n    \u003cul id=\"list-container\"\u003e\n      {displayedNumbers.map(n =\u003e \u003cItem key={n} value={n} /\u003e)}\n    \u003c/ul\u003e\n  );\n};\n\nconst App = () =\u003e {\n  const [onlyEven, setOnlyEven] = useState(false);\n  const [renderCount, setRenderCount] = useState(1);\n\n  useEffect(() =\u003e {\n    console.log(\"[React] 👍 App Mounted\");\n  }, []);\n\n\n  return (\n    \u003cdiv style={{ padding: 20, fontFamily: 'sans-serif' }}\u003e\n      \u003ch1\u003eZexplorer Memo Test\u003c/h1\u003e\n\n      {/* Control Panel */}\n      \u003cdiv style={{ marginBottom: 15 }}\u003e\n        \u003cbutton\n          id=\"btn-toggle\"\n          onClick={() =\u003e setOnlyEven(prev =\u003e !prev)}\n        \u003e\n          {onlyEven ? \"Show All\" : \"Show Even Only\"}\n        \u003c/button\u003e\n\n        \u003cbutton\n          id=\"btn-force\"\n          onClick={() =\u003e setRenderCount(c =\u003e c + 1)}\n          style={{ marginLeft: 10 }}\n        \u003e\n          Force Re-render ({renderCount})\n        \u003c/button\u003e\n      \u003c/div\u003e\n\n      \u003cp\u003eStatus: {onlyEven ? \"Filtering Active\" : \"Showing All\"}\u003c/p\u003e\n\n      {/* Nested List */}\n      \u003cList onlyEven={onlyEven} /\u003e\n    \u003c/div\u003e\n  );\n};\n\nconst rootNode = document.getElementById('root');\nif (rootNode) {\n  const root = createRoot(rootNode);\n  root.render(\u003cApp /\u003e);\n}\n```\n\n```sh\nbun run build_react.js   # produces dist/app.js\nzig build example -Dname=test_react -Doptimize=ReleaseFast\n```\n\nThe test simulates clicks via `dispatchEvent` and verifies that `useMemo` works correctly: filtering triggers a recalculation, but a force re-render does not.\n\n\u003c/details\u003e\n\n### Preact with `html` template strings\n\n\u003cdetails\u003e\u003csummary\u003ePreact code + test\u003c/summary\u003e\n\n```html\n\u003chtml\u003e\n  \u003cbody\u003e\n    \u003ch1\u003ePreact Demo - Nested Components Test\u003c/h1\u003e\n    \u003cdiv id=\"root\"\u003e\u003c/div\u003e\n    \u003cscript type=\"module\"\u003e\n      import { h, render } from \"preact\";\n      import { useState } from \"preact/hooks\";\n\n      console.log(\"[JS] Preact + Hooks loaded\");\n\n      const root = document.getElementById(\"root\");\n\n      let globalSetCount = null;\n\n      // Nested component: Button with onclick prop\n      const Button = ({ id, children, onClick }) =\u003e {\n        console.log(\"[JS] Button component render\");\n        return h(\"button\", { id, onclick: onClick }, children);\n      };\n\n      // Nested component: Counter display\n      const CountDisplay = ({ count }) =\u003e {\n        console.log(\"[JS] CountDisplay render, count =\", count);\n        return h(\"p\", { id: \"count-display\" }, `Count: ${count}`);\n      };\n\n      // Parent component with nested children + useState\n      const App = () =\u003e {\n        const [count, setCount] = useState(0);\n        globalSetCount = setCount;\n        console.log(\"[JS] App render, count =\", count);\n\n        const handleClick = () =\u003e {\n          console.log(\"[JS] onclick prop triggered!\");\n          setCount((c) =\u003e c + 1);\n        };\n\n        return h(\n          \"div\",\n          { class: \"app\" },\n          h(\"h1\", null, \"Preact Counter\"),\n          h(CountDisplay, { count }),\n          h(Button, { id: \"increment-btn\", onClick: handleClick }, \"+1\"),\n        );\n      };\n\n      try {\n        render(h(App), root);\n        console.log(\"[JS] Rendered successfully!\");\n        console.log(\"[JS] innerHTML:\", root.innerHTML);\n\n        // Test onclick via dispatchEvent\n        const btn = document.getElementById(\"increment-btn\");\n        if (btn) {\n          console.log(\"[JS] Testing onclick prop...\");\n          for (let i = 0; i \u003c 3; i++) {\n            setTimeout(\n              () =\u003e btn.dispatchEvent(new Event(\"click\")),\n              (i + 1) * 100,\n            );\n          }\n          setTimeout(() =\u003e {\n            console.log(\n              \"[JS] Final count:\",\n              document.getElementById(\"count-display\")?.textContent,\n            );\n          }, 500);\n        }\n      } catch (e) {\n        console.log(\"[JS] ERROR:\", e.message);\n        console.log(\n          \"[JS] TIP: Run with --release=fast to avoid stack overflow\",\n        );\n      }\n    \u003c/script\u003e\n  \u003c/body\u003e\n\u003c/html\u003e\n\n```\n\n\u003c/details\u003e\n\n```sh\n\nzig build example -Dname=test_htm -Doptimize=ReleaseFast\n```\n\n\n### SolidJS templated with `html`\n\n```sh\nzig build example -Dname=test_solidjs --release=fast\n```\n\n\u003cdetails\u003e\u003csummary\u003eSolidJS with createSignal, createEffect, and setInterval\u003c/summary\u003e\n\n```html\n\u003chtml\u003e\n  \u003cbody\u003e\n    \u003ch1\u003eTesting CDN import: SolidJS (Nested Components)\u003c/h1\u003e\n    \u003cdiv id=\"root\"\u003e\u003c/div\u003e\n    \u003cscript type=\"module\"\u003e\n      import { createSignal, createEffect, onMount } from \"solid-js\";\n      import { render } from \"solid-js/web\";\n      import html from \"solid-js/html\";\n\n      console.log(\"[JS] SolidJS loaded\");\n      const root = document.getElementById(\"root\");\n\n      // --- Nested Components ---\n      // Note: solid-js/html's dynamicProperty wraps ALL function-valued\n      // component props as reactive getters (calls them on access).\n      // Unlike JSX (where Babel distinguishes signal accessors from\n      // regular functions), html templates treat all functions as\n      // reactive accessors. So we pass event handlers wrapped in an\n      // object to avoid auto-calling.\n\n      const Button = (props) =\u003e {\n        const handleClick = () =\u003e props.actions.increment();\n        return html`\n          \u003cbutton type=\"button\" id=\"btn\" onclick=${handleClick}\u003e\n            ${props.children}\n          \u003c/button\u003e\n        `;\n      };\n\n      function Counter(props) {\n        createEffect(() =\u003e {\n          console.log(\"[JS] Effect: count =\", props.count);\n        });\n\n        return html`\n          \u003cdiv class=\"counter-app\"\u003e\n            \u003ch2\u003eSolidJS Counter (Nested)\u003c/h2\u003e\n            \u003cp id=\"count-display\"\u003eCount: ${() =\u003e props.count}\u003c/p\u003e\n            \u003c${Button} actions=${props.actions} children=${\"👍 +1\"} /\u003e\n          \u003c/div\u003e\n        `;\n      }\n\n      const App = () =\u003e {\n        const [localCount, setLocalCount] = createSignal(0);\n        // Wrap handlers in an object so dynamicProperty doesn't\n        // auto-call them (objects are not functions)\n        const actions = { increment: () =\u003e setLocalCount((c) =\u003e c + 1) };\n\n        onMount(() =\u003e {\n          console.log(\"[JS] App mounted\");\n          console.log(\"[JS] initial innerHTML:\", root.innerHTML);\n        });\n\n        return html`\u003c${Counter} count=${localCount} actions=${actions} /\u003e`;\n      };\n\n      try {\n        render(() =\u003e html`\u003c${App} /\u003e`, root);\n        console.log(\"[JS] First render:\", root.innerHTML);\n\n        // Simulate periodic clicks\n        let iterations = 0;\n        const interval = setInterval(() =\u003e {\n          iterations++;\n          const btn = document.getElementById(\"btn\");\n          if (btn) {\n            console.log(\"[JS] dispatching click\", iterations);\n            btn.dispatchEvent(new Event(\"click\", { bubbles: true }));\n          }\n          if (iterations \u003e= 3) {\n            clearInterval(interval);\n            console.log(\"[JS] Auto-increment stopped after 3 iterations\");\n            console.log(\n              \"[JS] Final:\",\n              document.getElementById(\"count-display\")?.textContent,\n            );\n          }\n        }, 100);\n      } catch (e) {\n        console.log(\"[JS] SolidJS Error:\", e.message);\n        if (e.stack)\n          console.log(\n            \"[JS] Stack:\",\n            e.stack.split(\"\\n\").slice(0, 5).join(\"\\n\"),\n          );\n      }\n    \u003c/script\u003e\n  \u003c/body\u003e\n\u003c/html\u003e\n```\n\n\u003c/details\u003e\n\n### Vue with template strings\n\n```sh\nzig build example -Dname=test_vue --release=fast\n```\n\n\u003cdetails\u003e\u003csummary\u003eVue 3 with ref, template compiler, and dispatchEvent\u003c/summary\u003e\n\n```html\n\u003chtml\u003e\n  \u003cbody\u003e\n    \u003ch1\u003eTesting CDN import: Vue 3 (Template Compiler)\u003c/h1\u003e\n    \u003cdiv id=\"root\"\u003e\u003c/div\u003e\n    \u003cscript type=\"module\"\u003e\n      // Vue checks for browser globals during mount\n      if (typeof SVGElement === \"undefined\") {\n        globalThis.SVGElement = class SVGElement {};\n      }\n      if (typeof Element === \"undefined\") {\n        globalThis.Element = class Element {};\n      }\n\n      // import Vue from \"vue\";\n\n      // const { createApp, ref, compile } = Vue;\n      import { createApp, ref, compile } from \"vue\";\n\n      console.log(\"[JS] Vue 3 loaded (template compiler path)\");\n      console.log(\"[JS] createApp:\", typeof createApp);\n      console.log(\"[JS] ref:\", typeof ref);\n\n      // const root = document.getElementById(\"root\");\n\n      const Counter = {\n        setup() {\n          const count = ref(0);\n          const increment = () =\u003e {\n            console.log(\"[JS] Button clicked!\");\n            count.value++;\n          };\n          return { count, increment };\n        },\n        template: `\n          \u003cdiv class=\"counter-app\"\u003e\n            \u003ch2\u003eVue Counter (Template)\u003c/h2\u003e\n            \u003cp id=\"count-display\"\u003eCount: {{ count }}\u003c/p\u003e\n            \u003cbutton id=\"increment-btn\" @click=\"increment\"\u003e+1\u003c/button\u003e\n          \u003c/div\u003e\n        `,\n      };\n\n      try {\n        // First, manually compile the template to see what the compiler outputs\n        // if (typeof compile === \"function\") {\n        //   const result = compile(Counter.template);\n        //   console.log(\"[JS] Compiled render code:\", result.toString());\n        // } else {\n        //   console.log(\"[JS] compile function not available\");\n        // }\n\n        const app = createApp(Counter);\n        const root = document.getElementById(\"root\");\n\n        app.mount(\"#root\");\n        console.log(\"[JS] First render:\", root.innerHTML);\n        // __flush();\n\n        // simulate periodic clicks\n        let iterations = 0;\n        const interval = setInterval(() =\u003e {\n          iterations++;\n          const btn = document.getElementById(\"increment-btn\");\n          if (btn) {\n            btn.dispatchEvent(new Event(\"click\", { bubbles: true }));\n            // __flush();\n          }\n          if (iterations \u003e= 3) {\n            clearInterval(interval);\n            console.log(\"[JS] Auto-increment stopped after 3 iterations\");\n            console.log(\n              \"[JS] Final:\",\n              document.getElementById(\"count-display\")?.textContent,\n            );\n          }\n        }, 100);\n      } catch (e) {\n        console.log(\"[JS] Vue Error:\", e.message);\n        if (e.stack)\n          console.log(\n            \"[JS] Stack:\",\n            e.stack.split(\"\\n\").slice(0, 5).join(\"\\n\"),\n          );\n      }\n    \u003c/script\u003e\n  \u003c/body\u003e\n\u003c/html\u003e\n```\n\n\u003c/details\u003e\n\n---\n\n## Tests \u0026 performance\n\n| Operation            | zexplorer | JSDOM+DOMPurify |\n| -------------------- | --------- | --------------- |\n| Cold start           | 2.5ms     | 30ms            |\n| Sanitize 36kB HTML   | 2.1ms     | 11ms            |\n| Create 10k DOM nodes | 26ms      | 191ms           |\n\n### Running framework code\n\nTo ensure the Web primitives are correctly implemented in `zexplorer`, we tested various frameworks and used the [js-vanilla-bench-framework tests](https://github.com/krausest/js-framework-benchmark): React 19, Preact, SolidJS, Vue 3, Svelte 5, Lit-html, HTMX, Bau.\n\n| Feature           | zexplorer                 | JSDOM           | Puppeteer       |\n| ----------------- | ------------------------- | --------------- | --------------- |\n| Startup time      | 2ms                       | ~30ms           | ~500ms          |\n| DOM sanitization  | Built-in, DOM \u0026 CSS-aware | Needs DOMPurify | Browser context |\n| Memory footprint  | 10MB                      | ~50MB           | ~200MB          |\n| Web API coverage  | (essential)               | ~90%            | 100%            |\n| JavaScript engine | QuickJS (bytecode)        | Node.js V8      | Chrome V8       |\n\nSource:\n\n- [Vanilla-1-keyed](https://github.com/krausest/js-framework-benchmark/blob/master/frameworks/non-keyed/vanillajs-1/src/Main.js)\n- [Vanilla-2-non-keyd](https://github.com/krausest/js-framework-benchmark/blob/master/frameworks/non-keyed/vanillajs-3/src/Main.js)\n- [Vanilla-3-k](https://github.com/krausest/js-framework-benchmark/blob/master/frameworks/keyed/vanillajs-3/src/Main.js)\n- [bau](https://github.com/krausest/js-framework-benchmark/blob/master/frameworks/non-keyed/bau/main.js)\n\nRef: browser Vanilla\n\n| Test             | Ref  | [V1-keyd] | [V2-nonKeyd] | [V3-keyd] |     | [bau] |\n| ---------------- | ---- | --------- | ------------ | --------- | --- | ----- |\n| Create 1k        | 22.0 | 3.08      | 1.65         | 1.67      |     | 1.97  |\n| Replace 1k       | 24.4 | 3.03      | 2.46         | 2.73      |     | 1.94  |\n| Partial Up (10k) | 9.5  | 2.82      | 8.86         | 7.92      |     | 5.40  |\n| Select Row       | 2.2  | 0.02      | 0.02         | 0.01      |     | 0.01  |\n| Swap Rows        | 11.7 | 0.05      | 0.09         | 0.11      |     | 0.01  |\n| Remove Row       | 9.2  | 0.01      | 0.05         | 0.05      |     | 0.00  |\n| Create 10k       | 229  | 28.71     | 16.06        | 16.15     |     | 19.03 |\n| Append 1k        | 25.6 | 3.48      | 4.28         | 4.10      |     | 2.06  |\n| Clear            | 9.0  | 4.21      | 6.08         | 5.66      |     | 0.01  |\n| --               | --   | --        | --           | --        | --  | --    |\n| Total Engine     | --   | 78        | 84           | 82        |     | 57    |\n\n(c) CDN import\n\n- [Comp(*) Solid](https://github.com/krausest/js-framework-benchmark/blob/master/frameworks/keyed/solid/src/main.jsx)\n- [Temp(**) Solid](https://github.com/ndrean/zexplorer/src/examples/js-bench-solid.js)\n- [Svelte5](https://github.com/krausest/js-framework-benchmark/blob/master/frameworks/keyed/svelte/src/App.svelte) (compiled with `svelte/compiler`, bundled with Bun)\n- [Vue3](https://github.com/ndrean/zexplorer/src/examples/js-bench-vue3.js)\n- \n\n\n\n(*) compiled JSX-\u003eJS with `bun`\n\n(**) templated with `html` and using `map` instead of `For`\n\n(***) CDN + templated\n\n**[React familly - vDOM]**\n\n- [React19](https://github.com/krausest/js-framework-benchmark/blob/master/frameworks/keyed/react-hooks/src/main.jsx)\n- [Preact](https://github.com/krausest/js-framework-benchmark/blob/master/frameworks/keyed/react-hooks/src/main.jsx) (same source, built with preact/compat)\n\n| Test             | React19^ | Preact^^ |\n| ---------------- | -------- | -------- |\n| Create 1k        | 105.0    | 157.8    |\n| Replace 1k       | 106.2    | 159.1    |\n| Partial Up (10k) | 128.6    | 166.0    |\n| Select Row       | 47.2     | 5.9      |\n| Swap Rows        | 37.1     | 4.9      |\n| Remove Row       | 5.1      | 4.9      |\n| Create 10k       | 3601.9   | 5670.4   |\n| Append 1k        | 154.9    | 121.4    |\n| Clear            | 92.7     | 10.3     |\n| --               | --       | --       |\n| Total Engine     | 8715     | 8255     |\n\n(^) React production build (`process.env.NODE_ENV = \"production\"`)\n\n(^^) Preact/compat production build (same JSX source as React, aliased via build plugin)\n\nSvelte 5's compiled approach generates direct imperative DOM operations (no VDOM), making it the fastest compiled framework on QuickJS — on par with Compiled Solid for 1k operations and significantly faster than React/Preact/Vue. Preact is lighter than React (3KB vs 45KB) but its microtask-scheduled rendering creates more GC pressure at scale. React's fiber architecture pays off for partial updates where its diffing skips unchanged subtrees more efficiently.\n\n#### SolidJS\n\n[SolidJS](https://www.solidjs.com/)\n\n##### SolidJS js-framework-benchmark\n\nSource from js-framework-benchmark: \u003chttps://github.com/ndrean/zexplorer/blob/main/src/examples/zxp-frams/BenchSolid.js\u003e\n\nWe used two forms:\n\n- we transformed the previous inot a \"templated version\" using `solid-js/html`. The source is: \u003chttps://github.com/ndrean/zexplorer/blob/main/src/examples/zxp-frams/BenchSolid.js\u003e\n- We used a compiled form. We transform the JSX-\u003eJS with `babel` and bundled the code with `bun`. The resulting compiled code is:\n\u003chttps://github.com/ndrean/zexplorer/src/examples/vendor/bench-solid.js\u003e.\n\nThe following script that we run using `bun build_bench_solid.js`: \u003chttps://github.com/ndrean/zexplorer/blob/main/src/examples/zxp-frams/build_bench_solid.js\u003e\n\nWe provide a Zig file to compile and run directly\n\n```sh\nzig build example -Dname=frameworks/solidjs/js-bench-solid --release=fast\n```\n\nor use the `serve` mode\n\n```sh\n./zig-out/bin/zxp serve\n```\n\n```sh\ncurl -s -X POST http://localhost:9984/run --data-binary @src/examples/frameworks/solidjs/run-templated.js\n\ncurl -s -X POST http://localhost:9984/run --data-binary @src/examples/frameworks/solidjs/run-compiled.js\n```\n\n| Test             | Ref  | Solid (compiled) | Solid (templated) | \n| ---------------- | ---- | --------- | ---------- |\n| Create 1k        | 22.0 | 20.5      | 18       |\n| Replace 1k       | 24.4 | 19      | 17       |\n| Partial Up (10k) | 9.5  | 7       | 102      |\n| Select Row       | 2.2  | 3.3       | 108      |\n| Swap Rows        | 11.7 | 1.0       | 8       |\n| Remove Row       | 9.2  | 0.5       | 8       |\n| Create 10k       | 229  | 123     | 95      |\n| Append 1k        | 25.6 | 13.5      | 108      |\n| Clear            | 9.0  | 31      | 23       |\n\n\u003e In the templated with `html`, we use `map` instead of the optimzed `For`.\n\n##### SolidJS example\n\n\u003cdetails\u003e\u003csummary\u003eHTML with embedded JavaScript code running SolidJS simulating three clicks on a button\u003c/summary\u003e\n\n[Source](https://github.com/ndrean/zexplorer/blob/main/src/examples/frameworks/solidjs/test_solidjs.html)\n\n```html\n\u003chtml\u003e\n  \u003cbody\u003e\n    \u003ch1\u003eTesting CDN import: SolidJS (Nested Components)\u003c/h1\u003e\n    \u003cdiv id=\"root\"\u003e\u003c/div\u003e\n    \u003cscript type=\"module\"\u003e\n      import { createSignal, createEffect, onMount } from \"solid-js\";\n      import { render } from \"solid-js/web\";\n      import html from \"solid-js/html\";\n\n      console.log(\"[JS] SolidJS loaded\");\n      const root = document.getElementById(\"root\");\n\n      // --- Nested Components ---\n      // Note: solid-js/html's dynamicProperty wraps ALL function-valued\n      // component props as reactive getters (calls them on access).\n      // Unlike JSX (where Babel distinguishes signal accessors from\n      // regular functions), html templates treat all functions as\n      // reactive accessors. So we pass event handlers wrapped in an\n      // object to avoid auto-calling.\n\n      const Button = (props) =\u003e {\n        const handleClick = () =\u003e props.actions.increment();\n        return html`\n          \u003cbutton type=\"button\" id=\"btn\" onclick=${handleClick}\u003e\n            ${props.children}\n          \u003c/button\u003e\n        `;\n      };\n\n      function Counter(props) {\n        createEffect(() =\u003e {\n          console.log(\"[JS] Effect: count =\", props.count);\n        });\n\n        return html`\n          \u003cdiv class=\"counter-app\"\u003e\n            \u003ch2\u003eSolidJS Counter (Nested)\u003c/h2\u003e\n            \u003cp id=\"count-display\"\u003eCount: ${() =\u003e props.count}\u003c/p\u003e\n            \u003c${Button} actions=${props.actions} children=${\"👍 +1\"} /\u003e\n          \u003c/div\u003e\n        `;\n      }\n\n      const App = () =\u003e {\n        const [localCount, setLocalCount] = createSignal(0);\n        // Wrap handlers in an object so dynamicProperty doesn't\n        // auto-call them (objects are not functions)\n        const actions = { increment: () =\u003e setLocalCount((c) =\u003e c + 1) };\n\n        onMount(() =\u003e {\n          console.log(\"[JS] App mounted\");\n          console.log(\"[JS] initial innerHTML:\", root.innerHTML);\n        });\n\n        return html`\u003c${Counter} count=${localCount} actions=${actions} /\u003e`;\n      };\n\n      try {\n        render(() =\u003e html`\u003c${App} /\u003e`, root);\n        console.log(\"[JS] First render:\", root.innerHTML);\n\n        // Simulate periodic clicks\n        let iterations = 0;\n        const interval = setInterval(() =\u003e {\n          iterations++;\n          const btn = document.getElementById(\"btn\");\n          if (btn) {\n            console.log(\"[JS] dispatching click\", iterations);\n            btn.dispatchEvent(new Event(\"click\", { bubbles: true }));\n          }\n          if (iterations \u003e= 3) {\n            clearInterval(interval);\n            console.log(\"[JS] Auto-increment stopped after 3 iterations\");\n            console.log(\n              \"[JS] Final:\",\n              document.getElementById(\"count-display\")?.textContent,\n            );\n          }\n        }, 100);\n      } catch (e) {\n        console.log(\"[JS] SolidJS Error:\", e.message);\n        if (e.stack)\n          console.log(\n            \"[JS] Stack:\",\n            e.stack.split(\"\\n\").slice(0, 5).join(\"\\n\"),\n          );\n      }\n    \u003c/script\u003e\n  \u003c/body\u003e\n\u003c/html\u003e\n```\n\n\u003c/details\u003e\n\nYou can run it either:\n\n```sh\nzig build example -Dname=frameworks/solidjs/test_solidjs\n```\n\nor using the dev-server:\n\n```sh\n./zig-out/bin/zxp serve\n```\n\n```sh\ncurl -s -X POST http://localhost:9984/run --data-binary @src/examples/frameworks/solidjs/test_solid.js\n```\n\nThe output shows the counter being incremented as expected:\n\n```txt\n[JS] SolidJS loaded\n[JS] App mounted\n[JS] initial innerHTML: \u003cdiv class=\"counter-app\"\u003e\u003ch2\u003eSolidJS Counter (Nested)\u003c/h2\u003e\u003cp id=\"count-display\"\u003eCount: 0\u003c!--#--\u003e\u003c/p\u003e\u003cbutton type=\"button\" id=\"btn\"\u003e👍 +1\u003c/button\u003e\u003c!--#--\u003e\u003c/div\u003e\n[JS] Effect: count = 0\n[JS] First render: \u003cdiv class=\"counter-app\"\u003e\u003ch2\u003eSolidJS Counter (Nested)\u003c/h2\u003e\u003cp id=\"count-display\"\u003eCount: 0\u003c!--#--\u003e\u003c/p\u003e\u003cbutton type=\"button\" id=\"btn\"\u003e👍 +1\u003c/button\u003e\u003c!--#--\u003e\u003c/div\u003e\n[JS] dispatching click 1\n[JS] Effect: count = 1\n[JS] dispatching click 2\n[JS] Effect: count = 2\n[JS] dispatching click 3\n[JS] Effect: count = 3\n[JS] Auto-increment stopped after 3 iterations\n[JS] Final: Count: 3\n```\n\n---\n\n#### React\n\n#### Lit\n\n[Lit](https://lit.dev/docs/v1/lit-html/introduction/)\n\n- [Lit-html js-framework-benchmark source](https://github.com/krausest/js-framework-benchmark/tree/master/frameworks/non-keyed/lit-html)\n- [Source in the repo](https://github.com/ndrean/zexplorer/blob/main/src/examples/frameworks/lit-html/js-bench-lit-html.js)\n\nTo run it, you can either run a Zig build:\n\n```sh\nzig build example -Dname=frameworks/lit-html/js-bench-lit-html --release=fast\n```\n\nor use the `serve` mode and send an HTTP request where the payload is a JS snippet to load the HTML and run the embedded scripts:\n\n```sh\n./zig-out/bin/zxp serve\n```\n\n```sh\ncurl -s -X POST http://localhost:9984/run --data-binary @src/examples/frameworks/lit-html/run.js\n```\n\n| Test                 | Lit-html |\n| -------------------- | -------- |\n| Create 1k            | 20 ms    |\n| Replace 1k           | 5 ms     |\n| Partial Update (10k) | 25 ms    |\n| Select Row           | 25 ms    |\n| Swap Rows            | 2.5 ms   |\n| Remove Row           | 3.3 ms   |\n| Create 10k           | 122 ms   |\n| Append 1k            | 38 ms    |\n| Clear                | 11 ms    |\n\n---\n\n#### Svelte 5\n\n[Svelte](https://svelte.dev/)\n\n- [Svelte 5 js-framework-benchmark source](https://github.com/krausest/js-framework-benchmark/blob/master/frameworks/keyed/svelte/src/App.svelte)\n- [Source in the repo](https://github.com/ndrean/zexplorer/blob/main/src/examples/zxp-frams/BenchSvelte.svelte)\n\nThe Svelte code is compiled using `bun`: [bun runner](httsp://github.com/ndrean/zexplorer/blob/main/src/examples/zxp-frams/build_bench_svelte.js)\n\nThe compiled [code source](https://github.com/ndrean/zexplorer/src/examples/vendor/bench-svelte.js)\n\nWe provide a Zig file to compile and run directly\n\n```sh\nzig build example -Dname=frameworks/svelte/js-benchsvelte --release=fast\n```\n\nor use the `serve` mode:\n\n```sh\n./zig-out/bin/zxp serve   \n```\n\n```sh\ncurl -s -X POST http://localhost:9984/run --data-binary @src/examples/frameworks/svelte/run.js\n```\n\n| Test             |Svelte5 (compiled)  |\n| ---------------- |--------            |\n| Create 1k        | 26                 |\n| Replace 1k       | 26.9               |\n| Partial Up (10k) | 8.7                |\n| Select Row       | 3.0                |\n| Swap Rows        | 1.1                |\n| Remove Row       | 0.7                |\n| Create 10k       | 422.1              |\n| Append 1k        | 22.8               |\n| Clear            | 9.1                |\n\n#### Vue 3\n\n[Vue](https://vuejs.org/)\n\n- [Vue js-framework-benchmark source](https://github.com/ndrean/zexplorer/src/examples/js-bench-vue3.js)\n- [Source in the repo](https://github.com/ndrean/zexplorer/blob/main/src/examples/frameworks/vue/js-bench-vue.js)\n\nWe provide a Zig file to compile and run directly\n\n```sh\nzig build example -Dname=frameworks/vue/js-bench-vue --release=fastor use the `serve` mode:\n\n```sh\n./zig-out/bin/zxp serve   \n```\n\n```sh\ncurl -s -X POST http://localhost:9984/run --data-binary @src/examples/frameworks/vue/run.js\n```\n\n| Test             | Vue 3 (templated) |\n| ---------------- |--------           |\n| Create 1k        | 50                |\n| Replace 1k       | 49                |\n| Partial Up (10k) | 191               |\n| Select Row       | 177               |\n| Swap Rows        | 19                |\n| Remove Row       | 17                |\n| Create 10k       | 422               |\n| Append 1k        | 220               |\n| Clear            | 32                |\n\n### zexplorer vs jsdom\n\nWhile JSDOM emulates more of the many web standards, zexplorer can run Vanilla code, Preact/React code (no JSX, via templating or compiled), Vue (via templating), SolidJS (via templating or compiled), or Svelte 5 (compiled). Check the examples below.\n\nWe present a comparison in performance between `JSDOM` and `zexplorer` on a Vanilla example where build a simple DOM and run querySelectors and populate elements.\n\n\u003cdetails\u003e\u003csummary\u003eJSDOM script\u003c/summary\u003e\n\n```js\nconst { JSDOM } = require(\"jsdom\");\nconst { performance } = require(\"perf_hooks\");\n\nconst values = [100, 1000, 10_000, 20_000, 50_000];\n\nconsole.log(\"\\n=== JSDOM Benchmark --------------------------------\\n\");\n\nfor (const nb of values) {\n  const globalStart = performance.now();\n\n  // We enable runScripts so we can execute the test logic inside the context\n  const dom = new JSDOM(`\u003c!DOCTYPE html\u003e\u003cbody\u003e\u003c/body\u003e`, {\n    runScripts: \"dangerously\",\n    resources: \"usable\",\n  });\n\n  const { window } = dom;\n\n  window.NB = nb;\n\n  // Benchmark Script\n\n  const scriptContent = `\n    let start = performance.now();\n    const NB = window.NB; // Access injected global\n    console.log(\\`[Internal] Starting DOM creation test with \\${NB} elements\\`);\n    \n    const btn = document.createElement(\"button\");\n    const form = document.createElement(\"form\");\n    form.appendChild(btn);\n    document.body.appendChild(form);\n\n    const mylist = document.createElement(\"ul\");\n\n    for (let i = 1; i \u003c= NB; i++) {\n      const item = document.createElement(\"li\");\n      item.textContent = \"Item \" + i * 10;\n      item.setAttribute(\"id\", i.toString());\n      mylist.appendChild(item);\n    }\n    document.body.appendChild(mylist);\n\n    const lis = document.querySelectorAll(\"li\");\n    \n    let clickCount = 0;\n        \n    btn.addEventListener(\"click\", () =\u003e {\n      clickCount++;\n      btn.textContent = \\`Clicked \\${clickCount}\\`;\n    });\n\n    const clickEvent = new window.Event(\"click\");\n    for (let i = 0; i \u003c NB; i++) {\n      btn.dispatchEvent(clickEvent);\n    }\n\n    let time = performance.now() - start;\n\n    console.log(\n      JSON.stringify({\n        time: time.toFixed(2),\n        elementCount: lis.length,\n        last_li_id: lis[lis.length - 1].getAttribute(\"id\"),\n        last_li_text: lis[lis.length - 1].textContent,\n        success: clickCount === NB,\n      })\n    );\n  `;\n\n  console.log(`[Node] Running with NB=${nb}`);\n  window.eval(scriptContent);\n\n  const globalEnd = performance.now();\n  const totalMs = (globalEnd - globalStart).toFixed(2);\n\n  console.log(`⚡️ JSDOM Total Time: ${totalMs}ms\\n`);\n\n  window.close();\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003eZexplorer script:\u003c/summary\u003e\n\n```zig\nconst std = @import(\"std\");\nconst z = @import(\"zexplorer\");\nconst ScriptEngine = z.ScriptEngine;\n\npub fn main() !void {\n    var debug_allocator: std.heap.DebugAllocator(.{}) = .init;\n    const gpa, const is_debug = gpa: {\n        break :gpa switch (builtin.mode) {\n            .Debug, .ReleaseSafe =\u003e .{ debug_allocator.allocator(), true },\n            .ReleaseFast, .ReleaseSmall =\u003e .{ std.heap.c_allocator, false },\n        };\n    };\n    defer if (is_debug) {\n        _ = debug_allocator.deinit();\n    };\n\n    const sandbox_root = try std.fs.cwd().realpathAlloc(gpa, \".\");\n    defer gpa.free(sandbox_root);\n\n    try bench(gpa, sandbox_root);\n}\n\nfn bench(allocator: std.mem.Allocator, sbx: []const u8) !void {\n    z.print(\"\\n=== JS-simple-bench --------------------------------\\n\\n\", .{});\n\n    const values = [_]u32{ 100, 1000, 10000, 20000, 50000 };\n\n    for (values) |v| {\n        z.print(\"[Zig]-\u003e Running with NB={d}\\n\", .{v});\n        const start = std.time.nanoTimestamp();\n        var engine = try ScriptEngine.init(allocator, sbx);\n\n        const js =\n            \\\\ let start = performance.now();\n            \\\\ console.log(`Starting DOM creation test with {d} elements`);\n            \\\\ const btn = document.createElement(\"button\");\n            \\\\ const form = document.createElement(\"form\");\n            \\\\ form.appendChild(btn);\n            \\\\ document.body.appendChild(form);\n            \\\\ const mylist = document.createElement(\"ul\");\n            \\\\ for (let i = 1; i \u003c= parseInt({d}); i++) {{\n            \\\\   const item = document.createElement(\"li\");\n            \\\\   item.textContent = \"Item \" + i * 10;\n            \\\\   item.setAttribute(\"id\", i.toString());\n            \\\\   mylist.appendChild(item);\n            \\\\ }}\n            \\\\ document.body.appendChild(mylist);\n            \\\\\n            // \\\\ let time = performance.now() - start;\n            \\\\\n            \\\\ const lis = document.querySelectorAll(\"li\");\n            \\\\ console.log(lis.length);\n            \\\\\n            // \\\\ start = performance.now();\n            \\\\ let clickCount = 0;\n            \\\\ btn.addEventListener(\"click\", () =\u003e {{\n            \\\\  clickCount++;\n            \\\\  btn.textContent = `Clicked ${{clickCount}}`;\n            \\\\ }});\n            \\\\\n            \\\\ // Simulate clicks\n            \\\\ for (let i = 0; i \u003c parseInt({d}); i++) {{\n            \\\\   btn.dispatchEvent(\"click\");\n            \\\\ }}\n            \\\\\n            \\\\ const time = performance.now() - start;\n            \\\\\n            \\\\ console.log(\n            \\\\   JSON.stringify({{\n            \\\\     time: time.toFixed(2),\n            \\\\     elementCount: lis.length,\n            \\\\     last_li_id: lis[lis.length - 1].getAttribute(\"id\"),\n            \\\\     last_li_text: lis[lis.length - 1].textContent,\n            \\\\     success: clickCount === parseInt({d}),\n            \\\\   }}),\n            \\\\ );\n        ;\n\n        const script = try std.fmt.allocPrint(allocator, js, .{ v, v, v, v });\n        defer allocator.free(script);\n        const body = try std.fmt.allocPrint(allocator, \"\u003cbody\u003e\u003cscript\u003e{s}\u003c/script\u003e\u003c/body\u003e\", .{script});\n        // z.print(\"{s}\\n\", .{body});\n        defer allocator.free(body);\n        try engine.loadHTML(body);\n\n        try engine.executeScripts(allocator, \".\");\n\n        const end = std.time.nanoTimestamp();\n        const ms = @as(f64, @floatFromInt(end - start)) / 1_000_000.0;\n        std.debug.print(\"\\n⚡️ Zexplorer Engine Total Time: {d:.2}ms\\n\\n\", .{ms});\n\n        engine.deinit();\n    }\n}\n```\n\n\u003c/details\u003e\n\n**Results**\n\nThe start time of the engine is approx 1.5ms (difference between the engine setup \u0026 loop execution and the JavaScript execution, as measured by `performance()` ).\n\n| #rows  | JS/Zexplorer | Total/Zexplorer | JS/JSDom | Total/JSDom |\n| ------ | ------------ | --------------- | -------- | ----------- |\n| 100    | 0.29 ms      | 1.86 ms         | 9.81 ms  | 36.72 ms    |\n| 1_000  | 2.42 ms      | 3.77 ms         | 27.9 ms  | 32.90 ms    |\n| 10_000 | 24.83 ms     | 26.24 ms        | 191.1 ms | 197.9 ms    |\n| 20_000 | 49.11 ms     | 50.50 ms        | 315.4 ms | 318.9 ms    |\n| 50_000 | 125.28 ms    | 126.58 ms       | 741.4 ms | 745.5 ms    |\n\nThe DOM operations are externalized from the JavaScript runtime and these results demonstrate this clearly.\n\n---\n\n### Tests Zaniter module\n\nThe goal is to be as performant as [DOMPurify](https://github.com/cure53/DOMPurify) in terms of sanitization. It's a DOM-level sanitizer, not a string filter. It performs the sanitization in context, meaning  DOM and CSS aware, so retains the structure. It can allow framework attributes.\n\nThis is a two phase process. We firstly parse the input into a real DocumentFragment. It walks the tree DOM and attributes, URIs and CSS (parsed \u0026 sanitized). It applies whitelist and [html_specs rules](https://github.com/ndrean/z","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fndrean%2Fzexplorer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fndrean%2Fzexplorer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fndrean%2Fzexplorer/lists"}