@knowvah/dot-engine 2.7.0 → 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 (122) 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/attr-get.d.ts +30 -0
  5. package/dist/common/attr-get.d.ts.map +1 -0
  6. package/dist/common/fma.d.ts.map +1 -1
  7. package/dist/common/htmltable-emit-fill.d.ts.map +1 -1
  8. package/dist/common/htmltable-label.d.ts +3 -0
  9. package/dist/common/htmltable-label.d.ts.map +1 -1
  10. package/dist/common/htmltable-lex-attvalue.d.ts.map +1 -1
  11. package/dist/common/htmltable.d.ts +5 -0
  12. package/dist/common/htmltable.d.ts.map +1 -1
  13. package/dist/common/layers.d.ts +12 -0
  14. package/dist/common/layers.d.ts.map +1 -1
  15. package/dist/common/layout-state.d.ts +8 -0
  16. package/dist/common/layout-state.d.ts.map +1 -0
  17. package/dist/common/make-label.d.ts.map +1 -1
  18. package/dist/common/multicolor.d.ts +6 -3
  19. package/dist/common/multicolor.d.ts.map +1 -1
  20. package/dist/common/nodeinit.d.ts.map +1 -1
  21. package/dist/common/poly-init.d.ts +13 -12
  22. package/dist/common/poly-init.d.ts.map +1 -1
  23. package/dist/common/poly-sizing.d.ts +1 -0
  24. package/dist/common/poly-sizing.d.ts.map +1 -1
  25. package/dist/common/postproc.d.ts.map +1 -1
  26. package/dist/common/style-resolve.d.ts +11 -9
  27. package/dist/common/style-resolve.d.ts.map +1 -1
  28. package/dist/gvc/device-cluster.d.ts +5 -6
  29. package/dist/gvc/device-cluster.d.ts.map +1 -1
  30. package/dist/gvc/device.d.ts.map +1 -1
  31. package/dist/gvc/first-layer.d.ts +21 -5
  32. package/dist/gvc/first-layer.d.ts.map +1 -1
  33. package/dist/gvc/gvrender-set-style.d.ts.map +1 -1
  34. package/dist/gvc/usershape.d.ts +20 -0
  35. package/dist/gvc/usershape.d.ts.map +1 -1
  36. package/dist/index.js +512 -286
  37. package/dist/index.js.map +4 -4
  38. package/dist/layout/circo/index.d.ts.map +1 -1
  39. package/dist/layout/dot/graph-label.d.ts.map +1 -1
  40. package/dist/layout/dot/init.d.ts +10 -0
  41. package/dist/layout/dot/init.d.ts.map +1 -1
  42. package/dist/layout/dot/splines.d.ts.map +1 -1
  43. package/dist/layout/fdp/init.d.ts.map +1 -1
  44. package/dist/layout/neato/conjgrad-mkernel.d.ts.map +1 -1
  45. package/dist/layout/neato/constrained-majorization.d.ts.map +1 -1
  46. package/dist/layout/neato/edge-len.d.ts +6 -2
  47. package/dist/layout/neato/edge-len.d.ts.map +1 -1
  48. package/dist/layout/neato/init.d.ts.map +1 -1
  49. package/dist/layout/neato/kk.d.ts.map +1 -1
  50. package/dist/layout/neato/make-graph-data.d.ts.map +1 -1
  51. package/dist/layout/neato/matrix-ops.d.ts +0 -6
  52. package/dist/layout/neato/matrix-ops.d.ts.map +1 -1
  53. package/dist/layout/neato/smart-ini-x.d.ts.map +1 -1
  54. package/dist/layout/neato/splines.d.ts.map +1 -1
  55. package/dist/layout/neato/start.d.ts.map +1 -1
  56. package/dist/layout/neato/stress-kernel.d.ts +2 -5
  57. package/dist/layout/neato/stress-kernel.d.ts.map +1 -1
  58. package/dist/layout/neato/stress.d.ts +6 -0
  59. package/dist/layout/neato/stress.d.ts.map +1 -1
  60. package/dist/layout/osage/index.d.ts.map +1 -1
  61. package/dist/layout/patchwork/index.d.ts.map +1 -1
  62. package/dist/layout/sfdp/init.d.ts.map +1 -1
  63. package/dist/layout/twopi/init.d.ts +12 -6
  64. package/dist/layout/twopi/init.d.ts.map +1 -1
  65. package/dist/model/cgraph-ops.d.ts +9 -0
  66. package/dist/model/cgraph-ops.d.ts.map +1 -1
  67. package/dist/render/color-resolve.d.ts +7 -0
  68. package/dist/render/color-resolve.d.ts.map +1 -1
  69. package/dist/render/json.d.ts +17 -2
  70. package/dist/render/json.d.ts.map +1 -1
  71. package/dist/render/map.d.ts +3 -5
  72. package/dist/render/map.d.ts.map +1 -1
  73. package/dist/render.js +518 -292
  74. package/dist/render.js.map +4 -4
  75. package/package.json +1 -1
  76. package/src/common/agerr.ts +69 -8
  77. package/src/common/attr-get.ts +46 -0
  78. package/src/common/fma.ts +11 -1
  79. package/src/common/htmltable-emit-fill.ts +11 -7
  80. package/src/common/htmltable-label.ts +6 -3
  81. package/src/common/htmltable-lex-attvalue.ts +17 -1
  82. package/src/common/htmltable.ts +10 -5
  83. package/src/common/layers.ts +31 -2
  84. package/src/common/layout-state.ts +35 -0
  85. package/src/common/make-label.ts +20 -4
  86. package/src/common/multicolor.ts +12 -6
  87. package/src/common/nodeinit.ts +2 -1
  88. package/src/common/poly-init.ts +22 -19
  89. package/src/common/poly-sizing.ts +4 -4
  90. package/src/common/postproc.ts +4 -1
  91. package/src/common/style-resolve.ts +118 -50
  92. package/src/gvc/device-cluster.ts +31 -39
  93. package/src/gvc/device.ts +55 -43
  94. package/src/gvc/first-layer.ts +60 -11
  95. package/src/gvc/gvrender-set-style.ts +1 -6
  96. package/src/gvc/usershape.ts +40 -0
  97. package/src/layout/circo/index.ts +4 -0
  98. package/src/layout/dot/graph-label.ts +1 -15
  99. package/src/layout/dot/init.ts +32 -0
  100. package/src/layout/dot/splines.ts +3 -0
  101. package/src/layout/fdp/init.ts +17 -0
  102. package/src/layout/neato/conjgrad-mkernel.ts +2 -3
  103. package/src/layout/neato/constrained-majorization.ts +2 -1
  104. package/src/layout/neato/edge-len.ts +39 -7
  105. package/src/layout/neato/init.ts +8 -3
  106. package/src/layout/neato/kk.ts +2 -1
  107. package/src/layout/neato/make-graph-data.ts +1 -2
  108. package/src/layout/neato/matrix-ops.ts +1 -11
  109. package/src/layout/neato/smart-ini-x.ts +1 -1
  110. package/src/layout/neato/splines.ts +3 -1
  111. package/src/layout/neato/start.ts +3 -2
  112. package/src/layout/neato/stress-kernel.ts +45 -86
  113. package/src/layout/neato/stress.ts +19 -0
  114. package/src/layout/osage/index.ts +8 -1
  115. package/src/layout/patchwork/index.ts +4 -0
  116. package/src/layout/sfdp/init.ts +3 -0
  117. package/src/layout/twopi/init.ts +21 -9
  118. package/src/layout/twopi/pipeline.ts +3 -3
  119. package/src/model/cgraph-ops.ts +19 -0
  120. package/src/render/color-resolve.ts +17 -2
  121. package/src/render/json.ts +9 -10
  122. 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.0",
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 */
@@ -0,0 +1,46 @@
1
+ // SPDX-License-Identifier: EPL-2.0
2
+
3
+ /**
4
+ * Attribute reads with cgraph's `agxget` semantics: an object's own value,
5
+ * else the declared default it was created under. A leaf module (model types
6
+ * only) so label and shape code can share it without an import cycle.
7
+ *
8
+ * @see lib/cgraph/attr.c:agxget
9
+ */
10
+
11
+ import type { Node } from '../model/node.js';
12
+ import type { Graph } from '../model/graph.js';
13
+
14
+ /**
15
+ * Read a node attribute: the node's own value, else the node-attribute defaults
16
+ * (`node [...]`) captured from the node's declaring scope up to root *at the
17
+ * time the node was created*. The snapshot makes a `node [...]` declared after
18
+ * the node — at any scope — correctly not apply, and lets a cluster-scoped
19
+ * default reach nodes declared in that cluster, as the oracle does.
20
+ *
21
+ * Programmatically created nodes have no snapshot; they fall back to the live
22
+ * defaults of the passed graph `g` and root.
23
+ * @see lib/cgraph/attr.c:agxget; src/parser/builder.ts:effectiveNodeDefaults
24
+ */
25
+ export function nodeAttr(n: Node, g: Graph, key: string): string | undefined {
26
+ const own = n.attrs.get(key);
27
+ if (own !== undefined) return own;
28
+ if (n.nodeDefaultsSnapshot !== undefined) return n.nodeDefaultsSnapshot.get(key);
29
+ return g.nodeDefaults.get(key)
30
+ ?? (g !== g.root ? g.root.nodeDefaults.get(key) : undefined);
31
+ }
32
+
33
+ /**
34
+ * Read a graph attribute with subgraph→root inheritance, mirroring C's
35
+ * agxget (late_nnstring(sg, agfindgraphattr(sg, key), ...) in do_graph_label):
36
+ * a cluster inherits `fontname`/`fontsize`/`fontcolor` set on an ancestor
37
+ * (e.g. root `graph[fontname=Arial]`) when it sets none of its own.
38
+ * @see lib/common/input.c:do_graph_label
39
+ */
40
+ export function graphAttrInherited(sg: Graph, key: string): string | undefined {
41
+ // Own value wins; otherwise the ancestor defaults SNAPSHOTTED when this
42
+ // subgraph opened (not a live parent walk) — so an ancestor attribute set
43
+ // later in statement order does not retroactively apply (2184).
44
+ // @see lib/cgraph/graph.c:agsubg (parse-time defval copy)
45
+ return sg.attrs.get(key) ?? sg.graphDefaultsSnapshot?.get(key);
46
+ }
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
+ }
@@ -16,8 +16,11 @@ import { makeHtmlLabelTo } from './htmltable-label.js';
16
16
  import { labelStderr } from './htmltable-attr-warn.js';
