@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.
Files changed (91) hide show
  1. package/dist/common/arrows-shapes-poly.d.ts.map +1 -1
  2. package/dist/common/bezier.d.ts +12 -0
  3. package/dist/common/bezier.d.ts.map +1 -0
  4. package/dist/common/htmltable-attr-warn.d.ts +41 -0
  5. package/dist/common/htmltable-attr-warn.d.ts.map +1 -0
  6. package/dist/common/htmltable-fontinfo.d.ts +42 -0
  7. package/dist/common/htmltable-fontinfo.d.ts.map +1 -0
  8. package/dist/common/htmltable-lex-expat.d.ts +63 -0
  9. package/dist/common/htmltable-lex-expat.d.ts.map +1 -0
  10. package/dist/common/htmltable-lex-units.d.ts +32 -0
  11. package/dist/common/htmltable-lex-units.d.ts.map +1 -0
  12. package/dist/common/htmltable-lex.d.ts +10 -2
  13. package/dist/common/htmltable-lex.d.ts.map +1 -1
  14. package/dist/common/htmltable-parse-attrs.d.ts +7 -5
  15. package/dist/common/htmltable-parse-attrs.d.ts.map +1 -1
  16. package/dist/common/htmltable-parse-font.d.ts +33 -0
  17. package/dist/common/htmltable-parse-font.d.ts.map +1 -0
  18. package/dist/common/htmltable-parse-state.d.ts +39 -0
  19. package/dist/common/htmltable-parse-state.d.ts.map +1 -0
  20. package/dist/common/htmltable-parse.d.ts +18 -23
  21. package/dist/common/htmltable-parse.d.ts.map +1 -1
  22. package/dist/common/htmltable-pos.d.ts.map +1 -1
  23. package/dist/common/htmltable-types.d.ts +33 -3
  24. package/dist/common/htmltable-types.d.ts.map +1 -1
  25. package/dist/common/htmltable.d.ts +3 -2
  26. package/dist/common/htmltable.d.ts.map +1 -1
  27. package/dist/common/spline-midpoint.d.ts.map +1 -1
  28. package/dist/common/splines-geom.d.ts +5 -6
  29. package/dist/common/splines-geom.d.ts.map +1 -1
  30. package/dist/common/splines-routespl.d.ts.map +1 -1
  31. package/dist/common/taper.d.ts.map +1 -1
  32. package/dist/gvc/anchor.d.ts.map +1 -1
  33. package/dist/index.js +1366 -648
  34. package/dist/index.js.map +4 -4
  35. package/dist/layout/dot/compound-clip.d.ts +5 -5
  36. package/dist/layout/dot/compound-clip.d.ts.map +1 -1
  37. package/dist/layout/dot/compound-geom.d.ts +2 -7
  38. package/dist/layout/dot/compound-geom.d.ts.map +1 -1
  39. package/dist/layout/dot/compound.d.ts +1 -1
  40. package/dist/layout/dot/compound.d.ts.map +1 -1
  41. package/dist/layout/dot/edge-route-clip.d.ts +1 -1
  42. package/dist/layout/dot/edge-route-clip.d.ts.map +1 -1
  43. package/dist/layout/dot/ortho-adapter.d.ts +13 -4
  44. package/dist/layout/dot/ortho-adapter.d.ts.map +1 -1
  45. package/dist/layout/dot/splines-flat-labeled.d.ts.map +1 -1
  46. package/dist/layout/dot/splines-label.d.ts.map +1 -1
  47. package/dist/layout/neato/bfs.d.ts +10 -3
  48. package/dist/layout/neato/bfs.d.ts.map +1 -1
  49. package/dist/layout/neato/init.d.ts +6 -11
  50. package/dist/layout/neato/init.d.ts.map +1 -1
  51. package/dist/layout/neato/splines.d.ts.map +1 -1
  52. package/dist/parser/index.d.ts.map +1 -1
  53. package/dist/parser/scan-token.d.ts +40 -0
  54. package/dist/parser/scan-token.d.ts.map +1 -0
  55. package/dist/render/svg-edge-split.d.ts.map +1 -1
  56. package/dist/render.js +1388 -670
  57. package/dist/render.js.map +4 -4
  58. package/package.json +1 -1
  59. package/src/common/arrows-shapes-poly.ts +9 -10
  60. package/src/common/bezier.ts +33 -0
  61. package/src/common/htmltable-attr-warn.ts +270 -0
  62. package/src/common/htmltable-fontinfo.ts +83 -0
  63. package/src/common/htmltable-lex-expat.ts +357 -0
  64. package/src/common/htmltable-lex-units.ts +75 -0
  65. package/src/common/htmltable-lex.ts +221 -160
  66. package/src/common/htmltable-parse-attrs.ts +10 -14
  67. package/src/common/htmltable-parse-font.ts +92 -0
  68. package/src/common/htmltable-parse-state.ts +64 -0
  69. package/src/common/htmltable-parse.ts +138 -98
  70. package/src/common/htmltable-pos.ts +9 -8
  71. package/src/common/htmltable-types.ts +35 -3
  72. package/src/common/htmltable.ts +14 -7
  73. package/src/common/spline-midpoint.ts +2 -22
  74. package/src/common/splines-geom.ts +5 -25
  75. package/src/common/splines-routespl.ts +21 -13
  76. package/src/common/taper.ts +2 -14
  77. package/src/gvc/anchor.ts +5 -14
  78. package/src/layout/dot/__fixtures__/2796-flat-labeled-lost.dot +161 -0
  79. package/src/layout/dot/compound-clip.ts +9 -19
  80. package/src/layout/dot/compound-geom.ts +6 -23
  81. package/src/layout/dot/compound.ts +1 -1
  82. package/src/layout/dot/edge-route-clip.ts +6 -29
  83. package/src/layout/dot/ortho-adapter.ts +5 -5
  84. package/src/layout/dot/splines-flat-labeled.ts +3 -1
  85. package/src/layout/dot/splines-label.ts +3 -29
  86. package/src/layout/neato/bfs.ts +13 -4
  87. package/src/layout/neato/init.ts +15 -15
  88. package/src/layout/neato/splines.ts +4 -8
  89. package/src/parser/index.ts +13 -1
  90. package/src/parser/scan-token.ts +289 -0
  91. package/src/render/svg-edge-split.ts +5 -13
