YouTube Summaries

← All summaries

Writing 127 lines of XML parser instead of taking a dependency

2026-09-07 Mon ⏱ 2 hr 30 min tsodingdaily

A recreational programming stream where Tsoding needs to read the stack.xml inside an OpenRaster (.ora) file from Jai, a language with no XML — or even JSON — library. Rather than pull in libxml or shell out to Python, he writes a parser for exactly the subset of XML his one file uses: 127 lines, no XML-specific dependency. The parser is then used to rebuild a sprite atlas out of the layered .ora file so he can align the "line boil" animation frames of his music player's digits in Krita.

Why the parser exists at all

The music player (the Dimooper-adjacent project he has been streaming) draws its digits, buttons and icons from hand-drawn atlases. The wobbly look is the traditional animation technique called line boil: each glyph is drawn three times and the frames are cycled. The atlas layout is one column per symbol, one row per boil frame.

The problem is that the frames were drawn sloppily long ago and the digits visibly wave. Fixing that requires overlaying frames semi-transparently and nudging/rotating them until the shape stays intact while remaining slightly different — work that is only possible in a layered editor, not in a flat atlas.

So he already has a script going one direction: atlas → .ora. A .ora file is just a zip containing a mimetype, the layer PNGs, and a stack.xml describing the layer tree — effectively an open-source PSD. What's missing is the reverse: .ora → atlas, which means parsing that XML from Jai. And Jai, being Jonathan Blow's language, ships no XML parser (and no JSON parser either), which Tsoding says he fully respects.

The thesis: parse the file you have, not the spec

The core argument of the stream: he does not have "crazy XMLs", he has one kind of XML, and he only needs enough of the format to extract layer names, positions and image paths. Pulling in a large dependency written in another language to read a 5.7 KB file with no CDATA, no text nodes, no single-quoted attributes and no entities is the wrong trade.

He compares it to writing your own game engine: people hear "own engine" and imagine reimplementing Unreal, when in fact a retro pixel-art platformer needs very little. Same here — the win is depending on a small piece of code you fully control instead of a big one you don't, and the escape hatch stays open: the day a genuinely gnarly XML shows up, bring in the library.

Chat repeatedly objects with spec features (CDATA, quote states, tail elements after the root); each time the answer is the same — is that in this file? It isn't, so it isn't implemented.

First approach: a state machine (discarded)

The first attempt tracks a state enum (text / tag), appends bytes into a string builder, and flips state on < and >. Byproducts of this phase:

  • A digression on Jai's string builder being a linked list of large chunks rather than a dynamic array: pointers into it stay valid while appending, at the cost of an explicit builder→string conversion step. His own dynamic-array builders have the opposite trade-off.
  • Row/column tracking so traced chunks can be jumped to from Emacs (with the obligatory +1 because Emacs counts from one), plus separate start_row / start_column saved on state transitions, since the running position points at the end of a chunk.
  • Rendering control characters as escapes when tracing, so newlines don't wreck the output.

Then he throws the state machine away: since there are no text nodes to collect, the whole thing collapses into ordinary top-down parsing over a slice.

Second approach: recursive-descent-shaped trimming

The rewrite is built from small primitives over a string slice: trim_whitespace, trim_until_char, trim_name (returning the collected span), and expect_char. A realization mid-way: expect_char and a "chop it only if present" variant are the same function — it returns a bool, and the caller decides whether a false means "report an error" or "branch to different logic". That single function drives the opening/closing/self-closing tag distinction.

The parse loop then reads: trim whitespace, expect <, check for / to tell a closing tag from an opening one, read the name (empty name = error), and for opening tags loop over =name = "value"= pairs until trim_name returns nothing.

The DOM is deliberately minimal — a node has a name, a dynamic array of child pointers, and a hash table of properties. No text nodes, because the file has none; they can be added when needed.

Tree construction uses an explicit stack of node pointers (he notes the call stack would work too — "recursive descent into madness"). Opening tags push; closing tags pop, assert the popped name matches, and append the node to the new top's children; when the stack empties, that node is the root and the loop ends. Self-closing tags do both in one step, at the cost of a little duplicated stack juggling that he judges acceptable.

Bugs found by running it: a missing expect_char after reading a quoted value, and — the one real spec surprise — attribute names in this file contain dashes, so is_name becomes "alphanumeric or dash".

Error handling is deliberately just asserts plus a "failed at <offset>" print. His justification: this is compile-time asset-pipeline code that only ever runs on his machine, so proper diagnostics aren't worth the stream time. He does note that if he wanted good errors he'd have to carry locations into the nodes.

After factoring the whole thing into a parse_xml(content) -> *Node function, he measures it: 127 lines, no XML dependency (libc aside). That measurement is the point of the stream.

Second pass: XML tree → atlas

An aside he makes here: XML and JSON are both parsed twice — once text→tree, once tree→the thing you actually wanted (the second pass usually called validation). A SAX-style parser would fuse them, but he doesn't want SAX.

The extraction pass walks the document: the root must be image (giving =w=/=h=, which are the cell dimensions), its first child a stack named root, and each group under it a stack whose name is the digit. An assert immediately catches a non-=stack= child — a background layer he'd forgotten — handled with a continue. Counting groups gives 11 columns; the max children per group gives 3 rows; multiplied by cell size that reproduces exactly the original atlas dimensions.

For the blit he writes a small image layer, mirroring his olive.c conventions: pixels, width, height, and stride. He explains stride at length — the number of elements to skip to reach the next row, equal to width for a whole image but larger for a sub-image, which is what makes zero-copy sub-views of an atlas possible. On top of that: image_pixel(img, x, y) returning a pointer (the key convenience for thinking 2D over a 1D array), image_view for sub-images, image_new, and image_load / image_save via the STB image bindings that Jai does ship.

Placing a layer is then two nested views: first the cell at (group index × cell width, digit index × cell height), then within that cell the layer's own =x=/=y= offset (the PNGs are trimmed, so the offset matters), sized to the loaded image. image_copy asserts matching dimensions and copies pixel by pixel.

Running it produces an atlas identical to the original, and after pointing the player at the regenerated file the digits no longer wave — the alignment work he'd done in Krita now round-trips. The remaining manual step is re-zipping the .ora (he muses about writing his own zip/unzip next stream).

Asides worth keeping

  • How to learn programming: the people who are good at it never asked. They wanted to mod a game, or make some specific thing, and programming found them along the way. Do what you actually want to do, fail, learn.
  • Jai as a metaprogramming / build tool: at compile time you can run a compiler event loop and react to events such as "this function was type-checked", inspect and modify it, and recompile — useful for instrumentation like profilers. The value is that it's one coherent tool, versus CMake being a generator for a generator for a generator.
  • Jai availability: binaries to the public are expected to be worked on after the game's release (early October); open-sourcing comes after that. The language is visibly unfinished in daily use.
  • Krita gets praise for its Ctrl-Enter command palette and for deliberately mimicking Photoshop, which makes switching easy.
  • Parsing layered editor formats for asset pipelines is a well-established game-dev practice — people do the same with PSD in C/C++.