17
17
  import { substObj, type GraphObj } from './subst.js';
18
18
  import { Edge } from '../model/edge.js';
19
+ import { Node } from '../model/node.js';
20
+ import { nodeAttr, graphAttrInherited } from './attr-get.js';
19
21
  import { Graph } from '../model/graph.js';
20
22
  import { htmlEntityUTF8 } from './html-entities.js';
23
+ import { usershapeDpi } from '../gvc/usershape.js';
21
24
 
22
25
  export const DEFAULT_FONTSIZE = 14.0;
23
26
  /** C's default fontname (non-Windows); renderers alias it, e.g. SVG
@@ -33,17 +36,27 @@ export { htmlEntityUTF8 };
33
36
  /**
34
37
  * The object's pen color: `pencolor` attr, else `color` attr, else undefined
35
38
  * (no default). An HTML table/cell border with no explicit COLOR inherits it.
39
+ * Read as C's agget does, so declared defaults count: a node's `node [...]`
40
+ * snapshot and a subgraph's inherited graph attributes (894589). Edge
41
+ * defaults are already materialized into `attrs` by the builder.
36
42
  * @see lib/common/htmltable.c:getPenColor
37
43
  */
38
44
  function getPenColor(obj?: GraphObj): string | undefined {
39
45
  if (obj === undefined) return undefined;
40
- const pc = obj.attrs.get('pencolor');
46
+ const pc = aggetObj(obj, 'pencolor');
41
47
  if (pc !== undefined && pc !== '') return pc;
42
- const c = obj.attrs.get('color');
48
+ const c = aggetObj(obj, 'color');
43
49
  if (c !== undefined && c !== '') return c;
44
50
  return undefined;
45
51
  }
