@knowvah/dot-engine 2.6.6 → 2.6.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/common/htmltable-attr-warn.d.ts +10 -4
  2. package/dist/common/htmltable-attr-warn.d.ts.map +1 -1
  3. package/dist/common/htmltable-label.d.ts +37 -0
  4. package/dist/common/htmltable-label.d.ts.map +1 -0
  5. package/dist/common/htmltable-lex-attvalue.d.ts +59 -0
  6. package/dist/common/htmltable-lex-attvalue.d.ts.map +1 -0
  7. package/dist/common/htmltable-lex-chardata.d.ts +37 -0
  8. package/dist/common/htmltable-lex-chardata.d.ts.map +1 -0
  9. package/dist/common/htmltable-lex-expat.d.ts +26 -13
  10. package/dist/common/htmltable-lex-expat.d.ts.map +1 -1
  11. package/dist/common/htmltable-lex.d.ts.map +1 -1
  12. package/dist/common/htmltable-parse-attrs.d.ts +3 -1
  13. package/dist/common/htmltable-parse-attrs.d.ts.map +1 -1
  14. package/dist/common/htmltable-parse.d.ts +6 -5
  15. package/dist/common/htmltable-parse.d.ts.map +1 -1
  16. package/dist/common/htmltable-pos.d.ts +3 -26
  17. package/dist/common/htmltable-pos.d.ts.map +1 -1
  18. package/dist/common/htmltable-types.d.ts +5 -1
  19. package/dist/common/htmltable-types.d.ts.map +1 -1
  20. package/dist/common/htmltable.d.ts +3 -2
  21. package/dist/common/htmltable.d.ts.map +1 -1
  22. package/dist/common/make-label.d.ts.map +1 -1
  23. package/dist/index.js +310 -144
  24. package/dist/index.js.map +4 -4
  25. package/dist/render.js +310 -144
  26. package/dist/render.js.map +4 -4
  27. package/package.json +1 -1
  28. package/src/common/htmltable-attr-warn.ts +24 -7
  29. package/src/common/htmltable-label.ts +109 -0
  30. package/src/common/htmltable-lex-attvalue.ts +141 -0
  31. package/src/common/htmltable-lex-chardata.ts +95 -0
  32. package/src/common/htmltable-lex-expat.ts +143 -48
  33. package/src/common/htmltable-lex.ts +41 -36
  34. package/src/common/htmltable-parse-attrs.ts +10 -13
  35. package/src/common/htmltable-parse.ts +46 -61
  36. package/src/common/htmltable-pos.ts +5 -88
  37. package/src/common/htmltable-types.ts +6 -1
  38. package/src/common/htmltable.ts +16 -16
  39. package/src/common/make-label.ts +8 -3
@@ -44,12 +44,11 @@ const MSG_TEXT_BEFORE_TABLE = 'Syntax error: non-space string used before <TABLE
44
44
  /** @see lib/common/htmlparse.y:298 (table rule) */
45
45
  const MSG_TEXT_AFTER_TABLE = 'Syntax error: non-space string used after </TABLE>';
46
46
 
47
- /**
48
- * C's `nonSpace` on the lexer's text, which has already dropped control
49
- * characters; the port's tokens still carry tab, LF and CR.
50
- * @see lib/common/htmlparse.y:nonSpace
51
- */
52
- const nonSpace = (text: string): boolean => /[^ \t\n\r]/.test(text);
47
+ /** Any character other than a space (the lexer already dropped control characters). @see lib/common/htmlparse.y:nonSpace */
48
+ const nonSpace = (text: string): boolean => /[^ ]/.test(text);
49
+
50
+ /** Text in `ctx->xb` that no token carried, which the table rule's nonSpace also reads. */
51
+ const heldNonSpace = (tbl: Token | undefined): boolean => tbl?.type === 'open' && nonSpace(tbl.heldText ?? '');
53
52
 
