<?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[ExtensionGuy Dev Blog]]></title><description><![CDATA[Engineering notes on building offline, privacy-first browser extensions: Pandoc WebAssembly, Word to LaTeX conversion, and local-first tools.]]></description><link>https://extensionguy.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sat, 07 Nov 2026 11:17:18 GMT</lastBuildDate><atom:link href="https://extensionguy.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How I Run Pandoc's WebAssembly Build Inside a Browser Extension (Offline Word-to-LaTeX)]]></title><description><![CDATA[How I run Pandoc's WebAssembly build in a Manifest V3 extension to convert Word to LaTeX offline, using a Web Worker and no uploads.]]></description><link>https://extensionguy.hashnode.dev/how-i-run-pandoc-s-webassembly-build-inside-a-browser-extension-offline-word-to-latex</link><guid isPermaLink="true">https://extensionguy.hashnode.dev/how-i-run-pandoc-s-webassembly-build-inside-a-browser-extension-offline-word-to-latex</guid><category><![CDATA[WebAssembly]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[latex]]></category><category><![CDATA[privacy]]></category><dc:creator><![CDATA[graphic designer]]></dc:creator><pubDate>Fri, 06 Nov 2026 09:04:31 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ae316888d53904dd05a9d96/05b032d7-4187-4a2a-bcdb-788c2f87b6be.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The problem</p>
<p>I once lost hours reformatting a Word manuscript into LaTeX, and I didn't want to upload an unpublished paper to a random converter website. Pandoc handles this conversion very well, but it means installing a command-line tool, which most people writing papers won't do.</p>
<p>So I wanted Pandoc's engine with no install, no upload, and no server. The result is Word to LaTeX (Offline), a browser extension that runs Pandoc locally through WebAssembly.</p>
<p>Why WebAssembly instead of a server Privacy: the document never leaves your machine, so there is nothing to log, retain, or leak. Offline: once installed, it works with no network connection. Same engine as the CLI: this is real Pandoc, not a JavaScript re-implementation, so the output matches what you'd get on the command line. Pandoc in the browser</p>
<p>Pandoc is written in Haskell. GHC, the Haskell compiler, can now target WebAssembly (wasm32-wasi), and Pandoc releases from 3.9 onward include an official WASM build. I use that build (Pandoc 3.10) rather than maintaining my own toolchain, which keeps updates simple: when a new Pandoc version ships, I swap the binary and rerun my test documents.</p>
<p>The tradeoff is size. The binary is large by web standards, tens of megabytes uncompressed, so I bundle it inside the extension package. Users download it once at install time instead of on every conversion, and conversions start without network latency.</p>
<p>Architecture</p>
<p>The flow is:</p>
<p>The user drops a .docx file or pastes a table. The page reads the file into an ArrayBuffer. The bytes are sent to a Web Worker that hosts the Pandoc WASM runtime. Pandoc converts DOCX to LaTeX entirely in memory. The LaTeX string is posted back and shown in the UI.</p>
<p>Running Pandoc in a worker is important. Conversion is CPU-heavy, and doing it on the main thread would freeze the interface on large manuscripts.</p>
<p>A simplified version of the messaging layer:</p>
<p>js // worker.js (simplified) self.onmessage = async (event) =&gt; { const { id, bytes, standalone } = event.data; try { // Wrapper around the Pandoc WASM runtime: docx in, LaTeX out. const latex = await runPandoc(bytes, { from: "docx", to: "latex", standalone, // true = complete document, false = fragment }); self.postMessage({ id, ok: true, latex }); } catch (err) { self.postMessage({ id, ok: false, error: String(err) }); } }; js // page side (simplified) const worker = new Worker("worker.js");</p>
<p>function convert(file, standalone = false) { return new Promise(async (resolve, reject) =&gt; { const id = crypto.randomUUID(); const bytes = await file.arrayBuffer(); const onMessage = (e) =&gt; { if (<a href="http://e.data.id">e.data.id</a> !== id) return; worker.removeEventListener("message", onMessage); e.data.ok ? resolve(e.data.latex) : reject(new Error(e.data.error)); }; worker.addEventListener("message", onMessage); worker.postMessage({ id, bytes, standalone }, [bytes]); }); }</p>
<p>Transferring the ArrayBuffer instead of copying it avoids duplicating large files in memory.</p>
<p>Running WASM inside a Manifest V3 extension</p>
<p>Manifest V3 is stricter about code execution, but WebAssembly bundled with the extension is allowed. The default extension CSP permits it, so loading a .wasm file from the package works without relaxing security or requesting extra permissions. The extension needs no special permissions, and every file stays inside the browser sandbox.</p>
<p>Cleaning up Pandoc's output</p>
<p>Raw Pandoc LaTeX is correct but not always paste-ready. For Overleaf and TeXstudio I post-process the output:</p>
<p>Remove \tightlist, which depends on a macro that isn't defined in a fresh document. Unwrap \pandocbounded{} wrappers so the body is plain LaTeX. In fragment mode, output only the body so it pastes into an existing document without a duplicate preamble. "Complete document" mode produces a standalone file.</p>
<p>Tables are written with booktabs and longtable, so they look right and span pages.</p>
<p>Beyond DOCX: pasted tables and OCR</p>
<p>Pasted tables go through the same local conversion path. I also added an experimental image-to-LaTeX mode using Tesseract.js, also running locally, for typed single-line equations. It is limited: radicals are often misread, matrices are unreliable, and handwriting isn't supported. I tested it on an 80-sample set (97.5% compiled), but that measures whether the output compiles, not whether it is correct. Always review OCR output.</p>
<p>Check the "offline" claim yourself</p>
<p>Don't take my word for it. Open DevTools, switch to the Network tab, convert a document, and confirm that no requests leave your machine. The code is on GitHub so you can audit it.</p>
<p>Licensing</p>
<p>Pandoc is GPL-2.0 or later. I publish the engine's license and attribution here, and the website code is open source.</p>
<p>Try it Online converter (no install) Chrome Edge</p>
<p>If a manuscript doesn't convert cleanly, open an issue with the document structure and I'll look into it.</p>
]]></content:encoded></item></channel></rss>