nicevg 0.15.1 → 0.16.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.
package/README.md CHANGED
@@ -59,13 +59,50 @@ console.log(svg.changes); // 何を直し、何を動かしたか
59
59
  | `nodes[].label`、`fontSize` | 中に書く文字(`\n` で改行)。子を持つノードでは左上に、それ以外では中央に置く |
60
60
  | `nodes[].parent` | このノードを入れる親ノードの id |
61
61
  | `nodes[].allowOverlap` | 意図した重なりなら `true` |
62
- | `connectors[].from`、`to` | 両端のノードの id |
63
- | `connectors[].points` | 経路の折れ点。省くと、修正と配置調整が両端のノードの辺から辺へ経路を引く |
64
- | `connectors[].label`、`fontSize` | コネクタの脇に置く文字 |
62
+ | `nodes[].class` | 描き方を `style` から選ぶ名前。付けたノードは `width`・`height`・`shape`・`label` を使わず、`style` が描く |
63
+ | `edges[].from`、`to` | 両端のノードの id。`edges` は前の名前の `connectors` でも渡せる(両方は渡せない) |
64
+ | `edges[].points` | 経路の折れ点。省くと、修正と配置調整が両端のノードの辺から辺へ経路を引く |
65
+ | `edges[].label`、`fontSize` | コネクタの脇に置く文字 |
66
+ | `style` | クラス名から、そのクラスのノードを描く関数への対応 |
67
+ | `rootAttrs` | ルートの `<svg>` に付ける属性(`{ "font-family": "'Yu Gothic', sans-serif" }` など)。中の文字はこれを継承する。`xmlns`・`viewBox`・`width`・`height` は nicevg が書くので渡せない |
68
+
69
+ ### style
70
+
71
+ `style` の関数はノードを受け取り、ノードの左上を原点にして描いた SVG を `svg` で返す。ノードに足した項目(下の `no` など)も受け取れる。置く位置は `render` がノードの `x`、`y` と配置調整の移動から `translate` で付ける。
72
+
73
+ ```ts
74
+ import { render, svg, text } from "nicevg";
75
+
76
+ const image = render({
77
+ nodes: [
78
+ { id: "customer", x: 20, y: 40, width: 80, height: 40, label: "顧客" },
79
+ { id: "orders", class: "store", x: 260, y: 40, no: "D1", label: "注文" },
80
+ ],
81
+ edges: [{ from: "customer", to: "orders", label: "注文" }],
82
+ style: {
83
+ store: ({ no, label }) => {
84
+ const id = text(String(no)), name = text(String(label));
85
+ const sep = id.width + 24, w = sep + name.width + 24, h = Math.max(id.height, name.height) + 20;
86
+ return svg`
87
+ <g fill="none" stroke="#333">
88
+ <path d="M ${w} 0 H 0 V ${h} H ${w}"/>
89
+ <line x1="${sep}" y1="0" x2="${sep}" y2="${h}"/>
90
+ </g>
91
+ ${id.at({ cx: sep / 2, cy: h / 2 })}
92
+ ${name.at({ left: sep + 12, cy: h / 2 })}`;
93
+ },
94
+ },
95
+ });
96
+ ```
97
+
98
+ - `` svg`...` `` の中は SVG そのもの。埋め込んだ文字列はエスケープされ、`svg` の断片、`text`、それらの配列はそのまま入る。断片は `.at(x, y)` で原点をずらせる。
99
+ - `text(文字列, 大きさ)` は 1 行の `<text>`。`width`・`height` は nicevg と同じ見積もり。`.at({ 横, 縦 })` で、横は `left`・`cx`・`right`、縦は `top`・`cy`・`bottom` から 1 つずつ選び、文字の箱のその点をその座標に置く。
100
+ - ノードの範囲は、描いたもの(`rect`、`circle`、`ellipse`、`line`、`polyline`、`polygon`、`path`、`text`)を囲む矩形。コネクタはその辺から出入りする。曲線と円弧は端点だけで数える。
101
+ - 修正と配置調整はノードを動かすが、大きさは変えない。中の `<text>` はラベルとして検査しないので、収まりは描く側が決める。
65
102
 
