<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Bonzai2Carn</title>
    <description>The latest articles on DEV Community by Bonzai2Carn (@bonzai2carn).</description>
    <link>https://dev.to/bonzai2carn</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3821639%2F5996c0fb-fd15-4c12-9a9b-9216046f0bfb.png</url>
      <title>DEV Community: Bonzai2Carn</title>
      <link>https://dev.to/bonzai2carn</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bonzai2carn"/>
    <language>en</language>
    <item>
      <title>Why Web Workers Swallow Your Stacktraces (And How to Write Specs to Fix It)</title>
      <dc:creator>Bonzai2Carn</dc:creator>
      <pubDate>Tue, 07 Jul 2026 14:30:00 +0000</pubDate>
      <link>https://dev.to/bonzai2carn/why-web-workers-swallow-your-stacktraces-and-how-to-write-specs-to-fix-it-56de</link>
      <guid>https://dev.to/bonzai2carn/why-web-workers-swallow-your-stacktraces-and-how-to-write-specs-to-fix-it-56de</guid>
      <description>&lt;p&gt;&lt;strong&gt;TLDR:&lt;/strong&gt; A spec that says "the function checks &lt;code&gt;r.bbox.x &amp;lt; pageWidth / 2&lt;/code&gt;" without saying where &lt;code&gt;pageWidth&lt;/code&gt; comes from is not a spec. It is a description of behavior with a hidden dependency. Every architecture document I have written with that pattern has produced at least one ReferenceError in implementation.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Pattern That Keeps Failing
&lt;/h2&gt;

&lt;p&gt;Architecture documents are good at describing what code should do. They are bad at describing where values come from and which function boundaries they cross.&lt;/p&gt;

&lt;p&gt;The page assembly refactor spec said:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Check for FEATURE_LAYOUT: 2 cols, left is all visual, right is text. Compare each region's &lt;code&gt;r.bbox.x&lt;/code&gt; against &lt;code&gt;pageWidth / 2&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is a correct behavioral description. It says nothing about execution context. The implementer writes it into &lt;code&gt;_detectAutoZones&lt;/code&gt;, a module-level function, and references &lt;code&gt;pageWidth&lt;/code&gt; by name. The spec did not say &lt;code&gt;pageWidth&lt;/code&gt; needed to be passed as a parameter. The spec did not say &lt;code&gt;_detectAutoZones&lt;/code&gt; was a module-level function. Both facts were implicit.&lt;/p&gt;

&lt;p&gt;The result: &lt;code&gt;ReferenceError: pageWidth is not defined&lt;/code&gt; on every PDF load, with a stacktrace that points at the worker message handler instead of the actual line.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Keeps Happening
&lt;/h2&gt;

&lt;p&gt;Architecture documents are written in prose. Prose does not have a type system. A sentence like "the function uses &lt;code&gt;pageWidth&lt;/code&gt;" can mean:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The function receives &lt;code&gt;pageWidth&lt;/code&gt; as a parameter.&lt;/li&gt;
&lt;li&gt;The function reads &lt;code&gt;pageWidth&lt;/code&gt; from a module-level variable.&lt;/li&gt;
&lt;li&gt;The function is nested inside a caller that defines &lt;code&gt;pageWidth&lt;/code&gt; and closes over it.&lt;/li&gt;
&lt;li&gt;The function reads &lt;code&gt;pageWidth&lt;/code&gt; from an object passed in.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All four are syntactically valid JavaScript. The prose does not distinguish between them. The implementer picks one and moves on.&lt;/p&gt;

&lt;p&gt;When the spec is written by the same person who will implement it, option 3 is tempting because it is the least-friction path. The value is just "there" without needing to thread it through function signatures. It works when the function is actually nested. It throws when the function ends up at module level for any reason (extracted for reuse, moved for readability, placed outside the call site by default).&lt;/p&gt;




&lt;h2&gt;
  
  
  What Correct Specs Look Like
&lt;/h2&gt;

&lt;p&gt;A spec that is actually implementable names the function signature:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;_detectAutoZones(regions, numCols, pageWidth)&lt;/code&gt;: &lt;code&gt;pageWidth&lt;/code&gt; is &lt;code&gt;viewport.width || 612&lt;/code&gt;, passed from &lt;code&gt;assemblePage&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is two extra words. It removes all ambiguity. The implementer knows the parameter needs to exist. The reviewer can check that the call site passes it. The bug does not happen.&lt;/p&gt;

&lt;p&gt;The discipline: any time you write "the function uses X" in an architecture document, immediately write where X comes from. If it is a parameter, name it in the signature. If it is a module constant, name the constant. If it is a derived value, show the derivation.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Stacktrace Problem
&lt;/h2&gt;

&lt;p&gt;ReferenceErrors in Web Workers have a specific failure mode: the worker's error handler catches the error, serializes &lt;code&gt;err.message&lt;/code&gt; (a string), and posts it to the main thread. The stack is not forwarded. The main thread reconstructs a new Error from the message string and throws it. DevTools shows the main thread throw, not the worker's.&lt;/p&gt;

&lt;p&gt;This means a ReferenceError in a worker looks like an error in the worker message handler, not in the actual throwing function. The real location is invisible. You find it by grepping for the identifier named in the error message.&lt;/p&gt;

&lt;p&gt;This is a general problem with any architecture that serializes errors across execution boundaries (workers, iframes, service workers, error-catching middleware). The message string is preserved. The stack is not. Every ReferenceError in such a system requires a grep rather than a stacktrace to locate.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Uncomfortable Implication
&lt;/h2&gt;

&lt;p&gt;If your architecture document has not specified where every value used by every function comes from, your implementation will have bugs that grep finds faster than debuggers. That is not a criticism of the implementer. It is a criticism of the spec.&lt;/p&gt;

&lt;p&gt;The solution is not more thorough prose. It is specifying function signatures explicitly, the same way you would in TypeScript or a statically-typed language. Write the types. Write the parameter names. Write where the values come from. Three lines of explicit signature beats three paragraphs of behavioral description every time.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>javascript</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Web Components vs. Iframes: A Hard Lesson in DOM Isolation Barriers</title>
      <dc:creator>Bonzai2Carn</dc:creator>
      <pubDate>Sat, 04 Jul 2026 14:30:00 +0000</pubDate>
      <link>https://dev.to/bonzai2carn/web-components-vs-iframes-a-hard-lesson-in-dom-isolation-barriers-1icm</link>
      <guid>https://dev.to/bonzai2carn/web-components-vs-iframes-a-hard-lesson-in-dom-isolation-barriers-1icm</guid>
      <description>&lt;h2&gt;
  
  
  TLDR
&lt;/h2&gt;

&lt;p&gt;A custom element that fetched a canonical app's HTML and swapped &lt;code&gt;document.body.innerHTML&lt;/code&gt; looked clean on the surface. It worked until it didn't: the swap raced with the existing app's event handlers, producing a tool that rendered correctly but did nothing. The correct pattern, an iframe pointing at the canonical URL with &lt;code&gt;?view=&lt;/code&gt;, was already in use by the VS Code extension. It took three weeks to apply the same answer to the web.&lt;/p&gt;




&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;a href="https://ginexys.com" rel="noopener noreferrer"&gt;Ginexys&lt;/a&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Assumption That Seemed Reasonable
&lt;/h2&gt;

&lt;p&gt;Ten SEO landing pages need unique &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; metadata but should load the same tool. A custom element that fetches the canonical tool HTML and injects it into the current page would deduplicate the tool code while keeping per-page metadata. One component, ten thin wrapper pages. Fewer moving parts.&lt;/p&gt;




&lt;h2&gt;
  
  
  How It Broke
&lt;/h2&gt;

&lt;p&gt;The custom element ran &lt;code&gt;document.body.innerHTML = canonicalBody.innerHTML&lt;/code&gt;. This replaced the body after the scripts that loaded the tool had already attached their event handlers to the original DOM.&lt;/p&gt;

&lt;p&gt;The app init pattern was &lt;code&gt;DOMContentLoaded → attach handlers → ready&lt;/code&gt;. After the innerHTML swap, the DOM the handlers were attached to was gone. The new DOM from the canonical body had no handlers attached. Sometimes the app's deferred script loaded against the old body and ran to completion. Sometimes it ran against the new body. Sometimes it ran twice. The outcome was non-deterministic.&lt;/p&gt;

&lt;p&gt;Symptoms: tabs switched. File dialogs opened. Buttons were visually interactive. But clicking "process file" extracted nothing. Clicking "export" produced nothing. Clicking a gated feature opened nothing. No console errors. Everything looked correct and did nothing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why It Was Hard to Find
&lt;/h2&gt;

&lt;p&gt;The canonical URL was always used for testing. &lt;code&gt;/tools/pdf-processor/&lt;/code&gt; loaded fine, worked correctly, passed every test.&lt;/p&gt;

&lt;p&gt;A Vite plugin had been added to redirect the root canonical URL to &lt;code&gt;/editor/&lt;/code&gt; to prevent a PDF.js worker from hitting the root URL and receiving HTML instead of JavaScript. This redirect routed all user traffic through the wrapper. The standalone test path stopped existing. The bug only appeared on the path users actually took, and that path looked correct visually.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Was Thrown Away
&lt;/h2&gt;

&lt;p&gt;Three web component files, 8KB of code. The approach was not wrong in principle. It was wrong for an app that does imperative DOM initialization. A custom element that injects static HTML into a page is fine. A custom element that injects a running app into a page, with scripts that have already begun attaching handlers, is not.&lt;/p&gt;

&lt;p&gt;Also discarded: the assumption that "deduplicate HTML" means "inject HTML". Deduplication of a running app means isolation, not injection.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Replaced It
&lt;/h2&gt;

&lt;p&gt;Each SEO landing page became a thin HTML with unique metadata and a single full-viewport iframe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;iframe&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/tools/pdf-processor/?view=editor"&lt;/span&gt;
        &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"width:100%;height:100vh;border:none;"&lt;/span&gt;
        &lt;span class="na"&gt;allow=&lt;/span&gt;&lt;span class="s"&gt;"..."&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The canonical app loads inside its own document. Its scripts initialize against its own DOM. Nothing races. The &lt;code&gt;?view=editor&lt;/code&gt; query parameter activates the right tab. No DOM swap, no event handler collision.&lt;/p&gt;

&lt;p&gt;The VS Code extension had been doing this correctly since the beginning: load the canonical &lt;code&gt;index.html&lt;/code&gt; into a webview, inject a global for mode selection, let the app init normally. Three weeks later, the web followed the same pattern.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Lesson
&lt;/h2&gt;

&lt;p&gt;When you need to embed an app inside another document, use an isolation boundary that the browser already provides. The iframe is the answer. Web components are for components, meaning UI elements that render within the host document's DOM. An existing running app with its own initialization sequence is not a component. Load it at its own origin and talk to it via postMessage or URL parameters.&lt;/p&gt;

