clay-server 4.8.2 → 5.0.0-beta.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.
Files changed (82) hide show
  1. package/LICENSES/README.md +13 -0
  2. package/LICENSES/open-iconic.txt +21 -0
  3. package/LICENSES/plantuml-salt.txt +674 -0
  4. package/lib/assets/salt-assets.json +15 -0
  5. package/lib/assets/salt-colors.json +156 -0
  6. package/lib/assets/salt-font-10.bin.gz +0 -0
  7. package/lib/assets/salt-font-13.bin.gz +0 -0
  8. package/lib/assets/salt-font-14.bin.gz +0 -0
  9. package/lib/assets/salt-font-16.bin.gz +0 -0
  10. package/lib/assets/salt-font-18.bin.gz +0 -0
  11. package/lib/assets/salt-font-20.bin.gz +0 -0
  12. package/lib/assets/salt-font-metrics.bin.gz +0 -0
  13. package/lib/assets/salt-font-metrics.json +101 -0
  14. package/lib/assets/salt-font-monospace-10.bin.gz +0 -0
  15. package/lib/assets/salt-font-monospace-13.bin.gz +0 -0
  16. package/lib/assets/salt-font-monospace-14.bin.gz +0 -0
  17. package/lib/assets/salt-font-monospace-16.bin.gz +0 -0
  18. package/lib/assets/salt-font-monospace-18.bin.gz +0 -0
  19. package/lib/assets/salt-font-monospace-20.bin.gz +0 -0
  20. package/lib/assets/salt-font-monospace.bin.gz +0 -0
  21. package/lib/assets/salt-icons.json +671 -0
  22. package/lib/bundled-skills/clay-sketch/SKILL.md +198 -0
  23. package/lib/mcp-browser-proxy.js +37 -0
  24. package/lib/mcp-connection-store.js +39 -0
  25. package/lib/mcp-network.js +46 -0
  26. package/lib/mcp-oauth.js +65 -0
  27. package/lib/mcp-proxy.js +32 -0
  28. package/lib/mcp-remote-connections.js +215 -0
  29. package/lib/mcp-session-access.js +15 -0
  30. package/lib/plantuml-renderer.js +43 -0
  31. package/lib/project-file-http.js +2 -0
  32. package/lib/project-mcp.js +130 -357
  33. package/lib/project-plantuml.js +48 -0
  34. package/lib/project-session-document.js +17 -1
  35. package/lib/project-user-message.js +2 -0
  36. package/lib/project-wireframe-http.js +88 -0
  37. package/lib/project.js +28 -45
  38. package/lib/public/app.js +7 -0
  39. package/lib/public/css/filebrowser.css +5 -0
  40. package/lib/public/css/mcp-connections.css +86 -0
  41. package/lib/public/css/skills-workbench.css +74 -0
  42. package/lib/public/css/wireframe.css +95 -0
  43. package/lib/public/index.html +16 -15
  44. package/lib/public/modules/app-messages.js +10 -2
  45. package/lib/public/modules/filebrowser.js +29 -3
  46. package/lib/public/modules/markdown.js +3 -1
  47. package/lib/public/modules/mcp-connection-view.js +136 -0
  48. package/lib/public/modules/mcp-permissions.js +122 -0
  49. package/lib/public/modules/mcp-ui.js +85 -461
  50. package/lib/public/modules/plantuml-preview.js +76 -0
  51. package/lib/public/modules/skills-actions.js +47 -79
  52. package/lib/public/modules/skills-detail.js +32 -145
  53. package/lib/public/modules/skills-list-view.js +29 -0
  54. package/lib/public/modules/skills-state.js +4 -2
  55. package/lib/public/modules/skills.js +104 -399
  56. package/lib/public/modules/split-view.js +14 -0
  57. package/lib/public/modules/vendor-login-browser.js +16 -5
  58. package/lib/public/modules/wireframe-chat.js +52 -0
  59. package/lib/public/modules/wireframe-render.js +69 -0
  60. package/lib/public/modules/wireframe-style.js +6 -0
  61. package/lib/public/modules/wireframe-viewport.js +70 -0
  62. package/lib/public/modules/wireframe-workbench.js +116 -0
  63. package/lib/public/style.css +3 -0
  64. package/lib/salt-chrome.js +36 -0
  65. package/lib/salt-hand.js +37 -0
  66. package/lib/salt-images.js +87 -0
  67. package/lib/salt-layout.js +60 -0
  68. package/lib/salt-options.js +23 -0
  69. package/lib/salt-overlays.js +45 -0
  70. package/lib/salt-parser.js +153 -0
  71. package/lib/salt-screen.js +111 -0
  72. package/lib/salt-svg-shapes.js +80 -0
  73. package/lib/salt-svg.js +164 -0
  74. package/lib/salt-text.js +112 -0
  75. package/lib/salt-tree.js +45 -0
  76. package/lib/sdk-bridge.js +5 -17
  77. package/lib/sdk-mcp-descriptors.js +2 -1
  78. package/lib/session-document-mcp-server.js +9 -0
  79. package/lib/vendor-login-browser-worker.js +18 -8
  80. package/lib/vendor-login-browser.js +10 -3
  81. package/lib/yoke/adapters/codex.js +1 -18
  82. package/package.json +6 -2
