MCPcopy Index your code
hub / github.com/chearon/dropflow

github.com/chearon/dropflow @v0.6.1

Chat with this repo
repository ↗ · DeepWiki ↗ · release v0.6.1 ↗ · + Follow
1,563 symbols 4,338 edges 83 files 73 documented · 5%
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

Dropflow is a CSS layout engine created to explore the reaches of the foundational CSS standards (that is: inlines, blocks, floats, positioning and eventually tables, but not flexbox or grid). It has a high quality text layout implementation and is capable of displaying many of the languages of the world. You can use it to generate PDFs or images on the backend with Node and node-canvas or render rich, wrapped text to a canvas in the browser.

Features

  • Supports over 30 properties including complex ones like float
  • Bidirectional and RTL text
  • Hyperscript (h()) API with styles as objects in addition to accepting HTML and CSS
  • Any OpenType/TrueType buffer can (and must) be registered
  • JPEG, BMP, PNG, and GIF <img>s supported (backend support may differ)
  • Font fallbacks at the grapheme level
  • Colored diacritics
  • Desirable line breaking (e.g. carries starting padding to the next line)
  • Optimized shaping
  • Inherited and cascaded styles are never calculated twice
  • Handles as many CSS layout edge cases as I can find
  • Fully typed
  • Lots of tests
  • Fast

Supported CSS rules

Following are rules that work or will work soon. Shorthand properties are not listed. If you see all components of a shorthand (for example, border-style, border-width, border-color) then the shorthand is assumed to be supported (for example border).

Inline formatting

Property Values Status
color rgba(), rgb(), #rrggbb, #rgb, #rgba ✅‍ Works
direction ltr, rtl ✅‍ Works
font-‍family ✅‍ Works
font-‍size em, px, smaller etc, small etc, cm etc ✅‍ Works
font-‍stretch condensed etc ✅‍ Works
font-‍style normal, italic, oblique ✅‍ Works
font-‍variant 🚧‍ Planned
font-‍weight normal, bolder, lighter light, bold, 100-900 ✅‍ Works
letter-‍spacing 🚧‍ Planned
line-‍height normal, px, em, %, number ✅‍ Works
tab-‍size 🚧‍ Planned
text-‍align start, end, left, right, center ✅‍ Works
text-‍decoration 🚧‍ Planned
unicode-‍bidi 🚧‍ Planned
vertical-‍align baseline, middle, sub, super, text-top, text-bottom, %, px etc, top, bottom ✅‍ Works
white-‍space normal, nowrap, pre, pre-wrap, pre-line ✅‍ Works
word-‍break

overflow-‍wrap,word-‍wrap | break-word, normal

anywhere, normal | ✅‍ Works |

Block formatting

Property Values Status
clear left, right, both, none ✅‍ Works
float left, right, none ✅‍ Works
writing-‍mode horizontal-tb, vertical-lr, vertical-rl 🏗 Partially done1

1Implemented for BFCs but not IFCs yet

Boxes and positioning

Property Values Status
background-‍clip border-box, content-box, padding-box ✅‍ Works
background-‍color rgba(), rgb(), #rrggbb, #rgb, #rgba ✅‍ Works
border-‍color rgba(), rgb(), #rrggbb, #rgb, #rgba ✅‍ Works
border-‍style solid, none ✅‍ Works
border-‍width em, px, cm etc ✅‍ Works
top, right, bottom, left em, px, %, cm etc ✅‍ Works
box-‍sizing border-box, content-box ✅‍ Works
display block ✅‍ Works
display inline ✅‍ Works
display inline-block ✅‍ Works
display flow-root ✅‍ Works
display none ✅‍ Works
display table 🚧‍ Planned
height em, px, %, cm etc, auto ✅‍ Works
margin em, px, %, cm etc, auto ✅‍ Works
max-height, max-width,

min-height, min-width | em, px, %, cm etc, auto | 🚧‍ Planned | | padding | em, px, %, cm etc | ✅‍ Works | | position | absolute | 🚧‍ Planned | | position | fixed | 🚧‍ Planned | | position | relative | ✅‍ Works | | transform | | 🚧‍ Planned | | overflow | hidden, visible | ✅‍ Works | | width | em, px, %, cm etc, auto | ✅‍ Works | | z-index | number, auto | ✅‍ Works | | zoom | number, % | ✅‍ Works |

Usage

Dropflow works off of a DOM with inherited and calculated styles, the same way that browsers do. You create the DOM with the familiar h() function, and specify styles as plain objects.

import * as flow from 'dropflow';
import {createCanvas} from 'canvas';
import fs from 'node:fs';

