flong.dev — an impression of your code you can carry off and share. A tool from uRadical.
A flong is the paper mould used in stereotype printing: an impression taken from a forme of set type — portable, reproducible, and not the type itself. The story is on /about.html, which is a second plain HTML page rather than a route, so
dist/still needs no rewrite rules.
Turn code into a shareable image using real VS Code themes. Runs entirely in the browser — no backend, no WASM, nothing uploaded.
npm install
npm run dev # http://localhost:5173
npm run build # -> dist/
npm run smoke # end-to-end check of the theme import pipelinedist/ is a plain folder of static files with relative asset paths and no
routing, so it drops unchanged onto GitHub Pages (including project subpaths),
S3, Netlify, or anything else. There are no host config files, no rewrite rules,
and no required response headers.
Verified: serving dist/ from a nested /some-repo/ path resolves all assets.
| Need | How it works client-side |
|---|---|
| Syntax highlighting | Shiki — same TextMate grammars and theme JSON as VS Code |
| Theme catalogue | 65 bundled themes + Open VSX search (fully CORS-open) |
| Everything else | Drop a .vsix — unzipped in-browser with fflate |
| Persistence | IndexedDB (theme JSON is too big for localStorage) |
| Export | modern-screenshot, canvas + clipboard |
The Microsoft Marketplace API returns 200 to a browser but sends no
access-control-allow-origin, so it cannot be called from a page. The .vsix
drop zone is the workaround: CORS restricts this page fetching Marketplace, not
the user downloading a file themselves. That closes the catalogue gap with no proxy.
Shiki's default Oniguruma engine is a ~500KB WASM binary. The pure-JS engine is 44KB and was verified to produce byte-identical output across TypeScript, Python, Rust, C++, Ruby, Bash and Markdown, with all 35 tested grammars loading.
Beyond size, WASM would undermine host-independence: .wasm must be served as
application/wasm (plain S3 does not set this) and needs a wasm-unsafe-eval
CSP allowance. The build ships zero .wasm files.
If an exotic grammar ever fails, the engine is a single constructor argument in
src/lib/highlighter.js — swap in createOnigurumaEngine for that case.
src/
index.html
app.js wiring
state.js one state object + a change event
styles/
tokens.css :root design tokens + app shell
card.css the export target (light DOM)
base.js shared shadow-DOM styles
components/ native custom elements
code-card.js ← NO shadow root, on purpose
theme-picker.js typeahead (bundled + imported)
settings-panel.js
import-panel.js .vsix / .json drop + Open VSX search
export-bar.js
lib/
highlighter.js single Shiki instance, lazy grammars/themes
vsix.js zip -> package.json -> theme JSON
theme-normalize.js JSONC, type inference, include detection
theme-store.js IndexedDB
openvsx.js
export.js
scripts/
gen-registries.js emits the static import maps (runs during build)
smoke.mjs
code-card has no shadow root. Screenshot libraries serialise a node into an
SVG <foreignObject>, and shadow roots are their classic failure case. Shiki's
output is already fully inline-styled, so the card needs no encapsulation. Every
other component is a normal shadow-DOM custom element.
Grammars and themes are loaded from generated import maps. Vite can only
code-split a dynamic import() it can statically analyse, and
import(`@shikijs/langs/${id}`) is not analysable. scripts/gen-registries.js
emits explicit thunk maps so each grammar becomes its own lazy chunk. Add a
language by editing the LANGS array there.
Findings from scanning real extensions (see npm run smoke):
typeis often missing — Dracula and GitHub both omit it. Inferred fromuiThemein package.json, or from background luminance for a bare.json.- JSONC — most themes are strict JSON, but some (Night Owl Light) contain
comments and crash
JSON.parse. Parsed withjsonc-parser. - Multiple themes per extension — One Dark Pro ships 5, GitHub ships 9.
includechains — unresolvable from a single-file upload; detected and reported with a suggestion to upload the.vsixinstead..tmThemeplists — skipped rather than mis-parsed.
Nine styles under Window style, grouped Modern / Retro:
- Modern — macOS (traffic lights), Windows 11 (thin stroked controls), Linux (GNOME headerbar), Plain bar, None
- Retro — Windows 3.11, Windows 95, Windows XP, Mac OS 8, Mac OS 9, Solaris CDE
Mac OS 8 and 9 are both Platinum and genuinely very similar; rather than invent differences, OS 8 carries the details that actually distinguish it — the heavier frame bevel and drop shadow that 8.5 later lightened, and the collapse (windowshade) box 8.0 introduced.
Two deliberate choices:
Controls are inline SVG, never text glyphs. A unicode ✕ or ▼ would
depend on a font being present when the card is serialised for export — the same
failure mode the bundled fonts exist to avoid.
Retro frames are whole windows, not just captions. The theme colours are
handed to CSS as --theme-bg / --theme-fg on .card-window. Modern chrome
adopts them for the entire window; retro frames paint the window in their own
system colour and let only the client well carry the theme, then add the period
furniture around it — menu bar and sunken bevel on Windows, platinum scrollbar
and resize grip on Mac OS 9 (which correctly gets no menu bar, because on classic
Mac OS the menu bar belonged to the screen, not the window).
Selector gotcha: data-chrome sits on .card-window, so frame rules must
be compounded — .card-window[data-chrome="win95"]. The descendant form
[data-chrome="win95"] .card-window matches nothing and fails silently, which is
exactly how these frames once appeared to work while their borders and system
background were never applied at all.
Retro frames keep their own palette and geometry. Modern chrome tints itself
from the theme's foreground via currentColor; the retro frames do not, because
the point is period-accurate chrome wrapped around themed code. They also ignore
the corner-radius slider (FIXED_RADIUS in state.js) — a bevelled Windows 95
frame with rounded corners is simply wrong.
The default background is From theme: the backdrop gradient is generated from the active theme's own palette, so Dracula gets its pink, Synthwave '84 gets orange-to-magenta, and an imported theme nobody has ever seen still looks deliberate. This is the one thing a fixed set of designed themes cannot do.
Accents come from the theme's workbench colours in priority order
(activityBarBadge.background, progressBar.background, textLink.foreground,
…), falling back to the most saturated token colours when a theme's chrome is all
greys.
The hard constraint is that the card must not sink into a backdrop derived from
its own editor.background. Because hue changes perceived luminance a lot — a
pale yellow-green at 86% lightness is far brighter than a warm orange at the same
value — the separation is measured, not assumed: each gradient stop is nudged
until it clears a real contrast ratio against the card. npm run backgrounds
checks this across 24 themes; the worst case is currently 1.32.
Min width is a floor, not a fixed width. Narrow snippets stop shrink-wrapping into a cramped column, while long lines still expand the card rather than scrolling inside a fixed box — horizontal scroll would be invisible in an export. The slider's leftmost position reads Auto and removes the floor entirely.
The language picker defaults to Auto, which re-detects on every edit and shows
what it settled on (Auto · python). Choosing a language explicitly turns
detection off and it stays off; choosing Auto again hands control back and
immediately re-detects.
src/lib/detect-language.js is a weighted heuristic, not a dependency —
highlight.js's auto-detect means shipping a second full highlighter and VS Code's
ML model is several megabytes. It scores ~28 languages and is correct on all 24
samples in the test above.
Two traps worth keeping in mind if you extend the rules:
- A generic type parameter (
Promise<User>) matches the same<Capitalisedpattern that signals JSX, so plain.tsreads as.tsxunless real JSX evidence (className=, a closing tag,return (<) is also required. - That JSX evidence then matches
</html>in real HTML, so the JSX branch is additionally gated on the snippet already scoring as JavaScript-family.
Three tiers, in increasing order of how portable the exported result is:
- Bundled, self-hosted — JetBrains Mono (default), Fira Code, Geist Mono, Hack. Freely redistributable, so every export format travels correctly. Plus the always-available system-mono stacks.
- Detected system fonts — "Find my fonts". Uses
queryLocalFonts()on Chromium (permission prompt, behind a click), falling back to canvas width probing everywhere else — no prompt, works in every browser. Verified on macOS: probing found JetBrains Mono, Fira Code, Hack Nerd Font, Menlo, Monaco, Andale Mono and Courier New. - Embedded font files — drop a
.woff2/.woff/.ttf/.otf; it is inlined as a data-URI@font-faceand persisted to IndexedDB.
Three of the bundled families are SIL OFL 1.1 and Hack is MIT; full texts are in
public/licenses/, linked from the footer. All four were chosen partly because
none carries a Reserved Font Name that applies here, so they can be subset
without renaming — unlike IBM Plex Mono ("Plex") or Source Code Pro ('Source'),
where subsetting would force a rename under OFL.
Worth knowing: rasterising text into a PNG is not font redistribution — it is an image of shapes, so no font licence governs it. That means the PNG path is clean regardless of which font a user picks, including proprietary system fonts like SF Mono or a commercial font added via "Embed a font file…". Only SVG and "Copy styled" embed actual font data.
Sharing caveat, which the UI states inline: a PNG rasterises text to pixels, so a system font is perfectly shareable there. SVG export and "Copy styled" keep the text as text with a font-family name, so a recipient without that font gets a fallback and a different layout. Only tier 3 is safe for those, and the export bar warns when the current selection is not.
Note that embedding a commercially licensed font (Operator Mono, Berkeley Mono, Dank Mono) into a shared SVG is redistribution. Rasterising it into a PNG is not.
Ligatures are exposed as a toggle, since they are most of the reason people install Fira Code or JetBrains Mono.
Two stores, split by size and by when they are needed:
- localStorage (
flong.prefs.v1, a few hundred bytes) — the selected theme, window style, font, and every slider. It is synchronous, so these apply before the first paint and you never see a flash of the defaults. - IndexedDB — imported theme JSON and embedded font files. Far too large for localStorage, which caps around 5MB and would fill after a handful of themes.
A selected imported theme is stored as a reference, not a copy; it is re-resolved from IndexedDB once the library loads, so it briefly falls back on a cold start.
Your code is not stored. It would be local-only and technically harmless, but silently keeping someone's source across sessions on a shared machine is not a decision to make on their behalf.
Your code never leaves your browser. No account, no upload, no server. Highlighting, rendering and export all happen locally.
The only network request is optional: searching Open VSX for a theme sends your search term. Your code is never included in it.
Verified rather than asserted — the whole source contains three fetch calls,
all in lib/openvsx.js, and no analytics or third-party scripts. A network
capture of the built dist/ across a full workflow (type, change theme, change
window style, export PNG) recorded 16 requests, all to the app's own origin and
none external. Re-testing with a sentinel string in the editor confirmed no
outbound URL ever carries editor content.
Not "unbuilt" — declined, and not planned.
A URL-hash implementation would be technically safe (fragments are never sent to a server), but it puts the user's code into a link they then paste elsewhere, and it means the privacy claim above needs a footnote. A claim that needs explaining stops being trusted. Export the PNG and share that instead.
If this is ever revisited, note what makes the current claim durable: there is no backend that could change behaviour later, and no analytics SDK that could start collecting on a version bump. Adding a link shortener or any server-side persistence would end that, and the sentence at the top of this section would have to come down.
The build-time Marketplace indexer, for a larger searchable catalogue than Open VSX alone provides.