46
52
 
53
+ /** `agget(obj, key)` for any object kind. @see lib/cgraph/attr.c:agxget */
54
+ function aggetObj(obj: GraphObj, key: string): string | undefined {
55
+ if (obj instanceof Node) return nodeAttr(obj, obj.root, key);
56
+ if (obj instanceof Graph) return graphAttrInherited(obj, key);
57
+ return obj.attrs.get(key);
58
+ }
59
+
47
60
  /** The root graph owning `obj` (edges resolve through their tail node). */
48
61
  function rootOf(obj: GraphObj): Graph {
49
62
  return obj instanceof Edge ? obj.tail.root : obj.root;
@@ -205,11 +218,14 @@ export function makeAnyLabel(
205
218
  // pen color so an HTML table/cell border with no explicit COLOR inherits
206
219
  // it (htmltable.c:1911 getPenColor). @see getPenColor
207
220
  // ADR-2: a per-context sizer (when set) wins over the global one.
208
- 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;
209
225
  // One stderr for the label: doInt's unterminated warning runs into the
210
226
  // next write, `in label of ...` included (agerr AGPREV).
211
227
  const stderr = labelStderr();
212
- 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);
213
229
  if (rv !== 0 && obj !== undefined) stderr.write(`${inLabelOf(obj)}\n`);
214
230
  stderr.flush();
215
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
  }