// Register fonts before layout. This is a required step.
const roboto1 = new flow.FontFace('Roboto', new URL('file:///Roboto-Regular.ttf'), {weight: 400});
const roboto2 = new flow.FontFace('Roboto', new URL('file:///Roboto-Bold.ttf'), {weight: 700});
flow.fonts.add(roboto1).add(roboto2);

// Always create styles at the top-level of your module if you can.
const divStyle = flow.style({
  backgroundColor: {r: 28, g: 10, b: 0, a: 1},
  textAlign: 'center',
  color: {r: 179, g: 200, b: 144, a: 1}
});

// Since we're creating styles directly, colors are numbers
const spanStyle = flow.style({
  color: {r: 115, g: 169, b: 173, a: 1},
  fontWeight: 700
});

// Create a DOM
const rootElement = flow.dom(
  flow.h('div', {style: divStyle}, [
    'Hello, ',
    flow.h('span', {style: spanStyle}, ['World!'])
  ])
);

// Layout and paint into the entire canvas (see also renderToCanvasContext)
const canvas = createCanvas(250, 50);
await flow.renderToCanvas(rootElement, canvas);

// Save your image
fs.writeFileSync(new URL('file:///hello.png'), canvas.toBuffer());

Hello world against a dark background, with "world" bolded and colored differently

HTML

This API is only recommended if performance is not a concern, or for learning purposes. Parsing adds extra time (though it is fast thanks to @fb55) and increases bundle size significantly.

import * as flow from 'dropflow';
import parse from 'dropflow/parse.js';
import {createCanvas} from 'canvas';
import fs from 'node:fs';

const roboto1 = new flow.FontFace('Roboto', new URL('file:///Roboto-Regular.ttf'), {weight: 400});
const roboto2 = new flow.FontFace('Roboto', new URL('file:///Roboto-Bold.ttf'), {weight: 700});
flow.fonts.add(roboto1).add(roboto2);

const rootElement = parse(`



    Hello, <span style="color: #73a9ad; font-weight: bold;">World!</span>



`);

const canvas = createCanvas(250, 50);
flow.renderToCanvas(rootElement, canvas);

canvas.createPNGStream().pipe(fs.createWriteStream(new URL('hello.png', import.meta.url)));

Performance characteristics

Performance is a top goal and is second only to correctness. Run the performance examples in the examples directory to see the numbers for yourself.

  • 8 paragraphs with several inline spans of different fonts can be turned from HTML to image in 9ms on a 2019 MacBook Pro and 13ms on a 2012 MacBook Pro (perf-1.ts)
  • The Little Prince (over 500 paragraphs) can be turned from HTML to image in under 160ms on a 2019 MacBook Pro and under 250ms on a 2012 MacBook Pro (perf-2.ts)
  • A 10-letter word can be generated and laid out (not painted) in under 25µs on a 2019 MacBook Pro and under 50µs on a 2012 MacBook Pro (perf-3.ts)

The fastest performance can be achieved by using the hyperscript API, which creates a DOM directly and skips the typical HTML and CSS parsing steps. Take care to re-use style objects to get the most benefits. Reflows at different widths are faster than recreating the layout tree.

API

The first two steps are:

  1. Register fonts
  2. Create a DOM via the Hyperscript or Parse API

Then, you can either render the DOM into a canvas using its size as the viewport:

  1. Render DOM to canvas

