@knowvah/dot-engine 2.6.6 → 2.6.8

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.
Files changed (39) hide show
  1. package/dist/common/htmltable-attr-warn.d.ts +10 -4
  2. package/dist/common/htmltable-attr-warn.d.ts.map +1 -1
  3. package/dist/common/htmltable-label.d.ts +37 -0
  4. package/dist/common/htmltable-label.d.ts.map +1 -0
  5. package/dist/common/htmltable-lex-attvalue.d.ts +59 -0
  6. package/dist/common/htmltable-lex-attvalue.d.ts.map +1 -0
  7. package/dist/common/htmltable-lex-chardata.d.ts +37 -0
  8. package/dist/common/htmltable-lex-chardata.d.ts.map +1 -0
  9. package/dist/common/htmltable-lex-expat.d.ts +26 -13
  10. package/dist/common/htmltable-lex-expat.d.ts.map +1 -1
  11. package/dist/common/htmltable-lex.d.ts.map +1 -1
  12. package/dist/common/htmltable-parse-attrs.d.ts +3 -1
  13. package/dist/common/htmltable-parse-attrs.d.ts.map +1 -1
  14. package/dist/common/htmltable-parse.d.ts +6 -5
  15. package/dist/common/htmltable-parse.d.ts.map +1 -1
  16. package/dist/common/htmltable-pos.d.ts +3 -26
  17. package/dist/common/htmltable-pos.d.ts.map +1 -1
  18. package/dist/common/htmltable-types.d.ts +5 -1
  19. package/dist/common/htmltable-types.d.ts.map +1 -1
  20. package/dist/common/htmltable.d.ts +3 -2
  21. package/dist/common/htmltable.d.ts.map +1 -1
  22. package/dist/common/make-label.d.ts.map +1 -1
  23. package/dist/index.js +310 -144
  24. package/dist/index.js.map +4 -4
  25. package/dist/render.js +310 -144
  26. package/dist/render.js.map +4 -4
  27. package/package.json +1 -1
  28. package/src/common/htmltable-attr-warn.ts +24 -7
  29. package/src/common/htmltable-label.ts +109 -0
  30. package/src/common/htmltable-lex-attvalue.ts +141 -0
  31. package/src/common/htmltable-lex-chardata.ts +95 -0
  32. package/src/common/htmltable-lex-expat.ts +143 -48
  33. package/src/common/htmltable-lex.ts +41 -36
  34. package/src/common/htmltable-parse-attrs.ts +10 -13
  35. package/src/common/htmltable-parse.ts +46 -61
  36. package/src/common/htmltable-pos.ts +5 -88
  37. package/src/common/htmltable-types.ts +6 -1
  38. package/src/common/htmltable.ts +16 -16
  39. package/src/common/make-label.ts +8 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knowvah/dot-engine",
3
- "version": "2.6.6",
3
+ "version": "2.6.8",
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",
@@ -11,7 +11,7 @@
11
11
  * @see lib/common/htmllex.c:doAttrs
12
12
  */
13
13
 
14
- /** An attribute as written: name in source case, value verbatim. */
14
+ /** An attribute as startElement gets it: name in source case, value decoded by expat. */
15
15
  export type AttrEntry = readonly [name: string, value: string];
16
16
 