&lt;p&gt;The sign that you are solving the wrong problem: when your custom element needs to replace &lt;code&gt;document.body.innerHTML&lt;/code&gt;, you are trying to do iframe isolation without the isolation.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Seven Table Parsers, One Interface: Designing a Table Formatter and Node Editor (TAFNE)</title>
      <dc:creator>Bonzai2Carn</dc:creator>
      <pubDate>Thu, 02 Jul 2026 14:30:00 +0000</pubDate>
      <link>https://dev.to/bonzai2carn/seven-table-parsers-one-interface-designing-a-table-formatter-and-node-editor-tafne-50gf</link>
      <guid>https://dev.to/bonzai2carn/seven-table-parsers-one-interface-designing-a-table-formatter-and-node-editor-tafne-50gf</guid>
      <description>

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;a href="https://ginexys.com/app/tafne" rel="noopener noreferrer"&gt;Table Formatter&lt;/a&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/carnworkstudios/TAFNE" rel="noopener noreferrer"&gt;Repo&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first question most people ask when they see TAFNE accept HTML, CSV, TSV, Markdown, JSON, ASCII art tables, and SQL INSERT statements from the same input field is: how does it know which one you pasted?&lt;/p&gt;

&lt;p&gt;The short answer: it doesn't always. You tell it. Or the file extension tells it. Or for unknown text files, it makes a content-based guess.&lt;/p&gt;

&lt;p&gt;The longer answer is that each of these seven formats requires a genuinely different parsing approach, and the differences are interesting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Dispatcher
&lt;/h2&gt;

&lt;p&gt;The entry point is a switch statement in &lt;code&gt;parseInput()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="nx"&gt;tableHtml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseHtmlInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ascii&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="nx"&gt;tableHtml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseAsciiInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;csv&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="nx"&gt;tableHtml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseCsvInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="nx"&gt;tableHtml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseTextInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;markdown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tableHtml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseMarkdownInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="nx"&gt;tableHtml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseJsonInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sql&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="nx"&gt;tableHtml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseSqlInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every branch takes the same input: a raw text string. Every branch produces the same output: an HTML table string. What happens in between is completely different.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Parsing Strategies
&lt;/h2&gt;

&lt;p&gt;The seven parsers split into two fundamental camps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Semantic parsers&lt;/strong&gt; rely on explicit structure. The format declares its own structure using tags, keys, or reserved keywords. The parser just has to read that declaration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Heuristic parsers&lt;/strong&gt; rely on pattern recognition. The format is a convention, not a formal specification. The parser makes educated guesses based on what it sees.&lt;/p&gt;

&lt;p&gt;HTML and JSON are semantic. CSV, TSV, and ASCII are heuristic. Markdown and SQL sit in the middle.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Semantic Parsers
&lt;/h2&gt;

&lt;p&gt;The HTML parser uses a regex to find table elements:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tablePattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&amp;lt;table&lt;/span&gt;&lt;span class="se"&gt;[\s\S]&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;&amp;lt;&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;table&amp;gt;/gi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;matches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tablePattern&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The structure is already there. The parser's job is to extract and normalize it, not reconstruct it.&lt;/p&gt;

&lt;p&gt;The JSON parser reads explicit keys and values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The column names are the object keys. The rows are the array elements. No guessing involved. If the input is valid JSON, the structure is unambiguous.&lt;/p&gt;

&lt;p&gt;The JSON parser also handles a common real-world case: JSON that wraps the array in an outer object. If &lt;code&gt;JSON.parse&lt;/code&gt; returns an object rather than an array, the parser looks for the first key whose value is a non-empty array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;arrayKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;arrayKey&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;arrayKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This handles API responses that return &lt;code&gt;{ "results": [...] }&lt;/code&gt; or &lt;code&gt;{ "data": [...] }&lt;/code&gt; without requiring the user to drill into the JSON manually.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Heuristic Parsers
&lt;/h2&gt;

&lt;p&gt;The CSV parser assumes commas separate columns and newlines separate rows. That's it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cells&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cell&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^"|"$/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quotes are stripped from around cells. The first row is assumed to be headers. This works for the vast majority of real CSV files, but it will fail on CSV files that contain commas inside quoted fields (a common edge case in RFC 4180-compliant CSV). The current parser treats every comma as a delimiter regardless of quoting context. For most practical use cases, this is fine.&lt;/p&gt;

&lt;p&gt;The ASCII parser skips separator lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;+---&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;+===&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cells&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;|&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cell&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ASCII tables use &lt;code&gt;+---+---+&lt;/code&gt; for horizontal separators and &lt;code&gt;| val | val |&lt;/code&gt; for data rows. The parser identifies separators by looking for the &lt;code&gt;+---&lt;/code&gt; pattern, skips them, and splits data rows on pipes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The State Machine: Markdown
&lt;/h2&gt;

&lt;p&gt;Markdown tables have three types of lines: the header row, the separator row, and data rows. The separator row is syntactically distinct but carries no data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\|?[\s&lt;/span&gt;&lt;span class="sr"&gt;|:&lt;/span&gt;&lt;span class="se"&gt;\-]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;\|?&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// separator&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;headerDone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="nx"&gt;tableHtml&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;headerDone&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;td&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/td&amp;gt;`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;th&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/th&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;headerDone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;headerDone&lt;/code&gt; flag is a minimal state machine. Before processing the first data row, cells become &lt;code&gt;&amp;lt;th&amp;gt;&lt;/code&gt;. After, they become &lt;code&gt;&amp;lt;td&amp;gt;&lt;/code&gt;. The separator line is identified by a regex that matches lines containing only pipes, spaces, colons, and dashes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hunter: SQL
&lt;/h2&gt;

&lt;p&gt;The SQL parser is the most technically specific. It uses a capturing regex to scan for INSERT statements:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;insertRe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/INSERT&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+INTO&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;\S&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;\(([^&lt;/span&gt;&lt;span class="sr"&gt;)&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)\)\s&lt;/span&gt;&lt;span class="sr"&gt;*VALUES&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;\(([^&lt;/span&gt;&lt;span class="sr"&gt;)&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)\)&lt;/span&gt;&lt;span class="sr"&gt;/gi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;insertRe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;`"'&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;vals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^'|'$/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;vals&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;exec&lt;/code&gt; method called in a loop advances the regex cursor after each match. Column names come from the first match's first capture group. Values come from every match's second group.&lt;/p&gt;

&lt;p&gt;Single quotes around values are stripped. Escaped single quotes (&lt;code&gt;''&lt;/code&gt;) are converted back to single quotes. The result is clean cell values, one row per INSERT.&lt;/p&gt;

&lt;h2&gt;
  
  
  Auto-Detection for File Loads
&lt;/h2&gt;

&lt;p&gt;When a file is loaded instead of pasted, the format is detected from the extension. For &lt;code&gt;.txt&lt;/code&gt; files and unknowns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;tableHtml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseTextInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;tableHtml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseCsvInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;tableHtml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseTextInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tabs win over commas. If neither is present, the tab-delimited parser handles it anyway, which will at least produce a single-column table from the line breaks.&lt;/p&gt;

&lt;p&gt;Seven formats. One interface. The parsers are the wall between "raw text someone pasted" and "a table you can edit."&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://github.com/carnworkstudios/TAFNE" rel="noopener noreferrer"&gt;github.com/carnworkstudios/TAFNE&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>html</category>
    </item>
    <item>
      <title>Why We Treat HTML as a CAD Format for PDF (And Why It Works)</title>
      <dc:creator>Bonzai2Carn</dc:creator>
      <pubDate>Tue, 30 Jun 2026 04:00:00 +0000</pubDate>
      <link>https://dev.to/bonzai2carn/why-we-treat-html-as-a-cad-format-for-pdf-and-why-it-works-1j8f</link>
      <guid>https://dev.to/bonzai2carn/why-we-treat-html-as-a-cad-format-for-pdf-and-why-it-works-1j8f</guid>
      <description>&lt;p&gt;Most PDF-to-HTML tools stop at extraction. You get a dump of text, maybe some tables, and a "download HTML" button. That's the end of the story.&lt;/p&gt;

&lt;p&gt;We didn't stop there. And the reason is simple: extracted HTML is not a document you're done with. It's a document you're about to edit.&lt;/p&gt;




&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;a href="https://ginexys.com/app/pdf" rel="noopener noreferrer"&gt;PDF Processor&lt;/a&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/carnworkstudios/doc-extractor" rel="noopener noreferrer"&gt;Repo&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The problem with "extracted output"
&lt;/h2&gt;

&lt;p&gt;When you extract a PDF, you get a structural snapshot of the original. That snapshot is close to what you want, but rarely exactly what you want. Tables have merged cells that should be split. Headings got classified as paragraphs. A two-column layout that made sense in print looks wrong on a screen. A numbered list starts at 3 because the PDF had a callout box in between.&lt;/p&gt;

&lt;p&gt;Most tools hand you this output and say: open it in Word, fix it there.&lt;/p&gt;

&lt;p&gt;That's a context switch. Every context switch is friction. Friction compounds.&lt;/p&gt;




&lt;h2&gt;
  
  
  HTML is already a spatial document format
&lt;/h2&gt;

&lt;p&gt;Here's what most people don't realize: HTML rendered in a browser is a box model. Every element (every heading, paragraph, table, callout) is a box with dimensions, position, and CSS-computed layout. The browser calculates all of this automatically.&lt;/p&gt;

&lt;p&gt;That box model is essentially a CAD coordinate system. You already have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Positioned containers (zones, columns, regions)&lt;/li&gt;
&lt;li&gt;Reflowable layout (CSS Grid)&lt;/li&gt;
&lt;li&gt;Semantic element types (headings, paragraphs, lists, tables)&lt;/li&gt;
&lt;li&gt;A full editing surface (contenteditable)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What was missing was the interaction layer to treat it like one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two modes, one surface
&lt;/h2&gt;

&lt;p&gt;The Doc tab in Ginexys PDF Processor has two modes on the same surface:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edit Mode:&lt;/strong&gt; the existing contenteditable surface. Click into text, type, use the formatting toolbar. The browser handles all the text editing mechanics. This is what you use when you're making content corrections.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Selection Mode:&lt;/strong&gt; a layout editing layer. Click "Select" in the toolbar. Now every extracted zone and region gets a drag handle. You can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Drag zones to reorder sections of the page&lt;/li&gt;
&lt;li&gt;Drag individual regions (headings, paragraphs, tables) within or across zones&lt;/li&gt;
&lt;li&gt;Marquee-select multiple regions and group them into a new zone&lt;/li&gt;
&lt;li&gt;Right-click any element and choose "Edit Code" to see and edit its raw HTML in a Monaco editor&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Switch back to Edit Mode with the same button. The two modes share the same DOM, with no conversion and no re-render.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why not absolute positioning?
&lt;/h2&gt;

&lt;p&gt;The obvious CAD metaphor is Figma: drag elements freely, place them anywhere. We explicitly chose not to do this.&lt;/p&gt;

&lt;p&gt;The reason is that our output is HTML, and HTML in a browser is a flow document. Absolute positioning breaks that. An absolutely-positioned element is outside the flow: it doesn't affect other elements, doesn't respond to container resizes, and doesn't export correctly to Markdown or XML or DOC.&lt;/p&gt;

&lt;p&gt;Drag-to-reorder in document flow is more useful than drag-to-anywhere. You're reorganizing a document, not designing a poster.&lt;/p&gt;




&lt;h2&gt;
  
  
  Edit Code: the escape hatch
&lt;/h2&gt;

&lt;p&gt;Every extracted element has a "Edit Code" option in the right-click menu. This opens a Monaco editor dialog with the element's raw &lt;code&gt;outerHTML&lt;/code&gt;. You can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add a CSS class&lt;/li&gt;
&lt;li&gt;Change the tag from &lt;code&gt;&amp;lt;h4&amp;gt;&lt;/code&gt; to &lt;code&gt;&amp;lt;h3&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Rewrite a paragraph's content entirely&lt;/li&gt;
&lt;li&gt;Fix a table cell that parsed incorrectly&lt;/li&gt;
&lt;li&gt;Add an attribute&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Click Apply. The element is replaced in the live DOM. The change propagates to the Monaco source editor and the Visual Diff tab automatically.&lt;/p&gt;

&lt;p&gt;This is the escape hatch that makes the higher-level tools trustworthy. If the drag handles can't express what you need, the code editor can.&lt;/p&gt;




&lt;h2&gt;
  
  
  The export chain closes the loop
&lt;/h2&gt;

&lt;p&gt;Selection Mode and Edit Code aren't decorative. Every change you make in the Doc tab flows through the same sync coordinator (&lt;code&gt;applyHtmlEverywhere&lt;/code&gt;) that the Monaco source editor uses. When you export:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HTML:&lt;/strong&gt; the edited DOM, with images inlined as base64&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Markdown:&lt;/strong&gt; converted from the live DOM structure (real GFM: pipe tables, &lt;code&gt;###&lt;/code&gt; headings, &lt;code&gt;- bullets&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;XML:&lt;/strong&gt; semantic tree (&lt;code&gt;&amp;lt;heading level="3"&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;table&amp;gt;&amp;lt;row&amp;gt;&amp;lt;cell&amp;gt;&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DOC:&lt;/strong&gt; Office Open XML envelope, opens in Word/LibreOffice/Google Docs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDF:&lt;/strong&gt; browser print dialog, scoped to the Doc content&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What you see is what you export.&lt;/p&gt;




&lt;h2&gt;
  
  
  SiaS: the tool works without the service
&lt;/h2&gt;

&lt;p&gt;This is the SiaS (Software-in-a-Service) model applied. The offline tool, which includes geometry extraction, Doc editing, Selection Mode, Edit Code, and all five export formats, works entirely without an account, without a server, without any network connection.&lt;/p&gt;

&lt;p&gt;The AI layer (Docling-powered extraction, GINEX schema analysis) sits on top. It makes the tool smarter. But the tool is already useful without it.&lt;/p&gt;

&lt;p&gt;The CAD layer is part of the base tool. It always will be.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Ginexys PDF Processor is available at ginexys.com. Free, offline, no account required.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>html</category>
      <category>architecture</category>
      <category>webdev</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Debugging Mobile Drag &amp; CSS Specificity in a Real-Time PDF Diff Tool</title>
      <dc:creator>Bonzai2Carn</dc:creator>
      <pubDate>Sat, 27 Jun 2026 14:30:00 +0000</pubDate>
      <link>https://dev.to/bonzai2carn/debugging-mobile-drag-css-specificity-in-a-real-time-pdf-diff-tool-1509</link>
      <guid>https://dev.to/bonzai2carn/debugging-mobile-drag-css-specificity-in-a-real-time-pdf-diff-tool-1509</guid>
      <description>&lt;p&gt;&lt;strong&gt;TLDR:&lt;/strong&gt; Three UI problems, three different root causes. The column detection fix was algorithmic. The mobile drag was an axis-detection oversight. The CSS specificity bug was a cascade law I already know but applied wrong under time pressure.&lt;/p&gt;




&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;a href="https://ginexys.com/app/pdf" rel="noopener noreferrer"&gt;PDF Processor&lt;/a&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/carnworkstudios/doc-extractor" rel="noopener noreferrer"&gt;Repo&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What We Set Out To Do
&lt;/h2&gt;

&lt;p&gt;Four tasks entered this session:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fix raiko-aistats: 0 column splits on all 9 pages of a two-column LaTeX PDF.&lt;/li&gt;
&lt;li&gt;Fix visual-diff mobile drag: stacked layout (&amp;lt; 1024px) used horizontal &lt;code&gt;clientX&lt;/code&gt; even though the divider was now vertical.&lt;/li&gt;
&lt;li&gt;Add touch support to the compare diff resizer: mouse-only, no mobile drag.&lt;/li&gt;
&lt;li&gt;Redesign the diff tab chrome: two rows of controls eating 82px before any content appeared.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The column detection fix was already covered in its own post-mortem. This one is about everything else.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Mobile Drag Problem
&lt;/h2&gt;

&lt;p&gt;The visual-diff layout switches to &lt;code&gt;flex-direction: column&lt;/code&gt; at 1024px. The original &lt;code&gt;initDividerResize()&lt;/code&gt; always read &lt;code&gt;clientX&lt;/code&gt; and called &lt;code&gt;outerWidth()&lt;/code&gt; on the first pane. Neither is meaningful when the axis is vertical.&lt;/p&gt;

&lt;p&gt;The fix sounds simple: detect which axis the layout is using. The trap was how to detect it. You cannot use a window width check because the breakpoint is a CSS media query and can be overridden. The correct source of truth is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;getComputedStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$layout&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nx"&gt;flexDirection&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;column&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Computed style reads what CSS actually applied, not what the JavaScript thinks the breakpoint should be. This check runs at drag start, not at init, so it handles viewport resizes between page load and drag attempt.&lt;/p&gt;

&lt;p&gt;The same pattern drives cursor choice: &lt;code&gt;row-resize&lt;/code&gt; vs &lt;code&gt;col-resize&lt;/code&gt;, and whether to write &lt;code&gt;flex: 0 0 ${topPct}%&lt;/code&gt; to height or width.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Diff Tab Redesign
&lt;/h2&gt;

&lt;p&gt;Two problems with the original design:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two separate rows of controls (mode tabs + a toolbar row) consumed ~82px before any content.&lt;/li&gt;
&lt;li&gt;The layout and precision controls were visually grouped but semantically separated.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The redesign collapses everything into a single 36px bar. Three pill groups (Rich/Plain, Split/Unified, Word/Char) sit left-aligned in a flex row. Stats (N added, N removed) sit right-aligned. The pill group uses a container background with a raised active-pill shadow, which is the standard segmented control pattern.&lt;/p&gt;

&lt;p&gt;This required no HTML restructuring of the diff panels themselves. Only the chrome above them changed.&lt;/p&gt;




&lt;h2&gt;
  
  
  The CSS Specificity Disaster
&lt;/h2&gt;

&lt;p&gt;After the redesign, &lt;code&gt;#view-diff&lt;/code&gt; started rendering on top of every other tab. The panels were supposed to be &lt;code&gt;display: none&lt;/code&gt; when inactive. The diff panel was always visible.&lt;/p&gt;

&lt;p&gt;Initial read of the user's report: "It's not the height. It's either you rename it view-diff where the name that is being referred to is probably compare-diff or something like that."&lt;/p&gt;

&lt;p&gt;That sentence is about an ID mismatch hypothesis. I checked the IDs. They matched. The real culprit was in the CSS cascade.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;.view-panel&lt;/code&gt; rule sets &lt;code&gt;display: none&lt;/code&gt; on all panels. A later rule &lt;code&gt;.diff-layout&lt;/code&gt; set &lt;code&gt;display: flex&lt;/code&gt;. These are both single-class selectors with equal specificity. Source order breaks the tie. &lt;code&gt;.diff-layout&lt;/code&gt; appears later in the file. It wins. Every &lt;code&gt;.view-panel.diff-layout&lt;/code&gt; element gets &lt;code&gt;display: flex&lt;/code&gt; whether it is the active tab or not.&lt;/p&gt;

&lt;p&gt;The fix is two characters wide: add &lt;code&gt;.view-panel&lt;/code&gt; to the diff-layout rule.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Before: overrides display:none for all panels */&lt;/span&gt;
&lt;span class="nc"&gt;.diff-layout&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;flex-direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* After: only sets flex direction, never fights display:none */&lt;/span&gt;
&lt;span class="nc"&gt;.view-panel.diff-layout&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;flex-direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two-class specificity (0,2,0) beats the single-class &lt;code&gt;.view-panel&lt;/code&gt; rule (0,1,0), so the active-tab rule wins when it needs to. The inactive tabs keep &lt;code&gt;display: none&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Failed
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Wrong hypothesis first.&lt;/strong&gt; The user's wording pointed toward an ID mismatch. I checked IDs first. That was the wrong tree. The cascade investigation was second. In hindsight: "always visible" is a specificity smell, not a naming smell.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;display: flex&lt;/code&gt; on a layout helper class.&lt;/strong&gt; Adding layout properties to a semantic class that gets applied alongside &lt;code&gt;view-panel&lt;/code&gt; is the setup for this exact problem. A layout class should set layout properties (direction, wrap, gap). It should not set &lt;code&gt;display&lt;/code&gt; unless it is the element that owns the display decision. &lt;code&gt;.view-panel&lt;/code&gt; owns the display decision here. &lt;code&gt;.diff-layout&lt;/code&gt; does not.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Survived
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Pill group segmented controls are a permanent pattern in this codebase now.&lt;/li&gt;
&lt;li&gt;Axis-detecting drag via &lt;code&gt;getComputedStyle&lt;/code&gt; is the correct approach for responsive dividers.&lt;/li&gt;
&lt;li&gt;Touch support via &lt;code&gt;{ passive: false }&lt;/code&gt; and &lt;code&gt;e.touches?.[0] ?? e&lt;/code&gt; is now consistent across both dividers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The session closed with a clean build. The four items that entered finished as fixed.&lt;/p&gt;

</description>
      <category>css</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>ux</category>
    </item>
    <item>
      <title>Under the Hood: Drag, Touch, and CSS Cascade in a Real Diff UI</title>
      <dc:creator>Bonzai2Carn</dc:creator>
      <pubDate>Thu, 25 Jun 2026 14:30:00 +0000</pubDate>
      <link>https://dev.to/bonzai2carn/under-the-hood-drag-touch-and-css-cascade-in-a-real-diff-ui-1b66</link>
      <guid>https://dev.to/bonzai2carn/under-the-hood-drag-touch-and-css-cascade-in-a-real-diff-ui-1b66</guid>
      <description>&lt;p&gt;&lt;strong&gt;TLDR:&lt;/strong&gt; Three interconnected UI problems reveal how layout-aware drag detection, unified touch/mouse event handling, and CSS specificity interact when you redesign a panel that lives inside a visibility-toggled tab system.&lt;/p&gt;




&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;a href="https://ginexys.com/app/schema" rel="noopener noreferrer"&gt;Schema Editor&lt;/a&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/carnworkstudios/schema-editor" rel="noopener noreferrer"&gt;Repo&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Responsive Drag Problem
&lt;/h2&gt;

&lt;p&gt;The visual-diff layout uses &lt;code&gt;flex-direction: row&lt;/code&gt; on wide screens and &lt;code&gt;flex-direction: column&lt;/code&gt; on mobile (&amp;lt; 1024px). The divider between the two panes needs to do different things depending on which axis is active.&lt;/p&gt;

&lt;h3&gt;
  
  
  Naive approach and why it fails
&lt;/h3&gt;

&lt;p&gt;A window-width check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerWidth&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* vertical */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* horizontal */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This breaks if the user resizes the window after the divider was initialized. It also breaks if the breakpoint is overridden by a more specific CSS rule. Window width is not the source of truth here. CSS is.&lt;/p&gt;

&lt;h3&gt;
  
  
  Correct approach: read computed flex direction
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isStacked&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;getComputedStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$layout&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nx"&gt;flexDirection&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;column&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;getComputedStyle&lt;/code&gt; returns the resolved value after all cascades and media queries have applied. Reading it inside &lt;code&gt;startDrag()&lt;/code&gt; means it reflects the layout at the moment the user puts a finger or pointer on the divider, not the layout at page load.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unified event position extraction
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getEventPos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;touches&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;isStacked&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientY&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientX&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mouse events and touch events have the same coordinate fields once you extract the first touch from the list. The optional chaining &lt;code&gt;?.[0]&lt;/code&gt; safely returns &lt;code&gt;undefined&lt;/code&gt; for mouse events, which then falls through to &lt;code&gt;?? e&lt;/code&gt; (the event itself). This is equivalent to a ternary but shorter and avoids importing lodash or a touch-helper library.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dimension tracking
&lt;/h3&gt;

&lt;p&gt;At drag start, capture the current first-pane dimension:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;$first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$layout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.vd-pane&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;startSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;isStacked&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;$first&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;outerHeight&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;$first&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;outerWidth&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During drag, compute the new size clamped to a min and max to prevent panes from collapsing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;totalH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$layout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;outerHeight&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;totalH&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;startSize&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;topPct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newH&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;totalH&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;$panes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;css&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;flex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`0 0 &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;topPct&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;%`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;$panes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;css&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;flex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`0 0 &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;topPct&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;%`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using &lt;code&gt;flex: 0 0 N%&lt;/code&gt; instead of &lt;code&gt;width: N%&lt;/code&gt; or &lt;code&gt;height: N%&lt;/code&gt; works on both axis orientations because the flex shorthand sets the flex-basis. The browser maps flex-basis to the main axis automatically, using width in row layouts and height in column layouts.&lt;/p&gt;




&lt;h2&gt;
  
  
  Touch Event Registration
&lt;/h2&gt;

&lt;p&gt;The dragging pattern requires three event pairs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Mouse&lt;/th&gt;
&lt;th&gt;Touch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Start&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mousedown&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;touchstart&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Move&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mousemove&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;touchmove&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;End&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mouseup&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;touchend&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Why &lt;code&gt;{ passive: false }&lt;/code&gt; matters
&lt;/h3&gt;

&lt;p&gt;The browser defaults &lt;code&gt;touchmove&lt;/code&gt; to passive to enable smooth scrolling. A passive listener cannot call &lt;code&gt;e.preventDefault()&lt;/code&gt;. Without &lt;code&gt;preventDefault()&lt;/code&gt; on touchmove, the browser scrolls the page instead of running the drag handler.&lt;/p&gt;

&lt;p&gt;Registering touch events with &lt;code&gt;{ passive: false }&lt;/code&gt; tells the browser this listener may prevent default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;$divider&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;touchstart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;startDrag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;passive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;touchmove&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;doDrag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;passive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;touchend&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;endDrag&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: &lt;code&gt;touchend&lt;/code&gt; does not need &lt;code&gt;{ passive: false }&lt;/code&gt; because we never prevent default on it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why touchmove goes on document, not the divider
&lt;/h3&gt;

&lt;p&gt;If the user's finger moves faster than the browser can process drag events, the pointer position can leave the divider element. If the listener is only on the divider, you lose the event mid-drag. Attaching &lt;code&gt;touchmove&lt;/code&gt; and &lt;code&gt;touchend&lt;/code&gt; to &lt;code&gt;document&lt;/code&gt; ensures the drag completes correctly even if the pointer drifts off the handle.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Diff Tab CSS Architecture
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The 36px diff bar
&lt;/h3&gt;

&lt;p&gt;The original diff chrome had two rows: a tab row (Rich/Plain) and a toolbar row (Split/Unified, Word/Char, stats). Total height: ~82px.&lt;/p&gt;

&lt;p&gt;The redesign collapses this into a single &lt;code&gt;div.diff-bar&lt;/code&gt; at &lt;code&gt;height: 36px&lt;/code&gt;. The bar is a flex container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.diff-bar&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;align-items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;justify-content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;space-between&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;36px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;flex-shrink&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--toolbar-bg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nl"&gt;border-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--border-dark&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;flex-shrink: 0&lt;/code&gt; prevents the bar from compressing when the diff workspace below it is larger than the available height. &lt;code&gt;height: 36px&lt;/code&gt; is an explicit ceiling, not min-height, because the bar should never grow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pill group segmented control
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.diff-pill-group&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;align-items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--border&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.diff-pill&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;22px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;transparent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--text-muted&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;11px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;font-weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;500&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;background&lt;/span&gt; &lt;span class="m"&gt;.1s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt; &lt;span class="m"&gt;.1s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.diff-pill.active&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--surface&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--accent-dark&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nl"&gt;box-shadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="m"&gt;3px&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;.10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The container &lt;code&gt;background: var(--border)&lt;/code&gt; serves as the "track" color. The active pill lifts out of it with &lt;code&gt;background: var(--surface)&lt;/code&gt; and a shadow. This is the same pattern Apple uses for segmented controls in UIKit: it reads as a single control, not a group of buttons.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Specificity Bug
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Setup
&lt;/h3&gt;

&lt;p&gt;The tab visibility system works like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* line ~645 in styles.css */&lt;/span&gt;
&lt;span class="nc"&gt;.view-panel&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* active panel overrides per JS */&lt;/span&gt;
&lt;span class="nc"&gt;.view-panel.active&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The diff layout helper class was added to make the diff panel a column-direction flex container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* line ~888 in styles.css: WRONG */&lt;/span&gt;
&lt;span class="nc"&gt;.diff-layout&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;flex-direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why it overwrote &lt;code&gt;display: none&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;CSS specificity score for &lt;code&gt;.view-panel&lt;/code&gt; is (0, 1, 0). CSS specificity score for &lt;code&gt;.diff-layout&lt;/code&gt; is (0, 1, 0). Equal specificity. Source order breaks the tie. &lt;code&gt;.diff-layout&lt;/code&gt; appears later in the file. It wins. Every element with class &lt;code&gt;diff-layout&lt;/code&gt; gets &lt;code&gt;display: flex&lt;/code&gt; regardless of any earlier &lt;code&gt;display: none&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* CORRECT */&lt;/span&gt;
&lt;span class="nc"&gt;.view-panel.diff-layout&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;flex-direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two-class specificity score is (0, 2, 0). This beats the single-class &lt;code&gt;.view-panel&lt;/code&gt; rule when both apply. More importantly: removing &lt;code&gt;display: flex&lt;/code&gt; from this rule means it never fights the visibility system at all. &lt;code&gt;.view-panel.diff-layout&lt;/code&gt; now only sets direction, which does nothing unless the element is already displayed.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;.view-panel.active&lt;/code&gt; rule (also (0, 2, 0)) fires when JS adds the active class and sets &lt;code&gt;display: flex&lt;/code&gt; correctly. Source order then resolves the two (0, 2, 0) rules in favor of &lt;code&gt;.active&lt;/code&gt; because it was written after &lt;code&gt;.view-panel.diff-layout&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The lesson
&lt;/h3&gt;

&lt;p&gt;Layout helper classes should not set &lt;code&gt;display&lt;/code&gt;. The element that controls visibility owns the &lt;code&gt;display&lt;/code&gt; property. A class that sets layout direction on a visibility-toggled element must either match specificity exactly or remove &lt;code&gt;display&lt;/code&gt; from its rule.&lt;/p&gt;

</description>
      <category>css</category>
      <category>webdev</category>
      <category>uidesign</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Most PDF Extractors Use the Wrong API: Here’s What We Built Instead</title>
      <dc:creator>Bonzai2Carn</dc:creator>
      <pubDate>Tue, 23 Jun 2026 14:30:00 +0000</pubDate>
      <link>https://dev.to/bonzai2carn/most-pdf-extractors-use-the-wrong-api-heres-what-we-built-instead-5dgh</link>
      <guid>https://dev.to/bonzai2carn/most-pdf-extractors-use-the-wrong-api-heres-what-we-built-instead-5dgh</guid>
      <description>&lt;p&gt;&lt;strong&gt;TLDR:&lt;/strong&gt; PDF.js exposes three data sources at three fidelity levels. The industry default is the one that was built as a convenience wrapper for the other two. This is not laziness, because there are real reasons it happened, but it is the root cause of why most frontend PDF extraction breaks on academic papers, publications, and anything that isn't a corporate report.&lt;/p&gt;




&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;a href="https://ginexys.com/app/pdf" rel="noopener noreferrer"&gt;PDF Processor&lt;/a&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/carnworkstudios/doc-extractor" rel="noopener noreferrer"&gt;Repo&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The Hierarchy Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;When people say "PDF extraction," they mean &lt;code&gt;getTextContent()&lt;/code&gt;. Text items, positions, advance widths. This is what pdfplumber, PyMuPDF, pdf-parse, and almost every browser-side PDF tool reads.&lt;/p&gt;

&lt;p&gt;Here is what &lt;code&gt;getTextContent()&lt;/code&gt; actually is: a derived, post-processed view of &lt;code&gt;getOperatorList()&lt;/code&gt;. PDF.js collects text paint operators from the raw operator stream, applies the current CTM, and packages the results. It is not reading a different part of the PDF. It is giving you a processed version of data that is already available in a more complete form.&lt;/p&gt;

&lt;p&gt;Above that: &lt;code&gt;getStructTree()&lt;/code&gt;. Not derived from the paint stream at all. It reads the logical structure tree from the PDF cross-reference table. Tables, paragraphs, headings, figures, formulas. Every glyph run tagged with its semantic role, linked to the paint stream via Marked Content IDs.&lt;/p&gt;

&lt;p&gt;The hierarchy is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;getStructTree()     # what the document means
getOperatorList()   # what the document draws
getTextContent()    # a filtered view of what the document draws
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most tools use the third one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Happened
&lt;/h2&gt;

&lt;p&gt;There are real reasons &lt;code&gt;getTextContent()&lt;/code&gt; became the default:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is good enough for 80% of documents.&lt;/strong&gt; Corporate reports, legal briefs, and simple technical manuals have straightforward text flows. &lt;code&gt;getTextContent()&lt;/code&gt; gives you positioned text items and that is enough to reconstruct paragraphs and headers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The struct tree is frequently wrong.&lt;/strong&gt; Word exports tag table cells as &lt;code&gt;&amp;lt;P&amp;gt;&lt;/code&gt;. InDesign creates arbitrary nesting that reflects layer creation order, not reading order. A tool that trusts the struct tree on arbitrary input will fail on a significant fraction of documents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The MCID join is not automatic.&lt;/strong&gt; PDF.js does not give you "text item → struct tree node" in one call. You have to walk the operator list, maintain a MCID stack at each &lt;code&gt;BMC&lt;/code&gt;/&lt;code&gt;BDC&lt;/code&gt; open/close, record the current MCID for each text paint op, and join that to the struct tree. That is non-trivial to implement correctly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Toolchain inertia.&lt;/strong&gt; PDFBox, pdfminer, and the other foundational tools are 10–15 years old. They prioritized the text content API. Everything built on top of them inherited the same priority.&lt;/p&gt;

&lt;p&gt;These are valid reasons. They are also not the same as "getTextContent() is correct."&lt;/p&gt;




&lt;h2&gt;
  
  
  What You Miss
&lt;/h2&gt;

&lt;p&gt;When you use only &lt;code&gt;getTextContent()&lt;/code&gt;, you miss:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Table structure.&lt;/strong&gt; The struct tree gives you &lt;code&gt;Table → TR → TD&lt;/code&gt; directly. &lt;code&gt;getTextContent()&lt;/code&gt; gives you positioned text items that happen to be inside table cells. You have to infer the table grid from item positions, which requires heuristics, thresholds, and fails on borderless tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Display-math blocks.&lt;/strong&gt; LaTeX equation environments produce glyph runs that PDF.js collapses into single items in &lt;code&gt;getTextContent()&lt;/code&gt;. The full equation arrives as one item whose width spans the display block. Individual characters are not surfaced. Trying to detect column boundaries on a LaTeX paper using item X-extents will find that display equations bridge every candidate column gap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Column geometry.&lt;/strong&gt; Multi-column layouts in publishing tools often include explicit vertical rules, which are path operators drawing a line at the column boundary. These are in &lt;code&gt;getOperatorList()&lt;/code&gt;. They are not in &lt;code&gt;getTextContent()&lt;/code&gt;. Column detection from text positions is an inference. Column detection from an explicit vertical rule at the same X is a fact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reading order.&lt;/strong&gt; &lt;code&gt;getTextContent()&lt;/code&gt; returns items in paint order, not reading order. For a 2-column document, that might be reading order, or it might not, depending on how the PDF was authored. The struct tree, for well-tagged documents, returns leaves in reading order by design.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Cascade Is Not Optional
&lt;/h2&gt;

&lt;p&gt;The correct architecture is a cascade:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Try &lt;code&gt;getStructTree()&lt;/code&gt;. If table regions are present, extract them directly. No column detection needed.&lt;/li&gt;
&lt;li&gt;Try &lt;code&gt;getOperatorList()&lt;/code&gt; geometry: full-height vertical rules, clip stack. If column rules are present, use them directly. No text-based inference needed.&lt;/li&gt;
&lt;li&gt;Fall through to &lt;code&gt;getTextContent()&lt;/code&gt; with geometric inference (bipartite partition, stream detection). This is correct for untagged documents with minimal path geometry.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is not three times the work. Tiers 1 and 2 are fast exits. If the struct tree has tables, you skip all the geometry inference for those zones. If a vertical rule is present, you skip the bipartite algorithm. The fallback (Tier 3) only runs when no higher-fidelity signal is available, which is most documents today, but not most well-authored documents.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Uncomfortable Part
&lt;/h2&gt;

&lt;p&gt;Running the cascade as a diagnostic on three test PDFs found that all three PDFs fall through to Tier 3. No struct tree, no vertical column rules, in any of them.&lt;/p&gt;

&lt;p&gt;This could be read as: the cascade doesn't help for documents people actually use.&lt;/p&gt;

&lt;p&gt;The correct reading is: the test suite is three PDFs, and all three happen to be untagged. Amazon earnings releases, Siemens engineering manuals, and LaTeX preprints produce no struct tree output by default. But a PDF exported from Microsoft Word with the "Create bookmarks" option, or from Adobe Acrobat with the accessibility features enabled, or from InDesign with the tagging export, all produce struct trees.&lt;/p&gt;

&lt;p&gt;The cascade will be exercised when the document population expands. The diagnostic confirms the fallback is correct. The architecture is in place. The next step is the display-math filter: two lines in the fallback scan that make the LaTeX failure case work without touching anything else.&lt;/p&gt;

</description>
      <category>pdf</category>
      <category>javascript</category>
      <category>architecture</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why Splitting a 2,500-Line File Broke Our Architecture</title>
      <dc:creator>Bonzai2Carn</dc:creator>
      <pubDate>Sat, 20 Jun 2026 14:30:00 +0000</pubDate>
      <link>https://dev.to/bonzai2carn/why-splitting-a-2500-line-file-broke-our-architecture-2lc1</link>
      <guid>https://dev.to/bonzai2carn/why-splitting-a-2500-line-file-broke-our-architecture-2lc1</guid>
      <description>&lt;h3&gt;
  
  
  TLDR
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;h2&gt;
  
  
  A single 2500-line &lt;code&gt;index.html&lt;/code&gt; with all JS inline worked. Splitting it into modules surfaced three classes of bugs: arrow functions with broken &lt;code&gt;$(this)&lt;/code&gt;, initialization order errors from circular-looking imports, and implicit state dependencies that were invisible inside a shared closure. The bugs were not created by the split. They were revealed by it.
&lt;/h2&gt;
&lt;/blockquote&gt;




&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;a href="https://ginexys.com/app/schema" rel="noopener noreferrer"&gt;Schema CAD Editor&lt;/a&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Assumption That Seemed Reasonable
&lt;/h2&gt;

&lt;p&gt;A large single-file codebase is a starting point. You iterate fast, everything is in one place, there are no import errors, no build steps. When the codebase is ready for production, you split it into proper modules. Splitting is a cleanup task, not a risky refactor.&lt;/p&gt;

&lt;p&gt;This assumption is correct about the first part: monolith development is fast. It is wrong about the second: splitting is not cleanup. It is a refactor that must handle every bug the monolith hid.&lt;/p&gt;

&lt;h2&gt;
  
  
  When It Failed
&lt;/h2&gt;

&lt;p&gt;The first failure was the trace wire button. The &lt;code&gt;click&lt;/code&gt; handler used an arrow function with &lt;code&gt;$(this)&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#traceWireBtn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mode&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// arrow function: 'this' is not the button&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setMode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the monolith, &lt;code&gt;$(this)&lt;/code&gt; in an arrow function refers to the outer &lt;code&gt;this&lt;/code&gt;, which was the window-level module object. The &lt;code&gt;data('mode')&lt;/code&gt; attribute happened to exist on the module object from a previous assignment. The handler worked.&lt;/p&gt;

&lt;p&gt;After the split, the module object no longer had &lt;code&gt;data('mode')&lt;/code&gt;. The attribute was on the button element. &lt;code&gt;mode&lt;/code&gt; was &lt;code&gt;undefined&lt;/code&gt;. The trace wire button silently did nothing.&lt;/p&gt;

&lt;p&gt;The second failure was accordion panels. Their toggle logic was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.accordion-header&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.accordion-body&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slideToggle&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="c1"&gt;// 'this' needed to be the clicked header&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This used a regular function correctly. But the CSS for &lt;code&gt;.active&lt;/code&gt; state was in a different section of the monolith that was moved to a separate CSS file during the split. The toggle added &lt;code&gt;.active&lt;/code&gt; to the element; the CSS for &lt;code&gt;.active&lt;/code&gt; was in a file that was not loaded at that point in the HTML. The panels visually appeared broken.&lt;/p&gt;

&lt;p&gt;The third failure was theme initialization. The dark mode toggle set a class on &lt;code&gt;document.body&lt;/code&gt;. After the split, the toggle module loaded before the theme initialization module. The theme module read &lt;code&gt;localStorage.getItem('theme')&lt;/code&gt; and set the class. The toggle module read the current class from &lt;code&gt;document.body&lt;/code&gt; to set its initial state. Because initialization order was not guaranteed, the toggle sometimes read the class before the theme module set it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Was Actually Wrong
&lt;/h2&gt;

&lt;p&gt;Shared closure scope in the monolith masked three categories of problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;this&lt;/code&gt; binding:&lt;/strong&gt; Arrow functions used &lt;code&gt;$(this)&lt;/code&gt; and happened to work because the outer &lt;code&gt;this&lt;/code&gt; contained the expected data. The coincidence ended when modules changed what &lt;code&gt;this&lt;/code&gt; referred to.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Load order:&lt;/strong&gt; The monolith was a single script block. Everything initialized in order. Modules loaded in any order the HTML specified. Implicit dependencies on load order became explicit failures.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CSS scope:&lt;/strong&gt; CSS was inline in the same file as the JS. After extraction to separate CSS files, rules needed to be included in the right order. Two rules in the monolith with accidental order-dependency broke when they were in different files with different load positions.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What Got Deleted
&lt;/h2&gt;

&lt;p&gt;The monolith itself. 2500 lines of HTML/CSS/JS became 11 files: 2 CSS files and 9 JS modules organized into &lt;code&gt;core/&lt;/code&gt;, &lt;code&gt;canvas/&lt;/code&gt;, and &lt;code&gt;features/&lt;/code&gt; directories.&lt;/p&gt;

&lt;p&gt;The deletion also cleared the test surface for the three bug classes: the broken arrow functions, the initialization race, and the CSS load-order issues were all visible and fixable once isolated into their own files.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Replaced It
&lt;/h2&gt;

&lt;p&gt;Module files with explicit exports and imports. Each module owns its state. Imports are explicit. The initialization order is determined by a top-level init function in &lt;code&gt;core/svgEditor.js&lt;/code&gt; that calls each module's init in the correct sequence.&lt;/p&gt;

&lt;p&gt;The arrow function handlers were converted to regular &lt;code&gt;function&lt;/code&gt; declarations. The CSS was loaded in a fixed order by the HTML. The initialization race was resolved by explicit sequencing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Lesson
&lt;/h2&gt;

&lt;p&gt;A monolith does not hide bugs by preventing them. It hides them by providing the environment they need to not manifest. When the environment changes (a module split), the bugs become visible. Splitting is not the cause. The delay is.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>refactorit</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building a High-Performance CAD Engine in Vanilla JavaScript (No Frameworks)</title>
      <dc:creator>Bonzai2Carn</dc:creator>
      <pubDate>Thu, 18 Jun 2026 14:30:00 +0000</pubDate>
      <link>https://dev.to/bonzai2carn/building-a-high-performance-cad-engine-in-vanilla-javascript-no-frameworks-5ie</link>
      <guid>https://dev.to/bonzai2carn/building-a-high-performance-cad-engine-in-vanilla-javascript-no-frameworks-5ie</guid>
      <description>&lt;p&gt;The modern web is built on frameworks. React, Vue, Svelte—they’ve made building UI easier, but they’ve also made us comfortable with a certain amount of "abstraction tax."&lt;/p&gt;

&lt;p&gt;When I started building the &lt;strong&gt;Schema Editor&lt;/strong&gt;, a browser-native tool for electrical and architectural schematics, I hit a wall with that tax almost immediately. &lt;/p&gt;

&lt;p&gt;CAD tools are different from standard CRUD apps. You aren't just clicking buttons; you're manipulating thousands of SVG elements, running real-time pathfinding algorithms, and handling complex coordinate transformations (Tilt, Yaw, Perspective) all at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottleneck of Re-rendering
&lt;/h2&gt;

&lt;p&gt;In a framework-based app, state changes trigger a re-render. If I move a component in a diagram, I have to update its position, recalculate all its connected "Manhattan" wires, and re-draw the selection handles. &lt;/p&gt;

&lt;p&gt;Doing this through a virtual DOM or a reactive dependency graph adds milliseconds of latency per frame. On a complex diagram, that’s the difference between 60fps and a stuttering mess.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Direct DOM &amp;amp; Specialized Kernels
&lt;/h2&gt;

&lt;p&gt;I decided to build the engine with &lt;strong&gt;Zero Dependencies&lt;/strong&gt;. Just pure Vanilla JS and the SVG DOM.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Direct Manipulation&lt;/strong&gt;: Instead of waiting for a framework to batch updates, we update SVG attributes (&lt;code&gt;x&lt;/code&gt;, &lt;code&gt;y&lt;/code&gt;, &lt;code&gt;d&lt;/code&gt;) directly in the mousemove handler. &lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Spatial Indexing&lt;/strong&gt;: To handle "hit detection" (knowing which wire you're clicking), we don't iterate through every element. We use a custom &lt;strong&gt;KD-Tree&lt;/strong&gt; spatial index that allows us to query the canvas in &lt;code&gt;O(log n)&lt;/code&gt; time.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Geometry Pipeline&lt;/strong&gt;: We built a 4-phase geometry pipeline that handles everything from coordinate snapping to 3D perspective transformations before the data even touches the DOM.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;The result is an editor that feels like a native desktop application. It’s light (under 100KB gzipped), starts instantly, and works perfectly on mobile browsers where CPU resources are limited.&lt;/p&gt;

&lt;p&gt;It also makes the code incredibly approachable for contributors. You don’t need to learn a specific framework’s lifecycle or build system to add a new symbol to our &lt;strong&gt;Electrical&lt;/strong&gt;, &lt;strong&gt;Software&lt;/strong&gt;, or &lt;strong&gt;Construction&lt;/strong&gt; kits. You just need to know JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  Join the Project
&lt;/h2&gt;

&lt;p&gt;Schema Editor is free and open source. We're building a tool that respects the performance requirements of engineering while embracing the accessibility of the web.&lt;/p&gt;

&lt;p&gt;Check out the code and the live demo here:&lt;br&gt;
&lt;a href="https://github.com/carnworkstudios/schema-editor" rel="noopener noreferrer"&gt;github.com/carnworkstudios/schema-editor&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>performance</category>
    </item>
    <item>
      <title>Stop Parsing PDFs at Render Time: A Better Architecture for Structured Extraction</title>
      <dc:creator>Bonzai2Carn</dc:creator>
      <pubDate>Tue, 16 Jun 2026 14:30:00 +0000</pubDate>
      <link>https://dev.to/bonzai2carn/stop-parsing-pdfs-at-render-time-a-better-architecture-for-structured-extraction-5fb8</link>
      <guid>https://dev.to/bonzai2carn/stop-parsing-pdfs-at-render-time-a-better-architecture-for-structured-extraction-5fb8</guid>
      <description>&lt;p&gt;&lt;strong&gt;TLDR:&lt;/strong&gt; The reason most frontend PDF extraction is wrong is that developers try to infer document structure from the rendered visual output instead of from the operator stream. De Casteljau subdivision, pixel-based column detection, and raster-scan zone boundaries are workarounds for not reading the data correctly in the first place.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://ginexys.com/app/pdf" rel="noopener noreferrer"&gt;PDF Processor&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Industry Default Is Backwards
&lt;/h2&gt;

&lt;p&gt;Here is what most frontend PDF tools do: render the page to a canvas at some scale, read text positions from &lt;code&gt;getTextContent()&lt;/code&gt;, optionally run a computer vision model over the canvas, then try to infer whether those text positions form columns, tables, or lists based on pixel proximity.&lt;/p&gt;

&lt;p&gt;This is backwards. The PDF already contains explicit structural information in the operator stream. The table you are trying to infer from pixel columns is drawn with literal path operators: &lt;code&gt;moveTo&lt;/code&gt;, &lt;code&gt;lineTo&lt;/code&gt;, &lt;code&gt;rectangle&lt;/code&gt;, &lt;code&gt;stroke&lt;/code&gt;. The zone boundaries you are trying to detect from text Y-positions are encoded in the CTM stack and fill operations. You are trying to reconstruct from the rendered artifact what was always explicit in the source.&lt;/p&gt;




&lt;h2&gt;
  
  
  The De Casteljau Example
&lt;/h2&gt;

&lt;p&gt;I proposed de Casteljau subdivision for Bezier bounding boxes in my own architecture document and then rejected it during stress testing.&lt;/p&gt;

&lt;p&gt;De Casteljau is a subdivision algorithm. You split the curve at its midpoint, recursively subdivide both halves, and stop when the control point hull is small enough. Then you use that hull as an approximation of the bounding box.&lt;/p&gt;

&lt;p&gt;This is the right algorithm when you need to render the curve or find the nearest point on it. It is the wrong algorithm for bounding boxes because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You choose a stopping tolerance. Too loose and the bbox is wrong for diagonal curves. Too tight and you recurse deeply on every curve, including simple arcs.&lt;/li&gt;
&lt;li&gt;The segments you produce go through downstream filtering. A &lt;code&gt;minLen&lt;/code&gt; guard eats the short segments from the recursion tail. Now your bbox is even more wrong.&lt;/li&gt;
&lt;li&gt;There is no reason for any of this. The analytical solution is one application of the quadratic formula. It is exact. It does not recurse. It does not allocate segments.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The correct algorithm has been in every graphics textbook for 40 years. We still reach for subdivision because it feels intuitive: you can watch it work visually, you can tune the tolerance, it feels like you understand what it is doing. But "feels intuitive" is not a correctness argument.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Zone Boundary Example
&lt;/h2&gt;

&lt;p&gt;The page assembly design philosophy document I updated this session contains three zone detection bugs. One of them: &lt;code&gt;_detectAutoZones&lt;/code&gt; computes zone boundaries as midpoints between &lt;code&gt;yCenter&lt;/code&gt; values of adjacent region groups.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;yCenter&lt;/code&gt; of a region is the midpoint of its bounding box. The midpoint between two region &lt;code&gt;yCenter&lt;/code&gt; values is some coordinate inside the gap between them, maybe. But a region's &lt;code&gt;ry&lt;/code&gt; (used in the zone filter) is &lt;code&gt;Math.round(region.yCenter)&lt;/code&gt;, not the region's top edge. Sub-pixel rounding can place a region in the wrong zone.&lt;/p&gt;

&lt;p&gt;The fix is simple: use &lt;code&gt;bbox.y&lt;/code&gt; (the region's actual top edge) for zone boundaries, and compare against &lt;code&gt;rBboxY&lt;/code&gt; (the region's top edge rounded to pixel) in the filter. Every region's top edge is the natural zone membership criterion, because it is where the region starts.&lt;/p&gt;

&lt;p&gt;The midpoint heuristic exists because it sounds reasonable: "put the boundary halfway between the two groups." But zone membership should be determined by where content starts, not by a geometric midpoint between content centers. The former is structural. The latter is visual.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the Correct Approach Requires
&lt;/h2&gt;

&lt;p&gt;It requires reading the operator stream correctly, not just &lt;code&gt;getTextContent()&lt;/code&gt;. It requires building a CTM stack and tracking matrix state per subpath. It requires classifying subpaths geometrically (RECT vs FREE_PATH) before doing any analysis. It requires emitting canonical segments with provenance, not just (x1, y1, x2, y2) tuples.&lt;/p&gt;

&lt;p&gt;This is more work than pixel-based heuristics. But it produces deterministic output for the same PDF across renders, across scale factors, across pdfjs versions. Pixel-based output is not deterministic: it depends on anti-aliasing, subpixel positioning, font hinting.&lt;/p&gt;

&lt;p&gt;A PDF extractor that gives you different columns on the same document at 100% and 150% zoom is not extracting structure. It is pattern-matching visual artifacts and calling it structure.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Uncomfortable Implication
&lt;/h2&gt;

&lt;p&gt;If you are building a PDF extraction tool for production use and you are not parsing the operator stream, you are building a demo. You can get it to work on the 20 PDFs in your test suite. You cannot get it to work reliably on the PDFs your users will upload.&lt;/p&gt;

&lt;p&gt;The path through the operator stream is harder. It requires understanding the CTM, the fill/stroke state machine, the &lt;code&gt;constructPath&lt;/code&gt; compound op, the difference between &lt;code&gt;setFillRGBColor&lt;/code&gt; and &lt;code&gt;setFillColorN&lt;/code&gt;, the fact that &lt;code&gt;curveTo2&lt;/code&gt; and &lt;code&gt;curveTo3&lt;/code&gt; are shorthand variants with implicit control points. These are not documented anywhere obvious; you read the PDF spec and the pdfjs source.&lt;/p&gt;

&lt;p&gt;But you only have to understand it once. Then it works for every PDF, not just the ones you tested on.&lt;/p&gt;

</description>
      <category>pdf</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The Empty Quadrant: Mapping the Design Space of Frontend PDF Extraction</title>
      <dc:creator>Bonzai2Carn</dc:creator>
      <pubDate>Thu, 14 May 2026 13:30:00 +0000</pubDate>
      <link>https://dev.to/bonzai2carn/the-empty-quadrant-mapping-the-design-space-of-frontend-pdf-extraction-166g</link>
      <guid>https://dev.to/bonzai2carn/the-empty-quadrant-mapping-the-design-space-of-frontend-pdf-extraction-166g</guid>
      <description>&lt;p&gt;A user asked me a sharp question yesterday:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Looking at your extraction pipeline, pdfjs + geometryWorker + lattice + visualGridMapper, what makes this any different from any other extraction approach for frontend only, no backend or compiled engine?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's the right question to ask any author of a tool. So I sat down and surveyed the space honestly. What I found was more interesting than my gut answer.&lt;/p&gt;

&lt;p&gt;The pipeline isn't different because of clever algorithms. The lattice reconstruction is the same lattice reconstruction every server-side tool uses. The KD-tree proximity is a textbook nearest-neighbor query. Y-band paragraph clustering is in a 1996 paper. &lt;strong&gt;The math is borrowed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What's different is the &lt;em&gt;quadrant of the design space&lt;/em&gt; the pipeline occupies, and the architectural commitments it took to land there.&lt;/p&gt;

&lt;p&gt;This post maps that design space. It catalogs what's already in each cell, identifies the empty one, and explains why it stayed empty long enough for a niche to form.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The 2×2 grid
&lt;/h2&gt;

&lt;p&gt;Two axes describe almost every PDF extraction project I've encountered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Approach axis&lt;/strong&gt;: deterministic vs. ML-based.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output axis&lt;/strong&gt;: visual fidelity vs. semantic structure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plot them and you get four cells.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 DETERMINISTIC                 ML-BASED
                ───────────────                ────────
  SEMANTIC      pdfplumber, Tabula,            Adobe Extract API,
  STRUCTURE     Camelot, PyMuPDF               Textract, Azure DI,
  (backend)                                     transformers.js + layout
                                                models (frontend)

  VISUAL        pdf2htmlEX                     —
  FIDELITY      (frontend WASM)
  (frontend)

  TEXT-ONLY     pdfreader, pdf-extract,        tesseract.js
  STREAM        the naive getTextContent()     (OCR over rendered canvas)
                recipe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three observations fall out of this map immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backend dominates the deterministic-structural cell.&lt;/strong&gt; Everything serious about extracting structure from PDFs without ML lives on a server. pdfplumber, Tabula, Camelot, PyMuPDF — all Python, all backend, all decades of accumulated implementation knowledge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frontend is well-represented but compromised.&lt;/strong&gt; Each frontend project gives up something significant. &lt;code&gt;pdf2htmlEX&lt;/code&gt; reproduces visual appearance perfectly but ships zero semantic structure. &lt;code&gt;tesseract.js&lt;/code&gt; works on scanned PDFs but throws away the native text layer that digital PDFs hand you for free. The transformers.js + layout-model approach handles weird documents but ships multi-megabyte model weights and opaque failure modes. The naive &lt;code&gt;getTextContent()&lt;/code&gt; recipe and its Y-clustering descendants give you a flat blob and don't read the operator list at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There's a frontend cell that's empty.&lt;/strong&gt; Deterministic. Structural. No ML weights. No raster step. No backend.&lt;/p&gt;

&lt;p&gt;That empty cell is where this pipeline sits.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The naive 95 percent
&lt;/h2&gt;

&lt;p&gt;Before we look at what fills the four occupied cells, it's worth establishing the baseline. Roughly 95 percent of frontend PDF extraction code in the wild does this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pdf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pdfjsLib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDocument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;promise&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;numPages&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTextContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;it&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works on a memo. It collapses on a two-column research paper. It liquefies on a table. It can't tell a heading from a paragraph. It has no concept of reading order on a complex page.&lt;/p&gt;

&lt;p&gt;Everything beyond this baseline is a project trying harder. There are four serious such projects. None of them sits in the deterministic-structural-frontend cell.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. The four occupied frontend cells
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Cell A: &lt;code&gt;pdf2htmlEX&lt;/code&gt; — visual fidelity, no semantics
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;pdf2htmlEX&lt;/code&gt; is a WASM port of an old C++ project. It walks the PDF and emits absolutely-positioned &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;s that visually reproduce the source.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"position:absolute; top:124px; left:88px; font-size:11pt"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;A table cell&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"position:absolute; top:124px; left:240px; font-size:11pt"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Another&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"position:absolute; top:124px; left:392px; font-size:11pt"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Cell&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to render the PDF in a browser and let the user select text, this is unbeatable. If you want any semantic structure (a &lt;code&gt;&amp;lt;table&amp;gt;&lt;/code&gt;, an &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;, a paragraph block), you're back to scraping divs by their bounding boxes — the same problem the user started with.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cell B: &lt;code&gt;tesseract.js&lt;/code&gt; — OCR
&lt;/h3&gt;

&lt;p&gt;Render each page to canvas. Run OCR on the canvas. Get text + bounding boxes back.&lt;/p&gt;

&lt;p&gt;This is the right answer for &lt;strong&gt;scanned&lt;/strong&gt; PDFs that have no native text layer. It's the wrong answer for digital PDFs that already have perfect text. You're feeding selectable text through an image-to-text model and getting a degraded copy of what was already there. Plus a 2MB WASM payload, plus seconds-per-page latency.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cell C: &lt;code&gt;transformers.js&lt;/code&gt; + layout models — ML-based structural
&lt;/h3&gt;

&lt;p&gt;Load a layout-aware model (DocLayout-YOLO, LayoutLM, or similar) into the browser via ONNX or transformers.js. Render each page to canvas. Run inference. Get back labeled regions: &lt;code&gt;TABLE&lt;/code&gt;, &lt;code&gt;TEXT&lt;/code&gt;, &lt;code&gt;FIGURE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is where the modern industry is heading. It works on weird, varied document types. It generalizes. But:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The model weights are megabytes (DocLayout-YOLO Nano alone is ~6MB ONNX).&lt;/li&gt;
&lt;li&gt;First inference takes seconds.&lt;/li&gt;
&lt;li&gt;Failure modes are opaque — when the model misclassifies, you have no levers.&lt;/li&gt;
&lt;li&gt;You're shipping an ML inference engine to do something that, for digital PDFs, can be done with pure geometry.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cell D: &lt;code&gt;pdfreader&lt;/code&gt;, &lt;code&gt;pdf-extract&lt;/code&gt;, and friends — text-only Y-clustering
&lt;/h3&gt;

&lt;p&gt;These libraries take &lt;code&gt;getTextContent()&lt;/code&gt; items, cluster them by Y position, sort by X, and produce slightly more structured output than the flat-blob recipe.&lt;/p&gt;

&lt;p&gt;The fundamental limit: they only consume the text content. They never call &lt;code&gt;getOperatorList()&lt;/code&gt;. They cannot see vector lines. They cannot detect a table border, distinguish an underline from a horizontal rule, or recognize a chart axis. Their world is text and only text.&lt;/p&gt;

&lt;p&gt;For prose-heavy documents, that's fine. For anything with tables, they degrade to row-smashing.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. The empty cell, and what fills it
&lt;/h2&gt;

&lt;p&gt;The deterministic-structural-frontend cell asks for a tool that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Runs entirely in the browser. No server.&lt;/li&gt;
&lt;li&gt;Ships no ML model weights. Determinism via geometry.&lt;/li&gt;
&lt;li&gt;Reads the operator list, not just the text content. Vector-aware.&lt;/li&gt;
&lt;li&gt;Outputs semantic structure: tables with topology, headings, paragraphs, lists, reading order.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To fill it, this pipeline does the following:&lt;/p&gt;

&lt;h3&gt;
  
  
  4.1 CTM-baked vector segments
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ctmAdapter.js (simplified)&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;fnArray&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fnArray&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;OPS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;save&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="nx"&gt;ctmStack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fnArray&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;OPS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;restore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;ctm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ctmStack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fnArray&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;OPS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;ctm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mulMatrix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;argsArray&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fnArray&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;OPS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;constructPath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Walk subpaths, transform each point through CTM × viewport.transform,&lt;/span&gt;
    &lt;span class="c1"&gt;// emit normalized H/V segment records.&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the move that puts the pipeline in a different category from cells B and D. We don't just consume text. We consume the operator list and reconstruct the page's vector skeleton in viewport coordinates. We can &lt;em&gt;see&lt;/em&gt; the table borders before any text math runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2 Region-typed classification &lt;em&gt;before&lt;/em&gt; extraction
&lt;/h3&gt;

&lt;p&gt;Most pipelines run sequential passes: find tables, find paragraphs, find lists. Each pass works against the full text pool. Then you deduplicate at the end and hope the passes didn't disagree.&lt;/p&gt;

&lt;p&gt;This pipeline does the opposite. Classify regions first, then route scoped text into each region's specialist extractor. The mechanism is a single &lt;code&gt;assignedTextIndices&lt;/code&gt; set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lattice&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;lattices&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tableTextIndices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tm&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;textMeta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;assignedTextIndices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// skip consumed&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;insideBBox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lattice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bbox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tablePad&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;tableTextIndices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;assignedTextIndices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// mark as consumed&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;regions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TABLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lattice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;textItemIndices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tableTextIndices&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// later: paragraph/heading/list passes only see un-consumed text&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The invariant is: &lt;strong&gt;a text item belongs to exactly one region.&lt;/strong&gt; No leakage by construction. The bug class of "table text accidentally in a paragraph" is preempted, not patched.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.3 Underline-vs-border discrimination
&lt;/h3&gt;

&lt;p&gt;A naive lattice reconstructor sees every horizontal line and tries to use it as a table border. This produces phantom 1×1 tables under every underlined heading.&lt;/p&gt;

&lt;p&gt;We classify each H-segment against the text baselines using KD-tree-style proximity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;hSegs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tm&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;textMeta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;yDist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hY&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;tm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;yDist&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;yDist&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="nx"&gt;tm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vx&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;hXMax&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;tm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vWidth&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;hXMin&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="nx"&gt;hLen&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;tm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vWidth&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;2.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;underlineSegIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a horizontal line sits 0–5px below a text baseline with overlapping X-span, it's an underline. Tag it. Remove from the table-detection pool. ~99% of phantom tables disappear.&lt;/p&gt;

&lt;p&gt;I have not seen another browser-side PDF extractor that does this. Tabula has equivalents on the backend. On the frontend, every other tool I've audited just hands all H-lines to the lattice and lives with phantom tables.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.4 Topological cell-merge inference
&lt;/h3&gt;

&lt;p&gt;Naive table extractors detect cell merges by visual whitespace heuristics ("if these two cells have no visible boundary between their text, they're merged"). This is unreliable. Tables with thin internal borders look unmerged but are; tables with wide cell padding look merged but aren't.&lt;/p&gt;

&lt;p&gt;This pipeline asks the geometry directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;vLinePresent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;vLines&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;yA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;yB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;eps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;vLines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;eps&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;yMin&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;yA&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;eps&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;yMax&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;yB&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;eps&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Is there an actual merged vertical-line record at this X position spanning [yA, yB]? If yes, the cell boundary exists; the cells are separate. If no, extend the colspan. Topological, not visual.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.5 Nearest-cell Euclidean snap
&lt;/h3&gt;

&lt;p&gt;Strict point-in-box assignment drops text whose origin is 0.1px outside a cell, which is common because PDF rendering coordinates have jitter. We use Euclidean distance to the nearest cell center with a 15px snap threshold:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;bestR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bestC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;minDist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;Infinity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;ri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;ri&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;numRows&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;ri&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;ci&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;ci&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;numCols&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;ci&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ci&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;sx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sx&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ci&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ri&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;sy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sy&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ri&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dx&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;dy&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;dy&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dist&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;minDist&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;minDist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dist&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;bestR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ri&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;bestC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ci&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;minDist&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;cells&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;bestR&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;bestC&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Magnetic, not literal. Coordinate jitter doesn't drop data.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.6 Worker-isolated full pipeline
&lt;/h3&gt;

&lt;p&gt;Most browser PDF extractors run on the main thread. The geometry pipeline here loads PDF.js as a &lt;em&gt;nested worker&lt;/em&gt; inside the geometry worker. CTM baking, lattice reconstruction, classification, assembly — all off the main thread. The UI stays responsive on a 200-page document.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.7 Per-page streaming
&lt;/h3&gt;

&lt;p&gt;Naive extractors accumulate the whole document into one structured-clone payload at the end. That dies on large PDFs with stack-overflow errors in &lt;code&gt;postMessage&lt;/code&gt;. We emit per-page &lt;code&gt;'page'&lt;/code&gt; messages from the worker, the main thread accumulates incrementally, and the UI can show progressive results.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;page&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;tables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tableCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not algorithmic novelty. Engineering discipline that lets the architecture survive 76-page technical manuals.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.8 VisualGridMapper as a downstream operator
&lt;/h3&gt;

&lt;p&gt;The output isn't a dead &lt;code&gt;&amp;lt;table&amp;gt;&lt;/code&gt; string. It's a live HTML table that we can immediately remap into a Cartesian array using &lt;code&gt;VisualGridMapper&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mapper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;VisualGridMapper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// mapper.grid[row][col] now holds origin/spanned cell metadata.&lt;/span&gt;
&lt;span class="c1"&gt;// Transposes, merges, splits all become matrix operations.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the bridge into the table-formatter half of the platform. Other extractors stop at "here's a &lt;code&gt;&amp;lt;table&amp;gt;&lt;/code&gt;." We hand the user something they can keep manipulating mathematically.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. What's borrowed and what's new
&lt;/h2&gt;

&lt;p&gt;Worth being honest about which pieces of this are original engineering versus academic standard:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Borrowed:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The lattice algorithm itself — intersection clustering, row/column projection. Same as Tabula, Camelot, pdfplumber.&lt;/li&gt;
&lt;li&gt;Y-band paragraph clustering — pdfminer-style, in academic literature since the 90s.&lt;/li&gt;
&lt;li&gt;XY-cut column detection — known since the 80s.&lt;/li&gt;
&lt;li&gt;KD-tree spatial indexing — textbook.&lt;/li&gt;
&lt;li&gt;DOMPurify, jQuery, Monaco — off-the-shelf.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Original to this pipeline (or unusual in the niche):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The full assembly running in a Web Worker on top of PDF.js as a nested worker.&lt;/li&gt;
&lt;li&gt;The non-overlapping-region invariant via &lt;code&gt;assignedTextIndices&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The underline-discrimination heuristic with the specific 0–5px / 2.5×-width thresholds.&lt;/li&gt;
&lt;li&gt;The coordinate-space discipline: storing both &lt;code&gt;vWidth/vFont&lt;/code&gt; (viewport) and &lt;code&gt;width/fontSize&lt;/code&gt; (PDF points) on every text-meta record, with explicit comments about which to use where.&lt;/li&gt;
&lt;li&gt;The per-page streaming pattern that survives 100+ page documents.&lt;/li&gt;
&lt;li&gt;The integration with &lt;code&gt;VisualGridMapper&lt;/code&gt; for downstream mathematical manipulation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pipeline is a composition. The composition is the contribution.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Why this cell stayed empty
&lt;/h2&gt;

&lt;p&gt;If the deterministic-structural-frontend cell is valuable, why hadn't anyone filled it?&lt;/p&gt;

&lt;p&gt;Three reasons, in order of how convincing each one is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Economics push toward backend.&lt;/strong&gt; If you have a use case that needs structural PDF extraction, you almost certainly have a server. The serious tools live in Python and have for a decade. There's no incentive to port them unless you specifically need data to stay on the client device — which is a real but niche requirement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Existing frontend tools are anchored to other quadrants.&lt;/strong&gt; &lt;code&gt;pdf2htmlEX&lt;/code&gt; is committed to visual fidelity. &lt;code&gt;tesseract.js&lt;/code&gt; is committed to OCR. The transformers.js camp is committed to ML generalization. Each is well-architected for its quadrant and would require an architectural rewrite to drift into the deterministic-structural cell. Nobody had a reason to do that work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The pieces are scattered.&lt;/strong&gt; PDF.js gives you the operator list but assumes you'll use it for rendering. Lattice algorithms are described in papers, not packaged as npm modules. KD-tree libraries assume preformatted data. Web Worker isolation has its own ergonomic learning curve. Climbing the staircase to assemble all of these is real engineering work, and unless you have a strong reason to be in this exact cell, the cost-benefit doesn't pencil.&lt;/p&gt;

&lt;p&gt;We had a reason. The platform we're building is browser-native by &lt;em&gt;commitment&lt;/em&gt;, not accident. Every other tool in our pipeline runs in the browser. Sending PDFs to a server for structural extraction would have broken the architectural model. So we climbed.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. The lesson above the niche
&lt;/h2&gt;

&lt;p&gt;There's a generalization worth saying out loud, because it applies far beyond PDF tooling.&lt;/p&gt;

&lt;p&gt;When you wonder whether you're reinventing a wheel, do the survey. But ask the right question. The question is not &lt;em&gt;"has anyone solved this problem?"&lt;/em&gt; — the answer to that is almost always yes, somewhere. The question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What set of constraints does my version satisfy that nobody else's version satisfies?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Constraints are commitments. &lt;em&gt;No backend. No model weights. Worker isolation. Deterministic output. Per-page streaming. Open source.&lt;/em&gt; Each one is a deliberate refusal of a path other people took.&lt;/p&gt;

&lt;p&gt;The intersection of constraints is where new niches live. The math you use &lt;em&gt;inside&lt;/em&gt; that intersection is often the same math everyone else uses. That's fine. The originality isn't in the math. It's in the negative space; the things you said no to.&lt;/p&gt;

&lt;p&gt;The pipeline isn't different because the algorithms are different. It's different because of where it runs and what it refuses to be.&lt;/p&gt;




&lt;p&gt;The full pipeline is open source as part of the &lt;a href="https://github.com/carnworkstudios" rel="noopener noreferrer"&gt;GINEXYS&lt;/a&gt; project. If you find a fifth camp I missed, or if you've built something that fills the empty quadrant differently, the issue tracker is open. I'm specifically curious whether anyone else has implemented in-browser CTM baking against pdfjs-dist's operator list — that piece felt the loneliest in my survey.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>pdf</category>
      <category>architecture</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How to Stop PDF Parsers from Hallucinating Tables out of Thin Air</title>
      <dc:creator>Bonzai2Carn</dc:creator>
      <pubDate>Tue, 12 May 2026 15:54:05 +0000</pubDate>
      <link>https://dev.to/bonzai2carn/how-to-stop-pdf-parsers-from-hallucinating-tables-out-of-thin-air-n0</link>
      <guid>https://dev.to/bonzai2carn/how-to-stop-pdf-parsers-from-hallucinating-tables-out-of-thin-air-n0</guid>
      <description>&lt;p&gt;PDF extraction is usually blind. &lt;/p&gt;

&lt;p&gt;If you've ever tried to write a script to scrape a PDF, you know exactly what I mean. You run the PDF through a generic text extractor, and instead of a clean table, you get a jammed wall of text where the columns are violently shoved into a single vertical stack. &lt;/p&gt;

&lt;p&gt;Or worse, you try to use a table extractor, and it hallucinates tables everywhere. See a bold heading with an underline? The parser thinks that's a 1x1 table. See a horizontal divider between paragraphs? Boom, phantom table. &lt;/p&gt;

&lt;p&gt;Why does this happen? Because most PDF parsers process the document in a strict, sequential pipeline. They look at all the lines. They look at all the text. And they just smash them together.&lt;/p&gt;

&lt;p&gt;I got tired of this. So I re-engineered the extraction pipeline in our PDF processor to stop reading the document like a machine, and start &lt;em&gt;seeing&lt;/em&gt; it like a human.&lt;/p&gt;

&lt;p&gt;Here is the math behind Context-Aware PDF Extraction.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Blind Extraction Problem
&lt;/h2&gt;

&lt;p&gt;Previously, our extraction pipeline worked like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Find all horizontal and vertical line segments (&lt;code&gt;H-segs&lt;/code&gt; and &lt;code&gt;V-segs&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Run them through a &lt;code&gt;LatticeReconstructor&lt;/code&gt; to find intersecting grids.&lt;/li&gt;
&lt;li&gt;Treat every grid as a table.&lt;/li&gt;
&lt;li&gt;Dump all the text in the document into those grids using a strict "is this point inside this box" check.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This was a disaster for documents that mixed paragraphs with tables. &lt;/p&gt;

&lt;p&gt;If a paragraph had a decorative underline, the &lt;code&gt;LatticeReconstructor&lt;/code&gt; would see the H-line, panic, and try to build a table out of it. &lt;br&gt;
If text was slightly offset inside a table cell due to coordinate jitter, the "point-in-box" check would fail, and the text would just vanish from the output.&lt;/p&gt;

&lt;p&gt;I needed the parser to understand &lt;em&gt;context&lt;/em&gt;. &lt;/p&gt;


&lt;h2&gt;
  
  
  2. Enter the Context Classifier
&lt;/h2&gt;

&lt;p&gt;To fix this, I built the &lt;code&gt;contextClassifier&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Instead of treating the PDF as a bucket of shapes and text, the &lt;code&gt;contextClassifier&lt;/code&gt; walks the document and groups every single item into spatially bounded, typed regions: &lt;code&gt;TABLE&lt;/code&gt;, &lt;code&gt;PARAGRAPH&lt;/code&gt;, &lt;code&gt;HEADING&lt;/code&gt;, &lt;code&gt;LIST&lt;/code&gt;, and &lt;code&gt;IMAGE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But how do you tell a machine the difference between a table border and a decorative underline? &lt;/p&gt;

&lt;p&gt;You use proximity math.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// KD-tree style proximity: check if text sits exactly on top of an H-line&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;hSegs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tm&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;textMeta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;yDist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hY&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;tm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 

        &lt;span class="c1"&gt;// Underline: line is 0–5px below the text baseline&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;yDist&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;yDist&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;overlappingXSpan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;underlineSegIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a horizontal line is exactly 0 to 5 pixels below a text baseline, and its width roughly matches the text width, it's not a table border. It's an underline. &lt;/p&gt;

&lt;p&gt;By tagging and removing these underlines &lt;em&gt;before&lt;/em&gt; we run the table reconstruction, we eliminate 99% of phantom tables. &lt;/p&gt;




&lt;h2&gt;
  
  
  3. Scoping the Text (No More Collisions)
&lt;/h2&gt;

&lt;p&gt;Once the tables are detected, we calculate the exact bounding box of the table grid. &lt;/p&gt;

&lt;p&gt;Instead of throwing all the document's text at the table builder, the classifier scoops up &lt;em&gt;only&lt;/em&gt; the text items that physically live inside that bounding box.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tableTextIndices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tm&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;textMeta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;insideBBox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bbox&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;tableTextIndices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;assignedTextIndices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Mark as consumed!&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This does two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It guarantees that table text doesn't accidentally leak into paragraphs.&lt;/li&gt;
&lt;li&gt;It guarantees that paragraph text doesn't get sucked into table cells.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once a text item is claimed by a region, it's marked as consumed. &lt;/p&gt;




&lt;h2&gt;
  
  
  4. The Nearest-Cell Proximity Assignment
&lt;/h2&gt;

&lt;p&gt;Even with scoped text, getting the text into the correct table cell was still failing due to PDF rendering quirks. A cell might be at &lt;code&gt;x: 10.5&lt;/code&gt;, but the text was at &lt;code&gt;x: 10.4&lt;/code&gt;. A strict bounding box check would drop the text.&lt;/p&gt;

&lt;p&gt;I ripped out the strict containment checks and replaced them with a nearest-neighbor proximity model. &lt;/p&gt;

&lt;p&gt;For every piece of text, we find its nearest cell center using Euclidean distance. If it's within a 15px threshold, it snaps into place. No more jitter. No more dropped data.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. The Page Assembler
&lt;/h2&gt;

&lt;p&gt;Finally, the &lt;code&gt;pageAssembler&lt;/code&gt; takes over. &lt;/p&gt;

&lt;p&gt;It receives an array of perfectly classified, non-overlapping regions. It sorts them top-to-bottom based on their Y-coordinates. &lt;/p&gt;

&lt;p&gt;Then, it just iterates through them and calls the right extractor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If it's a &lt;code&gt;TABLE&lt;/code&gt;, it sends the scoped text to the &lt;code&gt;tableBuilder&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If it's a &lt;code&gt;HEADING&lt;/code&gt;, it wraps it in an &lt;code&gt;&amp;lt;h3&amp;gt;&lt;/code&gt; or &lt;code&gt;&amp;lt;h4&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If it's a &lt;code&gt;LIST&lt;/code&gt;, it strips the bullet points and outputs clean &lt;code&gt;&amp;lt;ul&amp;gt;&amp;lt;li&amp;gt;&lt;/code&gt; tags.&lt;/li&gt;
&lt;li&gt;If it's a &lt;code&gt;PARAGRAPH&lt;/code&gt;, it sends it to the &lt;code&gt;textRebuilder&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result? True document reading order. &lt;/p&gt;

&lt;p&gt;You upload a messy, complex PDF filled with tables, paragraphs, and lists. The pipeline classifies it, scopes the data, and spits out clean, semantically correct HTML. &lt;/p&gt;

&lt;p&gt;No backend processing. No AI hallucination. Just pure, deterministic math running directly in your browser using &lt;code&gt;pdfjs-dist&lt;/code&gt; and vanilla JS. &lt;/p&gt;

&lt;p&gt;The PDF is finally readable.&lt;/p&gt;

&lt;p&gt;You can find the repo at &lt;a href="https://github.com/carnworkstudios/doc-extractor" rel="noopener noreferrer"&gt;doc-extractor&lt;/a&gt; or give it a try at &lt;a href="https://ginexys.com/tools/pdf-processor" rel="noopener noreferrer"&gt;Ginexys&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>pdf</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
