render-tag 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +159 -0
- package/lib/index.d.ts +12 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +69 -0
- package/lib/index.js.map +1 -0
- package/lib/layout.d.ts +16 -0
- package/lib/layout.d.ts.map +1 -0
- package/lib/layout.js +1323 -0
- package/lib/layout.js.map +1 -0
- package/lib/parse.d.ts +9 -0
- package/lib/parse.d.ts.map +1 -0
- package/lib/parse.js +25 -0
- package/lib/parse.js.map +1 -0
- package/lib/render.d.ts +6 -0
- package/lib/render.d.ts.map +1 -0
- package/lib/render.js +351 -0
- package/lib/render.js.map +1 -0
- package/lib/style-resolver.d.ts +10 -0
- package/lib/style-resolver.d.ts.map +1 -0
- package/lib/style-resolver.js +257 -0
- package/lib/style-resolver.js.map +1 -0
- package/lib/types.d.ts +143 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +2 -0
- package/lib/types.js.map +1 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# render-tag
|
|
2
|
+
|
|
3
|
+
Render HTML rich text onto a canvas element using pure 2D canvas API. No SVG, no `foreignObject` — just `fillText`, `measureText`, and drawing primitives.
|
|
4
|
+
|
|
5
|
+
**Website & demos:** [https://polotno.com/render-tag/](https://polotno.com/render-tag/)
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
Browsers can render HTML into canvas via SVG `foreignObject`, but it's slow (~100ms) and inconsistent across browsers. `render-tag` parses your HTML, resolves styles via `getComputedStyle`, then lays out and draws everything with canvas 2D calls. It's **10-60x faster** than SVG-based approaches.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install render-tag
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { renderHTML } from 'render-tag';
|
|
21
|
+
|
|
22
|
+
const { canvas, height } = renderHTML(
|
|
23
|
+
'<p>Hello <strong>world</strong></p>',
|
|
24
|
+
{ width: 400 }
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
document.body.appendChild(canvas);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### With CSS
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
const { canvas } = renderHTML(
|
|
34
|
+
'<p class="title">Styled text</p>',
|
|
35
|
+
{
|
|
36
|
+
width: 600,
|
|
37
|
+
css: `
|
|
38
|
+
.title {
|
|
39
|
+
font-family: Georgia, serif;
|
|
40
|
+
font-size: 24px;
|
|
41
|
+
color: #1a1a1a;
|
|
42
|
+
}
|
|
43
|
+
`,
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### With web fonts
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
const { canvas } = renderHTML(
|
|
52
|
+
'<p>Custom font text</p>',
|
|
53
|
+
{
|
|
54
|
+
width: 500,
|
|
55
|
+
css: `
|
|
56
|
+
@font-face {
|
|
57
|
+
font-family: 'MyFont';
|
|
58
|
+
src: url('https://example.com/font.woff2') format('woff2');
|
|
59
|
+
}
|
|
60
|
+
p { font-family: 'MyFont', sans-serif; font-size: 18px; }
|
|
61
|
+
`,
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### High-DPI / Retina
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
const { canvas } = renderHTML(html, {
|
|
70
|
+
width: 600,
|
|
71
|
+
pixelRatio: window.devicePixelRatio,
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Render onto existing canvas
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
const canvas = document.getElementById('my-canvas');
|
|
79
|
+
renderHTML(html, { canvas, width: 800, height: 600 });
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## API
|
|
83
|
+
|
|
84
|
+
### `renderHTML(html, options): RenderResult`
|
|
85
|
+
|
|
86
|
+
| Option | Type | Default | Description |
|
|
87
|
+
|---|---|---|---|
|
|
88
|
+
| `width` | `number` | *required* | Layout width in CSS pixels |
|
|
89
|
+
| `height` | `number` | auto | Fixed height (auto-sized from content if omitted) |
|
|
90
|
+
| `css` | `string` | `''` | CSS stylesheet (supports `@font-face`, classes, selectors) |
|
|
91
|
+
| `canvas` | `HTMLCanvasElement` | created | Target canvas element |
|
|
92
|
+
| `pixelRatio` | `number` | `1` | Device pixel ratio for sharp rendering |
|
|
93
|
+
| `useDomMeasurements` | `boolean` | `true` | Use DOM probes for cross-browser line height accuracy. Disable for DOM-free rendering (pure canvas). |
|
|
94
|
+
|
|
95
|
+
Returns `{ canvas: HTMLCanvasElement, height: number }`.
|
|
96
|
+
|
|
97
|
+
The function is **synchronous**. Fonts must be loaded before calling.
|
|
98
|
+
|
|
99
|
+
## What it renders
|
|
100
|
+
|
|
101
|
+
- Paragraphs, headings, divs, spans
|
|
102
|
+
- Bold, italic, underline, strikethrough, overline
|
|
103
|
+
- Text colors, background colors
|
|
104
|
+
- Font families, sizes, weights (100-900)
|
|
105
|
+
- Line height, letter spacing, text alignment (left/center/right/justify)
|
|
106
|
+
- Ordered and unordered lists with nesting
|
|
107
|
+
- Inline styles and CSS classes
|
|
108
|
+
- `@font-face` web fonts (loaded automatically)
|
|
109
|
+
- Flexbox layout (row/column)
|
|
110
|
+
- Table layout (basic)
|
|
111
|
+
- Text shadows, text stroke, gradient text
|
|
112
|
+
- Decoration styles: solid, dotted, dashed, double, wavy
|
|
113
|
+
- RTL text, CJK characters, emoji
|
|
114
|
+
- `pre-wrap` whitespace handling
|
|
115
|
+
- `overflow-wrap: break-word`
|
|
116
|
+
|
|
117
|
+
## Cross-browser consistency
|
|
118
|
+
|
|
119
|
+
The library targets Chrome as the primary browser. For consistent rendering across Chrome and Firefox, add these CSS rules to your input:
|
|
120
|
+
|
|
121
|
+
```css
|
|
122
|
+
/* Suppress Firefox's ::marker extra line height (~1.5px per list item).
|
|
123
|
+
render-tag draws list markers itself, so this loses nothing visually. */
|
|
124
|
+
li::marker { content: none; font-size: 0; line-height: 0; }
|
|
125
|
+
|
|
126
|
+
/* Fix Firefox emoji position drift (apply to elements with emoji).
|
|
127
|
+
Firefox's canvas kerning differs from DOM kerning for emoji characters,
|
|
128
|
+
causing cumulative X position shift. Disabling kerning makes them match.
|
|
129
|
+
Note: this slightly affects letter pair spacing for regular text. */
|
|
130
|
+
.has-emoji { font-kerning: none; }
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
With `useDomMeasurements: true` (the default), the library uses hidden DOM probes to match Firefox's actual line box heights. If you disable DOM measurements (`useDomMeasurements: false`), the CSS above becomes especially important for Firefox consistency.
|
|
134
|
+
|
|
135
|
+
## How it works
|
|
136
|
+
|
|
137
|
+
1. **Parse** HTML with `DOMParser`
|
|
138
|
+
2. **Resolve styles** via hidden DOM + `getComputedStyle` (CSS cascade for free)
|
|
139
|
+
3. **Layout** with pure canvas `measureText` (block flow, inline wrapping, margin collapsing)
|
|
140
|
+
4. **Render** with canvas 2D API (`fillText`, `fillRect`, `strokeText`, etc.)
|
|
141
|
+
|
|
142
|
+
Layout is computed from CSS values and canvas text metrics. Optional DOM probes (`useDomMeasurements`) improve cross-browser accuracy for line heights and mixed-font wrapping.
|
|
143
|
+
|
|
144
|
+
## Development
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
npm install
|
|
148
|
+
npx playwright install chromium
|
|
149
|
+
|
|
150
|
+
# Run visual comparison demo
|
|
151
|
+
npm run dev
|
|
152
|
+
|
|
153
|
+
# Run tests (60 cases, Chromium via Playwright)
|
|
154
|
+
npm test
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## License
|
|
158
|
+
|
|
159
|
+
MIT
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { RenderOptions, RenderResult, LayoutLine } from './types.js';
|
|
2
|
+
export type { RenderOptions, RenderResult, LayoutLine };
|
|
3
|
+
/**
|
|
4
|
+
* Render an HTML string onto a canvas element using pure 2D canvas API.
|
|
5
|
+
* Fonts must already be loaded on the page before calling this function.
|
|
6
|
+
*
|
|
7
|
+
* @param html - HTML string to render
|
|
8
|
+
* @param options - Rendering options (width is required)
|
|
9
|
+
* @returns The canvas element and computed content height
|
|
10
|
+
*/
|
|
11
|
+
export declare function renderHTML(html: string, options: RenderOptions): RenderResult;
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,UAAU,EAAc,MAAM,YAAY,CAAC;AAMtF,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,CAAC;AAExD;;;;;;;GAOG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,aAAa,GACrB,YAAY,CAgEd"}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { parseHTML } from './parse.js';
|
|
2
|
+
import { resolveStyles } from './style-resolver.js';
|
|
3
|
+
import { buildLayoutTree } from './layout.js';
|
|
4
|
+
import { renderNode } from './render.js';
|
|
5
|
+
/**
|
|
6
|
+
* Render an HTML string onto a canvas element using pure 2D canvas API.
|
|
7
|
+
* Fonts must already be loaded on the page before calling this function.
|
|
8
|
+
*
|
|
9
|
+
* @param html - HTML string to render
|
|
10
|
+
* @param options - Rendering options (width is required)
|
|
11
|
+
* @returns The canvas element and computed content height
|
|
12
|
+
*/
|
|
13
|
+
export function renderHTML(html, options) {
|
|
14
|
+
const { width, height, css: extraCSS, pixelRatio = 1, useDomMeasurements = true, debug } = options;
|
|
15
|
+
// 1. Parse HTML and extract CSS
|
|
16
|
+
const { fragment, css } = parseHTML(html, extraCSS);
|
|
17
|
+
// 2. Resolve styles using hidden DOM (getComputedStyle only, no measurements)
|
|
18
|
+
const { tree, cleanup } = resolveStyles(fragment, css, width, height);
|
|
19
|
+
// 3. Create canvas
|
|
20
|
+
const canvas = options.canvas || document.createElement('canvas');
|
|
21
|
+
const tmpCanvas = document.createElement('canvas');
|
|
22
|
+
const ctx = tmpCanvas.getContext('2d');
|
|
23
|
+
ctx.fontKerning = 'normal';
|
|
24
|
+
// 4. Build layout tree using pure canvas measurement
|
|
25
|
+
const { root, height: contentHeight } = buildLayoutTree(ctx, tree, width, useDomMeasurements, debug);
|
|
26
|
+
// 5. Size the output canvas
|
|
27
|
+
const finalHeight = height || contentHeight;
|
|
28
|
+
canvas.width = Math.ceil(width * pixelRatio);
|
|
29
|
+
canvas.height = Math.ceil(finalHeight * pixelRatio);
|
|
30
|
+
canvas.style.width = `${width}px`;
|
|
31
|
+
canvas.style.height = `${finalHeight}px`;
|
|
32
|
+
const renderCtx = canvas.getContext('2d');
|
|
33
|
+
renderCtx.scale(pixelRatio, pixelRatio);
|
|
34
|
+
// 6. Render to canvas
|
|
35
|
+
renderNode(renderCtx, root);
|
|
36
|
+
// 7. Extract lines from layout tree — group by Y with tolerance.
|
|
37
|
+
// Use fontSize as proxy for height when grouping. Sub/sup text has
|
|
38
|
+
// different Y but belongs on the same visual line as parent text.
|
|
39
|
+
const wordPositions = [];
|
|
40
|
+
function walkLines(node) {
|
|
41
|
+
if (node.type === 'text' && node.text.trim()) {
|
|
42
|
+
wordPositions.push({ y: node.y, fontSize: node.style.fontSize, text: node.text });
|
|
43
|
+
}
|
|
44
|
+
if (node.type === 'box') {
|
|
45
|
+
for (const child of node.children)
|
|
46
|
+
walkLines(child);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
walkLines(root);
|
|
50
|
+
wordPositions.sort((a, b) => a.y - b.y);
|
|
51
|
+
const lines = [];
|
|
52
|
+
let lineMaxFontSize = 0;
|
|
53
|
+
for (const wp of wordPositions) {
|
|
54
|
+
const lastLine = lines[lines.length - 1];
|
|
55
|
+
const tolerance = Math.max(lineMaxFontSize, wp.fontSize) * 0.5;
|
|
56
|
+
if (lastLine && Math.abs(wp.y - lastLine.y) < tolerance) {
|
|
57
|
+
lastLine.text += wp.text;
|
|
58
|
+
lineMaxFontSize = Math.max(lineMaxFontSize, wp.fontSize);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
lines.push({ y: Math.round(wp.y), text: wp.text });
|
|
62
|
+
lineMaxFontSize = wp.fontSize;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// 8. Cleanup
|
|
66
|
+
cleanup();
|
|
67
|
+
return { canvas, height: finalHeight, layoutRoot: root, lines };
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/lib/layout.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { StyledNode, LayoutBox, ResolvedStyle } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Build a canvas font string from resolved style.
|
|
4
|
+
*/
|
|
5
|
+
export declare function buildCanvasFont(style: ResolvedStyle): string;
|
|
6
|
+
/**
|
|
7
|
+
* Build the layout tree from the styled tree using pure canvas measurement.
|
|
8
|
+
* No DOM measurements used — all positions computed from CSS values + canvas.measureText.
|
|
9
|
+
*/
|
|
10
|
+
export declare function getDomMeasureCount(): number;
|
|
11
|
+
export declare function resetDomMeasureCount(): void;
|
|
12
|
+
export declare function buildLayoutTree(ctx: CanvasRenderingContext2D, styledTree: StyledNode, containerWidth: number, useDomMeasurements?: boolean, debug?: (entry: import('./types.ts').DebugEntry) => void): {
|
|
13
|
+
root: LayoutBox;
|
|
14
|
+
height: number;
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=layout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../src/layout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAc,SAAS,EAAc,aAAa,EAAE,MAAM,YAAY,CAAC;AAqG/F;;GAEG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAO5D;AA+0CD;;;GAGG;AACH,wBAAgB,kBAAkB,WAA+B;AACjE,wBAAgB,oBAAoB,SAA4B;AAEhE,wBAAgB,eAAe,CAC7B,GAAG,EAAE,wBAAwB,EAC7B,UAAU,EAAE,UAAU,EACtB,cAAc,EAAE,MAAM,EACtB,kBAAkB,UAAO,EACzB,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,YAAY,EAAE,UAAU,KAAK,IAAI,GACvD;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAcrC"}
|