Skip to content

About

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

Manuscript Desk

A Markdown rendering app built for the Frontend Developer assignment. Drop in a .md file and it's typeset on a paper page against a dark studio canvas, with an outline sidebar and a one-click copy that carries HTML, plain text, and the original Markdown to the clipboard at once.

Setup

npm install
npm run dev      # http://localhost:5173
npm run build    # production build to dist/
npm run preview  # serve the production build locally

No environment variables, no backend, no API keys. Everything — parsing, rendering, and the copy pipeline — runs in the browser.

Try it with the files in samples/: technical-design-review.md exercises the full GFM feature set (tables, nested lists, code blocks with a language, blockquotes, strikethrough, footnote-style references), malformed-edge-cases.md exercises the failure paths (a broken table, an unclosed code fence, a dangling link, mixed whitespace), and gfm-edge-cases.md sweeps trickier-but-valid GFM (task lists, table alignment, nested/loose/tight lists, hard line breaks, long unbroken strings) — the last one is what caught the three bugs described below.

Design & technical decisions

Concept. The brief is explicitly about rendering AI-generated documents, so I leaned into a "manuscript desk" idea: a dark studio canvas with a single warm parchment page under a spotlight. The page is the one thing that's supposed to hold your attention — everything else (toolbar, outline, canvas) stays quiet and out of the way. Fonts: Fraunces for document headings (a display serif with some personality), Source Serif 4 for document body text, IBM Plex Sans for the app chrome, and JetBrains Mono for code.

Rendering. react-markdown + remark-gfm for the required GFM feature set (tables, strikethrough, autolinks, task-list-adjacent list handling), with rehype-highlight for fenced code block syntax highlighting. Raw HTML in the source (stray <div>s, HTML comments some AI models leave in) is stripped via skipHtml rather than shown as literal text or executed — that's the graceful-degradation choice for "malformed" content that isn't in the required feature list.

Copy button. This was the trickiest requirement. One click writes three representations to the clipboard in a single ClipboardItem:

  • text/html — built with a separate unified/remark/rehype pipeline (not by reading the live DOM) that inlines CSS directly onto each element (style="..." on every heading, table cell, code block, etc.), because Word, Google Docs, and most rich-text editors ignore external/Tailwind classes on paste and only respect inline styles or a handful of semantic tags.
  • text/plain — derived by mounting that HTML in an offscreen div and reading innerText, which handles block-level line breaks and table cell spacing better than a hand-rolled regex strip.
  • text/markdown — the original source, attempted where the browser's ClipboardItem accepts a non-standard MIME type. Chrome currently rejects raw text/markdown in the constructor in my testing, so this silently drops out and the button's tooltip/status line reports exactly which formats actually made it onto the clipboard (e.g. "as HTML · Plain text") rather than claiming success it didn't deliver.

The button never throws on an unsupported browser: it retries with fewer formats, and falls back to writeText as a last resort, surfacing a "Couldn't copy" state only if every path fails.

Resilience. A RenderErrorBoundary wraps the document body so a pathological parse tree can't blank the whole app — it degrades to a message instead. Malformed constructs (an unclosed code fence, a broken table, a dangling [link() don't throw; CommonMark's own error-tolerant grammar handles most of it by treating the input literally, which I verified against a dedicated edge-case file rather than assuming.

A real bug this caught. While sweeping a wider set of GFM edge cases (task lists, deeply nested lists, long unbroken strings, table alignment) I found that remark-gfm injects its own className onto certain nodes — task-list-item on checkbox list items, contains-task-list on their parent <ul> — and my custom renderers were spreading {...props} after setting className, so that incoming className silently replaced mine instead of merging with it. The visible symptom: checked/ unchecked task items rendered with no styling at all, falling back to inherited white text on the parchment background — nearly invisible. Every custom component now explicitly merges classNames. I also found and fixed a related layout bug (a nested list inside a task item rendering on the same line as its parent) and a text-overflow bug (long unbroken strings breaking out of the page). All three are the kind of thing that looks fine on a "normal" test document and only shows up once you write markdown specifically trying to break each feature — which is what samples/gfm-edge-cases.md is for.

Accessibility, checked rather than assumed. Beyond the usual (focus rings, landmarks, reduced motion), I ran automated checks rather than eyeballing it: WCAG contrast ratios for every text/ background pairing (one, the "faint" hint-text color, failed at 2.94:1 and was retuned to 4.75:1), full keyboard tab order through both the empty and loaded states, heading-level sequencing (no skipped levels, which matters for screen-reader users navigating by heading), and that image alt text survives the render pipeline.

Accessibility & responsiveness. Visible focus rings everywhere, prefers-reduced-motion respected, semantic landmarks (header, main, aside with aria-label), the outline collapses into a toggled drawer under lg, and the toolbar/paper padding scale down for mobile rather than just shrinking text.

Use of AI coding assistants

Built with Claude (Sonnet) as a pair programmer: scaffolding the Vite/Tailwind/react-markdown setup, writing the inline-style clipboard pipeline, and iterating on the visual design. I used it to generate a real test document (samples/technical-design-review.md) and a deliberately broken one (samples/malformed-edge-cases.md) to verify rendering and the copy pipeline end-to-end — including actually driving a headless browser to click the copy button and read back the clipboard contents, which caught that Chrome silently rejects the text/markdown MIME type in ClipboardItem, and that raw HTML comments in source markdown were rendering as visible garbage text before skipHtml was added.

What I'd improve with more time

  • Bundle size. highlight.js's full language grammar set pulls the JS bundle over Vite's 500KB warning threshold. I'd switch to lowlight's common subset or lazy-load languages on demand.
  • Chrome's custom clipboard formats. Chrome 104+ supports arbitrary MIME types on the clipboard under a "web <type>" prefix (a different mechanism than the plain text/markdown I attempt first). I'd add that as a second attempt so Chrome-to-Chrome copy/paste between two instances of this app could round-trip the original Markdown exactly.
  • GitHub-style alert callouts (> [!NOTE], > [!WARNING]) aren't in the required feature list, so they currently render as plain blockquotes with the literal [!NOTE] text — a small remark plugin would turn these into proper styled callouts.
  • Large file handling. Documents in the tens of thousands of words would benefit from virtualized rendering; right now the whole document mounts at once.
  • Automated tests. Given more runway I'd add a small Vitest + Testing Library suite around the clipboard pipeline and the outline extraction, since those are the two places with the most "correct-looking but subtly wrong" failure modes.

About

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors