@itsthecsguy/linework 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/NOTICE +11 -0
- package/README.md +164 -2
- package/assets/drawio/LICENSE +201 -0
- package/assets/drawio/VERSION +1 -0
- package/assets/drawio/favicon.ico +0 -0
- package/assets/drawio/images/1x1.png +0 -0
- package/assets/drawio/images/2x2.png +0 -0
- package/assets/drawio/images/3x3.png +0 -0
- package/assets/drawio/images/ajax-loader.gif +0 -0
- package/assets/drawio/images/android-chrome-512x512.png +0 -0
- package/assets/drawio/images/apple-touch-icon.png +0 -0
- package/assets/drawio/images/aui-wait.gif +0 -0
- package/assets/drawio/images/broken.png +0 -0
- package/assets/drawio/images/browserconfig.xml +9 -0
- package/assets/drawio/images/close.png +0 -0
- package/assets/drawio/images/cloud-connector-atlas.png +0 -0
- package/assets/drawio/images/collapsed.gif +0 -0
- package/assets/drawio/images/delete.png +0 -0
- package/assets/drawio/images/document-google-drive-icon-80.png +0 -0
- package/assets/drawio/images/draw-search.png +0 -0
- package/assets/drawio/images/drawio.svg +19 -0
- package/assets/drawio/images/drawlogo-color.svg +50 -0
- package/assets/drawio/images/drawlogo-gray.svg +18 -0
- package/assets/drawio/images/drawlogo-text-bottom.svg +75 -0
- package/assets/drawio/images/drawlogo.svg +18 -0
- package/assets/drawio/images/drawlogo128.png +0 -0
- package/assets/drawio/images/drawlogo144.png +0 -0
- package/assets/drawio/images/drawlogo256.png +0 -0
- package/assets/drawio/images/drawlogo48-gray.png +0 -0
- package/assets/drawio/images/drawlogo48.png +0 -0
- package/assets/drawio/images/drawlogo80.png +0 -0
- package/assets/drawio/images/droptarget.png +0 -0
- package/assets/drawio/images/edit.gif +0 -0
- package/assets/drawio/images/edit16.png +0 -0
- package/assets/drawio/images/edit32.png +0 -0
- package/assets/drawio/images/edit80.png +0 -0
- package/assets/drawio/images/embed-icon.png +0 -0
- package/assets/drawio/images/expanded.gif +0 -0
- package/assets/drawio/images/facebook.png +0 -0
- package/assets/drawio/images/favicon-16x16.png +0 -0
- package/assets/drawio/images/favicon-32x32.png +0 -0
- package/assets/drawio/images/file.png +0 -0
- package/assets/drawio/images/folder.png +0 -0
- package/assets/drawio/images/gadget-screenshot.png +0 -0
- package/assets/drawio/images/gadget-thumb.png +0 -0
- package/assets/drawio/images/github-logo-white.svg +6 -0
- package/assets/drawio/images/github-logo.svg +17 -0
- package/assets/drawio/images/gitlab-logo-white.svg +1 -0
- package/assets/drawio/images/gitlab-logo.svg +1 -0
- package/assets/drawio/images/glyphicons_blogger.png +0 -0
- package/assets/drawio/images/glyphicons_facebook.png +0 -0
- package/assets/drawio/images/glyphicons_github.png +0 -0
- package/assets/drawio/images/glyphicons_google.png +0 -0
- package/assets/drawio/images/glyphicons_star.png +0 -0
- package/assets/drawio/images/glyphicons_twitter.png +0 -0
- package/assets/drawio/images/glyphicons_youtube.png +0 -0
- package/assets/drawio/images/google-drive-connector-atlas.png +0 -0
- package/assets/drawio/images/google-drive-connector-atlas.svg +121 -0
- package/assets/drawio/images/google-drive-logo-white.svg +9 -0
- package/assets/drawio/images/google-drive-logo.svg +17 -0
- package/assets/drawio/images/google-share.png +0 -0
- package/assets/drawio/images/icon-192-maskable.png +0 -0
- package/assets/drawio/images/icon-192.png +0 -0
- package/assets/drawio/images/icon-512-maskable.png +0 -0
- package/assets/drawio/images/icon-512.png +0 -0
- package/assets/drawio/images/icon-search.svg +21 -0
- package/assets/drawio/images/img-hi-res.png +0 -0
- package/assets/drawio/images/img-lo-res.png +0 -0
- package/assets/drawio/images/list.svg +16 -0
- package/assets/drawio/images/locked.png +0 -0
- package/assets/drawio/images/log.png +0 -0
- package/assets/drawio/images/logo-confluence.png +0 -0
- package/assets/drawio/images/logo-flat-small.png +0 -0
- package/assets/drawio/images/logo-flat.png +0 -0
- package/assets/drawio/images/logo-jira.png +0 -0
- package/assets/drawio/images/logo-small.gif +0 -0
- package/assets/drawio/images/logo-white.png +0 -0
- package/assets/drawio/images/logo.png +0 -0
- package/assets/drawio/images/manifest.json +25 -0
- package/assets/drawio/images/move.png +0 -0
- package/assets/drawio/images/notion-logo-white.svg +1 -0
- package/assets/drawio/images/notion-logo.svg +1 -0
- package/assets/drawio/images/onedrive-connector-atlas.png +0 -0
- package/assets/drawio/images/onedrive-connector-atlas.svg +248 -0
- package/assets/drawio/images/onedrive-logo-white.svg +29 -0
- package/assets/drawio/images/onedrive-logo.svg +29 -0
- package/assets/drawio/images/osa_database.png +0 -0
- package/assets/drawio/images/osa_drive-harddisk.png +0 -0
- package/assets/drawio/images/plant-uml-atlas.png +0 -0
- package/assets/drawio/images/samepage-icon-color.svg +37 -0
- package/assets/drawio/images/save.png +0 -0
- package/assets/drawio/images/sidebar-android.png +0 -0
- package/assets/drawio/images/sidebar-archimate.png +0 -0
- package/assets/drawio/images/sidebar-arrows.png +0 -0
- package/assets/drawio/images/sidebar-arrows2.png +0 -0
- package/assets/drawio/images/sidebar-basic.png +0 -0
- package/assets/drawio/images/sidebar-c4.png +0 -0
- package/assets/drawio/images/sidebar-cabinets.png +0 -0
- package/assets/drawio/images/sidebar-cumulus.png +0 -0
- package/assets/drawio/images/sidebar-dfd.png +0 -0
- package/assets/drawio/images/sidebar-dynamics365.png +0 -0
- package/assets/drawio/images/sidebar-eip.png +0 -0
- package/assets/drawio/images/sidebar-er.png +0 -0
- package/assets/drawio/images/sidebar-floorplans.png +0 -0
- package/assets/drawio/images/sidebar-flowchart.png +0 -0
- package/assets/drawio/images/sidebar-gcp3.png +0 -0
- package/assets/drawio/images/sidebar-general.png +0 -0
- package/assets/drawio/images/sidebar-kubernetes.png +0 -0
- package/assets/drawio/images/sidebar-leanmapping.png +0 -0
- package/assets/drawio/images/sidebar-mockups.png +0 -0
- package/assets/drawio/images/sidebar-network.png +0 -0
- package/assets/drawio/images/sidebar-network2.png +0 -0
- package/assets/drawio/images/sidebar-openstack.png +0 -0
- package/assets/drawio/images/sidebar-sitemap.png +0 -0
- package/assets/drawio/images/sidebar-threatmodeling.png +0 -0
- package/assets/drawio/images/sidebar-uml.png +0 -0
- package/assets/drawio/images/sketch-logo128.png +0 -0
- package/assets/drawio/images/spin.gif +0 -0
- package/assets/drawio/images/sprites.png +0 -0
- package/assets/drawio/images/stop-flat-icon-80.png +0 -0
- package/assets/drawio/images/submenu.gif +0 -0
- package/assets/drawio/images/touch-submenu.png +0 -0
- package/assets/drawio/images/transparent.gif +0 -0
- package/assets/drawio/images/trello-logo-white-orange.svg +6 -0
- package/assets/drawio/images/trello-logo-white.svg +6 -0
- package/assets/drawio/images/trello-logo.svg +1 -0
- package/assets/drawio/images/tweet.png +0 -0
- package/assets/drawio/images/update16.png +0 -0
- package/assets/drawio/images/update32.png +0 -0
- package/assets/drawio/images/update80.png +0 -0
- package/assets/drawio/images/youtube.png +0 -0
- package/assets/drawio/img/clipart/Battery_0_128x128.png +0 -0
- package/assets/drawio/img/clipart/Battery_100_128x128.png +0 -0
- package/assets/drawio/img/clipart/Battery_50_128x128.png +0 -0
- package/assets/drawio/img/clipart/Battery_75_128x128.png +0 -0
- package/assets/drawio/img/clipart/Battery_allstates_128x128.png +0 -0
- package/assets/drawio/img/clipart/Bluetooth_128x128.png +0 -0
- package/assets/drawio/img/clipart/Earth_globe_128x128.png +0 -0
- package/assets/drawio/img/clipart/Empty_Folder_128x128.png +0 -0
- package/assets/drawio/img/clipart/Full_Folder_128x128.png +0 -0
- package/assets/drawio/img/clipart/Gear_128x128.png +0 -0
- package/assets/drawio/img/clipart/Keys_128x128.png +0 -0
- package/assets/drawio/img/clipart/Lock_128x128.png +0 -0
- package/assets/drawio/img/clipart/Mouse_Pointer_128x128.png +0 -0
- package/assets/drawio/img/clipart/Plug_128x128.png +0 -0
- package/assets/drawio/img/clipart/Ships_Wheel_128x128.png +0 -0
- package/assets/drawio/img/clipart/Star_128x128.png +0 -0
- package/assets/drawio/img/clipart/Tire_128x128.png +0 -0
- package/assets/drawio/index.html +92 -0
- package/assets/drawio/js/PostConfig.js +7 -0
- package/assets/drawio/js/PreConfig.js +13 -0
- package/assets/drawio/js/app.min.js +18466 -0
- package/assets/drawio/js/bootstrap.js +428 -0
- package/assets/drawio/js/diagramly/LibavoidRouting.js +4459 -0
- package/assets/drawio/js/export-init.js +21 -0
- package/assets/drawio/js/extensions.min.js +10478 -0
- package/assets/drawio/js/libavoid-js/LICENSE +505 -0
- package/assets/drawio/js/libavoid-js/libavoid-routing.js +1633 -0
- package/assets/drawio/js/libavoid-js/libavoid.min.js +59 -0
- package/assets/drawio/js/main.js +22 -0
- package/assets/drawio/js/rough/rough.min.js +1 -0
- package/assets/drawio/js/shapes-14-6-5.min.js +3457 -0
- package/assets/drawio/js/stencils.min.js +447 -0
- package/assets/drawio/mxgraph/css/common.css +147 -0
- package/assets/drawio/resources/dia.txt +2901 -0
- package/assets/drawio/styles/dark-default.xml +109 -0
- package/assets/drawio/styles/default-old.xml +109 -0
- package/assets/drawio/styles/default.xml +203 -0
- package/assets/drawio/styles/fonts/ArchitectsDaughter-Regular.ttf +0 -0
- package/assets/drawio/styles/grapheditor.css +4501 -0
- package/assets/drawio/styles/high-contrast.css +128 -0
- package/assets/drawio/styles/thumb_horz.png +0 -0
- package/assets/drawio/styles/thumb_vertical.png +0 -0
- package/assets/fonts/ibm-plex-mono-LICENSE.txt +93 -0
- package/assets/fonts/ibm-plex-sans-LICENSE.txt +93 -0
- package/assets/fonts/kalam-400.woff2 +0 -0
- package/assets/fonts/kalam-700.woff2 +0 -0
- package/assets/fonts/kalam-LICENSE.txt +93 -0
- package/assets/fonts/plex-mono-400.woff2 +0 -0
- package/assets/fonts/plex-mono-600.woff2 +0 -0
- package/assets/fonts/plex-sans-400.woff2 +0 -0
- package/assets/fonts/plex-sans-600.woff2 +0 -0
- package/dist/api.js +181 -0
- package/dist/cli.js +262 -0
- package/dist/drawio/icons.js +121 -0
- package/dist/drawio/merge.js +309 -0
- package/dist/drawio/read.js +180 -0
- package/dist/drawio/show.js +501 -0
- package/dist/drawio/theme.js +425 -0
- package/dist/drawio/write.js +300 -0
- package/dist/edit/server.js +212 -0
- package/dist/git.js +46 -0
- package/dist/import/mermaid.js +569 -0
- package/dist/layout/layout.js +212 -0
- package/dist/layout/sequence.js +333 -0
- package/dist/layout/text.js +73 -0
- package/dist/lint/check.js +184 -0
- package/dist/lint/fixlabels.js +128 -0
- package/dist/mcp/server.js +239 -0
- package/dist/publish.js +121 -0
- package/dist/render/browser.js +210 -0
- package/dist/render/fonts.js +66 -0
- package/dist/report.js +36 -0
- package/dist/spec/model.js +53 -0
- package/dist/spec/parse.js +497 -0
- package/package.json +71 -4
- package/skills/linework/SKILL.md +214 -0
- package/skills/linework/examples/billing-er.drawio.svg +4 -0
- package/skills/linework/examples/billing-er.yaml +31 -0
- package/skills/linework/examples/checkout.drawio.svg +4 -0
- package/skills/linework/examples/checkout.yaml +28 -0
- package/skills/linework/examples/pay-sequence.drawio.svg +4 -0
- package/skills/linework/examples/pay-sequence.yaml +28 -0
- package/skills/linework/examples/payment-state.drawio.svg +4 -0
- package/skills/linework/examples/payment-state.yaml +22 -0
- package/skills/linework/examples/refund-flow.drawio.svg +4 -0
- package/skills/linework/examples/refund-flow.yaml +29 -0
- package/skills/linework/examples/serverless-aws.drawio.svg +4 -0
- package/skills/linework/examples/serverless-aws.yaml +23 -0
- package/skills/linework/references/google-docs.md +46 -0
- package/skills/linework/references/spec.md +312 -0
- package/skills/linework/references/style-guide.md +53 -0
package/dist/publish.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// `linework publish`: copy diagrams into a Google Drive folder synced by Google
|
|
2
|
+
// Drive for desktop, so draw.io's Google Docs add-on can insert them and later
|
|
3
|
+
// refresh them. Each file always lands at the same path, which keeps its Drive
|
|
4
|
+
// file id, so "Update diagrams" in Docs picks up new versions.
|
|
5
|
+
//
|
|
6
|
+
// Edits can come back the other way: someone opens the diagram from Docs in
|
|
7
|
+
// draw.io, which saves to the Drive copy. publish notices that the Drive copy
|
|
8
|
+
// changed since it wrote it, refuses to overwrite it, and `--pull` copies it back.
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import crypto from 'node:crypto';
|
|
13
|
+
import { execFileSync } from 'node:child_process';
|
|
14
|
+
const MANIFEST = '.linework-publish.json';
|
|
15
|
+
/** Google Drive for desktop folders on this machine. */
|
|
16
|
+
export function driveFolders() {
|
|
17
|
+
const found = [];
|
|
18
|
+
if (process.platform === 'darwin') {
|
|
19
|
+
const cs = path.join(os.homedir(), 'Library', 'CloudStorage');
|
|
20
|
+
if (fs.existsSync(cs))
|
|
21
|
+
for (const d of fs.readdirSync(cs))
|
|
22
|
+
if (d.startsWith('GoogleDrive-')) {
|
|
23
|
+
const my = fs.readdirSync(path.join(cs, d)).find((n) => n === 'My Drive' || n === 'Meine Ablage' || /^my drive$/i.test(n));
|
|
24
|
+
if (my)
|
|
25
|
+
found.push(path.join(cs, d, my));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else if (process.platform === 'win32') {
|
|
29
|
+
for (const letter of 'GHIJKLMNOPQRSTUVWXYZ') {
|
|
30
|
+
const p = `${letter}:\\My Drive`;
|
|
31
|
+
if (fs.existsSync(p))
|
|
32
|
+
found.push(p);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return found;
|
|
36
|
+
}
|
|
37
|
+
function projectName(cwd) {
|
|
38
|
+
try {
|
|
39
|
+
return path.basename(execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim());
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return path.basename(cwd);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function projectRoot(cwd) {
|
|
46
|
+
try {
|
|
47
|
+
return execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return cwd;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const sha = (buf) => crypto.createHash('sha1').update(buf).digest('hex');
|
|
54
|
+
/**
|
|
55
|
+
* Where publish writes. An explicit `to` is used as is. Otherwise the single
|
|
56
|
+
* Google Drive folder on this machine, under linework/<project>.
|
|
57
|
+
*/
|
|
58
|
+
export function publishDir(to, cwd = process.cwd()) {
|
|
59
|
+
if (to)
|
|
60
|
+
return path.resolve(cwd, to);
|
|
61
|
+
if (process.env.LINEWORK_PUBLISH_DIR)
|
|
62
|
+
return path.resolve(cwd, process.env.LINEWORK_PUBLISH_DIR);
|
|
63
|
+
const drives = driveFolders();
|
|
64
|
+
if (drives.length === 1)
|
|
65
|
+
return path.join(drives[0], 'linework', projectName(cwd));
|
|
66
|
+
if (drives.length > 1)
|
|
67
|
+
throw new Error(`found several Google Drive folders; pick one with --to:\n${drives.map((d) => ' ' + d).join('\n')}`);
|
|
68
|
+
throw new Error('no Google Drive folder found. Install Google Drive for desktop (it syncs a local folder to Drive), or pass --to <folder> to publish into any synced folder.');
|
|
69
|
+
}
|
|
70
|
+
export function publish(files, opts = {}) {
|
|
71
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
72
|
+
const dir = publishDir(opts.to, cwd);
|
|
73
|
+
const root = projectRoot(cwd);
|
|
74
|
+
const manifestPath = path.join(dir, MANIFEST);
|
|
75
|
+
const manifest = fs.existsSync(manifestPath) ? JSON.parse(fs.readFileSync(manifestPath, 'utf8')) : {};
|
|
76
|
+
const items = [];
|
|
77
|
+
for (const f of files) {
|
|
78
|
+
const src = path.resolve(cwd, f);
|
|
79
|
+
if (!fs.existsSync(src))
|
|
80
|
+
throw new Error(`${f} doesn't exist`);
|
|
81
|
+
if (!/\.(drawio\.svg|drawio\.png|drawio|svg)$/.test(src))
|
|
82
|
+
throw new Error(`${f}: publish takes .drawio.svg or .drawio files`);
|
|
83
|
+
// Keep the project's folder structure so two diagrams with the same name don't collide.
|
|
84
|
+
const rel = path.relative(root, src).startsWith('..') ? path.basename(src) : path.relative(root, src);
|
|
85
|
+
const dest = path.join(dir, rel);
|
|
86
|
+
const key = rel.split(path.sep).join('/');
|
|
87
|
+
const srcBuf = fs.readFileSync(src);
|
|
88
|
+
const destBuf = fs.existsSync(dest) ? fs.readFileSync(dest) : undefined;
|
|
89
|
+
// Changed in Drive since we last wrote it: someone edited it from Docs or Drive.
|
|
90
|
+
const editedInDrive = !!destBuf && manifest[key] !== undefined && sha(destBuf) !== manifest[key];
|
|
91
|
+
if (opts.pull) {
|
|
92
|
+
if (!destBuf)
|
|
93
|
+
throw new Error(`${dest} doesn't exist, so there is nothing to pull`);
|
|
94
|
+
if (sha(destBuf) !== sha(srcBuf)) {
|
|
95
|
+
fs.writeFileSync(src, destBuf);
|
|
96
|
+
items.push({ file: src, dest, action: 'pulled' });
|
|
97
|
+
}
|
|
98
|
+
else
|
|
99
|
+
items.push({ file: src, dest, action: 'unchanged' });
|
|
100
|
+
manifest[key] = sha(destBuf);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (editedInDrive && sha(destBuf) !== sha(srcBuf) && !opts.force) {
|
|
104
|
+
items.push({ file: src, dest, action: 'conflict' });
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (destBuf && sha(destBuf) === sha(srcBuf)) {
|
|
108
|
+
manifest[key] = sha(srcBuf);
|
|
109
|
+
items.push({ file: src, dest, action: 'unchanged' });
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
113
|
+
// Overwrite in place (not delete and recreate) so Drive keeps the same file id.
|
|
114
|
+
fs.writeFileSync(dest, srcBuf);
|
|
115
|
+
manifest[key] = sha(srcBuf);
|
|
116
|
+
items.push({ file: src, dest, action: 'published' });
|
|
117
|
+
}
|
|
118
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
119
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
120
|
+
return items;
|
|
121
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// Headless draw.io. Loads the bundled draw.io runtime into Chromium and uses
|
|
2
|
+
// its own Graph code to measure, render and inspect diagrams, so what linework
|
|
3
|
+
// renders matches what the draw.io editor shows.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { chromium } from 'playwright-core';
|
|
8
|
+
import { FONT_STACK } from '../drawio/theme.js';
|
|
9
|
+
import { FACES, embedFonts, fontFaceCss, fontsDir } from './fonts.js';
|
|
10
|
+
const ORIGIN = 'http://aiviz.local';
|
|
11
|
+
export function assetsDir() {
|
|
12
|
+
// dist/render/browser.js or src/render/browser.ts -> <pkg>/assets/drawio
|
|
13
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
for (let i = 0; i < 4; i++) {
|
|
15
|
+
const candidate = path.join(dir, 'assets', 'drawio');
|
|
16
|
+
if (fs.existsSync(path.join(candidate, 'js', 'app.min.js')))
|
|
17
|
+
return candidate;
|
|
18
|
+
dir = path.dirname(dir);
|
|
19
|
+
}
|
|
20
|
+
throw new Error('linework: bundled draw.io runtime not found (assets/drawio). Reinstall linework.');
|
|
21
|
+
}
|
|
22
|
+
const host = () => `<!DOCTYPE html><html><head><meta charset="utf-8">
|
|
23
|
+
<script src="js/export-init.js"></script>
|
|
24
|
+
<link rel="stylesheet" href="mxgraph/css/common.css">
|
|
25
|
+
<script src="js/app.min.js"></script>
|
|
26
|
+
<script src="js/shapes-14-6-5.min.js"></script>
|
|
27
|
+
<script src="js/stencils.min.js"></script>
|
|
28
|
+
<script src="js/libavoid-js/libavoid.min.js"></script>
|
|
29
|
+
<script src="js/libavoid-js/libavoid-routing.js"></script>
|
|
30
|
+
<script src="js/diagramly/LibavoidRouting.js"></script>
|
|
31
|
+
<script src="js/rough/rough.min.js"></script>
|
|
32
|
+
<style>body{margin:0;background:#fff} #out{display:inline-block}
|
|
33
|
+
${fontFaceCss((f) => `/aiviz-fonts/${f}`)}</style>
|
|
34
|
+
</head><body><div id="out"></div><script>
|
|
35
|
+
window.FONT_STACK = ${JSON.stringify(FONT_STACK)};
|
|
36
|
+
function decodeInto(graph, xml) {
|
|
37
|
+
var doc = mxUtils.parseXml(xml);
|
|
38
|
+
var node = Editor.extractGraphModel(doc.documentElement, false);
|
|
39
|
+
new mxCodec(node.ownerDocument).decode(node, graph.getModel());
|
|
40
|
+
var bg = node.getAttribute('background');
|
|
41
|
+
graph.background = bg && bg != 'none' ? bg : '#ffffff';
|
|
42
|
+
}
|
|
43
|
+
function withGraph(xml, fn) {
|
|
44
|
+
var c = document.createElement('div');
|
|
45
|
+
c.style.cssText = 'position:absolute;left:-99999px;top:0;width:4000px;height:4000px';
|
|
46
|
+
document.body.appendChild(c);
|
|
47
|
+
var graph = new Graph(c);
|
|
48
|
+
graph.setEnabled(false);
|
|
49
|
+
try { decodeInto(graph, xml); return fn(graph); } finally { graph.destroy(); c.remove(); }
|
|
50
|
+
}
|
|
51
|
+
var ctx = document.createElement('canvas').getContext('2d');
|
|
52
|
+
window.aiviz = {
|
|
53
|
+
measure: function (reqs) {
|
|
54
|
+
return reqs.map(function (r) {
|
|
55
|
+
if (!r[0]) return 0;
|
|
56
|
+
ctx.font = r[2] + ' ' + r[1] + 'px ' + (r[3] || window.FONT_STACK);
|
|
57
|
+
return Math.ceil(ctx.measureText(r[0]).width);
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
render: function (xml, embed) {
|
|
61
|
+
return withGraph(xml, function (graph) {
|
|
62
|
+
var svg = graph.getSvg(graph.background, 1, 16, false, null, true, null, null, null, false, false);
|
|
63
|
+
if (embed) svg.setAttribute('content', xml);
|
|
64
|
+
var out = document.getElementById('out');
|
|
65
|
+
out.innerHTML = '';
|
|
66
|
+
out.appendChild(svg.cloneNode(true));
|
|
67
|
+
return Graph.xmlDeclaration + '\\n' + (embed ? Graph.svgFileComment + '\\n' : '') + Graph.svgDoctype + '\\n' + mxUtils.getXml(svg);
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
// Route the given edges around vertices with draw.io's libavoid router (the same
|
|
71
|
+
// code as Arrange > Layout > Orthogonal routing). Returns mxGraphModel XML.
|
|
72
|
+
reroute: function (xml, edgeIds) {
|
|
73
|
+
return withGraph(xml, function (graph) {
|
|
74
|
+
var model = graph.getModel();
|
|
75
|
+
var cells = edgeIds.map(function (id) { return model.getCell(id); }).filter(Boolean);
|
|
76
|
+
if (cells.length && globalThis.Avoid) {
|
|
77
|
+
model.beginUpdate();
|
|
78
|
+
try { LibavoidRouting.routeCells(graph, globalThis.Avoid, cells, null, true); } finally { model.endUpdate(); }
|
|
79
|
+
}
|
|
80
|
+
return mxUtils.getXml(new mxCodec().encode(model));
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
// Names of stencils in a bundled set, e.g. stencilNames('aws4.xml').
|
|
84
|
+
stencilNames: function (set) {
|
|
85
|
+
var before = Object.keys(mxStencilRegistry.stencils);
|
|
86
|
+
mxStencilRegistry.loadStencilSet(STENCIL_PATH + '/' + set, null, null, false);
|
|
87
|
+
return Object.keys(mxStencilRegistry.stencils).filter(function (k) { return before.indexOf(k) < 0; });
|
|
88
|
+
},
|
|
89
|
+
hasStencil: function (name) {
|
|
90
|
+
return mxStencilRegistry.getStencil(name) != null;
|
|
91
|
+
},
|
|
92
|
+
inspect: function (xml) {
|
|
93
|
+
return withGraph(xml, function (graph) {
|
|
94
|
+
var model = graph.getModel(), out = [];
|
|
95
|
+
for (var id in model.cells) {
|
|
96
|
+
var cell = model.cells[id];
|
|
97
|
+
if (!cell || (!cell.vertex && !cell.edge)) continue;
|
|
98
|
+
var st = graph.view.getState(cell);
|
|
99
|
+
if (!st) continue;
|
|
100
|
+
var style = st.style || {};
|
|
101
|
+
var tb = st.text && st.text.boundingBox;
|
|
102
|
+
out.push({
|
|
103
|
+
id: cell.id,
|
|
104
|
+
vertex: !!cell.vertex,
|
|
105
|
+
edge: !!cell.edge,
|
|
106
|
+
parent: cell.parent ? cell.parent.id : null,
|
|
107
|
+
container: style.container == '1' || (cell.value && cell.value.getAttribute && cell.value.getAttribute('aiviz_type') == 'group'),
|
|
108
|
+
text: style.text != null || style.shape == 'text',
|
|
109
|
+
shape: style.shape || null,
|
|
110
|
+
aivizType: (cell.value && cell.value.getAttribute && cell.value.getAttribute('aiviz_type')) || null,
|
|
111
|
+
source: cell.edge && cell.source ? cell.source.id : null,
|
|
112
|
+
target: cell.edge && cell.target ? cell.target.id : null,
|
|
113
|
+
box: { x: st.x, y: st.y, w: st.width, h: st.height },
|
|
114
|
+
textBox: tb ? { x: tb.x, y: tb.y, w: tb.width, h: tb.height } : null,
|
|
115
|
+
label: graph.getLabel(cell) ? String(graph.getLabel(cell)).split(/<br\\s*\\/?>/i)[0].replace(/<[^>]+>/g, ' ').replace(/\\s+/g, ' ').trim() : '',
|
|
116
|
+
points: st.absolutePoints ? st.absolutePoints.filter(Boolean).map(function (p) { return { x: p.x, y: p.y }; }) : null,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
window.ready = true;
|
|
124
|
+
</script></body></html>`;
|
|
125
|
+
const TYPES = { '.js': 'text/javascript', '.css': 'text/css', '.html': 'text/html', '.txt': 'text/plain', '.svg': 'image/svg+xml', '.png': 'image/png' };
|
|
126
|
+
export class DrawioBrowser {
|
|
127
|
+
browser;
|
|
128
|
+
page;
|
|
129
|
+
constructor(browser, page) {
|
|
130
|
+
this.browser = browser;
|
|
131
|
+
this.page = page;
|
|
132
|
+
}
|
|
133
|
+
static async launch(opts = {}) {
|
|
134
|
+
const browser = await launchChromium();
|
|
135
|
+
const page = await browser.newPage({ deviceScaleFactor: opts.scale ?? 2 });
|
|
136
|
+
const dir = assetsDir();
|
|
137
|
+
const fonts = fontsDir();
|
|
138
|
+
const html = host();
|
|
139
|
+
await page.route(`${ORIGIN}/**`, async (route) => {
|
|
140
|
+
const url = new URL(route.request().url());
|
|
141
|
+
if (url.pathname === '/' || url.pathname === '/host.html')
|
|
142
|
+
return route.fulfill({ status: 200, contentType: 'text/html', body: html });
|
|
143
|
+
if (url.pathname.startsWith('/aiviz-fonts/')) {
|
|
144
|
+
const f = FACES.find((x) => `/aiviz-fonts/${x.file}` === url.pathname);
|
|
145
|
+
return f ? route.fulfill({ status: 200, contentType: 'font/woff2', body: fs.readFileSync(path.join(fonts, f.file)) }) : route.fulfill({ status: 404, body: '' });
|
|
146
|
+
}
|
|
147
|
+
const file = path.join(dir, path.normalize(url.pathname));
|
|
148
|
+
if (!file.startsWith(dir) || !fs.existsSync(file))
|
|
149
|
+
return route.fulfill({ status: 404, body: '' });
|
|
150
|
+
return route.fulfill({ status: 200, contentType: TYPES[path.extname(file)] ?? 'application/octet-stream', body: fs.readFileSync(file) });
|
|
151
|
+
});
|
|
152
|
+
// Nothing else may load: keeps rendering offline and deterministic.
|
|
153
|
+
await page.route(/^(?!http:\/\/aiviz\.local\/).*/, (route) => route.abort());
|
|
154
|
+
await page.goto(`${ORIGIN}/host.html`);
|
|
155
|
+
await page.waitForFunction(() => window.ready === true, null, { timeout: 30000 });
|
|
156
|
+
// Measuring before a font loads would size every box for the fallback font.
|
|
157
|
+
const ok = await page.evaluate((faces) => Promise.all(faces.map((f) => document.fonts.load(`${f.weight} 16px "${f.family}"`).then((r) => r.length > 0))), FACES.map(({ family, weight }) => ({ family, weight })));
|
|
158
|
+
if (ok.includes(false))
|
|
159
|
+
throw new Error('linework: bundled fonts failed to load (assets/fonts). Reinstall linework.');
|
|
160
|
+
return new DrawioBrowser(browser, page);
|
|
161
|
+
}
|
|
162
|
+
measure(reqs) {
|
|
163
|
+
return this.page.evaluate((r) => window.aiviz.measure(r), reqs);
|
|
164
|
+
}
|
|
165
|
+
/** Returns .drawio.svg text with the model embedded. */
|
|
166
|
+
async renderSvg(xml) {
|
|
167
|
+
return embedFonts(await this.page.evaluate((x) => window.aiviz.render(x, true), xml));
|
|
168
|
+
}
|
|
169
|
+
/** Renders and screenshots the diagram. Writes `file` when given; always returns the PNG bytes. */
|
|
170
|
+
async renderPng(xml, file) {
|
|
171
|
+
await this.page.evaluate((x) => window.aiviz.render(x, false), xml);
|
|
172
|
+
return this.page.locator('#out svg').screenshot({ path: file, omitBackground: false });
|
|
173
|
+
}
|
|
174
|
+
/** Reroutes the given edge cells with libavoid. Returns mxGraphModel XML. */
|
|
175
|
+
reroute(xml, edgeIds) {
|
|
176
|
+
return this.page.evaluate(([x, ids]) => window.aiviz.reroute(x, ids), [xml, edgeIds]);
|
|
177
|
+
}
|
|
178
|
+
stencilNames(set) {
|
|
179
|
+
return this.page.evaluate((s) => window.aiviz.stencilNames(s), set);
|
|
180
|
+
}
|
|
181
|
+
hasStencil(name) {
|
|
182
|
+
return this.page.evaluate((n) => window.aiviz.hasStencil(n), name);
|
|
183
|
+
}
|
|
184
|
+
inspect(xml) {
|
|
185
|
+
return this.page.evaluate((x) => window.aiviz.inspect(x), xml);
|
|
186
|
+
}
|
|
187
|
+
close() {
|
|
188
|
+
return this.browser.close();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
async function launchChromium() {
|
|
192
|
+
const errors = [];
|
|
193
|
+
const attempts = [];
|
|
194
|
+
if (process.env.LINEWORK_CHROME)
|
|
195
|
+
attempts.push(() => chromium.launch({ executablePath: process.env.LINEWORK_CHROME }));
|
|
196
|
+
attempts.push(() => chromium.launch());
|
|
197
|
+
attempts.push(() => chromium.launch({ channel: 'chrome' }));
|
|
198
|
+
attempts.push(() => chromium.launch({ channel: 'msedge' }));
|
|
199
|
+
for (const a of attempts) {
|
|
200
|
+
try {
|
|
201
|
+
return await a();
|
|
202
|
+
}
|
|
203
|
+
catch (e) {
|
|
204
|
+
errors.push(e.message.split('\n')[0]);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
throw new Error('linework needs a Chromium-based browser to lay out and render diagrams, and none was found.\n' +
|
|
208
|
+
'Fix one of these ways:\n npx playwright install chromium\n install Google Chrome\n set LINEWORK_CHROME=/path/to/chrome\n' +
|
|
209
|
+
`(tried: ${errors.join(' | ')})`);
|
|
210
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Bundled fonts. The headless browser measures and renders with them, and every
|
|
2
|
+
// SVG gets a subset of the faces it uses (only the glyphs in its text), so a
|
|
3
|
+
// diagram looks the same on GitHub, in a browser and in a doc, and text that fit
|
|
4
|
+
// when linework sized the boxes still fits wherever it is shown.
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import subsetFont from 'subset-font';
|
|
8
|
+
import { assetsDir } from './browser.js';
|
|
9
|
+
export const FACES = [
|
|
10
|
+
{ family: 'IBM Plex Sans', weight: 400, file: 'plex-sans-400.woff2' },
|
|
11
|
+
{ family: 'IBM Plex Sans', weight: 600, file: 'plex-sans-600.woff2' },
|
|
12
|
+
{ family: 'Kalam', weight: 400, file: 'kalam-400.woff2' },
|
|
13
|
+
{ family: 'Kalam', weight: 700, file: 'kalam-700.woff2' },
|
|
14
|
+
{ family: 'IBM Plex Mono', weight: 400, file: 'plex-mono-400.woff2' },
|
|
15
|
+
{ family: 'IBM Plex Mono', weight: 600, file: 'plex-mono-600.woff2' },
|
|
16
|
+
];
|
|
17
|
+
export function fontsDir() {
|
|
18
|
+
return path.join(path.dirname(assetsDir()), 'fonts');
|
|
19
|
+
}
|
|
20
|
+
/** @font-face rules pointing at the font files, for pages that can fetch them. */
|
|
21
|
+
export function fontFaceCss(url) {
|
|
22
|
+
return FACES.map((f) => `@font-face{font-family:"${f.family}";font-weight:${f.weight};font-style:normal;src:url(${url(f.file)}) format("woff2");}`).join('\n');
|
|
23
|
+
}
|
|
24
|
+
const cache = new Map();
|
|
25
|
+
const read = (f) => {
|
|
26
|
+
let b = cache.get(f.file);
|
|
27
|
+
if (!b)
|
|
28
|
+
cache.set(f.file, (b = fs.readFileSync(path.join(fontsDir(), f.file))));
|
|
29
|
+
return b;
|
|
30
|
+
};
|
|
31
|
+
const ENT = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ' };
|
|
32
|
+
const decode = (s) => s.replace(/&(#x[0-9a-f]+|#\d+|\w+);/gi, (m, e) => e[0] === '#' ? String.fromCodePoint(e[1] === 'x' || e[1] === 'X' ? parseInt(e.slice(2), 16) : parseInt(e.slice(1), 10)) : (ENT[e] ?? m));
|
|
33
|
+
const STYLE_RE = /<style id="aiviz-fonts"[^>]*>[\s\S]*?<\/style>/g;
|
|
34
|
+
/** Families from FACES that the SVG asks for. */
|
|
35
|
+
export function familiesIn(svg) {
|
|
36
|
+
const used = new Set();
|
|
37
|
+
for (const m of svg.matchAll(/font-family(?:\s*:\s*|=")(?:"|["'])?([^,;"'&<]+)/g))
|
|
38
|
+
used.add(m[1].trim());
|
|
39
|
+
return [...new Set(FACES.map((f) => f.family))].filter((f) => used.has(f));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Put subset copies of the bundled fonts the SVG uses into its <defs>. Replaces
|
|
43
|
+
* a previous embed, so it's safe to run on a file again after an edit.
|
|
44
|
+
*/
|
|
45
|
+
export async function embedFonts(svg) {
|
|
46
|
+
svg = svg.replace(STYLE_RE, '');
|
|
47
|
+
const families = familiesIn(svg);
|
|
48
|
+
if (!families.length)
|
|
49
|
+
return svg;
|
|
50
|
+
let text = '';
|
|
51
|
+
for (const m of svg.matchAll(/>([^<>]+)</g))
|
|
52
|
+
text += decode(m[1]);
|
|
53
|
+
// Blueprint's group titles use CSS text-transform, so they need the capitals too.
|
|
54
|
+
text = [...new Set(text + text.toUpperCase() + ' ')].join('');
|
|
55
|
+
const rules = [];
|
|
56
|
+
for (const f of FACES.filter((x) => families.includes(x.family))) {
|
|
57
|
+
const sub = await subsetFont(read(f), text, { targetFormat: 'woff2' });
|
|
58
|
+
rules.push(`@font-face{font-family:"${f.family}";font-weight:${f.weight};font-style:normal;src:url(data:font/woff2;base64,${sub.toString('base64')}) format("woff2");}`);
|
|
59
|
+
}
|
|
60
|
+
const style = `<style id="aiviz-fonts" type="text/css">${rules.join('')}</style>`;
|
|
61
|
+
if (/<defs\s*\/>/.test(svg))
|
|
62
|
+
return svg.replace(/<defs\s*\/>/, `<defs>${style}</defs>`);
|
|
63
|
+
if (/<defs[^>]*>/.test(svg))
|
|
64
|
+
return svg.replace(/<defs[^>]*>/, (m) => m + style);
|
|
65
|
+
return svg.replace(/<svg\b[^>]*>/, (m) => `${m}<defs>${style}</defs>`);
|
|
66
|
+
}
|
package/dist/report.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export function buildLines(res, png) {
|
|
2
|
+
const s = res.check.stats;
|
|
3
|
+
const lines = [`wrote ${res.out} (${s.nodes} nodes, ${s.edges} edges, ${s.width}x${s.height}px, ${res.direction})`];
|
|
4
|
+
if (res.merge) {
|
|
5
|
+
const m = res.merge;
|
|
6
|
+
const parts = [`kept ${m.kept} positions${m.handMoved ? ` (${m.handMoved} set by hand)` : ''}`];
|
|
7
|
+
if (m.placed.length)
|
|
8
|
+
parts.push(`placed ${m.placed.join(', ')}`);
|
|
9
|
+
if (m.grown.length)
|
|
10
|
+
parts.push(`grew ${m.grown.join(', ')}`);
|
|
11
|
+
if (m.removed.length)
|
|
12
|
+
parts.push(`removed ${m.removed.join(', ')}`);
|
|
13
|
+
if (m.rerouted)
|
|
14
|
+
parts.push(`rerouted ${m.rerouted} edges`);
|
|
15
|
+
if (m.carried)
|
|
16
|
+
parts.push(`kept ${m.carried} notes/annotations`);
|
|
17
|
+
lines.push(`merged into the existing layout: ${parts.join('; ')}`);
|
|
18
|
+
if (m.keptUnknown.length)
|
|
19
|
+
lines.push(`note: kept ${m.keptUnknown.join(', ')}: added by hand, and this spec didn't come from the current file. To work from the latest version run show first; to delete them, remove them from that output.`);
|
|
20
|
+
}
|
|
21
|
+
if (res.discardedLayout)
|
|
22
|
+
lines.push('relayout: discarded the hand-arranged layout and laid out from scratch');
|
|
23
|
+
if (png)
|
|
24
|
+
lines.push(`preview ${png}`);
|
|
25
|
+
for (const w of res.warnings)
|
|
26
|
+
lines.push(`spec warning: ${w.path}: ${w.message}`);
|
|
27
|
+
return [...lines, ...checkLines(res.check)];
|
|
28
|
+
}
|
|
29
|
+
export function checkLines(res) {
|
|
30
|
+
const errors = res.findings.filter((f) => f.level === 'error');
|
|
31
|
+
const warnings = res.findings.filter((f) => f.level === 'warning');
|
|
32
|
+
return [
|
|
33
|
+
`check: ${errors.length} errors, ${warnings.length} warnings, ${res.stats.crossings} edge crossings`,
|
|
34
|
+
...res.findings.map((f) => ` ${f.level}: ${f.message} [${f.rule}]`),
|
|
35
|
+
];
|
|
36
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// The normalized diagram model every other module works from.
|
|
2
|
+
// The YAML the AI writes is parsed into this by parse.ts.
|
|
3
|
+
export const DIAGRAM_TYPES = ['architecture', 'flow', 'state', 'sequence', 'er'];
|
|
4
|
+
export const DIRECTIONS = ['auto', 'TB', 'LR', 'BT', 'RL'];
|
|
5
|
+
export const THEMES = ['clean', 'soft', 'dark', 'sketch', 'blueprint', 'c4'];
|
|
6
|
+
export const COLORS = ['blue', 'violet', 'pink', 'red', 'orange', 'amber', 'green', 'teal', 'gray'];
|
|
7
|
+
// Kinds are per diagram type so the AI gets a short, relevant list.
|
|
8
|
+
export const KINDS = {
|
|
9
|
+
architecture: {
|
|
10
|
+
service: { shape: 'box', color: 'violet', description: 'your own service, API or app server (default)' },
|
|
11
|
+
person: { shape: 'box', color: 'pink', description: 'a human user or role' },
|
|
12
|
+
client: { shape: 'box', color: 'blue', description: 'browser app, mobile app, CLI' },
|
|
13
|
+
function: { shape: 'box', color: 'violet', dashed: true, description: 'serverless function or job' },
|
|
14
|
+
database: { shape: 'cylinder', color: 'green', description: 'relational or document database' },
|
|
15
|
+
cache: { shape: 'box', color: 'green', description: 'Redis, Memcached, CDN cache' },
|
|
16
|
+
storage: { shape: 'cylinder', color: 'green', description: 'object or file storage, data lake' },
|
|
17
|
+
queue: { shape: 'pill', color: 'amber', description: 'queue, topic or event stream' },
|
|
18
|
+
external: { shape: 'box', color: 'gray', dashed: true, description: 'third-party system you do not own' },
|
|
19
|
+
},
|
|
20
|
+
flow: {
|
|
21
|
+
step: { shape: 'box', color: 'neutral', description: 'an action or process step (default)' },
|
|
22
|
+
decision: { shape: 'diamond', color: 'amber', description: 'a yes/no or multi-way branch; label outgoing edges' },
|
|
23
|
+
terminal: { shape: 'pill', color: 'gray', description: 'start or end of the flow' },
|
|
24
|
+
data: { shape: 'parallelogram', color: 'blue', description: 'input or output data' },
|
|
25
|
+
error: { shape: 'box', color: 'red', semantic: true, description: 'failure or rejection path' },
|
|
26
|
+
},
|
|
27
|
+
state: {
|
|
28
|
+
state: { shape: 'box', color: 'violet', description: 'a state (default)' },
|
|
29
|
+
success: { shape: 'box', color: 'green', semantic: true, description: 'a successful terminal state' },
|
|
30
|
+
error: { shape: 'box', color: 'red', semantic: true, description: 'a failure state' },
|
|
31
|
+
start: { shape: 'start', color: 'ink', description: 'initial pseudo-state; usually written as [*] in edges' },
|
|
32
|
+
end: { shape: 'end', color: 'ink', description: 'final pseudo-state; usually written as [*] in edges' },
|
|
33
|
+
},
|
|
34
|
+
sequence: {
|
|
35
|
+
service: { shape: 'box', color: 'violet', description: 'your own service or component (default)' },
|
|
36
|
+
person: { shape: 'box', color: 'pink', description: 'a human actor' },
|
|
37
|
+
client: { shape: 'box', color: 'blue', description: 'browser, mobile app, CLI' },
|
|
38
|
+
function: { shape: 'box', color: 'violet', dashed: true, description: 'serverless function or job' },
|
|
39
|
+
database: { shape: 'box', color: 'green', description: 'database or cache' },
|
|
40
|
+
queue: { shape: 'box', color: 'amber', description: 'queue, topic or event stream' },
|
|
41
|
+
external: { shape: 'box', color: 'gray', dashed: true, description: 'third-party system' },
|
|
42
|
+
},
|
|
43
|
+
er: {
|
|
44
|
+
entity: { shape: 'table', color: 'violet', description: 'a table or entity (default)' },
|
|
45
|
+
view: { shape: 'table', color: 'blue', dashed: true, description: 'a view or derived table' },
|
|
46
|
+
external: { shape: 'table', color: 'gray', dashed: true, description: 'a table owned by another system' },
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
export const DEFAULT_KIND = { architecture: 'service', flow: 'step', state: 'state', sequence: 'service', er: 'entity' };
|
|
50
|
+
export const FRAME_OPS = ['alt', 'par', 'opt', 'loop', 'critical', 'break'];
|
|
51
|
+
export function kindDef(type, kind) {
|
|
52
|
+
return KINDS[type][kind] ?? KINDS[type][DEFAULT_KIND[type]];
|
|
53
|
+
}
|