66
103
  `Svg` の `report.diagram` は、描いた図を読み直したもので、ノード(`bounds`、ラベルの範囲 `labelBounds`)、コネクタ(`points`)、ラベル(`bounds`、説明するコネクタの id `connector`)を持つ。コネクタは id を持たないので、`from` と `to` で見分ける。文字の幅は、全角を 1 文字あたりフォントサイズ、それ以外を 0.59 倍として見積もる。
67
104
 
68
- 形の書き方が違う入力や、存在しないノードを指す `from`・`to`・`parent` は、どこが何を指しているか(`connectors[0].to` など)を書いた `NicevgError` を投げる。
105
+ 形の書き方が違う入力、存在しないノードを指す `from`・`to`・`parent`、`style` にないクラス、整形式でない SVG を描いたスタイルは、どこが何を指しているか(`connectors[0].to`、`nodes[1].class` など)を書いた `NicevgError` を投げる。
69
106
 
70
107
  描画は呼んだスレッドで最後まで走る。大きな図の配置調整は 1〜2 秒かかり、その間すべてのコアを使う。
71
108
 
package/dist/draw.d.ts ADDED
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Drawing a node's picture for `render()`'s `style`, as SVG written out:
3
+ *
4
+ * ```ts
5
+ * const store = ({ no, label }) => {
6
+ * const id = text(no), name = text(label);
7
+ * const sep = id.width + 24, w = sep + name.width + 24, h = Math.max(id.height, name.height) + 20;
8
+ * return svg`
9
+ * <path d="M ${w} 0 H 0 V ${h} H ${w}" fill="none" stroke="#333"/>
10
+ * <line x1="${sep}" y1="0" x2="${sep}" y2="${h}" stroke="#333"/>
11
+ * ${id.at({ cx: sep / 2, cy: h / 2 })}
12
+ * ${name.at({ left: sep + 12, cy: h / 2 })}`;
13
+ * };
14
+ * ```
15
+ */
16
+ /** SVG written with `svg`: embedded as it is, where strings are escaped. */
17
+ export declare class SvgFragment {
18
+ #private;
19
+ constructor(markup: string);
20
+ toString(): string;
21
+ /** This fragment with its origin moved to `x`, `y`. */
22
+ at(x: number, y: number): SvgFragment;
23
+ }
24
+ /** SVG markup with values embedded: strings escaped, fragments, texts and arrays of them as they are. */
25
+ export declare function svg(strings: TemplateStringsArray, ...values: unknown[]): SvgFragment;
26
+ /** Where a text goes: one of `left`, `cx`, `right` across, one of `top`, `cy`, `bottom` down. */
27
+ export type TextPlace = ({
28
+ left: number;
29
+ } | {
30
+ cx: number;
31
+ } | {
32
+ right: number;
33
+ }) & ({
34
+ top: number;
35
+ } | {
36
+ cy: number;
37
+ } | {
38
+ bottom: number;
39
+ });
40
+ /** A line of text, sized the way nicevg measures text, placed with `at`. */
41
+ export declare class Text {
42
+ readonly content: string;
43
+ readonly size: number;
44
+ readonly place: TextPlace;
45
+ /** Its width, wide characters one font size each and others 0.59. */
46
+ readonly width: number;
47
+ /** Its line's height, 1.2 font sizes. */
48
+ readonly height: number;
49
+ constructor(content: string, size?: number, place?: TextPlace);
50
+ /** This text with the point of its box named in `place` at the coordinates given. */
51
+ at(place: TextPlace): Text;
52
+ /** The `<text>` element, centred on its line by `dominant-baseline`. */
53
+ toString(): string;
54
+ }
55
+ /** A line of text at `size` (14 by default), to embed in `svg` once placed with `at`. */
56
+ export declare function text(content: string, size?: number): Text;
package/dist/draw.js ADDED
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Drawing a node's picture for `render()`'s `style`, as SVG written out:
3
+ *
4
+ * ```ts
5
+ * const store = ({ no, label }) => {
6
+ * const id = text(no), name = text(label);
7
+ * const sep = id.width + 24, w = sep + name.width + 24, h = Math.max(id.height, name.height) + 20;
8
+ * return svg`
9
+ * <path d="M ${w} 0 H 0 V ${h} H ${w}" fill="none" stroke="#333"/>
10
+ * <line x1="${sep}" y1="0" x2="${sep}" y2="${h}" stroke="#333"/>
11
+ * ${id.at({ cx: sep / 2, cy: h / 2 })}
12
+ * ${name.at({ left: sep + 12, cy: h / 2 })}`;
13
+ * };
14
+ * ```
15
+ */
16
+ const escape = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
17
+ /** SVG written with `svg`: embedded as it is, where strings are escaped. */
18
+ export class SvgFragment {
19
+ #markup;
20
+ constructor(markup) {
21
+ this.#markup = markup;
22
+ }
23
+ toString() {
24
+ return this.#markup;
25
+ }
26
+ /** This fragment with its origin moved to `x`, `y`. */
27
+ at(x, y) {
28
+ return svg `<g transform="translate(${x} ${y})">${this}</g>`;
29
+ }
30
+ }
31
+ function embed(value) {
32
+ if (value instanceof SvgFragment || value instanceof Text) {
33
+ return value.toString();
34
+ }
35
+ if (Array.isArray(value)) {
36
+ return value.map(embed).join("");
37
+ }
38
+ return escape(String(value));
39
+ }
40
+ /** SVG markup with values embedded: strings escaped, fragments, texts and arrays of them as they are. */
41
+ export function svg(strings, ...values) {
42
+ return new SvgFragment(strings.reduce((markup, string, index) => markup + embed(values[index - 1]) + string));
43
+ }
44
+ // shortcut: the common wide ranges (CJK, kana, hangul, fullwidth forms), not every East Asian Wide character; widen it if a script measures short.
45
+ const WIDE = /[ᄀ-ᅟ⺀-〾ぁ-㏿㐀-䶿一-鿿ꀀ-꓏가-힣豈-﫿︰-﹏＀-⦆¢-₩]|[\u{20000}-\u{3fffd}]/u;
46
+ /** A line of text, sized the way nicevg measures text, placed with `at`. */
47
+ export class Text {
48
+ content;
49
+ size;
50
+ place;
51
+ /** Its width, wide characters one font size each and others 0.59. */
52
+ width;
53
+ /** Its line's height, 1.2 font sizes. */
54
+ height;
55
+ constructor(content, size = 14, place = { left: 0, top: 0 }) {
56
+ this.content = content;
57
+ this.size = size;
58
+ this.place = place;
59
+ let width = 0;
60
+ for (const character of content) {
61
+ width += WIDE.test(character) ? size : character.length * size * 0.59;
62
+ }
63
+ this.width = Math.ceil(width);
64
+ this.height = Math.ceil(size * 1.2);
65
+ }
66
+ /** This text with the point of its box named in `place` at the coordinates given. */
67
+ at(place) {
68
+ const across = ["left", "cx", "right"].filter((key) => key in place);
69
+ const down = ["top", "cy", "bottom"].filter((key) => key in place);
70
+ if (across.length !== 1 || down.length !== 1) {
71
+ throw new TypeError(`text "${this.content}": at() takes one of left, cx, right and one of top, cy, bottom, not ${Object.keys(place).join(", ") || "none"}`);
72
+ }
73
+ return new Text(this.content, this.size, place);
74
+ }
75
+ /** The `<text>` element, centred on its line by `dominant-baseline`. */
76
+ toString() {
77
+ const place = this.place;
78
+ const [x, anchor] = "cx" in place ? [place.cx, "middle"] : "right" in place ? [place.right, "end"] : [place.left, "start"];
79
+ const y = "cy" in place ? place.cy : "top" in place ? place.top + this.height / 2 : place.bottom - this.height / 2;
80
+ return `<text x="${x}" y="${y}" font-size="${this.size}" text-anchor="${anchor}" dominant-baseline="central">${escape(this.content)}</text>`;
81
+ }
82
+ }
83
+ /** A line of text at `size` (14 by default), to embed in `svg` once placed with `at`. */
84
+ export function text(content, size = 14) {
85
+ return new Text(content, size);
86
+ }
87
+ //# sourceMappingURL=draw.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"draw.js","sourceRoot":"","sources":["../src/draw.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -20,6 +20,8 @@
20
20
  * Rendering runs on the calling thread and blocks it until done; arranging
