{"id":51781768,"url":"https://github.com/conorbronsdon/mojo-xml","last_synced_at":"2026-07-20T13:01:32.138Z","repository":{"id":369602059,"uuid":"1290489610","full_name":"conorbronsdon/mojo-xml","owner":"conorbronsdon","description":"General-purpose XML in pure Mojo — an xml.etree.ElementTree-shaped DOM. 14/14 CPython byte-match.","archived":false,"fork":false,"pushed_at":"2026-07-06T03:55:14.000Z","size":249,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-06T05:10:52.101Z","etag":null,"topics":["dom","elementtree","mojo","mojolang","pure-mojo","python-interop","xml","xml-parser"],"latest_commit_sha":null,"homepage":null,"language":"Mojo","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/conorbronsdon.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"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":"2026-07-06T01:53:08.000Z","updated_at":"2026-07-06T04:42:19.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/conorbronsdon/mojo-xml","commit_stats":null,"previous_names":["conorbronsdon/mojo-xml"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/conorbronsdon/mojo-xml","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/conorbronsdon%2Fmojo-xml","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/conorbronsdon%2Fmojo-xml/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/conorbronsdon%2Fmojo-xml/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/conorbronsdon%2Fmojo-xml/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/conorbronsdon","download_url":"https://codeload.github.com/conorbronsdon/mojo-xml/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/conorbronsdon%2Fmojo-xml/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35686949,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-07-20T02:08:10.276Z","status":"ssl_error","status_checked_at":"2026-07-20T02:08:09.736Z","response_time":111,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"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":["dom","elementtree","mojo","mojolang","pure-mojo","python-interop","xml","xml-parser"],"created_at":"2026-07-20T13:01:31.221Z","updated_at":"2026-07-20T13:01:32.133Z","avatar_url":"https://github.com/conorbronsdon.png","language":"Mojo","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n# mojo-xml\n\n**General-purpose XML parsing in pure Mojo — an `xml.etree.ElementTree`-shaped API. No Python dependencies, no FFI.**\n\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](LICENSE)\n[![Mojo](https://img.shields.io/badge/Mojo-1.0.0b3%2B_nightly-orange?style=flat-square)](https://mojolang.org)\n[![Podcast](https://img.shields.io/badge/Podcast-Chain_of_Thought-purple?style=flat-square)](https://chainofthought.show)\n[![X](https://img.shields.io/badge/X-@ConorBronsdon-black?style=flat-square\u0026logo=x)](https://x.com/ConorBronsdon)\n\n\u003cimg src=\"docs/demo.gif\" alt=\"Demo: mojo run examples/catalog.mojo parses a catalog, queries it with the ElementTree API (find/findtext/iter), and serializes it back — 14/14 byte-match vs CPython xml.etree, 6,000-iter fuzz\" width=\"820\"\u003e\n\n\u003c/div\u003e\n\nAs of mid-2026 the Mojo ecosystem has JSON, TOML, CSV, and YAML parsers, and a\nfeed parser ([mojo-feed](https://github.com/conorbronsdon/mojo-feed)) — but no\ngeneral-purpose XML library. mojo-xml fills that gap: parse any XML document\ninto a tree of `Element`s and walk it with the API Python developers already\nknow from `xml.etree.ElementTree`. It's built on the same fuzzed pull parser\nthat powers mojo-feed, with a proper scoped-namespace DOM layer on top.\n\n### Coming from Python\n\nIf you know Python's `xml.etree.ElementTree`, the API is nearly 1:1:\n\n| Python (`xml.etree.ElementTree`)   | mojo-xml                          |\n| ---------------------------------- | --------------------------------- |\n| `root = ET.fromstring(xml)`        | `var root = fromstring(xml)`      |\n| `root.findall(\"book\")`             | `root.findall(\"book\")`            |\n| `el.get(\"id\")`                     | `el.get(\"id\")`                    |\n| `el.findtext(\"title\")`             | `el.findtext(\"title\")`            |\n| `root.iter(\"title\")`               | `root.iter(\"title\")`              |\n| `ET.tostring(el)`                  | `tostring(el)`                    |\n| `ET.SubElement(parent, \"tag\")`     | `SubElement(parent, \"tag\")`       |\n\n## What it does\n\n- **Parse to a tree**: `fromstring(text)` returns the root `Element`; every\n  element carries `tag`, `attrib`, `text`, `tail`, and `children` — the exact\n  ElementTree model, including the `text`/`tail` split that trips up most\n  hand-rolled parsers (`\u003ca\u003et0\u003cb/\u003et1\u003c/a\u003e` → `a.text=\"t0\"`, `b.tail=\"t1\"`).\n- **Namespaces, resolved properly**: prefixes and default `xmlns` resolve to\n  Clark notation (`\u003ca:foo xmlns:a=\"http://x\"\u003e` → tag `{http://x}foo`), with a\n  *scoped* xmlns stack so nested prefix redefinition is honored. Default\n  namespaces apply to element names but not unprefixed attributes, per the XML\n  namespaces spec.\n- **Query**: `find` / `find_opt` / `findall` / `findtext` / `iter` with a\n  pragmatic ElementTree path subset — `tag`, `a/b/c`, `*`, `.//tag`,\n  `.//a/b`, leading `./`, and Clark-notation `{uri}local`.\n- **Serialize**: `tostring(elem)` writes well-formed XML back out, escaping\n  text and attribute values and declaring collected namespaces on the root.\n  `fromstring(tostring(e))` round-trips to a structurally-equal tree.\n- **Entities, CDATA, comments, PIs, DOCTYPE, and encoding normalization**\n  (UTF-16/BOM/Latin-1/Windows-1252) — inherited from the underlying pull parser.\n- **Strict by default**: like ElementTree, `fromstring` rejects malformed input\n  — mismatched or stray end tags, unclosed elements, undefined entities,\n  multiple document elements (junk after the root), non-whitespace text outside\n  the root, and unbound namespace prefixes — rather than silently recovering.\n  (The reserved `xml:` prefix is implicitly bound and needs no declaration.)\n\n## What it deliberately does NOT do (yet)\n\n- **Full XPath.** `find`/`findall` support the common ElementTree path subset\n  above, not predicates, axes, or functions.\n- **DTD / schema validation.** Well-formedness is checked; validation against a\n  DTD or XSD is out of scope for v0.1.\n- **Streaming for unbounded input.** `fromstring` builds a full in-memory tree.\n  For event-at-a-time processing over huge documents, drop down to the\n  `XmlPullParser` (also exported) — the same pull API mojo-feed uses.\n\n## Install\n\nWith [pixi](https://pixi.prefix.dev):\n\n```bash\npixi install\npixi run test\npixi run demo\n```\n\nOr with uv:\n\n```bash\nuv venv\nuv pip install mojo --index https://whl.modular.com/nightly/simple/ --prerelease allow\n.venv/bin/mojo run -I src test/test_etree.mojo\n```\n\nRequires a Mojo nightly (`\u003e=1.0.0b3`).\n\n## Usage\n\n```mojo\nfrom xml import fromstring, tostring, Element\n\ndef main() raises:\n    var root = fromstring(String(\n        \"\u003ccatalog\u003e\u003cbook id='b1'\u003e\u003ctitle\u003eMojo\u003c/title\u003e\u003c/book\u003e\u003c/catalog\u003e\"\n    ))\n    print(root.tag)                        # catalog\n    var book = root.find(\"book\")           # first \u003cbook\u003e\n    print(book.get(\"id\"))                  # b1\n    print(book.findtext(\"title\"))          # Mojo\n\n    for t in root.iter(\"title\"):           # every \u003ctitle\u003e at any depth\n        print(t.text)\n\n    print(tostring(root))                  # serialize back to XML\n```\n\nNamespaced documents resolve to Clark notation:\n\n```mojo\nvar svg = fromstring(open(\"logo.svg\", \"r\").read())\n# svg.tag == \"{http://www.w3.org/2000/svg}svg\"\nfor rect in svg.iter(\"{http://www.w3.org/2000/svg}rect\"):\n    print(rect.get(\"width\"), rect.get(\"height\"))\n```\n\n`find` raises if there's no match; `find_opt` returns an `Optional[Element]`:\n\n```mojo\nvar maybe = root.find_opt(\"missing\")\nif maybe:\n    print(maybe.value().tag)\n```\n\n## Conformance \u0026 robustness\n\n**Byte-for-byte against CPython.** `test/anchor_run.py` parses a corpus of real\ngeneral-XML documents (SVG, a Maven `pom.xml`, a sitemap, a SOAP envelope, an\nAndroid layout, an entity-heavy config, an Atom feed, an RSS/podcast feed, an\nXHTML fragment, deeply-nested mixed content with comments and PIs, a CDATA\ndocument, one with attributes in multiple namespaces, an empty-element-heavy\ndocument, and one with CRLF line endings and multi-line attribute values) two\nways — with mojo-xml and with Python's own `xml.etree.ElementTree` — and dumps\neach tree in an identical canonical format. **All 14 match element-for-element**\nin strict, whitespace-exact mode: Clark-notation namespaces (including the\nreserved `xml:` prefix), sorted attributes, the `text`/`tail` model, XML 1.0\nline-ending and attribute-value normalization, and entity decoding are identical\nto CPython.\n\n```bash\npixi run test                       # 65 DOM tests + 39 pull-parser tests\npython3 test/anchor_run.py --strip  # optional: env MOJO=\u003cmojo binary\u003e\n```\n\n**Tests.** 104 total: `test/test_etree.mojo` (65 — parsing, text/tail, entities,\nCDATA, namespaces, find/findall/findtext/iter, mutation, serialization and\nescaping, malformed-input error cases) and `test/test_pull.mojo` (39 — the\nunderlying tokenizer).\n\n**Fuzzing.** `test/fuzz_drive.py` mutates the corpus plus 18 adversarial\nsynthetics (deep nesting, wide fan-out, namespace bombs, entity/char-ref\nfloods, malformed UTF-8, huge attribute counts, oversized tokens, and\ncomment/CDATA/newline edge cases): 6,000 iterations with **zero crashes and\nzero hangs** — malformed input either parses or raises a clean error. Hostile\ninput is bounded: element nesting is capped (`MAX_DEPTH = 512`) so a\npathologically deep document raises instead of driving the tree-walking APIs\ninto quadratic time.\n\n## Design notes \u0026 limitations worth knowing\n\n- **`iter` and `tostring` return owned copies**, not references (Mojo's\n  ownership model). Cost is linear in the visited subtree for any real document;\n  the `MAX_DEPTH` cap bounds the worst case on adversarial deep chains.\n- **`SubElement(parent, tag)` returns a snapshot copy**, not a live reference\n  into `parent.children`. To build a rich child, construct it fully, then\n  `parent.append(child^)`.\n- **Serialization order** of attributes and generated namespace prefixes\n  follows `Dict` insertion order; round-trip *structural* equality holds\n  regardless of order.\n- **Namespace scoping** is lexical (pushed/popped as elements open and close) —\n  stronger than the document-flat resolution the feed parser uses.\n\n## Part of a pure-Mojo library suite\n\nEleven pure-Mojo libraries that mirror familiar Python stdlib and PyPI APIs, filling\ngaps in the native Mojo ecosystem:\n\n- [mojo-feed](https://github.com/conorbronsdon/mojo-feed) — RSS/Atom/JSON Feed\n  parsing (Python's `feedparser`); shares this library's pull parser\n- [mojo-captions](https://github.com/conorbronsdon/mojo-captions) — SRT and\n  WebVTT subtitle/transcript parsing (no Python stdlib parallel)\n- [mojo-html](https://github.com/conorbronsdon/mojo-html) — HTML parsing and\n  article extraction (Python's `readability`)\n- [mojo-markdown](https://github.com/conorbronsdon/mojo-markdown) —\n  CommonMark markdown parsing (Python's `markdown`)\n- [mojo-unicodedata](https://github.com/conorbronsdon/mojo-unicodedata) —\n  Unicode normalization and case folding (Python's `unicodedata`)\n- [mojo-url](https://github.com/conorbronsdon/mojo-url) — URL parsing and\n  encoding (Python's `urllib.parse`)\n- [mojo-diff](https://github.com/conorbronsdon/mojo-diff) — text diffing\n  (Python's `difflib`)\n- [mojo-template](https://github.com/conorbronsdon/mojo-template) — a\n  Jinja-flavored template engine (Python's `jinja2`)\n- [mojo-tar](https://github.com/conorbronsdon/mojo-tar) — tar archive\n  reading and writing (Python's `tarfile`)\n- [mojo-redis](https://github.com/conorbronsdon/mojo-redis) — a Redis\n  client (Python's `redis-py`)\n\n## Contributing\n\nIssues and PRs welcome — especially real-world XML that parses differently from\n`xml.etree` (attach the document or a snippet) and path-matching gaps. Run\n`pixi run test` and `python3 test/anchor_run.py` before sending a PR.\n\n## About\n\nBuilt by [Conor Bronsdon](https://conorbronsdon.com) — host of\n[Chain of Thought](https://chainofthought.show), a podcast about AI agents,\ninfrastructure, and engineering. Find me on [X](https://x.com/ConorBronsdon) or\n[LinkedIn](https://www.linkedin.com/in/conorbronsdon).\n\n---\n\n## Disclaimer\n\n*All views, opinions, and statements expressed on this account/in this repo are solely my own and are made in my personal capacity. They do not reflect, and should not be construed as reflecting, the views, positions, or policies of Modular. This account is not affiliated with, authorized by, or endorsed by my employer in any way.*\n\n## License\n\nLicensed under the [MIT License](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fconorbronsdon%2Fmojo-xml","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fconorbronsdon%2Fmojo-xml","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fconorbronsdon%2Fmojo-xml/lists"}