@knowvah/dot-engine 2.6.7 → 2.6.9

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 (45) 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-expat.d.ts +15 -4
  8. package/dist/common/htmltable-lex-expat.d.ts.map +1 -1
  9. package/dist/common/htmltable-lex.d.ts.map +1 -1
  10. package/dist/common/htmltable-parse-attrs.d.ts +3 -1
  11. package/dist/common/htmltable-parse-attrs.d.ts.map +1 -1
  12. package/dist/common/htmltable-parse.d.ts +6 -5
  13. package/dist/common/htmltable-parse.d.ts.map +1 -1
  14. package/dist/common/htmltable-pos.d.ts +3 -26
  15. package/dist/common/htmltable-pos.d.ts.map +1 -1
  16. package/dist/common/htmltable-types.d.ts +3 -1
  17. package/dist/common/htmltable-types.d.ts.map +1 -1
  18. package/dist/common/htmltable.d.ts +3 -2
  19. package/dist/common/htmltable.d.ts.map +1 -1
  20. package/dist/common/make-label.d.ts.map +1 -1
  21. package/dist/common/subst.d.ts +8 -0
  22. package/dist/common/subst.d.ts.map +1 -1
  23. package/dist/gvc/anchor.d.ts.map +1 -1
  24. package/dist/index.js +308 -105
  25. package/dist/index.js.map +4 -4
  26. package/dist/render/xml-escape.d.ts +8 -6
  27. package/dist/render/xml-escape.d.ts.map +1 -1
  28. package/dist/render.js +308 -105
  29. package/dist/render.js.map +4 -4
  30. package/package.json +1 -1
  31. package/src/common/htmltable-attr-warn.ts +24 -7
  32. package/src/common/htmltable-label.ts +109 -0
  33. package/src/common/htmltable-lex-attvalue.ts +141 -0
  34. package/src/common/htmltable-lex-expat.ts +59 -27
  35. package/src/common/htmltable-lex.ts +9 -10
  36. package/src/common/htmltable-parse-attrs.ts +10 -13
  37. package/src/common/htmltable-parse.ts +16 -29
  38. package/src/common/htmltable-pos.ts +5 -88
  39. package/src/common/htmltable-types.ts +4 -1
  40. package/src/common/htmltable.ts +16 -16
  41. package/src/common/make-label.ts +8 -3
  42. package/src/common/poly-gencode.ts +2 -2
  43. package/src/common/subst.ts +12 -0
  44. package/src/gvc/anchor.ts +6 -5
  45. package/src/render/xml-escape.ts +23 -33
