@knowvah/dot-engine 2.7.1 → 2.8.0

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 (117) hide show
  1. package/dist/api.js.map +2 -2
  2. package/dist/common/agerr.d.ts +8 -1
  3. package/dist/common/agerr.d.ts.map +1 -1
  4. package/dist/common/fma.d.ts.map +1 -1
  5. package/dist/common/htmltable-emit-fill.d.ts.map +1 -1
  6. package/dist/common/htmltable-label.d.ts +3 -0
  7. package/dist/common/htmltable-label.d.ts.map +1 -1
  8. package/dist/common/htmltable-lex-attvalue.d.ts.map +1 -1
  9. package/dist/common/htmltable.d.ts +5 -0
  10. package/dist/common/htmltable.d.ts.map +1 -1
  11. package/dist/common/layers.d.ts +12 -0
  12. package/dist/common/layers.d.ts.map +1 -1
  13. package/dist/common/layout-state.d.ts +8 -0
  14. package/dist/common/layout-state.d.ts.map +1 -0
  15. package/dist/common/make-label.d.ts.map +1 -1
  16. package/dist/common/multicolor.d.ts +6 -3
  17. package/dist/common/multicolor.d.ts.map +1 -1
  18. package/dist/common/nodeinit.d.ts.map +1 -1
  19. package/dist/common/poly-init.d.ts +11 -0
  20. package/dist/common/poly-init.d.ts.map +1 -1
  21. package/dist/common/poly-sizing.d.ts +1 -0
  22. package/dist/common/poly-sizing.d.ts.map +1 -1
  23. package/dist/common/postproc.d.ts.map +1 -1
  24. package/dist/common/style-resolve.d.ts +11 -9
  25. package/dist/common/style-resolve.d.ts.map +1 -1
  26. package/dist/gvc/device-cluster.d.ts +5 -6
  27. package/dist/gvc/device-cluster.d.ts.map +1 -1
  28. package/dist/gvc/device.d.ts.map +1 -1
  29. package/dist/gvc/first-layer.d.ts +21 -5
  30. package/dist/gvc/first-layer.d.ts.map +1 -1
  31. package/dist/gvc/gvrender-set-style.d.ts.map +1 -1
  32. package/dist/gvc/usershape.d.ts +20 -0
  33. package/dist/gvc/usershape.d.ts.map +1 -1
  34. package/dist/index.js +494 -275
  35. package/dist/index.js.map +4 -4
  36. package/dist/layout/circo/index.d.ts.map +1 -1
  37. package/dist/layout/dot/init.d.ts +10 -0
  38. package/dist/layout/dot/init.d.ts.map +1 -1
  39. package/dist/layout/dot/splines.d.ts.map +1 -1
  40. package/dist/layout/fdp/init.d.ts.map +1 -1
  41. package/dist/layout/neato/conjgrad-mkernel.d.ts.map +1 -1
  42. package/dist/layout/neato/constrained-majorization.d.ts.map +1 -1
  43. package/dist/layout/neato/edge-len.d.ts +6 -2
  44. package/dist/layout/neato/edge-len.d.ts.map +1 -1
  45. package/dist/layout/neato/init.d.ts.map +1 -1
  46. package/dist/layout/neato/kk.d.ts.map +1 -1
  47. package/dist/layout/neato/make-graph-data.d.ts.map +1 -1
  48. package/dist/layout/neato/matrix-ops.d.ts +0 -6
  49. package/dist/layout/neato/matrix-ops.d.ts.map +1 -1
  50. package/dist/layout/neato/smart-ini-x.d.ts.map +1 -1
  51. package/dist/layout/neato/splines.d.ts.map +1 -1
  52. package/dist/layout/neato/start.d.ts.map +1 -1
  53. package/dist/layout/neato/stress-kernel.d.ts +2 -5
  54. package/dist/layout/neato/stress-kernel.d.ts.map +1 -1
  55. package/dist/layout/neato/stress.d.ts +6 -0
  56. package/dist/layout/neato/stress.d.ts.map +1 -1
  57. package/dist/layout/osage/index.d.ts.map +1 -1
  58. package/dist/layout/patchwork/index.d.ts.map +1 -1
  59. package/dist/layout/sfdp/init.d.ts.map +1 -1
  60. package/dist/layout/twopi/init.d.ts +12 -6
  61. package/dist/layout/twopi/init.d.ts.map +1 -1
  62. package/dist/model/cgraph-ops.d.ts +9 -0
  63. package/dist/model/cgraph-ops.d.ts.map +1 -1
  64. package/dist/render/color-resolve.d.ts +7 -0
  65. package/dist/render/color-resolve.d.ts.map +1 -1
  66. package/dist/render/json.d.ts +17 -2
  67. package/dist/render/json.d.ts.map +1 -1
  68. package/dist/render/map.d.ts +3 -5
  69. package/dist/render/map.d.ts.map +1 -1
  70. package/dist/render.js +500 -281
  71. package/dist/render.js.map +4 -4
  72. package/package.json +1 -1
  73. package/src/common/agerr.ts +69 -8
  74. package/src/common/fma.ts +11 -1
  75. package/src/common/htmltable-emit-fill.ts +11 -7
  76. package/src/common/htmltable-label.ts +6 -3
  77. package/src/common/htmltable-lex-attvalue.ts +17 -1
  78. package/src/common/htmltable.ts +10 -5
  79. package/src/common/layers.ts +31 -2
  80. package/src/common/layout-state.ts +35 -0
  81. package/src/common/make-label.ts +6 -2
  82. package/src/common/multicolor.ts +12 -6
  83. package/src/common/nodeinit.ts +2 -1
  84. package/src/common/poly-init.ts +18 -1
  85. package/src/common/poly-sizing.ts +4 -4
  86. package/src/common/postproc.ts +4 -1
  87. package/src/common/style-resolve.ts +118 -50
  88. package/src/gvc/device-cluster.ts +31 -39
  89. package/src/gvc/device.ts +55 -43
  90. package/src/gvc/first-layer.ts +60 -11
  91. package/src/gvc/gvrender-set-style.ts +1 -6
  92. package/src/gvc/usershape.ts +40 -0
  93. package/src/layout/circo/index.ts +4 -0
  94. package/src/layout/dot/init.ts +32 -0
  95. package/src/layout/dot/splines.ts +3 -0
  96. package/src/layout/fdp/init.ts +17 -0
  97. package/src/layout/neato/conjgrad-mkernel.ts +2 -3
  98. package/src/layout/neato/constrained-majorization.ts +2 -1
  99. package/src/layout/neato/edge-len.ts +39 -7
  100. package/src/layout/neato/init.ts +8 -3
  101. package/src/layout/neato/kk.ts +2 -1
  102. package/src/layout/neato/make-graph-data.ts +1 -2
  103. package/src/layout/neato/matrix-ops.ts +1 -11
  104. package/src/layout/neato/smart-ini-x.ts +1 -1
  105. package/src/layout/neato/splines.ts +3 -1
  106. package/src/layout/neato/start.ts +3 -2
  107. package/src/layout/neato/stress-kernel.ts +45 -86
  108. package/src/layout/neato/stress.ts +19 -0
  109. package/src/layout/osage/index.ts +8 -1
  110. package/src/layout/patchwork/index.ts +4 -0
  111. package/src/layout/sfdp/init.ts +3 -0
  112. package/src/layout/twopi/init.ts +21 -9
  113. package/src/layout/twopi/pipeline.ts +3 -3
  114. package/src/model/cgraph-ops.ts +19 -0
  115. package/src/render/color-resolve.ts +17 -2
  116. package/src/render/json.ts +9 -10
  117. package/src/render/map.ts +5 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knowvah/dot-engine",
3
- "version": "2.7.1",
3
+ "version": "2.8.0",
4
4
  "description": "Faithful TypeScript port of Graphviz (dot, neato, fdp, sfdp, circo, twopi, osage, patchwork) — no C: no native binary, no WASM. Oracle-verified against the C implementation, runs in the browser.",
5
5
  "keywords": [
6
6
  "graphviz",
@@ -4,6 +4,15 @@
4
4
  * The one stderr writer for messages C prints through cgraph's agerr family
5
5
  * (`agerr`, `agerrorf`, `agwarningf`, `AGPREV` continuations). Sites whose C
6
6
  * counterpart is `fprintf(stderr)`/`fputs`/`printf` do not use it.
7
+ *
8
+ * Partial lines (AD-7): the port strips C's trailing `\n` at every call site,
9
+ * so `agerr`/`agerrorf`/`agwarningf` always finish a line. A site whose C
10
+ * message has no trailing `\n` (`emit.c:parseSegs`) opts in with
11
+ * {@link agerrPartial}: the text is held, and the next `agerr*` message is
12
+ * appended to it (its own prefix included, as C's `out` writes it) and the
13
+ * joined line printed on the held line's channel. A held line is flushed on
14
+ * its own by {@link agerrLineOn}/{@link agerrFinished} and by render-state
15
+ * reset.
7
16
  */
8
17
 
9
18
  import { registerRenderState } from './render-state.js';
@@ -30,7 +39,14 @@ const OCTAL_RADIX = 8;
30
39
  */
31
40
  let lastLevel: 'error' | 'warning' = 'warning';
32
41
 
42
+ /**
43
+ * The held partial line and its channel (one slot: stderr has one cursor).
44
+ * Cleared by {@link flushPending}; reset per render.
45
+ */
46
+ const pending: { line: string | null; channel: AgerrChannel } = { line: null, channel: 'warn' };
47
+
33
48
  registerRenderState(() => {
49
+ flushPending();
34
50
  lastLevel = 'warning';
35
51
  });
36
52
 
@@ -55,25 +71,70 @@ export function escapeControl(message: string): string {
55
71
  * channel, escaped as `default_usererrf` would.
56
72
  */
57
73
  export function agerrLineOn(channel: AgerrChannel, line: string): void {
74
+ flushPending();
75
+ writeLine(channel, line);
76
+ }
77
+
78
+ /** Print a held partial line, if any, as its own line. */
79
+ function flushPending(): void {
80
+ if (pending.line === null) return;
81
+ const { line, channel } = pending;
82
+ pending.line = null;
83
+ writeLine(channel, line);
84
+ }
85
+
86
+ function writeLine(channel: AgerrChannel, line: string): void {
58
87
  if (channel === 'warn') console.warn(escapeControl(line));
59
88
  else console.error(escapeControl(line));
60
89
  }
61
90
 
91
+ /** The text `out` writes for one message: prefix (none for AGPREV) + body. */
92
+ function assemble(level: AgerrLevel, msg: string): string {
93
+ if (level === 'prev') return msg;
94
+ const prefix = level === 'error' ? PREFIX_ERROR : PREFIX_WARNING;
95
+ return `${prefix}${PREFIX_SEP}${msg}`;
96
+ }
97
+
98
+ /** Console channel of a message; AGPREV continues the previous level's. */
99
+ function channelOf(level: AgerrLevel): AgerrChannel {
100
+ const eff = level === 'prev' ? lastLevel : level;
101
+ return eff === 'warning' ? 'warn' : 'error';
102
+ }
103
+
104
+ /**
105
+ * Shared body of `agerr`/`agerrPartial`: record the level, assemble the text,
106
+ * join it onto a held line (which fixes the channel), then hold or print.
107
+ */
108
+ function emit(level: AgerrLevel, msg: string, channel: AgerrChannel | undefined, hold: boolean): void {
109
+ const target = channel ?? channelOf(level);
110
+ if (level !== 'prev') lastLevel = level;
111
+ const text = assemble(level, msg);
112
+ if (pending.line === null) {
113
+ pending.channel = target;
114
+ pending.line = text;
115
+ } else pending.line += text;
116
+ if (!hold) flushPending();
117
+ }
118
+
62
119
  /**
63
120
  * The part of C's `agerr(level, ...)` that is not formatting: the `Error: `/
64
121
  * `Warning: ` prefix (none for AGPREV), escaping, and the console channel
65
122
  * (`console.warn` for warnings, `console.error` otherwise; AGPREV continues
66
- * the previous level's channel).
123
+ * the previous level's channel). Finishes the line, joining a held partial.
67
124
  * @see lib/cgraph/agerror.c:out
68
125
  */
69
126
  export function agerr(level: AgerrLevel, msg: string, channel?: AgerrChannel): void {
70
- if (level === 'prev') {
71
- agerrLineOn(channel ?? (lastLevel === 'warning' ? 'warn' : 'error'), msg);
72
- return;
73
- }
74
- lastLevel = level;
75
- const prefix = level === 'error' ? PREFIX_ERROR : PREFIX_WARNING;
76
- agerrLineOn(channel ?? (level === 'warning' ? 'warn' : 'error'), `${prefix}${PREFIX_SEP}${msg}`);
127
+ emit(level, msg, channel, false);
128
+ }
129
+
130
+ /**
131
+ * `agerr` for a C message with no trailing `\n`: the line stays open and the
132
+ * next `agerr*` message continues it. The channel follows this (first)
133
+ * message's level unless `channel` is given.
134
+ * @see lib/cgraph/agerror.c:out
135
+ */
136
+ export function agerrPartial(level: AgerrLevel, msg: string, channel?: AgerrChannel): void {
137
+ emit(level, msg, channel, true);
77
138
  }
78
139
 
79
140
  /** @see lib/cgraph/agerror.c:agerrorf */
package/src/common/fma.ts CHANGED
@@ -13,6 +13,10 @@
13
13
  * half-ULP boundary of s — not observed for any supported input
14
14
  * (validated against a full-precision C oracle).
15
15
  *
16
+ * A non-finite product or addend short-circuits to `a * b + c` (or the
17
+ * non-finite `c` when a and b are finite) before the split: the split's `a·b - p` terms turn an
18
+ * infinite operand into Inf - Inf = NaN where hardware fmadd gives ±Inf.
19
+ *
16
20
  * Per Boldo & Muller, "Some functions computable with a fused-mac"
17
21
  * (IEEE Trans. Computers, 2011) — the exact emulation needs
18
22
  * round-to-odd; this is the standard approximation without it.
@@ -25,8 +29,14 @@ const SPLIT = 134217729;
25
29
  * round(a·b + c) with one rounding (fmadd).
26
30
  */
27
31
  export function fma(a: number, b: number, c: number): number {
28
- /* Dekker two-product: p + err == a·b exactly */
29
32
  const p = a * b;
33
+ /* Non-finite product or addend: the IEEE fmadd result is p + c, except
34
+ * that a finite a·b which merely overflowed p yields the non-finite c. */
35
+ if (!Number.isFinite(p) || !Number.isFinite(c)) {
36
+ return Number.isFinite(a) && Number.isFinite(b) && !Number.isFinite(c) ? c : p + c;
37
+ }
38
+
39
+ /* Dekker two-product: p + err == a·b exactly */
30
40
  const at = SPLIT * a;
31
41
  const ahi = at - (at - a);
32
42
  const alo = a - ahi;
@@ -16,7 +16,7 @@ import { ObjType, EmitState, MapShape } from '../gvc/job.js';
16
16
  import type { RendererPlugin } from '../gvc/context.js';
17
17
  import { FillType, PenType } from '../gvc/context.js';
18
18
  import { transformPoint } from '../gvc/device.js';
19
- import { firstColor, resolveRenderColor } from '../render/color-resolve.js';
19
+ import { firstColor, resolveFillColorName, resolveRenderColor } from '../render/color-resolve.js';
20
20
  import { emitRoundedBezier } from './poly-shapes.js';
21
21
  import type { PlacedHtml, PlacedCell } from './htmltable-pos.js';
22
22
  import { htmlStyle } from './htmltable-style.js';
@@ -28,7 +28,7 @@ import {
28
28
  BORDER_BOTTOM,
29
29
  BORDER_MASK,
30
30
  } from './htmltable-types.js';
31
- import { parseSegs, parseSegsSilent } from './multicolor.js';
31
+ import { MSG_MANY_COLORS, parseSegs, parseSegsSilent } from './multicolor.js';
32
32
  import { agwarningf } from './agerr.js';
33
33
  import { registerRenderState } from './render-state.js';
34
34
 
@@ -43,9 +43,6 @@ export interface HtmlStopColor {
43
43
  frac: number;
44
44
  }
45
45
 
46
- /** @see lib/common/emit.c:findStopColor */
47
- const MSG_MANY_COLORS = 'More than 2 colors specified for a gradient - ignoring remaining';
48
-
49
46
  /**
50
47
  * Port of C's findStopColor over parseSegs: null when parseSegs returns
51
48
  * non-zero (including the rv 3 over-full warning), has < 2 segments or has no
@@ -197,8 +194,10 @@ function penTypeOf(style: string | undefined): PenType {
197
194
  * @see lib/common/htmltable.c:setFill
198
195
  */
199
196
  export function withHtmlPaint(paint: HtmlPaint, job: RenderJob, drawFn: () => void): void {
197
+ // gvrender_set_fillcolor hands the cut name to colorxlate even when empty
198
+ // (":red" -> ""), so the empty name warns and fills opaque black.
200
199
  paintObj.fillColor = paint.fill !== undefined
201
- ? resolveRenderColor(firstColor(paint.fill)) : { type: 'none' };
200
+ ? resolveFillColorName(firstColor(paint.fill)) : { type: 'none' };
202
201
  paintObj.penColor = paint.pen !== undefined
203
202
  ? resolveRenderColor(paint.pen) : { type: 'string', s: 'transparent' };
204
203
  applyHtmlFill(paint);
@@ -425,7 +424,12 @@ const HTML_DEFAULT_COLOR = 'black';
425
424
  /** setFill's names: the fill (and a gradient's stop). @see htmltable.c:setFill */
426
425
  function bgNames(bgcolor: string): string[] {
427
426
  const spec = parseGradientSpec(bgcolor);
428
- return spec === null ? [bgcolor] : [spec[0], spec[1]];
427
+ if (spec !== null) return [spec[0], spec[1]];
428
+ // A spec parseSegs rejects, or an empty cut name (":red"), is resolved by
429
+ // setFill itself, after findStopColor's own diagnostics: leave it to the
430
+ // paint. @see lib/common/htmltable.c:setFill
431
+ if (parseSegsSilent(bgcolor).error !== 0 || firstColor(bgcolor) === '') return [];
432
+ return [bgcolor];
429
433
  }
430
434
 
431
435
  /** One cell in emit_html_cell order: fill, border, then the child. */
@@ -11,6 +11,7 @@
11
11
 
12
12
  import type { TextlabelT } from './types.js';
13
13
  import type { TextMeasurer } from './textmeasure.js';
14
+ import type { ImageResolver } from '../gvc/image-resolver.js';
14
15
  import type { HtmlLabel, ImageSizer } from './htmltable-types.js';
15
16
  import { sizeHtmlLabel, parseHtmlLabelRv, inheritTablePenColor } from './htmltable.js';
16
17
  import { makeLabel } from './make-label.js';
@@ -28,7 +29,9 @@ export interface HtmlLabelResult {
28
29
  }
29
30
 
30
31
  /** The label's font, the object's pen color, and the `<IMG>` sizer. */
31
- export type HtmlLabelInfo = HtmlFontInfo & { imageSizer?: ImageSizer; pencolor?: string };
32
+ export type HtmlLabelInfo = HtmlFontInfo & {
33
+ imageSizer?: ImageSizer; imageResolver?: ImageResolver; imageDpi?: number; pencolor?: string;
34
+ };
32
35
 
33
36
  /**
34
37
  * make_html_label: parse, size and position an HTML-like label. Returns C's
@@ -44,7 +47,7 @@ export function makeHtmlLabelTo(
44
47
  measurer: TextMeasurer,
45
48
  stderr: StderrLines,
46
49
  ): HtmlLabelResult {
47
- const { fontname, fontsize, fontcolor, imageSizer, pencolor } = finfo;
50
+ const { fontname, fontsize, fontcolor, imageSizer, imageResolver, imageDpi, pencolor } = finfo;
48
51
  let lbl: HtmlLabel, lexWarn: boolean; // lexWarn: htmllex.c's ctx->warn, rv bit 1
49
52
  try {
50
53
  ({ label: lbl, warn: lexWarn } = parseHtmlLabelRv(content, stderr));
@@ -61,7 +64,7 @@ export function makeHtmlLabelTo(
61
64
  // @see lib/common/htmltable.c:1911 (getPenColor), :1406/:1556 (inheritance)
62
65
  if (lbl.kind === 'table') inheritTablePenColor(lbl.table, pencolor);
63
66
  const rv = Number(lexWarn) | sizeHtmlLabel(lbl, measurer, {
64
- fontsize, fontname, imageSizer, stderr,
67
+ fontsize, fontname, imageSizer, imageResolver, imageDpi, stderr,
65
68
  });
66
69
  const placed = posHtmlLabel(lbl, { fontname, fontsize, fontcolor }, measurer);
67
70
  const dim = lbl.dimen ?? { w: 0, h: 0 };
@@ -33,6 +33,22 @@ const DEC_DIGIT = /[0-9]/;
33
33
  const HEX_DIGIT = /[0-9A-Fa-f]/;
34
34
  /** Bytes expat's tokenizer rejects anywhere (BT_NONXML): controls other than tab, LF and CR. */
35
35
  const NON_XML = /[\x00-\x08\x0b\x0c\x0e-\x1f]/;
36
+ /** The same classification for U+FFFE, U+FFFF and a lone surrogate (the UTF-8 form of one is invalid). */
37
+ const NON_XML_BEYOND_ASCII = /[\ufffe\uffff]/;
38
+ const HIGH_SURROGATE = /[\ud800-\udbff]/;
39
+ const LOW_SURROGATE = /[\udc00-\udfff]/;
40
+
41
+ /**
42
+ * Whether the character at `k` is BT_NONXML in a value: what `CONTENT_NON_XML`
43
+ * (htmltable-lex-expat.ts) rejects in content, one tokenizer table for both.
44
+ * @see expat lib/asciitab.h
45
+ */
46
+ function isNonXmlAt(text: string, k: number): boolean {
47
+ const c = text[k]!;
48
+ if (NON_XML.test(c) || NON_XML_BEYOND_ASCII.test(c)) return true;
49
+ if (HIGH_SURROGATE.test(c)) return !LOW_SURROGATE.test(text[k + 1] ?? '');
50
+ return LOW_SURROGATE.test(c) && !HIGH_SURROGATE.test(text[k - 1] ?? '');
51
+ }
36
52
 
37
53
  /** A reference's digits from `i` up to `;`: the index after it, or where scanning stops. */
38
54
  function scanDigits(text: string, i: number, digit: RegExp): number | AttValueScan {
@@ -74,7 +90,7 @@ export function scanAttValue(text: string, j: number): AttValueScan {
74
90
  const c = text[k];
75
91
  if (c === undefined) return { partial: true };
76
92
  if (c === q) return { close: k };
77
- if (c === '<' || NON_XML.test(c)) return { invalid: k };
93
+ if (c === '<' || isNonXmlAt(text, k)) return { invalid: k };
78
94
  if (c !== '&') {
79
95
  k++;
80
96
  continue;
@@ -22,9 +22,10 @@ import type {
22
22
  HtmlCell, HtmlCellContent, HtmlImage, HtmlLabel, HtmlTable, HtmlText,
23
23
  ImageSizer,
24
24
  } from './htmltable-types.js';
25
- import { findImageSize, imageIsSupplied, imageOpenWarning } from '../gvc/usershape.js';
25
+ import { DEFAULT_DPI, imageIsSupplied, imageOpenWarning, usershapeSizePts } from '../gvc/usershape.js';
26
26
  import { registerRenderState } from './render-state.js';
27
27
  import { PointSet } from './pointset.js';
28
+ import type { ImageResolver } from '../gvc/image-resolver.js';
28
29
  import type { TextMeasurer } from './textmeasure.js';
29
30
  import { buildLineRuns, runsAreSimple } from './htmltable-pos-runs.js';
30
31
  import { withTableFont } from './htmltable-fontinfo.js';
@@ -87,6 +88,10 @@ export interface HtmlSizeEnv {
87
88
  fontsize: number;
88
89
  fontname: string;
89
90
  imageSizer?: ImageSizer;
91
+ /** Per-context resolver: bytes it supplies count as a found file. */
92
+ imageResolver?: ImageResolver;
93
+ /** The graph's usershape dpi (`gvusershape_size` pixel to point); default 96. */
94
+ imageDpi?: number;
90
95
  stderr?: StderrLines; // the label's stderr; none: straight to the console
91
96
  }
92
97
 
@@ -132,20 +137,20 @@ const sizeTextContent = (
132
137
  /** Images whose sizing failed: size_html_img's rv = 1 on every later call. */
133
138
  const missingImages = { set: new WeakSet<HtmlImage>() };
134
139
 
135
- const sizeHtmlImg = (img: HtmlImage, { imageSizer, stderr }: HtmlSizeEnv): number => {
140
+ const sizeHtmlImg = (img: HtmlImage, { imageSizer, imageResolver, imageDpi, stderr }: HtmlSizeEnv): number => {
136
141
  // C sizes each image once (processTbl); the port's size and pos passes
137
142
  // share layoutHtmlTable, so guard against re-sizing (and re-warning). The
138
143
  // returned rv still follows C on every call.
139
144
  if (img.width !== undefined && img.height !== undefined) return missingImages.set.has(img) ? 1 : 0;
140
- const sz = imageSizer !== undefined ? imageSizer(img.src) : findImageSize(img.src);
145
+ const sz = usershapeSizePts(img.src, imageDpi ?? DEFAULT_DPI, imageSizer);
141
146
  if (sz !== null) {
142
- img.width = sz.w; img.height = sz.h;
147
+ img.width = sz.x; img.height = sz.y;
143
148
  return 0;
144
149
  }
145
150
  // Missing-image: gvusershape_open's warning (unless bytes are supplied),
146
151
  // then zero size + the Error.
147
152
  // @see lib/gvc/gvusershape.c:gvusershape_file_access
148
- if (!imageIsSupplied(img.src, imageSizer)) agerrLine(stderr, `Warning: ${imageOpenWarning(img.src)}`);
153
+ if (!imageIsSupplied(img.src, imageSizer, imageResolver)) agerrLine(stderr, `Warning: ${imageOpenWarning(img.src)}`);
149
154
  // C: agerrorf("No or improper image file=\"%s\"\n", img->src) — agerr
150
155
  // prefixes "Error: " on stderr.
151
156
  agerrLine(stderr, `Error: No or improper image file="${img.src}"`);
@@ -6,6 +6,7 @@
6
6
  // only in the layers it belongs to (its `layer` attribute, or all layers when
7
7
  // it has none). All of this is inert unless numLayers > 1.
8
8
 
9
+ import { agwarningf } from './agerr.js';
9
10
  import type { Graph } from '../model/graph.js';
10
11
  import type { Node } from '../model/node.js';
11
12
  import type { Edge } from '../model/edge.js';
@@ -21,6 +22,11 @@ export interface LayerInfo {
21
22
  numLayers: number;
22
23
  sep: string;
23
24
  listSep: string;
25
+ /**
26
+ * C `gvc->layerlist`: `[count, l1, …, lcount, numLayers + 1]` from the
27
+ * `layerselect` attribute, or null when absent or matching no layer.
28
+ */
29
+ layerList: number[] | null;
24
30
  }
25
31
 
26
32
  /** Split `s` on any character in the `delims` set, dropping empty tokens
@@ -39,10 +45,13 @@ export function parseLayers(g: Graph): LayerInfo {
39
45
  // Conflict: a char in both sep sets → ignore listSep (C warns + clears it).
40
46
  if ([...sep].some((c) => listSep.includes(c))) listSep = '';
41
47
  if (p === undefined || p === '') {
42
- return { layerIDs: [], numLayers: 1, sep, listSep };
48
+ return { layerIDs: [], numLayers: 1, sep, listSep, layerList: null };
43
49
  }
44
50
  const toks = splitOnSet(p, sep);
45
- return { layerIDs: [null, ...toks], numLayers: toks.length, sep, listSep };
51
+ const info: LayerInfo = { layerIDs: [null, ...toks], numLayers: toks.length, sep, listSep, layerList: null };
52
+ const sel = g.attrs.get('layerselect');
53
+ if (sel !== undefined && sel !== '') info.layerList = parseLayerselect(info, sel);
54
+ return info;
46
55
  }
47
56
 
48
57
  function isNaturalNumber(s: string): boolean {
@@ -111,3 +120,23 @@ export function edgeInLayer(info: LayerInfo, layerNum: number, e: Edge): boolean
111
120
  return pn === '' || selectedLayer(info, layerNum, pn);
112
121
  });
113
122
  }
123
+
124
+ /**
125
+ * Walk each layer index and keep those `selectedLayer` matches against `p`.
126
+ * Returns `[n, l1..ln, numLayers + 1]`, or null (after a warning) when nothing
127
+ * matches. The warning's "specifed" is native's spelling.
128
+ * @see lib/common/emit.c:parse_layerselect
129
+ */
130
+ export function parseLayerselect(info: LayerInfo, p: string): number[] | null {
131
+ const picked: number[] = [];
132
+ for (let i = 1; i <= info.numLayers; i++) {
133
+ if (selectedLayer(info, i, p)) picked.push(i);
134
+ }
135
+ if (picked.length === 0) {
136
+ agwarningf(
137
+ `The layerselect attribute "${p}" does not match any layer specifed by the layers attribute - ignored.`,
138
+ );
139
+ return null;
140
+ }
141
+ return [picked.length, ...picked, info.numLayers + 1];
142
+ }
@@ -0,0 +1,35 @@
1
+ // SPDX-License-Identifier: EPL-2.0
2
+
3
+ /**
4
+ * The C global `State` ("last finished phase"). It is a per-process variable
5
+ * in C; the port resets it per render (AD-4) to its initial value.
6
+ *
7
+ * @see lib/common/globals.h:State
8
+ */
9
+
10
+ import { registerRenderState } from './render-state.js';
11
+
12
+ /** Phase values for `State`. @see lib/common/const.h:GVBEGIN, GVSPLINES */
13
+ export const GVBEGIN = 0;
14
+ export const GVSPLINES = 1;
15
+
16
+ /**
17
+ * Holder for the C global `State`. C's initial value is 0 (BSS) and
18
+ * graph_init assigns GVBEGIN, which is the same value.
19
+ * @see lib/common/input.c:graph_init (State = GVBEGIN)
20
+ */
21
+ const layoutState = { state: GVBEGIN };
22
+
23
+ registerRenderState(() => {
24
+ layoutState.state = GVBEGIN;
25
+ });
26
+
27
+ /** Read `State`. @see lib/common/globals.h:State */
28
+ export function getState(): number {
29
+ return layoutState.state;
30
+ }
31
+
32
+ /** Assign `State`. @see lib/common/globals.h:State */
33
+ export function setState(value: number): void {
34
+ layoutState.state = value;
35
+ }
@@ -20,6 +20,7 @@ import { Node } from '../model/node.js';
20
20
  import { nodeAttr, graphAttrInherited } from './attr-get.js';
21
21
  import { Graph } from '../model/graph.js';
22
22
  import { htmlEntityUTF8 } from './html-entities.js';
23
+ import { usershapeDpi } from '../gvc/usershape.js';
23
24
 
24
25
  export const DEFAULT_FONTSIZE = 14.0;
25
26
  /** C's default fontname (non-Windows); renderers alias it, e.g. SVG
@@ -217,11 +218,14 @@ export function makeAnyLabel(
217
218
  // pen color so an HTML table/cell border with no explicit COLOR inherits
218
219
  // it (htmltable.c:1911 getPenColor). @see getPenColor
219
220
  // ADR-2: a per-context sizer (when set) wins over the global one.
220
- const imageSizer = obj !== undefined ? rootOf(obj).info.gvc?.imageSizer : undefined;
221
+ const root = obj !== undefined ? rootOf(obj) : undefined;
222
+ const imageSizer = root?.info.gvc?.imageSizer;
223
+ const imageResolver = root?.info.gvc?.imageResolver;
224
+ const imageDpi = root !== undefined ? usershapeDpi(root.attrs) : undefined;
221
225
  // One stderr for the label: doInt's unterminated warning runs into the
222
226
  // next write, `in label of ...` included (agerr AGPREV).
223
227
  const stderr = labelStderr();
224
- const { label, rv } = makeHtmlLabelTo(content, { ...font, pencolor: getPenColor(obj), imageSizer }, measurer, stderr);
228
+ const { label, rv } = makeHtmlLabelTo(content, { ...font, pencolor: getPenColor(obj), imageSizer, imageResolver, imageDpi }, measurer, stderr);
225
229
  if (rv !== 0 && obj !== undefined) stderr.write(`${inLabelOf(obj)}\n`);
226
230
  stderr.flush();
227
231
  return label;
@@ -13,7 +13,7 @@
13
13
  * @see lib/common/emit.c:470 parseSegs
14
14
  */
15
15
 
16
- import { agerr, agerrorf, agwarningf } from './agerr.js';
16
+ import { agerr, agerrPartial, agerrorf } from './agerr.js';
17
17
  import type { Edge } from '../model/edge.js';
18
18
  import { registerRenderState } from './render-state.js';
19
19
 
@@ -64,6 +64,9 @@ function claimWarned(): boolean {
64
64
  /** A claim that never fires: parses without touching or consuming the flag. */
65
65
  const claimNever = (): boolean => false;
66
66
 
67
+ /** @see lib/common/emit.c:findStopColor */
68
+ export const MSG_MANY_COLORS = 'More than 2 colors specified for a gradient - ignoring remaining';
69
+
67
70
  /** @see lib/common/emit.c:451 EPS */
68
71
  const EPS = 1e-5;
69
72
 
@@ -187,9 +190,10 @@ function scanSegs(
187
190
  * On rv 1 or 2 the returned segs array is empty.
188
191
  *
189
192
  * The rv 2 error and rv 3 warning are printed here, once per process (the
190
- * shared `warned` flag). C's callers then append an AGPREV line for rv > 1;
191
- * `context` supplies it (e.g. `in edge a -> b`): the warning continues the
192
- * same line as C's AGPREV does, the error's context follows on its own line.
193
+ * shared `warned` flag). The warning is a held partial line (no `\n`, as in
194
+ * C); C's callers then append an AGPREV line for rv > 1; `context` supplies it
195
+ * (e.g. `in edge a -> b`) and completes the held line. Without `context` the
196
+ * next agerr message continues the line.
193
197
  *
194
198
  * @see lib/common/emit.c:470 parseSegs
195
199
  */
@@ -199,8 +203,10 @@ export function parseSegs(
199
203
  ): { segs: ColorSeg[]; error: 0 | 1 | 2 | 3 } {
200
204
  const result = scanSegs(colorlist, claimWarned);
201
205
  if (result.error === 3) {
202
- const tail = context === undefined ? '' : context();
203
- agwarningf(`Total size > 1 in "${colorlist}" color spec ${tail}`);
206
+ // C's message has no newline: the line stays open for the next agerr*
207
+ // call (or the caller's AGPREV context line).
208
+ agerrPartial('warning', `Total size > 1 in "${colorlist}" color spec `);
209
+ if (context !== undefined) agerr('prev', context());
204
210
  } else if (result.error === 2) {
205
211
  agerrorf(`Illegal value in "${colorlist}" color attribute; float expected after ';'`);
206
212
  if (context !== undefined) agerr('prev', context());
@@ -15,7 +15,7 @@ import type { TextMeasurer } from './textmeasure.js';
15
15
  import type { PolygonT, ShapeDesc, TextlabelT } from './types.js';
16
16
  import { ShapeKind } from './types.js';
17
17
  import { bindShape } from './shapes.js';
18
- import { assignShapeInfo, buildNodeLabel, nodeAttr, readFontAttrs } from './poly-init.js';
18
+ import { assignShapeInfo, buildNodeLabel, nodeAttr, nodeImageSize, readFontAttrs } from './poly-init.js';
19
19
  import { makeAnyLabel } from './make-label.js';
20
20
  import { isHtmlValue, htmlValueContent } from './html-string.js';
21
21
  import { NODE_XLABEL } from '../layout/dot/rank.js';
@@ -154,6 +154,7 @@ export function polySizeParamsFromNode(
154
154
  isPlain: shape.name === 'plain',
155
155
  optionShape: poly.option.shape, // size_gen discriminant (STAR/CYLINDER)
156
156
  nojustify: mapbool(nodeAttr(n, g, 'nojustify')), // shapes.c:2133
157
+ imageSize: nodeImageSize(n, g), // shapes.c:2039-2051
157
158
  flip,
158
159
  };
159
160
  }
@@ -26,7 +26,7 @@ import {
26
26
  } from './make-label.js';
27
27
  import { computeVertices } from './poly-vertices.js';
28
28
  import { agwarningf } from './agerr.js';
29
- import { imageIsSupplied, imageOpenWarning } from '../gvc/usershape.js';
29
+ import { imageIsSupplied, imageOpenWarning, usershapeDpi, usershapeSizePts } from '../gvc/usershape.js';
30
30
 
31
31
  // Default half-dimensions in points (72 dpi, 0.75 in wide, 0.5 in tall)
32
32
  const DEFAULT_LW = 27;
@@ -114,6 +114,23 @@ function warnMissingImage(n: Node, g: Graph, shapefile: string | undefined): voi
114
114
  agwarningf(`No or improper image="${image}" for node "${n.name}"`);
115
115
  }
116
116
 
117
+ /**
118
+ * poly_init's imagesize for a non-custom shape's `image=`: gvusershape_size in
119
+ * points plus the fixed 2pt padding, and GD_has_images. Null (C's imagesize 0)
120
+ * when there is no image or the caller supplies no size; the missing-image
121
+ * warning stays in warnMissingImage.
122
+ * @see lib/common/shapes.c:2039-2051
123
+ */
124
+ export function nodeImageSize(n: Node, g: Graph): { x: number; y: number } | undefined {
125
+ const shapefile = nodeAttr(n, g, 'shapefile');
126
+ if (shapefile !== undefined && shapefile !== '') return undefined;
127
+ const gvc = g.root.info.gvc;
128
+ const sz = usershapeSizePts(nodeAttr(n, g, 'image'), usershapeDpi(g.root.attrs), gvc?.imageSizer);
129
+ if (sz === null) return undefined;
130
+ g.root.info.has_images = true;
131
+ return { x: sz.x + 2, y: sz.y + 2 }; // some fixed padding
132
+ }
133
+
117
134
  export function polyInit(n: Node, g: Graph, measurer: TextMeasurer): void {
118
135
  const shapeName = nodeAttr(n, g, 'shape') ?? 'ellipse';
119
136
  const shapefile = nodeAttr(n, g, 'shapefile');
@@ -89,6 +89,7 @@ export interface PolySizeParams {
89
89
  * @see lib/common/shapes.c:2132-2145
90
90
  */
91
91
  nojustify?: boolean;
92
+ imageSize?: Point; // padded image size in points, shapes.c:2020-2051 imagesize
92
93
  }
93
94
 
94
95
  /** Node geometry in points: left/right half-widths and height. */
@@ -550,7 +551,8 @@ export function polySize(p: PolySizeParams): PolySizeResult {
550
551
  const init = initialSizePts(p);
551
552
  const dimen = quantize(padLabelDimen(p.labelDimen, p.margin, p.isPlain), p.quantumIn);
552
553
  const { sides, isBox } = effectiveShape(p);
553
- let bb: Point = { x: dimen.x, y: dimen.y };
554
+ const img = p.imageSize ?? { x: 0, y: 0 }; // shapes.c:2055: bb = max(dimen, imagesize)
555
+ let bb: Point = { x: Math.max(dimen.x, img.x), y: Math.max(dimen.y, img.y) };
554
556
  // C shapes.c:2073-2085 dispatch: box → exact fit; poly_desc shapes (star,
555
557
  // cylinder) → their size_gen; everything else → generic ellipse fit.
556
558
  if (isBox) {
@@ -583,8 +585,6 @@ export function polySize(p: PolySizeParams): PolySizeResult {
583
585
  * Compute the label's justification borders (ND_label->space) and vertical
584
586
  * alignment — the "space available for label" block of poly_init. `bb` is the
585
587
  * final (constrained) box; `minBb` is the pre-constraint shape-fit box.
586
- * Image sizing is unported, so `imagesize` is 0 (which matches C for the
587
- * missing-image repro cases); revisit when image loading lands.
588
588
  * @see lib/common/shapes.c:poly_init (2132-2152)
589
589
  */
590
590
  function labelSpace(
@@ -604,7 +604,7 @@ function labelSpace(
604
604
  spaceX = dimen.x - spacex;
605
605
  }
606
606
  // space.y: !fixedshape → dimen.y + (bb.y - min_bb.y); fixedshape keeps dimen.y.
607
- const spaceY = fixedshape ? dimen.y : dimen.y + (bb.y - minBb.y);
607
+ const spaceY = fixedshape ? dimen.y : dimen.y + ((bb.y - minBb.y) + Math.max(0, (p.imageSize?.y ?? 0) - dimen.y));
608
608
  return { space: { x: spaceX, y: spaceY }, valign: labelValign(p) };
609
609
  }
610
610
 
@@ -27,6 +27,7 @@ import {
27
27
  } from '../layout/dot/init.js';
28
28
  import { addXLabels } from './xlabels-place.js';
29
29
  import { registerRenderState } from './render-state.js';
30
+ import { getState, GVSPLINES } from './layout-state.js';
30
31
  import { agerrorf } from './agerr.js';
31
32
 
32
33
  /** Concentrated multi-edge duplicate — never drawn. @see lib/common/const.h:IGNORED */
@@ -221,7 +222,9 @@ function translateDrawing(g: Graph): void {
221
222
 
222
223
  for (const v of g.nodes.values()) {
223
224
  mapNode(v);
224
- // State == GVSPLINES is always true post-spline-routing in the dot pipeline
225
+ // C: `if (State == GVSPLINES)` guards the whole edge loop; layouts that
226
+ // never route (patchwork, osage) leave State at GVBEGIN.
227
+ if (getState() !== GVSPLINES) continue;
225
228
  for (const e of g.edges) {
226
229
  if (e.tail !== v) continue;
227
230
  // C map_edge: a spline-less (non-concentrate, non-IGNORED) edge was