@@ -10,6 +10,7 @@
10
10
  import type { Node } from '../model/node.js';
11
11
  import type { Graph } from '../model/graph.js';
12
12
  import type { TextMeasurer } from './textmeasure.js';
13
+ import { nodeAttr } from './attr-get.js';
13
14
  import type { PolygonT, ShapeDesc } from './types.js';
14
15
  import { ShapeKind } from './types.js';
15
16
  import type { TextlabelT } from './types.js';
@@ -25,31 +26,16 @@ import {
25
26
  } from './make-label.js';
26
27
  import { computeVertices } from './poly-vertices.js';
27
28
  import { agwarningf } from './agerr.js';
28
- import { imageIsSupplied, imageOpenWarning } from '../gvc/usershape.js';
29
+ import { imageIsSupplied, imageOpenWarning, usershapeDpi, usershapeSizePts } from '../gvc/usershape.js';
29
30
 
30
31
  // Default half-dimensions in points (72 dpi, 0.75 in wide, 0.5 in tall)
31
32
  const DEFAULT_LW = 27;
32
33
  const DEFAULT_RW = 27;
33
34
  const DEFAULT_HT = 36;
34
35
 
35
- /**
36
- * Read a node attribute: the node's own value, else the node-attribute defaults
37
- * (`node [...]`) captured from the node's declaring scope up to root *at the
38
- * time the node was created*. The snapshot makes a `node [...]` declared after
39
- * the node — at any scope — correctly not apply, and lets a cluster-scoped
40
- * default reach nodes declared in that cluster, as the oracle does.
41
- *
42
- * Programmatically created nodes have no snapshot; they fall back to the live
43
- * defaults of the passed graph `g` and root.
44
- * @see lib/cgraph/attr.c:agxget; src/parser/builder.ts:effectiveNodeDefaults
45
- */
46
- export function nodeAttr(n: Node, g: Graph, key: string): string | undefined {
47
- const own = n.attrs.get(key);
48
- if (own !== undefined) return own;
49
- if (n.nodeDefaultsSnapshot !== undefined) return n.nodeDefaultsSnapshot.get(key);
50
- return g.nodeDefaults.get(key)
51
- ?? (g !== g.root ? g.root.nodeDefaults.get(key) : undefined);
52
- }
36
+ // nodeAttr moved to attr-get.ts (leaf module, shared with make-label.ts);
37
+ // re-exported so existing importers are unchanged.
38
+ export { nodeAttr };
53
39
 
54
40
  /** Read label-font attributes from node, falling back to defaults. */
55
41
  export function readFontAttrs(n: Node, g: Graph): {
@@ -128,6 +114,23 @@ function warnMissingImage(n: Node, g: Graph, shapefile: string | undefined): voi
128
114
  agwarningf(`No or improper image="${image}" for node "${n.name}"`);
129
115
  }
130
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
+
131
134
  export function polyInit(n: Node, g: Graph, measurer: TextMeasurer): void {
132
135
  const shapeName = nodeAttr(n, g, 'shape') ?? 'ellipse';
133
136
  const shapefile = nodeAttr(n, g, 'shapefile');