@knowvah/dot-engine 2.6.3 → 2.6.5
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/arrows-shapes-poly.d.ts.map +1 -1
- package/dist/common/bezier.d.ts +12 -0
- package/dist/common/bezier.d.ts.map +1 -0
- package/dist/common/htmltable-attr-warn.d.ts +41 -0
- package/dist/common/htmltable-attr-warn.d.ts.map +1 -0
- package/dist/common/htmltable-fontinfo.d.ts +42 -0
- package/dist/common/htmltable-fontinfo.d.ts.map +1 -0
- package/dist/common/htmltable-lex-expat.d.ts +63 -0
- package/dist/common/htmltable-lex-expat.d.ts.map +1 -0
- package/dist/common/htmltable-lex-units.d.ts +32 -0
- package/dist/common/htmltable-lex-units.d.ts.map +1 -0
- package/dist/common/htmltable-lex.d.ts +10 -2
- package/dist/common/htmltable-lex.d.ts.map +1 -1
- package/dist/common/htmltable-parse-attrs.d.ts +7 -5
- package/dist/common/htmltable-parse-attrs.d.ts.map +1 -1
- package/dist/common/htmltable-parse-font.d.ts +33 -0
- package/dist/common/htmltable-parse-font.d.ts.map +1 -0
- package/dist/common/htmltable-parse-state.d.ts +39 -0
- package/dist/common/htmltable-parse-state.d.ts.map +1 -0
- package/dist/common/htmltable-parse.d.ts +18 -23
- package/dist/common/htmltable-parse.d.ts.map +1 -1
- package/dist/common/htmltable-pos.d.ts.map +1 -1
- package/dist/common/htmltable-types.d.ts +33 -3
- 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/spline-midpoint.d.ts.map +1 -1
- package/dist/common/splines-geom.d.ts +5 -6
- package/dist/common/splines-geom.d.ts.map +1 -1
- package/dist/common/splines-routespl.d.ts.map +1 -1
- package/dist/common/taper.d.ts.map +1 -1
- package/dist/gvc/anchor.d.ts.map +1 -1
- package/dist/index.js +1366 -648
- package/dist/index.js.map +4 -4
- package/dist/layout/dot/compound-clip.d.ts +5 -5
- package/dist/layout/dot/compound-clip.d.ts.map +1 -1
- package/dist/layout/dot/compound-geom.d.ts +2 -7
- package/dist/layout/dot/compound-geom.d.ts.map +1 -1
- package/dist/layout/dot/compound.d.ts +1 -1
- package/dist/layout/dot/compound.d.ts.map +1 -1
- package/dist/layout/dot/edge-route-clip.d.ts +1 -1
- package/dist/layout/dot/edge-route-clip.d.ts.map +1 -1
- package/dist/layout/dot/ortho-adapter.d.ts +13 -4
- 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/dot/splines-label.d.ts.map +1 -1
- package/dist/layout/neato/bfs.d.ts +10 -3
- package/dist/layout/neato/bfs.d.ts.map +1 -1
- package/dist/layout/neato/init.d.ts +6 -11
- package/dist/layout/neato/init.d.ts.map +1 -1
- package/dist/layout/neato/splines.d.ts.map +1 -1
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/parser/scan-token.d.ts +40 -0
- package/dist/parser/scan-token.d.ts.map +1 -0
- package/dist/render/svg-edge-split.d.ts.map +1 -1
- package/dist/render.js +1388 -670
- package/dist/render.js.map +4 -4
- package/package.json +1 -1
- package/src/common/arrows-shapes-poly.ts +9 -10
- package/src/common/bezier.ts +33 -0
- package/src/common/htmltable-attr-warn.ts +270 -0
- package/src/common/htmltable-fontinfo.ts +83 -0
- package/src/common/htmltable-lex-expat.ts +357 -0
- package/src/common/htmltable-lex-units.ts +75 -0
- package/src/common/htmltable-lex.ts +221 -160
- package/src/common/htmltable-parse-attrs.ts +10 -14
- package/src/common/htmltable-parse-font.ts +92 -0
- package/src/common/htmltable-parse-state.ts +64 -0
- package/src/common/htmltable-parse.ts +138 -98
- package/src/common/htmltable-pos.ts +9 -8
- package/src/common/htmltable-types.ts +35 -3
- package/src/common/htmltable.ts +14 -7
- package/src/common/spline-midpoint.ts +2 -22
- package/src/common/splines-geom.ts +5 -25
- package/src/common/splines-routespl.ts +21 -13
- package/src/common/taper.ts +2 -14
- package/src/gvc/anchor.ts +5 -14
- package/src/layout/dot/__fixtures__/2796-flat-labeled-lost.dot +161 -0
- package/src/layout/dot/compound-clip.ts +9 -19
- package/src/layout/dot/compound-geom.ts +6 -23
- package/src/layout/dot/compound.ts +1 -1
- package/src/layout/dot/edge-route-clip.ts +6 -29
- package/src/layout/dot/ortho-adapter.ts +5 -5
- package/src/layout/dot/splines-flat-labeled.ts +3 -1
- package/src/layout/dot/splines-label.ts +3 -29
- package/src/layout/neato/bfs.ts +13 -4
- package/src/layout/neato/init.ts +15 -15
- package/src/layout/neato/splines.ts +4 -8
- package/src/parser/index.ts +13 -1
- package/src/parser/scan-token.ts +289 -0
- package/src/render/svg-edge-split.ts +5 -13
|
@@ -12,13 +12,17 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { HtmlParseError } from './htmltable-types.js';
|
|
15
|
-
import type { Token } from './htmltable-types.js';
|
|
16
|
-
import {
|
|
15
|
+
import type { LexerOutput, OpenToken, TextToken, Token } from './htmltable-types.js';
|
|
16
|
+
import { htmlEntityUTF8 } from './html-entities.js';
|
|
17
|
+
import { checkAttrs } from './htmltable-attr-warn.js';
|
|
18
|
+
import type { AttrEntry } from './htmltable-attr-warn.js';
|
|
19
|
+
import { ExpatFeed, fedBytes, findUndefinedEntity, parseTagChunk } from './htmltable-lex-expat.js';
|
|
20
|
+
import type { ExpatResult, ExpatToken } from './htmltable-lex-expat.js';
|
|
21
|
+
import { findNext } from './htmltable-lex-units.js';
|
|
22
|
+
import type { Unit } from './htmltable-lex-units.js';
|
|
17
23
|
|
|
18
24
|
export type { Token } from './htmltable-types.js';
|
|
19
25
|
|
|
20
|
-
type Attrs = { [k: string]: string };
|
|
21
|
-
|
|
22
26
|
const KNOWN_TAGS = new Set([
|
|
23
27
|
'TABLE', 'TR', 'TD', 'TH', 'FONT', 'BR', 'IMG',
|
|
24
28
|
'B', 'I', 'U', 'O', 'S', 'SUB', 'SUP', 'HR', 'VR', 'HTML',
|
|
@@ -40,17 +44,19 @@ const isWs = (c: string | undefined): boolean =>
|
|
|
40
44
|
* phantom attributes). Bare or unquoted attributes are not well-formed
|
|
41
45
|
* XML — expat rejects the document and the label falls back to plain
|
|
42
46
|
* text; throwing here reproduces that.
|
|
47
|
+
* Names keep their source case (doAttrs prints them as written) and pairs
|
|
48
|
+
* keep document order.
|
|
43
49
|
* @see lib/common/htmllex.c (XML_Parse "not well-formed" error path)
|
|
44
50
|
*/
|
|
45
|
-
function
|
|
46
|
-
const out:
|
|
51
|
+
function parseAttrPairs(raw: string): AttrEntry[] {
|
|
52
|
+
const out: AttrEntry[] = [];
|
|
47
53
|
let i = 0;
|
|
48
54
|
while (i < raw.length) {
|
|
49
55
|
while (isWs(raw[i])) i++;
|
|
50
56
|
if (i >= raw.length) break;
|
|
51
57
|
const m = /^[\w-]+/.exec(raw.slice(i));
|
|
52
58
|
if (m === null) throw new HtmlParseError(raw.slice(i, i + 10));
|
|
53
|
-
const name = m[0]
|
|
59
|
+
const name = m[0];
|
|
54
60
|
i += m[0].length;
|
|
55
61
|
while (isWs(raw[i])) i++;
|
|
56
62
|
if (raw[i] !== '=') throw new HtmlParseError(name);
|
|
@@ -60,7 +66,7 @@ function parseAttrs(raw: string) {
|
|
|
60
66
|
if (q !== '"' && q !== "'") throw new HtmlParseError(name);
|
|
61
67
|
const end = raw.indexOf(q, i + 1);
|
|
62
68
|
if (end < 0) throw new HtmlParseError(name);
|
|
63
|
-
out[name
|
|
69
|
+
out.push([name, raw.slice(i + 1, end)]);
|
|
64
70
|
i = end + 1;
|
|
65
71
|
}
|
|
66
72
|
return out;
|
|
@@ -93,68 +99,64 @@ function cleanText(raw: string) {
|
|
|
93
99
|
return raw.replace(/[\x00-\x1f\x7f]/g, (c) => isKeep(c) ? c : '');
|
|
94
100
|
}
|
|
95
101
|
|
|
96
|
-
/**
|
|
97
|
-
function
|
|
98
|
-
let j = start;
|
|
99
|
-
while (j < src.length && src[j] !== '<') j++;
|
|
102
|
+
/** The text token of a text run, if anything is left after cleaning. */
|
|
103
|
+
function textTokens(raw: string): Token[] {
|
|
100
104
|
// Decode XML/HTML entities in the text run. C's HTML lexer resolves entities
|
|
101
105
|
// via expat during tokenization (htmllex.c), so both sizing and emit see the
|
|
102
106
|
// decoded UTF-8 text. Skipping this measured the raw "[el..." (9-10 chars)
|
|
103
107
|
// instead of "[el..." (6 chars), inflating edge-label vnodes and shifting
|
|
104
108
|
// layout (corpus 1949: +18.7px). Plain-string labels already decode via the
|
|
105
109
|
// make_label path, so only HTML-delimited labels diverged.
|
|
106
|
-
const clean = htmlEntityUTF8(cleanText(
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
while (j < src.length && src[j] !== '>') j++;
|
|
115
|
-
const inner = src.slice(start + 1, j).trim();
|
|
116
|
-
const next = j + 1;
|
|
117
|
-
if (inner.startsWith('!--')) return next;
|
|
118
|
-
if (inner.startsWith('/')) { tokens.push(parseCloseToken(inner)); return next; }
|
|
110
|
+
const clean = htmlEntityUTF8(cleanText(raw));
|
|
111
|
+
return clean.length > 0 ? [{ type: 'text', value: clean }] : [];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The tokens of one complete `<..>` tag. */
|
|
115
|
+
function tagTokens(text: string): Token[] {
|
|
116
|
+
const inner = text.slice(1, -1).trim();
|
|
117
|
+
if (inner.startsWith('/')) return [parseCloseToken(inner)];
|
|
119
118
|
// Self-closing <X/>: expat reports startElement then endElement.
|
|
120
119
|
// @see lib/common/htmllex.c (XML_SetElementHandler callbacks)
|
|
121
120
|
if (inner.endsWith('/')) {
|
|
122
121
|
const open = parseOpenToken(inner.slice(0, -1).trim());
|
|
123
|
-
|
|
124
|
-
return next;
|
|
122
|
+
return [open, { type: 'close', tag: open.tag }];
|
|
125
123
|
}
|
|
126
|
-
|
|
127
|
-
|
|
124
|
+
return [parseOpenToken(inner)];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The open token for `tag` and its raw attribute text: `doAttrs` runs the
|
|
129
|
+
* attributes through the element's table, and the stderr it would write rides
|
|
130
|
+
* on the token for the parser to print.
|
|
131
|
+
* @see lib/common/htmllex.c:doAttrs
|
|
132
|
+
*/
|
|
133
|
+
function openToken(tag: string, raw: string): OpenToken {
|
|
134
|
+
const { attrs, warnings, warn } = checkAttrs(tag, parseAttrPairs(raw));
|
|
135
|
+
const tok: OpenToken = { type: 'open', tag, attrs };
|
|
136
|
+
if (warnings.length > 0) tok.warnings = warnings;
|
|
137
|
+
if (warn) tok.warn = true;
|
|
138
|
+
return tok;
|
|
128
139
|
}
|
|
129
140
|
|
|
130
141
|
/** Parse open tag — placed last so Lizard length-to-EOF stays within 30. */
|
|
131
142
|
function parseOpenToken(inner: string) {
|
|
132
143
|
const sp = inner.search(/\s/);
|
|
133
|
-
|
|
134
|
-
const tag = inner.toUpperCase();
|
|
135
|
-
checkTag(tag);
|
|
136
|
-
return { type: 'open' as const, tag: normalizeTag(tag), attrs: parseAttrs('') };
|
|
137
|
-
}
|
|
138
|
-
const tag = inner.slice(0, sp).toUpperCase();
|
|
144
|
+
const tag = (sp === -1 ? inner : inner.slice(0, sp)).toUpperCase();
|
|
139
145
|
checkTag(tag);
|
|
140
|
-
return
|
|
146
|
+
return openToken(normalizeTag(tag), sp === -1 ? '' : inner.slice(sp + 1));
|
|
141
147
|
}
|
|
142
148
|
|
|
143
149
|
/**
|
|
144
|
-
* expat `XML_ErrorString` texts for the
|
|
145
|
-
*
|
|
146
|
-
*
|
|
150
|
+
* expat `XML_ErrorString` texts for the errors the port detects. Taken from
|
|
151
|
+
* native dot's stderr (graphviz 16.1.0): expat's xmlparse.c is not vendored
|
|
152
|
+
* with the graphviz sources.
|
|
147
153
|
* @see lib/common/htmllex.c:htmllex (XML_ErrorString at the XML_Parse failure)
|
|
148
154
|
*/
|
|
149
155
|
const XML_ERROR_UNDEFINED_ENTITY = 'undefined entity';
|
|
150
156
|
const XML_ERROR_TAG_MISMATCH = 'mismatched tag';
|
|
151
|
-
const XML_ERROR_INVALID_TOKEN = 'not well-formed (invalid token)';
|
|
152
157
|
const XML_ERROR_DUPLICATE_ATTRIBUTE = 'duplicate attribute';
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const XML_PREDEFINED_ENTITIES = new Set(['lt', 'gt', 'amp', 'quot', 'apos']);
|
|
156
|
-
|
|
157
|
-
const XML_NAME = /^[A-Za-z_:][\w.:-]*$/;
|
|
158
|
+
/** yacc's message when the token stream ends inside an unfinished construct. */
|
|
159
|
+
const SYNTAX_ERROR = 'syntax error';
|
|
158
160
|
|
|
159
161
|
/**
|
|
160
162
|
* A label failure C reports on stderr before dropping the HTML label
|
|
@@ -236,171 +238,230 @@ export function htmlErrorAtToken(message: string, tok: Token | undefined): HtmlL
|
|
|
236
238
|
return htmlErrorInLine(message, chunkLine(info), info.context);
|
|
237
239
|
}
|
|
238
240
|
|
|
239
|
-
/**
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
241
|
+
/** Where an expat token sits in the label and the units C had lexed when it ran. */
|
|
242
|
+
interface TokenSite {
|
|
243
|
+
src: string;
|
|
244
|
+
tok: ExpatToken;
|
|
245
|
+
st: ChunkState;
|
|
244
246
|
}
|
|
245
247
|
|
|
246
|
-
/**
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
248
|
+
/**
|
|
249
|
+
* Raise the error C hits for this tag: expat's duplicate attribute and
|
|
250
|
+
* mismatched tag first (detected before the end handler runs), then
|
|
251
|
+
* `lexerror`'s unknown element.
|
|
252
|
+
* @see lib/common/htmllex.c:lexerror
|
|
253
|
+
*/
|
|
254
|
+
function checkTagToken(site: TokenSite): void {
|
|
255
|
+
const { src, tok, st } = site;
|
|
256
|
+
const tag = parseTagChunk(src.slice(tok.start, tok.end));
|
|
257
|
+
if (tag === undefined) return;
|
|
258
|
+
const context = st.prev + st.curr;
|
|
259
|
+
if (tag.close) {
|
|
260
|
+
if (st.stack.length > 0 && st.stack.pop() !== tag.name) {
|
|
261
|
+
throw htmlErrorInLine(XML_ERROR_TAG_MISMATCH, lineAt(src, tok.start), context);
|
|
255
262
|
}
|
|
263
|
+
} else {
|
|
264
|
+
if (tok.dup !== undefined) throw htmlErrorInLine(XML_ERROR_DUPLICATE_ATTRIBUTE, lineAt(src, tok.dup), context);
|
|
265
|
+
if (!tag.selfClose) st.stack.push(tag.name);
|
|
266
|
+
}
|
|
267
|
+
if (KNOWN_TAGS.has(tag.name.toUpperCase())) return;
|
|
268
|
+
const text = `Unknown HTML element <${tag.name}> on line ${lineAt(src, tok.start)} `;
|
|
269
|
+
throw new HtmlLexError(tag.name.toUpperCase(), text, [`Error: ${text}`]);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** The tokens one expat event gives the parser, after the errors C would hit. */
|
|
273
|
+
function eventTokens(site: TokenSite): Token[] {
|
|
274
|
+
const { src, tok, st } = site;
|
|
275
|
+
const text = src.slice(tok.start, tok.end);
|
|
276
|
+
if (tok.kind === 'comment' || tok.kind === 'pi') return [];
|
|
277
|
+
if (tok.kind === 'tag') {
|
|
278
|
+
checkTagToken(site);
|
|
279
|
+
return tagTokens(text);
|
|
280
|
+
}
|
|
281
|
+
const off = findUndefinedEntity(text);
|
|
282
|
+
if (off >= 0) {
|
|
283
|
+
throw htmlErrorInLine(XML_ERROR_UNDEFINED_ENTITY, lineAt(src, tok.start + off), st.prev + st.curr);
|
|
256
284
|
}
|
|
257
|
-
return
|
|
285
|
+
return textTokens(text);
|
|
258
286
|
}
|
|
259
287
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
288
|
+
/** What the lexer wrote since it last returned a token. */
|
|
289
|
+
interface Carry {
|
|
290
|
+
warnings: string[];
|
|
291
|
+
warn: boolean;
|
|
264
292
|
}
|
|
265
293
|
|
|
266
|
-
/**
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
return { close, selfClose: !close && body.endsWith('/'), name };
|
|
294
|
+
/** A label's lexing so far (the state `htmllex` keeps between calls). */
|
|
295
|
+
interface LexRun {
|
|
296
|
+
src: string;
|
|
297
|
+
tokens: Token[];
|
|
298
|
+
st: ChunkState;
|
|
299
|
+
expat: ExpatFeed;
|
|
300
|
+
carry: Carry;
|
|
274
301
|
}
|
|
275
302
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
303
|
+
/** Move what `carry` holds onto `first`, the token the parser is about to get. */
|
|
304
|
+
function attachCarry(carry: Carry, first: Token): void {
|
|
305
|
+
if (carry.warnings.length > 0) first.warnings = [...carry.warnings, ...(first.warnings ?? [])];
|
|
306
|
+
if (carry.warn) first.warn = true;
|
|
307
|
+
carry.warnings = [];
|
|
308
|
+
carry.warn = false;
|
|
279
309
|
}
|
|
280
310
|
|
|
281
|
-
|
|
282
|
-
|
|
311
|
+
/** Fold the output of a token the parser will never see into `carry`. */
|
|
312
|
+
function absorb(carry: Carry, lost: Token[]): void {
|
|
313
|
+
for (const t of lost) {
|
|
314
|
+
carry.warnings.push(...(t.warnings ?? []));
|
|
315
|
+
if (t.warn === true) carry.warn = true;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
283
318
|
|
|
284
|
-
|
|
319
|
+
/** One event's tokens and where it came from. */
|
|
320
|
+
interface EventGroup {
|
|
321
|
+
tokens: Token[];
|
|
322
|
+
tok: ExpatToken;
|
|
323
|
+
}
|
|
285
324
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
325
|
+
/** The events of a call that gave the parser something, in order; the first error aborts. */
|
|
326
|
+
function eventGroups(run: LexRun, res: ExpatResult): EventGroup[] {
|
|
327
|
+
const groups: EventGroup[] = [];
|
|
328
|
+
for (const tok of res.tokens) {
|
|
329
|
+
const tokens = eventTokens({ src: run.src, tok, st: run.st });
|
|
330
|
+
if (tokens.length > 0) groups.push({ tokens, tok });
|
|
331
|
+
}
|
|
332
|
+
if (res.invalid !== undefined) {
|
|
333
|
+
const { message, offset } = res.invalid;
|
|
334
|
+
throw htmlErrorInLine(message, lineAt(run.src, offset), run.st.prev + run.st.curr);
|
|
335
|
+
}
|
|
336
|
+
return groups;
|
|
290
337
|
}
|
|
291
338
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
339
|
+
/**
|
|
340
|
+
* Hand the tokens of the event that won to the parser, joining a run of
|
|
341
|
+
* strings into one.
|
|
342
|
+
* @see lib/common/htmlparse.y:string
|
|
343
|
+
*/
|
|
344
|
+
function deliver(run: LexRun, last: EventGroup): void {
|
|
345
|
+
const first = last.tokens[0]!;
|
|
346
|
+
attachCarry(run.carry, first);
|
|
347
|
+
const info: ChunkInfo = { src: run.src, pos: last.tok.start, end: last.tok.end, context: run.st.prev + run.st.curr };
|
|
348
|
+
const prev = run.tokens.at(-1);
|
|
349
|
+
if (prev?.type === 'text' && first.type === 'text') {
|
|
350
|
+
appendString(prev, first);
|
|
351
|
+
chunkInfo.set(prev, { ...info, pos: chunkInfo.get(prev)?.pos ?? info.pos });
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
for (const t of last.tokens) chunkInfo.set(t, info);
|
|
355
|
+
run.tokens.push(...last.tokens);
|
|
295
356
|
}
|
|
296
357
|
|
|
297
358
|
/**
|
|
298
|
-
*
|
|
299
|
-
*
|
|
359
|
+
* Run the events of one `XML_Parse` call. Each handler assigns `ctx->tok`, so
|
|
360
|
+
* only the last event's tokens reach the parser; what the others wrote still
|
|
361
|
+
* went to stderr. Then the call's error, if any.
|
|
362
|
+
* @see lib/common/htmllex.c:htmllex
|
|
300
363
|
*/
|
|
301
|
-
function
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
if (
|
|
306
|
-
j = skipSpace(chunk, j + 1);
|
|
307
|
-
const q = chunk[j];
|
|
308
|
-
if (q !== '"' && q !== "'") return invalidToken(j);
|
|
309
|
-
const end = chunk.indexOf(q, j + 1);
|
|
310
|
-
return end < 0 ? undefined : { name, next: end + 1 };
|
|
364
|
+
function runEvents(run: LexRun, res: ExpatResult): void {
|
|
365
|
+
const groups = eventGroups(run, res);
|
|
366
|
+
const last = groups.pop();
|
|
367
|
+
for (const g of groups) absorb(run.carry, g.tokens);
|
|
368
|
+
if (last !== undefined) deliver(run, last);
|
|
311
369
|
}
|
|
312
370
|
|
|
313
371
|
/**
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
372
|
+
* `string : string T_string`. A run of T_string tokens (a comment between two
|
|
373
|
+
* strings yields no token) is one string: the characters accumulate in the
|
|
374
|
+
* shared buffer until the parser reduces them.
|
|
375
|
+
* @see lib/common/htmlparse.y:string
|
|
318
376
|
*/
|
|
319
|
-
function
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
377
|
+
function appendString(into: TextToken, more: TextToken): void {
|
|
378
|
+
into.value += more.value;
|
|
379
|
+
if (more.warnings !== undefined) into.warnings = [...(into.warnings ?? []), ...more.warnings];
|
|
380
|
+
if (more.warn === true) into.warn = true;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** The label string as C holds it: `start` and `end` bound the part `findNext` scans. */
|
|
384
|
+
export interface LabelRange {
|
|
385
|
+
start: number;
|
|
386
|
+
end: number;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
interface LexUnit extends Unit {
|
|
390
|
+
start: number;
|
|
332
391
|
}
|
|
333
392
|
|
|
334
393
|
/**
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
* @see lib/common/htmllex.c:lexerror
|
|
394
|
+
* C's units: `<HTML>` first, the label cut by `findNext`, then `</HTML>`. A
|
|
395
|
+
* source without the wrapper has empty ends.
|
|
396
|
+
* @see lib/common/htmllex.c:htmllex
|
|
339
397
|
*/
|
|
340
|
-
function
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
} else {
|
|
348
|
-
const bad = findAttrError(st.curr, 1 + tag.name.length);
|
|
349
|
-
if (bad !== undefined) {
|
|
350
|
-
throw htmlErrorInLine(bad.message, lineAt(src, start + bad.offset), st.prev + st.curr);
|
|
351
|
-
}
|
|
352
|
-
if (!tag.selfClose) st.stack.push(tag.name);
|
|
398
|
+
function* lexUnits(src: string, label: LabelRange): Generator<LexUnit> {
|
|
399
|
+
if (label.start > 0) yield { start: 0, end: label.start, warnings: [], warn: false };
|
|
400
|
+
const text = src.slice(label.start, label.end);
|
|
401
|
+
for (let at = 0; at < text.length;) {
|
|
402
|
+
const u = findNext(text, at);
|
|
403
|
+
yield { start: label.start + at, end: label.start + u.end, warnings: u.warnings, warn: u.warn };
|
|
404
|
+
at = u.end;
|
|
353
405
|
}
|
|
354
|
-
if (
|
|
355
|
-
const text = `Unknown HTML element <${tag.name}> on line ${lineAt(src, start)} `;
|
|
356
|
-
throw new HtmlLexError(tag.name.toUpperCase(), text, [`Error: ${text}`]);
|
|
406
|
+
if (label.end < src.length) yield { start: label.end, end: src.length, warnings: [], warn: false };
|
|
357
407
|
}
|
|
358
408
|
|
|
359
|
-
/**
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
if (off >= 0) {
|
|
367
|
-
throw htmlErrorInLine(XML_ERROR_UNDEFINED_ENTITY, lineAt(src, start + off), st.prev + st.curr);
|
|
368
|
-
}
|
|
409
|
+
/**
|
|
410
|
+
* The syntax error yacc reports when the stream ends while expat still holds
|
|
411
|
+
* an unfinished token (the held bytes swallowed the closing `</HTML>`).
|
|
412
|
+
* @see lib/common/htmllex.c:htmlerror
|
|
413
|
+
*/
|
|
414
|
+
function unfinishedError(run: LexRun, held: number): HtmlParseError {
|
|
415
|
+
return htmlErrorInLine(SYNTAX_ERROR, lineAt(run.src, held), run.st.prev + run.st.curr);
|
|
369
416
|
}
|
|
370
417
|
|
|
371
418
|
/** Tokens produced before the first lexical error, plus that error. */
|
|
372
419
|
export interface TokenizeResult {
|
|
373
420
|
tokens: Token[];
|
|
374
421
|
error?: HtmlParseError;
|
|
422
|
+
/** Output of the lexer after its last token: printed when the parser asks for more. */
|
|
423
|
+
eof?: LexerOutput;
|
|
375
424
|
}
|
|
376
425
|
|
|
426
|
+
const eofOutput = (carry: Carry): LexerOutput | undefined => {
|
|
427
|
+
if (carry.warnings.length === 0 && !carry.warn) return undefined;
|
|
428
|
+
return carry.warn ? { warnings: carry.warnings, warn: true } : { warnings: carry.warnings };
|
|
429
|
+
};
|
|
430
|
+
|
|
377
431
|
/**
|
|
378
432
|
* Tokenize until the first lexical error and return both. C interleaves
|
|
379
433
|
* lexing with the yacc parse, so a syntax error in the tokens before the
|
|
380
434
|
* failing chunk is reported in preference to the lexical error; callers that
|
|
381
435
|
* parse the partial stream get that precedence.
|
|
436
|
+
* @see lib/common/htmllex.c:htmllex
|
|
382
437
|
* @see lib/common/htmllex.c:findNext
|
|
383
438
|
*/
|
|
384
|
-
export function tokenizeDeferred(src: string): TokenizeResult {
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
439
|
+
export function tokenizeDeferred(src: string, label: LabelRange = { start: 0, end: src.length }): TokenizeResult {
|
|
440
|
+
const run: LexRun = {
|
|
441
|
+
src, tokens: [], st: { stack: [], prev: '', curr: '' }, expat: new ExpatFeed(src),
|
|
442
|
+
carry: { warnings: [], warn: false },
|
|
443
|
+
};
|
|
444
|
+
let error: HtmlParseError | undefined;
|
|
388
445
|
try {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
const info: ChunkInfo = { src, pos: start, end: i, context: st.prev + st.curr };
|
|
397
|
-
for (let k = first; k < tokens.length; k++) chunkInfo.set(tokens[k]!, info);
|
|
446
|
+
for (const u of lexUnits(src, label)) {
|
|
447
|
+
run.st.prev = run.st.curr;
|
|
448
|
+
run.st.curr = src.slice(u.start, u.end);
|
|
449
|
+
run.carry.warnings.push(...u.warnings);
|
|
450
|
+
if (u.warn) run.carry.warn = true;
|
|
451
|
+
const nbytes = fedBytes(run.st.curr, !run.st.curr.startsWith('<'));
|
|
452
|
+
runEvents(run, run.expat.feed(u.start, u.end, nbytes));
|
|
398
453
|
}
|
|
454
|
+
const held = run.expat.held;
|
|
455
|
+
if (held !== undefined) error = unfinishedError(run, held);
|
|
399
456
|
} catch (e) {
|
|
400
|
-
if (e instanceof HtmlParseError)
|
|
401
|
-
|
|
457
|
+
if (!(e instanceof HtmlParseError)) throw e;
|
|
458
|
+
error = e;
|
|
402
459
|
}
|
|
403
|
-
|
|
460
|
+
const result: TokenizeResult = { tokens: run.tokens };
|
|
461
|
+
if (error !== undefined) result.error = error;
|
|
462
|
+
const eof = eofOutput(run.carry);
|
|
463
|
+
if (eof !== undefined) result.eof = eof;
|
|
464
|
+
return result;
|
|
404
465
|
}
|
|
405
466
|
|
|
406
467
|
/** @see lib/common/htmllex.c:findNext */
|
|
@@ -66,7 +66,7 @@ export const parseTitle = (a: Record<string, string>): string | undefined =>
|
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
68
|
* Parse SIDES attribute into a bitmask matching C sidesfn().
|
|
69
|
-
* Unrecognised chars are
|
|
69
|
+
* Unrecognised chars are skipped (the lexer has already warned, as C does).
|
|
70
70
|
* C guard: if (flags != BORDER_MASK) p->flags |= flags — the full-mask case
|
|
71
71
|
* is a no-op (means "all sides", the default), so we return undefined for it.
|
|
72
72
|
* @see lib/common/htmllex.c:sidesfn
|
|
@@ -86,31 +86,27 @@ export const parseSides = (v: string | undefined): number | undefined => {
|
|
|
86
86
|
};
|
|
87
87
|
|
|
88
88
|
/**
|
|
89
|
-
* Parse GRADIENTANGLE attribute —
|
|
90
|
-
*
|
|
89
|
+
* Parse GRADIENTANGLE attribute — `strtol` prefix, as `num`. The lexer has
|
|
90
|
+
* already dropped values outside [0, 360] (doInt).
|
|
91
91
|
* @see lib/common/htmllex.c:gradientanglefn
|
|
92
92
|
*/
|
|
93
|
-
export const parseGradientAngle = (v: string | undefined): number | undefined =>
|
|
94
|
-
if (v === undefined) return undefined;
|
|
95
|
-
const n = parseInt(v, 10);
|
|
96
|
-
if (isNaN(n) || String(n) !== v.trim()) return undefined;
|
|
97
|
-
if (n < 0 || n > 360) return undefined;
|
|
98
|
-
return n;
|
|
99
|
-
};
|
|
93
|
+
export const parseGradientAngle = (v: string | undefined): number | undefined => num(v);
|
|
100
94
|
|
|
101
95
|
/**
|
|
102
|
-
* Parse COLUMNS attribute —
|
|
96
|
+
* Parse COLUMNS attribute — a value starting with "*" sets vrule=true
|
|
97
|
+
* (C tests `*v != '*'`; anything else was already ignored by the lexer).
|
|
103
98
|
* @see lib/common/htmllex.c:columnsfn
|
|
104
99
|
*/
|
|
105
100
|
export const parseColumns = (v: string | undefined): true | undefined =>
|
|
106
|
-
v
|
|
101
|
+
v?.startsWith('*') === true ? true : undefined;
|
|
107
102
|
|
|
108
103
|
/**
|
|
109
|
-
* Parse ROWS attribute —
|
|
104
|
+
* Parse ROWS attribute — a value starting with "*" sets hrule=true
|
|
105
|
+
* (C tests `*v != '*'`; anything else was already ignored by the lexer).
|
|
110
106
|
* @see lib/common/htmllex.c:rowsfn
|
|
111
107
|
*/
|
|
112
108
|
export const parseRows = (v: string | undefined): true | undefined =>
|
|
113
|
-
v
|
|
109
|
+
v?.startsWith('*') === true ? true : undefined;
|
|
114
110
|
|
|
115
111
|
export const buildCell = (content: HtmlCellContent[], a: Record<string, string>): HtmlCell => ({
|
|
116
112
|
kind: 'cell',
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EPL-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Font stack of the HTML-like label parser: `pushFont` merging, the inline
|
|
4
|
+
* tags' fonts, and the font a `fonttable` wrapper gives its table.
|
|
5
|
+
*
|
|
6
|
+
* @see lib/common/htmlparse.y:pushFont
|
|
7
|
+
* @see lib/common/htmlparse.y:fonttable
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { HtmlTableFont, HtmlTextItem, OpenToken } from './htmltable-types.js';
|
|
11
|
+
import { num } from './htmltable-parse-attrs.js';
|
|
12
|
+
|
|
13
|
+
export interface FontState {
|
|
14
|
+
bold?: boolean;
|
|
15
|
+
italic?: boolean;
|
|
16
|
+
underline?: boolean;
|
|
17
|
+
overline?: boolean;
|
|
18
|
+
strikethrough?: boolean;
|
|
19
|
+
subscript?: boolean;
|
|
20
|
+
superscript?: boolean;
|
|
21
|
+
fontColor?: string;
|
|
22
|
+
fontFace?: string;
|
|
23
|
+
fontSize?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const definedOf = (s: FontState): Partial<FontState> =>
|
|
27
|
+
Object.fromEntries(
|
|
28
|
+
Object.entries(s).filter(([, v]) => v !== undefined),
|
|
29
|
+
) as Partial<FontState>;
|
|
30
|
+
|
|
31
|
+
export const mergeFontState = (parent: FontState, child: FontState): FontState =>
|
|
32
|
+
({ ...parent, ...definedOf(child) }) as FontState;
|
|
33
|
+
|
|
34
|
+
const BOOL_FONT_KEYS = [
|
|
35
|
+
'bold', 'italic', 'underline', 'overline',
|
|
36
|
+
'strikethrough', 'subscript', 'superscript',
|
|
37
|
+
] as const;
|
|
38
|
+
type BoolFontKey = typeof BOOL_FONT_KEYS[number];
|
|
39
|
+
|
|
40
|
+
export const fontItemProps = (f: FontState): Partial<HtmlTextItem> => {
|
|
41
|
+
const out: Partial<HtmlTextItem> = {};
|
|
42
|
+
for (const k of BOOL_FONT_KEYS) {
|
|
43
|
+
if (f[k]) out[k as BoolFontKey] = true;
|
|
44
|
+
}
|
|
45
|
+
if (f.fontColor) out.fontColor = f.fontColor;
|
|
46
|
+
if (f.fontFace) out.fontFace = f.fontFace;
|
|
47
|
+
if (f.fontSize !== undefined) out.fontSize = f.fontSize;
|
|
48
|
+
return out;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
export const isInlineTag = (tag: string): boolean =>
|
|
53
|
+
['B', 'I', 'U', 'O', 'S', 'SUB', 'SUP'].includes(tag);
|
|
54
|
+
|
|
55
|
+
export const inlineFontState = (tag: string, _attrs: Record<string, string>): FontState => {
|
|
56
|
+
const fs: FontState = {};
|
|
57
|
+
if (tag === 'B') fs.bold = true;
|
|
58
|
+
if (tag === 'I') fs.italic = true;
|
|
59
|
+
if (tag === 'U') fs.underline = true;
|
|
60
|
+
if (tag === 'O') fs.overline = true;
|
|
61
|
+
if (tag === 'S') fs.strikethrough = true;
|
|
62
|
+
if (tag === 'SUB') fs.subscript = true;
|
|
63
|
+
if (tag === 'SUP') fs.superscript = true;
|
|
64
|
+
return fs;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** The font `pushFont` pushes for a wrapper tag, merged over the enclosing one. */
|
|
68
|
+
export const wrapperFont = (t: OpenToken, fontStack: FontState[]): FontState => {
|
|
69
|
+
const font = fontStack[fontStack.length - 1] ?? {};
|
|
70
|
+
if (t.tag !== 'FONT') return mergeFontState(font, inlineFontState(t.tag, t.attrs));
|
|
71
|
+
return mergeFontState(font, {
|
|
72
|
+
fontColor: t.attrs['color'],
|
|
73
|
+
fontFace: t.attrs['face'],
|
|
74
|
+
fontSize: num(t.attrs['point-size']),
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* `$2->font = *LIST_BACK(fontstack)`: set only while a `fonttable` wrapper's
|
|
80
|
+
* font is on the stack (the base entry stands for C's NULL).
|
|
81
|
+
* @see lib/common/htmlparse.y:294
|
|
82
|
+
*/
|
|
83
|
+
export const tableFont = (fontStack: FontState[]): HtmlTableFont | undefined => {
|
|
84
|
+
if (fontStack.length < 2) return undefined;
|
|
85
|
+
const { fontColor, fontFace, fontSize } = fontStack[fontStack.length - 1]!;
|
|
86
|
+
const font: HtmlTableFont = {};
|
|
87
|
+
if (fontColor !== undefined) font.fontColor = fontColor;
|
|
88
|
+
if (fontFace !== undefined) font.fontFace = fontFace;
|
|
89
|
+
if (fontSize !== undefined) font.fontSize = fontSize;
|
|
90
|
+
return font;
|
|
91
|
+
};
|
|
92
|
+
|