@@ -0,0 +1,64 @@
1
+ // SPDX-License-Identifier: EPL-2.0
2
+ /**
3
+ * Cursor state of the HTML-like label parser and the consume step that prints
4
+ * a token's lexer warnings. C lexes a token when the parser reaches it, so the
5
+ * warnings of a token the parser never gets to print nothing; T9's lexer
6
+ * warnings ride on the same `warnings` array.
7
+ *
8
+ * @see lib/common/htmllex.c:doAttrs
9
+ * @see lib/common/htmllex.c:clearHTMLlexer
10
+ */
11
+
12
+ import type { HtmlParseError, LexerOutput, Token } from './htmltable-types.js';
13
+ import type { StderrLines } from './htmltable-attr-warn.js';
14
+
15
+ export interface ParseState {
16
+ tokens: Token[];
17
+ pos: number;
18
+ /** Lexical error found after the last token: C reports it when the parser asks for more. */
19
+ pending?: HtmlParseError;
20
+ /** Where lexer warnings go as tokens are reached (C's stderr, line by line); none: silent. */
21
+ stderr?: StderrLines;
22
+ /** Tokens whose warnings are already out (a token is lexed once). */
23
+ emitted?: WeakSet<Token>;
24
+ /** `ctx->warn`: a reached token's warning made the label's rv 1. */
25
+ warn?: boolean;
26
+ /** What the lexer wrote after its last token, printed when the parser asks for more. */
27
+ eof?: LexerOutput;
28
+ }
29
+
30
+ export const peek = (s: ParseState): Token | undefined => s.tokens[s.pos];
31
+
32
+ /**
33
+ * Print what the C lexer wrote while lexing `tok`, once, and fold its `warn`
34
+ * into the label's rv.
35
+ */
36
+ export const emitWarnings = (s: ParseState, tok: Token): void => {
37
+ if (s.stderr === undefined || s.emitted?.has(tok) === true) return;
38
+ s.emitted?.add(tok);
39
+ emitOutput(s, tok);
40
+ };
41
+
42
+ const emitOutput = (s: ParseState, out: LexerOutput): void => {
43
+ for (const text of out.warnings ?? []) s.stderr?.write(text);
44
+ if (out.warn === true) s.warn = true;
45
+ };
46
+
47
+ /**
48
+ * The parser ran off the last token: C lexes the rest now, so what it wrote
49
+ * after that token (warnings of units that produced none) prints once.
50
+ */
51
+ export const emitEof = (s: ParseState): void => {
52
+ if (s.stderr === undefined || s.eof === undefined) return;
53
+ const out = s.eof;
54
+ delete s.eof;
55
+ emitOutput(s, out);
56
+ };
57
+
58
+ /** Take the token at the cursor; its warnings print now. */
59
+ export const consume = (s: ParseState): Token => {
60
+ const t = s.tokens[s.pos];
61
+ s.pos++;
62
+ if (t !== undefined) emitWarnings(s, t);
63
+ return t;
64
+ };
@@ -20,7 +20,14 @@ import type {
20
20
  OpenToken,
21
21
  } from './htmltable-types.js';
22
22
  import { HtmlParseError } from './htmltable-types.js';
23
+ import { StderrLines } from './htmltable-attr-warn.js';
24
+ import type { ParseState } from './htmltable-parse-state.js';
25
+ import { consume, emitEof, emitWarnings, peek } from './htmltable-parse-state.js';
23
26
  import { htmlErrorAtToken, tokenizeDeferred } from './htmltable-lex.js';
