@knowvah/dot-engine 2.6.2 → 2.6.4
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-lex.d.ts +38 -0
- package/dist/common/htmltable-lex.d.ts.map +1 -1
- package/dist/common/htmltable-parse-attrs.d.ts +41 -0
- package/dist/common/htmltable-parse-attrs.d.ts.map +1 -0
- package/dist/common/htmltable-parse.d.ts +3 -0
- package/dist/common/htmltable-parse.d.ts.map +1 -1
- package/dist/common/htmltable-pos.d.ts +17 -0
- package/dist/common/htmltable-pos.d.ts.map +1 -1
- package/dist/common/htmltable.d.ts +13 -1
- package/dist/common/htmltable.d.ts.map +1 -1
- package/dist/common/make-label.d.ts.map +1 -1
- package/dist/common/spline-midpoint.d.ts +8 -0
- package/dist/common/spline-midpoint.d.ts.map +1 -1
- package/dist/index.js +621 -323
- package/dist/index.js.map +4 -4
- package/dist/label/node.d.ts +7 -1
- package/dist/label/node.d.ts.map +1 -1
- package/dist/layout/circo/position-helpers.d.ts +15 -3
- package/dist/layout/circo/position-helpers.d.ts.map +1 -1
- package/dist/layout/circo/position.d.ts +17 -1
- package/dist/layout/circo/position.d.ts.map +1 -1
- package/dist/layout/dot/ortho-adapter.d.ts.map +1 -1
- package/dist/layout/dot/splines-flat-labeled.d.ts.map +1 -1
- package/dist/layout/neato/multispline.d.ts.map +1 -1
- package/dist/layout/pack/poly-pack.d.ts +2 -18
- package/dist/layout/pack/poly-pack.d.ts.map +1 -1
- package/dist/pathplan/shortest.d.ts.map +1 -1
- package/dist/render.js +774 -476
- package/dist/render.js.map +4 -4
- package/package.json +1 -1
- package/src/common/htmltable-lex.ts +265 -5
- package/src/common/htmltable-parse-attrs.ts +166 -0
- package/src/common/htmltable-parse.ts +262 -242
- package/src/common/htmltable-pos.ts +37 -38
- package/src/common/htmltable.ts +139 -45
- package/src/common/make-label.ts +19 -3
- package/src/common/spline-midpoint.ts +30 -9
- package/src/label/node.ts +8 -2
- package/src/layout/circo/position-helpers.ts +28 -17
- package/src/layout/circo/position.ts +28 -7
- package/src/layout/dot/__fixtures__/2796-flat-labeled-lost.dot +161 -0
- package/src/layout/dot/ortho-adapter.ts +20 -9
- package/src/layout/dot/splines-flat-labeled.ts +3 -1
- package/src/layout/neato/multispline.ts +17 -5
- package/src/layout/pack/poly-pack.ts +5 -31
- package/src/pathplan/shortest.ts +15 -2
|
@@ -8,63 +8,27 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type {
|
|
11
|
-
HtmlAlign,
|
|
12
11
|
HtmlCell,
|
|
13
12
|
HtmlCellContent,
|
|
14
|
-
HtmlHR,
|
|
15
13
|
HtmlImage,
|
|
16
14
|
HtmlLabel,
|
|
17
15
|
HtmlRow,
|
|
18
16
|
HtmlTable,
|
|
19
17
|
HtmlText,
|
|
20
18
|
HtmlTextItem,
|
|
21
|
-
HtmlVAlign,
|
|
22
19
|
Token,
|
|
23
20
|
OpenToken,
|
|
24
21
|
} from './htmltable-types.js';
|
|
22
|
+
import { HtmlParseError } from './htmltable-types.js';
|
|
23
|
+
import { htmlErrorAtToken, tokenizeDeferred } from './htmltable-lex.js';
|
|
25
24
|
import {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
} from './htmltable-
|
|
33
|
-
import { tokenize } from './htmltable-lex.js';
|
|
34
|
-
import { htmlEntityUTF8 } from './html-entities.js';
|
|
35
|
-
|
|
36
|
-
/** Decode XML/HTML entities in an anchor attribute value (href/target), as C's
|
|
37
|
-
* HTML parser does. The stored value is the real character (`&` → `&`), so
|
|
38
|
-
* imap emits it raw and cmapx re-escapes it. Undefined passes through. */
|
|
39
|
-
function anchorAttr(v: string | undefined): string | undefined {
|
|
40
|
-
return v === undefined ? undefined : htmlEntityUTF8(v);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
// Attribute helpers
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
|
-
|
|
47
|
-
const parseAlign = (v: string): HtmlAlign | undefined => {
|
|
48
|
-
const u = v.toUpperCase();
|
|
49
|
-
if (u === 'LEFT') return 'left';
|
|
50
|
-
if (u === 'RIGHT') return 'right';
|
|
51
|
-
if (u === 'CENTER') return 'center';
|
|
52
|
-
return undefined;
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
const parseVAlign = (v: string): HtmlVAlign | undefined => {
|
|
56
|
-
const u = v.toUpperCase();
|
|
57
|
-
if (u === 'TOP') return 'top';
|
|
58
|
-
if (u === 'BOTTOM') return 'bottom';
|
|
59
|
-
if (u === 'MIDDLE') return 'middle';
|
|
60
|
-
return undefined;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
const num = (s: string | undefined): number | undefined => {
|
|
64
|
-
if (s === undefined) return undefined;
|
|
65
|
-
const n = parseInt(s, 10);
|
|
66
|
-
return isNaN(n) ? undefined : n;
|
|
67
|
-
};
|
|
25
|
+
buildCell,
|
|
26
|
+
buildTable,
|
|
27
|
+
num,
|
|
28
|
+
parseAlign,
|
|
29
|
+
parseColumns,
|
|
30
|
+
parseRows,
|
|
31
|
+
} from './htmltable-parse-attrs.js';
|
|
68
32
|
|
|
69
33
|
// ---------------------------------------------------------------------------
|
|
70
34
|
// Font-stack — mirrors pushFont / popFont in htmlparse.y
|
|
@@ -115,8 +79,33 @@ const fontItemProps = (f: FontState): Partial<HtmlTextItem> => {
|
|
|
115
79
|
interface ParseState {
|
|
116
80
|
tokens: Token[];
|
|
117
81
|
pos: number;
|
|
82
|
+
/** Lexical error found after the last token: C reports it when the parser asks for more. */
|
|
83
|
+
pending?: HtmlParseError;
|
|
118
84
|
}
|
|
119
85
|
|
|
86
|
+
/** bison's default message for an unexpected token. @see lib/common/htmlparse.y */
|
|
87
|
+
const MSG_SYNTAX_ERROR = 'syntax error';
|
|
88
|
+
/** @see lib/common/htmlparse.y:288 (table rule) */
|
|
89
|
+
const MSG_TEXT_BEFORE_TABLE = 'Syntax error: non-space string used before <TABLE>';
|
|
90
|
+
/** @see lib/common/htmlparse.y:298 (table rule) */
|
|
91
|
+
const MSG_TEXT_AFTER_TABLE = 'Syntax error: non-space string used after </TABLE>';
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* C's `nonSpace` on the lexer's text, which has already dropped control
|
|
95
|
+
* characters; the port's tokens still carry tab, LF and CR.
|
|
96
|
+
* @see lib/common/htmlparse.y:nonSpace
|
|
97
|
+
*/
|
|
98
|
+
const nonSpace = (text: string): boolean => /[^ \t\n\r]/.test(text);
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The parse failure C reports through `htmlerror` when the lexer last returned
|
|
102
|
+
* `tok`. At end of input the deferred lexical error is what C would hit next.
|
|
103
|
+
* A token without a recorded chunk yields a plain, silent HtmlParseError.
|
|
104
|
+
* @see lib/common/htmllex.c:htmlerror
|
|
105
|
+
*/
|
|
106
|
+
const parseError = (s: ParseState, tok: Token | undefined, message: string): HtmlParseError =>
|
|
107
|
+
(tok === undefined ? s.pending : htmlErrorAtToken(message, tok)) ?? new HtmlParseError(message);
|
|
108
|
+
|
|
120
109
|
const peek = (s: ParseState): Token | undefined => s.tokens[s.pos];
|
|
121
110
|
|
|
122
111
|
const consume = (s: ParseState): Token => {
|
|
@@ -144,14 +133,29 @@ const inlineFontState = (tag: string, _attrs: Record<string, string>): FontState
|
|
|
144
133
|
return fs;
|
|
145
134
|
};
|
|
146
135
|
|
|
136
|
+
/**
|
|
137
|
+
* `br: T_br T_end_br`, likewise image, HR and VR: the open tag must be
|
|
138
|
+
* followed at once by its close (`<BR/>` yields both); anything else is a
|
|
139
|
+
* syntax error at that token.
|
|
140
|
+
* @see lib/common/htmlparse.y:br, image
|
|
141
|
+
*/
|
|
142
|
+
const consumeClose = (s: ParseState, tag: string): void => {
|
|
143
|
+
const nxt = peek(s);
|
|
144
|
+
if (nxt === undefined) return;
|
|
145
|
+
if (nxt.type === 'close' && nxt.tag === tag) {
|
|
146
|
+
consume(s);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
throw parseError(s, nxt, MSG_SYNTAX_ERROR);
|
|
150
|
+
};
|
|
151
|
+
|
|
147
152
|
const applyBrTag = (
|
|
148
153
|
s: ParseState, t: OpenToken, fontStack: FontState[], items: HtmlTextItem[],
|
|
149
154
|
): void => {
|
|
150
155
|
consume(s);
|
|
151
156
|
const font = fontStack[fontStack.length - 1] ?? {};
|
|
152
157
|
items.push({ br: true, brAlign: parseAlign(t.attrs['align'] ?? ''), ...fontItemProps(font) });
|
|
153
|
-
|
|
154
|
-
if (nxt?.type === 'close' && nxt.tag === 'BR') consume(s);
|
|
158
|
+
consumeClose(s, 'BR');
|
|
155
159
|
};
|
|
156
160
|
|
|
157
161
|
const applyFontTag = (
|
|
@@ -219,13 +223,6 @@ export const parseText = (s: ParseState, fontStack: FontState[]): HtmlText => {
|
|
|
219
223
|
// Cell content helpers — mirrors htmlparse.y cell rule
|
|
220
224
|
// ---------------------------------------------------------------------------
|
|
221
225
|
|
|
222
|
-
const parseHrContent = (s: ParseState): HtmlHR => {
|
|
223
|
-
consume(s);
|
|
224
|
-
const nxt = peek(s);
|
|
225
|
-
if (nxt?.type === 'close' && nxt.tag === 'HR') consume(s);
|
|
226
|
-
return { kind: 'hr' };
|
|
227
|
-
};
|
|
228
|
-
|
|
229
226
|
const parseImgContent = (
|
|
230
227
|
s: ParseState, t: OpenToken, cellAttrs: Record<string, string>,
|
|
231
228
|
): HtmlImage => {
|
|
@@ -235,30 +232,84 @@ const parseImgContent = (
|
|
|
235
232
|
src: cellAttrs['src'] ?? t.attrs['src'] ?? '',
|
|
236
233
|
scale: t.attrs['scale'],
|
|
237
234
|
};
|
|
238
|
-
|
|
239
|
-
if (nxt?.type === 'close' && nxt.tag === 'IMG') consume(s);
|
|
235
|
+
consumeClose(s, 'IMG');
|
|
240
236
|
return img;
|
|
241
237
|
};
|
|
242
238
|
|
|
243
|
-
|
|
244
|
-
|
|
239
|
+
/** What a cell holds so far: C's cell has exactly one child. */
|
|
240
|
+
interface CellScan {
|
|
241
|
+
kind: 'none' | 'text' | 'table' | 'image';
|
|
242
|
+
/** Non-space text before the nested table (checked when the table opens). */
|
|
243
|
+
textNonSpace: boolean;
|
|
244
|
+
/** Non-space text after the nested table (checked when the run ends). */
|
|
245
|
+
afterNonSpace: boolean;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** `table: opt_space T_table ... T_end_table opt_space` inside a cell. */
|
|
249
|
+
const scanCellTable = (
|
|
250
|
+
s: ParseState, t: Token, fontStack: FontState[], st: CellScan,
|
|
245
251
|
): HtmlCellContent[] => {
|
|
246
|
-
if (
|
|
247
|
-
if (
|
|
248
|
-
|
|
252
|
+
if (st.kind === 'image' || st.kind === 'table') throw parseError(s, t, MSG_SYNTAX_ERROR);
|
|
253
|
+
if (st.textNonSpace) throw parseError(s, t, MSG_TEXT_BEFORE_TABLE);
|
|
254
|
+
const tbl = parseTable(s, fontStack);
|
|
255
|
+
st.kind = 'table';
|
|
256
|
+
return [tbl];
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
/** Text run (strings and inline tags) of a cell holding text so far. */
|
|
260
|
+
const scanCellText = (s: ParseState, t: Token, fontStack: FontState[], st: CellScan): HtmlCellContent[] => {
|
|
261
|
+
const start = s.pos;
|
|
249
262
|
const txt = parseText(s, fontStack);
|
|
263
|
+
if (s.pos === start) throw parseError(s, t, MSG_SYNTAX_ERROR);
|
|
264
|
+
if (txt.items.some((i) => i.text !== undefined && nonSpace(i.text))) st.textNonSpace = true;
|
|
265
|
+
st.kind = 'text';
|
|
250
266
|
return txt.items.length > 0 ? [txt] : [];
|
|
251
267
|
};
|
|
252
268
|
|
|
269
|
+
/** Only the blanks of the table rule's trailing opt_space may follow a nested table. */
|
|
270
|
+
const scanAfterTable = (s: ParseState, t: Token, st: CellScan): HtmlCellContent[] => {
|
|
271
|
+
if (t.type !== 'text') throw parseError(s, t, st.afterNonSpace ? MSG_TEXT_AFTER_TABLE : MSG_SYNTAX_ERROR);
|
|
272
|
+
consume(s);
|
|
273
|
+
if (nonSpace(t.value)) st.afterNonSpace = true;
|
|
274
|
+
return [];
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
/** `image` as the cell's only child. */
|
|
278
|
+
const scanCellImage = (
|
|
279
|
+
s: ParseState, t: OpenToken, cellAttrs: Record<string, string>, st: CellScan,
|
|
280
|
+
): HtmlCellContent[] => {
|
|
281
|
+
if (st.kind !== 'none') throw parseError(s, t, MSG_SYNTAX_ERROR);
|
|
282
|
+
st.kind = 'image';
|
|
283
|
+
return [parseImgContent(s, t, cellAttrs)];
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
/** One step of `cell: T_cell (fonttable | fonttext | image) T_end_cell`. */
|
|
287
|
+
const scanCellItem = (
|
|
288
|
+
s: ParseState, t: Token, cellAttrs: Record<string, string>, fontStack: FontState[], st: CellScan,
|
|
289
|
+
): HtmlCellContent[] => {
|
|
290
|
+
if (st.kind === 'table') return scanAfterTable(s, t, st);
|
|
291
|
+
if (t.type === 'open' && t.tag === 'TABLE') return scanCellTable(s, t, fontStack, st);
|
|
292
|
+
if (t.type === 'open' && t.tag === 'IMG') return scanCellImage(s, t, cellAttrs, st);
|
|
293
|
+
if (st.kind === 'image') throw parseError(s, t, MSG_SYNTAX_ERROR);
|
|
294
|
+
return scanCellText(s, t, fontStack, st);
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
/** True at the cell's own `</TD>`; any other close tag is a syntax error. */
|
|
298
|
+
const endsCell = (s: ParseState, t: Token, st: CellScan): boolean => {
|
|
299
|
+
if (t.type !== 'close') return false;
|
|
300
|
+
const afterText = st.kind === 'table' && st.afterNonSpace;
|
|
301
|
+
if (t.tag === 'TD' && !afterText) return true;
|
|
302
|
+
throw parseError(s, t, afterText ? MSG_TEXT_AFTER_TABLE : MSG_SYNTAX_ERROR);
|
|
303
|
+
};
|
|
304
|
+
|
|
253
305
|
const parseCellContent = (
|
|
254
306
|
s: ParseState, cellAttrs: Record<string, string>, fontStack: FontState[],
|
|
255
307
|
): HtmlCellContent[] => {
|
|
256
308
|
const content: HtmlCellContent[] = [];
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if (
|
|
260
|
-
|
|
261
|
-
content.push(...parseCellItem(s, t, cellAttrs, fontStack));
|
|
309
|
+
const st: CellScan = { kind: 'none', textNonSpace: false, afterNonSpace: false };
|
|
310
|
+
for (let t = peek(s); t !== undefined; t = peek(s)) {
|
|
311
|
+
if (endsCell(s, t, st)) break;
|
|
312
|
+
content.push(...scanCellItem(s, t, cellAttrs, fontStack, st));
|
|
262
313
|
}
|
|
263
314
|
const nxt = peek(s);
|
|
264
315
|
if (nxt?.type === 'close' && nxt.tag === 'TD') consume(s);
|
|
@@ -270,86 +321,6 @@ const parseCellContent = (
|
|
|
270
321
|
return content;
|
|
271
322
|
};
|
|
272
323
|
|
|
273
|
-
const parseFixedSize = (a: Record<string, string>): true | undefined =>
|
|
274
|
-
a['fixedsize']?.toUpperCase() === 'TRUE' ? true : undefined;
|
|
275
|
-
|
|
276
|
-
const parseTitle = (a: Record<string, string>): string | undefined =>
|
|
277
|
-
a['title'] !== undefined ? a['title'] : a['tooltip'];
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* Parse SIDES attribute into a bitmask matching C sidesfn().
|
|
281
|
-
* Unrecognised chars are silently skipped (C issues a warning but continues).
|
|
282
|
-
* C guard: if (flags != BORDER_MASK) p->flags |= flags — the full-mask case
|
|
283
|
-
* is a no-op (means "all sides", the default), so we return undefined for it.
|
|
284
|
-
* @see lib/common/htmllex.c:sidesfn
|
|
285
|
-
*/
|
|
286
|
-
const parseSides = (v: string | undefined): number | undefined => {
|
|
287
|
-
if (v === undefined) return undefined;
|
|
288
|
-
let flags = 0;
|
|
289
|
-
for (const c of v.toLowerCase()) {
|
|
290
|
-
if (c === 'l') flags |= BORDER_LEFT;
|
|
291
|
-
else if (c === 't') flags |= BORDER_TOP;
|
|
292
|
-
else if (c === 'r') flags |= BORDER_RIGHT;
|
|
293
|
-
else if (c === 'b') flags |= BORDER_BOTTOM;
|
|
294
|
-
}
|
|
295
|
-
// C: if (flags != BORDER_MASK) p->flags |= flags
|
|
296
|
-
if (flags === 0 || flags === BORDER_MASK) return undefined;
|
|
297
|
-
return flags;
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Parse GRADIENTANGLE attribute — integer in [0, 360].
|
|
302
|
-
* Out-of-range or non-integer values are ignored (C issues a warning).
|
|
303
|
-
* @see lib/common/htmllex.c:gradientanglefn
|
|
304
|
-
*/
|
|
305
|
-
const parseGradientAngle = (v: string | undefined): number | undefined => {
|
|
306
|
-
if (v === undefined) return undefined;
|
|
307
|
-
const n = parseInt(v, 10);
|
|
308
|
-
if (isNaN(n) || String(n) !== v.trim()) return undefined;
|
|
309
|
-
if (n < 0 || n > 360) return undefined;
|
|
310
|
-
return n;
|
|
311
|
-
};
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* Parse COLUMNS attribute — only "*" is valid (sets vrule=true).
|
|
315
|
-
* @see lib/common/htmllex.c:columnsfn
|
|
316
|
-
*/
|
|
317
|
-
const parseColumns = (v: string | undefined): true | undefined =>
|
|
318
|
-
v === '*' ? true : undefined;
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* Parse ROWS attribute — only "*" is valid (sets hrule=true).
|
|
322
|
-
* @see lib/common/htmllex.c:rowsfn
|
|
323
|
-
*/
|
|
324
|
-
const parseRows = (v: string | undefined): true | undefined =>
|
|
325
|
-
v === '*' ? true : undefined;
|
|
326
|
-
|
|
327
|
-
const buildCell = (content: HtmlCellContent[], a: Record<string, string>): HtmlCell => ({
|
|
328
|
-
kind: 'cell',
|
|
329
|
-
content,
|
|
330
|
-
port: a['port'],
|
|
331
|
-
align: parseAlign(a['align'] !== undefined ? a['align'] : ''),
|
|
332
|
-
valign: parseVAlign(a['valign'] !== undefined ? a['valign'] : ''),
|
|
333
|
-
balign: parseAlign(a['balign'] !== undefined ? a['balign'] : ''),
|
|
334
|
-
bgcolor: a['bgcolor'],
|
|
335
|
-
color: a['color'],
|
|
336
|
-
border: num(a['border']),
|
|
337
|
-
cellpadding: num(a['cellpadding']),
|
|
338
|
-
cellspacing: num(a['cellspacing']),
|
|
339
|
-
sides: parseSides(a['sides']),
|
|
340
|
-
gradientangle: parseGradientAngle(a['gradientangle']),
|
|
341
|
-
style: a['style'],
|
|
342
|
-
width: num(a['width']),
|
|
343
|
-
height: num(a['height']),
|
|
344
|
-
fixedsize: parseFixedSize(a),
|
|
345
|
-
colspan: num(a['colspan']),
|
|
346
|
-
rowspan: num(a['rowspan']),
|
|
347
|
-
href: anchorAttr(a['href']),
|
|
348
|
-
title: parseTitle(a),
|
|
349
|
-
target: anchorAttr(a['target']),
|
|
350
|
-
id: a['id'],
|
|
351
|
-
});
|
|
352
|
-
|
|
353
324
|
/** @see lib/common/htmlparse.y:cell rule / lib/common/htmllex.c:mkCell */
|
|
354
325
|
const parseCell = (
|
|
355
326
|
s: ParseState, attrs: Record<string, string>, fontStack: FontState[],
|
|
@@ -359,72 +330,98 @@ const parseCell = (
|
|
|
359
330
|
// parseRow — parses <TR>...</TR>
|
|
360
331
|
// ---------------------------------------------------------------------------
|
|
361
332
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
333
|
+
/**
|
|
334
|
+
* `cells : cell | cells cell | cells VR cell`: VR marks the PRECEDING cell
|
|
335
|
+
* as vertically ruled and must sit between two cells.
|
|
336
|
+
* @see lib/common/htmlparse.y:329
|
|
337
|
+
*/
|
|
338
|
+
const parseRowVr = (s: ParseState, t: Token, cells: HtmlCell[]): void => {
|
|
339
|
+
const prev = cells[cells.length - 1];
|
|
340
|
+
if (prev === undefined) throw parseError(s, t, MSG_SYNTAX_ERROR);
|
|
341
|
+
consume(s);
|
|
342
|
+
consumeClose(s, 'VR');
|
|
343
|
+
prev.vruled = true;
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
interface RowScan {
|
|
347
|
+
cells: HtmlCell[];
|
|
348
|
+
/** A VR was seen: the next cell is required. */
|
|
349
|
+
needCell: boolean;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** A TD, or a VR between cells. */
|
|
353
|
+
const scanRowCell = (s: ParseState, t: Token, fontStack: FontState[], rs: RowScan): void => {
|
|
365
354
|
if (t.type === 'open' && t.tag === 'TD') {
|
|
366
355
|
consume(s);
|
|
367
|
-
cells.push(parseCell(s, t.attrs, fontStack));
|
|
368
|
-
|
|
356
|
+
rs.cells.push(parseCell(s, t.attrs, fontStack));
|
|
357
|
+
rs.needCell = false;
|
|
358
|
+
} else if (t.type === 'open' && t.tag === 'VR' && !rs.needCell) {
|
|
359
|
+
parseRowVr(s, t, rs.cells);
|
|
360
|
+
rs.needCell = true;
|
|
361
|
+
} else {
|
|
362
|
+
throw parseError(s, t, MSG_SYNTAX_ERROR);
|
|
369
363
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
if (prev === undefined) throw new HtmlParseError('VR');
|
|
379
|
-
prev.vruled = true;
|
|
380
|
-
return;
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
/** One token of `row: T_row cells T_end_row`; true at the row's `</TR>`. */
|
|
367
|
+
const scanRowToken = (s: ParseState, t: Token, fontStack: FontState[], rs: RowScan): boolean => {
|
|
368
|
+
if (t.type === 'text') { consume(s); return false; } // not in a cell: ignored
|
|
369
|
+
if (t.type !== 'close' || t.tag !== 'TR') {
|
|
370
|
+
scanRowCell(s, t, fontStack, rs);
|
|
371
|
+
return false;
|
|
381
372
|
}
|
|
382
|
-
|
|
373
|
+
if (rs.cells.length === 0 || rs.needCell) throw parseError(s, t, MSG_SYNTAX_ERROR);
|
|
374
|
+
return true;
|
|
383
375
|
};
|
|
384
376
|
|
|
385
377
|
/** @see lib/common/htmlparse.y:row + cells rules */
|
|
386
378
|
const parseRow = (s: ParseState, fontStack: FontState[]): HtmlRow => {
|
|
387
|
-
const cells:
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
if (!t) break;
|
|
391
|
-
if (t.type === 'close' && t.tag === 'TR') break;
|
|
392
|
-
processRowToken(s, t, cells, fontStack);
|
|
379
|
+
const rs: RowScan = { cells: [], needCell: false };
|
|
380
|
+
for (let t = peek(s); t !== undefined; t = peek(s)) {
|
|
381
|
+
if (scanRowToken(s, t, fontStack, rs)) break;
|
|
393
382
|
}
|
|
394
383
|
const nxt = peek(s);
|
|
395
384
|
if (nxt?.type === 'close' && nxt.tag === 'TR') consume(s);
|
|
396
|
-
return { cells };
|
|
385
|
+
return { cells: rs.cells };
|
|
397
386
|
};
|
|
398
387
|
|
|
399
388
|
// ---------------------------------------------------------------------------
|
|
400
389
|
// parseTable — parses <TABLE>...</TABLE>
|
|
401
390
|
// ---------------------------------------------------------------------------
|
|
402
391
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
392
|
+
interface TableScan {
|
|
393
|
+
rows: HtmlRow[];
|
|
394
|
+
/** An HR was seen: the next row is required. */
|
|
395
|
+
needRow: boolean;
|
|
396
|
+
hrule: boolean;
|
|
397
|
+
vrule: boolean;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** `rows : row | rows row | rows HR row`: a TR, or an HR after a row. */
|
|
401
|
+
const scanTableRow = (s: ParseState, t: Token, fontStack: FontState[], ts: TableScan): void => {
|
|
402
|
+
if (t.type === 'open' && t.tag === 'TR') {
|
|
403
|
+
consume(s);
|
|
404
|
+
ts.rows.push(parseTableRow(s, fontStack, ts));
|
|
405
|
+
ts.needRow = false;
|
|
406
|
+
} else if (t.type === 'open' && t.tag === 'HR' && ts.rows.length > 0 && !ts.needRow) {
|
|
407
|
+
consumeRowHr(s, ts.rows);
|
|
408
|
+
ts.needRow = true;
|
|
409
|
+
} else {
|
|
410
|
+
throw parseError(s, t, MSG_SYNTAX_ERROR);
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
/** One token of a table body; true after the table's `</TABLE>`. */
|
|
415
|
+
const scanTableToken = (s: ParseState, t: Token, fontStack: FontState[], ts: TableScan): boolean => {
|
|
416
|
+
if (t.type === 'text') { consume(s); return false; } // not in a cell: ignored
|
|
417
|
+
if (t.type !== 'close' || t.tag !== 'TABLE') {
|
|
418
|
+
scanTableRow(s, t, fontStack, ts);
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
if (ts.rows.length === 0 || ts.needRow) throw parseError(s, t, MSG_SYNTAX_ERROR);
|
|
422
|
+
consume(s);
|
|
423
|
+
return true;
|
|
424
|
+
};
|
|
428
425
|
|
|
429
426
|
/** @see lib/common/htmlparse.y:table rule / lib/common/htmllex.c:mkTbl */
|
|
430
427
|
export const parseTable = (s: ParseState, fontStack: FontState[]): HtmlTable => {
|
|
@@ -432,25 +429,16 @@ export const parseTable = (s: ParseState, fontStack: FontState[]): HtmlTable =>
|
|
|
432
429
|
if (open.type !== 'open' || open.tag !== 'TABLE') throw new HtmlParseError('TABLE');
|
|
433
430
|
// C addRow: every row starts ruled when ROWS="*" (htmlparse.y addRow);
|
|
434
431
|
// C setCell: every cell starts vruled when COLUMNS="*".
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
if (
|
|
443
|
-
consume(s);
|
|
444
|
-
rows.push(parseTableRow(s, fontStack, { hrule, vrule }));
|
|
445
|
-
continue;
|
|
446
|
-
}
|
|
447
|
-
if (t.type === 'open' && t.tag === 'HR') {
|
|
448
|
-
consumeRowHr(s, rows);
|
|
449
|
-
continue;
|
|
450
|
-
}
|
|
451
|
-
consume(s);
|
|
432
|
+
const ts: TableScan = {
|
|
433
|
+
rows: [],
|
|
434
|
+
needRow: false,
|
|
435
|
+
hrule: parseRows(open.attrs['rows']) === true,
|
|
436
|
+
vrule: parseColumns(open.attrs['columns']) === true,
|
|
437
|
+
};
|
|
438
|
+
for (let t = peek(s); t !== undefined; t = peek(s)) {
|
|
439
|
+
if (scanTableToken(s, t, fontStack, ts)) break;
|
|
452
440
|
}
|
|
453
|
-
return buildTable(rows, open.attrs);
|
|
441
|
+
return buildTable(ts.rows, open.attrs);
|
|
454
442
|
};
|
|
455
443
|
|
|
456
444
|
/** Parse one row, applying table-level rule defaults. @see lib/common/htmlparse.y addRow/setCell */
|
|
@@ -470,11 +458,9 @@ const parseTableRow = (
|
|
|
470
458
|
*/
|
|
471
459
|
const consumeRowHr = (s: ParseState, rows: HtmlRow[]): void => {
|
|
472
460
|
consume(s);
|
|
473
|
-
|
|
474
|
-
if (nxt?.type === 'close' && nxt.tag === 'HR') consume(s);
|
|
461
|
+
consumeClose(s, 'HR');
|
|
475
462
|
const prev = rows[rows.length - 1];
|
|
476
|
-
if (prev
|
|
477
|
-
prev.ruled = true;
|
|
463
|
+
if (prev !== undefined) prev.ruled = true;
|
|
478
464
|
};
|
|
479
465
|
|
|
480
466
|
// ---------------------------------------------------------------------------
|
|
@@ -497,14 +483,60 @@ const skipLeadingWs = (s: ParseState): void => {
|
|
|
497
483
|
}
|
|
498
484
|
};
|
|
499
485
|
|
|
486
|
+
/** After the top-level `</TABLE>`: blanks, then `</HTML>`. */
|
|
487
|
+
const finishTableLabel = (s: ParseState): void => {
|
|
488
|
+
let afterNonSpace = false;
|
|
489
|
+
for (let t = peek(s); t?.type === 'text'; t = peek(s)) {
|
|
490
|
+
consume(s);
|
|
491
|
+
if (nonSpace(t.value)) afterNonSpace = true;
|
|
492
|
+
}
|
|
493
|
+
const nxt = peek(s);
|
|
494
|
+
if (nxt === undefined) return;
|
|
495
|
+
if (afterNonSpace) throw parseError(s, nxt, MSG_TEXT_AFTER_TABLE);
|
|
496
|
+
if (nxt.type !== 'close' || nxt.tag !== 'HTML') throw parseError(s, nxt, MSG_SYNTAX_ERROR);
|
|
497
|
+
consume(s);
|
|
498
|
+
};
|
|
499
|
+
|
|
500
500
|
const parseTableLabel = (s: ParseState, fontStack: FontState[]): HtmlLabel => {
|
|
501
501
|
skipLeadingWs(s);
|
|
502
502
|
const table = parseTable(s, fontStack);
|
|
503
|
-
|
|
504
|
-
if (nxt?.type === 'close' && nxt.tag === 'HTML') consume(s);
|
|
503
|
+
finishTableLabel(s);
|
|
505
504
|
return { kind: 'table', table };
|
|
506
505
|
};
|
|
507
506
|
|
|
507
|
+
/** `html : T_html fonttext T_end_html`; a `<TABLE>` after text is the table rule's error. */
|
|
508
|
+
const parseTextLabel = (s: ParseState, fontStack: FontState[]): HtmlLabel => {
|
|
509
|
+
const texts: HtmlText[] = [];
|
|
510
|
+
const txt = parseText(s, fontStack);
|
|
511
|
+
if (txt.items.length > 0) texts.push(txt);
|
|
512
|
+
const nxt = peek(s);
|
|
513
|
+
if (nxt === undefined) return { kind: 'text', texts };
|
|
514
|
+
if (nxt.type === 'open' && nxt.tag === 'TABLE') {
|
|
515
|
+
// C's `table` rule aborts the whole label when non-space text precedes a
|
|
516
|
+
// top-level <TABLE>. Text that is only blanks means a font-wrapped table
|
|
517
|
+
// (`fonttable`), which this parser does not model: rejected without a
|
|
518
|
+
// message rather than with a wrong one.
|
|
519
|
+
// @see lib/common/htmlparse.y:288 (table rule nonSpace check)
|
|
520
|
+
if (txt.items.some((i) => i.text !== undefined && nonSpace(i.text))) {
|
|
521
|
+
throw parseError(s, nxt, MSG_TEXT_BEFORE_TABLE);
|
|
522
|
+
}
|
|
523
|
+
throw new HtmlParseError('TABLE');
|
|
524
|
+
}
|
|
525
|
+
if (nxt.type !== 'close' || nxt.tag !== 'HTML') throw parseError(s, nxt, MSG_SYNTAX_ERROR);
|
|
526
|
+
consume(s);
|
|
527
|
+
return { kind: 'text', texts };
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
/** Parse the token stream; the deferred lexical error wins once input runs out. */
|
|
531
|
+
const parseTokens = (s: ParseState): HtmlLabel => {
|
|
532
|
+
const fontStack: FontState[] = [{}];
|
|
533
|
+
const htmlOpen = consume(s);
|
|
534
|
+
if (htmlOpen.type !== 'open' || htmlOpen.tag !== 'HTML') throw new HtmlParseError('HTML');
|
|
535
|
+
const firstReal = findFirstRealToken(s.tokens, s.pos);
|
|
536
|
+
if (firstReal?.type === 'open' && firstReal.tag === 'TABLE') return parseTableLabel(s, fontStack);
|
|
537
|
+
return parseTextLabel(s, fontStack);
|
|
538
|
+
};
|
|
539
|
+
|
|
508
540
|
/**
|
|
509
541
|
* Parse an HTML-like label string.
|
|
510
542
|
* Input is the content between the outer `<` `>` delimiters (already stripped).
|
|
@@ -512,28 +544,16 @@ const parseTableLabel = (s: ParseState, fontStack: FontState[]): HtmlLabel => {
|
|
|
512
544
|
* @see lib/common/htmlparse.y:parseHTML
|
|
513
545
|
*/
|
|
514
546
|
export const parseHtmlLabel = (src: string): HtmlLabel => {
|
|
515
|
-
const
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
}
|
|
525
|
-
const texts: HtmlText[] = [];
|
|
526
|
-
const txt = parseText(s, fontStack);
|
|
527
|
-
if (txt.items.length > 0) texts.push(txt);
|
|
528
|
-
const nxt = peek(s);
|
|
529
|
-
// C's `table` rule aborts the whole label when non-space text precedes a
|
|
530
|
-
// top-level <TABLE> ("Syntax error: non-space string used before <TABLE>").
|
|
531
|
-
// parseText stopped at the <TABLE>; reaching it here means there was leading
|
|
532
|
-
// non-space text, so reject the label rather than leniently dropping the table.
|
|
533
|
-
// @see lib/common/htmlparse.y:288 (table rule nonSpace check)
|
|
534
|
-
if (nxt?.type === 'open' && nxt.tag === 'TABLE') {
|
|
535
|
-
throw new HtmlParseError('non-space string used before <TABLE>');
|
|
547
|
+
const { tokens, error } = tokenizeDeferred(`<HTML>${src}</HTML>`);
|
|
548
|
+
const s: ParseState = { tokens, pos: 0, pending: error };
|
|
549
|
+
let label: HtmlLabel;
|
|
550
|
+
try {
|
|
551
|
+
label = parseTokens(s);
|
|
552
|
+
} catch (e) {
|
|
553
|
+
// Running off the truncated stream means C's lexer would have hit the error.
|
|
554
|
+
if (error !== undefined && s.pos >= tokens.length) throw error;
|
|
555
|
+
throw e;
|
|
536
556
|
}
|
|
537
|
-
if (
|
|
538
|
-
return
|
|
557
|
+
if (error !== undefined) throw error;
|
|
558
|
+
return label;
|
|
539
559
|
};
|