Notepad Neo

Engineering

Notes from building a browser text editor

Notepad Neo is a rich-text editor built on one contenteditable div, with no editor framework and no runtime dependencies. Word and PDF export are written from scratch — a ZIP writer, an OOXML emitter and a PDF byte writer, all in the browser.

These are write-ups of the parts that turned out to be harder than expected. Each one describes something that actually broke in this app, quotes the code that ships today, and says plainly where the approach runs out.

What each write-up covers

Which modules of the editor each article covers The editor is built from a contenteditable paper element surrounded by five subsystems. The bidirectional text module and the toolbar sit on the input side; the storage adapter persists state; and the DOCX and PDF writers read the document out. Each of the five articles maps onto one of these subsystems. #editor-paper one contenteditable div bidi.ts per-block direction, 197 lines toolbar.ts font size, blocks, alignment docx/ — 785 lines zip.ts · ooxml.ts · index.ts pdf/ — ~1,300 lines sandbox · measure · writer storage.ts localStorage → IndexedDB INPUT SIDE OUTPUT SIDE
No document model sits between the DOM and anything else — which is what makes both exporters simple and the editing operations hard. Every article below is downstream of that one decision.
  1. · updated · 12 min read

    Why dir="auto" breaks a mixed-language editor

    A note that opens with the word "Hello" keeps every Arabic paragraph below it left-aligned — forever. Why the Unicode first-strong rule has to run per block, which code points count as strong, and the seven things that broke on the way there.

    • Unicode bidi
    • contenteditable
    • CSS logical properties
    • UAX #9
  2. · updated · 13 min read

    Generating a PDF in the browser by letting the browser do the layout

    PDF has no layout engine, so every glyph needs a coordinate. Measuring an offscreen clone instead of reimplementing line breaking, probing for baselines the DOM will not report, and the encoding wall that sends some documents to Print.

    • PDF
    • Layout measurement
    • Text encoding
    • Range API
  3. · updated · 13 min read

    Writing a .docx in the browser with no dependencies

    A .docx is a ZIP of XML parts. A 114-line ZIP writer, strict OOXML child ordering whose failure mode is an unopenable file, and why every character property needs a second complex-script twin.

    • OOXML
    • ZIP format
    • File export
    • ECMA-376
  4. · updated · 11 min read

    Changing font size in contenteditable without HierarchyRequestError

    range.surroundContents() throws on any selection that crosses an element boundary. The deprecated execCommand trick that replaces it, and the five follow-on bugs it creates.

    • contenteditable
    • execCommand
    • Selection API
    • CSS cascade
  5. · updated · 11 min read

    What happens when localStorage runs out

    Autosave into a 5 MiB cap fails silently and the user finds out when they close the tab. Why the fallback triggers on the failed write rather than on a size estimate, and the self-healing demotion nobody designed.

    • localStorage
    • IndexedDB
    • Local-first
    • Storage quotas