@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.
- package/dist/common/htmltable-attr-warn.d.ts +10 -4
- package/dist/common/htmltable-attr-warn.d.ts.map +1 -1
- package/dist/common/htmltable-label.d.ts +37 -0
- package/dist/common/htmltable-label.d.ts.map +1 -0
- package/dist/common/htmltable-lex-attvalue.d.ts +59 -0
- package/dist/common/htmltable-lex-attvalue.d.ts.map +1 -0
- package/dist/common/htmltable-lex-chardata.d.ts +37 -0
- package/dist/common/htmltable-lex-chardata.d.ts.map +1 -0
- package/dist/common/htmltable-lex-expat.d.ts +26 -13
- package/dist/common/htmltable-lex-expat.d.ts.map +1 -1
- package/dist/common/htmltable-lex.d.ts.map +1 -1
- package/dist/common/htmltable-parse-attrs.d.ts +3 -1
- package/dist/common/htmltable-parse-attrs.d.ts.map +1 -1
- package/dist/common/htmltable-parse.d.ts +6 -5
- package/dist/common/htmltable-parse.d.ts.map +1 -1
- package/dist/common/htmltable-pos.d.ts +3 -26
- package/dist/common/htmltable-pos.d.ts.map +1 -1
- package/dist/common/htmltable-types.d.ts +5 -1
- package/dist/common/htmltable-types.d.ts.map +1 -1
- package/dist/common/htmltable.d.ts +3 -2
- package/dist/common/htmltable.d.ts.map +1 -1
- package/dist/common/make-label.d.ts.map +1 -1
- package/dist/index.js +310 -144
- package/dist/index.js.map +4 -4
- package/dist/render.js +310 -144
- package/dist/render.js.map +4 -4
- package/package.json +1 -1
- package/src/common/htmltable-attr-warn.ts +24 -7
- package/src/common/htmltable-label.ts +109 -0
- package/src/common/htmltable-lex-attvalue.ts +141 -0
- package/src/common/htmltable-lex-chardata.ts +95 -0
- package/src/common/htmltable-lex-expat.ts +143 -48
- package/src/common/htmltable-lex.ts +41 -36
- package/src/common/htmltable-parse-attrs.ts +10 -13
- package/src/common/htmltable-parse.ts +46 -61
- package/src/common/htmltable-pos.ts +5 -88
- package/src/common/htmltable-types.ts +6 -1
- package/src/common/htmltable.ts +16 -16
- 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.
|
|
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
|
|
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
|
|
251
|
-
*
|
|
252
|
-
* next write continues it. `flush` emits a pending
|
|
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
|
-
|
|
261
|
-
this.pending
|
|
262
|
-
|
|
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 `	` 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 "[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
|
+
}
|