monospace-pretext is a repo for turning proportional typography into monospace-like typography in two different ways:
- a browser-side preview effect powered by
@chenglou/pretext - a real font-generation pipeline powered by Python
fontTools,ufoLib2, andufo2ft
Those two parts solve different problems.
- The browser package is for fast visual experiments. It keeps the original font family and rewrites page text into equal-width grapheme cells.
- The Python pipeline is for producing actual font files. It rewrites glyph metrics and outlines, then emits generated fonts and proof artifacts.
If you want a strange, live browser effect, use the JS API. If you want a real font file, use the CLI.
The JS package exports:
monospacePage()monospaceElement()createPretextMeasurer()
It uses Pretext to measure rendered grapheme widths in the active font, then wraps text nodes in fixed-width cells so a proportional font behaves like a fake monospace font inside the browser.
This is not font engineering. It is a DOM transformation.
The Python CLI can generate a draft monospaced font directly from a source glyf-based TTF/OTF.
It:
- opens the source font with
fontTools - derives a target advance width
- horizontally transforms outlines
- rewrites
hmtxmetrics - updates fixed-pitch metadata
- writes a new font file
This is useful for quick one-shot drafts.
The more serious path is the workspace flow.
It:
- initializes a workspace from a source font
- exports a source UFO
- derives glyph classes
- writes editable policy files
- rebuilds a generated UFO and compiled font from those policies
- emits a proof page that loads the generated font directly
This is the repo’s current “production” path.
The browser effect in src/index.ts works like this:
- Wait for fonts to load.
- Walk text nodes under a root element.
- Split text into graphemes.
- Measure each grapheme width with Pretext.
- Choose a cell width for the current font.
- Replace the original text node with wrapper and cell spans.
- Optionally observe mutations and reapply the effect.
There are two layout modes:
strict: force one hard cell width for every graphemeoptical: use softer cell sizing, center glyphs, and narrow spaces so text stays more readable
The direct generator in monospace_font_tools.py works like this:
- Load a source font with
TTFont. - Collect spacing glyphs and their widths.
- Resolve a shared target width with
max,average,median, orpercentile. - Measure glyph bounds.
- Transform outlines horizontally with
preserve,fit, ornormalize. - Rewrite horizontal metrics and metadata.
- Save the generated font.
The workspace system in workspace_tools.py adds iteration and policy control:
init-workspacecopies the original font into a workspace.- It exports a source UFO.
- It classifies glyphs into groups like
uppercase,lowercase,digits,punctuation,symbols,whitespace, andmarks. - It writes
rules.yamlandoverrides.yamlso widths and outline behavior can be tuned by class or by glyph. build-workspaceapplies those policies to the UFO.- It compiles a TTF with
ufo2ft, optionally emits WOFF2, and writes a proof HTML page.
The proof page loads the actual generated font file, not the browser DOM effect.
For the browser package:
npm install monospace-pretextFor the font pipeline:
pip install -r python/requirements.txtimport { monospacePage } from 'monospace-pretext'
await monospacePage({
observe: true,
mode: 'optical',
})Or scope it to one subtree:
import { monospaceElement } from 'monospace-pretext'
const hero = document.querySelector('.hero')
if (hero instanceof HTMLElement) {
await monospaceElement(hero, { mode: 'optical' })
}Run the demo:
npm install
npm run demoOpen the Vite URL it prints, usually http://127.0.0.1:5173/.
The checked-in demo loads a generated webfont at Roboto-DemoMono.woff2 and compares it against its Roboto source.
Regenerate that demo font:
pip install -r python/requirements.txt
npm run demo:fontBuild the static demo site:
npm run demo:build
npm run demo:previewIf you want the build to reflect a freshly regenerated font, run npm run demo:font first.
The production demo is written to site-dist/.
npx monospace-pretext-font ./MyFont.ttf --output ./MyFontMono.ttfLocal repo command:
npm run font:generate -- ./MyFont.ttf --output ./MyFontMono.ttfExample with explicit shaping controls:
npx monospace-pretext-font ./MyFont.ttf \
--width-mode percentile \
--percentile 0.9 \
--outline-mode normalize \
--normalization-strength 0.75 \
--fill-ratio 0.82Initialize a workspace:
npx monospace-pretext-font init-workspace ./MyFont.ttfBuild it:
npx monospace-pretext-font build-workspace ./MyFont-workspaceLocal repo commands:
npm run font:init-workspace -- ./MyFont.ttf
npm run font:build-workspace -- ./MyFont-workspaceThat produces:
project.yamlsources/original.ttfsources/<font-name>-source.ufopolicies/glyph-classes.yamlpolicies/rules.yamlpolicies/overrides.yamlbuild/<font-name>-mono.ufobuild/<font-name>-mono.ttfbuild/<font-name>-mono.woff2proof/index.html
The Node binary in monospace-pretext-font.js is a thin wrapper around the Python CLI in cli.py.
Supported commands:
generateinit-workspacebuild-workspace
The direct generate form is backward-compatible, so this still works:
npx monospace-pretext-font ./MyFont.ttf --output ./MyFontMono.ttfUseful direct-generator flags:
--target-width--width-mode max|average|median|percentile--percentile--outline-mode preserve|fit|normalize--normalization-strength--fill-ratio--family-suffix--keep-hinting
Useful workspace flags:
init-workspace --workspace ./custom-dirinit-workspace --family-suffix Monobuild-workspace --output-dir ./artifactsbuild-workspace --keep-hintingbuild-workspace --no-woff2
monospacePage() processes document.body and returns:
type MonospaceController = {
refresh(): Promise<void>
restore(): void
observe(): void
disconnect(): void
}monospaceElement() processes a single subtree and returns the same controller.
createPretextMeasurer() returns the default grapheme-width measurer backed by Pretext.
Browser options:
type MonospaceOptions = {
root?: Document | Element | DocumentFragment | ShadowRoot
ignoreSelector?: string
observe?: boolean
waitForFonts?: boolean
sampleText?: string
mode?: 'strict' | 'optical'
cellWidthPercentile?: number
spaceScale?: number
measureGrapheme?: (grapheme: string, font: string) => number
segmenter?: Intl.Segmenter
}ignoreSelectorskips nodes likescript,style, form controls, canvas, svg, and anything marked withdata-pretext-monospace-ignoreobservereruns the effect after DOM mutationswaitForFontswaits fordocument.fonts.readysampleTextseeds width measurementmodepicksstrictoropticalcellWidthPercentilecontrols optical-mode aggressivenessspaceScalenarrows spaces in optical modemeasureGraphemelets you replace the default Pretext-backed measurer
- src/index.ts: browser effect implementation
- demo/index.html: demo shell
- demo/main.ts: demo behavior and styling
- Roboto-Regular.ttf: vendored Apache-licensed source font used for the generated demo font
- Roboto-DemoMono.woff2: generated demo webfont produced by this repo
- LICENSE-Roboto.txt: upstream Roboto license
- vite.config.ts: Vite config for local demo development and GitHub Pages production builds
- deploy-pages.yml: GitHub Actions workflow that publishes the demo to GitHub Pages
- python/monospace_font_tools.py: direct font generator
- build_demo_font.py: script that regenerates the demo webfont from the vendored Roboto source
- python/workspace_tools.py: workspace init/build pipeline
- python/cli.py: CLI entrypoint
- python_tests/test_generator.py: direct-generator tests
- python_tests/test_workspace.py: workspace tests
The repo now includes a GitHub Actions workflow that deploys the demo site from main to GitHub Pages.
One-time setup:
- Open GitHub repository settings.
- Go to Pages.
- Under Build and deployment, set Source to
GitHub Actions.
After that, pushes to main will regenerate the demo font with npm run demo:font, build the demo with npm run demo:build, and publish site-dist/.
The Vite base path is derived from GITHUB_REPOSITORY in vite.config.ts, so project Pages deployments resolve assets under /<repo-name>/.
- Font generation currently supports
glyf-based TrueType/OpenType fonts. - CFF/CFF2 source fonts are not supported yet.
- Zero-width combining marks are preserved, but complex script quality is not the focus yet.
- Hinting is dropped by default after outline edits because the original hints are usually invalidated.
- The browser effect is intentionally fake. It rewrites DOM text and discards kerning/ligatures.
- The generated fonts are drafts. They are real fonts, but not a substitute for hand-drawn professional monospace design.
npm install
pip install -r python/requirements.txt
npm run demo:font
npm run demo:build
npm run check