Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

flong

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 pipeline

Hosting

dist/ 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.

Why it needs no server

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.

Why no WASM

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.

Layout

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

Two decisions worth not undoing

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.

Theme-file gotchas handled

Findings from scanning real extensions (see npm run smoke):

  • type is often missing — Dracula and GitHub both omit it. Inferred from uiTheme in 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 with jsonc-parser.
  • Multiple themes per extension — One Dark Pro ships 5, GitHub ships 9.
  • include chains — unresolvable from a single-file upload; detected and reported with a suggestion to upload the .vsix instead.
  • .tmTheme plists — skipped rather than mis-parsed.

Window chrome

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.

Theme-derived backgrounds

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.

Card width

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.

Language detection

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 <Capitalised pattern that signals JSX, so plain .ts reads as .tsx unless 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.

Fonts

Three tiers, in increasing order of how portable the exported result is:

  1. 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.
  2. 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.
  3. Embedded font files — drop a .woff2/.woff/.ttf/.otf; it is inlined as a data-URI @font-face and persisted to IndexedDB.

Licensing

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.

What persists between visits

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.

Privacy

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.

Share links are a deliberate non-goal

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.

Not built yet

The build-time Marketplace indexer, for a larger searchable catalogue than Open VSX alone provides.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages