<?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>Beyond the Naive Regex: Proper PDF Font Style Extraction</title>
      <dc:creator>Bonzai2Carn</dc:creator>
      <pubDate>Tue, 14 Jul 2026 13:47:42 +0000</pubDate>
      <link>https://dev.to/bonzai2carn/beyond-the-naive-regex-proper-pdf-font-style-extraction-5e82</link>
      <guid>https://dev.to/bonzai2carn/beyond-the-naive-regex-proper-pdf-font-style-extraction-5e82</guid>
      <description>&lt;p&gt;&lt;strong&gt;TLDR:&lt;/strong&gt; Build a &lt;code&gt;fontStyleMap&lt;/code&gt; from &lt;code&gt;page.commonObjs&lt;/code&gt; in the geometry worker. Each font name resolves to &lt;code&gt;{bold, italic}&lt;/code&gt; flags from the actual parsed font descriptor. Merge onto &lt;code&gt;textMeta&lt;/code&gt; items. Check &lt;code&gt;transform[2]&lt;/code&gt; (shear component) for synthetic italic. &lt;code&gt;textRebuilder&lt;/code&gt; wraps styled runs in &lt;code&gt;&amp;lt;strong&amp;gt;/&amp;lt;em&amp;gt;/&amp;lt;u&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Repo: &lt;a href="https://github.com/carnworkstudios/doc-extractor" rel="noopener noreferrer"&gt;tools/pdf-processor&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




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

&lt;p&gt;PDF.js gives each text item a &lt;code&gt;fontName&lt;/code&gt; string. These look like &lt;code&gt;ABCDEF+TimesNewRomanPS-BoldMT&lt;/code&gt;, which is a 6-character subset prefix followed by a PostScript-style variant name.&lt;/p&gt;

&lt;p&gt;The prefix changes on every export. You cannot reliably regex-match the family before stripping it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Naive Attempt
&lt;/h2&gt;

&lt;p&gt;Strip the prefix, then regex-match the remainder:&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="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;bold&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;heavy&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;black&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;i&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;name&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;A-Z&lt;/span&gt;&lt;span class="se"&gt;]{6}\+&lt;/span&gt;&lt;span class="sr"&gt;/&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;This works for well-named fonts. It fails silently for synthetic fonts named &lt;code&gt;Font12&lt;/code&gt; or &lt;code&gt;F1&lt;/code&gt;, fonts with non-English variant names, and PDFs where the exporter normalized the font name.&lt;/p&gt;




&lt;h2&gt;
  
  
  page.commonObjs: The Authoritative Source
&lt;/h2&gt;

&lt;p&gt;PDF.js exposes parsed font objects through &lt;code&gt;page.commonObjs&lt;/code&gt;. Each font object has &lt;code&gt;.bold&lt;/code&gt; and &lt;code&gt;.italic&lt;/code&gt; boolean properties computed from the font's actual glyph metrics and descriptor, not its name. This is the ground truth.&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;fontStyleMap&lt;/span&gt; &lt;span class="o"&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;uniqueFontNames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;textContent&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;i&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fontName&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="nb"&gt;Boolean&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;fn&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;uniqueFontNames&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;obj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;commonObjs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&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;obj&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cleaned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;fn&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;A-Z&lt;/span&gt;&lt;span class="se"&gt;]{6}\+&lt;/span&gt;&lt;span class="sr"&gt;/&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;fontStyleMap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bold&lt;/span&gt;   &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="sr"&gt;/bold|heavy|black/i&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;cleaned&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;italic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;italic&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="sr"&gt;/italic|oblique|slanted/i&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;cleaned&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;The &lt;code&gt;||&lt;/code&gt; fallback: trust the font object first, fall back to the cleaned name for fonts where &lt;code&gt;.bold&lt;/code&gt; is not set by the parser.&lt;/p&gt;




&lt;h2&gt;
  
  
  Synthetic Italic: The Shear Transform
&lt;/h2&gt;

&lt;p&gt;Some PDFs produce italic-looking text by applying a shear matrix to an upright font rather than loading an actual italic variant.&lt;/p&gt;

