This project hosts the dotfun-branded markdown editor showcased at dotfun.co/tools/markdown-studio.
It’s a static web app composed of three files:
index.html– Markup and DOM structure.styles.css– Dotfun-themed styling, including dark-mode tokens and component rules.app.js– Client-side logic (markdown parsing, live preview, copy/export hooks, footnotes, etc.).
No build tooling is required; open index.html in a browser to run the experience.
| Capability | Implementation | Notes |
|---|---|---|
| Markdown parsing + sanitisation | app.js (marked, DOMPurify) |
Configured for GFM (GitHub-Flavored Markdown). |
| Syntax highlighting | app.js highlight.js + CSS tokens |
Theme toggles between light/dark via CSS variables. |
| Task list sync | extractTaskItems, toggleTaskItem, preview change listener |
Clicking a checkbox in the preview rewrites the editor markdown so source and preview stay aligned. |
| Footnotes | preprocessFootnotes, renderFootnoteSection |
Renders references and keeps the ↩︎ link inline with content. |
| Copy actions | btnCopyHtml + btnCopyDocs handlers |
Copy HTML appends an attribution footer. Copy for Docs clones the preview, injects inline styles + attribution, writes HTML and plaintext to the clipboard. |
| Shareable links | app.js (btnShare, share helpers) |
Serialises markdown, preview settings, and theme into a compressed URL param, updates the address bar, and copies the link to the clipboard. |
| Bookmark toast | Styles in styles.css (.bookmark-callout)* + toast timers in app.js |
Toast reveals after 60s, explains keyboard shortcuts, and respects dismiss state via localStorage. |
| Preview settings modal | .settings__sheet, .settings__content, settingsPreview helpers |
Modal now mirrors live preview tokens and has its own scroll container; close button is positioned outside scroll area. |
| Dotfun feature list | index.html (.app__features) |
SEO-oriented content block after the footer. |
*The toast itself is injected and controlled by
app.js(search for"bookmarkCallout"in the script).
- The entire theme is tokenised via CSS custom properties (
styles.csstop section). Update palette values there for global changes. Preview variables are also written onto.settingsto keep the sandbox snippet in sync. - Only add new hard-coded colours if they represent new brand tokens—otherwise derive using the existing vars.
- Components use BEM-like naming (
bookmark-callout__text). Keep new rules consistent. - Dark-mode overrides should target
:is(.app[data-theme='dark'], body[data-theme='dark'])so components rendered outside the root container pick up the palette. Midnight code theme now uses--preview-code-block-textfor legibility in the settings sandbox and exports. - Bookmark callout accent colours derive from existing tokens; the dark theme now reads
--textfrom the shared token, so prefer overriding the variable instead of hard-coding colour values.
setThemeinapp.jsmirrors the active theme onto both.appandbodyviadataset.theme. This ensures sibling UI (e.g., the bookmark callout appended after the app container) inherits the correct dark-mode token set. When adding new theme-aware elements outside.app, rely on the shared CSS variables instead of duplicating colour declarations.- The bookmark toast (
.bookmark-callout) uses--text/--accenttokens for all states. Dark-mode overrides simply adjust the underlying custom property, keeping light/dark parity without duplicating component rules. - The bookmark toast intentionally waits 60 seconds before revealing. Update
BOOKMARK_TOAST_DELAYinapp.jsalongside the timing assertion intests/integration/bookmark-toast.test.cjsif you need a different cadence. - Settings dropdowns (
.settings__field select) keep the iOS-style double-linear-gradient caret. The background position is centered vertically, and the dark-theme override swaps in a warm neutral surface (rgba(41, 29, 18, 0.92)) with subtle border contrast. When adjusting form controls, tweak the token-driven background first, then the gradients for the caret if alignment shifts. - Range sliders and other inputs reuse the same accent variables; favour adjusting the tokens near the top of
styles.cssto propagate changes across the modal rather than editing individual component colours. - Local storage access is wrapped by
safeStorage,storageGet, andstorageSet. When persisting new data, use those helpers so SSR/tests can stub storage gracefully without repeating try/catch boilerplate. - Preview theme tokens now map through
PREVIEW_TOKEN_VAR_MAP+applyPreviewTokenVars. Extend the map instead of adding newsetPreviewVarcalls when introducing additional CSS custom properties. - Icons are sourced from Font Awesome 6 (loaded via CDN in
index.html). The settings button usesfa-gear, and the theme toggle swaps betweenfa-sun/fa-moonusing the existing.theme-iconvisibility rules.
- The toolbar’s Share Link button (
#btn-share) callsserializeShareStatePayloadinapp.js. The helper builds a payload withcreateShareStatePayload, which pulls preview settings directly fromDEFAULT_SETTINGS. That means newly introduced settings are automatically included in share links as soon as they are added to the defaults. - Each payload captures a schema version (
SHARE_STATE_VERSION), the active theme, the current markdown, and the normalised preview settings. Data is compressed with the URI-safe LZ codec returned bycreateShareCodec, keeping query strings compact even for longer documents. - State changes automatically schedule a new payload via
scheduleShareUrlUpdate, so the?share=...query always mirrors the latest markdown, theme, and settings. The toolbar button simply callsforceShareUrlUpdateto copy the already-current URL (with a fallback todocument.execCommand('copy')when the modern Clipboard API isn’t available). - On load,
readSharedStateFromUrlruns before hydration. If a share payload is present it seeds the preview settings, theme, and editor content before the rest of the app initialises, and then persists those values to localStorage so reloads stay in sync. - Evolving the schema: bump
SHARE_STATE_VERSIONwhen you store new fields, extendcreateShareStatePayloadto write them, and updatereadSharedStateFromUrlto interpret older payloads (e.g., supply defaults when a field is missing). Reuse existing helpers such asnormalizePreviewSettingsto validate incoming data instead of adding bespoke guards.
- Configuration / Constants – storage keys, sample document, helper for export attribution,
safeStorageutilities. - Markdown Helpers – footnote extraction, footnote rendering, task discovery.
- Preview Pipeline –
updatePreview,updateStats, highlight + code badge decorators, preview token computation. - Clipboard & Export –
copyPreviewForDocs,buildExportAttribution,flashMessage. - Event Wiring – editor input, toolbar buttons, clipboard actions, drag & drop, theme persistence.
The script is deliberately vanilla (no frameworks). If you add new functionality, keep it modular with clear helper functions.
- New Markdown Samples – adjust
sampleDocinapp.js. - New toolbar actions – add markup to
index.html, style instyles.css, wire listeners inapp.js. - Additional markdown features – configure
markedor post-process parsed HTML inupdatePreview. - Exports – tweak
buildExportAttributionor inline styles in the copy handler if target platforms (Docs, Notion, etc.) change.
Manual checklist when editing core behaviour:
- Load
index.htmlin a browser. - Toggle themes – ensure UI + toast + code blocks use correct palette.
- Use “Demo” to rehydrate sample markdown.
- Toggle checkboxes in both editor and preview – confirm they stay in sync.
- Trigger Copy HTML / Copy for Docs – paste into an HTML-aware editor and Google Docs to verify attribution + formatting.
- Resize viewport under 960px to confirm responsive layout.
- Prerequisites: Node.js 18+ and
npm installfrom the project root. - Run
npm testto execute every unit + integration suite undertests/via the Node test runner.- The command enables Node’s experimental coverage flag, so a summary table prints at the end and raw data lands in the
coverage/directory (handy for CI or IDE tooling). - DOM-heavy integration suites live in
tests/integration/, powered by a jsdom harness (tests/helpers/create-app-env.cjs) that loads the realapp.js. - Focused unit specs sit in
tests/unit/and assert colour utilities, footnote parsing, stats helpers, and preview setting clamps.
- The command enables Node’s experimental coverage flag, so a summary table prints at the end and raw data lands in the
- When triaging a failure, re-run an individual file with
node --test --experimental-test-coverage path/to/test.cjsto keep the coverage report intact.
Coverage highlights:
- Verifies theme tokens still land on both
.appandbody, and that the bookmark toast inherits the shared palette. - Guards the settings modal caret alignment, range inputs, and code-theme overrides across light/dark modes.
- Exercises the full markdown pipeline, ensuring sanitisation, code highlighting, and task-list syncing stay in lockstep.
- Confirms export payloads carry inline CSS, attribution, and rich clipboard fallbacks.
Whenever you add features, ship corresponding coverage:
- Integration specs in
tests/integration/should prove the end-to-end behaviour against the realapp.jsviacreateAppEnv. - Unit specs in
tests/unit/should lock down new helpers or utilities you introduce. - Update the README and test harness when you add new DOM affordances so the next agent—or your future self—can trace how to exercise them.
index.htmlnow sources the minified bundles (app.min.js,styles.min.css). The originalapp.js/styles.cssstay readable for development and testing.- Regenerate the bundles with
bun build app.js --minify --outfile app.min.jsandbun build styles.css --minify --outfile styles.min.css. Bun ships with this repo’s toolchain; install it viacurl https://bun.sh/installif it is not already available. - Keep
app.jsandstyles.cssas the single sources of truth. After edits, rebuild the minified outputs and commit both files so the static site keeps working without a build step.
- Consider adding unit tests around helpers (e.g., task parsing, footnotes) with a minimal Jest setup if complexity grows.