Engineering Journal
Schema Editor
Schema Editor

We had eleven parsers for one format and none of them worked

2026-08-02

TLDR

Eleven copies of a one-line regex accumulated for reading SVG path data. Four of them read relative coordinates as absolute, seven silently dropped them, and none handled curves. Every copy looked correct in isolation, and the format they all failed on is the default output of the most common tool our users import from.

The assumption that seemed reasonable

SVG path data looks simple when you only produce it. Our editor writes wires like this:

M 100 120 L 140 120 L 140 90 L 200 90

Absolute coordinates, only M and L. Reading that back is one line:

const pts = [...d.matchAll(/[ML]\s*([\d.eE+-]+)[,\s]+([\d.eE+-]+)/g)]
  .map(m => ({ x: parseFloat(m[1]), y: parseFloat(m[2]) }));

Given that we control the writer, this is genuinely correct. The assumption underneath it, never stated, was that we would only ever read data we had written.

That assumption was already false when it was made. The editor imports SVG.

When it failed

It did not fail loudly. It produced coordinates.

The report was that selection handles on an imported drawing were nowhere near the wire. Investigating that led somewhere else first, but on the way I grepped for path parsing and found eleven separate copies of that regex, and they did not agree with each other:

variantsitesbehaviour
/gi4matched relative commands, then read the offsets as absolute
/g7dropped every relative segment
Neither variant handled H, V, any curve, arcs, or implicit repeated coordinates (L 10 10 20 20, which is legal and common in optimized output).

Here is what the two variants do to the same wire, written relatively:

d = "m 100,120 l 40,0 l 0,-30 l 60,0"        (ends at 200,90)

/gi -> [(100,120), (40,0), (0,-30), (60,0)] offsets read as positions /g -> [] everything dropped

The /gi result is the dangerous one. It is four points. It has the right shape, the right count, the right types. It is just describing a completely different wire, most of it clustered near the origin.

And the format that triggers it is not exotic. Inkscape writes relative commands by default. So does most SVG optimization. Anything a user imported hit all four failure modes at once, while everything drawn in-app worked perfectly, which is why it survived so long.

What was actually wrong

Not the regex. The regex is fine for what it was written for.

What was wrong is that a regex reading a structured format is a parser, and we never acknowledged that we had written one. If it had been called parsePathData and lived in a file, someone would have asked what it does with C. Because it was an inline expression at a call site, it read as string handling, and string handling does not invite that question.

The duplication then guaranteed drift. Each new call site copied whichever nearby version the author saw, and someone at some point added /i to one of them, probably because a lowercase path was not matching. That change made four sites match more data and interpret it wrongly, which is worse than the seven that matched nothing. A partial fix to a duplicated parser makes the fleet less consistent, not more.

The compounding part: I had recently built wire cutting and endpoint re-anchoring on top of these reads. Those features write geometry back. So on an imported drawing they were computing from scattered coordinates and then persisting the result.

What got deleted

All eleven, replaced by one module. The four that remain in the codebase are guarded fallbacks behind a presence check, so a stripped build still limps rather than crashing.

The replacement is an actual parser: a tokenizer that handles numbers running together (10-20), exponents, and bare decimals; a command loop that resolves relative against a running point; curve flattening for C/S/Q/T; and arcs via the spec's endpoint-to-centre conversion so an imported arc contributes its bulge and not just its endpoints.

What replaced it, beyond the parser

Two things I would not have thought to add if this had stayed a regex.

A tight bounding box. Once you can read the geometry, you can compute a bbox that skips subpaths rendering no ink. getBBox() counts a lone moveto or zero-length segment, so one leftover vertex parked far from the artwork stretches the box across the gap and puts the selection handles on a phantom corner:

d = "M 10 10 L 90 10 L 90 60   M 1010 984 L 1010 984"
getBBox() equivalent : {x:10, y:10, w:1000, h:974}
tight                : {x:10, y:10, w:80,   h:50}

A capability check. The important one.

function isPolylineSafe(d) {
  if (/[CcSsQqTtAa]/.test(d)) return false;
  return subpathCount(d) <= 1;
}

Cutting a wire rewrites d as an M/L polyline. On a path with curves that is not a cut, it is a redraw. Every destructive operation now asks this first and declines with a message rather than quietly flattening a drawing. That is the part that turns a parser into a safety property: knowing what you cannot represent is worth as much as knowing what you can.

The generalizable lesson

If you are matching a grammar with a regex, you have written a parser. Give it a name and a file, because that is what makes people ask it the hard questions.

Three tells that this is happening to you right now:

The same expression appears three or more times. By then it has already diverged. Grep before you add another; I found the eleventh by looking for the first.

It only handles the subset you emit. Any format you both read and write will eventually be handed a file you did not write. The relevant question is not "what do we produce" but "what is legal in this format, and what does the most popular tool in this space produce by default." Those had different answers here, and the second one is what users actually give you.

Someone patched one copy. A flag added to one of eleven is not a fix, it is a fork. The moment a shared expression needs a variation is the moment it needed to be a function.

The bug was not that the regex was too simple. It was that nobody could see it was a parser, so nobody held it to a parser's standard.

Read this post in the full Engineering Journal →