54
53
  /**
55
54
  * The parse failure C reports through `htmlerror` when the lexer last returned
@@ -93,42 +92,40 @@ const applyBrTag = (
93
92
  consumeClose(s, 'BR');
94
93
  };
95
94
 
96
- const applyFontTag = (
97
- s: ParseState, t: OpenToken, fontStack: FontState[], items: HtmlTextItem[],
98
- ): void => {
99
- consume(s);
100
- const font = fontStack[fontStack.length - 1] ?? {};
101
- fontStack.push(mergeFontState(font, {
102
- fontColor: t.attrs['color'],
103
- fontFace: t.attrs['face'],
104
- fontSize: num(t.attrs['point-size']),
105
- }));
95
+ /**
96
+ * `font text n_font` and the other font-style rules: `text` is one or more
97
+ * text items, and the matching close tag must follow it. Anything else, an
98
+ * empty element included, is a syntax error at the token found instead.
99
+ * @see lib/common/htmlparse.y:textitem
100
+ */
101
+ const parseStyledText = (s: ParseState, tag: string, fontStack: FontState[], items: HtmlTextItem[]): void => {
106
102
  const inner = parseText(s, fontStack);
107
103
  fontStack.pop();
108
- items.push(...inner.items);
109
104
  const nxt = peek(s);
110
- if (nxt?.type === 'close' && nxt.tag === 'FONT') consume(s);
105
+ if (inner.items.length === 0 || nxt?.type !== 'close' || nxt.tag !== tag) {
106
+ throw parseError(s, nxt, MSG_SYNTAX_ERROR);
107
+ }
108
+ items.push(...inner.items);
109
+ consume(s);
111
110
  };
112
111
 