@@ -219,13 +219,11 @@ const rejectTableAfterTag = (s: ParseState, start: number): void => {
219
219
  // Cell content helpers — mirrors htmlparse.y cell rule
220
220
  // ---------------------------------------------------------------------------
221
221
 
222
- const parseImgContent = (
223
- s: ParseState, t: OpenToken, cellAttrs: Record<string, string>,
224
- ): HtmlImage => {
222
+ const parseImgContent = (s: ParseState, t: OpenToken): HtmlImage => {
225
223
  consume(s);
226
224
  const img: HtmlImage = {
227
225
  kind: 'image',
228
- src: cellAttrs['src'] ?? t.attrs['src'] ?? '',
226
+ src: t.attrs['src'] ?? '',
229
227
  scale: t.attrs['scale'],
230
228
  };
231
229
  consumeClose(s, 'IMG');
@@ -272,17 +270,15 @@ const scanAfterTable = (s: ParseState, t: Token, st: CellScan): HtmlCellContent[
272
270
  };
273
271
 
274
272
  /** `image` as the cell's only child. */
275
- const scanCellImage = (
276
- s: ParseState, t: OpenToken, cellAttrs: Record<string, string>, st: CellScan,
277
- ): HtmlCellContent[] => {
273
+ const scanCellImage = (s: ParseState, t: OpenToken, st: CellScan): HtmlCellContent[] => {
278
274
  if (st.kind !== 'none') throw parseError(s, t, MSG_SYNTAX_ERROR);
279
275
  st.kind = 'image';
280
- return [parseImgContent(s, t, cellAttrs)];
276
+ return [parseImgContent(s, t)];
281
277
  };
282
278
 
283
279
  /** One step of `cell: T_cell (fonttable | fonttext | image) T_end_cell`. */
284
280
  const scanCellItem = (
285
- s: ParseState, t: Token, cellAttrs: Record<string, string>, fontStack: FontState[], st: CellScan,
281
+ s: ParseState, t: Token, fontStack: FontState[], st: CellScan,
286
282
  ): HtmlCellContent[] => {
287
283
  if (st.kind === 'fonttable') throw parseError(s, t, MSG_SYNTAX_ERROR);
288
284
  if (st.kind === 'table') return scanAfterTable(s, t, st);
@@ -292,7 +288,7 @@ const scanCellItem = (
292
288
  return [parseFontTable(s, wrapper, fontStack)];
293
289
  }
294
290
  if (t.type === 'open' && t.tag === 'TABLE') return scanCellTable(s, t, fontStack, st);
295
- 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);
296
292
  if (st.kind === 'image') throw parseError(s, t, MSG_SYNTAX_ERROR);
297
293
  return scanCellText(s, t, fontStack, st);
298
294
  };
@@ -305,14 +301,12 @@ const endsCell = (s: ParseState, t: Token, st: CellScan): boolean => {
305
301
  throw parseError(s, t, afterText ? MSG_TEXT_AFTER_TABLE : MSG_SYNTAX_ERROR);
306
302
  };
307
303
 
308
- const parseCellContent = (
309
- s: ParseState, cellAttrs: Record<string, string>, fontStack: FontState[],
310
- ): HtmlCellContent[] => {
304
+ const parseCellContent = (s: ParseState, fontStack: FontState[]): HtmlCellContent[] => {
311
305
  const content: HtmlCellContent[] = [];
312
306
  const st: CellScan = { kind: 'none', textNonSpace: false, afterNonSpace: false };
313
307
  for (let t = peek(s); t !== undefined; t = peek(s)) {
314
308
  if (endsCell(s, t, st)) break;
315
- content.push(...scanCellItem(s, t, cellAttrs, fontStack, st));
309
+ content.push(...scanCellItem(s, t, fontStack, st));
316
310
  }
317
311
  const nxt = peek(s);
318
312
  if (nxt?.type === 'close' && nxt.tag === 'TD') consume(s);
@@ -327,7 +321,7 @@ const parseCellContent = (
327
321
  /** @see lib/common/htmlparse.y:cell rule / lib/common/htmllex.c:mkCell */
328
322
  const parseCell = (
329
323
  s: ParseState, attrs: Record<string, string>, fontStack: FontState[],
330
- ): HtmlCell => buildCell(parseCellContent(s, attrs, fontStack), attrs);
324
+ ): HtmlCell => buildCell(parseCellContent(s, fontStack), attrs);
331
325
 
332
326
  // ---------------------------------------------------------------------------
333
327
  // parseRow — parses <TR>...</TR>
@@ -560,21 +554,16 @@ export interface ParsedHtmlLabel {
560
554
  }
561
555
 
562
556
  /**
563
- * Parse an HTML-like label string, printing the lexer's warnings (one
564
- * `writeLine` per stderr line, `console.warn` by default) as the parser reaches
565
- * their tokens. Input is the content between the outer `<` `>` delimiters
566
- * (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).
567
561
  *
568
562
  * @see lib/common/htmlparse.y:parseHTML
569
563
  */
570
- export const parseHtmlLabelRv = (
571
- src: string,
572
- writeLine: (line: string) => void = (line) => console.warn(line),
573
- ): ParsedHtmlLabel => {
564
+ export const parseHtmlLabelRv = (src: string, stderr: StderrLines): ParsedHtmlLabel => {
574
565
  const { tokens, error, eof } = tokenizeDeferred(`<HTML>${src}</HTML>`, { start: 6, end: 6 + src.length });
575
- const s: ParseState = {
576
- tokens, pos: 0, pending: error, stderr: new StderrLines(writeLine), emitted: new WeakSet(), warn: false, eof,
577
- };
566
+ const s: ParseState = { tokens, pos: 0, pending: error, stderr, emitted: new WeakSet(), warn: false, eof };
578
567
  try {
579
568
  const label = parseTokens(s);
580
569
  if (error !== undefined) throw error;
@@ -583,8 +572,6 @@ export const parseHtmlLabelRv = (
583
572
  // Running off the truncated stream means C's lexer would have hit the error.
584
573
  if (error !== undefined && s.pos >= tokens.length) { emitEof(s); throw error; }
585
574
  throw e;
586
- } finally {
587
- s.stderr?.flush();
588
575
  }
589
576
  };
590
577
 
@@ -594,4 +581,4 @@ export const parseHtmlLabelRv = (
594
581
  * the sync pass prints.
595
582
  */
596
583
  export const parseHtmlLabel = (src: string): HtmlLabel =>
597
- 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;
@@ -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
@@ -22,7 +22,7 @@ import { FillType } from '../gvc/context.js';
22
22
  import { emitHtmlLabel } from './htmltable-emit.js';
23
23
  import { transformPoint } from '../gvc/device.js';
24
24
  import { nodeAttr } from './poly-init.js';
25
- import { substObjAnchor, interpretCRNL } from './subst.js';
25
+ import { substObjAnchor, preprocessTooltip } from './subst.js';
26
26
  import type { ResolvedFill, PolyStyleFlags } from './style-resolve.js';
27
27
  import {
28
28
  parseStyleFlags,
@@ -245,7 +245,7 @@ function anchorAttr(n: Node, key: string): string | undefined {
245
245
 
246
246
  /** Resolve tooltip string: explicit attr, or fall back to label text. */
247
247
  function resolveTooltip(n: Node, tooltip: string | undefined): string {
248
- if (tooltip !== undefined) return substObjAnchor(interpretCRNL(tooltip), n);
248
+ if (tooltip !== undefined) return substObjAnchor(preprocessTooltip(tooltip), n);
249
249
  const label = n.info.label as TextlabelT | undefined;
250
250
  return label?.text ?? '';
251
251
  }
@@ -14,6 +14,7 @@ import { Node } from '../model/node.js';
14
14
  import { Edge } from '../model/edge.js';
15
15
  import { Graph } from '../model/graph.js';
16
16
  import type { TextlabelT } from './types.js';
17
+ import { htmlEntityUTF8 } from './html-entities.js';
17
18
 
18
19
  export type GraphObj = Node | Edge | Graph;
19
20
 
@@ -124,3 +125,14 @@ export function interpretCRNL(str: string): string {
124
125
  }
125
126
  return out;
126
127
  }
128
+
129
+ /**
130
+ * Tooltip preprocessing: decode HTML entities to UTF-8, then interpret
131
+ * \n \l \r. C picks latin1ToUTF8 or htmlEntityUTF8 by GD_charset; both decode
132
+ * entities via htmlEntity, and the port's Latin-1 bytes are already decoded
133
+ * at input, so one branch serves both (as in make_label).
134
+ * @see lib/common/emit.c:preprocessTooltip
135
+ */
136
+ export function preprocessTooltip(s: string): string {
137
+ return interpretCRNL(htmlEntityUTF8(s));
138
+ }
package/src/gvc/anchor.ts CHANGED
@@ -17,7 +17,7 @@ import type { Node } from '../model/node.js';
17
17
  import type { Point, Box, Spline, Bezier } from '../model/geom.js';
18
18
  import type { TextlabelT, PolygonT } from '../common/types.js';
19
19
  import type { GraphObj } from '../common/subst.js';
20
- import { substObjAnchor, interpretCRNL } from '../common/subst.js';
20
+ import { substObjAnchor, preprocessTooltip } from '../common/subst.js';
21
21
  import { parseStyleFlags } from '../common/style-resolve.js';
22
22
  import { nodeAttr } from '../common/poly-init.js';
23
23
  import { bezier } from '../common/bezier.js';
@@ -36,12 +36,13 @@ function subst(s: string, obj: GraphObj): string {
36
36
  }
37
37
 
38
38
  /**
39
- * preprocessTooltip + strdup_and_subst_obj: interpret \n\l\r escapes, then
40
- * object substitution — matching the node tooltip path (poly-gencode.ts).
39
+ * preprocessTooltip + strdup_and_subst_obj: decode entities and interpret
40
+ * \n\l\r escapes, then object substitution — matching the node tooltip path
41
+ * (poly-gencode.ts).
41
42
  * @see lib/common/emit.c:preprocessTooltip
42
43
  */
43
44
  function tip(s: string, obj: GraphObj): string {
44
- return substObjAnchor(interpretCRNL(s), obj);
45
+ return substObjAnchor(preprocessTooltip(s), obj);
45
46
  }
46
47
 
47
48
  /** Label text of a textlabel, or null. @see device.ts:labelTextOf */
@@ -71,7 +72,7 @@ function pickEdge(e: Edge, keys: string[]): Picked {
71
72
  return pickFirst(e.attrs, keys, (s) => subst(s, e));
72
73
  }
73
74
 
74
- /** pickFirst with tooltip preprocessing (interpretCRNL + subst). */
75
+ /** pickFirst with tooltip preprocessing (preprocessTooltip + subst). */
75
76
  function pickEdgeTip(e: Edge, keys: string[]): Picked {
76
77
  return pickFirst(e.attrs, keys, (s) => tip(s, e));
77
78
  }
@@ -1,29 +1,25 @@
1
1
  // SPDX-License-Identifier: EPL-2.0
2
2
  //
3
- // XML escaping faithful to graphviz's gv_xml_escape (lib/util/xml.c). '<', '>',
4
- // '"', and "'" are always escaped; '&' is escaped to &amp; only when it does
5
- // not begin a valid entity (&name; / &#NNN; / &#xHH;). The text/tooltip variant
6
- // additionally honors the {raw, dash, nbsp} flags used by the SVG plugin.
3
+ // The SVG renderer's three gv_xml_escape flag sets (lib/util/xml.c), as presets
4
+ // over the faithful port in util/xml.ts. '<', '>', '"' and "'" are always
5
+ // escaped; '&' is escaped only when it does not begin an entity
6
+ // (xml_isentity), except under raw, which escapes every '&'.
7
7
 
8
- // Regexes for quote chars: /"/g and /'/g break Lizard's quote-tracker.
9
- // Use these named patterns instead to avoid the parser bug.
10
- const RE_DQUOTE = new RegExp('"', 'g');
11
- const RE_SQUOTE = new RegExp("'", 'g');
12
- // A bare '&' that does NOT begin a valid entity (&name; / &#NNN; / &#xHH;).
13
- // Mirrors xml_isentity (lib/util/xml.c): only such '&' are escaped to &amp;.
14
- const RE_AMP_NONENTITY = /&(?![A-Za-z]+;|#[0-9]+;|#x[0-9A-Fa-f]+;)/g;
8
+ import { gvXmlEscape, type XmlFlags } from '../util/xml.js';
9
+
10
+ /** flags {0}. @see plugin/core/gvrender_core_svg.c:svg_begin_anchor (href) */
11
+ const FLAGS_BASE: XmlFlags = { raw: false, dash: false, nbsp: false, utf8: false };
12
+ /** gvputs_xml's flags. @see lib/gvc/gvdevice.c:gvputs_xml */
13
+ const FLAGS_TITLE: XmlFlags = { raw: false, dash: true, nbsp: true, utf8: false };
14
+ /** @see plugin/core/gvrender_core_svg.c:svg_textspan, svg_begin_anchor (tooltip) */
15
+ const FLAGS_TEXT: XmlFlags = { raw: true, dash: true, nbsp: true, utf8: false };
15
16
 
16
17
  /**
17
- * Base XML escape (gv_xml_escape with flags {0}): ids, titles, hrefs.
18
- * @see lib/util/xml.c gv_xml_escape / xml_core / xml_isentity
18
+ * Base XML escape (gv_xml_escape with flags {0}): ids, hrefs.
19
+ * @see lib/util/xml.c:gv_xml_escape
19
20
  */
20
21
  export function escapeXml(s: string): string {
21
- let r = s.replace(RE_AMP_NONENTITY, '&amp;');
22
- r = r.replace(/</g, '&lt;');
23
- r = r.replace(/>/g, '&gt;');
24
- r = r.replace(RE_DQUOTE, '&quot;');
25
- r = r.replace(RE_SQUOTE, '&#39;');
26
- return r;
22
+ return gvXmlEscape(s, FLAGS_BASE);
27
23
  }
28
24
 
29
25
  /**
@@ -34,23 +30,17 @@ export function escapeXml(s: string): string {
34
30
  * @see lib/gvc/gvdevice.c:gvputs_xml, lib/util/xml.c:xml_core
35
31
  */
36
32
  export function escapeXmlTitle(s: string): string {
37
- let r = escapeXml(s);
38
- r = r.replace(/-/g, '&#45;');
39
- r = r.replace(/ {2,}/g, (m) => ' ' + '&#160;'.repeat(m.length - 1));
40
- return r;
33
+ return gvXmlEscape(s, FLAGS_TITLE);
41
34
  }
42
35
 
43
36
  /**
44
- * Text/tooltip XML escape (gv_xml_escape with flags {raw, dash, nbsp}):
45
- * additionally escapes '-'→&#45;, newline/CR→&#10;/&#13;, and 2nd+ consecutive
46
- * spaces→&#160;. Used for textspans and tooltips.
47
- * @see plugin/core/gvrender_core_svg.c:546 (svg_textspan flags)
37
+ * Text/tooltip XML escape (gv_xml_escape with flags {raw, dash, nbsp}): raw
38
+ * escapes every '&' -- entity-looking or not -- and newline/CR to
39
+ * &#10;/&#13;; also '-'→&#45; and 2nd+ consecutive spaces→&#160;. Used for
40
+ * textspans and xlink:title.
41
+ * @see plugin/core/gvrender_core_svg.c:svg_textspan, svg_begin_anchor
42
+ * @see lib/util/xml.c:xml_core (`flags.raw || !xml_isentity(s)`)
48
43
  */
49
44
  export function escapeXmlText(s: string): string {
50
- let r = escapeXml(s);
51
- r = r.replace(/-/g, '&#45;');
52
- r = r.replace(/\n/g, '&#10;');
53
- r = r.replace(/\r/g, '&#13;');
54
- r = r.replace(/ {2,}/g, (m) => ' ' + '&#160;'.repeat(m.length - 1));
55
- return r;
45
+ return gvXmlEscape(s, FLAGS_TEXT);
56
46
  }