@knowvah/dot-engine 2.6.9 → 2.6.11
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/dist/common/html-entities.d.ts +13 -5
- package/dist/common/html-entities.d.ts.map +1 -1
- package/dist/common/htmltable-emit-fill.d.ts +1 -1
- package/dist/common/htmltable-emit-fill.d.ts.map +1 -1
- package/dist/common/htmltable-emit.d.ts +8 -0
- package/dist/common/htmltable-emit.d.ts.map +1 -1
- package/dist/common/htmltable-lex-chardata.d.ts +9 -0
- package/dist/common/htmltable-lex-chardata.d.ts.map +1 -1
- package/dist/common/htmltable-style.d.ts +22 -0
- package/dist/common/htmltable-style.d.ts.map +1 -0
- package/dist/index.js +345 -284
- package/dist/index.js.map +4 -4
- package/dist/render.js +95 -34
- package/dist/render.js.map +4 -4
- package/package.json +1 -1
- package/src/common/html-entities.ts +77 -25
- package/src/common/htmltable-emit-fill.ts +7 -6
- package/src/common/htmltable-emit.ts +5 -22
- package/src/common/htmltable-lex-chardata.ts +42 -8
- package/src/common/htmltable-style.ts +38 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knowvah/dot-engine",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.11",
|
|
4
4
|
"description": "Faithful TypeScript port of Graphviz (dot, neato, fdp, sfdp, circo, twopi, osage, patchwork) — no C: no native binary, no WASM. Oracle-verified against the C implementation, runs in the browser.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"graphviz",
|
|
@@ -260,40 +260,92 @@ export const HTML_ENTITIES: Readonly<Record<string, number>> = {
|
|
|
260
260
|
"zwnj": 8204,
|
|
261
261
|
};
|
|
262
262
|
|
|
263
|
-
/**
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
263
|
+
/**
|
|
264
|
+
* Longest name htmlEntity looks up: its scan for the `;` stops after this
|
|
265
|
+
* many characters.
|
|
266
|
+
* @see lib/common/entities.h:ENTITY_NAME_LENGTH_MAX
|
|
267
|
+
*/
|
|
268
|
+
const ENTITY_NAME_LENGTH_MAX = 8;
|
|
267
269
|
|
|
268
|
-
/**
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
270
|
+
/** htmlEntity's result: the value (0 for none) and where scanning resumes. */
|
|
271
|
+
interface EntityScan {
|
|
272
|
+
value: number;
|
|
273
|
+
next: number;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* The `#` branch of htmlEntity, `at` on the `#`. It reads up to six decimal
|
|
278
|
+
* or five hex digits (`x` or `X`); the `;` must come within the first eight
|
|
279
|
+
* characters, else nothing is consumed. A `;` right after the digits, even
|
|
280
|
+
* none, is consumed with value `n`, so `&#;` keeps its `&` and loses `#;`.
|
|
281
|
+
* @see lib/common/utils.c:htmlEntity
|
|
282
|
+
*/
|
|
283
|
+
function numericEntity(s: string, at: number): EntityScan {
|
|
284
|
+
const hex = s[at + 1] === 'x' || s[at + 1] === 'X';
|
|
285
|
+
const digit = hex ? /^[0-9A-Fa-f]$/ : /^[0-9]$/;
|
|
286
|
+
let n = 0;
|
|
287
|
+
let i = hex ? 2 : 1;
|
|
288
|
+
for (; i < 8; i++) {
|
|
289
|
+
const c = s[at + i] ?? '';
|
|
290
|
+
if (!digit.test(c)) break;
|
|
291
|
+
n = n * (hex ? 16 : 10) + parseInt(c, 16);
|
|
292
|
+
}
|
|
293
|
+
if (i < 8 && s[at + i] === ';') return { value: n, next: at + i + 1 };
|
|
294
|
+
return { value: 0, next: at };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* The name branch of htmlEntity, `at` on the first name character: a `;`
|
|
299
|
+
* within ENTITY_NAME_LENGTH_MAX characters ends a name looked up in
|
|
300
|
+
* entities.h (case-sensitive; no `apos`).
|
|
301
|
+
* @see lib/common/utils.c:htmlEntity
|
|
302
|
+
*/
|
|
303
|
+
function namedEntity(s: string, at: number): EntityScan {
|
|
304
|
+
for (let i = 0; i < ENTITY_NAME_LENGTH_MAX; i++) {
|
|
305
|
+
const c = s[at + i];
|
|
306
|
+
if (c === undefined || c === '\0') break;
|
|
307
|
+
if (c === ';') {
|
|
308
|
+
const key = s.slice(at, at + i);
|
|
309
|
+
if (Object.hasOwn(HTML_ENTITIES, key)) return { value: HTML_ENTITIES[key]!, next: at + i + 1 };
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return { value: 0, next: at };
|
|
273
314
|
}
|
|
274
315
|
|
|
275
316
|
/**
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
* plus the full HTML 4 table (Symbol etc.). @see lib/common/utils.c:htmlEntity
|
|
317
|
+
* Check for an HTML entity at `at`, the character after an `&`.
|
|
318
|
+
* @see lib/common/utils.c:htmlEntity
|
|
279
319
|
*/
|
|
280
|
-
function
|
|
281
|
-
|
|
282
|
-
return BASIC_ENTITIES[body] ?? HTML_ENTITIES[body] ?? null;
|
|
320
|
+
function htmlEntity(s: string, at: number): EntityScan {
|
|
321
|
+
return s[at] === '#' ? numericEntity(s, at) : namedEntity(s, at);
|
|
283
322
|
}
|
|
284
323
|
|
|
285
324
|
/**
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
* Latin-1 inputs are decoded at the
|
|
289
|
-
* survey's render-one), so this
|
|
290
|
-
*
|
|
325
|
+
* Substitute entities like `{` and `&` with their characters: C's
|
|
326
|
+
* make_label UTF-8 branch. An `&` whose entity has no value stays (scanning
|
|
327
|
+
* resumes where htmlEntity left it). Latin-1 inputs are decoded at the
|
|
328
|
+
* byte->string boundary by the caller (the survey's render-one), so this
|
|
329
|
+
* covers them too, as does latin1ToUTF8's identical entity handling.
|
|
330
|
+
*
|
|
331
|
+
* C appends a value's bytes itself: below 0x7F one byte, below 0x7FF two,
|
|
332
|
+
* else three, truncated to `char`. For every value whose bytes are valid
|
|
333
|
+
* UTF-8 that is the character itself. 0x7F, 0x7FF, surrogates and values
|
|
334
|
+
* above 0xFFFF come out as invalid UTF-8, which a JS string cannot hold; the
|
|
335
|
+
* port keeps the character there (documented divergence).
|
|
336
|
+
* @see lib/common/utils.c:htmlEntityUTF8
|
|
337
|
+
* @see lib/common/utils.c:latin1ToUTF8
|
|
291
338
|
* @see lib/common/labels.c:make_label (CHAR_LATIN1 vs default)
|
|
292
339
|
*/
|
|
293
340
|
export function htmlEntityUTF8(s: string): string {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
341
|
+
let amp = s.indexOf('&');
|
|
342
|
+
if (amp < 0) return s;
|
|
343
|
+
let out = '';
|
|
344
|
+
let from = 0;
|
|
345
|
+
for (; amp >= 0; amp = s.indexOf('&', from)) {
|
|
346
|
+
const e = htmlEntity(s, amp + 1);
|
|
347
|
+
out += s.slice(from, amp) + (e.value ? String.fromCodePoint(e.value) : '&');
|
|
348
|
+
from = e.next;
|
|
349
|
+
}
|
|
350
|
+
return out + s.slice(from);
|
|
299
351
|
}
|
|
@@ -18,7 +18,7 @@ import { FillType, PenType } from '../gvc/context.js';
|
|
|
18
18
|
import { transformPoint } from '../gvc/device.js';
|
|
19
19
|
import { resolveRenderColor } from '../render/color-resolve.js';
|
|
20
20
|
import { emitRoundedBezier } from './poly-shapes.js';
|
|
21
|
-
import {
|
|
21
|
+
import { htmlStyle } from './htmltable-style.js';
|
|
22
22
|
import {
|
|
23
23
|
BORDER_LEFT,
|
|
24
24
|
BORDER_TOP,
|
|
@@ -94,7 +94,7 @@ export interface HtmlPaint {
|
|
|
94
94
|
pen?: string;
|
|
95
95
|
/** Pen width; default 1.0 (omitted from SVG at 1.0). */
|
|
96
96
|
penWidth?: number;
|
|
97
|
-
/**
|
|
97
|
+
/** HTML STYLE string; its dashed/dotted flags map to the pen type. */
|
|
98
98
|
penStyle?: string;
|
|
99
99
|
/**
|
|
100
100
|
* Second/stop color of a gradient fill. Presence switches the fill from
|
|
@@ -137,10 +137,11 @@ export function resetHtmlFillPenWidth(): void {
|
|
|
137
137
|
htmlFillPen = 1.0;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
/** Map
|
|
140
|
+
/** Map an HTML STYLE to the C pen type: dashed wins over dotted. @see lib/common/htmltable.c:doBorder */
|
|
141
141
|
function penTypeOf(style: string | undefined): PenType {
|
|
142
|
-
|
|
143
|
-
if (
|
|
142
|
+
const st = htmlStyle(style);
|
|
143
|
+
if (st.dashed) return PenType.Dashed;
|
|
144
|
+
if (st.dotted) return PenType.Dotted;
|
|
144
145
|
return PenType.Solid;
|
|
145
146
|
}
|
|
146
147
|
|
|
@@ -277,7 +278,7 @@ export function doBorder(
|
|
|
277
278
|
// value persists and leaks into the next bgcolor fill's stroke-width.
|
|
278
279
|
htmlFillPen = border;
|
|
279
280
|
withHtmlPaint({ pen: color, penWidth: border, penStyle: style }, job, () => {
|
|
280
|
-
if (
|
|
281
|
+
if (htmlStyle(style).rounded) {
|
|
281
282
|
// C: round_corners(job, mkPts(AF,b,border), 4, {rounded}, 0) — unfilled
|
|
282
283
|
// rounded border path; emitRoundedBezier transforms the AF ring itself.
|
|
283
284
|
emitRoundedBezier(mkPts(box, border, pos), { x: 0, y: 0 }, false, { renderer, job });
|
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
htmlFillPenWidth, resetHtmlFillPenWidth, type HtmlPaint,
|
|
36
36
|
} from './htmltable-emit-fill.js';
|
|
37
37
|
import { emitRoundedBezier } from './poly-shapes.js';
|
|
38
|
-
import {
|
|
38
|
+
import { htmlStyle } from './htmltable-style.js';
|
|
39
39
|
import { emitHtmlRules, initHtmlAnchor, endHtmlAnchor, htmlObjImgscale } from './htmltable-emit-rules.js';
|
|
40
40
|
|
|
41
41
|
export type { PlacedHtml, PlacedCell, PlacedLine, PlacedImage };
|
|
@@ -125,7 +125,7 @@ function bgFillPaint(ctx: BgFillCtx): HtmlPaint {
|
|
|
125
125
|
if (spec === null) return { fill: ctx.bgcolor, penWidth };
|
|
126
126
|
return {
|
|
127
127
|
fill: spec[0], stop: spec[1], gradientAngle: ctx.gradientangle ?? 0,
|
|
128
|
-
radial: ctx.style
|
|
128
|
+
radial: htmlStyle(ctx.style).radial, penWidth,
|
|
129
129
|
};
|
|
130
130
|
}
|
|
131
131
|
|
|
@@ -139,7 +139,7 @@ function bgFillPaint(ctx: BgFillCtx): HtmlPaint {
|
|
|
139
139
|
function emitBgFill(ctx: BgFillCtx): void {
|
|
140
140
|
const { box, pos, border, renderer, job } = ctx;
|
|
141
141
|
withHtmlPaint(bgFillPaint(ctx), job, () => {
|
|
142
|
-
if (
|
|
142
|
+
if (htmlStyle(ctx.style).rounded) {
|
|
143
143
|
emitRoundedBezier(mkPts(box, border, pos), { x: 0, y: 0 }, true, { renderer, job });
|
|
144
144
|
return;
|
|
145
145
|
}
|
|
@@ -258,23 +258,6 @@ export function emitHtmlImg(
|
|
|
258
258
|
*
|
|
259
259
|
* @see lib/common/htmltable.c:emit_html_cell
|
|
260
260
|
*/
|
|
261
|
-
/**
|
|
262
|
-
* True when an HTML element's STYLE declares it invisible.
|
|
263
|
-
*
|
|
264
|
-
* HTML STYLE is lexed by its OWN parser, not the node/edge one: tokens are
|
|
265
|
-
* split on space or comma, compared case-insensitively, and BOTH "INVIS" and
|
|
266
|
-
* "INVISIBLE" set the flag. (The node/edge parser splits on comma only and
|
|
267
|
-
* matches exact-case, so it cannot be reused here.)
|
|
268
|
-
* @see lib/common/htmllex.c:168 (DELIM " ,", strview_case_str_eq)
|
|
269
|
-
*/
|
|
270
|
-
function htmlStyleInvisible(style: string | undefined): boolean {
|
|
271
|
-
if (style === undefined) return false;
|
|
272
|
-
return style.split(/[ ,]+/).some((tok) => {
|
|
273
|
-
const t = tok.toUpperCase();
|
|
274
|
-
return t === 'INVIS' || t === 'INVISIBLE';
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
|
|
278
261
|
export function emitHtmlCell(
|
|
279
262
|
cell: PlacedCell,
|
|
280
263
|
pos: Point,
|
|
@@ -287,7 +270,7 @@ export function emitHtmlCell(
|
|
|
287
270
|
);
|
|
288
271
|
// An invisible cell emits no fill, no border and no content — only the
|
|
289
272
|
// anchor, which C opens before the gate. @see htmltable.c:emit_html_cell:643
|
|
290
|
-
if (
|
|
273
|
+
if (htmlStyle(cell.style).invisible) {
|
|
291
274
|
if (inAnchor) endHtmlAnchor(renderer, job);
|
|
292
275
|
return;
|
|
293
276
|
}
|
|
@@ -354,7 +337,7 @@ export function emitHtmlLabel(
|
|
|
354
337
|
const inAnchor = clustersLast ? false : initHtmlAnchor(anchorData, tblBox, renderer, job);
|
|
355
338
|
// An invisible TABLE suppresses its fill, ALL its cells, and its rules and
|
|
356
339
|
// border — the whole block C guards at emit_html_tbl:542.
|
|
357
|
-
const tblInvisible =
|
|
340
|
+
const tblInvisible = htmlStyle(placed.style).invisible;
|
|
358
341
|
if (!tblInvisible) {
|
|
359
342
|
if (placed.bgcolor !== undefined) {
|
|
360
343
|
emitBgFill({ bgcolor: placed.bgcolor, box: placed.box, pos, border: placed.border,
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
* @see lib/common/htmllex.c:characterData
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import {
|
|
15
|
+
import { HTML_ENTITIES } from './html-entities.js';
|
|
16
|
+
import { charRefNumber } from './htmltable-lex-attvalue.js';
|
|
16
17
|
import type { TagChunk } from './htmltable-lex-expat.js';
|
|
17
18
|
import type { Token } from './htmltable-types.js';
|
|
18
19
|
|
|
@@ -77,15 +78,48 @@ function cleanText(decoded: string) {
|
|
|
77
78
|
return decoded.replace(/[\x00-\x1f]/g, '');
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
/** expat's predefined entities: no DTD declares others. */
|
|
82
|
+
const XML_PREDEFINED: Readonly<Record<string, string>> = { lt: '<', gt: '>', amp: '&', quot: '"', apos: "'" };
|
|
83
|
+
|
|
84
|
+
/** Shortest and longest name scanEntity looks up. @see lib/common/utils.c:scanEntity */
|
|
85
|
+
const SCAN_ENTITY_MIN = 2;
|
|
86
|
+
const SCAN_ENTITY_MAX = 8;
|
|
87
|
+
|
|
88
|
+
const TEXT_REF = /&(#x[0-9A-Fa-f]+|#[0-9]+|[^&;]+);/g;
|
|
89
|
+
|
|
90
|
+
/** One reference of a text run, as scanEntity then expat leave it. */
|
|
91
|
+
function textRefValue(ref: string, body: string): string {
|
|
92
|
+
if (body[0] === '#') {
|
|
93
|
+
const v = charRefNumber(ref);
|
|
94
|
+
return v < 0 ? ref : String.fromCodePoint(v);
|
|
95
|
+
}
|
|
96
|
+
const n = body.length;
|
|
97
|
+
if (n >= SCAN_ENTITY_MIN && n <= SCAN_ENTITY_MAX && Object.hasOwn(HTML_ENTITIES, body)) {
|
|
98
|
+
return String.fromCodePoint(HTML_ENTITIES[body]!);
|
|
99
|
+
}
|
|
100
|
+
return Object.hasOwn(XML_PREDEFINED, body) ? XML_PREDEFINED[body]! : ref;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The characters expat hands characterData for a text run: scanEntity first
|
|
105
|
+
* rewrites each entities.h name to `&#N;` (its `&` only when no `#` follows),
|
|
106
|
+
* then expat decodes character references, of any length, and its five
|
|
107
|
+
* predefined entities. Runs expat rejects never get here (textRefError).
|
|
108
|
+
* @see lib/common/htmllex.c:findNext
|
|
109
|
+
* @see lib/common/utils.c:scanEntity
|
|
110
|
+
*/
|
|
111
|
+
export function decodeTextRefs(raw: string): string {
|
|
112
|
+
return raw.indexOf('&') < 0 ? raw : raw.replace(TEXT_REF, textRefValue);
|
|
113
|
+
}
|
|
114
|
+
|
|
80
115
|
/** The text token of a text run, if anything is left after cleaning. */
|
|
81
116
|
export function textTokens(raw: string): Token[] {
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
return charDataTokens(htmlEntityUTF8(raw));
|
|
117
|
+
// C's HTML lexer resolves entities via scanEntity and expat during
|
|
118
|
+
// tokenization (htmllex.c), so both sizing and emit see the decoded text.
|
|
119
|
+
// Skipping this measured the raw "[el..." (9-10 chars) instead of
|
|
120
|
+
// "[el..." (6 chars), inflating edge-label vnodes and shifting layout
|
|
121
|
+
// (corpus 1949: +18.7px).
|
|
122
|
+
return charDataTokens(decodeTextRefs(raw));
|
|
89
123
|
}
|
|
90
124
|
|
|
91
125
|
/** characterData on what expat decoded: a T_string when any character survives. */
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EPL-2.0
|
|
2
|
+
/**
|
|
3
|
+
* The HTML STYLE flags. C's stylefn sets an htmlstyle_t bitfield at lex time;
|
|
4
|
+
* the port keeps the legal tokens as a string (htmltable-attr-warn.ts joins
|
|
5
|
+
* them with ' ') and reads the flags back here, token by token in order, so
|
|
6
|
+
* SOLID still clears only the DOTTED/DASHED before it. HTML STYLE is not the
|
|
7
|
+
* node/edge style: tokens split on space or comma and compare
|
|
8
|
+
* case-insensitively, so `parseStyleFlags` cannot read it.
|
|
9
|
+
*
|
|
10
|
+
* @see lib/common/htmltable.h:htmlstyle_t
|
|
11
|
+
* @see lib/common/htmllex.c:stylefn (DELIM " ,", strview_case_str_eq)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** @see lib/common/htmltable.h:htmlstyle_t */
|
|
15
|
+
export interface HtmlStyle {
|
|
16
|
+
radial: boolean;
|
|
17
|
+
rounded: boolean;
|
|
18
|
+
invisible: boolean;
|
|
19
|
+
dotted: boolean;
|
|
20
|
+
dashed: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Read the htmlstyle_t flags from a kept HTML STYLE string. */
|
|
24
|
+
export function htmlStyle(style: string | undefined): HtmlStyle {
|
|
25
|
+
const s: HtmlStyle = { radial: false, rounded: false, invisible: false, dotted: false, dashed: false };
|
|
26
|
+
if (style === undefined) return s;
|
|
27
|
+
for (const tk of style.split(/[ ,]+/)) {
|
|
28
|
+
switch (tk.toUpperCase()) {
|
|
29
|
+
case 'ROUNDED': s.rounded = true; break;
|
|
30
|
+
case 'RADIAL': s.radial = true; break;
|
|
31
|
+
case 'SOLID': s.dotted = false; s.dashed = false; break;
|
|
32
|
+
case 'INVISIBLE': case 'INVIS': s.invisible = true; break;
|
|
33
|
+
case 'DOTTED': s.dotted = true; break;
|
|
34
|
+
case 'DASHED': s.dashed = true; break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return s;
|
|
38
|
+
}
|