@@ -0,0 +1,153 @@
1
+ // Clay's bounded parser for Salt wireframes; no PlantUML preprocessor execution.
2
+ function fail(message) { throw Object.assign(new Error(message), { status: 422 }); }
3
+ function tokenize(body) {
4
+ var tokens = [];
5
+ var text = '';
6
+ var quoted = false;
7
+ function flush() { if (text.trim()) tokens.push({ type: 'text', text: text.trim() }); text = ''; }
8
+ for (var i = 0; i < body.length; i++) {
9
+ var c = body[i];
10
+ if (c === '"') quoted = !quoted;
11
+ if (!quoted && (c === '{' || c === '}' || c === '|' || c === '\n')) {
12
+ flush();
13
+ var token = { type: c };
14
+ if (c === '{') {
15
+ token.style = '';
16
+ if (/[#!+\-^/*ST]/.test(body[i + 1] || ' ')) token.style = body[++i];
17
+ if (token.style === 'S' && /[I-]/.test(body[i + 1] || ' ')) token.style += body[++i];
18
+ if (token.style === 'T' && /[!+#-]/.test(body[i + 1] || ' ')) token.style += body[++i];
19
+ if (token.style === '^') {
20
+ var title = /^[ \t]*"([^"\n]*)"/.exec(body.slice(i + 1));
21
+ if (title) { token.title = title[1]; i += title[0].length; }
22
+ else { var unquoted = /^[^\n|{}]*/.exec(body.slice(i + 1))[0]; token.title = unquoted.trim(); i += unquoted.length; }
23
+ }
24
+ if (body[i + 1] && !/[\s{}|]/.test(body[i + 1]) && !token.style) {
25
+ fail('Unsupported Salt container. Use {, {+, {#, {!, {-, {^, {/, or {*.');
26
+ }
27
+ }
28
+ tokens.push(token);
29
+ } else text += c;
30
+ }
31
+ if (quoted) fail('Unclosed quoted text field.');
32
+ flush();
33
+ return tokens;
34
+ }
35
+
36
+ function label(text) {
37
+ return require('./salt-text').parseLabel(text);
38
+ }
39
+ function widget(text) {
40
+ var result = { type: 'label' };
41
+ var match;
42
+ if (text === '*') return { type: 'span' };
43
+ if (/^[.!]?$/.test(text)) result.type = 'space';
44
+ else if (/^(\.\.|==|~~|--).*\1$/.test(text) || /^(\.\.|==|~~|--)$/.test(text)) { result.type = 'separator'; result.style = text.slice(0, 2); text = ''; }
45
+ else if ((match = /^\[([ X]?)\]\s*(.*)$/.exec(text))) { result.type = 'checkbox'; result.checked = /x/i.test(match[1]); text = match[2]; }
46
+ else if ((match = /^\(([ X]?)\)\s*(.*)$/.exec(text))) { result.type = 'radio'; result.checked = /x/i.test(match[1]); text = match[2]; }
47
+ else if ((match = /^\[([^\]]+)\]$/.exec(text))) { result.type = 'button'; text = match[1]; }
48
+ else if ((match = /^"([^"\n]*)"$/.exec(text))) { result.type = 'input'; text = match[1]; }
49
+ else if (/^\^.*\^$/.test(text)) {
50
+ var choices = text.slice(1, -1).split('^').filter(Boolean);
51
+ result.type = 'select'; result.options = choices.slice(1).map(label); text = choices[0] || '';
52
+ }
53
+ else if (!/^\[x\]/.test(text) && /^[\["^]|^\*/.test(text) && !/^\*\*[^*]+\*\*$/.test(text) && !/^\*+\s+/.test(text)) fail('Unsupported or incomplete control: ' + text.slice(0, 60));
54
+ if (text === '*') return { type: 'span' };
55
+ result.charLength = text.replace(/<&[-\w]+>/g, '00').replace(/<\/?(?:b|i|s|u|w|color|back|font|size)(?::[^>]+)?>/gi, '').length;
56
+ return Object.assign(result, label(/^(button|input|select)$/.test(result.type) ? text.trim() : text));
57
+ }
58
+
59
+ function parseSalt(source) {
60
+ var screen = require('./salt-screen').parse(source);
61
+ if (/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/.test(source)) fail("Wireframe source contains unsupported control characters.");
62
+ var body;
63
+ var match = /^\s*@startsalt(?:[ \t]+[\w-]+)?[ \t]*\r?\n([\s\S]*?)\r?\n\s*@endsalt\s*$/i.exec(source);
64
+ if (match) body = match[1];
65
+ else {
66
+ match = /^\s*@startuml(?:[ \t]+[\w-]+)?[ \t]*\r?\n\s*salt\s*\r?\n([\s\S]*?)\r?\n\s*@enduml\s*$/i.exec(source);
67
+ if (match) body = match[1];
68
+ }
69
+ if (body === undefined) fail('The built-in viewer supports Salt wireframes. Use @startsalt / @endsalt around a layout. Other PlantUML diagram types are not supported.');
70
+ body = body.replace(/^[ \t]*'[^\n]*$/gm, '').replace(/\r/g, '');
71
+ var settings = require('./salt-options').parse(body);
72
+ body = settings.body;
73
+ if (/^[ \t]*(?:!|skinparam\b|scale\b|sprite\b)|%[a-z_]+\s*\(/im.test(body)) fail('PlantUML preprocessing, includes, macros, and unrecognized directives are not supported in built-in wireframes.');
74
+ var sprites = Object.create(null);
75
+ var spriteCount = 0;
76
+ body = body.replace(/<<([\w]*)\s*\n([.X\s]+?)\n\s*>>/g, function (_, name, pixels) {
77
+ var rows = pixels.trim().split(/\n/).map(function (line) { return line.trim(); });
78
+ if (!rows.length || rows.some(function (line) { return !/^[.X]+$/.test(line) || line.length !== rows[0].length; })) fail('Sprite rows must have equal widths and contain only . or X.');
79
+ var id = '__clay_sprite_' + spriteCount++ + '__';
80
+ sprites[id] = { type: 'sprite', pixels: rows, width: rows[0].length, height: rows.length };
81
+ if (name) sprites['<<' + name + '>>'] = sprites[id];
82
+ return id;
83
+ });
84
+ var tokens = tokenize(body);
85
+ var index = 0;
86
+ var count = 0;
87
+ function skipNewlines() { while (tokens[index] && tokens[index].type === '\n') index++; }
88
+ function container(depth) {
89
+ if (depth > 24) fail('Wireframe nesting is too deep (24 levels maximum).');
90
+ var start = tokens[index++];
91
+ var node = { type: 'container', style: start.style, title: start.title || '', titleData: start.title ? label(start.title) : null, rows: [] };
92
+ if (start.style[0] === 'S') { node.scroll = start.style; node.style = '+'; }
93
+ var row = [];
94
+ var expectsCell = false;
95
+ function cell(value) {
96
+ if (++count > 2000) fail('Wireframe has too many cells (2000 maximum).');
97
+ if (row.length >= 64) fail('Wireframe has too many columns (64 maximum).');
98
+ row.push(value); expectsCell = false;
99
+ }
100
+ function endRow() { if (expectsCell) cell(widget('.')); if (row.length) node.rows.push(row); row = []; }
101
+ while (index < tokens.length) {
102
+ var token = tokens[index];
103
+ if (token.type === '}') {
104
+ index++; endRow();
105
+ if (node.style === '*') {
106
+ node.popups = node.rows.slice(1); node.rows = node.rows.slice(0, 1);
107
+ node.popups.forEach(function (items) {
108
+ if (!node.rows[0].some(function (entry) { return entry.text === items[0].text; })) fail('Open menu must name an existing menu entry.');
109
+ });
110
+ }
111
+ return node;
112
+ }
113
+ if (token.type === '\n') {
114
+ if (node.style === '/' && row.length && !expectsCell) node.verticalTabs = true;
115
+ index++;
116
+ // A newline before/after a column separator is only formatting.
117
+ skipNewlines();
118
+ if (!expectsCell && (!tokens[index] || tokens[index].type !== '|')) endRow();
119
+ } else if (token.type === '|') {
120
+ if (!row.length || expectsCell) cell(widget('.'));
121
+ expectsCell = true; index++;
122
+ } else if (token.type === '{' || token.type === 'text') {
123
+ if (row.length && !expectsCell) fail('Separate cells with | or a newline.');
124
+ var value;
125
+ if (token.type === '{') value = container(depth + 1);
126
+ else { index++; value = sprites[token.text] ? Object.assign({}, sprites[token.text]) : widget(token.text); }
127
+ if (node.style[0] === 'T' && !row.length && value.type === 'label') {
128
+ var level = /^\+*/.exec(value.text)[0].length;
129
+ value = Object.assign({ type: 'label', level: level }, label(value.text.slice(level).trim()));
130
+ }
131
+ if (value.type === 'span') {
132
+ var previous = row[row.length - 1];
133
+ while (previous && previous.type === 'span') previous = previous.owner;
134
+ if (!previous) fail('A spanning cell needs a cell to its left.');
135
+ previous.span = (previous.span || 1) + 1; value.owner = previous;
136
+ }
137
+ cell(value);
138
+ } else fail('Unexpected Salt syntax.');
139
+ }
140
+ fail('Unclosed layout container. Add a closing }.');
141
+ }
142
+ skipNewlines();
143
+ if (!tokens[index] || tokens[index].type !== '{') fail('A Salt layout must start with {.');
144
+ var root = container(0);
145
+ skipNewlines();
146
+ if (index !== tokens.length) fail('Unexpected content after the layout. Use one outer container.');
147
+ root.options = settings.options;
148
+ if (screen) root.options.screen = screen;
149
+ root.options.labels = {};
150
+ ['title', 'header', 'footer', 'caption', 'legend'].forEach(function (name) { if (settings.options[name]) root.options.labels[name] = settings.options[name].split('\n').map(label); });
151
+ return root;
152
+ }
153
+ module.exports = { parseSalt: parseSalt };
@@ -0,0 +1,111 @@
1
+ // Optional Clay screen geometry. Plain Salt keeps its reference layout.
2
+ function fail(message) { throw Object.assign(new Error('Clay screen layout: ' + message), { status: 422 }); }
3
+ function parse(source) {
4
+ var screen = null;
5
+ var rules = Object.create(null);
6
+ source.replace(/^[ \t]*'[ \t]*@clay-(canvas|layout)[ \t]+([^\r\n]*)/gm, function (_, kind, value) {
7
+ if (kind === 'canvas') {
8
+ if (screen) fail('use one canvas declaration.');
9
+ var dimensions = /^(\d+)\s+(\d+)\s*$/.exec(value);
10
+ if (!dimensions) fail('canvas requires width and height in pixels.');
11
+ screen = { width: Number(dimensions[1]), height: Number(dimensions[2]), rules: rules };
12
+ if (screen.width < 240 || screen.height < 240 || screen.width > 4096 || screen.height > 4096) fail('canvas dimensions must be 240–4096 pixels.');
13
+ } else {
14
+ var parts = value.trim().split(/\s+/);
15
+ var key = parts.shift();
16
+ if (!/^root(?:\.\d+\.\d+)*$/.test(key) || rules[key]) fail('layout paths must be unique, such as root.1.0.');
17
+ var rule = {};
18
+ parts.forEach(function (part) {
19
+ var pair = /^(padding|gap|columns|rows)=(.+)$/.exec(part);
20
+ if (!pair || rule[pair[1]] !== undefined) fail('use padding, gap, columns or rows once per layout.');
21
+ if (/padding|gap/.test(pair[1])) {
22
+ if (!/^\d+$/.test(pair[2]) || Number(pair[2]) > 128) fail('padding and gap must be 0–128 pixels.');
23
+ rule[pair[1]] = Number(pair[2]);
24
+ } else {
25
+ var tracks = pair[2].split(',');
26
+ if (tracks.length > 64 || tracks.some(function (track) { return !/^(auto|\*|\d+)$/.test(track) || (/^\d+$/.test(track) && (Number(track) < 1 || Number(track) > 4096)); })) fail('tracks must be auto, *, or 1–4096 pixels.');
27
+ rule[pair[1]] = tracks;
28
+ }
29
+ });
30
+ rules[key] = rule;
31
+ }
32
+ return '';
33
+ });
34
+ if (!screen && Object.keys(rules).length) fail('layout rules require a canvas declaration.');
35
+ return screen;
36
+ }
37
+ function normal(node) { return node.type === 'container' && !/^[T/*]/.test(node.style) && !node.scroll; }
38
+ function tracks(spec, minimum, total, gap, name) {
39
+ if (spec && spec.length !== minimum.length) fail(name + ' track count does not match its layout.');
40
+ var sizes = minimum.map(function (min, i) {
41
+ var track = spec ? spec[i] : 'auto';
42
+ if (/^\d+$/.test(track) && Number(track) + .01 < min) fail(name + ' track ' + (i + 1) + ' needs at least ' + Math.ceil(min) + ' pixels.');
43
+ return /^\d+$/.test(track) ? Number(track) : min;
44
+ });
45
+ if (total !== undefined) {
46
+ var remaining = total - sizes.reduce(function (a, b) { return a + b; }, 0) - gap * (sizes.length - 1);
47
+ if (remaining < -.01) fail(name + ' overflows by ' + Math.ceil(-remaining) + ' pixels. Increase the canvas/track or simplify its contents.');
48
+ var flexible = sizes.map(function (_, i) { return spec && spec[i] === '*' ? i : -1; }).filter(function (i) { return i >= 0; });
49
+ flexible.forEach(function (i) { sizes[i] += remaining / flexible.length; });
50
+ }
51
+ return sizes;
52
+ }
53
+ function prepare(node, screen, key, used) {
54
+ if (!normal(node)) { if (screen.rules[key]) fail(key + ' must target a regular layout container.'); return; }
55
+ var rule = screen.rules[key] || {};
56
+ used.add(key);
57
+ var padding = rule.padding === undefined ? 16 : rule.padding;
58
+ var gap = rule.gap === undefined ? 12 : rule.gap;
59
+ var columns = Array(node.columns.length).fill(0), rows = Array(node.rows.length).fill(0);
60
+ node.rows.forEach(function (row, r) { row.forEach(function (child, c) {
61
+ if (child.type === 'span') return;
62
+ prepare(child, screen, key + '.' + r + '.' + c, used);
63
+ if (!child.span) columns[c] = Math.max(columns[c], child.width);
64
+ rows[r] = Math.max(rows[r], child.height);
65
+ }); });
66
+ node.rows.forEach(function (row) { row.forEach(function (child, c) {
67
+ if (!child.span || child.type === 'span') return;
68
+ var available = columns.slice(c, c + child.span).reduce(function (a, b) { return a + b; }, 0) + gap * (child.span - 1);
69
+ columns[c + child.span - 1] += Math.max(0, child.width - available);
70
+ }); });
71
+ columns = tracks(rule.columns, columns, undefined, gap, key + ' columns');
72
+ rows = tracks(rule.rows, rows, undefined, gap, key + ' rows');
73
+ node.screen = { key: key, padding: padding, gap: gap, rule: rule, columns: columns, rows: rows };
74
+ node.width = columns.reduce(function (a, b) { return a + b; }, 0) + gap * (columns.length - 1) + padding * 2;
75
+ node.height = rows.reduce(function (a, b) { return a + b; }, 0) + gap * (rows.length - 1) + padding * 2 + node.titleLabel.height;
76
+ }
77
+ function place(node, width, height) {
78
+ if (!node.screen) return;
79
+ var box = node.screen, p = box.padding, gap = box.gap;
80
+ node.width = width; node.height = height;
81
+ var cols = tracks(box.rule.columns, box.columns, width - p * 2, gap, box.key + ' columns');
82
+ var rows = tracks(box.rule.rows, box.rows, height - p * 2 - node.titleLabel.height, gap, box.key + ' rows');
83
+ var y = p + node.titleLabel.height;
84
+ box.cells = []; box.vertical = []; box.horizontal = [];
85
+ node.rows.forEach(function (row, r) {
86
+ var x = p;
87
+ row.forEach(function (child, c) {
88
+ if (child.type !== 'span') {
89
+ var w = cols.slice(c, c + (child.span || 1)).reduce(function (a, b) { return a + b; }, 0) + gap * ((child.span || 1) - 1);
90
+ var yy = child.type === 'container' ? y : y + Math.max(0, (rows[r] - child.height) / 2);
91
+ if (child.type === 'input') child.width = w;
92
+ box.cells.push({ node: child, x: x, y: yy, width: w, height: rows[r] });
93
+ place(child, w, rows[r]);
94
+ if (c > 0) box.vertical.push({ x: x - gap / 2, y: y - gap / 2, height: rows[r] + gap });
95
+ }
96
+ x += cols[c] + gap;
97
+ });
98
+ if (r > 0) box.horizontal.push(y - gap / 2);
99
+ y += rows[r] + gap;
100
+ });
101
+ }
102
+ function apply(root) {
103
+ var screen = root.options.screen;
104
+ if (!screen) return;
105
+ if (!normal(root)) fail('the canvas needs a regular outer container.');
106
+ if (root.options.scale !== 1 || ['title', 'header', 'footer', 'caption', 'legend'].some(function (key) { return root.options[key]; })) fail('put headings inside the canvas; scale and outside annotations are not supported in screen mode.');
107
+ var used = new Set(); prepare(root, screen, 'root', used);
108
+ Object.keys(screen.rules).forEach(function (key) { if (!used.has(key)) fail('no container at ' + key + '.'); });
109
+ place(root, screen.width - 2, screen.height - 2);
110
+ }
111
+ module.exports = { parse: parse, apply: apply };
@@ -0,0 +1,80 @@
1
+ // Reference SVG primitives; text is escaped and images contain resolved PNG bytes.
2
+ function escape(value) { return String(value).replace(/[&<>"']/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&apos;' }[c]; }); }
3
+ function createShapes(out, scale, handwritten) {
4
+ scale = scale || 1;
5
+ function number(value) { return String(Math.round(value * scale * 1000) / 1000); }
6
+ var ink = '#000';
7
+ var bounds = { x: 0, y: 0 };
8
+ var filters = Object.create(null);
9
+ function include(x, y) { bounds.x = Math.max(bounds.x, x); bounds.y = Math.max(bounds.y, y); }
10
+ function rect(x, y, w, h, fill, stroke, radius, thickness) {
11
+ include(x + w, y + h);
12
+ if (handwritten) {
13
+ var points = require('./salt-hand').rect(w, h, radius).map(function (p) { return [p[0] + x, p[1] + y]; });
14
+ out.push('<polygon points="' + points.map(function (p) { return p.map(number).join(','); }).join(' ') + '" fill="' + (fill || 'none') + '" stroke="' + (stroke || '#000') + '" stroke-width="' + number(thickness || 1) + '" stroke-linejoin="miter" stroke-miterlimit="10"/>'); return;
15
+ }
16
+ out.push('<rect x="' + number(x) + '" y="' + number(y) + '" width="' + number(w) + '" height="' + number(h) + '" fill="' + (fill || 'none') + '" stroke="' + (stroke || '#000') + '" stroke-width="' + number(thickness || 1) + '"' + (radius ? ' rx="' + number(radius) + '" ry="' + number(radius) + '"' : '') + '/>');
17
+ }
18
+ function line(x, y, xx, yy, stroke, thickness, dash) {
19
+ include(x, y); include(xx, yy);
20
+ if (handwritten) {
21
+ var points = require('./salt-hand').line(xx - x, yy - y);
22
+ out.push('<path d="' + points.map(function (p, i) { return (i ? 'L' : 'M') + number(p[0] + x) + ',' + number(p[1] + y); }).join(' ') + '" fill="none" stroke="' + (stroke || '#000') + '" stroke-width="' + number(thickness || 1) + '"/>'); return;
23
+ }
24
+ out.push('<line x1="' + number(x) + '" y1="' + number(y) + '" x2="' + number(xx) + '" y2="' + number(yy) + '" stroke="' + (stroke || '#000') + '" stroke-width="' + number(thickness || 1) + '"' + (dash ? ' stroke-dasharray="' + dash + '"' : '') + '/>');
25
+ }
26
+ function ellipse(x, y, w, h, fill) {
27
+ include(x + w, y + h);
28
+ out.push('<ellipse cx="' + number(x + w / 2) + '" cy="' + number(y + h / 2) + '" rx="' + number(w / 2) + '" ry="' + number(h / 2) + '" fill="' + (fill || 'none') + '" stroke="#000" stroke-width="' + number(1.5) + '"/>');
29
+ }
30
+ function polygon(points, thickness) {
31
+ points.forEach(function (p) { include(p[0], p[1]); });
32
+ out.push('<polygon points="' + points.map(function (p) { return p.map(number).join(','); }).join(' ') + '" fill="#000" stroke="#000" stroke-linejoin="miter" stroke-miterlimit="10" stroke-width="' + number(thickness || 1) + '"/>');
33
+ }
34
+ function path(d, fill, stroke, maxX, maxY) {
35
+ include(maxX, maxY);
36
+ out.push('<path d="' + d.replace(/-?\d+(?:\.\d+)?/g, function (n) { return number(Number(n)); }) + '" fill="' + fill + '"' + (stroke ? ' stroke="' + stroke + '" stroke-width="' + number(1) + '"' : '') + '/>');
37
+ }
38
+ function label(label, x, y) {
39
+ include(x + label.width, y + label.height - Math.min(...label.runs.map(function (r) { return r.descent || 0; })));
40
+ if (label.list) {
41
+ var level = label.list.level;
42
+ if (label.list.type === 'number') {
43
+ var prefix = require('./salt-text').measureLabel(require('./salt-text').parseLabel('1. '));
44
+ var start = x + label.indent - prefix.width;
45
+ labelText(prefix.runs[0], start, y + label.height - prefix.runs[0].descent);
46
+ } else if (level === 1) out.push('<ellipse cx="' + number(x + 5.5) + '" cy="' + number(y + label.height - 7.5) + '" rx="' + number(2.5) + '" ry="' + number(2.5) + '" fill="#000"/>');
47
+ else out.push('<rect x="' + number(x + level * 8 - 7) + '" y="' + number(y + label.height - 10) + '" width="' + number(3.5) + '" height="' + number(3.5) + '" fill="#000"/>');
48
+ x += label.indent;
49
+ }
50
+ label.runs.forEach(function (run) {
51
+ ink = run.color || '#000';
52
+ if (run.icon) {
53
+ var iy = y + Math.max(0, label.height - 11);
54
+ require('./salt-text').icons[run.icon].forEach(function (d) {
55
+ var axis = 0;
56
+ var moved = d.replace(/-?\d+(?:\.\d+)?/g, function (n) { return number(Number(n) + (axis++ % 2 ? iy - 6 : x - 6)); });
57
+ out.push('<path d="' + moved + '" fill="' + (run.color || '#000') + '"/>');
58
+ });
59
+ }
60
+ if (run.image) out.push('<image x="' + number(x) + '" y="' + number(y + label.height - run.height) + '" width="' + number(run.width) + '" height="' + number(run.height) + '" href="' + run.image.data + '"/>');
61
+ if (run.displayText) labelText(run, x, y + label.height - run.descent);
62
+ x += run.width;
63
+ });
64
+ }
65
+ function labelText(run, x, baseline) {
66
+ var decoration = run.wave ? 'wavy underline' : run.underline && !run.underlineColor ? 'underline' : run.strike && !run.strikeColor ? 'line-through' : '';
67
+ var filter = '';
68
+ if (run.background) {
69
+ var id = filters[run.background];
70
+ if (!id) { id = 'salt-back-' + Object.keys(filters).length; filters[run.background] = id; out.push('<defs><filter id="' + id + '" x="0" y="0" width="1" height="1"><feFlood flood-color="' + run.background + '" result="flood"/><feComposite in="SourceGraphic" in2="flood" operator="over"/></filter></defs>'); }
71
+ filter = ' filter="url(#' + id + ')"';
72
+ }
73
+ out.push('<text x="' + number(x + run.leadingWidth) + '" y="' + number(baseline) + '" fill="' + (run.color || '#000') + '" font-size="' + number(run.size || 12) + '"' + (run.displayText.length > 1 ? ' textLength="' + number(run.displayWidth) + '"' : '') + (run.bold ? ' font-weight="700"' : '') + (run.italic ? ' font-style="italic"' : '') + (run.mono ? ' font-family="monospace"' : '') + (decoration ? ' text-decoration="' + decoration + '"' : '') + filter + '>' + escape(run.displayText) + '</text>');
74
+ var size = run.size || 12;
75
+ if (run.strike && run.strikeColor) line(x + run.leadingWidth, baseline - size / 4, x + run.leadingWidth + run.displayWidth, baseline - size / 4, run.strikeColor, size / 28);
76
+ if (run.underline && run.underlineColor) line(x + run.leadingWidth, baseline + size / 14, x + run.leadingWidth + run.displayWidth, baseline + size / 14, run.underlineColor, size / 28);
77
+ }
78
+ return { lastInk: function () { return ink; }, path: path, bounds: bounds, rect: rect, line: line, ellipse: ellipse, polygon: polygon, label: label };
79
+ }
80
+ module.exports = { createShapes: createShapes };
@@ -0,0 +1,164 @@
1
+ // Geometry adapted from PlantUML 1.2026.8 Salt, (c) Arnaud Roques, GPL-3.0-or-later.
2
+ // See LICENSES/plantuml-salt.txt. Rendering is local and bounded; image bytes are pre-resolved.
3
+ var layout = require('./salt-layout');
4
+ var shapes = require('./salt-svg-shapes');
5
+ var overlays = require('./salt-overlays');
6
+ function renderSaltSvg(root) {
7
+ layout.measure(root);
8
+ require('./salt-screen').apply(root);
9
+ var out = [];
10
+ var scale = (root.options || {}).scale || 1;
11
+ var s = shapes.createShapes(out, scale, (root.options || {}).handwritten);
12
+ var front = [];
13
+ function grid(node, x, y) {
14
+ var horizontal = new Set();
15
+ var vertical = new Set();
16
+ var rows = node.heights.length;
17
+ var cols = node.columns.length;
18
+ var style = node.style;
19
+ if (/[+#^]/.test(style)) {
20
+ for (var c = 0; c < cols; c++) { horizontal.add('0,' + c); horizontal.add(rows + ',' + c); }
21
+ for (var r = 0; r < rows; r++) { vertical.add(r + ',0'); vertical.add(r + ',' + cols); }
22
+ }
23
+ node.rows.forEach(function (row, r) {
24
+ row.forEach(function (child, c) {
25
+ if (child.type === 'span') return;
26
+ var end = c + (child.span || 1);
27
+ if (/[#-]/.test(style)) for (var cc = c; cc < end; cc++) { horizontal.add(r + ',' + cc); horizontal.add((r + 1) + ',' + cc); }
28
+ if (/[#!]/.test(style)) { vertical.add(r + ',' + c); vertical.add(r + ',' + end); }
29
+ });
30
+ });
31
+ function referenceOrder(set) {
32
+ var capacity = 16;
33
+ while (set.size > capacity * 0.75) capacity *= 2;
34
+ function bucket(key) {
35
+ var p = key.split(',').map(Number);
36
+ var hash = p[0] * 47 + p[1];
37
+ return (hash ^ (hash >>> 16)) & (capacity - 1);
38
+ }
39
+ return Array.from(set).sort(function (a, b) { return bucket(a) - bucket(b); });
40
+ }
41
+ referenceOrder(horizontal).forEach(function (key) {
42
+ var p = key.split(',').map(Number);
43
+ s.line(x + node.xs[p[1]], y + node.ys[p[0]], x + node.xs[p[1] + 1], y + node.ys[p[0]]);
44
+ });
45
+ referenceOrder(vertical).forEach(function (key) {
46
+ var p = key.split(',').map(Number);
47
+ s.line(x + node.xs[p[1]], y + node.ys[p[0]], x + node.xs[p[1]], y + node.ys[p[0] + 1]);
48
+ });
49
+ if (node.title) {
50
+ s.rect(x + 6, y, node.titleLabel.width, node.titleLabel.height, '#FFF', '#FFF');
51
+ s.label(node.titleLabel, x + 6, y);
52
+ }
53
+ }
54
+ function tabs(node, x, y) {
55
+ var offset = 0;
56
+ node.items.forEach(function (child) {
57
+ var w = child.width;
58
+ var h = child.height;
59
+ if (node.vertical) {
60
+ s.label(child.label, x, y + offset + 2);
61
+ s.line(x, y + offset, x + node.width, y + offset);
62
+ s.line(x, y + offset, x, y + offset + h + 5);
63
+ s.line(x, y + offset + h + 5, x + node.width, y + offset + h + 5);
64
+ s.line(x + node.width, y + offset + h + 5, x + node.width, y + offset + h + 15);
65
+ offset += h + 15;
66
+ } else {
67
+ s.label(child.label, x + offset + 2, y);
68
+ s.line(x + offset, y, x + offset, y + h);
69
+ s.line(x + offset, y, x + offset + w + 5, y);
70
+ s.line(x + offset + w + 5, y, x + offset + w + 5, y + h);
71
+ s.line(x + offset + w + 5, y + h, x + offset + w + 15, y + h);
72
+ offset += w + 15;
73
+ }
74
+ });
75
+ }
76
+ function draw(node, x, y, w, h) {
77
+ if (node.screen) {
78
+ if (/[+#^]/.test(node.style)) s.rect(x, y, w, h);
79
+ if (node.title) s.label(node.titleLabel, x + node.screen.padding, y + 2);
80
+ if (/[#-]/.test(node.style)) node.screen.horizontal.forEach(function (yy) { s.line(x, y + yy, x + w, y + yy, '#AAA'); });
81
+ if (/[#!]/.test(node.style)) node.screen.vertical.forEach(function (line) { s.line(x + line.x, y + line.y, x + line.x, y + line.y + line.height, '#AAA'); });
82
+ node.screen.cells.forEach(function (cell) { draw(cell.node, x + cell.x, y + cell.y, cell.width, cell.height); });
83
+ return;
84
+ }
85
+ if (node.type === 'container') {
86
+ if (node.style[0] === 'T') return require('./salt-tree').draw(node, x, y, s, draw);
87
+ if (node.style === '/') return tabs(node, x, y);
88
+ if (node.style === '*') {
89
+ s.rect(x, y, w, h, '#DDD');
90
+ var mx = x;
91
+ node.items.forEach(function (child) { s.label(child.label, mx, y); mx += child.width + 10; });
92
+ front.push(function () { overlays.menu(node, x, y, s); });
93
+ return;
94
+ }
95
+ node.rows.forEach(function (row, r) {
96
+ row.forEach(function (child, c) {
97
+ if (child.type === 'span') return;
98
+ draw(child, x + node.xs[c] + 1, y + node.ys[r] + 1 + (r === 0 ? node.titleLabel.height / 2 : 0), node.xs[c + (child.span || 1)] - node.xs[c] - 1, node.heights[r] - 1);
99
+ });
100
+ });
101
+ grid(node, x, y);
102
+ if (node.scroll) { overlays.scroll(node, x, y, s); front.push(function () { overlays.scroll(node, x, y, s); }); }
103
+ return;
104
+ }
105
+ if (node.type === 'space') return;
106
+ if (node.type === 'sprite') {
107
+ for (var xx = 0; xx < node.width; xx++) for (var yy = 0; yy < node.height; yy++) {
108
+ if (node.pixels[yy][xx] === 'X') s.rect(x + xx, y + yy, 0.5, 0.5, s.lastInk() || '#000', '#000', 0, 0.5);
109
+ }
110
+ return;
111
+ }
112
+ if (node.type === 'separator') {
113
+ var yy = y + h / 2 - (node.style === '==' ? 1 : 0);
114
+ s.line(x, yy, x + w, yy, '#AAA', node.style === '~~' ? 1.5 : 1, node.style === '..' ? '1,2' : null);
115
+ if (node.style === '==') s.line(x, yy + 2, x + w, yy + 2, '#AAA');
116
+ return;
117
+ }
118
+ var label = node.label;
119
+ if (node.type === 'button') {
120
+ s.rect(x + 2.5, y + 2.5, node.width - 5, node.height - 5, '#EEE', '#000', 5, 2.5);
121
+ s.label(label, x + (node.width - label.width) / 2, y + 4.5);
122
+ } else if (node.type === 'input') {
123
+ s.label(label, x + 3, y);
124
+ s.line(x + 1, y + label.height, x + node.width - 2, y + label.height);
125
+ s.line(x + 1, y + label.height - 3, x + 1, y + label.height - 1);
126
+ s.line(x + node.width - 2, y + label.height - 3, x + node.width - 2, y + label.height - 1);
127
+ } else if (node.type === 'select') {
128
+ s.rect(x, y, node.width - 1, node.height - 1, '#EEE');
129
+ s.label(label, x + 2, y + 2);
130
+ var xx = x + node.width - 12;
131
+ s.line(xx, y, xx, y + node.height - 1);
132
+ s.polygon([[xx + 3, y + 6], [xx + 9, y + 6], [xx + 6, y + label.height - 2]]);
133
+ overlays.dropdown(node, x, y, s);
134
+ front.push(function () { overlays.dropdown(node, x, y, s); });
135
+ } else if (node.type === 'checkbox' || node.type === 'radio') {
136
+ s.label(label, x + 20, y);
137
+ if (node.type === 'radio') {
138
+ s.ellipse(x + 2, y + (node.height - 10) / 2, 10, 10);
139
+ if (node.checked) s.ellipse(x + 5, y + (node.height - 4) / 2, 4, 4, '#000');
140
+ } else {
141
+ s.rect(x + 2, y + (node.height - 10) / 2, 10, 10, 'none', '#000', 0, 1.5);
142
+ if (node.checked) s.polygon([[x + 3, y + 6], [x + 6, y + 9], [x + 13, y], [x + 6, y + 7]], 1.5);
143
+ }
144
+ } else s.label(label, x, y);
145
+ }
146
+ var decorated = require('./salt-chrome').decorate(root, function (x, y) { draw(root, x, y, root.width, root.height); }, s);
147
+ decorated.draw(root.options.screen ? 1 : 5, root.options.screen ? 1 : 5);
148
+ front.forEach(function (drawOverlay) { drawOverlay(); });
149
+ var margin = out.length ? 1 : 0;
150
+ var width = Math.floor(Math.max(decorated.width + 10, s.bounds.x) + margin);
151
+ var height = Math.floor(Math.max(decorated.height + 10, s.bounds.y) + margin);
152
+ if (root.options.screen) {
153
+ width = root.options.screen.width; height = root.options.screen.height;
154
+ if (s.bounds.x > width || s.bounds.y > height) throw Object.assign(new Error('Clay screen layout: drawing extends outside the canvas. Increase its dimensions or simplify the contents.'), { status: 422 });
155
+ }
156
+ width = Math.round(width * scale * 1000) / 1000; height = Math.round(height * scale * 1000) / 1000;
157
+ if (width > 8192 || height > 8192) throw Object.assign(new Error('Wireframe exceeds the 8192-pixel canvas limit.'), { status: 413 });
158
+ var background = (root.options || {}).background || '#FFFFFF';
159
+ if (background !== '#FFFFFF') out.unshift('<rect x="0" y="0" width="' + width + '" height="' + height + '" fill="' + background + '" stroke="none"/>');
160
+ out.unshift('<svg xmlns="http://www.w3.org/2000/svg"' + (root.options.screen ? ' data-clay-screen="true"' : '') + ' width="' + width + '" height="' + height + '" viewBox="0 0 ' + Math.floor(width) + ' ' + Math.floor(height) + '" preserveAspectRatio="none" style="width:' + Math.floor(width) + 'px;height:' + Math.floor(height) + 'px;background:' + ((root.options || {}).background || '#FFFFFF') + '" role="img" aria-label="Wireframe"><g font-family="sans-serif" lengthAdjust="spacing">');
161
+ out.push('</g></svg>');
162
+ return out.join('');
163
+ }
164
+ module.exports = { renderSaltSvg: renderSaltSvg };
@@ -0,0 +1,112 @@
1
+ // Salt text layout uses the pinned reference metrics, without fonts or Java at runtime.
2
+ var fs = require('node:fs');
3
+ var path = require('node:path');
4
+ var zlib = require('node:zlib');
5
+ var profiles = {};
6
+ var colors = require('./assets/salt-colors.json');
7
+ var icons = require('./assets/salt-icons.json');
8
+ function color(value) {
9
+ var hex = colors[value.toLowerCase()] || value.toUpperCase();
10
+ if (!/^#(?:[A-F0-9]{3}|[A-F0-9]{6})$/.test(hex)) fail('Unsupported color: ' + value);
11
+ return hex.replace(/^#(.)\1(.)\2(.)\3$/, '#$1$2$3');
12
+ }
13
+ function fail(message) { throw Object.assign(new Error(message), { status: 422 }); }
14
+ function parseLabel(value) {
15
+ var heading = /^(={1,6})\s*(.*)$/.exec(value);
16
+ var order = heading ? heading[1].length - 1 - (/^</.test(heading[2]) ? 1 : 0) : -1;
17
+ var size = heading ? order <= 0 ? 16 : order === 1 ? 14 : order === 2 ? 13 : 12 : 12;
18
+ if (heading) value = heading[2];
19
+ var list = /^(\*+|#+)\s+(\S.*)$/.exec(value);
20
+ if (list) value = list[2];
21
+ value = value.replace(/\*\*(.+?)\*\*/g, '<b>$1</b>');
22
+ value = value.replace(/<U\+([0-9a-f]{4,6})>/gi, function (_, cp) {
23
+ var code = parseInt(cp, 16); if (code > 0x10ffff || code >= 0xd800 && code <= 0xdfff) fail('Invalid Unicode code point.');
24
+ return String.fromCodePoint(code);
25
+ });
26
+ var state = { color: '#000', bold: !!heading && order < 3, italic: !!heading && order >= 3, size: size };
27
+ var runs = [];
28
+ var formats = { '**': 'bold', '//': 'italic', '""': 'mono', '--': 'strike', '__': 'underline', '~~': 'wave' };
29
+ var parts = value.split(/(<[^>]+>|\*\*|\/\/|""|--|__|~~)/g);
30
+ parts.forEach(function (part, index) {
31
+ if (!part) return;
32
+ if (formats[part]) {
33
+ var format = formats[part];
34
+ var closing = parts.indexOf(part, index + 1);
35
+ if (state[format] || closing > index && parts.slice(index + 1, closing).some(Boolean)) { state[format] = !state[format]; return; }
36
+ }
37
+ if (/^<&/.test(part)) {
38
+ var icon = part.slice(2, -1); if (!icons[icon]) fail('Unknown built-in icon: ' + icon);
39
+ runs.push(Object.assign({}, state, { text: '', icon: icon })); return;
40
+ }
41
+ if (/^<img:/i.test(part)) {
42
+ var url = part.slice(5, -1);
43
+ if (!/^https:\/\//i.test(url)) fail('Wireframe images require a public HTTPS URL.');
44
+ runs.push(Object.assign({}, state, { text: '', imageUrl: url })); return;
45
+ }
46
+ var tag = /^<(\/?)(b|i|s|u|w|color|back|font|size)(?::([^>]+))?>$/i.exec(part);
47
+ if (tag) {
48
+ var name = tag[2].toLowerCase(); var end = !!tag[1];
49
+ var key = { b: 'bold', i: 'italic', s: 'strike', u: 'underline', w: 'wave' }[name];
50
+ if (key) { state[key] = !end; state[key + 'Color'] = !end && tag[3] ? color(tag[3]) : null; }
51
+ else if (name === 'color') state.color = end ? '#000' : color(tag[3] || 'black');
52
+ else if (name === 'back') state.background = end ? null : color(tag[3] || 'white');
53
+ else if (name === 'font') { if (!end && !/^(monospaced?|sansserif|sans-serif)$/i.test(tag[3] || '')) fail('Unsupported font family.'); state.mono = !end && /^mono/i.test(tag[3]); }
54
+ else if (name === 'size') { state.size = end ? size : Number(tag[3]); if ([10, 12, 13, 14, 16, 18, 20].indexOf(state.size) === -1) fail('Supported font sizes are 10, 12, 13, 14, 16, 18, and 20.'); }
55
+ return;
56
+ }
57
+ if (/<\/?[a-z][^>]*>|<\$|\[\[/i.test(part)) fail('Unsupported embedded markup in wireframe text.');
58
+ var previous = runs[runs.length - 1];
59
+ if (previous && !previous.icon && !previous.imageUrl && JSON.stringify(Object.assign({}, previous, { text: '' })) === JSON.stringify(Object.assign({}, state, { text: '' }))) previous.text += part;
60
+ else runs.push(Object.assign({}, state, { text: part }));
61
+ });
62
+ return { text: runs.map(function (r) { return r.text || r.icon || ''; }).join(''), runs: runs,
63
+ list: list ? { type: list[1][0] === '*' ? 'bullet' : 'number', level: list[1].length } : null,
64
+ bold: runs.some(function (r) { return r.bold; }), italic: runs.some(function (r) { return r.italic; }) };
65
+ }
66
+ function measureRun(run) {
67
+ if (run.icon) return Object.assign({}, run, { width: 10, height: 8, descent: 0 });
68
+ if (run.imageUrl) {
69
+ if (!run.image) fail('Wireframe image was not resolved.');
70
+ return Object.assign({}, run, { width: run.image.width, height: run.image.height, descent: 0 });
71
+ }
72
+ var profile = run.mono ? 'monospace' + (run.size && run.size !== 12 ? '-' + run.size : '') : !run.size || run.size === 12 ? 'metrics' : String(run.size);
73
+ if (!profiles[profile]) profiles[profile] = zlib.gunzipSync(fs.readFileSync(path.join(__dirname, 'assets/salt-font-' + profile + '.bin.gz')));
74
+ var metrics = profiles[profile];
75
+ var style = (run.bold ? 1 : 0) + (run.italic ? 2 : 0);
76
+ var width = 0;
77
+ var height = 0;
78
+ var descent = 0;
79
+ for (var c of run.text) {
80
+ var cp = c.codePointAt(0);
81
+ // Supplementary characters use the reference missing-glyph advance.
82
+ if (cp > 65535) cp = 0xfffd;
83
+ var offset = (style * 65536 + cp) * 12;
84
+ width += metrics.readFloatBE(offset);
85
+ height = Math.max(height, metrics.readFloatBE(offset + 4));
86
+ descent = Math.max(descent, metrics.readFloatBE(offset + 8));
87
+ }
88
+ return Object.assign({}, run, { width: width, height: height, descent: descent });
89
+ }
90
+ function measureLabel(node, trim) {
91
+ var runs = (node.runs || parseLabel(node.text || '').runs).map(function (r) { return Object.assign({}, r); });
92
+ if (trim && runs.length) {
93
+ runs[0].text = runs[0].text.trimStart();
94
+ runs[runs.length - 1].text = runs[runs.length - 1].text.trimEnd();
95
+ }
96
+ runs = runs.filter(function (r) { return r.text || r.icon || r.imageUrl; });
97
+ if (!runs.length) runs.push({ text: '\u00a0', bold: false, italic: false });
98
+ runs = runs.filter(function (r) { return r.text || r.icon || r.imageUrl; }).map(function (r) {
99
+ var measured = measureRun(r);
100
+ var leading = /^ */.exec(r.text)[0];
101
+ if (r.icon || r.imageUrl) return measured;
102
+ measured.displayText = r.text.trim() || '\u00a0';
103
+ measured.displayWidth = measureRun(Object.assign({}, r, { text: measured.displayText })).width;
104
+ measured.leadingWidth = r.text.trim() ? measureRun(Object.assign({}, r, { text: leading })).width : 0;
105
+ return measured;
106
+ });
107
+ var list = node.list;
108
+ var indent = list ? list.type === 'number' ? measureRun({ text: '1. ', size: 12 }).width * list.level : list.level === 1 ? 12 : list.level * 8 : 0;
109
+ return { runs: runs, list: list, indent: indent, width: indent + runs.reduce(function (sum, r) { return sum + r.width; }, 0),
110
+ height: Math.max(0, runs.reduce(function (h, r) { return Math.max(h, r.height); }, 0)) };
111
+ }
112
+ module.exports = { color: color, icons: icons, parseLabel: parseLabel, measureLabel: measureLabel };