27
+ import type { FontState } from './htmltable-parse-font.js';
28
+ import {
29
+ fontItemProps, inlineFontState, isInlineTag, mergeFontState, tableFont, wrapperFont,
30
+ } from './htmltable-parse-font.js';
24
31
  import {
25
32
  buildCell,
26
33
  buildTable,
@@ -30,59 +37,6 @@ import {
30
37
  parseRows,
31
38
  } from './htmltable-parse-attrs.js';
32
39
 
33
- // ---------------------------------------------------------------------------
34
- // Font-stack — mirrors pushFont / popFont in htmlparse.y
35
- // ---------------------------------------------------------------------------
36
-
37
- interface FontState {
38
- bold?: boolean;
39
- italic?: boolean;
40
- underline?: boolean;
41
- overline?: boolean;
42
- strikethrough?: boolean;
43
- subscript?: boolean;
44
- superscript?: boolean;
45
- fontColor?: string;
46
- fontFace?: string;
47
- fontSize?: number;
48
- }
49
-
50
- const definedOf = (s: FontState): Partial<FontState> =>
51
- Object.fromEntries(
52
- Object.entries(s).filter(([, v]) => v !== undefined),
53
- ) as Partial<FontState>;
54
-
55
- const mergeFontState = (parent: FontState, child: FontState): FontState =>
56
- ({ ...parent, ...definedOf(child) }) as FontState;
57
-
58
- const BOOL_FONT_KEYS = [
59
- 'bold', 'italic', 'underline', 'overline',
60
- 'strikethrough', 'subscript', 'superscript',
61
- ] as const;
62
- type BoolFontKey = typeof BOOL_FONT_KEYS[number];
63
-
64
- const fontItemProps = (f: FontState): Partial<HtmlTextItem> => {
65
- const out: Partial<HtmlTextItem> = {};
66
- for (const k of BOOL_FONT_KEYS) {
67
- if (f[k]) out[k as BoolFontKey] = true;
68
- }
69
- if (f.fontColor) out.fontColor = f.fontColor;
70
- if (f.fontFace) out.fontFace = f.fontFace;
71
- if (f.fontSize !== undefined) out.fontSize = f.fontSize;
72
- return out;
73
- };
74
-
75
- // ---------------------------------------------------------------------------
76
- // Parser state
77
- // ---------------------------------------------------------------------------
78
-
79
- interface ParseState {
80
- tokens: Token[];
81
- pos: number;
82
- /** Lexical error found after the last token: C reports it when the parser asks for more. */
83
- pending?: HtmlParseError;
84
- }
85
-
86
40
  /** bison's default message for an unexpected token. @see lib/common/htmlparse.y */
87
41
  const MSG_SYNTAX_ERROR = 'syntax error';
88
42
  /** @see lib/common/htmlparse.y:288 (table rule) */
@@ -103,36 +57,17 @@ const nonSpace = (text: string): boolean => /[^ \t\n\r]/.test(text);
103
57
  * A token without a recorded chunk yields a plain, silent HtmlParseError.
104
58
  * @see lib/common/htmllex.c:htmlerror
105
59
  */
106
- const parseError = (s: ParseState, tok: Token | undefined, message: string): HtmlParseError =>
107
- (tok === undefined ? s.pending : htmlErrorAtToken(message, tok)) ?? new HtmlParseError(message);
108
-
109
- const peek = (s: ParseState): Token | undefined => s.tokens[s.pos];
110
-
111
- const consume = (s: ParseState): Token => {
112
- const t = s.tokens[s.pos];
113
- s.pos++;
114
- return t;
60
+ const parseError = (s: ParseState, tok: Token | undefined, message: string): HtmlParseError => {
61
+ // The lexer already ran for the token the error is reported at.
62
+ if (tok !== undefined) emitWarnings(s, tok);
63
+ else emitEof(s);
64
+ return (tok === undefined ? s.pending : htmlErrorAtToken(message, tok)) ?? new HtmlParseError(message);
115
65
  };
116
66
 
117
67
  // ---------------------------------------------------------------------------
118
68
  // Inline-tag helpers — mirrors htmlparse.y fonttext / inline rules
119
69
  // ---------------------------------------------------------------------------
120
70
 
121
- const isInlineTag = (tag: string): boolean =>
122
- ['B', 'I', 'U', 'O', 'S', 'SUB', 'SUP'].includes(tag);
123
-
124
- const inlineFontState = (tag: string, _attrs: Record<string, string>): FontState => {
125
- const fs: FontState = {};
126
- if (tag === 'B') fs.bold = true;
127
- if (tag === 'I') fs.italic = true;
128
- if (tag === 'U') fs.underline = true;
129
- if (tag === 'O') fs.overline = true;
130
- if (tag === 'S') fs.strikethrough = true;
131
- if (tag === 'SUB') fs.subscript = true;
132
- if (tag === 'SUP') fs.superscript = true;
133
- return fs;
134
- };
135
-
136
71
  /**
137
72
  * `br: T_br T_end_br`, likewise image, HR and VR: the open tag must be
138
73
  * followed at once by its close (`<BR/>` yields both); anything else is a
@@ -219,6 +154,70 @@ export const parseText = (s: ParseState, fontStack: FontState[]): HtmlText => {
219
154
  return { kind: 'text', items };
220
155
  };
221
156
 
157
+ // ---------------------------------------------------------------------------
158
+ // fonttable — a <TABLE> wrapped in one font-like tag
159
+ // ---------------------------------------------------------------------------
160
+
161
+ /** Tags of `fonttable : font table n_font | italic ... | bold ...`. @see lib/common/htmlparse.y:307 */
162
+ const FONTTABLE_TAGS: readonly string[] = ['FONT', 'B', 'I', 'U', 'O'];
163
+
164
+ /**
165
+ * The wrapper tag when the tokens at the cursor open a `fonttable`: one of
166
+ * FONTTABLE_TAGS, then only strings (the table rule's opt_space), then TABLE.
167
+ */
168
+ const fontTableWrapper = (s: ParseState): OpenToken | undefined => {
169
+ const open = peek(s);
170
+ if (open?.type !== 'open' || !FONTTABLE_TAGS.includes(open.tag)) return undefined;
171
+ let i = s.pos + 1;
172
+ while (s.tokens[i]?.type === 'text') i++;
173
+ const next = s.tokens[i];
174
+ return next?.type === 'open' && next.tag === 'TABLE' ? open : undefined;
175
+ };
176
+
177
+ /** Consume strings; true when any is non-space (the table rule's nonSpace check). */
178
+ const skipStrings = (s: ParseState): boolean => {
179
+ let found = false;
180
+ for (let t = peek(s); t?.type === 'text'; t = peek(s)) {
181
+ consume(s);
182
+ if (nonSpace(t.value)) found = true;
183
+ }
184
+ return found;
185
+ };
186
+
187
+ /**
188
+ * `font table n_font` (and the I/U/O/B forms): the wrapper's font sits on the
189
+ * fontstack while the table parses, then pops at the closing tag.
190
+ * @see lib/common/htmlparse.y:fonttable
191
+ */
192
+ const parseFontTable = (s: ParseState, open: OpenToken, fontStack: FontState[]): HtmlTable => {
193
+ consume(s);
194
+ fontStack.push(wrapperFont(open, fontStack));
195
+ const textBefore = skipStrings(s);
196
+ const tblTok = peek(s);
197
+ if (textBefore && tblTok !== undefined) throw parseError(s, tblTok, MSG_TEXT_BEFORE_TABLE);
198
+ const table = parseTable(s, fontStack);
199
+ const textAfter = skipStrings(s);
200
+ const close = peek(s);
201
+ if (close !== undefined && textAfter) throw parseError(s, close, MSG_TEXT_AFTER_TABLE);
202
+ if (close?.type !== 'close' || close.tag !== open.tag) throw parseError(s, close, MSG_SYNTAX_ERROR);
203
+ consume(s);
204
+ fontStack.pop();
205
+ return table;
206
+ };
207
+
208
+ /**
209
+ * A <TABLE> that follows a consumed tag can only be a syntax error: the table
210
+ * rule's opt_space admits strings alone.
211
+ * @see lib/common/htmlparse.y:287
212
+ */
213
+ const rejectTableAfterTag = (s: ParseState, start: number): void => {
214
+ const nxt = peek(s);
215
+ if (nxt?.type !== 'open' || nxt.tag !== 'TABLE') return;
216
+ if (s.tokens.slice(start, s.pos).some((t) => t.type !== 'text')) {
217
+ throw parseError(s, nxt, MSG_SYNTAX_ERROR);
218
+ }
219
+ };
220
+
222
221
  // ---------------------------------------------------------------------------
223
222
  // Cell content helpers — mirrors htmlparse.y cell rule
224
223
  // ---------------------------------------------------------------------------
@@ -238,7 +237,7 @@ const parseImgContent = (
238
237
 
239
238
  /** What a cell holds so far: C's cell has exactly one child. */
240
239
  interface CellScan {
241
- kind: 'none' | 'text' | 'table' | 'image';
240
+ kind: 'none' | 'text' | 'table' | 'fonttable' | 'image';
242
241
  /** Non-space text before the nested table (checked when the table opens). */
243
242
  textNonSpace: boolean;
244
243
  /** Non-space text after the nested table (checked when the run ends). */
@@ -261,6 +260,7 @@ const scanCellText = (s: ParseState, t: Token, fontStack: FontState[], st: CellS
261
260
  const start = s.pos;
262
261
  const txt = parseText(s, fontStack);
263
262
  if (s.pos === start) throw parseError(s, t, MSG_SYNTAX_ERROR);
263
+ rejectTableAfterTag(s, start);
264
264
  if (txt.items.some((i) => i.text !== undefined && nonSpace(i.text))) st.textNonSpace = true;
265
265
  st.kind = 'text';
266
266
  return txt.items.length > 0 ? [txt] : [];
@@ -287,7 +287,13 @@ const scanCellImage = (
287
287
  const scanCellItem = (
288
288
  s: ParseState, t: Token, cellAttrs: Record<string, string>, fontStack: FontState[], st: CellScan,
289
289
  ): HtmlCellContent[] => {
290
+ if (st.kind === 'fonttable') throw parseError(s, t, MSG_SYNTAX_ERROR);
290
291
  if (st.kind === 'table') return scanAfterTable(s, t, st);
292
+ const wrapper = st.kind === 'none' ? fontTableWrapper(s) : undefined;
293
+ if (wrapper !== undefined) {
294
+ st.kind = 'fonttable';
295
+ return [parseFontTable(s, wrapper, fontStack)];
296
+ }
291
297
  if (t.type === 'open' && t.tag === 'TABLE') return scanCellTable(s, t, fontStack, st);
292
298
  if (t.type === 'open' && t.tag === 'IMG') return scanCellImage(s, t, cellAttrs, st);
293
299
  if (st.kind === 'image') throw parseError(s, t, MSG_SYNTAX_ERROR);
@@ -438,7 +444,10 @@ export const parseTable = (s: ParseState, fontStack: FontState[]): HtmlTable =>
438
444
  for (let t = peek(s); t !== undefined; t = peek(s)) {
439
445
  if (scanTableToken(s, t, fontStack, ts)) break;
440
446
  }
441
- return buildTable(ts.rows, open.attrs);
447
+ const table = buildTable(ts.rows, open.attrs);
448
+ const font = tableFont(fontStack);
449
+ if (font !== undefined) table.font = font;
450
+ return table;
442
451
  };
443
452
 
444
453
  /** Parse one row, applying table-level rule defaults. @see lib/common/htmlparse.y addRow/setCell */
@@ -497,6 +506,16 @@ const finishTableLabel = (s: ParseState): void => {
497
506
  consume(s);
498
507
  };
499
508
 
509
+ /** `html : T_html fonttable T_end_html` with a font-like wrapper. */
510
+ const parseFontTableLabel = (s: ParseState, open: OpenToken, fontStack: FontState[]): HtmlLabel => {
511
+ const table = parseFontTable(s, open, fontStack);
512
+ const nxt = peek(s);
513
+ if (nxt === undefined) return { kind: 'table', table };
514
+ if (nxt.type !== 'close' || nxt.tag !== 'HTML') throw parseError(s, nxt, MSG_SYNTAX_ERROR);
515
+ consume(s);
516
+ return { kind: 'table', table };
517
+ };
518
+
500
519
  const parseTableLabel = (s: ParseState, fontStack: FontState[]): HtmlLabel => {
501
520
  skipLeadingWs(s);
502
521
  const table = parseTable(s, fontStack);
@@ -504,25 +523,22 @@ const parseTableLabel = (s: ParseState, fontStack: FontState[]): HtmlLabel => {
504
523
  return { kind: 'table', table };
505
524
  };
506
525
 
507
- /** `html : T_html fonttext T_end_html`; a `<TABLE>` after text is the table rule's error. */
526
+ /** `html : T_html fonttext T_end_html` (a textitem is required); a `<TABLE>` after text is the table rule's error. */
508
527
  const parseTextLabel = (s: ParseState, fontStack: FontState[]): HtmlLabel => {
509
528
  const texts: HtmlText[] = [];
529
+ const start = s.pos;
510
530
  const txt = parseText(s, fontStack);
511
531
  if (txt.items.length > 0) texts.push(txt);
512
532
  const nxt = peek(s);
513
533
  if (nxt === undefined) return { kind: 'text', texts };
514
534
  if (nxt.type === 'open' && nxt.tag === 'TABLE') {
515
535
  // 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.
536
+ // top-level <TABLE>; after any tag it is a plain syntax error.
519
537
  // @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');
538
+ rejectTableAfterTag(s, start);
539
+ throw parseError(s, nxt, MSG_TEXT_BEFORE_TABLE);
524
540
  }
525
- if (nxt.type !== 'close' || nxt.tag !== 'HTML') throw parseError(s, nxt, MSG_SYNTAX_ERROR);
541
+ if (nxt.type !== 'close' || nxt.tag !== 'HTML' || s.pos === start) throw parseError(s, nxt, MSG_SYNTAX_ERROR);
526
542
  consume(s);
527
543
  return { kind: 'text', texts };
528
544
  };
@@ -532,28 +548,52 @@ const parseTokens = (s: ParseState): HtmlLabel => {
532
548
  const fontStack: FontState[] = [{}];
533
549
  const htmlOpen = consume(s);
534
550
  if (htmlOpen.type !== 'open' || htmlOpen.tag !== 'HTML') throw new HtmlParseError('HTML');
551
+ const wrapper = fontTableWrapper(s);
552
+ if (wrapper !== undefined) return parseFontTableLabel(s, wrapper, fontStack);
535
553
  const firstReal = findFirstRealToken(s.tokens, s.pos);
536
554
  if (firstReal?.type === 'open' && firstReal.tag === 'TABLE') return parseTableLabel(s, fontStack);
537
555
  return parseTextLabel(s, fontStack);
538
556
  };
539
557
 
558
+ /** A parsed label and `ctx->warn`, which `make_html_label` ORs into its rv. */
559
+ export interface ParsedHtmlLabel {
560
+ label: HtmlLabel;
561
+ warn: boolean;
562
+ }
563
+
540
564
  /**
541
- * Parse an HTML-like label string.
542
- * Input is the content between the outer `<` `>` delimiters (already stripped).
565
+ * Parse an HTML-like label string, printing the lexer's warnings (one
566
+ * `writeLine` per stderr line, `console.warn` by default) as the parser reaches
567
+ * their tokens. Input is the content between the outer `<` `>` delimiters
568
+ * (already stripped).
543
569
  *
544
570
  * @see lib/common/htmlparse.y:parseHTML
545
571
  */
546
- export const parseHtmlLabel = (src: string): HtmlLabel => {
547
- const { tokens, error } = tokenizeDeferred(`<HTML>${src}</HTML>`);
548
- const s: ParseState = { tokens, pos: 0, pending: error };
549
- let label: HtmlLabel;
572
+ export const parseHtmlLabelRv = (
573
+ src: string,
574
+ writeLine: (line: string) => void = (line) => console.warn(line),
575
+ ): ParsedHtmlLabel => {
576
+ const { tokens, error, eof } = tokenizeDeferred(`<HTML>${src}</HTML>`, { start: 6, end: 6 + src.length });
577
+ const s: ParseState = {
578
+ tokens, pos: 0, pending: error, stderr: new StderrLines(writeLine), emitted: new WeakSet(), warn: false, eof,
579
+ };
550
580
  try {
551
- label = parseTokens(s);
581
+ const label = parseTokens(s);
582
+ if (error !== undefined) throw error;
583
+ return { label, warn: s.warn === true };
552
584
  } catch (e) {
553
585
  // Running off the truncated stream means C's lexer would have hit the error.
554
- if (error !== undefined && s.pos >= tokens.length) throw error;
586
+ if (error !== undefined && s.pos >= tokens.length) { emitEof(s); throw error; }
555
587
  throw e;
588
+ } finally {
589
+ s.stderr?.flush();
556
590
  }
557
- if (error !== undefined) throw error;
558
- return label;
559
591
  };
592
+
593
+ /**
594
+ * {@link parseHtmlLabelRv} without printing: callers that only inspect the
595
+ * label (the async font/image collection pass) must not repeat the warnings
596
+ * the sync pass prints.
597
+ */
598
+ export const parseHtmlLabel = (src: string): HtmlLabel =>
599
+ parseHtmlLabelRv(src, () => undefined).label;
@@ -20,7 +20,7 @@ import type { TextlabelT } from './types.js';
20
20
  import type { TextMeasurer } from './textmeasure.js';
21
21
  import type { HtmlCell, HtmlImage, HtmlLabel, HtmlTable, HtmlText, ImageSizer } from './htmltable-types.js';
22
22
  import {
23
- layoutHtmlTable, cellPad, cellBorder, sizeHtmlLabel, parseHtmlLabel, inheritTablePenColor,
23
+ layoutHtmlTable, cellPad, cellBorder, sizeHtmlLabel, parseHtmlLabelRv, inheritTablePenColor, withTableFont,
24
24
  } from './htmltable.js';
25
25
  import { makeLabel } from './make-label.js';
26
26
  import { HtmlLexError } from './htmltable-lex.js';
@@ -292,8 +292,8 @@ function alignImageBox(cbox: Box, cell: HtmlCell, iw: number, ih: number): void
292
292
  }
293
293
  }
294
294
 
295
- /** Position a nested <TABLE> child inside the cell's inset content box.
296
- * @see lib/common/htmltable.c:pos_html_cell (HTML_TBL branch) */
295
+ /** Position a nested <TABLE> child inside the cell's inset content box; its font push
296
+ * plays emit_html_tbl's role. @see htmltable.c:pos_html_cell (HTML_TBL branch), emit_html_tbl */
297
297
  function placeCellNested(ctx: CellPlaceCtx): PlacedHtml | undefined {
298
298
  const { cell, tbl, box, finfo, measurer, sidesMask } = ctx;
299
299
  const nested = cell.content.find((c): c is HtmlTable => c.kind === 'table');
@@ -303,7 +303,7 @@ function placeCellNested(ctx: CellPlaceCtx): PlacedHtml | undefined {
303
303
  ll: { x: box.ll.x + inset, y: box.ll.y + inset },
304
304
  ur: { x: box.ur.x - inset, y: box.ur.y - inset },
305
305
  };
306
- return posHtmlTable(nested, finfo, measurer, cbox, sidesMask);
306
+ return withTableFont(finfo, nested.font, 'emit', () => posHtmlTable(nested, finfo, measurer, cbox, sidesMask));
307
307
  }
308
308
 
309
309
  /** Position one cell's text inside its box, copying decoration metadata. @see pos_html_cell */
@@ -516,7 +516,8 @@ export function posHtmlLabel(
516
516
  finfo: HtmlFontInfo,
517
517
  measurer: TextMeasurer,
518
518
  ): PlacedHtml {
519
- if (label.kind === 'table') return posHtmlTable(label.table, finfo, measurer);
519
+ const env = { ...finfo }; // copied: the emit-side savef leak must not outlive the label
520
+ if (label.kind === 'table') return withTableFont(env, label.table.font, 'emit', () => posHtmlTable(label.table, env, measurer));
520
521
  return posTextLabel(label, finfo, measurer);
521
522
  }
522
523
 
@@ -542,9 +543,9 @@ export function makeHtmlLabelRv(
542
543
  measurer: TextMeasurer,
543
544
  ): HtmlLabelResult {
544
545
  const { fontname, fontsize, fontcolor, imageSizer, pencolor } = finfo;
545
- let lbl: HtmlLabel;
546
+ let lbl: HtmlLabel, lexWarn: boolean; // lexWarn: htmllex.c's ctx->warn, rv bit 1
546
547
  try {
547
- lbl = parseHtmlLabel(content);
548
+ ({ label: lbl, warn: lexWarn } = parseHtmlLabelRv(content));
548
549
  } catch (e) {
549
550
  // C aborts the HTML parse on a syntax error (YYABORT): parseHTML returns
550
551
  // rv 3 (ctx->error) with no label, and make_html_label returns it with
@@ -557,7 +558,7 @@ export function makeHtmlLabelRv(
557
558
  // which then flows table->cell->nested-table.
558
559
  // @see lib/common/htmltable.c:1911 (getPenColor), :1406/:1556 (inheritance)
559
560
  if (lbl.kind === 'table') inheritTablePenColor(lbl.table, pencolor);
560
- const rv = sizeHtmlLabel(lbl, measurer, {
561
+ const rv = Number(lexWarn) | sizeHtmlLabel(lbl, measurer, {
561
562
  fontsize, fontname, imageSizer,
562
563
  });
563
564
  const placed = posHtmlLabel(lbl, { fontname, fontsize, fontcolor }, measurer);
@@ -176,10 +176,24 @@ export interface HtmlRow {
176
176
  ruled?: boolean;
177
177
  }
178
178
 
179
+ /**
180
+ * The font a `fonttable` wrapper (or an enclosing one) gives a table: the
181
+ * name, color and size `pushFontInfo` swaps into the environment.
182
+ * @see lib/common/htmltable.h:htmltbl_t.font
183
+ * @see lib/common/htmltable.c:pushFontInfo
184
+ */
185
+ export interface HtmlTableFont {
186
+ fontColor?: string;
187
+ fontFace?: string;
188
+ fontSize?: number;
189
+ }
190
+
179
191
  /** @see lib/common/htmltable.h:htmltbl_t */
180
192
  export interface HtmlTable {
181
193
  kind: 'table';
182
194
  rows: HtmlRow[];
195
+ /** Set only inside a `fonttable` wrapper. @see lib/common/htmlparse.y:294 */
196
+ font?: HtmlTableFont;
183
197
  border?: number;
184
198
  cellborder?: number;
185
199
  cellspacing?: number;
@@ -238,21 +252,39 @@ export type HtmlLabel =
238
252
  // the lex file's function boundaries when it encounters `|` union syntax.
239
253
  // ---------------------------------------------------------------------------
240
254
 
255
+ /**
256
+ * What the C lexer wrote to stderr while producing a token, and the `ctx->warn`
257
+ * it set. Any token can carry it: a unit that yields no token (a comment) or
258
+ * an event lost to a later one in the same `XML_Parse` call still wrote.
259
+ * @see lib/common/htmllex.c:htmllex
260
+ */
261
+ export interface LexerOutput {
262
+ /**
263
+ * Exact text per write, including any trailing newline (a message without
264
+ * one runs into the next write). The parser prints it when it consumes the
265
+ * token.
266
+ * @see lib/common/htmllex.c:doAttrs
267
+ */
268
+ warnings?: string[];
269
+ /** Some handler set `ctx->warn`: the label's `rv` becomes 1. */
270
+ warn?: true;
271
+ }
272
+
241
273
  /** @see lib/common/htmllex.c:startElement */
242
- export interface OpenToken {
274
+ export interface OpenToken extends LexerOutput {
243
275
  type: 'open';
244
276
  tag: string;
245
277
  attrs: Record<string, string>;
246
278
  }
247
279
 
248
280
  /** @see lib/common/htmllex.c:endElement */
249
- export interface CloseToken {
281
+ export interface CloseToken extends LexerOutput {
250
282
  type: 'close';
251
283
  tag: string;
252
284
  }
253
285
 
254
286
  /** @see lib/common/htmllex.c:characterData */
255
- export interface TextToken {
287
+ export interface TextToken extends LexerOutput {
256
288
  type: 'text';
257
289
  value: string;
258
290
  }
@@ -12,10 +12,10 @@ export { HtmlParseError } from './htmltable-types.js';
12
12
  export type {
13
13
  HtmlAlign, HtmlVAlign,
14
14
  HtmlHR, HtmlImage, HtmlTextItem, HtmlText,
15
- HtmlCellContent, HtmlCell, HtmlRow, HtmlTable, HtmlLabel,
15
+ HtmlCellContent, HtmlCell, HtmlRow, HtmlTable, HtmlTableFont, HtmlLabel,
16
16
  ImageSizer,
17
17
  } from './htmltable-types.js';
18
- export { parseHtmlLabel } from './htmltable-parse.js';
18
+ export { parseHtmlLabel, parseHtmlLabelRv } from './htmltable-parse.js';
19
19
  export type { TextMeasurer } from './textmeasure.js';
20
20
 
21
21
  import type {
@@ -26,6 +26,9 @@ import type { ImageSizer } from './htmltable-types.js';
26
26
  import { findImageSize } from '../gvc/usershape.js';
27
27
  import { PointSet } from './pointset.js';
28
28
  import type { TextMeasurer } from './textmeasure.js';
29
+ import { withTableFont } from './htmltable-fontinfo.js';
30
+
31
+ export { withTableFont } from './htmltable-fontinfo.js';
29
32
 
30
33
  const DEFAULT_BORDER = 1;
31
34
  const DEFAULT_CELLPADDING = 2;
@@ -84,7 +87,7 @@ export interface HtmlSizeEnv {
84
87
  imageSizer?: ImageSizer;
85
88
  }
86
89
 
87
- const DEFAULT_SIZE_ENV: HtmlSizeEnv = { fontsize: DEFAULT_FONTSIZE, fontname: '' };
90
+ const defaultSizeEnv = (): HtmlSizeEnv => ({ fontsize: DEFAULT_FONTSIZE, fontname: '' });
88
91
 
89
92
  /** One measured text line (a C "span"). @see size_html_txt */
90
93
  interface SizedLine {
@@ -196,7 +199,7 @@ export const htmlTextIsSimple = (texts: HtmlText[], env: HtmlSizeEnv): boolean =
196
199
  const sizeTextContent = (
197
200
  texts: HtmlText[],
198
201
  measurer: TextMeasurer,
199
- env: HtmlSizeEnv = DEFAULT_SIZE_ENV,
202
+ env: HtmlSizeEnv = defaultSizeEnv(),
200
203
  ): { w: number; h: number } => {
201
204
  const lines = splitSizedLines(texts, measurer, env);
202
205
  const simple = htmlTextIsSimple(texts, env);
@@ -481,7 +484,7 @@ export interface HtmlTableLayout {
481
484
  export const layoutHtmlTable = (
482
485
  tbl: HtmlTable,
483
486
  measurer: TextMeasurer,
484
- env: HtmlSizeEnv = DEFAULT_SIZE_ENV,
487
+ env: HtmlSizeEnv = defaultSizeEnv(),
485
488
  ): HtmlTableLayout => {
486
489
  const spacing = getSpacing(tbl);
487
490
  const border = getBorder(tbl);
@@ -515,7 +518,7 @@ const warnFixedTable = (tbl: HtmlTable, dims: { wd: number; ht: number }): numbe
515
518
  };
516
519
 
517
520
  /** @see lib/common/htmltable.c:size_html_tbl */
518
- const sizeTableInner = (
521
+ const sizeTableBody = (
519
522
  tbl: HtmlTable,
520
523
  measurer: TextMeasurer,
521
524
  env: HtmlSizeEnv,
@@ -546,6 +549,10 @@ const sizeTableInner = (
546
549
  return rv | fixed;
547
550
  };
548
551
 
552
+ /** @see lib/common/htmltable.c:size_html_tbl (pushFontInfo ... popFontInfo around the body) */
553
+ const sizeTableInner = (tbl: HtmlTable, measurer: TextMeasurer, env: HtmlSizeEnv): number =>
554
+ withTableFont(env, tbl.font, 'size', () => sizeTableBody(tbl, measurer, env));
555
+
549
556
  /**
550
557
  * Compute and write `dimen` onto all cells and the label.
551
558
  *
@@ -559,7 +566,7 @@ const sizeTableInner = (
559
566
  export const sizeHtmlLabel = (
560
567
  label: HtmlLabel,
561
568
  measurer: TextMeasurer,
562
- env: HtmlSizeEnv = DEFAULT_SIZE_ENV,
569
+ env: HtmlSizeEnv = defaultSizeEnv(),
563
570
  ): number => {
564
571
  if (label.kind === 'table') {
565
572
  const rv = sizeTableInner(label.table, measurer, env);
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import type { Point } from '../model/geom.js';
12
+ import { bezier } from './bezier.js';
12
13
  import { fma } from './fma.js';
13
14
 
14
15
  type BezLike = { list: Point[]; sflag: number; eflag: number; sp: Point; ep: Point; size?: number };
@@ -30,27 +31,6 @@ export function dist2(p: Point, q: Point): number {
30
31
  /** DIST: sqrt of the fused DIST2 (`fmadd` then `fsqrt`). @see lib/common/geom.h:DIST */
31
32
  function distPt(p: Point, q: Point): number { return Math.sqrt(dist2(p, q)); }
32
33
 
33
- /**
34
- * Point at t on a cubic: C's de Casteljau triangle. Each
35
- * `(1.0 - t) * a + t * b` compiles to `fmul v, b, t; fmla v, a, 1-t`, i.e.
36
- * fma(a, 1 - t, t * b), both in Bezier itself and where dotneato_closest
37
- * inlines it (libgvc 16.1 disassembly). The Left/Right halves are not needed
38
- * here, so only the apex is kept.
39
- * @see lib/common/utils.c:Bezier
40
- */
41
- function bezier(v: Point[], t: number): Point {
42
- const u = 1.0 - t;
43
- const x = [v[0].x, v[1].x, v[2].x, v[3].x];
44
- const y = [v[0].y, v[1].y, v[2].y, v[3].y];
45
- for (let i = 1; i <= 3; i++) {
46
- for (let j = 0; j <= 3 - i; j++) {
47
- x[j] = fma(x[j], u, t * x[j + 1]);
48
- y[j] = fma(y[j], u, t * y[j + 1]);
49
- }
50
- }
51
- return { x: x[0], y: y[0] };
52
- }
53
-
54
34
  /** @see lib/common/utils.c:dotneato_closest */
55
35
  export function dotneatoClosest(spl: SplLike, pt: Point): Point {
56
36
  let besti = 0; let bestj = -1; let bestd2 = Number.MAX_VALUE;
@@ -76,7 +56,7 @@ export function dotneatoClosest(spl: SplLike, pt: Point): Point {
76
56
  let pt2: Point;
77
57
  for (;;) {
78
58
  const t = (lo + hi) / 2.0;
79
- pt2 = bezier(c, t);
59
+ pt2 = bezier(c, t, null, null);
80
60
  if (Math.abs(dlo2 - dhi2) < 1.0 || Math.abs(hi - lo) < 0.00001) break;
81
61
  if (dlo2 < dhi2) { hi = t; dhi2 = dist2(pt2, pt); }
82
62
  else { lo = t; dlo2 = dist2(pt2, pt); }