nicevg 0.0.0-stage → 0.15.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 +101 -2
- package/dist/bun-ffi.d.ts +2 -0
- package/dist/bun-ffi.js +23 -0
- package/dist/bun-ffi.js.map +1 -0
- package/dist/index.d.ts +145 -0
- package/dist/index.js +65 -0
- package/dist/index.js.map +1 -0
- package/dist/native.d.ts +4 -0
- package/dist/native.js +34 -0
- package/dist/native.js.map +1 -0
- package/dist/node-ffi.d.ts +2 -0
- package/dist/node-ffi.js +23 -0
- package/dist/node-ffi.js.map +1 -0
- 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 +56 -4
- package/src/bun-ffi.ts +27 -0
- package/src/index.ts +176 -0
- package/src/lib.rs +81 -0
- package/src/native.ts +44 -0
- package/src/node-ffi.ts +27 -0
package/README.md
CHANGED
|
@@ -1,3 +1,102 @@
|
|
|
1
|
-
#
|
|
1
|
+
# nicevg for TypeScript (Bun and Node)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
nicevg で図を SVG に描く。TypeScript や JavaScript から、Bun では `bun:ffi`、Node では [koffi](https://koffi.dev/) でネイティブライブラリを呼ぶ。図はノードとコネクタの配列(Sketch)のほか、SVG の文字列でも渡せる。処理は Rust のネイティブコードで動くので、CLI と同じ速さで、出力も同じになる。
|
|
4
|
+
|
|
5
|
+
## インストール
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install nicevg # または bun add nicevg
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
パッケージには、次の環境向けにビルド済みのネイティブライブラリが入っているので、インストール時にスクリプトは走らず、Rust も要らない。
|
|
12
|
+
|
|
13
|
+
| 環境 | 対象 |
|
|
14
|
+
|---|---|
|
|
15
|
+
| Linux | x64、arm64(glibc 2.17 以降) |
|
|
16
|
+
| macOS | arm64、x64 |
|
|
17
|
+
| Windows | x64 |
|
|
18
|
+
|
|
19
|
+
## 使い方
|
|
20
|
+
|
|
21
|
+
入口は `render()` 1 つ。図を受け取り、配置の問題を直し、ノードを少し動かして折れ・交差・混雑を減らして、SVG を表す `Svg` を返す。
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { render } from "nicevg";
|
|
25
|
+
|
|
26
|
+
const svg = render({
|
|
27
|
+
nodes: [
|
|
28
|
+
{ id: "order", x: 20, y: 20, width: 120, height: 56, label: "Order" },
|
|
29
|
+
{ id: "bill", x: 300, y: 160, width: 120, height: 56, label: "Bill" },
|
|
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" },
|
|
35
|
+
],
|
|
36
|
+
});
|
|
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); // 何を直し、何を動かしたか
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- `input` は `Sketch`、`{ svg: string }`、または前に返った `Svg`。
|
|
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` | コネクタの脇に置く文字 |
|
|
65
|
+
|
|
66
|
+
`Svg` の `report.diagram` は、描いた図を読み直したもので、ノード(`bounds`、ラベルの範囲 `labelBounds`)、コネクタ(`points`)、ラベル(`bounds`、説明するコネクタの id `connector`)を持つ。コネクタは id を持たないので、`from` と `to` で見分ける。文字の幅は、全角を 1 文字あたりフォントサイズ、それ以外を 0.59 倍として見積もる。
|
|
67
|
+
|
|
68
|
+
形の書き方が違う入力や、存在しないノードを指す `from`・`to`・`parent` は、どこが何を指しているか(`connectors[0].to` など)を書いた `NicevgError` を投げる。
|
|
69
|
+
|
|
70
|
+
描画は呼んだスレッドで最後まで走る。大きな図の配置調整は 1〜2 秒かかり、その間すべてのコアを使う。
|
|
71
|
+
|
|
72
|
+
## 開発
|
|
73
|
+
|
|
74
|
+
Bun と Node(型を取り除いて .ts を動かせる 22.18 以降)、Rust のツールチェーンが要る。
|
|
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/` の順に探す。
|
|
84
|
+
|
|
85
|
+
`v*` のタグを push すると、`.github/workflows/npm.yml` が各環境のネイティブライブラリを作ってパッケージに入れ、Bun と Node でテストしてから npm に公開する。タグの版と `package.json` の `version` が違えば止まるので、リリースのときは Rust の `Cargo.toml`(ルートと `bindings/js`)と一緒に `package.json` の版も上げる。手で実行すると、公開せずにビルドとパックだけを確かめられる。
|
|
86
|
+
|
|
87
|
+
公開は npm の Trusted Publishing(GitHub Actions の OIDC)で、npm のトークンを使わず、配布元の証明(provenance)も自動で付く。
|
|
88
|
+
|
|
89
|
+
Trusted Publisher はすでに npm にあるパッケージにしか登録できないので、最初の 1 回だけは手元から公開する。npm のアカウントで 2FA を有効にしたうえで、次のようにする。
|
|
90
|
+
|
|
91
|
+
1. Actions の「Publish to npm」を手で実行する(手での実行は公開しない)。全環境のライブラリをそれぞれの環境でテストし、まとめたパッケージが、成果物 `npm-package` に残る。
|
|
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
|
+
```
|
|
101
|
+
|
|
102
|
+
以後はタグの push だけで公開される。npm のトークンは作らない(2FA を回避するトークンは、npm が段階的に廃止している)。
|
package/dist/bun-ffi.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// The native library through Bun's FFI.
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
export function open(path) {
|
|
4
|
+
// Loaded here rather than imported, so that the package loads on Node.
|
|
5
|
+
const { dlopen, FFIType, toArrayBuffer } = createRequire(import.meta.url)("bun:ffi");
|
|
6
|
+
// Bun runs on 64-bit platforms only, where a `usize` is a `u64`.
|
|
7
|
+
const { symbols } = dlopen(path, {
|
|
8
|
+
nicevg_render: { args: [FFIType.ptr, FFIType.u64, FFIType.ptr], returns: FFIType.ptr },
|
|
9
|
+
nicevg_free: { args: [FFIType.ptr, FFIType.u64], returns: FFIType.void },
|
|
10
|
+
});
|
|
11
|
+
return (request) => {
|
|
12
|
+
const replyLength = new BigUint64Array(1);
|
|
13
|
+
const reply = symbols.nicevg_render(request, request.length, replyLength);
|
|
14
|
+
const length = Number(replyLength[0]);
|
|
15
|
+
try {
|
|
16
|
+
return new TextDecoder().decode(new Uint8Array(toArrayBuffer(reply, 0, length)));
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
symbols.nicevg_free(reply, length);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=bun-ffi.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bun-ffi.js","sourceRoot":"","sources":["../src/bun-ffi.ts"],"names":[],"mappings":"AAAA,wCAAwC;AAExC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAM5C,MAAM,UAAU,IAAI,CAAC,IAAY;IAC/B,uEAAuE;IACvE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,GAA6B,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC;IAC/G,iEAAiE;IACjE,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,CAAC,IAAI,EAAE;QAC/B,aAAa,EAAE,EAAE,IAAI,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,GAAG,EAAE;QACtF,WAAW,EAAE,EAAE,IAAI,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,IAAI,EAAE;KACzE,CAAC,CAAC;IACH,OAAO,CAAC,OAAO,EAAE,EAAE;QACjB,MAAM,WAAW,GAAG,IAAI,cAAc,CAAC,CAAC,CAAC,CAAC;QAC1C,MAAM,KAAK,GAAG,OAAO,CAAC,aAAa,CAAC,OAAO,EAAE,OAAO,CAAC,MAAM,EAAE,WAAW,CAAY,CAAC;QACrF,MAAM,MAAM,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;QACtC,IAAI,CAAC;YACH,OAAO,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,IAAI,UAAU,CAAC,aAAa,CAAC,KAAK,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;QACnF,CAAC;gBAAS,CAAC;YACT,OAAO,CAAC,WAAW,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QACrC,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nicevg for TypeScript on Bun and Node: renders diagrams given as shapes
|
|
3
|
+
* (or as SVG text) into SVG, repairing their layout and arranging their
|
|
4
|
+
* nodes, by calling the native library through Bun's FFI or through koffi.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { render } from "nicevg";
|
|
8
|
+
*
|
|
9
|
+
* const svg = render({
|
|
10
|
+
* nodes: [
|
|
11
|
+
* { id: "order", x: 20, y: 20, width: 120, height: 56, label: "Order" },
|
|
12
|
+
* { id: "bill", x: 300, y: 160, width: 120, height: 56, label: "Bill" },
|
|
13
|
+
* ],
|
|
14
|
+
* connectors: [{ from: "order", to: "bill", label: "invoice" }],
|
|
15
|
+
* });
|
|
16
|
+
* await svg.write("diagram.svg");
|
|
17
|
+
* svg.report.diagram.connectors; // where the routes ended up, in the order given
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Rendering runs on the calling thread and blocks it until done; arranging
|
|
21
|
+
* a large diagram takes a second or two, using every core.
|
|
22
|
+
*/
|
|
23
|
+
export interface Point {
|
|
24
|
+
x: number;
|
|
25
|
+
y: number;
|
|
26
|
+
}
|
|
27
|
+
export interface Bounds {
|
|
28
|
+
x: number;
|
|
29
|
+
y: number;
|
|
30
|
+
width: number;
|
|
31
|
+
height: number;
|
|
32
|
+
}
|
|
33
|
+
/** A box, or a circle whose bounds are the square around it. */
|
|
34
|
+
export interface SketchNode {
|
|
35
|
+
id: string;
|
|
36
|
+
x: number;
|
|
37
|
+
y: number;
|
|
38
|
+
width: number;
|
|
39
|
+
height: number;
|
|
40
|
+
shape?: "rect" | "circle";
|
|
41
|
+
/** Its text, lines split at `\n`: at the top left of a node with children, centred in any other. */
|
|
42
|
+
label?: string;
|
|
43
|
+
fontSize?: number;
|
|
44
|
+
/** The node this one sits inside. */
|
|
45
|
+
parent?: string;
|
|
46
|
+
/** Whether it may overlap other nodes on purpose. */
|
|
47
|
+
allowOverlap?: boolean;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A connector from one node to another, along `points` when given. Without
|
|
51
|
+
* them it runs between the nodes' centres, which rendering reroutes from
|
|
52
|
+
* side to side. Connectors have no id: in the rendered diagram, tell them
|
|
53
|
+
* apart by their `from` and `to`.
|
|
54
|
+
*/
|
|
55
|
+
export interface SketchConnector {
|
|
56
|
+
from: string;
|
|
57
|
+
to: string;
|
|
58
|
+
points?: Point[];
|
|
59
|
+
/** Its text, lines split at `\n`, placed beside it. */
|
|
60
|
+
label?: string;
|
|
61
|
+
fontSize?: number;
|
|
62
|
+
}
|
|
63
|
+
/** A diagram as nodes and the connectors between them. */
|
|
64
|
+
export interface Sketch {
|
|
65
|
+
nodes: SketchNode[];
|
|
66
|
+
connectors?: SketchConnector[];
|
|
67
|
+
}
|
|
68
|
+
/** A diagram to render: a sketch, SVG text following nicevg's SVG contract, or an Svg rendered before. */
|
|
69
|
+
export type Input = Sketch | {
|
|
70
|
+
svg: string;
|
|
71
|
+
} | Svg;
|
|
72
|
+
export interface DiagramNode {
|
|
73
|
+
id: string;
|
|
74
|
+
shape?: "circle";
|
|
75
|
+
bounds: Bounds;
|
|
76
|
+
labelBounds: Bounds[];
|
|
77
|
+
parentId?: string;
|
|
78
|
+
allowOverlap?: boolean;
|
|
79
|
+
}
|
|
80
|
+
export interface DiagramConnector {
|
|
81
|
+
/** Its id in an SVG; a sketch's connectors get one for their labels to refer to. */
|
|
82
|
+
id: string;
|
|
83
|
+
from: string;
|
|
84
|
+
to: string;
|
|
85
|
+
points: Point[];
|
|
86
|
+
}
|
|
87
|
+
export interface DiagramLabel {
|
|
88
|
+
id: string;
|
|
89
|
+
bounds: Bounds;
|
|
90
|
+
/** The id of the connector it describes. */
|
|
91
|
+
connector?: string;
|
|
92
|
+
}
|
|
93
|
+
export interface Diagram {
|
|
94
|
+
nodes: DiagramNode[];
|
|
95
|
+
connectors: DiagramConnector[];
|
|
96
|
+
labels: DiagramLabel[];
|
|
97
|
+
unsupportedElements: {
|
|
98
|
+
id: string;
|
|
99
|
+
tagName: string;
|
|
100
|
+
}[];
|
|
101
|
+
}
|
|
102
|
+
export interface Issue {
|
|
103
|
+
code: string;
|
|
104
|
+
message: string;
|
|
105
|
+
elements: string[];
|
|
106
|
+
details?: Record<string, unknown>;
|
|
107
|
+
}
|
|
108
|
+
export interface AnalysisReport {
|
|
109
|
+
valid: boolean;
|
|
110
|
+
issues: Issue[];
|
|
111
|
+
drawingBounds: Bounds | null;
|
|
112
|
+
diagram: Diagram;
|
|
113
|
+
}
|
|
114
|
+
export interface Change {
|
|
115
|
+
code: string;
|
|
116
|
+
message: string;
|
|
117
|
+
elements: string[];
|
|
118
|
+
}
|
|
119
|
+
/** A rendered diagram as SVG, with what rendering changed and the diagram read back from it. */
|
|
120
|
+
export declare class Svg {
|
|
121
|
+
#private;
|
|
122
|
+
/** What fixing and arranging changed, in order. */
|
|
123
|
+
readonly changes: Change[];
|
|
124
|
+
/** The diagram read back: where its nodes, routes and labels ended up, and any issue left. */
|
|
125
|
+
readonly report: AnalysisReport;
|
|
126
|
+
constructor(text: string,
|
|
127
|
+
/** What fixing and arranging changed, in order. */
|
|
128
|
+
changes: Change[],
|
|
129
|
+
/** The diagram read back: where its nodes, routes and labels ended up, and any issue left. */
|
|
130
|
+
report: AnalysisReport);
|
|
131
|
+
/** The SVG text. */
|
|
132
|
+
toString(): string;
|
|
133
|
+
/** Writes the SVG text to a file. */
|
|
134
|
+
write(path: string | URL): Promise<void>;
|
|
135
|
+
}
|
|
136
|
+
/** An input nicevg cannot read, saying what is wrong and where. */
|
|
137
|
+
export declare class NicevgError extends Error {
|
|
138
|
+
name: string;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Renders a diagram into SVG: repairs its layout errors (overlapping nodes,
|
|
142
|
+
* routes through nodes, stray labels and more), routes the connectors given
|
|
143
|
+
* no points, and moves nodes a little where that reads better.
|
|
144
|
+
*/
|
|
145
|
+
export declare function render(input: Input): Svg;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nicevg for TypeScript on Bun and Node: renders diagrams given as shapes
|
|
3
|
+
* (or as SVG text) into SVG, repairing their layout and arranging their
|
|
4
|
+
* nodes, by calling the native library through Bun's FFI or through koffi.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { render } from "nicevg";
|
|
8
|
+
*
|
|
9
|
+
* const svg = render({
|
|
10
|
+
* nodes: [
|
|
11
|
+
* { id: "order", x: 20, y: 20, width: 120, height: 56, label: "Order" },
|
|
12
|
+
* { id: "bill", x: 300, y: 160, width: 120, height: 56, label: "Bill" },
|
|
13
|
+
* ],
|
|
14
|
+
* connectors: [{ from: "order", to: "bill", label: "invoice" }],
|
|
15
|
+
* });
|
|
16
|
+
* await svg.write("diagram.svg");
|
|
17
|
+
* svg.report.diagram.connectors; // where the routes ended up, in the order given
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Rendering runs on the calling thread and blocks it until done; arranging
|
|
21
|
+
* a large diagram takes a second or two, using every core.
|
|
22
|
+
*/
|
|
23
|
+
import { writeFile } from "node:fs/promises";
|
|
24
|
+
import { call as native } from "./native.js";
|
|
25
|
+
/** A rendered diagram as SVG, with what rendering changed and the diagram read back from it. */
|
|
26
|
+
export class Svg {
|
|
27
|
+
changes;
|
|
28
|
+
report;
|
|
29
|
+
#text;
|
|
30
|
+
constructor(text,
|
|
31
|
+
/** What fixing and arranging changed, in order. */
|
|
32
|
+
changes,
|
|
33
|
+
/** The diagram read back: where its nodes, routes and labels ended up, and any issue left. */
|
|
34
|
+
report) {
|
|
35
|
+
this.changes = changes;
|
|
36
|
+
this.report = report;
|
|
37
|
+
this.#text = text;
|
|
38
|
+
}
|
|
39
|
+
/** The SVG text. */
|
|
40
|
+
toString() {
|
|
41
|
+
return this.#text;
|
|
42
|
+
}
|
|
43
|
+
/** Writes the SVG text to a file. */
|
|
44
|
+
write(path) {
|
|
45
|
+
return writeFile(path, this.#text);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** An input nicevg cannot read, saying what is wrong and where. */
|
|
49
|
+
export class NicevgError extends Error {
|
|
50
|
+
name = "NicevgError";
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Renders a diagram into SVG: repairs its layout errors (overlapping nodes,
|
|
54
|
+
* routes through nodes, stray labels and more), routes the connectors given
|
|
55
|
+
* no points, and moves nodes a little where that reads better.
|
|
56
|
+
*/
|
|
57
|
+
export function render(input) {
|
|
58
|
+
const request = new TextEncoder().encode(JSON.stringify(input instanceof Svg ? { svg: input.toString() } : input));
|
|
59
|
+
const reply = JSON.parse(native(request));
|
|
60
|
+
if ("error" in reply) {
|
|
61
|
+
throw new NicevgError(reply.error);
|
|
62
|
+
}
|
|
63
|
+
return new Svg(reply.ok.svg, reply.ok.changes, reply.ok.report);
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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"}
|
package/dist/native.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Fixes and arranges the diagram in a JSON request, returning the JSON reply. */
|
|
2
|
+
export type Render = (request: Uint8Array) => string;
|
|
3
|
+
/** Fixes and arranges the diagram in `request`, opening the library on first use. */
|
|
4
|
+
export declare function call(request: Uint8Array): string;
|
package/dist/native.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Finding and calling the native library, through Bun's FFI on Bun and
|
|
2
|
+
// through koffi elsewhere.
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { open as openOnBun } from "./bun-ffi.js";
|
|
7
|
+
import { open as openOnNode } from "./node-ffi.js";
|
|
8
|
+
const extension = process.platform === "win32" ? "dll" : process.platform === "darwin" ? "dylib" : "so";
|
|
9
|
+
const libraryName = process.platform === "win32" ? `nicevg_ffi.${extension}` : `libnicevg_ffi.${extension}`;
|
|
10
|
+
/** The package's root: this file sits in `src/`, or compiled in `dist/`. */
|
|
11
|
+
const root = fileURLToPath(new URL("..", import.meta.url));
|
|
12
|
+
/**
|
|
13
|
+
* Where the native library may be: as set, packaged for this platform in
|
|
14
|
+
* `native/<platform>-<arch>/`, or built in this repository.
|
|
15
|
+
*/
|
|
16
|
+
function libraryPath() {
|
|
17
|
+
const candidates = [
|
|
18
|
+
process.env.NICEVG_LIBRARY,
|
|
19
|
+
join(root, "native", `${process.platform}-${process.arch}`, libraryName),
|
|
20
|
+
join(root, "..", "..", "target", "release", libraryName),
|
|
21
|
+
].filter((candidate) => candidate !== undefined);
|
|
22
|
+
const found = candidates.find((candidate) => existsSync(candidate));
|
|
23
|
+
if (!found) {
|
|
24
|
+
throw new Error(`nicevg: no native library for ${process.platform}-${process.arch}; looked at ${candidates.join(", ")}.`);
|
|
25
|
+
}
|
|
26
|
+
return found;
|
|
27
|
+
}
|
|
28
|
+
let render;
|
|
29
|
+
/** Fixes and arranges the diagram in `request`, opening the library on first use. */
|
|
30
|
+
export function call(request) {
|
|
31
|
+
render ??= (typeof Bun === "undefined" ? openOnNode : openOnBun)(libraryPath());
|
|
32
|
+
return render(request);
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=native.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native.js","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,2BAA2B;AAE3B,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,IAAI,IAAI,SAAS,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,IAAI,IAAI,UAAU,EAAE,MAAM,eAAe,CAAC;AAKnD,MAAM,SAAS,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;AACxG,MAAM,WAAW,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,cAAc,SAAS,EAAE,CAAC,CAAC,CAAC,iBAAiB,SAAS,EAAE,CAAC;AAC5G,4EAA4E;AAC5E,MAAM,IAAI,GAAG,aAAa,CAAC,IAAI,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE3D;;;GAGG;AACH,SAAS,WAAW;IAClB,MAAM,UAAU,GAAG;QACjB,OAAO,CAAC,GAAG,CAAC,cAAc;QAC1B,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,IAAI,EAAE,EAAE,WAAW,CAAC;QACxE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,CAAC;KACzD,CAAC,MAAM,CAAC,CAAC,SAAS,EAAuB,EAAE,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC;IACtE,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC;IACpE,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACb,iCAAiC,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,IAAI,eAAe,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CACzG,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,IAAI,MAA0B,CAAC;AAE/B,qFAAqF;AACrF,MAAM,UAAU,IAAI,CAAC,OAAmB;IACtC,MAAM,KAAK,CAAC,OAAO,GAAG,KAAK,WAAW,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;IAChF,OAAO,MAAM,CAAC,OAAO,CAAC,CAAC;AACzB,CAAC"}
|
package/dist/node-ffi.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// The native library through koffi, for Node.
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
export function open(path) {
|
|
4
|
+
// Loaded here rather than imported, so that Bun never loads it.
|
|
5
|
+
const koffi = createRequire(import.meta.url)("koffi");
|
|
6
|
+
const library = koffi.load(path);
|
|
7
|
+
const render = library.func("void *nicevg_render(const uint8_t *request, size_t length, _Out_ size_t *reply_length)");
|
|
8
|
+
const free = library.func("void nicevg_free(void *reply, size_t length)");
|
|
9
|
+
return (request) => {
|
|
10
|
+
const replyLength = [0];
|
|
11
|
+
const reply = render(request, request.length, replyLength);
|
|
12
|
+
const length = Number(replyLength[0]);
|
|
13
|
+
try {
|
|
14
|
+
// Copied out rather than viewed in place, which Electron forbids.
|
|
15
|
+
const bytes = koffi.decode(reply, koffi.array("uint8_t", length, "Typed"));
|
|
16
|
+
return new TextDecoder().decode(bytes);
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
free(reply, length);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=node-ffi.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-ffi.js","sourceRoot":"","sources":["../src/node-ffi.ts"],"names":[],"mappings":"AAAA,8CAA8C;AAE9C,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAI5C,MAAM,UAAU,IAAI,CAAC,IAAY;IAC/B,gEAAgE;IAChE,MAAM,KAAK,GAA2B,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC;IAC9E,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjC,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CACzB,wFAAwF,CACzF,CAAC;IACF,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,8CAA8C,CAAC,CAAC;IAC1E,OAAO,CAAC,OAAO,EAAE,EAAE;QACjB,MAAM,WAAW,GAAG,CAAC,CAAC,CAAC,CAAC;QACxB,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;QAC3D,MAAM,MAAM,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;QACtC,IAAI,CAAC;YACH,kEAAkE;YAClE,MAAM,KAAK,GAAe,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;YACvF,OAAO,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACzC,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QACtB,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nicevg",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.15.0",
|
|
4
|
+
"description": "Deterministic checks and repairs for diagrams, given as shapes or SVG, on Bun and Node through native FFI.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"diagram",
|
|
7
|
+
"svg",
|
|
8
|
+
"layout",
|
|
9
|
+
"routing",
|
|
10
|
+
"ffi"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"homepage": "https://github.com/takagiy/nicevg#readme",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/takagiy/nicevg.git",
|
|
17
|
+
"directory": "bindings/js"
|
|
18
|
+
},
|
|
19
|
+
"type": "module",
|
|
20
|
+
"main": "./dist/index.js",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"bun": "./src/index.ts",
|
|
26
|
+
"default": "./dist/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"src",
|
|
32
|
+
"native"
|
|
33
|
+
],
|
|
34
|
+
"engines": {
|
|
35
|
+
"bun": ">=1.1",
|
|
36
|
+
"node": ">=20"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "bun run build:native && bun run build:js",
|
|
43
|
+
"build:native": "bun run scripts/build-native.ts",
|
|
44
|
+
"build:js": "tsc -p tsconfig.build.json",
|
|
45
|
+
"test": "bun run test:bun && bun run test:node",
|
|
46
|
+
"test:bun": "bun test ./test/bun",
|
|
47
|
+
"test:node": "node --test \"test/node/**/*.test.*\"",
|
|
48
|
+
"typecheck": "tsc --noEmit"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"koffi": "3.3.3"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/bun": "1.3.14",
|
|
55
|
+
"@types/node": "24.10.1",
|
|
56
|
+
"typescript": "5.9.3"
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/bun-ffi.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// The native library through Bun's FFI.
|
|
2
|
+
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
|
|
5
|
+
import type { Pointer } from "bun:ffi";
|
|
6
|
+
|
|
7
|
+
import type { Render } from "./native.ts";
|
|
8
|
+
|
|
9
|
+
export function open(path: string): Render {
|
|
10
|
+
// Loaded here rather than imported, so that the package loads on Node.
|
|
11
|
+
const { dlopen, FFIType, toArrayBuffer }: typeof import("bun:ffi") = createRequire(import.meta.url)("bun:ffi");
|
|
12
|
+
// Bun runs on 64-bit platforms only, where a `usize` is a `u64`.
|
|
13
|
+
const { symbols } = dlopen(path, {
|
|
14
|
+
nicevg_render: { args: [FFIType.ptr, FFIType.u64, FFIType.ptr], returns: FFIType.ptr },
|
|
15
|
+
nicevg_free: { args: [FFIType.ptr, FFIType.u64], returns: FFIType.void },
|
|
16
|
+
});
|
|
17
|
+
return (request) => {
|
|
18
|
+
const replyLength = new BigUint64Array(1);
|
|
19
|
+
const reply = symbols.nicevg_render(request, request.length, replyLength) as Pointer;
|
|
20
|
+
const length = Number(replyLength[0]);
|
|
21
|
+
try {
|
|
22
|
+
return new TextDecoder().decode(new Uint8Array(toArrayBuffer(reply, 0, length)));
|
|
23
|
+
} finally {
|
|
24
|
+
symbols.nicevg_free(reply, length);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nicevg for TypeScript on Bun and Node: renders diagrams given as shapes
|
|
3
|
+
* (or as SVG text) into SVG, repairing their layout and arranging their
|
|
4
|
+
* nodes, by calling the native library through Bun's FFI or through koffi.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { render } from "nicevg";
|
|
8
|
+
*
|
|
9
|
+
* const svg = render({
|
|
10
|
+
* nodes: [
|
|
11
|
+
* { id: "order", x: 20, y: 20, width: 120, height: 56, label: "Order" },
|
|
12
|
+
* { id: "bill", x: 300, y: 160, width: 120, height: 56, label: "Bill" },
|
|
13
|
+
* ],
|
|
14
|
+
* connectors: [{ from: "order", to: "bill", label: "invoice" }],
|
|
15
|
+
* });
|
|
16
|
+
* await svg.write("diagram.svg");
|
|
17
|
+
* svg.report.diagram.connectors; // where the routes ended up, in the order given
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Rendering runs on the calling thread and blocks it until done; arranging
|
|
21
|
+
* a large diagram takes a second or two, using every core.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { writeFile } from "node:fs/promises";
|
|
25
|
+
|
|
26
|
+
import { call as native } from "./native.ts";
|
|
27
|
+
|
|
28
|
+
export interface Point {
|
|
29
|
+
x: number;
|
|
30
|
+
y: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface Bounds {
|
|
34
|
+
x: number;
|
|
35
|
+
y: number;
|
|
36
|
+
width: number;
|
|
37
|
+
height: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A box, or a circle whose bounds are the square around it. */
|
|
41
|
+
export interface SketchNode {
|
|
42
|
+
id: string;
|
|
43
|
+
x: number;
|
|
44
|
+
y: number;
|
|
45
|
+
width: number;
|
|
46
|
+
height: number;
|
|
47
|
+
shape?: "rect" | "circle";
|
|
48
|
+
/** Its text, lines split at `\n`: at the top left of a node with children, centred in any other. */
|
|
49
|
+
label?: string;
|
|
50
|
+
fontSize?: number;
|
|
51
|
+
/** The node this one sits inside. */
|
|
52
|
+
parent?: string;
|
|
53
|
+
/** Whether it may overlap other nodes on purpose. */
|
|
54
|
+
allowOverlap?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A connector from one node to another, along `points` when given. Without
|
|
59
|
+
* them it runs between the nodes' centres, which rendering reroutes from
|
|
60
|
+
* side to side. Connectors have no id: in the rendered diagram, tell them
|
|
61
|
+
* apart by their `from` and `to`.
|
|
62
|
+
*/
|
|
63
|
+
export interface SketchConnector {
|
|
64
|
+
from: string;
|
|
65
|
+
to: string;
|
|
66
|
+
points?: Point[];
|
|
67
|
+
/** Its text, lines split at `\n`, placed beside it. */
|
|
68
|
+
label?: string;
|
|
69
|
+
fontSize?: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** A diagram as nodes and the connectors between them. */
|
|
73
|
+
export interface Sketch {
|
|
74
|
+
nodes: SketchNode[];
|
|
75
|
+
connectors?: SketchConnector[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 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;
|
|
80
|
+
|
|
81
|
+
export interface DiagramNode {
|
|
82
|
+
id: string;
|
|
83
|
+
shape?: "circle";
|
|
84
|
+
bounds: Bounds;
|
|
85
|
+
labelBounds: Bounds[];
|
|
86
|
+
parentId?: string;
|
|
87
|
+
allowOverlap?: boolean;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface DiagramConnector {
|
|
91
|
+
/** Its id in an SVG; a sketch's connectors get one for their labels to refer to. */
|
|
92
|
+
id: string;
|
|
93
|
+
from: string;
|
|
94
|
+
to: string;
|
|
95
|
+
points: Point[];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface DiagramLabel {
|
|
99
|
+
id: string;
|
|
100
|
+
bounds: Bounds;
|
|
101
|
+
/** The id of the connector it describes. */
|
|
102
|
+
connector?: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface Diagram {
|
|
106
|
+
nodes: DiagramNode[];
|
|
107
|
+
connectors: DiagramConnector[];
|
|
108
|
+
labels: DiagramLabel[];
|
|
109
|
+
unsupportedElements: { id: string; tagName: string }[];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface Issue {
|
|
113
|
+
code: string;
|
|
114
|
+
message: string;
|
|
115
|
+
elements: string[];
|
|
116
|
+
details?: Record<string, unknown>;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface AnalysisReport {
|
|
120
|
+
valid: boolean;
|
|
121
|
+
issues: Issue[];
|
|
122
|
+
drawingBounds: Bounds | null;
|
|
123
|
+
diagram: Diagram;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface Change {
|
|
127
|
+
code: string;
|
|
128
|
+
message: string;
|
|
129
|
+
elements: string[];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** A rendered diagram as SVG, with what rendering changed and the diagram read back from it. */
|
|
133
|
+
export class Svg {
|
|
134
|
+
readonly #text: string;
|
|
135
|
+
|
|
136
|
+
constructor(
|
|
137
|
+
text: string,
|
|
138
|
+
/** What fixing and arranging changed, in order. */
|
|
139
|
+
readonly changes: Change[],
|
|
140
|
+
/** The diagram read back: where its nodes, routes and labels ended up, and any issue left. */
|
|
141
|
+
readonly report: AnalysisReport,
|
|
142
|
+
) {
|
|
143
|
+
this.#text = text;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The SVG text. */
|
|
147
|
+
toString(): string {
|
|
148
|
+
return this.#text;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Writes the SVG text to a file. */
|
|
152
|
+
write(path: string | URL): Promise<void> {
|
|
153
|
+
return writeFile(path, this.#text);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** An input nicevg cannot read, saying what is wrong and where. */
|
|
158
|
+
export class NicevgError extends Error {
|
|
159
|
+
override name = "NicevgError";
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Renders a diagram into SVG: repairs its layout errors (overlapping nodes,
|
|
164
|
+
* routes through nodes, stray labels and more), routes the connectors given
|
|
165
|
+
* no points, and moves nodes a little where that reads better.
|
|
166
|
+
*/
|
|
167
|
+
export function render(input: Input): Svg {
|
|
168
|
+
const request = new TextEncoder().encode(JSON.stringify(input instanceof Svg ? { svg: input.toString() } : input));
|
|
169
|
+
const reply = JSON.parse(native(request)) as
|
|
170
|
+
| { ok: { svg: string; changes: Change[]; report: AnalysisReport } }
|
|
171
|
+
| { error: string };
|
|
172
|
+
if ("error" in reply) {
|
|
173
|
+
throw new NicevgError(reply.error);
|
|
174
|
+
}
|
|
175
|
+
return new Svg(reply.ok.svg, reply.ok.changes, reply.ok.report);
|
|
176
|
+
}
|
package/src/lib.rs
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
//! A C interface to nicevg for runtimes that load native libraries, such as
|
|
2
|
+
//! Bun's FFI or koffi on Node: a diagram as JSON in, the fixed and arranged
|
|
3
|
+
//! diagram as JSON out.
|
|
4
|
+
//!
|
|
5
|
+
//! The request is either `{"svg": "..."}` or a sketch (`{"nodes": [...],
|
|
6
|
+
//! "connectors": [...]}`). The reply is `{"ok": ...}`, holding the fix
|
|
7
|
+
//! result, or `{"error": "..."}`.
|
|
8
|
+
|
|
9
|
+
use std::panic::{AssertUnwindSafe, catch_unwind};
|
|
10
|
+
|
|
11
|
+
use serde_json::{Value, json};
|
|
12
|
+
|
|
13
|
+
#[global_allocator]
|
|
14
|
+
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
|
15
|
+
|
|
16
|
+
enum Input {
|
|
17
|
+
Svg(String),
|
|
18
|
+
Sketch(nicevg::Sketch),
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
fn read(request: &[u8]) -> Result<Input, String> {
|
|
22
|
+
let value: Value = serde_json::from_slice(request).map_err(|error| format!("invalid JSON: {error}"))?;
|
|
23
|
+
if let Some(svg) = value.get("svg") {
|
|
24
|
+
return svg
|
|
25
|
+
.as_str()
|
|
26
|
+
.map(|svg| Input::Svg(svg.to_owned()))
|
|
27
|
+
.ok_or_else(|| "svg: expected a string".to_owned());
|
|
28
|
+
}
|
|
29
|
+
serde_path_to_error::deserialize(value)
|
|
30
|
+
.map(Input::Sketch)
|
|
31
|
+
.map_err(|error| format!("{}: {}", error.path(), error.inner()))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
fn render(request: &[u8]) -> Result<Value, String> {
|
|
35
|
+
let result = match read(request)? {
|
|
36
|
+
Input::Svg(svg) => nicevg::arrange(&svg).map_err(|error| error.to_string())?,
|
|
37
|
+
Input::Sketch(sketch) => nicevg::arrange_sketch(&sketch).map_err(|error| error.to_string())?,
|
|
38
|
+
};
|
|
39
|
+
Ok(serde_json::to_value(result).expect("serializable result"))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Fixes and arranges the diagram in the JSON request of `length` bytes at
|
|
43
|
+
/// `request`, and returns the JSON reply, its length written to
|
|
44
|
+
/// `reply_length`. The caller frees the reply with [`nicevg_free`].
|
|
45
|
+
///
|
|
46
|
+
/// # Safety
|
|
47
|
+
///
|
|
48
|
+
/// `request` points to `length` readable bytes and `reply_length` to a
|
|
49
|
+
/// writable `usize`.
|
|
50
|
+
#[unsafe(no_mangle)]
|
|
51
|
+
pub unsafe extern "C" fn nicevg_render(request: *const u8, length: usize, reply_length: *mut usize) -> *mut u8 {
|
|
52
|
+
let request = if length == 0 {
|
|
53
|
+
&[][..]
|
|
54
|
+
} else {
|
|
55
|
+
// SAFETY: the caller passes `length` readable bytes.
|
|
56
|
+
unsafe { std::slice::from_raw_parts(request, length) }
|
|
57
|
+
};
|
|
58
|
+
let reply = match catch_unwind(AssertUnwindSafe(|| render(request))) {
|
|
59
|
+
Ok(Ok(value)) => json!({ "ok": value }),
|
|
60
|
+
Ok(Err(message)) => json!({ "error": message }),
|
|
61
|
+
Err(_) => json!({ "error": "nicevg stopped on an internal error" }),
|
|
62
|
+
};
|
|
63
|
+
let bytes = serde_json::to_vec(&reply)
|
|
64
|
+
.expect("serializable reply")
|
|
65
|
+
.into_boxed_slice();
|
|
66
|
+
// SAFETY: the caller passes a writable `usize`.
|
|
67
|
+
unsafe { *reply_length = bytes.len() };
|
|
68
|
+
Box::into_raw(bytes).cast::<u8>()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// Frees a reply [`nicevg_render`] returned.
|
|
72
|
+
///
|
|
73
|
+
/// # Safety
|
|
74
|
+
///
|
|
75
|
+
/// `reply` and `length` are a reply and its length from [`nicevg_render`],
|
|
76
|
+
/// freed only once.
|
|
77
|
+
#[unsafe(no_mangle)]
|
|
78
|
+
pub unsafe extern "C" fn nicevg_free(reply: *mut u8, length: usize) {
|
|
79
|
+
// SAFETY: the reply was a boxed slice of `length` bytes.
|
|
80
|
+
drop(unsafe { Box::from_raw(std::ptr::slice_from_raw_parts_mut(reply, length)) });
|
|
81
|
+
}
|
package/src/native.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Finding and calling the native library, through Bun's FFI on Bun and
|
|
2
|
+
// through koffi elsewhere.
|
|
3
|
+
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
|
|
8
|
+
import { open as openOnBun } from "./bun-ffi.ts";
|
|
9
|
+
import { open as openOnNode } from "./node-ffi.ts";
|
|
10
|
+
|
|
11
|
+
/** Fixes and arranges the diagram in a JSON request, returning the JSON reply. */
|
|
12
|
+
export type Render = (request: Uint8Array) => string;
|
|
13
|
+
|
|
14
|
+
const extension = process.platform === "win32" ? "dll" : process.platform === "darwin" ? "dylib" : "so";
|
|
15
|
+
const libraryName = process.platform === "win32" ? `nicevg_ffi.${extension}` : `libnicevg_ffi.${extension}`;
|
|
16
|
+
/** The package's root: this file sits in `src/`, or compiled in `dist/`. */
|
|
17
|
+
const root = fileURLToPath(new URL("..", import.meta.url));
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Where the native library may be: as set, packaged for this platform in
|
|
21
|
+
* `native/<platform>-<arch>/`, or built in this repository.
|
|
22
|
+
*/
|
|
23
|
+
function libraryPath(): string {
|
|
24
|
+
const candidates = [
|
|
25
|
+
process.env.NICEVG_LIBRARY,
|
|
26
|
+
join(root, "native", `${process.platform}-${process.arch}`, libraryName),
|
|
27
|
+
join(root, "..", "..", "target", "release", libraryName),
|
|
28
|
+
].filter((candidate): candidate is string => candidate !== undefined);
|
|
29
|
+
const found = candidates.find((candidate) => existsSync(candidate));
|
|
30
|
+
if (!found) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`nicevg: no native library for ${process.platform}-${process.arch}; looked at ${candidates.join(", ")}.`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
return found;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let render: Render | undefined;
|
|
39
|
+
|
|
40
|
+
/** Fixes and arranges the diagram in `request`, opening the library on first use. */
|
|
41
|
+
export function call(request: Uint8Array): string {
|
|
42
|
+
render ??= (typeof Bun === "undefined" ? openOnNode : openOnBun)(libraryPath());
|
|
43
|
+
return render(request);
|
|
44
|
+
}
|
package/src/node-ffi.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// The native library through koffi, for Node.
|
|
2
|
+
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
|
|
5
|
+
import type { Render } from "./native.ts";
|
|
6
|
+
|
|
7
|
+
export function open(path: string): Render {
|
|
8
|
+
// Loaded here rather than imported, so that Bun never loads it.
|
|
9
|
+
const koffi: typeof import("koffi") = createRequire(import.meta.url)("koffi");
|
|
10
|
+
const library = koffi.load(path);
|
|
11
|
+
const render = library.func(
|
|
12
|
+
"void *nicevg_render(const uint8_t *request, size_t length, _Out_ size_t *reply_length)",
|
|
13
|
+
);
|
|
14
|
+
const free = library.func("void nicevg_free(void *reply, size_t length)");
|
|
15
|
+
return (request) => {
|
|
16
|
+
const replyLength = [0];
|
|
17
|
+
const reply = render(request, request.length, replyLength);
|
|
18
|
+
const length = Number(replyLength[0]);
|
|
19
|
+
try {
|
|
20
|
+
// Copied out rather than viewed in place, which Electron forbids.
|
|
21
|
+
const bytes: Uint8Array = koffi.decode(reply, koffi.array("uint8_t", length, "Typed"));
|
|
22
|
+
return new TextDecoder().decode(bytes);
|
|
23
|
+
} finally {
|
|
24
|
+
free(reply, length);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|