&lt;p&gt;The text item's &lt;code&gt;transform&lt;/code&gt; array is &lt;code&gt;[a, b, c, d, e, f]&lt;/code&gt;. The &lt;code&gt;c&lt;/code&gt; component is horizontal shear. A non-zero &lt;code&gt;c&lt;/code&gt; means the glyphs are slanted.&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;syntheticItalic&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;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.01&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;syntheticItalic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;italic&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;This catches faux italic rendering that the font object alone would miss.&lt;/p&gt;




&lt;h2&gt;
  
  
  Underlines: Vector Segment Pairing
&lt;/h2&gt;

&lt;p&gt;Underlines in PDFs are separate vector line segments drawn beneath text, not a font property. &lt;code&gt;ctmAdapter.js&lt;/code&gt; classifies horizontal segments with a Y position within &lt;code&gt;~0.35× font size&lt;/code&gt; below a text item as underlines. The &lt;code&gt;textMeta&lt;/code&gt; item receives &lt;code&gt;underlined: true&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Propagating to the Renderer
&lt;/h2&gt;

&lt;p&gt;Flags flow: &lt;code&gt;geometryWorker&lt;/code&gt; → &lt;code&gt;textMeta&lt;/code&gt; → &lt;code&gt;_scopeItems&lt;/code&gt; → &lt;code&gt;textItems&lt;/code&gt; passed to &lt;code&gt;textRebuilder&lt;/code&gt;. The rebuilder groups consecutive same-style items into runs:&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;_wrapInlineStyle&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="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;)&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;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_escHtml&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;underlined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;u&amp;gt;&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="s2"&gt;&amp;lt;/u&amp;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;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;italic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;em&amp;gt;&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="s2"&gt;&amp;lt;/em&amp;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;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;strong&amp;gt;&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="s2"&gt;&amp;lt;/strong&amp;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;html&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;Nesting order: underline outermost, bold innermost, matching standard HTML precedence for browser rendering.&lt;/p&gt;




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

&lt;p&gt;A line like &lt;code&gt;WARNING Do not proceed without reading the SAFETY section&lt;/code&gt; in a PDF might produce:&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;strong&amp;gt;&lt;/span&gt;WARNING&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt; Do not proceed without reading the &lt;span class="nt"&gt;&amp;lt;em&amp;gt;&lt;/span&gt;SAFETY&lt;span class="nt"&gt;&amp;lt;/em&amp;gt;&lt;/span&gt; section
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No OCR pass. No ML font classifier. Just font metadata that was already inside the PDF.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>pdf</category>
      <category>typography</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Diagnosing a Failing PDF Extraction Pipeline</title>
      <dc:creator>Bonzai2Carn</dc:creator>
      <pubDate>Sat, 11 Jul 2026 14:30:00 +0000</pubDate>
      <link>https://dev.to/bonzai2carn/diagnosing-a-failing-pdf-extraction-pipeline-j2l</link>
      <guid>https://dev.to/bonzai2carn/diagnosing-a-failing-pdf-extraction-pipeline-j2l</guid>
      <description>&lt;p&gt;&lt;strong&gt;TLDR:&lt;/strong&gt; We stress-tested a deterministic column detection pipeline against a LaTeX academic paper. The hypothesis going in was wrong. The real failure mode was found in the data, not in the algorithm. Here is what we assumed, what we found, and where the work actually needs to happen.&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 Setup
&lt;/h2&gt;

&lt;p&gt;The PDF extraction pipeline uses a bipartite band partition algorithm to detect 2-column layouts. It was built against two test documents: an Amazon earnings release (single-column financial) and a Siemens engineering manual (2-column, rich path geometry). Both work correctly.&lt;/p&gt;

&lt;p&gt;The third document, a LaTeX academic paper, was always expected to be a harder case. But the failure mode we expected was wrong.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Wrong Hypothesis
&lt;/h2&gt;