17
17
  export interface CheckedAttrs {
@@ -247,9 +247,9 @@ export function checkAttrs(tag: string, pairs: readonly AttrEntry[]): CheckedAtt
247
247
  // ---------------------------------------------------------------------------
248
248
 
249
249
  /**
250
- * C's stderr as lines: text is appended, each completed line goes to
251
- * `writeLine`, and a message without a trailing newline stays pending so the
252
- * next write continues it. `flush` emits a pending partial line.
250
+ * C's stderr as messages: a write ending in a newline goes to `writeLine`
251
+ * (newline dropped, embedded newlines kept), while a message without one stays
252
+ * pending so the next write continues it. `flush` emits a pending message.
253
253
  */
254
254
  export class StderrLines {
255
255
  private pending = '';
@@ -257,9 +257,11 @@ export class StderrLines {
257
257
  constructor(private readonly writeLine: (line: string) => void) {}
258
258
 
259
259
  write(text: string): void {
260
- const parts = (this.pending + text).split('\n');
261
- this.pending = parts.pop() ?? '';
262
- for (const line of parts) this.writeLine(line);
260
+ this.pending += text;
261
+ if (!this.pending.endsWith('\n')) return;
262
+ const line = this.pending.slice(0, -1);
263
+ this.pending = '';
264
+ this.writeLine(line);
263
265
  }
264
266
 
265
267
  flush(): void {
@@ -268,3 +270,18 @@ export class StderrLines {
268
270
  this.pending = '';
269
271
  }
270
272
  }
273
+
274
+ /** A finished stderr line on the console channel the port uses for it. */
275
+ export const consoleLine = (line: string): void => {
276
+ if (line.startsWith('Warning')) console.warn(line);
277
+ else console.error(line);
278
+ };
279
+
280
+ /** One label's stderr: what make_html_label and make_label print, in order. */
281
+ export const labelStderr = (): StderrLines => new StderrLines(consoleLine);
282
+
283
+ /** agerr of one newline-terminated message: to `out`, or the console when there is none. */
284
+ export const agerrLine = (out: StderrLines | undefined, msg: string): void => {
285
+ if (out === undefined) consoleLine(msg);
286
+ else out.write(`${msg}\n`);
287
+ };
@@ -0,0 +1,109 @@
1
+ // SPDX-License-Identifier: EPL-2.0
2
+
3
+ /**
4
+ * make_html_label: parse, size and position an HTML-like label, writing what
5
+ * C writes to stderr along the way through one {@link StderrLines}, so a
6
+ * message without a trailing newline (htmllex.c's doInt) runs into the next
7
+ * write: a later warning, the parse error, or make_label's `in label of ...`.
8
+ *
9
+ * @see lib/common/htmltable.c:make_html_label
10
+ */
11
+
12
+ import type { TextlabelT } from './types.js';
13
+ import type { TextMeasurer } from './textmeasure.js';
14
+ import type { HtmlLabel, ImageSizer } from './htmltable-types.js';
15
+ import { sizeHtmlLabel, parseHtmlLabelRv, inheritTablePenColor } from './htmltable.js';
16
+ import { makeLabel } from './make-label.js';
17
+ import { HtmlLexError } from './htmltable-lex.js';
18
+ import { labelStderr, type StderrLines } from './htmltable-attr-warn.js';
19
+ import { posHtmlLabel, type HtmlFontInfo, type PlacedHtml } from './htmltable-pos.js';
20
+
21
+ /** parseHTML's rv for a label that failed to parse (`ctx->error`). */
22
+ const HTML_PARSE_FATAL = 3;
23
+
24
+ /** A built HTML label plus make_html_label's "problem" return value. */
25
+ export interface HtmlLabelResult {
26
+ label: TextlabelT;
27
+ rv: number;
28
+ }
29
+
30
+ /** The label's font, the object's pen color, and the `<IMG>` sizer. */
31
+ export type HtmlLabelInfo = HtmlFontInfo & { imageSizer?: ImageSizer; pencolor?: string };
32
+
33
+ /**
34
+ * make_html_label: parse, size and position an HTML-like label. Returns C's
35
+ * rv too (nonzero: the caller appends `in label of ...`, labels.c:make_label).
36
+ * Everything C prints goes to `stderr`, which the caller owns and flushes once
37
+ * its own continuation is written. The optional `imageSizer` resolves
38
+ * `<IMG SRC>`; absent means C's missing image.
39
+ * @see lib/common/htmltable.c:make_html_label
40
+ */
41
+ export function makeHtmlLabelTo(
42
+ content: string,
43
+ finfo: HtmlLabelInfo,
44
+ measurer: TextMeasurer,
45
+ stderr: StderrLines,
46
+ ): HtmlLabelResult {
47
+ const { fontname, fontsize, fontcolor, imageSizer, pencolor } = finfo;
48
+ let lbl: HtmlLabel, lexWarn: boolean; // lexWarn: htmllex.c's ctx->warn, rv bit 1
49
+ try {
50
+ ({ label: lbl, warn: lexWarn } = parseHtmlLabelRv(content, stderr));
51
+ } catch (e) {
52
+ // C aborts the HTML parse on a syntax error (YYABORT): parseHTML returns
53
+ // rv 3 (ctx->error) with no label, and make_html_label returns it with
54
+ // lp->html cleared and no spans, so the label renders empty.
55
+ // @see lib/common/htmltable.c:1880 (rv == 3), htmlparse.y (YYABORT)
56
+ if (e instanceof HtmlLexError) for (const line of e.diagnostics) stderr.write(`${line}\n`);
57
+ return { label: makeLabel('', fontname, fontsize, fontcolor, measurer), rv: HTML_PARSE_FATAL };
58
+ }
59
+ // A top-level table with no explicit COLOR inherits the object's pen color,
60
+ // which then flows table->cell->nested-table.
61
+ // @see lib/common/htmltable.c:1911 (getPenColor), :1406/:1556 (inheritance)
62
+ if (lbl.kind === 'table') inheritTablePenColor(lbl.table, pencolor);
63
+ const rv = Number(lexWarn) | sizeHtmlLabel(lbl, measurer, {
64
+ fontsize, fontname, imageSizer, stderr,
65
+ });
66
+ const placed = posHtmlLabel(lbl, { fontname, fontsize, fontcolor }, measurer);
67
+ const dim = lbl.dimen ?? { w: 0, h: 0 };
68
+ // C replaces table-label text: "this may be used for the title and
69
+ // alt fields in image maps" — anchors inherit it as default tooltip.
70
+ // @see lib/common/htmltable.c:make_html_label (line 1935)
71
+ const text = lbl.kind === 'table' ? '<TABLE>' : content;
72
+ return { label: htmlTextLabel(text, { fontname, fontsize, fontcolor }, placed, dim), rv };
73
+ }
74
+
75
+ /** {@link makeHtmlLabelTo} on a stderr of its own, flushed before it returns. */
76
+ export function makeHtmlLabelRv(content: string, finfo: HtmlLabelInfo, measurer: TextMeasurer): HtmlLabelResult {
77
+ const stderr = labelStderr();
78
+ try {
79
+ return makeHtmlLabelTo(content, finfo, measurer, stderr);
80
+ } finally {
81
+ stderr.flush();
82
+ }
83
+ }
84
+
85
+ /** {@link makeHtmlLabelRv} without the problem flag. */
86
+ export function makeHtmlLabel(content: string, finfo: HtmlLabelInfo, measurer: TextMeasurer): TextlabelT {
87
+ return makeHtmlLabelRv(content, finfo, measurer).label;
88
+ }
89
+
90
+ /** Build the TextlabelT wrapper around a positioned HTML label. */
91
+ function htmlTextLabel(
92
+ text: string,
93
+ finfo: HtmlFontInfo,
94
+ placed: PlacedHtml,
95
+ dim: { w: number; h: number },
96
+ ): TextlabelT {
97
+ return {
98
+ text,
99
+ fontname: finfo.fontname, fontcolor: finfo.fontcolor, charset: 0,
100
+ fontsize: finfo.fontsize,
101
+ dimen: { x: dim.w, y: dim.h },
102
+ space: { x: dim.w, y: dim.h },
103
+ pos: { x: 0, y: 0 },
104
+ u: { kind: 'html', html: placed },
105
+ valign: 'c'.charCodeAt(0),
106
+ set: false,
107
+ html: true,
108
+ };
109
+ }
@@ -0,0 +1,141 @@
1
+ // SPDX-License-Identifier: EPL-2.0
2
+ /**
3
+ * expat's handling of an attribute value, which `doAttrs` only ever sees
4
+ * decoded: scanAtts tokenizes the quoted value (a `<`, or an `&` that does not
5
+ * start a well-formed reference, is an invalid token), storeAtts then
6
+ * normalizes it (references decoded, white space to spaces) and reports an
7
+ * undefined entity or a reference to a character XML does not allow.
8
+ *
9
+ * Not vendored with graphviz; read from libexpat R_2_6_4.
10
+ * @see expat lib/xmltok_impl.c:PREFIX(scanAtts)
11
+ * @see expat lib/xmltok_impl.c:PREFIX(scanRef)
12
+ * @see expat lib/xmlparse.c:storeAtts
13
+ * @see expat lib/xmlparse.c:appendAttributeValue
14
+ */
15
+
16
+ /** expat `XML_ErrorString(XML_ERROR_UNDEFINED_ENTITY)`. */
17
+ export const XML_ERROR_UNDEFINED_ENTITY = 'undefined entity';
18
+ /** expat `XML_ErrorString(XML_ERROR_BAD_CHAR_REF)`. */
19
+ export const XML_ERROR_BAD_CHAR_REF = 'reference to invalid character number';
20
+
21
+ /** How scanAtts ends a quoted value: at its closing quote, short of data, or on an invalid token. */
22
+ export type AttValueScan = { close: number } | { partial: true } | { invalid: number };
23
+
24
+ /** A storeAtts error: its message and where expat's event pointer is (undefined: the tag's start). */
25
+ export interface AttValueError {
26
+ message: string;
27
+ offset?: number;
28
+ }
29
+
30
+ const NAME_START = /[A-Za-z_:\u0080-￿]/;
31
+ const NAME_CHAR = /[\w.:\u0080-￿-]/;
32
+ const DEC_DIGIT = /[0-9]/;
33
+ const HEX_DIGIT = /[0-9A-Fa-f]/;
34
+ /** Bytes expat's tokenizer rejects anywhere (BT_NONXML): controls other than tab, LF and CR. */
35
+ const NON_XML = /[\x00-\x08\x0b\x0c\x0e-\x1f]/;
36
+
37
+ /** A reference's digits from `i` up to `;`: the index after it, or where scanning stops. */
38
+ function scanDigits(text: string, i: number, digit: RegExp): number | AttValueScan {
39
+ if (i >= text.length) return { partial: true };
40
+ if (!digit.test(text[i]!)) return { invalid: i };
41
+ for (let k = i + 1; k < text.length; k++) {
42
+ if (text[k] === ';') return k + 1;
43
+ if (!digit.test(text[k]!)) return { invalid: k };
44
+ }
45
+ return { partial: true };
46
+ }
47
+
48
+ /**
49
+ * The reference after the `&` at `i - 1`: the index after its `;`, or where
50
+ * scanning stops.
51
+ * @see expat lib/xmltok_impl.c:PREFIX(scanRef)
52
+ * @see expat lib/xmltok_impl.c:PREFIX(scanCharRef)
53
+ */
54
+ function scanRef(text: string, i: number): number | AttValueScan {
55
+ const c = text[i];
56
+ if (c === undefined) return { partial: true };
57
+ if (c === '#') return text[i + 1] === 'x' ? scanDigits(text, i + 2, HEX_DIGIT) : scanDigits(text, i + 1, DEC_DIGIT);
58
+ if (!NAME_START.test(c)) return { invalid: i };
59
+ for (let k = i + 1; k < text.length; k++) {
60
+ if (text[k] === ';') return k + 1;
61
+ if (!NAME_CHAR.test(text[k]!)) return { invalid: k };
62
+ }
63
+ return { partial: true };
64
+ }
65
+
66
+ /**
67
+ * scanAtts over the value whose opening quote is at `j`.
68
+ * @see expat lib/xmltok_impl.c:PREFIX(scanAtts) ("in attribute value")
69
+ */
70
+ export function scanAttValue(text: string, j: number): AttValueScan {
71
+ const q = text[j];
72
+ let k = j + 1;
73
+ for (;;) {
74
+ const c = text[k];
75
+ if (c === undefined) return { partial: true };
76
+ if (c === q) return { close: k };
77
+ if (c === '<' || NON_XML.test(c)) return { invalid: k };
78
+ if (c !== '&') {
79
+ k++;
80
+ continue;
81
+ }
82
+ const r = scanRef(text, k + 1);
83
+ if (typeof r !== 'number') return r;
84
+ k = r;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * XmlCharRefNumber: the code point of `&#...;`, or -1 for one XML does not
90
+ * allow (controls other than tab, LF and CR; surrogates; U+FFFE and U+FFFF;
91
+ * beyond U+10FFFF).
92
+ * @see expat lib/xmltok_impl.c:PREFIX(charRefNumber)
93
+ * @see expat lib/xmltok.c:checkCharRefNumber
94
+ */
95
+ export function charRefNumber(ref: string): number {
96
+ const hex = ref.startsWith('&#x');
97
+ const n = parseInt(ref.slice(hex ? 3 : 2, -1), hex ? 16 : 10);
98
+ if (n >= 0x110000) return -1;
99
+ if (n >= 0xd800 && n <= 0xdfff) return -1;
100
+ if (n < 0x20 && NON_XML.test(String.fromCharCode(n))) return -1;
101
+ return n === 0xfffe || n === 0xffff ? -1 : n;
102
+ }
103
+
104
+ const PREDEFINED: Readonly<Record<string, string>> = { lt: '<', gt: '>', amp: '&', quot: '"', apos: "'" };
105
+
106
+ /** The references of a well-formed value, and its CR LF pairs. */
107
+ const VALUE_PIECE = /&#x?[0-9A-Fa-f]+;|&[^;]+;|\r\n|[\t\n\r]/g;
108
+
109
+ /**
110
+ * storeAtts' check of a well-formed value (`raw`, between the quotes, at
111
+ * `base`): the first undefined entity or disallowed character reference. No
112
+ * DTD, so only the five predefined entities exist.
113
+ * @see expat lib/xmlparse.c:appendAttributeValue
114
+ */
115
+ export function attValueError(raw: string, base: number): AttValueError | undefined {
116
+ for (const m of raw.matchAll(VALUE_PIECE)) {
117
+ const ref = m[0];
118
+ if (!ref.startsWith('&')) continue;
119
+ if (ref.startsWith('&#')) {
120
+ if (charRefNumber(ref) < 0) return { message: XML_ERROR_BAD_CHAR_REF, offset: base + m.index };
121
+ } else if (!Object.hasOwn(PREDEFINED, ref.slice(1, -1))) {
122
+ return { message: XML_ERROR_UNDEFINED_ENTITY };
123
+ }
124
+ }
125
+ return undefined;
126
+ }
127
+
128
+ /**
129
+ * appendAttributeValue for a CDATA attribute (no DTD declares one otherwise)
130
+ * on a value storeAtts accepted: references become their characters, which
131
+ * stay as they are (a `&#9;` is a tab); a tab, LF, CR or CR LF in the source
132
+ * becomes one space.
133
+ * @see expat lib/xmlparse.c:appendAttributeValue
134
+ */
135
+ export function decodeAttValue(raw: string): string {
136
+ return raw.replace(VALUE_PIECE, (m) => {
137
+ if (!m.startsWith('&')) return ' ';
138
+ if (m.startsWith('&#')) return String.fromCodePoint(charRefNumber(m));
139
+ return PREDEFINED[m.slice(1, -1)]!;
140
+ });
141
+ }
@@ -0,0 +1,95 @@
1
+ // SPDX-License-Identifier: EPL-2.0
2
+ /**
3
+ * The htmllex state characterData reads: `ctx->inCell`, which the element
4
+ * handlers set, and `ctx->xb`, the buffer it appends to. Every handler assigns
5
+ * `ctx->tok`, so when one `XML_Parse` call fires several events only the last
6
+ * token reaches the parser, but characters an earlier event appended stay in
7
+ * xb: they lead the next string the parser reduces, or count in the table
8
+ * rule's nonSpace check.
9
+ *
10
+ * @see lib/common/htmllex.c:startElement
11
+ * @see lib/common/htmllex.c:endElement
12
+ * @see lib/common/htmllex.c:characterData
13
+ */
14
+
15
+ import { htmlEntityUTF8 } from './html-entities.js';
16
+ import type { TagChunk } from './htmltable-lex-expat.js';
17
+ import type { Token } from './htmltable-types.js';
18
+
19
+ /** `ctx->inCell`, and the text in `ctx->xb` that no token carried to the parser. */
20
+ export interface CharDataState {
21
+ inCell: boolean;
22
+ xb: string;
23
+ }
24
+
25
+ /** initHTMLlexer: text is kept until the first TABLE or TR. @see lib/common/htmllex.c:initHTMLlexer */
26
+ export const initialCharData = (): CharDataState => ({ inCell: true, xb: '' });
27
+
28
+ /** startElement's assignment to inCell for element `name` (upper case). */
29
+ function startInCell(name: string, inCell: boolean): boolean {
30
+ if (name === 'TABLE' || name === 'TR' || name === 'TH') return false;
31
+ return name === 'TD' ? true : inCell;
32
+ }
33
+
34
+ /** endElement's assignment to inCell for element `name` (upper case). */
35
+ function endInCell(name: string, inCell: boolean): boolean {
36
+ if (name === 'TABLE') return true;
37
+ return name === 'TD' ? false : inCell;
38
+ }
39
+
40
+ /** Run a tag's element handlers on inCell; `<X/>` runs startElement, then endElement. */
41
+ export function tagInCell(st: CharDataState, tag: TagChunk): void {
42
+ const name = tag.name.toUpperCase();
43
+ if (tag.close) {
44
+ st.inCell = endInCell(name, st.inCell);
45
+ return;
46
+ }
47
+ st.inCell = startInCell(name, st.inCell);
48
+ if (tag.selfClose) st.inCell = endInCell(name, st.inCell);
49
+ }
50
+
51
+ /** Keep the text of tokens a later event in the same call overwrote. */
52
+ export function keepLostText(st: CharDataState, lost: readonly Token[]): void {
53
+ for (const t of lost) if (t.type === 'text') st.xb += t.value;
54
+ }
55
+
56
+ /**
57
+ * Hand xb's kept text to the token the parser is about to get: a string
58
+ * starts with it, and a TABLE carries it for the table rule's nonSpace check.
59
+ * Any other token leaves it in xb for the next of those.
60
+ * @see lib/common/htmlparse.y:table
61
+ */
62
+ export function releaseXb(st: CharDataState, first: Token): void {
63
+ if (st.xb === '') return;
64
+ if (first.type === 'text') first.value = st.xb + first.value;
65
+ else if (first.type === 'open' && first.tag === 'TABLE') first.heldText = st.xb;
66
+ else return;
67
+ st.xb = '';
68
+ }
69
+
70
+ /**
71
+ * characterData keeps each byte `>= ' '` of the text expat decoded: anything
72
+ * below 0x20 goes (tab, LF and CR too, from the source or a character
73
+ * reference); DEL and non-ASCII stay.
74
+ * @see lib/common/htmllex.c:characterData
75
+ */
76
+ function cleanText(decoded: string) {
77
+ return decoded.replace(/[\x00-\x1f]/g, '');
78
+ }
79
+
80
+ /** The text token of a text run, if anything is left after cleaning. */
81
+ export function textTokens(raw: string): Token[] {
82
+ // Decode XML/HTML entities in the text run. C's HTML lexer resolves entities
83
+ // via expat during tokenization (htmllex.c), so both sizing and emit see the
84
+ // decoded UTF-8 text. Skipping this measured the raw "&#91;el..." (9-10 chars)
85
+ // instead of "[el..." (6 chars), inflating edge-label vnodes and shifting
86
+ // layout (corpus 1949: +18.7px). Plain-string labels already decode via the
87
+ // make_label path, so only HTML-delimited labels diverged.
88
+ return charDataTokens(htmlEntityUTF8(raw));
89
+ }
90
+
91
+ /** characterData on what expat decoded: a T_string when any character survives. */
92
+ export function charDataTokens(decoded: string): Token[] {
93
+ const clean = cleanText(decoded);
94
+ return clean.length > 0 ? [{ type: 'text', value: clean }] : [];
95
+ }