@knowvah/dot-engine 2.6.8 → 2.6.10

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knowvah/dot-engine",
3
- "version": "2.6.8",
3
+ "version": "2.6.10",
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",
@@ -18,7 +18,7 @@ import { FillType, PenType } from '../gvc/context.js';
18
18
  import { transformPoint } from '../gvc/device.js';
19
19
  import { resolveRenderColor } from '../render/color-resolve.js';
20
20
  import { emitRoundedBezier } from './poly-shapes.js';
21
- import { parseStyleFlags } from './style-resolve.js';
21
+ import { htmlStyle } from './htmltable-style.js';
22
22
  import {
23
23
  BORDER_LEFT,
24
24
  BORDER_TOP,
@@ -94,7 +94,7 @@ export interface HtmlPaint {
94
94
  pen?: string;
95
95
  /** Pen width; default 1.0 (omitted from SVG at 1.0). */
96
96
  penWidth?: number;
97
- /** Style string; "dashed"/"dotted" map to the pen type. */
97
+ /** HTML STYLE string; its dashed/dotted flags map to the pen type. */
98
98
  penStyle?: string;
99
99
  /**
100
100
  * Second/stop color of a gradient fill. Presence switches the fill from
@@ -137,10 +137,11 @@ export function resetHtmlFillPenWidth(): void {
137
137
  htmlFillPen = 1.0;
138
138
  }
139
139
 
140
- /** Map a style string to the C pen type. @see lib/common/htmltable.c:doBorder */
140
+ /** Map an HTML STYLE to the C pen type: dashed wins over dotted. @see lib/common/htmltable.c:doBorder */
141
141
  function penTypeOf(style: string | undefined): PenType {
142
- if (style !== undefined && style.includes('dashed')) return PenType.Dashed;
143
- if (style !== undefined && style.includes('dotted')) return PenType.Dotted;
142
+ const st = htmlStyle(style);
143
+ if (st.dashed) return PenType.Dashed;
144
+ if (st.dotted) return PenType.Dotted;
144
145
  return PenType.Solid;
145
146
  }
146
147
 
@@ -277,7 +278,7 @@ export function doBorder(
277
278
  // value persists and leaks into the next bgcolor fill's stroke-width.
278
279
  htmlFillPen = border;
279
280
  withHtmlPaint({ pen: color, penWidth: border, penStyle: style }, job, () => {
280
- if (parseStyleFlags(style).rounded) {
281
+ if (htmlStyle(style).rounded) {
281
282
  // C: round_corners(job, mkPts(AF,b,border), 4, {rounded}, 0) — unfilled
282
283
  // rounded border path; emitRoundedBezier transforms the AF ring itself.
283
284
  emitRoundedBezier(mkPts(box, border, pos), { x: 0, y: 0 }, false, { renderer, job });
@@ -35,7 +35,7 @@ import {
35
35
  htmlFillPenWidth, resetHtmlFillPenWidth, type HtmlPaint,
36
36
  } from './htmltable-emit-fill.js';
37
37
  import { emitRoundedBezier } from './poly-shapes.js';
38
- import { parseStyleFlags } from './style-resolve.js';
38
+ import { htmlStyle } from './htmltable-style.js';
39
39
  import { emitHtmlRules, initHtmlAnchor, endHtmlAnchor, htmlObjImgscale } from './htmltable-emit-rules.js';
40
40
 
41
41
  export type { PlacedHtml, PlacedCell, PlacedLine, PlacedImage };
@@ -125,7 +125,7 @@ function bgFillPaint(ctx: BgFillCtx): HtmlPaint {
125
125
  if (spec === null) return { fill: ctx.bgcolor, penWidth };
126
126
  return {
127
127
  fill: spec[0], stop: spec[1], gradientAngle: ctx.gradientangle ?? 0,
128
- radial: ctx.style !== undefined && ctx.style.includes('radial'), penWidth,
128
+ radial: htmlStyle(ctx.style).radial, penWidth,
129
129
  };
130
130
  }
131
131
 
@@ -139,7 +139,7 @@ function bgFillPaint(ctx: BgFillCtx): HtmlPaint {
139
139
  function emitBgFill(ctx: BgFillCtx): void {
140
140
  const { box, pos, border, renderer, job } = ctx;
141
141
  withHtmlPaint(bgFillPaint(ctx), job, () => {
142
- if (parseStyleFlags(ctx.style).rounded) {
142
+ if (htmlStyle(ctx.style).rounded) {
143
143
  emitRoundedBezier(mkPts(box, border, pos), { x: 0, y: 0 }, true, { renderer, job });
144
144
  return;
145
145
  }
@@ -258,23 +258,6 @@ export function emitHtmlImg(
258
258
  *
259
259
  * @see lib/common/htmltable.c:emit_html_cell
260
260
  */
261
- /**
262
- * True when an HTML element's STYLE declares it invisible.
263
- *
264
- * HTML STYLE is lexed by its OWN parser, not the node/edge one: tokens are
265
- * split on space or comma, compared case-insensitively, and BOTH "INVIS" and
266
- * "INVISIBLE" set the flag. (The node/edge parser splits on comma only and
267
- * matches exact-case, so it cannot be reused here.)
268
- * @see lib/common/htmllex.c:168 (DELIM " ,", strview_case_str_eq)
269
- */
270
- function htmlStyleInvisible(style: string | undefined): boolean {
271
- if (style === undefined) return false;
272
- return style.split(/[ ,]+/).some((tok) => {
273
- const t = tok.toUpperCase();
274
- return t === 'INVIS' || t === 'INVISIBLE';
275
- });
276
- }
277
-
278
261
  export function emitHtmlCell(
279
262
  cell: PlacedCell,
280
263
  pos: Point,
@@ -287,7 +270,7 @@ export function emitHtmlCell(
287
270
  );
288
271
  // An invisible cell emits no fill, no border and no content — only the
289
272
  // anchor, which C opens before the gate. @see htmltable.c:emit_html_cell:643
290
- if (htmlStyleInvisible(cell.style)) {
273
+ if (htmlStyle(cell.style).invisible) {
291
274
  if (inAnchor) endHtmlAnchor(renderer, job);
292
275
  return;
293
276
  }
@@ -354,7 +337,7 @@ export function emitHtmlLabel(
354
337
  const inAnchor = clustersLast ? false : initHtmlAnchor(anchorData, tblBox, renderer, job);
355
338
  // An invisible TABLE suppresses its fill, ALL its cells, and its rules and
356
339
  // border — the whole block C guards at emit_html_tbl:542.
357
- const tblInvisible = htmlStyleInvisible(placed.style);
340
+ const tblInvisible = htmlStyle(placed.style).invisible;
358
341
  if (!tblInvisible) {
359
342
  if (placed.bgcolor !== undefined) {
360
343
  emitBgFill({ bgcolor: placed.bgcolor, box: placed.box, pos, border: placed.border,
@@ -0,0 +1,38 @@
1
+ // SPDX-License-Identifier: EPL-2.0
2
+ /**
3
+ * The HTML STYLE flags. C's stylefn sets an htmlstyle_t bitfield at lex time;
4
+ * the port keeps the legal tokens as a string (htmltable-attr-warn.ts joins
5
+ * them with ' ') and reads the flags back here, token by token in order, so
6
+ * SOLID still clears only the DOTTED/DASHED before it. HTML STYLE is not the
7
+ * node/edge style: tokens split on space or comma and compare
8
+ * case-insensitively, so `parseStyleFlags` cannot read it.
9
+ *
10
+ * @see lib/common/htmltable.h:htmlstyle_t
11
+ * @see lib/common/htmllex.c:stylefn (DELIM " ,", strview_case_str_eq)
12
+ */
13
+
14
+ /** @see lib/common/htmltable.h:htmlstyle_t */
15
+ export interface HtmlStyle {
16
+ radial: boolean;
17
+ rounded: boolean;
18
+ invisible: boolean;
19
+ dotted: boolean;
20
+ dashed: boolean;
21
+ }
22
+
23
+ /** Read the htmlstyle_t flags from a kept HTML STYLE string. */
24
+ export function htmlStyle(style: string | undefined): HtmlStyle {
25
+ const s: HtmlStyle = { radial: false, rounded: false, invisible: false, dotted: false, dashed: false };
26
+ if (style === undefined) return s;
27
+ for (const tk of style.split(/[ ,]+/)) {
28
+ switch (tk.toUpperCase()) {
29
+ case 'ROUNDED': s.rounded = true; break;
30
+ case 'RADIAL': s.radial = true; break;
31
+ case 'SOLID': s.dotted = false; s.dashed = false; break;
32
+ case 'INVISIBLE': case 'INVIS': s.invisible = true; break;
33
+ case 'DOTTED': s.dotted = true; break;
34
+ case 'DASHED': s.dashed = true; break;
35
+ }
36
+ }
37
+ return s;
38
+ }
@@ -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
  }