<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Proofdesk]]></title><description><![CDATA[Proofdesk]]></description><link>https://proofdesk.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sun, 25 Oct 2026 09:12:16 GMT</lastBuildDate><atom:link href="https://proofdesk.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[I built a browser IDE for publishing math textbooks — here's how the insane build pipeline works
]]></title><description><![CDATA[Most developers have never heard of PreTeXt. I hadn't either until a math professor at Vanderbilt showed me their workflow for writing an open-source…]]></description><link>https://proofdesk.hashnode.dev/i-built-a-browser-ide-for-publishing-math-textbooks-here-s-how-the-insane-build-pipeline-works</link><guid isPermaLink="true">https://proofdesk.hashnode.dev/i-built-a-browser-ide-for-publishing-math-textbooks-here-s-how-the-insane-build-pipeline-works</guid><category><![CDATA[React]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[AWS]]></category><category><![CDATA[Devops]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[Express]]></category><category><![CDATA[Docker]]></category><category><![CDATA[docker images]]></category><category><![CDATA[Docker compose]]></category><category><![CDATA[docker-volume]]></category><category><![CDATA[Redis]]></category><category><![CDATA[bullmq]]></category><category><![CDATA[#MonacoEditor]]></category><category><![CDATA[Python]]></category><category><![CDATA[playwright]]></category><category><![CDATA[GitHub]]></category><category><![CDATA[oauth]]></category><category><![CDATA[ec2]]></category><dc:creator><![CDATA[Harsha Raj Kumar]]></dc:creator><pubDate>Thu, 23 Apr 2026 06:11:11 GMT</pubDate><content:encoded><![CDATA[<p>Here's what that workflow looked like:</p>
<ol>
<li><p>Edit XML in a text editor</p>
</li>
<li><p>Open a terminal</p>
</li>
<li><p>Run a Docker build that takes 15–20 minutes</p>
</li>
<li><p>Open a browser and manually navigate to the right chapter</p>
</li>
<li><p>Realize one equation is slightly wrong</p>
</li>
<li><p>Go back to step 1</p>
</li>
</ol>
<p>No live preview. No integrated editor. No way to share a draft without giving someone SSH access. Just a loop of edit → build → check → repeat.</p>
<p>I spent the last few months building <strong>Proofdesk</strong> to fix this — a full-stack browser workspace where professors can write, build, preview, and publish math textbooks without ever leaving the browser.</p>
<p>But honestly? The interesting part isn't the React frontend or the Node backend. The interesting part is the build pipeline I had to engineer to make any of this work.</p>
<hr />
<h2>What is PreTeXt and why does anyone use it?</h2>
<p>PreTeXt is an XML-based markup language for writing math textbooks. Instead of writing raw LaTeX, you write structured XML that describes the <em>content</em> — theorems, definitions, proofs, figures — and the toolchain handles rendering.</p>
<p>The output is genuinely beautiful: properly typeset math, interactive 3D visualizations, accessible HTML, and a consistent structure that works across print and web. Georgia Tech's Introduction to Linear Algebra textbook (used by thousands of students) is written in PreTeXt.</p>
<p>The catch: the toolchain is a time capsule from 2014.</p>
<hr />
<h2>The dependency hell I walked into</h2>
<p>The PreTeXt build system was written for a very specific era. Running it in 2026 means dealing with:</p>
<ul>
<li><p><strong>Python 2</strong> — <code>unichr()</code>, <code>print</code> statements, <code>Popen</code> with <code>universal_newlines=True</code> returning bytes instead of strings</p>
</li>
<li><p><strong>Ruby 2-era gems</strong> — specifically <code>compass</code>, which is incompatible with Ruby 3.2+</p>
</li>
<li><p><strong>Inkscape 0.9x CLI syntax</strong> — the <code>--file=</code> and <code>--export-plain-svg=</code> flags were removed in Inkscape 1.x</p>
</li>
<li><p><strong>CoffeeScript 1.12.7</strong> — not compatible with CoffeeScript 2.x</p>
</li>
<li><p><strong>SCons with custom builders</strong> — a build system almost nobody uses anymore, with undocumented suffix declaration requirements that break cache-busting if you get them wrong</p>
</li>
</ul>
<p>None of this works on a modern Ubuntu system without patching. And I couldn't just fork the PreTeXt repo and fix it — professors need to pull upstream changes from the original repo without losing my fixes.</p>
<p>So I wrote a 720-line Docker build script that patches everything at build time, without permanently modifying the source.</p>
<hr />
<h2>How the build script works</h2>
<p>The approach: clone the upstream repo cleanly, apply patches in memory during the build, build the textbook, discard the patches. The source repo stays unmodified and pullable.</p>
<p>Here's what the script patches and why:</p>
<h3>Python 2 → Python 3</h3>
<pre><code class="language-bash"># unichr() was removed in Python 3
sed -i 's/unichr(/chr(/g' pretex/aglfn.py

# Inkscape 1.x dropped the old CLI flags entirely
# We rewrite the inkscape_script() method in-place
python3 - &lt;&lt;'EOF'
import re

with open('pretex/processtex.py', 'r') as f:
    src = f.read()

new_method = '''
def inkscape_script(self, pdf_file, svg_file):
    return [
        'inkscape',
        '--actions=export-type:svg;export-plain-svg;export-do',
        f'--export-filename={svg_file}',
        pdf_file
    ]
'''

src = re.sub(
    r'def inkscape_script.*?(?=\ndef |\nclass )',
    new_method,
    src,
    flags=re.DOTALL
)

with open('pretex/processtex.py', 'w') as f:
    f.write(src)
EOF
</code></pre>
<h3>Ruby 3.2 compatibility</h3>
<p><code>compass</code> uses <code>File.exists?</code> which was removed in Ruby 3.2 (it was deprecated since Ruby 2.1). Rather than fix compass, the script falls back to <code>sass-embedded</code>:</p>
<pre><code class="language-bash">if gem list compass -i &gt; /dev/null 2&gt;&amp;1; then
    compass compile
else
    # Fall back to sass-embedded — same output, modern Ruby compatible
    sass --no-source-map mathbook-assets/scss/mathbook.scss \
         mathbook-assets/css/mathbook.css
fi
</code></pre>
<h3>SCons builder suffix declarations</h3>
<p>This one took me an embarrassing amount of time. The custom <code>CatJS</code> and <code>CatCSS</code> SCons builders were missing explicit <code>suffix='.js'</code> and <code>suffix='.css'</code> declarations. Without them, SCons generates extensionless intermediate files. The <code>version_filter</code> cache-busting logic in <code>demos/SConscript</code> then can't find those files and aborts the build.</p>
<pre><code class="language-python"># Before (broken in Python 3 SCons)
def CatJS(env, target, sources):
    ...

# After (works)
def CatJS(env, target, sources):
    ...
env['BUILDERS']['CatJS'] = Builder(action=CatJS, suffix='.js')
</code></pre>
<hr />
<h2>The math rendering pipeline</h2>
<p>This is the part that makes Proofdesk worth building. Every equation in the textbook goes through this pipeline:</p>
<pre><code class="language-plaintext">PreTeXt XML
    ↓ xsltproc (XSL transforms)
HTML with &lt;script type="text/x-latex-inline"&gt;Ax = b&lt;/script&gt; placeholders
    ↓ pretex.py (extracts all LaTeX expressions)
Batch .tex files
    ↓ pdflatex
One PDF per equation
    ↓ fontforge (extracts font metrics + ToUnicode tables)
Font metric data
    ↓ Inkscape 1.x (--actions=export-type:svg;export-do)
Equation SVG files
    ↓ pretex.py (inlines SVGs back into HTML)
Final HTML with &lt;svg xmlns="..."&gt;...&lt;/svg&gt; embedded
</code></pre>
<p>The result: every equation is a crisp, perfectly-kerned SVG that renders identically at any zoom level, with no JavaScript runtime dependency. No MathJax loading delay. No font fallback artifacts. Just math that looks exactly like a printed textbook.</p>
<p>The fontforge step is what makes it possible. It extracts the exact character widths and kerning pairs from the Charter typeface so the SVG layout matches what pdflatex produced. This is the same approach academic publishers use for high-quality math rendering.</p>
<hr />
<h2>Docker-in-Docker orchestration</h2>
<p>The Proofdesk backend runs on EC2 and needs to spawn PreTeXt build containers on demand for each user session. The approach: mount the Docker socket into the backend container and use the Docker SDK for Node to create sibling containers.</p>
<pre><code class="language-javascript">const docker = new Docker({ socketPath: '/var/run/docker.sock' });

async function spawnBuildContainer(sessionId, repoPath, outputPath) {
  const container = await docker.createContainer({
    Image: 'proofdesk-pretext-builder',
    Cmd: ['build', '/repo', '/output'],
    HostConfig: {
      Binds: [
        `${repoPath}:/repo:ro`,
        `${outputPath}:/output:rw`,
      ],
      Memory: 1.5 * 1024 * 1024 * 1024, // 1.5GB cap per build
      AutoRemove: true,
    },
  });

  // Stream stdout/stderr back to the browser over WebSocket
  const stream = await container.attach({
    stream: true, stdout: true, stderr: true
  });

  stream.on('data', (chunk) =&gt; {
    wss.clients.forEach(client =&gt; {
      if (client.sessionId === sessionId) {
        client.send(JSON.stringify({ type: 'build-log', data: chunk.toString() }));
      }
    });
  });

  await container.start();
  return container;
}
</code></pre>
<p>The 1.5GB memory cap per container is critical on a t3.small with 2GB total RAM. The pretex equation renderer spawns multiple parallel pdflatex workers — without a cap, a single build OOM-kills the entire instance.</p>
<hr />
<h2>Real terminal in the browser</h2>
<p>Professors sometimes need to run SCons commands directly, inspect intermediate build artifacts, or debug a broken equation. So Proofdesk ships a real terminal — same TTY behavior as SSH, not a fake shell.</p>
<p>The stack: <code>node-pty</code> on the backend spawns a real pseudoterminal. The output streams over WebSocket to <code>xterm.js</code> on the frontend.</p>
<pre><code class="language-javascript">// Backend — spawn a real PTY
const pty = require('node-pty');

const shell = pty.spawn('bash', [], {
  name: 'xterm-color',
  cols: 80,
  rows: 24,
  cwd: sessionWorkDir,
  env: process.env,
});

shell.on('data', (data) =&gt; {
  ws.send(JSON.stringify({ type: 'terminal', data }));
});

ws.on('message', (msg) =&gt; {
  const { type, data } = JSON.parse(msg);
  if (type === 'terminal-input') shell.write(data);
  if (type === 'terminal-resize') shell.resize(data.cols, data.rows);
});
</code></pre>
<p>The resize handling matters more than you'd think. Without it, tools like <code>vim</code> and <code>less</code> render incorrectly when the browser window changes size.</p>
<hr />
<h2>Shareable preview links</h2>
<p>One of the most useful features for professors: share a draft chapter with a student or co-author without giving them GitHub access.</p>
<pre><code class="language-javascript">// POST /build/share/:sessionId
router.post('/share/:sessionId', async (req, res) =&gt; {
  const token = crypto.randomBytes(16).toString('hex'); // 32-char hex
  const expiresAt = Date.now() + 7 * 24 * 60 * 60 * 1000; // 7 days

  await redis.set(
    `share:${token}`,
    JSON.stringify({ sessionId: req.params.sessionId, expiresAt }),
    'EX', 7 * 24 * 60 * 60
  );

  res.json({ url: `\({BASE_URL}/shared/\){token}` });
});

// GET /shared/:token/* — no auth required
router.get('/shared/:token/*', async (req, res) =&gt; {
  const entry = await redis.get(`share:${req.params.token}`);
  if (!entry) return res.status(404).send('Link expired or not found');

  const { sessionId, expiresAt } = JSON.parse(entry);
  if (Date.now() &gt; expiresAt) return res.status(410).send('Link expired');

  const filePath = path.join(outputDir, sessionId, req.params[0] || 'overview.html');
  res.sendFile(filePath);
});
</code></pre>
<p>No S3, no CDN — the built output lives on the EC2 filesystem and gets served directly. Good enough for academic use at this scale.</p>
<hr />
<h2>What broke in production (and what I'm still debugging)</h2>
<p>I'll be honest: the deployed site at <a href="https://proofdesk.duckdns.org">proofdesk.duckdns.org</a> has a text overlap bug that doesn't reproduce locally. My current theory is that the Charter web fonts are being served by Nginx without the correct <code>font/woff2</code> MIME type, causing the browser to silently reject them and fall back to a system serif with different letter-spacing.</p>
<p>If you've hit this before — Nginx serving fonts from a Docker volume with wrong MIME types — I'd genuinely love to hear how you fixed it.</p>
<hr />
<h2>What's next</h2>
<ul>
<li><p><strong>Multi-textbook support</strong> — paste any PreTeXt GitHub URL and get a full workspace instantly</p>
</li>
<li><p><strong>Incremental builds</strong> — only rebuild changed chapters, cutting build time from 15 min to under 2 min</p>
</li>
<li><p><strong>Student mode</strong> — read-only workspace with a purpose-limited AI tutor scoped to the textbook content</p>
</li>
<li><p><strong>CRDT collaboration</strong> — operational-transform or CRDT-based simultaneous editing for co-authors</p>
</li>
<li><p><strong>LMS integration</strong> — Canvas/Blackboard SSO for institutional deployment</p>
</li>
</ul>
<hr />
<h2>The repo</h2>
<p>Proofdesk is fully open source: <strong>github.com/harsharajkumar/proofdesk</strong></p>
<p>If you write or maintain a PreTeXt textbook, I'd genuinely love feedback on what's missing from your workflow. And if you're a developer who's dealt with legacy Python 2 / Ruby 2 / Inkscape 0.9x build systems in Docker — you have my respect.</p>
<hr />
<p><em>I'm Harsha Raj Kumar, MS CS at Vanderbilt University, Research Assistant at VU-BEAM Lab. Building AI tools and infrastructure systems. Find me at</em> <a href="https://github.com/harsharajkumar"><em>github.com/harsharajkumar</em></a><em>.</em></p>
]]></content:encoded></item></channel></rss>