nicevg 0.15.1 → 0.17.1
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 +69 -79
- package/dist/draw.d.ts +56 -0
- package/dist/draw.js +87 -0
- package/dist/draw.js.map +1 -0
- package/dist/index.d.ts +40 -5
- package/dist/index.js +25 -1
- package/dist/index.js.map +1 -1
- package/native/darwin-arm64/libnicevg_ffi.dylib +0 -0
- package/native/darwin-x64/libnicevg_ffi.dylib +0 -0
- package/native/linux-arm64/libnicevg_ffi.so +0 -0
- package/native/linux-x64/libnicevg_ffi.so +0 -0
- package/native/win32-x64/nicevg_ffi.dll +0 -0
- package/package.json +1 -1
- package/src/draw.ts +105 -0
- package/src/index.ts +69 -6
package/README.md
CHANGED
|
@@ -1,102 +1,92 @@
|
|
|
1
|
-
# nicevg
|
|
1
|
+
# nicevg
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/nicevg)
|
|
4
|
+
[](https://github.com/takagiy/nicevg/actions/workflows/ci.yml)
|
|
5
|
+
[](https://github.com/takagiy/nicevg/blob/main/LICENSE)
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
An SVG diagram tool for AI agents, for TypeScript and JavaScript on Bun and Node. It fixes a diagram's problems mechanically on the agent's behalf, and optimizes the layout while preserving the original intent.
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
npm install nicevg # または bun add nicevg
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
パッケージには、次の環境向けにビルド済みのネイティブライブラリが入っているので、インストール時にスクリプトは走らず、Rust も要らない。
|
|
12
|
-
|
|
13
|
-
| 環境 | 対象 |
|
|
9
|
+
| Before | After |
|
|
14
10
|
|---|---|
|
|
15
|
-
|
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
|  |  |
|
|
12
|
+
|
|
13
|
+
## Install
|
|
18
14
|
|
|
19
|
-
|
|
15
|
+
```sh
|
|
16
|
+
npm install nicevg
|
|
17
|
+
# or
|
|
18
|
+
bun add nicevg
|
|
19
|
+
```
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
## Example
|
|
22
22
|
|
|
23
23
|
```ts
|
|
24
|
-
import { render } from "nicevg";
|
|
24
|
+
import { render, svg, text } from "nicevg";
|
|
25
25
|
|
|
26
|
-
const
|
|
26
|
+
const image = render({
|
|
27
27
|
nodes: [
|
|
28
28
|
{ id: "order", x: 20, y: 20, width: 120, height: 56, label: "Order" },
|
|
29
|
-
{ id: "
|
|
30
|
-
{ id: "user", x: 40, y: 200, width: 80, height: 80, shape: "circle", label: "User" },
|
|
31
|
-
],
|
|
32
|
-
connectors: [
|
|
33
|
-
{ from: "order", to: "bill", label: "invoice" },
|
|
34
|
-
{ from: "user", to: "order" },
|
|
29
|
+
{ id: "orders", class: "pill", x: 300, y: 160, label: "Orders" },
|
|
35
30
|
],
|
|
31
|
+
edges: [{ from: "order", to: "orders", label: "save" }],
|
|
32
|
+
style: {
|
|
33
|
+
pill: ({ label }) => {
|
|
34
|
+
const name = text(label);
|
|
35
|
+
const w = name.width + 32, h = name.height + 20;
|
|
36
|
+
return svg`
|
|
37
|
+
<rect width="${w}" height="${h}" rx="${h / 2}" fill="white" stroke="black"/>
|
|
38
|
+
${name.at({ cx: w / 2, cy: h / 2 })}`;
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
rootAttrs: { "font-family": "sans-serif" },
|
|
36
42
|
});
|
|
37
|
-
|
|
38
|
-
await svg.write("diagram.svg"); // ファイルに書く
|
|
39
|
-
const text = svg.toString(); // SVG の文字列(`${svg}` でも同じ)
|
|
40
|
-
|
|
41
|
-
for (const node of svg.report.diagram.nodes) {
|
|
42
|
-
console.log(node.id, node.bounds); // 動いた後の位置と大きさ
|
|
43
|
-
}
|
|
44
|
-
for (const connector of svg.report.diagram.connectors) {
|
|
45
|
-
console.log(connector.from, connector.to, connector.points); // 両端のノードと、直交する経路の折れ点
|
|
46
|
-
}
|
|
47
|
-
console.log(svg.report.issues); // 残った問題(なければ空)
|
|
48
|
-
console.log(svg.changes); // 何を直し、何を動かしたか
|
|
43
|
+
await image.write("diagram.svg");
|
|
49
44
|
```
|
|
50
45
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
### Sketch
|
|
54
|
-
|
|
55
|
-
| 項目 | 内容 |
|
|
56
|
-
|---|---|
|
|
57
|
-
| `nodes[].id`、`x`、`y`、`width`、`height` | ノードの id と、左上の位置と大きさ |
|
|
58
|
-
| `nodes[].shape` | `"rect"`(既定)か `"circle"`。円は外接する正方形を `x`〜`height` で渡す |
|
|
59
|
-
| `nodes[].label`、`fontSize` | 中に書く文字(`\n` で改行)。子を持つノードでは左上に、それ以外では中央に置く |
|
|
60
|
-
| `nodes[].parent` | このノードを入れる親ノードの id |
|
|
61
|
-
| `nodes[].allowOverlap` | 意図した重なりなら `true` |
|
|
62
|
-
| `connectors[].from`、`to` | 両端のノードの id |
|
|
63
|
-
| `connectors[].points` | 経路の折れ点。省くと、修正と配置調整が両端のノードの辺から辺へ経路を引く |
|
|
64
|
-
| `connectors[].label`、`fontSize` | コネクタの脇に置く文字 |
|
|
46
|
+
## API
|
|
65
47
|
|
|
66
|
-
|
|
48
|
+
### `render(input)`
|
|
67
49
|
|
|
68
|
-
|
|
50
|
+
Fixes the diagram, nudges its nodes into a cleaner layout and returns an `Svg`. `input` is a sketch (below), `{ svg: string }` with SVG text, or an `Svg` returned before. Bad input throws a `NicevgError` that says which field is wrong, such as `edges[0].to` or `nodes[1].class`.
|
|
69
51
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
```sh
|
|
77
|
-
bun install
|
|
78
|
-
bun run build # cargo でネイティブライブラリを作って native/ に置き、tsc で src/ を dist/ に書き出す
|
|
79
|
-
bun run test # Bun で src/ を、Node でビルドした dist/ をテストする
|
|
80
|
-
bun run typecheck
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
パッケージは Bun には `src/` の TypeScript を、Node には `dist/` の JavaScript と型定義を渡す(`package.json` の `exports`)。ネイティブライブラリは `NICEVG_LIBRARY` 環境変数、`native/<platform>-<arch>/`(`bun run build` が置く場所で、公開するパッケージでは全環境ぶん)、リポジトリの `target/release/` の順に探す。
|
|
52
|
+
| Option | |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `nodes` | The boxes, circles and styled nodes (below). |
|
|
55
|
+
| `edges` | The lines between nodes (below). `connectors` is accepted as its old name. |
|
|
56
|
+
| `style` | For each `class` used by a node, a function drawing that node with `svg` and `text`. |
|
|
57
|
+
| `rootAttrs` | Attributes for the root `<svg>`, such as `{ "font-family": "sans-serif" }`. |
|
|
84
58
|
|
|
85
|
-
|
|
59
|
+
| Node field | |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `id`, `x`, `y` | Its id and its top left corner. |
|
|
62
|
+
| `width`, `height` | Its size. Not used with `class`. |
|
|
63
|
+
| `label`, `fontSize` | Its text, lines split at `\n`. |
|
|
64
|
+
| `shape` | `"rect"` (the default) or `"circle"`. |
|
|
65
|
+
| `parent` | The id of the node it sits inside. |
|
|
66
|
+
| `class` | The `style` that draws it. Other fields you add are passed to that style. |
|
|
67
|
+
| `allowOverlap` | `true` if it overlaps other nodes on purpose. |
|
|
68
|
+
|
|
69
|
+
| Edge field | |
|
|
70
|
+
|---|---|
|
|
71
|
+
| `from`, `to` | The ids of the nodes it joins. |
|
|
72
|
+
| `label`, `fontSize` | Its text, placed beside it. |
|
|
73
|
+
| `points` | Its route. Without one, rendering routes it from side to side. |
|
|
86
74
|
|
|
87
|
-
|
|
75
|
+
### `Svg`
|
|
88
76
|
|
|
89
|
-
|
|
77
|
+
| Member | |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `write(path)` | Writes the SVG to a file. |
|
|
80
|
+
| `toString()` | The SVG text. |
|
|
81
|
+
| `report.issues` | Problems left after rendering; empty when there are none. |
|
|
82
|
+
| `report.diagram` | The diagram read back: `nodes` with their `bounds`, `connectors` with their `points`, and `labels`. |
|
|
83
|
+
| `changes` | What rendering fixed and moved, in order. |
|
|
90
84
|
|
|
91
|
-
|
|
92
|
-
2. それを手元に落として公開する(2FA のワンタイムコードを聞かれる)。手元からの公開なので、この版には配布元の証明が付かない。
|
|
93
|
-
```sh
|
|
94
|
-
gh run download <run-id> -n npm-package
|
|
95
|
-
npm publish ./nicevg-<version>.tgz
|
|
96
|
-
```
|
|
97
|
-
3. このリポジトリと `npm.yml` を Trusted Publisher に登録する(npmjs.com のパッケージの設定か、次のコマンド。どちらも 2FA が要る)。
|
|
98
|
-
```sh
|
|
99
|
-
npm trust github nicevg --file npm.yml --repo takagiy/nicevg --allow-publish
|
|
100
|
-
```
|
|
85
|
+
### Drawing a style
|
|
101
86
|
|
|
102
|
-
|
|
87
|
+
| Function | |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `` svg`...` `` | SVG markup. Embedded strings are escaped; `svg` fragments, texts and arrays of them go in as they are. |
|
|
90
|
+
| `text(content, size = 14)` | One line of text, with the `width` and `height` nicevg measures it at. |
|
|
91
|
+
| `text(...).at({ left \| cx \| right, top \| cy \| bottom })` | Places the text so that point of its box sits at those coordinates. |
|
|
92
|
+
| `` svg`...`.at(x, y) `` | The fragment with its origin moved to `x`, `y`. |
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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
|
package/dist/draw.js.map
ADDED
|
@@ -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,13 +62,46 @@ export interface SketchConnector {
|
|
|
60
62
|
label?: string;
|
|
61
63
|
fontSize?: number;
|
|
62
64
|
}
|
|
63
|
-
/**
|
|
64
|
-
|
|
65
|
-
|
|
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 extends StyledNode = StyledNode> = (node: Node) => SvgFragment;
|
|
83
|
+
/** A style for each class the nodes name, each given the nodes of its class. */
|
|
84
|
+
export type Styles<Node extends SketchNode | StyledNode> = {
|
|
85
|
+
[Class in Extract<Node, StyledNode>["class"]]: Style<Extract<Node, {
|
|
86
|
+
class: Class;
|
|
87
|
+
}>>;
|
|
88
|
+
};
|
|
89
|
+
/** A diagram as nodes and the edges (connectors) between them. */
|
|
90
|
+
export interface Sketch<Node extends SketchNode | StyledNode = SketchNode | StyledNode> {
|
|
91
|
+
nodes: readonly Node[];
|
|
92
|
+
edges?: SketchConnector[];
|
|
93
|
+
/** The same as `edges`, its name before; pass one or the other. */
|
|
66
94
|
connectors?: SketchConnector[];
|
|
95
|
+
/** How to draw the nodes of each class. */
|
|
96
|
+
style?: Styles<NoInfer<Node>>;
|
|
97
|
+
/**
|
|
98
|
+
* Attributes for the root `<svg>`, such as `font-family`, which its texts
|
|
99
|
+
* inherit. nicevg writes `xmlns`, `viewBox`, `width` and `height` itself.
|
|
100
|
+
*/
|
|
101
|
+
rootAttrs?: Record<string, string | number>;
|
|
67
102
|
}
|
|
68
103
|
/** A diagram to render: a sketch, SVG text following nicevg's SVG contract, or an Svg rendered before. */
|
|
69
|
-
export type Input = Sketch | {
|
|
104
|
+
export type Input<Node extends SketchNode | StyledNode = SketchNode | StyledNode> = Sketch<Node> | {
|
|
70
105
|
svg: string;
|
|
71
106
|
} | Svg;
|
|
72
107
|
export interface DiagramNode {
|
|
@@ -142,4 +177,4 @@ export declare class NicevgError extends Error {
|
|
|
142
177
|
* routes through nodes, stray labels and more), routes the connectors given
|
|
143
178
|
* no points, and moves nodes a little where that reads better.
|
|
144
179
|
*/
|
|
145
|
-
export declare function render(input: Input): Svg;
|
|
180
|
+
export declare function render<const Node extends SketchNode | StyledNode>(input: Input<Node>): Svg;
|
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,34 @@ 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
|
+
// Each style is called only with nodes of its own class.
|
|
69
|
+
const styles = (style ?? {});
|
|
70
|
+
if (edges && connectors) {
|
|
71
|
+
throw new NicevgError("edges and connectors are the same; pass one of them");
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
nodes: nodes.map((node, index) => {
|
|
75
|
+
if (!("class" in node)) {
|
|
76
|
+
return node;
|
|
77
|
+
}
|
|
78
|
+
const draw = Object.hasOwn(styles, node.class) ? styles[node.class] : undefined;
|
|
79
|
+
if (!draw) {
|
|
80
|
+
throw new NicevgError(`nodes[${index}].class: no style named "${node.class}"`);
|
|
81
|
+
}
|
|
82
|
+
const { id, x, y, parent, allowOverlap } = node;
|
|
83
|
+
return { id, x, y, parent, allowOverlap, svg: String(draw(node)) };
|
|
84
|
+
}),
|
|
85
|
+
connectors: edges ?? connectors,
|
|
86
|
+
rootAttrs: rootAttrs && Object.fromEntries(Object.entries(rootAttrs).map(([name, value]) => [name, String(value)])),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
65
89
|
//# 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;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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,14 +72,48 @@ export interface SketchConnector {
|
|
|
69
72
|
fontSize?: number;
|
|
70
73
|
}
|
|
71
74
|
|
|
72
|
-
/**
|
|
73
|
-
|
|
74
|
-
|
|
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 extends StyledNode = StyledNode> = (node: Node) => SvgFragment;
|
|
94
|
+
|
|
95
|
+
/** A style for each class the nodes name, each given the nodes of its class. */
|
|
96
|
+
export type Styles<Node extends SketchNode | StyledNode> = {
|
|
97
|
+
[Class in Extract<Node, StyledNode>["class"]]: Style<Extract<Node, { class: Class }>>;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/** A diagram as nodes and the edges (connectors) between them. */
|
|
101
|
+
export interface Sketch<Node extends SketchNode | StyledNode = SketchNode | StyledNode> {
|
|
102
|
+
nodes: readonly Node[];
|
|
103
|
+
edges?: SketchConnector[];
|
|
104
|
+
/** The same as `edges`, its name before; pass one or the other. */
|
|
75
105
|
connectors?: SketchConnector[];
|
|
106
|
+
/** How to draw the nodes of each class. */
|
|
107
|
+
style?: Styles<NoInfer<Node>>;
|
|
108
|
+
/**
|
|
109
|
+
* Attributes for the root `<svg>`, such as `font-family`, which its texts
|
|
110
|
+
* inherit. nicevg writes `xmlns`, `viewBox`, `width` and `height` itself.
|
|
111
|
+
*/
|
|
112
|
+
rootAttrs?: Record<string, string | number>;
|
|
76
113
|
}
|
|
77
114
|
|
|
78
115
|
/** A diagram to render: a sketch, SVG text following nicevg's SVG contract, or an Svg rendered before. */
|
|
79
|
-
export type Input = Sketch | { svg: string } | Svg;
|
|
116
|
+
export type Input<Node extends SketchNode | StyledNode = SketchNode | StyledNode> = Sketch<Node> | { svg: string } | Svg;
|
|
80
117
|
|
|
81
118
|
export interface DiagramNode {
|
|
82
119
|
id: string;
|
|
@@ -164,8 +201,10 @@ export class NicevgError extends Error {
|
|
|
164
201
|
* routes through nodes, stray labels and more), routes the connectors given
|
|
165
202
|
* no points, and moves nodes a little where that reads better.
|
|
166
203
|
*/
|
|
167
|
-
export function render(input: Input): Svg {
|
|
168
|
-
const request = new TextEncoder().encode(
|
|
204
|
+
export function render<const Node extends SketchNode | StyledNode>(input: Input<Node>): Svg {
|
|
205
|
+
const request = new TextEncoder().encode(
|
|
206
|
+
JSON.stringify(input instanceof Svg ? { svg: input.toString() } : "svg" in input ? input : sketchRequest(input)),
|
|
207
|
+
);
|
|
169
208
|
const reply = JSON.parse(native(request)) as
|
|
170
209
|
| { ok: { svg: string; changes: Change[]; report: AnalysisReport } }
|
|
171
210
|
| { error: string };
|
|
@@ -174,3 +213,27 @@ export function render(input: Input): Svg {
|
|
|
174
213
|
}
|
|
175
214
|
return new Svg(reply.ok.svg, reply.ok.changes, reply.ok.report);
|
|
176
215
|
}
|
|
216
|
+
|
|
217
|
+
/** The sketch as the native library reads it: styled nodes drawn, edges as connectors. */
|
|
218
|
+
function sketchRequest<Node extends SketchNode | StyledNode>({ nodes, edges, connectors, style, rootAttrs }: Sketch<Node>) {
|
|
219
|
+
// Each style is called only with nodes of its own class.
|
|
220
|
+
const styles = (style ?? {}) as Record<string, Style>;
|
|
221
|
+
if (edges && connectors) {
|
|
222
|
+
throw new NicevgError("edges and connectors are the same; pass one of them");
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
nodes: nodes.map((node, index) => {
|
|
226
|
+
if (!("class" in node)) {
|
|
227
|
+
return node;
|
|
228
|
+
}
|
|
229
|
+
const draw = Object.hasOwn(styles, node.class) ? styles[node.class] : undefined;
|
|
230
|
+
if (!draw) {
|
|
231
|
+
throw new NicevgError(`nodes[${index}].class: no style named "${node.class}"`);
|
|
232
|
+
}
|
|
233
|
+
const { id, x, y, parent, allowOverlap } = node;
|
|
234
|
+
return { id, x, y, parent, allowOverlap, svg: String(draw(node)) };
|
|
235
|
+
}),
|
|
236
|
+
connectors: edges ?? connectors,
|
|
237
|
+
rootAttrs: rootAttrs && Object.fromEntries(Object.entries(rootAttrs).map(([name, value]) => [name, String(value)])),
|
|
238
|
+
};
|
|
239
|
+
}
|