&lt;p&gt;LaTeX PDFs have a lot of math. Math characters are typeset using font metrics where the advance width (how far the cursor moves after placing a glyph) does not match the ink width (how wide the character actually is). The assumption was:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;getTextContent()&lt;/code&gt; text items have advance-width-based widths&lt;/li&gt;
&lt;li&gt;Math glyphs have inflated advances&lt;/li&gt;
&lt;li&gt;Many math items → median font size pulled toward math sizes (~7pt)&lt;/li&gt;
&lt;li&gt;All thresholds calibrated from the body font size (PageScale S) are miscalibrated&lt;/li&gt;
&lt;li&gt;Column detection breaks because the gutter threshold is wrong&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is a reasonable hypothesis. It is also completely incorrect for this document.&lt;/p&gt;

&lt;p&gt;The diagnostic measured median S and mode S for every page. The divergence was 0.1pt. On some pages, mode was 9.5pt and median was 9.4pt. These are not meaningfully different. The calibration path is not the failure.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Real Failure
&lt;/h2&gt;

&lt;p&gt;After ruling out calibration, we looked at the actual items being processed.&lt;/p&gt;

&lt;p&gt;For a standard 2-column LaTeX paper, the column gutter is roughly at X≈310 (out of a ~620px viewport). The bipartite algorithm's fallback path, which runs when interval merge finds no clean gap, walks candidate X values and counts how many items cross each. The split point should be the X where the fewest items cross.&lt;/p&gt;

&lt;p&gt;On raiko-aistats-12.pdf, the fallback finds that every candidate X has the same high crossing count. Zero splits detected.&lt;/p&gt;

&lt;p&gt;Why? PDF.js &lt;code&gt;getTextContent()&lt;/code&gt; for this document does not expose individual math characters as separate items. Entire display-math equations arrive as single text items. A display-math block is full-width: it spans from the left column's left edge to the right column's right edge, crossing X≈310 along with X≈100, X≈150, and every other candidate.&lt;/p&gt;

&lt;p&gt;When the fallback scan runs, it counts one or two wide equation items as crossing all candidates. No candidate has a lower crossing count than any other. No split is selected.&lt;/p&gt;

&lt;p&gt;The gutter is real. The columns are real. The equations just happen to sit across the gutter and overwhelm the crossing count.&lt;/p&gt;




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

&lt;p&gt;The interval merge stage is correct. It correctly finds no clean gap because the equation items span the full page width in &lt;code&gt;getTextContent()&lt;/code&gt;: there is no gap to find in the item X-extents. The problem is not in interval merge; it is in how the fallback scan treats anomalously wide items.&lt;/p&gt;

&lt;p&gt;The calibration work (mode-S vs median-S) is still worth doing. The 0.1pt divergence on these three documents doesn't mean the fix is unnecessary. It means these three documents happen not to stress the calibration path. A document with 60% subscripts would.&lt;/p&gt;

&lt;p&gt;The three-tier architecture (getStructTree → getOperatorList → getTextContent) is correct as a long-term model. But for these three specific documents, all three tiers resolve to the same thing: Tier 3 (bipartite fallback) is the only active path. The struct tree is absent from all three. Full-height vertical column rules are absent from all three.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Actual Fix
&lt;/h2&gt;

&lt;p&gt;The fix is a pre-filter on the fallback crossing scan. Items where &lt;code&gt;vWidth &amp;gt; S * 4&lt;/code&gt; are anomalously wide relative to the body font size. They are display-math blocks, full-width images, or full-width headers, not normal paragraph text. They should not contribute to the crossing count in the fallback scan because they are not evidence about where the column boundary is.&lt;/p&gt;

&lt;p&gt;Filter them before the crossing scan runs. The surviving narrow items will have the correct gap pattern. Splits will be found.&lt;/p&gt;

&lt;p&gt;This is a two-line change in &lt;code&gt;_detectPageColumns&lt;/code&gt;. It does not touch the interval merge path, the three gates, or the bipartite structure.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the Architecture Document Changed
&lt;/h2&gt;

&lt;p&gt;Before this session: the pipeline had one active code path for all documents, calibrated by a median that is theoretically wrong for math-heavy documents.&lt;/p&gt;

&lt;p&gt;After this session: the architecture document defines a three-tier model, a diagnostic harness confirms which tier is active per document, and the specific failure mode for LaTeX papers is root-caused to item width anomalies, not calibration.&lt;/p&gt;

