render-tag 0.1.0 → 0.1.2
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 +105 -43
- package/lib/index.d.ts +22 -9
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +113 -39
- package/lib/index.js.map +1 -1
- package/lib/layout.d.ts.map +1 -1
- package/lib/layout.js +51 -8
- package/lib/layout.js.map +1 -1
- package/lib/parse.d.ts +1 -1
- package/lib/parse.d.ts.map +1 -1
- package/lib/parse.js +1 -4
- package/lib/parse.js.map +1 -1
- package/lib/render-tag.umd.js +10 -0
- package/lib/render-tag.umd.js.map +1 -0
- package/lib/types.d.ts +81 -17
- package/lib/types.d.ts.map +1 -1
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# render-tag
|
|
2
2
|
|
|
3
|
-
Render HTML rich text onto
|
|
3
|
+
Render HTML rich text onto canvas with the 2D API. No SVG, no `foreignObject` — just `fillText`, `measureText`, and drawing primitives.
|
|
4
4
|
|
|
5
5
|
**Website & demos:** [https://polotno.com/render-tag/](https://polotno.com/render-tag/)
|
|
6
6
|
|
|
@@ -8,6 +8,8 @@ Render HTML rich text onto a canvas element using pure 2D canvas API. No SVG, no
|
|
|
8
8
|
|
|
9
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
10
|
|
|
11
|
+
By design, render-tag focuses on **rich text only** — paragraphs, headings, lists, tables, inline formatting. It is not designed for interactive elements (buttons, inputs, iframes) or complex HTML layouts. This focus is what makes it fast.
|
|
12
|
+
|
|
11
13
|
## Install
|
|
12
14
|
|
|
13
15
|
```bash
|
|
@@ -17,85 +19,145 @@ npm install render-tag
|
|
|
17
19
|
## Usage
|
|
18
20
|
|
|
19
21
|
```typescript
|
|
20
|
-
import {
|
|
22
|
+
import { render } from 'render-tag';
|
|
21
23
|
|
|
22
|
-
const { canvas, height } =
|
|
23
|
-
'<p>Hello <strong>world</strong></p>',
|
|
24
|
-
|
|
25
|
-
);
|
|
24
|
+
const { canvas, height } = render({
|
|
25
|
+
html: '<p>Hello <strong>world</strong></p>',
|
|
26
|
+
width: 400,
|
|
27
|
+
});
|
|
26
28
|
|
|
27
29
|
document.body.appendChild(canvas);
|
|
28
30
|
```
|
|
29
31
|
|
|
30
32
|
### With CSS
|
|
31
33
|
|
|
34
|
+
Include `<style>` tags in your HTML string:
|
|
35
|
+
|
|
32
36
|
```typescript
|
|
33
|
-
const { canvas } =
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
width: 600,
|
|
37
|
-
css: `
|
|
37
|
+
const { canvas } = render({
|
|
38
|
+
html: `
|
|
39
|
+
<style>
|
|
38
40
|
.title {
|
|
39
41
|
font-family: Georgia, serif;
|
|
40
42
|
font-size: 24px;
|
|
41
43
|
color: #1a1a1a;
|
|
42
44
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
</style>
|
|
46
|
+
<p class="title">Styled text</p>
|
|
47
|
+
`,
|
|
48
|
+
width: 600,
|
|
49
|
+
});
|
|
46
50
|
```
|
|
47
51
|
|
|
48
|
-
###
|
|
52
|
+
### Font loading
|
|
53
|
+
|
|
54
|
+
`render` is **synchronous** and does not load fonts. You must ensure fonts are loaded before calling it. If a font isn't loaded, the browser falls back to a default font and text metrics will be wrong.
|
|
49
55
|
|
|
50
56
|
```typescript
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
);
|
|
57
|
+
// Load fonts before rendering
|
|
58
|
+
await document.fonts.load('400 16px "Roboto"');
|
|
59
|
+
await document.fonts.load('700 16px "Roboto"');
|
|
60
|
+
|
|
61
|
+
// Now render — fonts are guaranteed to be available
|
|
62
|
+
const { canvas } = render({ html, width: 500 });
|
|
63
|
+
|
|
64
|
+
// Re-render if fonts load later
|
|
65
|
+
document.fonts.onloadingdone = () => {
|
|
66
|
+
render({ html, width: 500 });
|
|
67
|
+
};
|
|
64
68
|
```
|
|
65
69
|
|
|
70
|
+
You do **not** need to pass `@font-face` rules separately. As long as fonts are loaded in the document (via `<link>`, `@font-face` in a stylesheet, or the CSS Font Loading API), `render` can use them.
|
|
71
|
+
|
|
66
72
|
### High-DPI / Retina
|
|
67
73
|
|
|
74
|
+
`pixelRatio` defaults to `devicePixelRatio`, so HiDPI displays are sharp out of the box. Override if needed:
|
|
75
|
+
|
|
68
76
|
```typescript
|
|
69
|
-
const { canvas } =
|
|
70
|
-
width: 600,
|
|
71
|
-
pixelRatio: window.devicePixelRatio,
|
|
72
|
-
});
|
|
77
|
+
const { canvas } = render({ html, width: 600, pixelRatio: 1 });
|
|
73
78
|
```
|
|
74
79
|
|
|
75
80
|
### Render onto existing canvas
|
|
76
81
|
|
|
77
82
|
```typescript
|
|
78
83
|
const canvas = document.getElementById('my-canvas');
|
|
79
|
-
|
|
84
|
+
render({ html, canvas, width: 800, height: 600 });
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Render onto existing context
|
|
88
|
+
|
|
89
|
+
Draw directly onto a context you control (no canvas resizing or scaling):
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
const ctx = myCanvas.getContext('2d');
|
|
93
|
+
render({ html, ctx, width: 400 });
|
|
80
94
|
```
|
|
81
95
|
|
|
96
|
+
This is useful for compositing multiple renders onto one canvas or rendering onto an `OffscreenCanvas`.
|
|
97
|
+
|
|
82
98
|
## API
|
|
83
99
|
|
|
84
|
-
### `
|
|
100
|
+
### `render(config): RenderResult`
|
|
101
|
+
|
|
102
|
+
All-in-one: compute layout and draw in a single call.
|
|
85
103
|
|
|
86
104
|
| Option | Type | Default | Description |
|
|
87
105
|
|---|---|---|---|
|
|
106
|
+
| `html` | `string` | *required* | HTML string to render (include `<style>` tags for CSS) |
|
|
88
107
|
| `width` | `number` | *required* | Layout width in CSS pixels |
|
|
89
108
|
| `height` | `number` | auto | Fixed height (auto-sized from content if omitted) |
|
|
90
|
-
| `
|
|
91
|
-
| `canvas` | `HTMLCanvasElement` | created | Target canvas element |
|
|
92
|
-
| `pixelRatio` | `number` | `
|
|
93
|
-
| `
|
|
109
|
+
| `ctx` | `CanvasRenderingContext2D` | — | Existing context to draw onto (no resizing/scaling) |
|
|
110
|
+
| `canvas` | `HTMLCanvasElement \| OffscreenCanvas` | created | Target canvas element (mutually exclusive with `ctx`) |
|
|
111
|
+
| `pixelRatio` | `number` | `devicePixelRatio` | Device pixel ratio for sharp rendering |
|
|
112
|
+
| `accuracy` | `'balanced' \| 'performance'` | `'balanced'` | `'balanced'` uses DOM probes for cross-browser line height accuracy. `'performance'` uses pure canvas API only. |
|
|
94
113
|
|
|
95
|
-
Returns `{ canvas
|
|
114
|
+
Returns `{ canvas, height, layoutRoot, lines }`.
|
|
96
115
|
|
|
97
116
|
The function is **synchronous**. Fonts must be loaded before calling.
|
|
98
117
|
|
|
118
|
+
### `layout(config): LayoutResult`
|
|
119
|
+
|
|
120
|
+
Compute layout without rendering. Use when you need to measure content or render the same layout onto multiple targets.
|
|
121
|
+
|
|
122
|
+
| Option | Type | Default | Description |
|
|
123
|
+
|---|---|---|---|
|
|
124
|
+
| `html` | `string` | *required* | HTML string (include `<style>` tags for CSS) |
|
|
125
|
+
| `width` | `number` | *required* | Layout width in CSS pixels |
|
|
126
|
+
| `height` | `number` | auto | Fixed height (auto-sized from content if omitted) |
|
|
127
|
+
| `accuracy` | `'balanced' \| 'performance'` | `'balanced'` | Measurement accuracy mode |
|
|
128
|
+
|
|
129
|
+
Returns `{ layoutRoot, height, lines }`.
|
|
130
|
+
|
|
131
|
+
### `drawLayout(config): { canvas }`
|
|
132
|
+
|
|
133
|
+
Draw a pre-computed layout onto a canvas or context.
|
|
134
|
+
|
|
135
|
+
| Option | Type | Default | Description |
|
|
136
|
+
|---|---|---|---|
|
|
137
|
+
| `layout` | `LayoutResult` | *required* | Result from `layout()` |
|
|
138
|
+
| `width` | `number` | *required* | Width used during layout (must match) |
|
|
139
|
+
| `ctx` | `CanvasRenderingContext2D` | — | Existing context to draw onto (no resizing/scaling) |
|
|
140
|
+
| `canvas` | `HTMLCanvasElement \| OffscreenCanvas` | created | Target canvas (mutually exclusive with `ctx`) |
|
|
141
|
+
| `pixelRatio` | `number` | `devicePixelRatio` | Device pixel ratio |
|
|
142
|
+
|
|
143
|
+
### Example: layout once, draw many
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
import { layout, drawLayout } from 'render-tag';
|
|
147
|
+
|
|
148
|
+
const result = layout({ html, width: 400 });
|
|
149
|
+
console.log('content height:', result.height);
|
|
150
|
+
|
|
151
|
+
// Draw onto a thumbnail canvas
|
|
152
|
+
drawLayout({ layout: result, width: 400, canvas: thumbnailCanvas });
|
|
153
|
+
|
|
154
|
+
// Draw onto the main canvas
|
|
155
|
+
drawLayout({ layout: result, width: 400, canvas: mainCanvas });
|
|
156
|
+
|
|
157
|
+
// Draw onto an existing context
|
|
158
|
+
drawLayout({ layout: result, width: 400, ctx: offscreenCtx });
|
|
159
|
+
```
|
|
160
|
+
|
|
99
161
|
## What it renders
|
|
100
162
|
|
|
101
163
|
- Paragraphs, headings, divs, spans
|
|
@@ -105,7 +167,6 @@ The function is **synchronous**. Fonts must be loaded before calling.
|
|
|
105
167
|
- Line height, letter spacing, text alignment (left/center/right/justify)
|
|
106
168
|
- Ordered and unordered lists with nesting
|
|
107
169
|
- Inline styles and CSS classes
|
|
108
|
-
- `@font-face` web fonts (loaded automatically)
|
|
109
170
|
- Flexbox layout (row/column)
|
|
110
171
|
- Table layout (basic)
|
|
111
172
|
- Text shadows, text stroke, gradient text
|
|
@@ -113,6 +174,7 @@ The function is **synchronous**. Fonts must be loaded before calling.
|
|
|
113
174
|
- RTL text, CJK characters, emoji
|
|
114
175
|
- `pre-wrap` whitespace handling
|
|
115
176
|
- `overflow-wrap: break-word`
|
|
177
|
+
- Soft hyphens (`­`)
|
|
116
178
|
|
|
117
179
|
## Cross-browser consistency
|
|
118
180
|
|
|
@@ -130,16 +192,16 @@ li::marker { content: none; font-size: 0; line-height: 0; }
|
|
|
130
192
|
.has-emoji { font-kerning: none; }
|
|
131
193
|
```
|
|
132
194
|
|
|
133
|
-
With `
|
|
195
|
+
With `accuracy: 'balanced'` (the default), the library uses hidden DOM probes to match Firefox's actual line box heights. With `accuracy: 'performance'`, the CSS above becomes especially important for Firefox consistency.
|
|
134
196
|
|
|
135
197
|
## How it works
|
|
136
198
|
|
|
137
199
|
1. **Parse** HTML with `DOMParser`
|
|
138
200
|
2. **Resolve styles** via hidden DOM + `getComputedStyle` (CSS cascade for free)
|
|
139
|
-
3. **Layout** with
|
|
201
|
+
3. **Layout** with canvas `measureText` (block flow, inline wrapping, margin collapsing)
|
|
140
202
|
4. **Render** with canvas 2D API (`fillText`, `fillRect`, `strokeText`, etc.)
|
|
141
203
|
|
|
142
|
-
|
|
204
|
+
Style resolution uses a hidden DOM element with `getComputedStyle` to get the full CSS cascade. Layout and rendering are done entirely with the canvas 2D API. Optional DOM probes (`accuracy: 'balanced'`) improve cross-browser accuracy for line heights and mixed-font wrapping.
|
|
143
205
|
|
|
144
206
|
## Development
|
|
145
207
|
|
package/lib/index.d.ts
CHANGED
|
@@ -1,12 +1,25 @@
|
|
|
1
|
-
import type { RenderOptions, RenderResult, LayoutLine } from './types.js';
|
|
2
|
-
export type { RenderOptions, RenderResult, LayoutLine };
|
|
3
|
-
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
*
|
|
1
|
+
import type { RenderConfig, RenderOptions, RenderResult, LayoutConfig, LayoutResult, DrawConfig, LayoutLine, AnyCanvas } from './types.js';
|
|
2
|
+
export type { RenderConfig, RenderOptions, RenderResult, LayoutConfig, LayoutResult, DrawConfig, LayoutLine };
|
|
3
|
+
/**
|
|
4
|
+
* Compute layout for an HTML string without rendering.
|
|
5
|
+
* Returns a reusable LayoutResult that can be drawn onto multiple targets via drawLayout().
|
|
6
|
+
*/
|
|
7
|
+
export declare function layout(config: LayoutConfig): LayoutResult;
|
|
8
|
+
/**
|
|
9
|
+
* Draw a pre-computed layout onto a canvas or context.
|
|
10
|
+
* Use with layout() to render the same content onto multiple targets.
|
|
11
|
+
*/
|
|
12
|
+
export declare function drawLayout(config: DrawConfig): {
|
|
13
|
+
canvas: AnyCanvas;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Render an HTML string onto a canvas using pure 2D canvas API.
|
|
17
|
+
* Convenience function combining layout() + drawLayout().
|
|
18
|
+
* Fonts must already be loaded before calling this function.
|
|
19
|
+
*/
|
|
20
|
+
export declare function render(config: RenderConfig): RenderResult;
|
|
21
|
+
/**
|
|
22
|
+
* @deprecated Use `render()` instead.
|
|
10
23
|
*/
|
|
11
24
|
export declare function renderHTML(html: string, options: RenderOptions): RenderResult;
|
|
12
25
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/index.d.ts.map
CHANGED
|
@@ -1 +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;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EAAE,aAAa,EAAE,YAAY,EACzC,YAAY,EAAE,YAAY,EAAE,UAAU,EACtC,UAAU,EAAc,SAAS,EAClC,MAAM,YAAY,CAAC;AAMpB,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,CAAC;AAmC9G;;;GAGG;AACH,wBAAgB,MAAM,CAAC,MAAM,EAAE,YAAY,GAAG,YAAY,CA6BzD;AAID;;;GAGG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,GAAG;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,CAyCpE;AAID;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,MAAM,EAAE,YAAY,GAAG,YAAY,CA2BzD;AAID;;GAEG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,aAAa,GACrB,YAAY,CAUd"}
|
package/lib/index.js
CHANGED
|
@@ -2,51 +2,19 @@ import { parseHTML } from './parse.js';
|
|
|
2
2
|
import { resolveStyles } from './style-resolver.js';
|
|
3
3
|
import { buildLayoutTree } from './layout.js';
|
|
4
4
|
import { renderNode } from './render.js';
|
|
5
|
-
|
|
6
|
-
|
|
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.
|
|
5
|
+
// ─── Line extraction ─────────────────────────────────────────────────
|
|
6
|
+
function extractLines(root) {
|
|
39
7
|
const wordPositions = [];
|
|
40
|
-
function
|
|
8
|
+
function walk(node) {
|
|
41
9
|
if (node.type === 'text' && node.text.trim()) {
|
|
42
10
|
wordPositions.push({ y: node.y, fontSize: node.style.fontSize, text: node.text });
|
|
43
11
|
}
|
|
44
12
|
if (node.type === 'box') {
|
|
45
13
|
for (const child of node.children)
|
|
46
|
-
|
|
14
|
+
walk(child);
|
|
47
15
|
}
|
|
48
16
|
}
|
|
49
|
-
|
|
17
|
+
walk(root);
|
|
50
18
|
wordPositions.sort((a, b) => a.y - b.y);
|
|
51
19
|
const lines = [];
|
|
52
20
|
let lineMaxFontSize = 0;
|
|
@@ -62,8 +30,114 @@ export function renderHTML(html, options) {
|
|
|
62
30
|
lineMaxFontSize = wp.fontSize;
|
|
63
31
|
}
|
|
64
32
|
}
|
|
65
|
-
|
|
33
|
+
return lines;
|
|
34
|
+
}
|
|
35
|
+
// ─── layout() ────────────────────────────────────────────────────────
|
|
36
|
+
/**
|
|
37
|
+
* Compute layout for an HTML string without rendering.
|
|
38
|
+
* Returns a reusable LayoutResult that can be drawn onto multiple targets via drawLayout().
|
|
39
|
+
*/
|
|
40
|
+
export function layout(config) {
|
|
41
|
+
const { html, width, height, accuracy = 'balanced', debug, } = config;
|
|
42
|
+
if (!width || width <= 0 || Number.isNaN(width)) {
|
|
43
|
+
throw new TypeError(`layout: width must be a positive number, got ${width}`);
|
|
44
|
+
}
|
|
45
|
+
const useDomMeasurements = accuracy === 'balanced';
|
|
46
|
+
const { fragment, css } = parseHTML(html);
|
|
47
|
+
const { tree, cleanup } = resolveStyles(fragment, css, width, height);
|
|
48
|
+
const tmpCanvas = document.createElement('canvas');
|
|
49
|
+
const measureCtx = tmpCanvas.getContext('2d');
|
|
50
|
+
measureCtx.fontKerning = 'normal';
|
|
51
|
+
const { root, height: contentHeight } = buildLayoutTree(measureCtx, tree, width, useDomMeasurements, debug);
|
|
52
|
+
const finalHeight = height || contentHeight;
|
|
53
|
+
const lines = extractLines(root);
|
|
66
54
|
cleanup();
|
|
67
|
-
return {
|
|
55
|
+
return { layoutRoot: root, height: finalHeight, lines };
|
|
56
|
+
}
|
|
57
|
+
// ─── drawLayout() ────────────────────────────────────────────────────
|
|
58
|
+
/**
|
|
59
|
+
* Draw a pre-computed layout onto a canvas or context.
|
|
60
|
+
* Use with layout() to render the same content onto multiple targets.
|
|
61
|
+
*/
|
|
62
|
+
export function drawLayout(config) {
|
|
63
|
+
const { layout: layoutResult, width, pixelRatio = globalThis.devicePixelRatio ?? 1, } = config;
|
|
64
|
+
if (config.ctx && config.canvas) {
|
|
65
|
+
throw new TypeError('drawLayout: ctx and canvas are mutually exclusive — provide one or neither');
|
|
66
|
+
}
|
|
67
|
+
const finalHeight = layoutResult.height;
|
|
68
|
+
let canvas;
|
|
69
|
+
let renderCtx;
|
|
70
|
+
if (config.ctx) {
|
|
71
|
+
renderCtx = config.ctx;
|
|
72
|
+
canvas = config.ctx.canvas;
|
|
73
|
+
}
|
|
74
|
+
else if (config.canvas) {
|
|
75
|
+
canvas = config.canvas;
|
|
76
|
+
canvas.width = Math.ceil(width * pixelRatio);
|
|
77
|
+
canvas.height = Math.ceil(finalHeight * pixelRatio);
|
|
78
|
+
if ('style' in canvas) {
|
|
79
|
+
canvas.style.width = `${width}px`;
|
|
80
|
+
canvas.style.height = `${finalHeight}px`;
|
|
81
|
+
}
|
|
82
|
+
renderCtx = canvas.getContext('2d');
|
|
83
|
+
renderCtx.scale(pixelRatio, pixelRatio);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
canvas = document.createElement('canvas');
|
|
87
|
+
canvas.width = Math.ceil(width * pixelRatio);
|
|
88
|
+
canvas.height = Math.ceil(finalHeight * pixelRatio);
|
|
89
|
+
canvas.style.width = `${width}px`;
|
|
90
|
+
canvas.style.height = `${finalHeight}px`;
|
|
91
|
+
renderCtx = canvas.getContext('2d');
|
|
92
|
+
renderCtx.scale(pixelRatio, pixelRatio);
|
|
93
|
+
}
|
|
94
|
+
renderNode(renderCtx, layoutResult.layoutRoot);
|
|
95
|
+
return { canvas };
|
|
96
|
+
}
|
|
97
|
+
// ─── render() ────────────────────────────────────────────────────────
|
|
98
|
+
/**
|
|
99
|
+
* Render an HTML string onto a canvas using pure 2D canvas API.
|
|
100
|
+
* Convenience function combining layout() + drawLayout().
|
|
101
|
+
* Fonts must already be loaded before calling this function.
|
|
102
|
+
*/
|
|
103
|
+
export function render(config) {
|
|
104
|
+
if (config.ctx && config.canvas) {
|
|
105
|
+
throw new TypeError('render: ctx and canvas are mutually exclusive — provide one or neither');
|
|
106
|
+
}
|
|
107
|
+
const layoutResult = layout({
|
|
108
|
+
html: config.html,
|
|
109
|
+
width: config.width,
|
|
110
|
+
height: config.height,
|
|
111
|
+
accuracy: config.accuracy,
|
|
112
|
+
debug: config.debug,
|
|
113
|
+
});
|
|
114
|
+
const { canvas } = drawLayout({
|
|
115
|
+
layout: layoutResult,
|
|
116
|
+
width: config.width,
|
|
117
|
+
ctx: config.ctx,
|
|
118
|
+
canvas: config.canvas,
|
|
119
|
+
pixelRatio: config.pixelRatio,
|
|
120
|
+
});
|
|
121
|
+
return {
|
|
122
|
+
canvas,
|
|
123
|
+
height: layoutResult.height,
|
|
124
|
+
layoutRoot: layoutResult.layoutRoot,
|
|
125
|
+
lines: layoutResult.lines,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
// ─── Deprecated ──────────────────────────────────────────────────────
|
|
129
|
+
/**
|
|
130
|
+
* @deprecated Use `render()` instead.
|
|
131
|
+
*/
|
|
132
|
+
export function renderHTML(html, options) {
|
|
133
|
+
return render({
|
|
134
|
+
html: options.css ? `<style>${options.css}</style>${html}` : html,
|
|
135
|
+
width: options.width,
|
|
136
|
+
height: options.height,
|
|
137
|
+
canvas: options.canvas,
|
|
138
|
+
pixelRatio: options.pixelRatio ?? 1,
|
|
139
|
+
accuracy: options.useDomMeasurements === false ? 'performance' : 'balanced',
|
|
140
|
+
debug: options.debug,
|
|
141
|
+
});
|
|
68
142
|
}
|
|
69
143
|
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/lib/layout.d.ts.map
CHANGED
|
@@ -1 +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;
|
|
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;AA83CD;;;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"}
|
package/lib/layout.js
CHANGED
|
@@ -309,10 +309,31 @@ function getSegmenter() {
|
|
|
309
309
|
function tokenizeString(ctx, text, run, allWords) {
|
|
310
310
|
// Split on zero-width spaces and soft hyphens (break opportunities)
|
|
311
311
|
if (text.includes('\u200B') || text.includes('\u00AD')) {
|
|
312
|
-
|
|
312
|
+
// Split but keep delimiters to distinguish soft hyphens from zero-width spaces
|
|
313
|
+
const parts = text.split(/(\u200B|\u00AD)/);
|
|
314
|
+
let nextIsSoftHyphen = false;
|
|
313
315
|
for (const part of parts) {
|
|
314
|
-
if (part)
|
|
315
|
-
|
|
316
|
+
if (part === '\u00AD') {
|
|
317
|
+
// Mark the PREVIOUS word as a soft-hyphen break point
|
|
318
|
+
nextIsSoftHyphen = true;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (part === '\u200B' || part === '') {
|
|
322
|
+
nextIsSoftHyphen = false;
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
const prevLen = allWords.length;
|
|
326
|
+
tokenizeString(ctx, part, run, allWords);
|
|
327
|
+
// If the previous delimiter was a soft hyphen, mark the word
|
|
328
|
+
// just before this part as having a soft-hyphen break opportunity
|
|
329
|
+
if (nextIsSoftHyphen && prevLen > 0) {
|
|
330
|
+
allWords[prevLen - 1].isSoftHyphenBreak = true;
|
|
331
|
+
}
|
|
332
|
+
nextIsSoftHyphen = false;
|
|
333
|
+
}
|
|
334
|
+
// If the text ends with a soft hyphen, mark the last word
|
|
335
|
+
if (nextIsSoftHyphen && allWords.length > 0) {
|
|
336
|
+
allWords[allWords.length - 1].isSoftHyphenBreak = true;
|
|
316
337
|
}
|
|
317
338
|
return;
|
|
318
339
|
}
|
|
@@ -509,7 +530,7 @@ function isCJK(char) {
|
|
|
509
530
|
function breakWordIfNeeded(ctx, word, contentWidth, currentLineWidth) {
|
|
510
531
|
// Check if word has CJK characters — always break at character level
|
|
511
532
|
const hasCJK = [...word.text].some(isCJK);
|
|
512
|
-
// Check if word needs break-word splitting
|
|
533
|
+
// Check if word needs break-word splitting — when it won't fit on a fresh line
|
|
513
534
|
const needsBreak = word.width > contentWidth &&
|
|
514
535
|
(word.style.overflowWrap === 'break-word' || word.style.wordBreak === 'break-all');
|
|
515
536
|
if (!hasCJK && !needsBreak)
|
|
@@ -555,13 +576,29 @@ function flowWordsIntoLines(ctx, words, contentWidth, whiteSpace, useBulletProbe
|
|
|
555
576
|
let currentLine = { words: [], totalWidth: 0, lineHeight: 0 };
|
|
556
577
|
const noWrap = whiteSpace === 'nowrap' || whiteSpace === 'pre';
|
|
557
578
|
const isPreWrap = whiteSpace === 'pre-wrap' || whiteSpace === 'pre' || whiteSpace === 'pre-line';
|
|
558
|
-
function pushLine() {
|
|
579
|
+
function pushLine(isSoftWrap = false) {
|
|
559
580
|
const hadWords = currentLine.words.length > 0;
|
|
560
581
|
// Trim trailing spaces
|
|
561
582
|
while (currentLine.words.length > 0 && currentLine.words[currentLine.words.length - 1].isSpace) {
|
|
562
583
|
currentLine.totalWidth -= currentLine.words[currentLine.words.length - 1].width;
|
|
563
584
|
currentLine.words.pop();
|
|
564
585
|
}
|
|
586
|
+
// Soft hyphen: if this is a soft wrap and the last word has a soft-hyphen
|
|
587
|
+
// break, append a visible '-' since the word is being broken here.
|
|
588
|
+
if (isSoftWrap && currentLine.words.length > 0) {
|
|
589
|
+
const lastWord = currentLine.words[currentLine.words.length - 1];
|
|
590
|
+
if (lastWord.isSoftHyphenBreak) {
|
|
591
|
+
applyFont(ctx, lastWord.style);
|
|
592
|
+
const hyphenWidth = ctx.measureText('-').width;
|
|
593
|
+
currentLine.words.push({
|
|
594
|
+
text: '-',
|
|
595
|
+
width: hyphenWidth,
|
|
596
|
+
style: lastWord.style,
|
|
597
|
+
isSpace: false,
|
|
598
|
+
});
|
|
599
|
+
currentLine.totalWidth += hyphenWidth;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
565
602
|
// In pre-wrap mode, space-only lines still need height (they are content)
|
|
566
603
|
if (currentLine.words.length > 0 || (hadWords && isPreWrap)) {
|
|
567
604
|
if (_debug) {
|
|
@@ -609,8 +646,14 @@ function flowWordsIntoLines(ctx, words, contentWidth, whiteSpace, useBulletProbe
|
|
|
609
646
|
? breakWordIfNeeded(ctx, word, contentWidth, currentLine.totalWidth)
|
|
610
647
|
: [word];
|
|
611
648
|
for (const piece of pieces) {
|
|
649
|
+
// Trailing punctuation (e.g. comma after </span>) should not wrap
|
|
650
|
+
// independently — browsers keep it with the preceding word.
|
|
651
|
+
const isTrailingPunct = !piece.isSpace && piece.text.length > 0 &&
|
|
652
|
+
/^[,.\;:!?\)\]\}'"»›]+$/.test(piece.text) &&
|
|
653
|
+
currentLine.words.length > 0 &&
|
|
654
|
+
!currentLine.words[currentLine.words.length - 1].isSpace;
|
|
612
655
|
// Would this piece overflow?
|
|
613
|
-
if (!piece.isSpace && currentLine.words.length > 0 &&
|
|
656
|
+
if (!piece.isSpace && !isTrailingPunct && currentLine.words.length > 0 &&
|
|
614
657
|
currentLine.totalWidth + piece.width > contentWidth) {
|
|
615
658
|
const overflow = currentLine.totalWidth + piece.width - contentWidth;
|
|
616
659
|
// For borderline cases (overflow < 1px), word-by-word delta
|
|
@@ -638,7 +681,7 @@ function flowWordsIntoLines(ctx, words, contentWidth, whiteSpace, useBulletProbe
|
|
|
638
681
|
data: { text: piece.text, overflow, lineWidth: currentLine.totalWidth, pieceWidth: piece.width, contentWidth, lineText },
|
|
639
682
|
});
|
|
640
683
|
}
|
|
641
|
-
pushLine();
|
|
684
|
+
pushLine(true);
|
|
642
685
|
afterHardBreak = false;
|
|
643
686
|
}
|
|
644
687
|
}
|
|
@@ -662,7 +705,7 @@ function flowWordsIntoLines(ctx, words, contentWidth, whiteSpace, useBulletProbe
|
|
|
662
705
|
data: { text: piece.text, remaining, domWidth, contentWidth },
|
|
663
706
|
});
|
|
664
707
|
}
|
|
665
|
-
pushLine();
|
|
708
|
+
pushLine(true);
|
|
666
709
|
afterHardBreak = false;
|
|
667
710
|
}
|
|
668
711
|
}
|