113
- const applyInlineTag = (
112
+ /** `font text n_font` / `bold text n_bold` / ...: push the tag's font, parse its text, pop. */
113
+ const applyStyleTag = (
114
114
  s: ParseState, t: OpenToken, fontStack: FontState[], items: HtmlTextItem[],
115
115
  ): void => {
116
116
  consume(s);
117
- const font = fontStack[fontStack.length - 1] ?? {};
118
- fontStack.push(mergeFontState(font, inlineFontState(t.tag, t.attrs)));
119
- const inner = parseText(s, fontStack);
120
- fontStack.pop();
121
- items.push(...inner.items);
122
- const nxt = peek(s);
123
- if (nxt?.type === 'close' && nxt.tag === t.tag) consume(s);
117
+ const own = t.tag === 'FONT'
118
+ ? { fontColor: t.attrs['color'], fontFace: t.attrs['face'], fontSize: num(t.attrs['point-size']) }
119
+ : inlineFontState(t.tag, t.attrs);
120
+ fontStack.push(mergeFontState(fontStack[fontStack.length - 1] ?? {}, own));
121
+ parseStyledText(s, t.tag, fontStack, items);
124
122
  };
125
123
 
126
124
  const applyOpenTag = (
127
125
  s: ParseState, t: OpenToken, fontStack: FontState[], items: HtmlTextItem[],
128
126
  ): boolean => {
129
127
  if (t.tag === 'BR') { applyBrTag(s, t, fontStack, items); return true; }
130
- if (t.tag === 'FONT') { applyFontTag(s, t, fontStack, items); return true; }
131
- if (isInlineTag(t.tag)) { applyInlineTag(s, t, fontStack, items); return true; }
128
+ if (t.tag === 'FONT' || isInlineTag(t.tag)) { applyStyleTag(s, t, fontStack, items); return true; }
132
129
  return false;
133
130
  };
134
131
 
@@ -194,7 +191,7 @@ const parseFontTable = (s: ParseState, open: OpenToken, fontStack: FontState[]):
194
191
  fontStack.push(wrapperFont(open, fontStack));
195
192
  const textBefore = skipStrings(s);
196
193
  const tblTok = peek(s);
197
- if (textBefore && tblTok !== undefined) throw parseError(s, tblTok, MSG_TEXT_BEFORE_TABLE);
194
+ if ((textBefore || heldNonSpace(tblTok)) && tblTok !== undefined) throw parseError(s, tblTok, MSG_TEXT_BEFORE_TABLE);
198
195
  const table = parseTable(s, fontStack);
199
196
  const textAfter = skipStrings(s);
200
197
  const close = peek(s);
@@ -222,13 +219,11 @@ const rejectTableAfterTag = (s: ParseState, start: number): void => {
222
219
  // Cell content helpers — mirrors htmlparse.y cell rule
223
220
  // ---------------------------------------------------------------------------
224
221
 
225
- const parseImgContent = (
226
- s: ParseState, t: OpenToken, cellAttrs: Record<string, string>,
227
- ): HtmlImage => {
222
+ const parseImgContent = (s: ParseState, t: OpenToken): HtmlImage => {
228
223
  consume(s);
229
224
  const img: HtmlImage = {
230
225
  kind: 'image',
231
- src: cellAttrs['src'] ?? t.attrs['src'] ?? '',
226
+ src: t.attrs['src'] ?? '',
232
227
  scale: t.attrs['scale'],
233
228
  };
234
229
  consumeClose(s, 'IMG');
@@ -249,7 +244,7 @@ const scanCellTable = (
249
244
  s: ParseState, t: Token, fontStack: FontState[], st: CellScan,
250
245
  ): HtmlCellContent[] => {
251
246
  if (st.kind === 'image' || st.kind === 'table') throw parseError(s, t, MSG_SYNTAX_ERROR);
252
- if (st.textNonSpace) throw parseError(s, t, MSG_TEXT_BEFORE_TABLE);
247
+ if (st.textNonSpace || heldNonSpace(t)) throw parseError(s, t, MSG_TEXT_BEFORE_TABLE);
253
248
  const tbl = parseTable(s, fontStack);
254
249
  st.kind = 'table';
255
250
  return [tbl];
@@ -275,17 +270,15 @@ const scanAfterTable = (s: ParseState, t: Token, st: CellScan): HtmlCellContent[
275
270
  };
276
271
 
277
272
  /** `image` as the cell's only child. */
278
- const scanCellImage = (
279
- s: ParseState, t: OpenToken, cellAttrs: Record<string, string>, st: CellScan,
280
- ): HtmlCellContent[] => {
273
+ const scanCellImage = (s: ParseState, t: OpenToken, st: CellScan): HtmlCellContent[] => {
281
274
  if (st.kind !== 'none') throw parseError(s, t, MSG_SYNTAX_ERROR);
282
275
  st.kind = 'image';
283
- return [parseImgContent(s, t, cellAttrs)];
276
+ return [parseImgContent(s, t)];
284
277
  };
285
278
 
286
279
  /** One step of `cell: T_cell (fonttable | fonttext | image) T_end_cell`. */
287
280
  const scanCellItem = (
288
- s: ParseState, t: Token, cellAttrs: Record<string, string>, fontStack: FontState[], st: CellScan,
281
+ s: ParseState, t: Token, fontStack: FontState[], st: CellScan,
289
282
  ): HtmlCellContent[] => {
290
283
  if (st.kind === 'fonttable') throw parseError(s, t, MSG_SYNTAX_ERROR);
291
284
  if (st.kind === 'table') return scanAfterTable(s, t, st);
@@ -295,7 +288,7 @@ const scanCellItem = (
295
288
  return [parseFontTable(s, wrapper, fontStack)];
296
289
  }
297
290
  if (t.type === 'open' && t.tag === 'TABLE') return scanCellTable(s, t, fontStack, st);
298
- if (t.type === 'open' && t.tag === 'IMG') return scanCellImage(s, t, cellAttrs, st);
291
+ if (t.type === 'open' && t.tag === 'IMG') return scanCellImage(s, t, st);
299
292
  if (st.kind === 'image') throw parseError(s, t, MSG_SYNTAX_ERROR);
300
293
  return scanCellText(s, t, fontStack, st);
301
294
  };
@@ -308,14 +301,12 @@ const endsCell = (s: ParseState, t: Token, st: CellScan): boolean => {
308
301
  throw parseError(s, t, afterText ? MSG_TEXT_AFTER_TABLE : MSG_SYNTAX_ERROR);
309
302
  };
310
303
 
311
- const parseCellContent = (
312
- s: ParseState, cellAttrs: Record<string, string>, fontStack: FontState[],
313
- ): HtmlCellContent[] => {
304
+ const parseCellContent = (s: ParseState, fontStack: FontState[]): HtmlCellContent[] => {
314
305
  const content: HtmlCellContent[] = [];
315
306
  const st: CellScan = { kind: 'none', textNonSpace: false, afterNonSpace: false };
316
307
  for (let t = peek(s); t !== undefined; t = peek(s)) {
317
308
  if (endsCell(s, t, st)) break;
318
- content.push(...scanCellItem(s, t, cellAttrs, fontStack, st));
309
+ content.push(...scanCellItem(s, t, fontStack, st));
319
310
  }
320
311
  const nxt = peek(s);
321
312
  if (nxt?.type === 'close' && nxt.tag === 'TD') consume(s);
@@ -330,7 +321,7 @@ const parseCellContent = (
330
321
  /** @see lib/common/htmlparse.y:cell rule / lib/common/htmllex.c:mkCell */
331
322
  const parseCell = (
332
323
  s: ParseState, attrs: Record<string, string>, fontStack: FontState[],
333
- ): HtmlCell => buildCell(parseCellContent(s, attrs, fontStack), attrs);
324
+ ): HtmlCell => buildCell(parseCellContent(s, fontStack), attrs);
334
325
 
335
326
  // ---------------------------------------------------------------------------
336
327
  // parseRow — parses <TR>...</TR>
@@ -479,7 +470,7 @@ const consumeRowHr = (s: ParseState, rows: HtmlRow[]): void => {
479
470
  const findFirstRealToken = (tokens: Token[], start: number): Token | undefined => {
480
471
  for (let i = start; i < tokens.length; i++) {
481
472
  const tk = tokens[i];
482
- if (tk.type !== 'text' || tk.value.trim() !== '') return tk;
473
+ if (tk.type !== 'text' || nonSpace(tk.value)) return tk;
483
474
  }
484
475
  return undefined;
485
476
  };
@@ -487,7 +478,7 @@ const findFirstRealToken = (tokens: Token[], start: number): Token | undefined =
487
478
  const skipLeadingWs = (s: ParseState): void => {
488
479
  while (true) {
489
480
  const nxt = peek(s);
490
- if (!nxt || nxt.type !== 'text' || nxt.value.trim() !== '') break;
481
+ if (!nxt || nxt.type !== 'text' || nonSpace(nxt.value)) break;
491
482
  consume(s);
492
483
  }
493
484
  };
@@ -518,6 +509,7 @@ const parseFontTableLabel = (s: ParseState, open: OpenToken, fontStack: FontStat
518
509
 
519
510
  const parseTableLabel = (s: ParseState, fontStack: FontState[]): HtmlLabel => {
520
511
  skipLeadingWs(s);
512
+ if (heldNonSpace(peek(s))) throw parseError(s, peek(s), MSG_TEXT_BEFORE_TABLE);
521
513
  const table = parseTable(s, fontStack);
522
514
  finishTableLabel(s);
523
515
  return { kind: 'table', table };
@@ -562,21 +554,16 @@ export interface ParsedHtmlLabel {
562
554
  }
563
555
 
564
556
  /**
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).
557
+ * Parse an HTML-like label string, writing the lexer's warnings to `stderr` as
558
+ * the parser reaches their tokens. The caller owns `stderr` and flushes it: a
559
+ * warning without a newline continues into the caller's next write. Input is
560
+ * the content between the outer `<` `>` delimiters (already stripped).
569
561
  *
570
562
  * @see lib/common/htmlparse.y:parseHTML
571
563
  */
572
- export const parseHtmlLabelRv = (
573
- src: string,
574
- writeLine: (line: string) => void = (line) => console.warn(line),
575
- ): ParsedHtmlLabel => {
564
+ export const parseHtmlLabelRv = (src: string, stderr: StderrLines): ParsedHtmlLabel => {
576
565
  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
- };
566
+ const s: ParseState = { tokens, pos: 0, pending: error, stderr, emitted: new WeakSet(), warn: false, eof };
580
567
  try {
581
568
  const label = parseTokens(s);
582
569
  if (error !== undefined) throw error;
@@ -585,8 +572,6 @@ export const parseHtmlLabelRv = (
585
572
  // Running off the truncated stream means C's lexer would have hit the error.
586
573
  if (error !== undefined && s.pos >= tokens.length) { emitEof(s); throw error; }
587
574
  throw e;
588
- } finally {
589
- s.stderr?.flush();
590
575
  }
591
576
  };
592
577
 
@@ -596,4 +581,4 @@ export const parseHtmlLabelRv = (
596
581
  * the sync pass prints.
597
582
  */
598
583
  export const parseHtmlLabel = (src: string): HtmlLabel =>
599
- parseHtmlLabelRv(src, () => undefined).label;
584
+ parseHtmlLabelRv(src, new StderrLines(() => undefined)).label;
@@ -16,14 +16,11 @@
16
16
  */
17
17
 
18
18
  import type { Box } from '../model/geom.js';
19
- import type { TextlabelT } from './types.js';
20
19
  import type { TextMeasurer } from './textmeasure.js';
21
- import type { HtmlCell, HtmlImage, HtmlLabel, HtmlTable, HtmlText, ImageSizer } from './htmltable-types.js';
20
+ import type { HtmlCell, HtmlImage, HtmlLabel, HtmlTable, HtmlText } from './htmltable-types.js';
22
21
  import {
23
- layoutHtmlTable, cellPad, cellBorder, sizeHtmlLabel, parseHtmlLabelRv, inheritTablePenColor, withTableFont,
22
+ layoutHtmlTable, cellPad, cellBorder, withTableFont,
24
23
  } from './htmltable.js';
25
- import { makeLabel } from './make-label.js';
26
- import { HtmlLexError } from './htmltable-lex.js';
27
24
  import { TOP, BOTTOM, LEFT, RIGHT } from './splines-constants.js';
28
25
  import type { PlacedLine, HtmlFontInfo } from './htmltable-pos-runs.js';
29
26
  import {
@@ -171,15 +168,14 @@ export interface PlacedHtml {
171
168
  * of pos_html_cell: LEFT flushes the box right edge in (`ur.x -= delx`), RIGHT
172
169
  * flushes the left edge in, default splits (center); VALIGN BOTTOM/TOP flush,
173
170
  * default centers on BOTH axes. Parallels the existing `alignImageBox`, but
174
- * text default-centers vertically where an image does not.
175
- * (`parseAlign` never yields HALIGN_TEXT, so the cell box always shrinks here —
176
- * exactly what C does for every non-TEXT cell.)
171
+ * text default-centers vertically where an image does not. ALIGN="TEXT" keeps
172
+ * the box's width: lines then align across the whole cell.
177
173
  * @see lib/common/htmltable.c:pos_html_cell (text branch, :1487-1526)
178
174
  */
179
175
  export function alignContentBox(box: Box, cell: HtmlCell, w: number, h: number): Box {
180
176
  const out: Box = { ll: { ...box.ll }, ur: { ...box.ur } };
181
177
  const delx = out.ur.x - out.ll.x - w;
182
- if (delx > 0) {
178
+ if (delx > 0 && cell.align !== 'text') {
183
179
  if (cell.align === 'left') out.ur.x -= delx;
184
180
  else if (cell.align === 'right') out.ll.x += delx;
185
181
  else { out.ll.x += delx / 2; out.ur.x -= delx / 2; }
@@ -520,82 +516,3 @@ export function posHtmlLabel(
520
516
  if (label.kind === 'table') return withTableFont(env, label.table.font, 'emit', () => posHtmlTable(label.table, env, measurer));
521
517
  return posTextLabel(label, finfo, measurer);
522
518
  }
523
-
524
- /** parseHTML's rv for a label that failed to parse (`ctx->error`). */
525
- const HTML_PARSE_FATAL = 3;
526
-
527
- /** A built HTML label plus make_html_label's "problem" return value. */
528
- export interface HtmlLabelResult {
529
- label: TextlabelT;
530
- rv: number;
531
- }
532
-
533
- /**
534
- * make_html_label: parse, size and position an HTML-like label. Returns C's
535
- * rv too (nonzero: the caller appends `in label of ...`, labels.c:make_label).
536
- * Parse diagnostics print here, once per label, as parseHTML does. The
537
- * optional `imageSizer` resolves `<IMG SRC>`; absent means C's missing image.
538
- * @see lib/common/htmltable.c:make_html_label
539
- */
540
- export function makeHtmlLabelRv(
541
- content: string,
542
- finfo: HtmlFontInfo & { imageSizer?: ImageSizer; pencolor?: string },
543
- measurer: TextMeasurer,
544
- ): HtmlLabelResult {
545
- const { fontname, fontsize, fontcolor, imageSizer, pencolor } = finfo;
546
- let lbl: HtmlLabel, lexWarn: boolean; // lexWarn: htmllex.c's ctx->warn, rv bit 1
547
- try {
548
- ({ label: lbl, warn: lexWarn } = parseHtmlLabelRv(content));
549
- } catch (e) {
550
- // C aborts the HTML parse on a syntax error (YYABORT): parseHTML returns
551
- // rv 3 (ctx->error) with no label, and make_html_label returns it with
552
- // lp->html cleared and no spans, so the label renders empty.
553
- // @see lib/common/htmltable.c:1880 (rv == 3), htmlparse.y (YYABORT)
554
- if (e instanceof HtmlLexError) for (const line of e.diagnostics) console.error(line);
555
- return { label: makeLabel('', fontname, fontsize, fontcolor, measurer), rv: HTML_PARSE_FATAL };
556
- }
557
- // A top-level table with no explicit COLOR inherits the object's pen color,
558
- // which then flows table->cell->nested-table.
559
- // @see lib/common/htmltable.c:1911 (getPenColor), :1406/:1556 (inheritance)
560
- if (lbl.kind === 'table') inheritTablePenColor(lbl.table, pencolor);
561
- const rv = Number(lexWarn) | sizeHtmlLabel(lbl, measurer, {
562
- fontsize, fontname, imageSizer,
563
- });
564
- const placed = posHtmlLabel(lbl, { fontname, fontsize, fontcolor }, measurer);
565
- const dim = lbl.dimen ?? { w: 0, h: 0 };
566
- // C replaces table-label text: "this may be used for the title and
567
- // alt fields in image maps" — anchors inherit it as default tooltip.
568
- // @see lib/common/htmltable.c:make_html_label (line 1935)
569
- const text = lbl.kind === 'table' ? '<TABLE>' : content;
570
- return { label: htmlTextLabel(text, { fontname, fontsize, fontcolor }, placed, dim), rv };
571
- }
572
-
573
- /** {@link makeHtmlLabelRv} without the problem flag. */
574
- export function makeHtmlLabel(
575
- content: string,
576
- finfo: HtmlFontInfo & { imageSizer?: ImageSizer; pencolor?: string },
577
- measurer: TextMeasurer,
578
- ): TextlabelT {
579
- return makeHtmlLabelRv(content, finfo, measurer).label;
580
- }
581
-
582
- /** Build the TextlabelT wrapper around a positioned HTML label. */
583
- function htmlTextLabel(
584
- text: string,
585
- finfo: HtmlFontInfo,
586
- placed: PlacedHtml,
587
- dim: { w: number; h: number },
588
- ): TextlabelT {
589
- return {
590
- text,
591
- fontname: finfo.fontname, fontcolor: finfo.fontcolor, charset: 0,
592
- fontsize: finfo.fontsize,
593
- dimen: { x: dim.w, y: dim.h },
594
- space: { x: dim.w, y: dim.h },
595
- pos: { x: 0, y: 0 },
596
- u: { kind: 'html', html: placed },
597
- valign: 'c'.charCodeAt(0),
598
- set: false,
599
- html: true,
600
- };
601
- }
@@ -40,6 +40,9 @@ export class HtmlParseError extends DotEngineError {
40
40
  /** @see lib/common/htmltable.h HALIGN_* flags */
41
41
  export type HtmlAlign = 'left' | 'center' | 'right';
42
42
 
43
+ /** A cell's ALIGN: TEXT (HALIGN_TEXT) leaves the text block the cell's width. @see lib/common/htmllex.c:cell_halignfn */
44
+ export type HtmlCellAlign = HtmlAlign | 'text';
45
+
43
46
  /** @see lib/common/htmltable.h VALIGN_* flags */
44
47
  export type HtmlVAlign = 'top' | 'middle' | 'bottom';
45
48
 
@@ -132,7 +135,7 @@ export interface HtmlCell {
132
135
  */
133
136
  vruled?: boolean;
134
137
  port?: string;
135
- align?: HtmlAlign;
138
+ align?: HtmlCellAlign;
136
139
  valign?: HtmlVAlign;
137
140
  balign?: HtmlAlign;
138
141
  bgcolor?: string;
@@ -275,6 +278,8 @@ export interface OpenToken extends LexerOutput {
275
278
  type: 'open';
276
279
  tag: string;
277
280
  attrs: Record<string, string>;
281
+ /** On a TABLE: `ctx->xb` text no token carried, read by the table rule's nonSpace. */
282
+ heldText?: string;
278
283
  }
279
284
 
280
285
  /** @see lib/common/htmllex.c:endElement */
@@ -20,13 +20,13 @@ export type { TextMeasurer } from './textmeasure.js';
20
20
 
21
21
  import type {
22
22
  HtmlCell, HtmlCellContent, HtmlImage, HtmlLabel, HtmlTable, HtmlText,
23
- HtmlTextItem,
23
+ HtmlTextItem, ImageSizer,
24
24
  } from './htmltable-types.js';
25
- import type { ImageSizer } from './htmltable-types.js';
26
25
  import { findImageSize } from '../gvc/usershape.js';
27
26
  import { PointSet } from './pointset.js';
28
27
  import type { TextMeasurer } from './textmeasure.js';
29
28
  import { withTableFont } from './htmltable-fontinfo.js';
29
+ import { agerrLine, type StderrLines } from './htmltable-attr-warn.js';
30
30
 
31
31
  export { withTableFont } from './htmltable-fontinfo.js';
32
32
 
@@ -85,6 +85,7 @@ export interface HtmlSizeEnv {
85
85
  fontsize: number;
86
86
  fontname: string;
87
87
  imageSizer?: ImageSizer;
88
+ stderr?: StderrLines; // the label's stderr; none: straight to the console
88
89
  }
89
90
 
90
91
  const defaultSizeEnv = (): HtmlSizeEnv => ({ fontsize: DEFAULT_FONTSIZE, fontname: '' });
@@ -230,21 +231,20 @@ const sizeTextContent = (
230
231
  /** Images whose sizing failed: size_html_img's rv = 1 on every later call. */
231
232
  const missingImages = new WeakSet<HtmlImage>();
232
233
 
233
- const sizeHtmlImg = (img: HtmlImage, imageSizer?: ImageSizer): number => {
234
+ const sizeHtmlImg = (img: HtmlImage, { imageSizer, stderr }: HtmlSizeEnv): number => {
234
235
  // C sizes each image once (processTbl); the port's size and pos passes
235
236
  // share layoutHtmlTable, so guard against re-sizing (and re-warning). The
236
237
  // returned rv still follows C on every call.
237
238
  if (img.width !== undefined && img.height !== undefined) return missingImages.has(img) ? 1 : 0;
238
239
  const sz = imageSizer !== undefined ? imageSizer(img.src) : findImageSize(img.src);
239
240
  if (sz !== null) {
240
- img.width = sz.w;
241
- img.height = sz.h;
241
+ img.width = sz.w; img.height = sz.h;
242
242
  return 0;
243
243
  }
244
244
  // Missing-image: zero size + warning.
245
245
  // C: agerrorf("No or improper image file=\"%s\"\n", img->src) — agerr
246
246
  // prefixes "Error: " on stderr.
247
- console.error(`Error: No or improper image file="${img.src}"`);
247
+ agerrLine(stderr, `Error: No or improper image file="${img.src}"`);
248
248
  img.width = 0;
249
249
  img.height = 0;
250
250
  missingImages.add(img);
@@ -262,7 +262,7 @@ const sizeContentItem = (
262
262
  return { ...(item.dimen !== undefined ? item.dimen : { w: 0, h: 0 }), rv };
263
263
  }
264
264
  if (item.kind === 'image') {
265
- const rv = sizeHtmlImg(item, env.imageSizer);
265
+ const rv = sizeHtmlImg(item, env);
266
266
  return { w: item.width ?? 0, h: item.height ?? 0, rv };
267
267
  }
268
268
  return { w: 0, h: 0, rv: 0 };
@@ -327,15 +327,15 @@ const sizeWarned = new WeakSet<object>();
327
327
  * @see lib/common/htmltable.c:1141 (size_html_cell FIXED_FLAG)
328
328
  */
329
329
  const fixedCellCheck = (
330
- cell: HtmlCell, sz: { w: number; h: number }, warn: boolean,
330
+ cell: HtmlCell, sz: { w: number; h: number }, warn: boolean, out: StderrLines | undefined,
331
331
  ): { rv: number; zero: boolean } => {
332
332
  if (!cell.fixedsize) return { rv: 0, zero: false };
333
333
  if (cell.width && cell.height) {
334
334
  const small = (cell.width < sz.w || cell.height < sz.h) && cell.content[0]?.kind !== 'image';
335
- if (small && warn) console.warn(WARN_CELL_TOO_SMALL);
335
+ if (small && warn) agerrLine(out, WARN_CELL_TOO_SMALL);
336
336
  return { rv: small ? 1 : 0, zero: true };
337
337
  }
338
- if (warn) console.warn(WARN_CELL_UNSPECIFIED);
338
+ if (warn) agerrLine(out, WARN_CELL_UNSPECIFIED);
339
339
  return { rv: 1, zero: false };
340
340
  };
341
341
 
@@ -360,7 +360,7 @@ const getCellSize = (
360
360
  let szy = ch + margin;
361
361
  const warn = !sizeWarned.has(cell);
362
362
  sizeWarned.add(cell);
363
- const fixed = fixedCellCheck(cell, { w: szx, h: szy }, warn);
363
+ const fixed = fixedCellCheck(cell, { w: szx, h: szy }, warn, env.stderr);
364
364
  rv |= fixed.rv;
365
365
  if (fixed.zero) { szx = 0; szy = 0; }
366
366
  const bw = cell.width !== undefined ? cell.width : 0;
@@ -504,16 +504,16 @@ export const layoutHtmlTable = (
504
504
  * table, the returned `rv = 1` follows C on every call.
505
505
  * @see lib/common/htmltable.c:size_html_tbl (:1683, :1689)
506
506
  */
507
- const warnFixedTable = (tbl: HtmlTable, dims: { wd: number; ht: number }): number => {
507
+ const warnFixedTable = (tbl: HtmlTable, dims: { wd: number; ht: number }, out: StderrLines | undefined): number => {
508
508
  const warn = !sizeWarned.has(tbl);
509
509
  sizeWarned.add(tbl);
510
510
  if (tbl.fixedsize !== true) return 0;
511
511
  if (tbl.width && tbl.height) {
512
512
  if (tbl.width >= dims.wd && tbl.height >= dims.ht) return 0;
513
- if (warn) console.warn(WARN_TABLE_TOO_SMALL);
513
+ if (warn) agerrLine(out, WARN_TABLE_TOO_SMALL);
514
514
  return 1;
515
515
  }
516
- if (warn) console.warn(WARN_TABLE_UNSPECIFIED);
516
+ if (warn) agerrLine(out, WARN_TABLE_UNSPECIFIED);
517
517
  return 1;
518
518
  };
519
519
 
@@ -525,7 +525,7 @@ const sizeTableBody = (
525
525
  ): number => {
526
526
  const { entries, widths, heights, spacing, border, rv } = layoutHtmlTable(tbl, measurer, env);
527
527
  if (entries.length === 0) {
528
- const fixed = warnFixedTable(tbl, sumTableDims({ widths, heights, ncols: 0, nrows: 0, spacing, border }));
528
+ const fixed = warnFixedTable(tbl, sumTableDims({ widths, heights, ncols: 0, nrows: 0, spacing, border }), env.stderr);
529
529
  tbl.dimen = { w: 0, h: 0 };
530
530
  return rv | fixed;
531
531
  }
@@ -535,7 +535,7 @@ const sizeTableBody = (
535
535
  const { wd, ht } = sumTableDims({ widths, heights, ncols, nrows, spacing, border });
536
536
  const baseWd = tbl.width !== undefined ? tbl.width : 0;
537
537
  const baseHt = tbl.height !== undefined ? tbl.height : 0;
538
- const fixed = warnFixedTable(tbl, { wd, ht });
538
+ const fixed = warnFixedTable(tbl, { wd, ht }, env.stderr);
539
539
  // C zeroes only the CONTENT-derived term for a FIXEDSIZE table that has BOTH
540
540
  // width and height, so the box becomes fmax(0, width)=width / fmax(0,height)=
541
541
  // height (the explicit size). A fixed table missing a dimension warns and
@@ -12,7 +12,8 @@
12
12
  import type { TextlabelT } from './types.js';
13
13
  import type { TextMeasurer, TextSize } from './textmeasure.js';
14
14
  import type { TextSpan } from './emit-types.js';
15
- import { makeHtmlLabelRv } from './htmltable-pos.js';
15
+ import { makeHtmlLabelTo } from './htmltable-label.js';
16
+ import { labelStderr } from './htmltable-attr-warn.js';
16
17
  import { substObj, type GraphObj } from './subst.js';
17
18
  import { Edge } from '../model/edge.js';
18
19
  import { Graph } from '../model/graph.js';
@@ -205,8 +206,12 @@ export function makeAnyLabel(
205
206
  // it (htmltable.c:1911 getPenColor). @see getPenColor
206
207
  // ADR-2: a per-context sizer (when set) wins over the global one.
207
208
  const imageSizer = obj !== undefined ? rootOf(obj).info.gvc?.imageSizer : undefined;
208
- const { label, rv } = makeHtmlLabelRv(content, { ...font, pencolor: getPenColor(obj), imageSizer }, measurer);
209
- if (rv !== 0 && obj !== undefined) console.error(inLabelOf(obj));
209
+ // One stderr for the label: doInt's unterminated warning runs into the
210
+ // next write, `in label of ...` included (agerr AGPREV).
211
+ const stderr = labelStderr();
212
+ const { label, rv } = makeHtmlLabelTo(content, { ...font, pencolor: getPenColor(obj), imageSizer }, measurer, stderr);
213
+ if (rv !== 0 && obj !== undefined) stderr.write(`${inLabelOf(obj)}\n`);
214
+ stderr.flush();
210
215
  return label;
211
216
  }
212
217
  // Plain path: resolve \G \N \E \T \H \L against the owning object