@vyaz/core
v0.6.0
Published
Rich-text layout engine — TypeScript, isomorphic (browser + Bun/Node.js), pixel-perfect typography. TextFrame/paragraph layout plus a TableFrame grid layout (colSpan/rowSpan, borders, nested tables).
Maintainers
Readme
@vyaz/core
Rich text layout engine — TypeScript, isomorphic (browser + Bun/Node.js), pixel-perfect typography.
Part of the Vyaz project. Parses styled text into positioned lines with precise font metrics, supporting both CSS Text and Office (PowerPoint/DrawingML) rendering modes.
The engine operates on a TextFrame → Paragraph → TextRun hierarchy, following W3C CSS Text, CSS Writing Modes, and CSS Inline Layout specifications.
Installation
bun add @vyaz/core
# or
npm install @vyaz/coreQuick Start
import { layoutTextFrame } from '@vyaz/core';
import type { TextFrame } from '@vyaz/core';
const frame: TextFrame = {
width: 400,
wrap: true,
paragraphs: [
{
style: { alignment: 'left', lineHeight: 1.4, spaceBefore: 0, spaceAfter: 0 },
children: [
{ text: 'Hello, Vyaz!', fontFamily: 'Arial', fontSize: 16, fontWeight: 'bold', fontStyle: 'normal', color: '#000' },
],
},
],
};
const result = layoutTextFrame(frame);
console.log(result.lines);Features
- Text frame layout — multi-paragraph frames with padding, wrapping, and vertical alignment
- Multi-font, multi-style text — bold, italic, size, color, subscript/superscript, letter-spacing
- Text alignment — left, center, right, justify
- Line wrapping — soft/hard breaks,
white-spacecontrol (normal, nowrap, pre) - Writing modes —
horizontal-tb,vertical-rl,vertical-lrwith text orientation - Auto-fit — scale text proportionally to fit the container (
AutofitConfig) - Inline widgets — embedded objects (icons, images) inside the text flow
TextRun.data— an open-ended metadata bag the layout engine never interprets, for cross-cutting features like a hyperlink'shref(see@vyaz/converters/@vyaz/renderer)- Tables —
TableFramegrid layout: measured column widths/row heights,colSpan/rowSpan, per-side borders (solid, dashed, rounded corners),before/afterdecorative slots, nested tables - Office-compatible mode —
mode: 'office'for PowerPoint/DrawingML rendering - Font metrics — system font registry with fontkit-based metric extraction
- Compiler — paragraph compilation with token preparation for external renderers
API Overview
Text Frame Layout
import { layoutTextFrame } from '@vyaz/core';
import type { TextFrame, TextFrameLayoutResult } from '@vyaz/core';
const frame: TextFrame = {
width: 600,
height: 400,
wrap: true,
padding: { top: 20, right: 20, bottom: 20, left: 20 },
verticalAlignment: 'top',
paragraphs: [
{
style: { alignment: 'left', lineHeight: 1.4, spaceBefore: 0, spaceAfter: 12 },
children: [
{ text: 'First paragraph', fontFamily: 'Arial', fontSize: 16, fontWeight: 'normal', fontStyle: 'normal', color: '#000' },
],
},
],
};
const result: TextFrameLayoutResult = layoutTextFrame(frame);
// → { lines, content: {width,height}, textBox: {x,y,width,height}, frame: {width?,height?}, overflow, fit, ... }content vs textBox — two box sizes on every result, pick per use case:
| Use case | Use | Why |
|---|---|---|
| Line positioning, cursor, selection, where the next paragraph starts | content | The CSS content-box: every line's full lineHeight, leading kept above the first line and below the last — matches how the text will keep flowing if you append more |
| Auto-size a shape to its text ("resize to fit"), export to PDF/PPTX, anything that shouldn't show a dangling gap below the last line | textBox | Trimmed: same top/left/right as content, bottom at the last line's baseline + real font descent — hugs the visible text |
| Snap/align a border or adjacent element to the visible bottom of the text | textBox.height | It's the tight box, not the line-box |
Tables
import { layoutTableFrame } from '@vyaz/core';
import type { TableFrame, TableLayoutResult } from '@vyaz/core';
const cell = (text: string) => ({
content: {
wrap: true,
paragraphs: [{
style: { alignment: 'left', lineHeight: 1.3, spaceBefore: 0, spaceAfter: 0 },
children: [{ type: 'text', text, fontFamily: 'Arial', fontSize: 14, fontWeight: 'normal', fontStyle: 'normal', color: '#000' }],
}],
},
});
const table: TableFrame = {
defaultCellStyle: { paddings: 8, borderWidths: 1, borderColors: '#ccc' },
rows: [
{ style: { bgColor: '#eee' }, cells: [cell('Name'), cell('Qty')] },
{ cells: [cell('Widget'), cell('3')] },
],
};
const result: TableLayoutResult = layoutTableFrame(table);
// → { width, height, contentBox, rows: [{ y, height, cells: [{ x, y, width, height, content, ... }] }] }Column widths and row heights are measured from cell content unless
columnWidths/rowHeights/width/height override them. A cell's
content can itself be a TableFrame (nested tables). @vyaz/renderer's
renderTableToSVG paints the result — see its README, or the
Tables guide.
Autofit
import { applyScale, findScale } from '@vyaz/core';
import type { AutoFitOptions, AutoFitResult } from '@vyaz/core';
const scale: AutoFitResult = findScale(contentWidth, contentHeight, frameWidth, frameHeight);
const scaledLines = applyScale(result.lines, scale);Font Registration & Metrics
import {
FontMetricsProvider,
SystemFontRegistry,
createFontFace,
getFontBuffer,
} from '@vyaz/core';
import type { FontMetrics, IFontMetricsProvider, FontFace } from '@vyaz/core';
const provider = new FontMetricsProvider();
// Node.js — register from a local file
import { readFileSync } from 'node:fs';
const buffer = readFileSync('/path/to/font.ttf');
await provider.registerFont('MyFont', { weight: 'bold', style: 'normal' }, buffer);
// Browser — register from a URL
await provider.registerFont('MyFont', {}, 'https://example.com/font.woff2');
// Get pixel metrics
const metrics: FontMetrics = provider.getMetrics('MyFont', 16);
// → { ascent, descent, capHeight, unitsPerEm, sourceTable }Compiler
import { compileParagraph, getParagraphText, makeFontToken } from '@vyaz/core';
import type { PreparedRichInlineItem } from '@vyaz/core';
const items: PreparedRichInlineItem[] = compileParagraph(paragraph, defaultStyle);
const text: string = getParagraphText(paragraph);
const token: string = makeFontToken(fontFamily, fontSize, fontWeight, fontStyle);Paragraph Layout Engine (low-level)
import { ParagraphLayoutEngine, paragraphLayoutEngine } from '@vyaz/core';
const engine = new ParagraphLayoutEngine();
const result = engine.layout(paragraph, maxWidth, yOffset);
// → ParagraphLayoutResult { lines: Line[], width, height, contentWidth, contentHeight }Package Structure
src/
├── compile/ — DocumentCompiler, paragraph→token compilation
├── layout/ — Layout engines (Paragraph, TextFrame, Positioning, AutoFit)
├── measure/ — Font metrics, fontkit integration, system font registry
├── types/ — TypeScript type definitions (Document, Font, Layout)
└── utils/ — Helpers (font, list, text transform, env detection)Requirements
- Runtime: Bun 1.x, Node.js 18+, or modern browser
- Optional:
@napi-rs/canvas,fontkit,get-system-fontsfor Node.js font metrics
License
MIT