&lt;p&gt;The heavy restructure (structTreeReader, ctmAdapter extensions, tiered classifyPage) is still ahead. But the next actionable fix, the one that makes raiko-aistats work, is the display-math pre-filter, not the restructure.&lt;/p&gt;

</description>
      <category>pdf</category>
      <category>testing</category>
      <category>architecture</category>
      <category>javascript</category>
    </item>
    <item>
      <title>The Three-Tier PDF Extraction Model: Demystifying PDF.js</title>
      <dc:creator>Bonzai2Carn</dc:creator>
      <pubDate>Thu, 09 Jul 2026 14:30:00 +0000</pubDate>
      <link>https://dev.to/bonzai2carn/the-three-tier-pdf-extraction-model-demystifying-pdfjs-52bc</link>
      <guid>https://dev.to/bonzai2carn/the-three-tier-pdf-extraction-model-demystifying-pdfjs-52bc</guid>
      <description>&lt;p&gt;&lt;strong&gt;TLDR:&lt;/strong&gt; Every manufactured PDF (not scanned) has three fidelity levels available to a browser-side extractor: a semantic structure tree, a geometric paint stream, and a text convenience API derived from the paint stream. Most tools use only the third. The correct architecture reads them top-down and exits as soon as a tier produces a complete answer.&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=""&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=""&gt;Repo&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The API Hierarchy
&lt;/h2&gt;

&lt;p&gt;PDF.js exposes three document-reading APIs. They are not three ways of reading the same data. They are three different data sources at different levels of the PDF format.&lt;/p&gt;

&lt;h3&gt;
  
  
  getTextContent()
&lt;/h3&gt;

&lt;p&gt;This is a convenience API. It returns positioned text items: &lt;code&gt;{ str, transform, width, height }&lt;/code&gt; for every glyph run on the page. You get text, position, and a typographic advance width.&lt;/p&gt;

&lt;p&gt;The important fact: &lt;strong&gt;this API is derived from &lt;code&gt;getOperatorList()&lt;/code&gt;&lt;/strong&gt;. PDF.js processes the operator list internally, collects text paint operators (&lt;code&gt;Tj&lt;/code&gt;, &lt;code&gt;TJ&lt;/code&gt;, &lt;code&gt;'&lt;/code&gt;, &lt;code&gt;"&lt;/code&gt;), applies the current text matrix and CTM, and packages the results as text items. &lt;code&gt;getTextContent()&lt;/code&gt; does not read a different part of the PDF. It is a processed view of the paint stream.&lt;/p&gt;

&lt;p&gt;This derivation has a cost: advance widths are typographic, not ink widths. For most text, these are the same. For math, they are not. A subscript character with a large italic correction has an advance width that includes white space intended for the next character. Equation blocks composed from individual glyphs may have items whose total advance width is wider than their actual ink.&lt;/p&gt;

&lt;p&gt;More critically for extraction: PDF.js may not produce individual items for every character in a math equation. Display-math blocks from LaTeX can arrive as single items spanning the full equation width. The individual character positions are not surfaced.&lt;/p&gt;

&lt;h3&gt;
  
  
  getOperatorList()
&lt;/h3&gt;

&lt;p&gt;This is the raw paint stream. Every drawing command the PDF renderer would execute: move-to, line-to, curve-to, rectangle, fill, stroke, set-color, set-font, save, restore. The CTM stack is implicit: &lt;code&gt;q&lt;/code&gt; pushes, &lt;code&gt;Q&lt;/code&gt; pops, &lt;code&gt;cm&lt;/code&gt; multiplies into the current matrix.&lt;/p&gt;

&lt;p&gt;This is the ground truth for geometry. Table lines, box borders, background fills, and column rules are all explicit path operators here. Nothing is inferred.&lt;/p&gt;

&lt;p&gt;Text paint operators (&lt;code&gt;Tj&lt;/code&gt;, &lt;code&gt;TJ&lt;/code&gt;) appear in the operator list with the current text matrix applied. This is where &lt;code&gt;getTextContent()&lt;/code&gt; reads its data. The difference: in the operator list, each paint operation is also wrapped in &lt;code&gt;BMC&lt;/code&gt;/&lt;code&gt;BDC&lt;/code&gt;...&lt;code&gt;EMC&lt;/code&gt; marked content blocks that carry a Marked Content ID (MCID).&lt;/p&gt;

&lt;h3&gt;
  
  
  getStructTree()
&lt;/h3&gt;

&lt;p&gt;This is not derived from the paint stream. It is a separate data structure stored in the PDF cross-reference table, specifically the logical structure tree. It encodes the semantic role of every painted element: &lt;code&gt;Table&lt;/code&gt;, &lt;code&gt;TR&lt;/code&gt;, &lt;code&gt;TD&lt;/code&gt;, &lt;code&gt;TH&lt;/code&gt;, &lt;code&gt;P&lt;/code&gt;, &lt;code&gt;H1&lt;/code&gt;–&lt;code&gt;H6&lt;/code&gt;, &lt;code&gt;Figure&lt;/code&gt;, &lt;code&gt;Formula&lt;/code&gt;, &lt;code&gt;L&lt;/code&gt; (list), &lt;code&gt;LI&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Each leaf node in the structure tree carries an MCID. Each &lt;code&gt;BMC&lt;/code&gt;/&lt;code&gt;BDC&lt;/code&gt; operator in the operator list also carries an MCID. Joining the two gives you: every glyph run → its semantic role.&lt;/p&gt;

&lt;p&gt;This is the data source that most extractors never read.&lt;/p&gt;




&lt;h2&gt;
  
  
  The MCID Join
&lt;/h2&gt;

&lt;p&gt;The join between structure tree and operator list is the technical centerpiece of Tier 1.&lt;/p&gt;

&lt;p&gt;Walk the operator list once. Maintain a MCID stack: push on &lt;code&gt;BDC&lt;/code&gt;, pop on &lt;code&gt;EMC&lt;/code&gt;. When you encounter a text paint operator (&lt;code&gt;Tj&lt;/code&gt;, &lt;code&gt;TJ&lt;/code&gt;), record the current top-of-stack MCID.&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;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;opList&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;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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;opList&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;opList&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;fn&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;beginMarkedContentProps&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;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;args&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MCID&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;mcidStack&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;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MCID&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;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;fn&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;endMarkedContent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;mcidStack&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="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;fn&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;showText&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;fn&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;showSpacedText&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;currentMcid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mcidStack&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;mcidStack&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;-&lt;/span&gt; &lt;span class="mi"&gt;1&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;currentMcid&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;opIndexToMcid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&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="nx"&gt;currentMcid&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;Then walk the structure tree. Each &lt;code&gt;Table&lt;/code&gt; node contains &lt;code&gt;TR&lt;/code&gt; nodes, which contain &lt;code&gt;TD&lt;/code&gt; nodes. Each &lt;code&gt;TD&lt;/code&gt; node has an MCID. Map that MCID to the text items collected above.&lt;/p&gt;

&lt;p&gt;Result: every text item has a semantic role. Tables fall out as &lt;code&gt;TD&lt;/code&gt; nodes grouped by &lt;code&gt;TR&lt;/code&gt; grouped by &lt;code&gt;Table&lt;/code&gt;. No column detection. No stream detection. No threshold.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tier 2: What ctmAdapter Is Already Close To Providing
&lt;/h2&gt;

&lt;p&gt;The current pipeline reads the operator list via &lt;code&gt;ctmAdapter.js&lt;/code&gt;, which emits subpath records and filled rectangles. It already collects &lt;code&gt;vSegs&lt;/code&gt; (vertical segments).&lt;/p&gt;

&lt;p&gt;The missing piece: nobody checks whether any &lt;code&gt;vSeg&lt;/code&gt; spans the full content height. A vertical rule in an engineering manual or newsletter that runs from top to bottom of the content area is explicit column geometry. It is more reliable than any inference from text positions.&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;contentHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;contentBottom&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;contentTop&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;columnRules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;vSegs&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;s&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;len&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;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y2&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y1&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;midX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x2&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;return&lt;/span&gt; &lt;span class="nx"&gt;len&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;contentHeight&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.60&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;midX&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;vpWidth&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.10&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;midX&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;vpWidth&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.90&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 &lt;code&gt;columnRules.length &amp;gt; 0&lt;/code&gt;, their X positions are used directly as column splits. The bipartite algorithm is skipped entirely. A geometric fact is used as a geometric fact.&lt;/p&gt;

&lt;p&gt;Two other operator list signals are currently discarded:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Clip stack&lt;/strong&gt;: &lt;code&gt;W&lt;/code&gt;/&lt;code&gt;W*&lt;/code&gt; operators define clip regions. Some PDFs clip each column to its column rectangle. Adjacent clip regions with a gap encode a column layout without any inference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paint order&lt;/strong&gt;: a text item painted after a filled rectangle is visually on top of that rectangle. When two filled regions overlap the same text item, paint order disambiguates which region the text belongs to.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Mode-S vs Median-S
&lt;/h2&gt;

&lt;p&gt;PageScale S is the body font size, which is the calibration constant from which all thresholds are derived. Currently: &lt;code&gt;S = median(vFont)&lt;/code&gt; across all text items on the page.&lt;/p&gt;

&lt;p&gt;For LaTeX papers with subscripts, superscripts, and equation characters, the distribution of font sizes is not unimodal. There is a cluster of body-text items at 10pt and a long tail of math characters at 6–7pt. The median of this distribution is pulled toward the tail.&lt;/p&gt;

&lt;p&gt;The fix is the mode, computed on 0.5pt bins:&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;bins&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;Map&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;bin&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;round&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;vFont&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="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="nx"&gt;bins&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bins&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;)&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="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="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;modeFont&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;modeCount&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="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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;bins&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;count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;modeCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;modeCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;modeFont&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bin&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;S&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;modeFont&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mode is the body text size on any well-structured document. Subscripts are a tail in the frequency distribution, not the center of mass.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the Diagnostic Found
&lt;/h2&gt;

&lt;p&gt;Running all three test PDFs through a read-only diagnostic harness produced:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;PDF&lt;/th&gt;
&lt;th&gt;Struct tree&lt;/th&gt;
&lt;th&gt;Column rules&lt;/th&gt;
&lt;th&gt;Mode-S vs Median-S&lt;/th&gt;
&lt;th&gt;Tier 3 result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AMZN (financial)&lt;/td&gt;
&lt;td&gt;Absent&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Identical&lt;/td&gt;
&lt;td&gt;0 splits ✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;59MN7C (engineering)&lt;/td&gt;
&lt;td&gt;Absent&lt;/td&gt;
&lt;td&gt;None found (diagnostic param bug)&lt;/td&gt;
&lt;td&gt;Identical&lt;/td&gt;
&lt;td&gt;Splits detected ✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;raiko-aistats (LaTeX)&lt;/td&gt;
&lt;td&gt;Absent&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;0.1pt divergence&lt;/td&gt;
&lt;td&gt;0 splits ✗&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The tiered architecture is correct in design. For the current test documents, all three PDFs fall through to Tier 3. The struct tree is not used because it is not present. Column rules are not used because none are present.&lt;/p&gt;

&lt;p&gt;The failure on raiko-aistats is entirely within Tier 3: the fallback crossing scan is not pre-filtering anomalously wide items. That is the next fix.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Hierarchy Summary
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;getStructTree()       # semantic ground truth, MCID-joined to paint stream
getOperatorList()     # geometric ground truth, path operators + text operators
getTextContent()      # derived convenience, lossy (no MCID, typographic widths)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every well-structured PDF authored in Word, InDesign, or a publishing tool has a populated struct tree. Every manufactured PDF (not scanned) has a full operator list. &lt;code&gt;getTextContent()&lt;/code&gt; is the right tool for 80% of cases where you just need text and approximate positions.&lt;/p&gt;

&lt;p&gt;For extraction that needs to correctly classify tables, columns, and reading order across arbitrary document types, you need all three APIs and a cascade strategy for which one to trust.&lt;/p&gt;

</description>
      <category>pdf</category>
      <category>architecture</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <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>
  </channel>
</rss>