21
21
  * a large diagram takes a second or two, using every core.
22
22
  */
23
+ import type { SvgFragment } from "./draw.ts";
24
+ export { SvgFragment, Text, type TextPlace, svg, text } from "./draw.ts";
23
25
  export interface Point {
24
26
  x: number;
25
27
  y: number;
@@ -60,10 +62,37 @@ export interface SketchConnector {
60
62
  label?: string;
61
63
  fontSize?: number;
62
64
  }
63
- /** A diagram as nodes and the connectors between them. */
65
+ /**
66
+ * A node drawn by the style its `class` names, with its origin at `x`, `y`.
67
+ * It lies around what the style draws, and rendering moves it but never
68
+ * resizes it. Its other fields, such as `label`, are for the style to use.
69
+ */
70
+ export interface StyledNode {
71
+ id: string;
72
+ x: number;
73
+ y: number;
74
+ class: string;
75
+ /** The node this one sits inside. */
76
+ parent?: string;
77
+ /** Whether it may overlap other nodes on purpose. */
78
+ allowOverlap?: boolean;
79
+ [field: string]: unknown;
80
+ }
81
+ /** Draws a styled node's picture, from its own origin, with `svg` and `text`. */
82
+ export type Style = (node: StyledNode) => SvgFragment;
83
+ /** A diagram as nodes and the edges (connectors) between them. */
64
84
  export interface Sketch {
65
- nodes: SketchNode[];
85
+ nodes: (SketchNode | StyledNode)[];
86
+ edges?: SketchConnector[];
87
+ /** The same as `edges`, its name before; pass one or the other. */
66
88
  connectors?: SketchConnector[];
89
+ /** How to draw the nodes of each class. */
90
+ style?: Record<string, Style>;
91
+ /**
92
+ * Attributes for the root `<svg>`, such as `font-family`, which its texts
93
+ * inherit. nicevg writes `xmlns`, `viewBox`, `width` and `height` itself.
94
+ */
95
+ rootAttrs?: Record<string, string | number>;
67
96
  }
68
97
  /** A diagram to render: a sketch, SVG text following nicevg's SVG contract, or an Svg rendered before. */
69
98
  export type Input = Sketch | {
package/dist/index.js CHANGED
@@ -22,6 +22,7 @@
22
22
  */
23
23
  import { writeFile } from "node:fs/promises";
24
24
  import { call as native } from "./native.js";
25
+ export { SvgFragment, Text, svg, text } from "./draw.js";
25
26
  /** A rendered diagram as SVG, with what rendering changed and the diagram read back from it. */
26
27
  export class Svg {
27
28
  changes;
@@ -55,11 +56,32 @@ export class NicevgError extends Error {
55
56
  * no points, and moves nodes a little where that reads better.
56
57
  */
57
58
  export function render(input) {
58
- const request = new TextEncoder().encode(JSON.stringify(input instanceof Svg ? { svg: input.toString() } : input));
59
+ const request = new TextEncoder().encode(JSON.stringify(input instanceof Svg ? { svg: input.toString() } : "svg" in input ? input : sketchRequest(input)));
59
60
  const reply = JSON.parse(native(request));
60
61
  if ("error" in reply) {
61
62
  throw new NicevgError(reply.error);
62
63
  }
63
64
  return new Svg(reply.ok.svg, reply.ok.changes, reply.ok.report);
64
65
  }
66
+ /** The sketch as the native library reads it: styled nodes drawn, edges as connectors. */
67
+ function sketchRequest({ nodes, edges, connectors, style = {}, rootAttrs }) {
68
+ if (edges && connectors) {
69
+ throw new NicevgError("edges and connectors are the same; pass one of them");
70
+ }
71
+ return {
72
+ nodes: nodes.map((node, index) => {
73
+ if (!("class" in node)) {
74
+ return node;
75
+ }
76
+ const draw = Object.hasOwn(style, node.class) ? style[node.class] : undefined;
77
+ if (!draw) {
78
+ throw new NicevgError(`nodes[${index}].class: no style named "${node.class}"`);
79
+ }
80
+ const { id, x, y, parent, allowOverlap } = node;
81
+ return { id, x, y, parent, allowOverlap, svg: String(draw(node)) };
82
+ }),
83
+ connectors: edges ?? connectors,
84
+ rootAttrs: rootAttrs && Object.fromEntries(Object.entries(rootAttrs).map(([name, value]) => [name, String(value)])),
85
+ };
86
+ }
65
87
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAE7C,OAAO,EAAE,IAAI,IAAI,MAAM,EAAE,MAAM,aAAa,CAAC;AA0G7C,gGAAgG;AAChG,MAAM,OAAO,GAAG;IAMH;IAEA;IAPF,KAAK,CAAS;IAEvB,YACE,IAAY;IACZ,mDAAmD;IAC1C,OAAiB;IAC1B,8FAA8F;IACrF,MAAsB;QAFtB,YAAO,GAAP,OAAO,CAAU;QAEjB,WAAM,GAAN,MAAM,CAAgB;QAE/B,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;IACpB,CAAC;IAED,oBAAoB;IACpB,QAAQ;QACN,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IAED,qCAAqC;IACrC,KAAK,CAAC,IAAkB;QACtB,OAAO,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;IACrC,CAAC;CACF;AAED,mEAAmE;AACnE,MAAM,OAAO,WAAY,SAAQ,KAAK;IAC3B,IAAI,GAAG,aAAa,CAAC;CAC/B;AAED;;;;GAIG;AACH,MAAM,UAAU,MAAM,CAAC,KAAY;IACjC,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,YAAY,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;IACnH,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAEnB,CAAC;IACtB,IAAI,OAAO,IAAI,KAAK,EAAE,CAAC;QACrB,MAAM,IAAI,WAAW,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACrC,CAAC;IACD,OAAO,IAAI,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC;AAClE,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nicevg",
3
- "version": "0.15.1",
3
+ "version": "0.16.0",
4
4
  "description": "Deterministic checks and repairs for diagrams, given as shapes or SVG, on Bun and Node through native FFI.",
5
5
  "keywords": [
6
6
  "diagram",
package/src/draw.ts ADDED
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Drawing a node's picture for `render()`'s `style`, as SVG written out:
3
+ *
4
+ * ```ts
5
+ * const store = ({ no, label }) => {
6
+ * const id = text(no), name = text(label);
7
+ * const sep = id.width + 24, w = sep + name.width + 24, h = Math.max(id.height, name.height) + 20;
8
+ * return svg`
9
+ * <path d="M ${w} 0 H 0 V ${h} H ${w}" fill="none" stroke="#333"/>
10
+ * <line x1="${sep}" y1="0" x2="${sep}" y2="${h}" stroke="#333"/>
11
+ * ${id.at({ cx: sep / 2, cy: h / 2 })}
12
+ * ${name.at({ left: sep + 12, cy: h / 2 })}`;
13
+ * };
14
+ * ```
15
+ */
16
+
17
+ const escape = (value: string) =>
18
+ value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
19
+
20
+ /** SVG written with `svg`: embedded as it is, where strings are escaped. */
21
+ export class SvgFragment {
22
+ readonly #markup: string;
23
+
24
+ constructor(markup: string) {
25
+ this.#markup = markup;
26
+ }
27
+
28
+ toString(): string {
29
+ return this.#markup;
30
+ }
31
+
32
+ /** This fragment with its origin moved to `x`, `y`. */
33
+ at(x: number, y: number): SvgFragment {
34
+ return svg`<g transform="translate(${x} ${y})">${this}</g>`;
35
+ }
36
+ }
37
+
38
+ function embed(value: unknown): string {
39
+ if (value instanceof SvgFragment || value instanceof Text) {
40
+ return value.toString();
41
+ }
42
+ if (Array.isArray(value)) {
43
+ return value.map(embed).join("");
44
+ }
45
+ return escape(String(value));
46
+ }
47
+
48
+ /** SVG markup with values embedded: strings escaped, fragments, texts and arrays of them as they are. */
49
+ export function svg(strings: TemplateStringsArray, ...values: unknown[]): SvgFragment {
50
+ return new SvgFragment(strings.reduce((markup, string, index) => markup + embed(values[index - 1]) + string));
51
+ }
52
+
53
+ // shortcut: the common wide ranges (CJK, kana, hangul, fullwidth forms), not every East Asian Wide character; widen it if a script measures short.
54
+ const WIDE = /[ᄀ-ᅟ⺀-〾ぁ-㏿㐀-䶿一-鿿ꀀ-꓏가-힣豈-﫿︰-﹏＀-⦆¢-₩]|[\u{20000}-\u{3fffd}]/u;
55
+
56
+ /** Where a text goes: one of `left`, `cx`, `right` across, one of `top`, `cy`, `bottom` down. */
57
+ export type TextPlace = ({ left: number } | { cx: number } | { right: number }) &
58
+ ({ top: number } | { cy: number } | { bottom: number });
59
+
60
+ /** A line of text, sized the way nicevg measures text, placed with `at`. */
61
+ export class Text {
62
+ /** Its width, wide characters one font size each and others 0.59. */
63
+ readonly width: number;
64
+ /** Its line's height, 1.2 font sizes. */
65
+ readonly height: number;
66
+
67
+ constructor(
68
+ readonly content: string,
69
+ readonly size = 14,
70
+ readonly place: TextPlace = { left: 0, top: 0 },
71
+ ) {
72
+ let width = 0;
73
+ for (const character of content) {
74
+ width += WIDE.test(character) ? size : character.length * size * 0.59;
75
+ }
76
+ this.width = Math.ceil(width);
77
+ this.height = Math.ceil(size * 1.2);
78
+ }
79
+
80
+ /** This text with the point of its box named in `place` at the coordinates given. */
81
+ at(place: TextPlace): Text {
82
+ const across = ["left", "cx", "right"].filter((key) => key in place);
83
+ const down = ["top", "cy", "bottom"].filter((key) => key in place);
84
+ if (across.length !== 1 || down.length !== 1) {
85
+ throw new TypeError(
86
+ `text "${this.content}": at() takes one of left, cx, right and one of top, cy, bottom, not ${Object.keys(place).join(", ") || "none"}`,
87
+ );
88
+ }
89
+ return new Text(this.content, this.size, place);
90
+ }
91
+
92
+ /** The `<text>` element, centred on its line by `dominant-baseline`. */
93
+ toString(): string {
94
+ const place = this.place as Record<string, number>;
95
+ const [x, anchor] =
96
+ "cx" in place ? [place.cx!, "middle"] : "right" in place ? [place.right!, "end"] : [place.left!, "start"];
97
+ const y = "cy" in place ? place.cy! : "top" in place ? place.top! + this.height / 2 : place.bottom! - this.height / 2;
98
+ return `<text x="${x}" y="${y}" font-size="${this.size}" text-anchor="${anchor}" dominant-baseline="central">${escape(this.content)}</text>`;
99
+ }
100
+ }
101
+
102
+ /** A line of text at `size` (14 by default), to embed in `svg` once placed with `at`. */
103
+ export function text(content: string, size = 14): Text {
104
+ return new Text(content, size);
105
+ }
package/src/index.ts CHANGED
@@ -23,8 +23,11 @@
23
23
 
24
24
  import { writeFile } from "node:fs/promises";
25
25
 
26
+ import type { SvgFragment } from "./draw.ts";
26
27
  import { call as native } from "./native.ts";
27
28
 
29
+ export { SvgFragment, Text, type TextPlace, svg, text } from "./draw.ts";
30
+
28
31
  export interface Point {
29
32
  x: number;
30
33
  y: number;
@@ -69,10 +72,39 @@ export interface SketchConnector {
69
72
  fontSize?: number;
70
73
  }
71
74
 
72
- /** A diagram as nodes and the connectors between them. */
75
+ /**
76
+ * A node drawn by the style its `class` names, with its origin at `x`, `y`.
77
+ * It lies around what the style draws, and rendering moves it but never
78
+ * resizes it. Its other fields, such as `label`, are for the style to use.
79
+ */
80
+ export interface StyledNode {
81
+ id: string;
82
+ x: number;
83
+ y: number;
84
+ class: string;
85
+ /** The node this one sits inside. */
86
+ parent?: string;
87
+ /** Whether it may overlap other nodes on purpose. */
88
+ allowOverlap?: boolean;
89
+ [field: string]: unknown;
90
+ }
91
+
92
+ /** Draws a styled node's picture, from its own origin, with `svg` and `text`. */
93
+ export type Style = (node: StyledNode) => SvgFragment;
94
+
95
+ /** A diagram as nodes and the edges (connectors) between them. */
73
96
  export interface Sketch {
74
- nodes: SketchNode[];
97
+ nodes: (SketchNode | StyledNode)[];
98
+ edges?: SketchConnector[];
99
+ /** The same as `edges`, its name before; pass one or the other. */
75
100
  connectors?: SketchConnector[];
101
+ /** How to draw the nodes of each class. */
102
+ style?: Record<string, Style>;
103
+ /**
104
+ * Attributes for the root `<svg>`, such as `font-family`, which its texts
105
+ * inherit. nicevg writes `xmlns`, `viewBox`, `width` and `height` itself.
106
+ */
107
+ rootAttrs?: Record<string, string | number>;
76
108
  }
77
109
 
78
110
  /** A diagram to render: a sketch, SVG text following nicevg's SVG contract, or an Svg rendered before. */
@@ -165,7 +197,9 @@ export class NicevgError extends Error {
165
197
  * no points, and moves nodes a little where that reads better.
166
198
  */
167
199
  export function render(input: Input): Svg {
168
- const request = new TextEncoder().encode(JSON.stringify(input instanceof Svg ? { svg: input.toString() } : input));
200
+ const request = new TextEncoder().encode(
201
+ JSON.stringify(input instanceof Svg ? { svg: input.toString() } : "svg" in input ? input : sketchRequest(input)),
202
+ );
169
203
  const reply = JSON.parse(native(request)) as
170
204
  | { ok: { svg: string; changes: Change[]; report: AnalysisReport } }
171
205
  | { error: string };
@@ -174,3 +208,25 @@ export function render(input: Input): Svg {
174
208
  }
175
209
  return new Svg(reply.ok.svg, reply.ok.changes, reply.ok.report);
176
210
  }
211
+
212
+ /** The sketch as the native library reads it: styled nodes drawn, edges as connectors. */
213
+ function sketchRequest({ nodes, edges, connectors, style = {}, rootAttrs }: Sketch) {
214
+ if (edges && connectors) {
215
+ throw new NicevgError("edges and connectors are the same; pass one of them");
216
+ }
217
+ return {
218
+ nodes: nodes.map((node, index) => {
219
+ if (!("class" in node)) {
220
+ return node;
221
+ }
222
+ const draw = Object.hasOwn(style, node.class) ? style[node.class] : undefined;
223
+ if (!draw) {
224
+ throw new NicevgError(`nodes[${index}].class: no style named "${node.class}"`);
225
+ }
226
+ const { id, x, y, parent, allowOverlap } = node;
227
+ return { id, x, y, parent, allowOverlap, svg: String(draw(node)) };
228
+ }),
229
+ connectors: edges ?? connectors,
230
+ rootAttrs: rootAttrs && Object.fromEntries(Object.entries(rootAttrs).map(([name, value]) => [name, String(value)])),
231
+ };
232
+ }