Or, you can use the lower-level functions to retain the layout, in case you want to re-layout at a different size, choose not to paint (for example if the layout isn't visible) or get intrinsics:

  1. Load dependent resources
  2. Generate a tree of layout boxes from the DOM
  3. Layout the box tree
  4. Paint the box tree to a target like canvas

Fonts

The first step in a dropflow program is to register fonts to be selected by the CSS font properties. Dropflow does not search system fonts, so you must construct a FontFace and add it at least once. The font registration API implements a subset of the CSS Font Loading API and adds one non-standard method, loadSync.

file:/// URLs will load() synchronously on the backend via readFileSync. To get synchronous behavior without having promises swallow errors, you can use the loadSync method.

ArrayBuffers are loaded immediately in the constructor, just like in the browser.

const fonts: FontFaceSet;

class FontFaceSet {
  ready: Promise<FontFaceSet>;
  has(face: FontFace): boolean;
  add(face: FontFace): FontFaceSet;
  delete(face: FontFace): boolean;
  clear(): void;
}

class FontFace {
  constructor(family: string, source: URL | ArrayBuffer, descriptors?: FontFaceDescriptors);
  load(): Promise<FontFace>;
  loaded: Promise<FontFace>;
}

interface FontFaceDescriptors {
  style?: 'normal' | 'italic' | 'oblique';
  weight?: number | 'normal' | 'bold' | 'bolder' | 'lighter';
  stretch?: 'normal' | 'ultra-condensed' | 'extra-condensed' | 'condensed' | 'semi-condensed' | 'semi-expanded' | 'expanded' | 'extra-expanded' | 'ultra-expanded';
  variant?: 'normal' | 'small-caps';
}

fonts

import * as flow from 'dropflow';

const roboto1 = new FontFace(
  'Roboto',
  new URL('https://cdn.jsdelivr.net/fontsource/fonts/roboto@latest/latin-400-normal.ttf')
);

const roboto2 = new FontFace(
  'Roboto',
  new URL('https://cdn.jsdelivr.net/fontsource/fonts/roboto@latest/latin-700-normal.ttf'),
  {weight: 'bold'}
);

flow.fonts.add(roboto1).add(roboto2);

for (const font of flow.fonts) font.load();
await fonts.ready;

// now you can do layout!

registerNotoFonts

import registerNotoFonts from 'dropflow/register-noto-fonts.js';
async function registerNotoFonts(): void;

Registers every Noto Sans font family. The fonts are published by FontSource and hosted by jsDelivr.

Note that this is a big import: there are more than 200 Noto Sans fonts, and the CJK fonts have large unicodeRange strings. It is probably better to register individual fonts for production use in a web browser. You could also copy and paste what you need from register-noto-fonts.ts.

For Latin, italic fonts are registered. For all scripts, one normal (400) weight and one bold (700) is registered.

Since dropflow cannot use system fonts, this is similar to having fallback fonts for many languages available on your operating system.

[!NOTE] While this will make the vast majority of text renderable, some scripts should be displayed with fonts made specifically for the language being displayed. For example, Chinese, Korean, and Japanese share common Unicode code points, but can render those characters differently. There is also a small cost to inspecting every character in the document. It is always better to use specific fonts when possible.

createFaceFromTables

function createFaceFromTables(source: URL | ArrayBufferLike): Promise<FontFace>;

This can be used if you want a font to be described (family, weight, etc) by its internal metadata. It also reads language information from the font, which will rank it more optimally in the fallback list for a run of text. It will also result in a more appropriate CJK font being chosen for CJK text when the language is known.

A Promise is returned if the URL is a non-file:// URL, otherwise, a FontFace is returned directly.

This function partly exists to keep behavior that dropflow used to have, since it did not used to support specifying custom font metadata for font selection (it only read metadata from inside the font). The test suite also takes advantage of the fallback list being properly ordered by language for its convenience. In most cases, it is fine to use the FontFace constructor instead.

function createFaceFromTablesSync(source: URL | ArrayBufferLike): FontFace;

If the source is an ArrayBuffer or a file:// URL in Node/Bun, this can be used to load synchronously and get synchronous exceptions.

Differences with the CSS Font Loading API

  1. Because dropflow doesn't use system fonts, all registered FontFaces are valid choices for fallback fonts. In the browser, if there isn't an exact

Extension points exported contracts — how you extend this code

IfcRenderItem (Interface)
(no doc) [7 implementers]
src/layout-text.ts
PaintBackend (Interface)
(no doc) [7 implementers]
src/paint.ts
Callbacks (Interface)
(no doc) [2 implementers]
src/parse-html.ts
FontFaceDescriptors (Interface)
(no doc)
src/text-font.ts
Environment (Interface)
(no doc)
src/environment.ts
LayoutContext (Interface)
(no doc)
src/layout-flow.ts
Rect (Interface)
(no doc)
src/paint-svg.ts
TreeLogOptions (Interface)
(no doc)
src/util.ts

Core symbols most depended-on inside this repo

peg$fail
called by 614
src/parse-css.js
get
called by 569
src/text-unicode-trie.ts
peg$parseS
called by 502
src/parse-css.js
push
called by 486
src/layout-text.ts
fill_set
called by 281
gen/lang-script-database.cc
add
called by 279
src/text-harfbuzz.ts
peg$literalExpectation
called by 217
src/parse-css.js
peg$parsecomment
called by 60
src/parse-css.js

Shape

Function 788
Method 616
Class 109
Interface 50

Languages

TypeScript99%
C++1%
C1%

Modules by API surface

src/parse-css.js370 symbols
src/layout-text.ts162 symbols
src/layout-flow.ts157 symbols
src/parse-html.ts122 symbols
src/text-harfbuzz.ts88 symbols
src/text-font.ts74 symbols
src/layout-box.ts67 symbols
src/style-query.ts61 symbols
src/style.ts50 symbols
src/paint.ts40 symbols
src/paint-canvas.ts38 symbols
src/dom.ts26 symbols

For agents

$ claude mcp add dropflow \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact