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.
- package/LICENSES/README.md +13 -0
- package/LICENSES/open-iconic.txt +21 -0
- package/LICENSES/plantuml-salt.txt +674 -0
- package/lib/assets/salt-assets.json +15 -0
- package/lib/assets/salt-colors.json +156 -0
- package/lib/assets/salt-font-10.bin.gz +0 -0
- package/lib/assets/salt-font-13.bin.gz +0 -0
- package/lib/assets/salt-font-14.bin.gz +0 -0
- package/lib/assets/salt-font-16.bin.gz +0 -0
- package/lib/assets/salt-font-18.bin.gz +0 -0
- package/lib/assets/salt-font-20.bin.gz +0 -0
- package/lib/assets/salt-font-metrics.bin.gz +0 -0
- package/lib/assets/salt-font-metrics.json +101 -0
- package/lib/assets/salt-font-monospace-10.bin.gz +0 -0
- package/lib/assets/salt-font-monospace-13.bin.gz +0 -0
- package/lib/assets/salt-font-monospace-14.bin.gz +0 -0
- package/lib/assets/salt-font-monospace-16.bin.gz +0 -0
- package/lib/assets/salt-font-monospace-18.bin.gz +0 -0
- package/lib/assets/salt-font-monospace-20.bin.gz +0 -0
- package/lib/assets/salt-font-monospace.bin.gz +0 -0
- package/lib/assets/salt-icons.json +671 -0
- package/lib/bundled-skills/clay-sketch/SKILL.md +198 -0
- package/lib/mcp-browser-proxy.js +37 -0
- package/lib/mcp-connection-store.js +39 -0
- package/lib/mcp-network.js +46 -0
- package/lib/mcp-oauth.js +65 -0
- package/lib/mcp-proxy.js +32 -0
- package/lib/mcp-remote-connections.js +215 -0
- package/lib/mcp-session-access.js +15 -0
- package/lib/plantuml-renderer.js +43 -0
- package/lib/project-file-http.js +2 -0
- package/lib/project-mcp.js +130 -357
- package/lib/project-plantuml.js +48 -0
- package/lib/project-session-document.js +17 -1
- package/lib/project-user-message.js +2 -0
- package/lib/project-wireframe-http.js +88 -0
- package/lib/project.js +28 -45
- package/lib/public/app.js +7 -0
- package/lib/public/css/filebrowser.css +5 -0
- package/lib/public/css/mcp-connections.css +86 -0
- package/lib/public/css/skills-workbench.css +74 -0
- package/lib/public/css/wireframe.css +95 -0
- package/lib/public/index.html +16 -15
- package/lib/public/modules/app-messages.js +10 -2
- package/lib/public/modules/filebrowser.js +29 -3
- package/lib/public/modules/markdown.js +3 -1
- package/lib/public/modules/mcp-connection-view.js +136 -0
- package/lib/public/modules/mcp-permissions.js +122 -0
- package/lib/public/modules/mcp-ui.js +85 -461
- package/lib/public/modules/plantuml-preview.js +76 -0
- package/lib/public/modules/skills-actions.js +47 -79
- package/lib/public/modules/skills-detail.js +32 -145
- package/lib/public/modules/skills-list-view.js +29 -0
- package/lib/public/modules/skills-state.js +4 -2
- package/lib/public/modules/skills.js +104 -399
- package/lib/public/modules/split-view.js +14 -0
- package/lib/public/modules/vendor-login-browser.js +16 -5
- package/lib/public/modules/wireframe-chat.js +52 -0
- package/lib/public/modules/wireframe-render.js +69 -0
- package/lib/public/modules/wireframe-style.js +6 -0
- package/lib/public/modules/wireframe-viewport.js +70 -0
- package/lib/public/modules/wireframe-workbench.js +116 -0
- package/lib/public/style.css +3 -0
- package/lib/salt-chrome.js +36 -0
- package/lib/salt-hand.js +37 -0
- package/lib/salt-images.js +87 -0
- package/lib/salt-layout.js +60 -0
- package/lib/salt-options.js +23 -0
- package/lib/salt-overlays.js +45 -0
- package/lib/salt-parser.js +153 -0
- package/lib/salt-screen.js +111 -0
- package/lib/salt-svg-shapes.js +80 -0
- package/lib/salt-svg.js +164 -0
- package/lib/salt-text.js +112 -0
- package/lib/salt-tree.js +45 -0
- package/lib/sdk-bridge.js +5 -17
- package/lib/sdk-mcp-descriptors.js +2 -1
- package/lib/session-document-mcp-server.js +9 -0
- package/lib/vendor-login-browser-worker.js +18 -8
- package/lib/vendor-login-browser.js +10 -3
- package/lib/yoke/adapters/codex.js +1 -18
- package/package.json +6 -2
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { sketchBrand, sketchCredit } from './wireframe-style.js';
|
|
2
|
+
import { store } from './store.js';
|
|
3
|
+
import { renderWireframe, wireframeSource, wireframeImage } from './wireframe-render.js';
|
|
4
|
+
import { openWireframe } from './wireframe-workbench.js';
|
|
5
|
+
import { mountWireframeViewport } from './wireframe-viewport.js';
|
|
6
|
+
|
|
7
|
+
export function renderWireframeBlocks(root) {
|
|
8
|
+
if (store.get('replayingHistory')) return;
|
|
9
|
+
root.querySelectorAll('pre code.language-clay-sketch, pre code.language-plantuml, pre code.language-puml, pre code.language-salt').forEach(function (code) {
|
|
10
|
+
var pre = code.parentElement;
|
|
11
|
+
if (!pre || pre.dataset.wireframeProcessed) return;
|
|
12
|
+
var source = wireframeSource(code.textContent, code.classList.contains('language-salt') ? 'salt' : 'plantuml');
|
|
13
|
+
// Wait for a complete PlantUML body while assistant text is streaming.
|
|
14
|
+
if (!/^\s*@end(?:salt|uml)\b/im.test(source)) return;
|
|
15
|
+
pre.dataset.wireframeProcessed = 'true';
|
|
16
|
+
var card = document.createElement('section');
|
|
17
|
+
card.className = 'wireframe-card';
|
|
18
|
+
card.setAttribute('aria-label', 'clay-sketch preview');
|
|
19
|
+
var header = document.createElement('header');
|
|
20
|
+
var label = document.createElement('div'); label.className = 'sketch-brand'; label.innerHTML = sketchBrand(); header.appendChild(label);
|
|
21
|
+
var open = document.createElement('button'); open.type = 'button'; open.dataset.openSketch = ''; open.textContent = 'Open sketch';
|
|
22
|
+
open.addEventListener('click', function (event) { event.stopPropagation(); openWireframe({ source: source, title: 'Sketch viewer' }); });
|
|
23
|
+
header.appendChild(open); card.appendChild(header);
|
|
24
|
+
var canvas = document.createElement('div'); canvas.className = 'wireframe-canvas'; card.appendChild(canvas);
|
|
25
|
+
var status = document.createElement('p'); status.setAttribute('role', 'status'); status.textContent = 'Rendering sketch…'; canvas.appendChild(status);
|
|
26
|
+
var details = document.createElement('details');
|
|
27
|
+
var summary = document.createElement('summary'); summary.textContent = 'Sketch source'; details.appendChild(summary);
|
|
28
|
+
pre.replaceWith(card); details.appendChild(pre);
|
|
29
|
+
var footer = document.createElement('div'); footer.className = 'sketch-source-footer'; footer.appendChild(details);
|
|
30
|
+
var credit = document.createElement('div'); credit.innerHTML = sketchCredit(); footer.appendChild(credit); card.appendChild(footer);
|
|
31
|
+
function render(current) {
|
|
32
|
+
canvas.replaceChildren(status); status.textContent = 'Rendering sketch…';
|
|
33
|
+
return renderWireframe(current, canvas).then(function (svg) {
|
|
34
|
+
if (!card.isConnected) return;
|
|
35
|
+
var image = wireframeImage(svg, 'clay-sketch preview');
|
|
36
|
+
canvas.replaceChildren(image);
|
|
37
|
+
mountWireframeViewport(canvas, image);
|
|
38
|
+
}).catch(function (error) {
|
|
39
|
+
if (!card.isConnected) return;
|
|
40
|
+
status.textContent = error.name === 'AbortError' ? 'Rendering was interrupted. Try again.' : error.message;
|
|
41
|
+
var retry = document.createElement('button'); retry.type = 'button'; retry.textContent = 'Retry';
|
|
42
|
+
retry.onclick = function () {
|
|
43
|
+
retry.remove();
|
|
44
|
+
render(current).catch(function () {});
|
|
45
|
+
};
|
|
46
|
+
canvas.appendChild(retry);
|
|
47
|
+
throw error;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
render(source).catch(function () {});
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
|
|
3
|
+
export function wireframeSource(source, language) {
|
|
4
|
+
source = String(source || '');
|
|
5
|
+
if (language === 'salt' && !/@start\w+/i.test(source)) return '@startsalt\n' + source + '\n@endsalt';
|
|
6
|
+
return source;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function wireframeImage(svg, title) {
|
|
10
|
+
if (svg.length > 8 * 1024 * 1024 || !/<svg[\s>]/i.test(svg)) throw new Error('The renderer returned an invalid diagram.');
|
|
11
|
+
var image = document.createElement('img');
|
|
12
|
+
image.alt = title || 'Wireframe';
|
|
13
|
+
image.draggable = false;
|
|
14
|
+
// SVG is displayed in an inert image context, never inserted as markup.
|
|
15
|
+
image.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg);
|
|
16
|
+
return image;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function pump() {
|
|
20
|
+
if (store.get('wireframeRenderActive')) return;
|
|
21
|
+
var queue = (store.get('wireframeRenderQueue') || []).slice();
|
|
22
|
+
var job = queue.shift();
|
|
23
|
+
if (!job) return;
|
|
24
|
+
store.set({ wireframeRenderQueue: queue });
|
|
25
|
+
if (!job.target.isConnected) { job.reject(new Error('Preview closed.')); pump(); return; }
|
|
26
|
+
var controller = new AbortController();
|
|
27
|
+
store.set({ wireframeRenderActive: controller });
|
|
28
|
+
var timer = setTimeout(function () { controller.abort(); }, 20000);
|
|
29
|
+
fetch('api/wireframe/render', { method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
30
|
+
body: JSON.stringify({ source: job.source }), signal: controller.signal, cache: 'no-store' }).then(function (response) {
|
|
31
|
+
return response.text().then(function (text) {
|
|
32
|
+
if (!response.ok) throw new Error(text || 'Unable to render this wireframe.');
|
|
33
|
+
return text;
|
|
34
|
+
});
|
|
35
|
+
}).then(function (svg) {
|
|
36
|
+
if (store.get('wireframeRenderActive') !== controller) throw new Error('Conversation changed.');
|
|
37
|
+
wireframeImage(svg);
|
|
38
|
+
if (svg.length < 500000) {
|
|
39
|
+
var cache = (store.get('wireframeRenderCache') || []).filter(function (entry) { return entry.source !== job.source; });
|
|
40
|
+
cache.push({ source: job.source, svg: svg });
|
|
41
|
+
store.set({ wireframeRenderCache: cache.slice(-12) });
|
|
42
|
+
}
|
|
43
|
+
job.resolve(svg);
|
|
44
|
+
}).catch(function (error) { job.reject(error); }).finally(function () {
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
if (store.get('wireframeRenderActive') === controller) store.set({ wireframeRenderActive: null });
|
|
47
|
+
pump();
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function renderWireframe(source, target) {
|
|
52
|
+
if (!source.trim() || new TextEncoder().encode(source).length > 100000) return Promise.reject(new Error('Wireframes must contain between 1 byte and 100 KB of source.'));
|
|
53
|
+
var cache = store.get('wireframeRenderCache') || [];
|
|
54
|
+
var hit = cache.find(function (entry) { return entry.source === source; });
|
|
55
|
+
if (hit) return Promise.resolve(hit.svg);
|
|
56
|
+
return new Promise(function (resolve, reject) {
|
|
57
|
+
var queue = (store.get('wireframeRenderQueue') || []).slice();
|
|
58
|
+
queue.push({ source: source, target: target, resolve: resolve, reject: reject });
|
|
59
|
+
store.set({ wireframeRenderQueue: queue });
|
|
60
|
+
pump();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
store.subscribe(function (state, previous) {
|
|
65
|
+
if (state.currentSlug === previous.currentSlug && state.activeSessionId === previous.activeSessionId && state.myUserId === previous.myUserId) return;
|
|
66
|
+
if (previous.wireframeRenderActive) previous.wireframeRenderActive.abort();
|
|
67
|
+
(previous.wireframeRenderQueue || []).forEach(function (job) { job.reject(new Error('Conversation changed.')); });
|
|
68
|
+
store.set({ wireframeRenderActive: null, wireframeRenderQueue: [], wireframeRenderCache: [] });
|
|
69
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export function sketchBrand() {
|
|
2
|
+
return '<span class="sketch-mark" aria-hidden="true">✎</span><span class="sketch-wordmark">clay<span>sketch</span></span>';
|
|
3
|
+
}
|
|
4
|
+
export function sketchCredit() {
|
|
5
|
+
return '<a class="sketch-credit" href="https://plantuml.com/salt" target="_blank" rel="noopener noreferrer">Built with PlantUML Salt <span aria-hidden="true">↗</span></a>';
|
|
6
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
|
|
3
|
+
function entries() { return store.get('wireframeViewports') || new Map(); }
|
|
4
|
+
function apply(entry) {
|
|
5
|
+
var image = entry.image;
|
|
6
|
+
if (!image.naturalWidth || !entry.viewport.isConnected || entries().get(entry.viewport) !== entry) return;
|
|
7
|
+
var style = getComputedStyle(entry.viewport);
|
|
8
|
+
var available = entry.viewport.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight);
|
|
9
|
+
if (available <= 0) return;
|
|
10
|
+
var fitted = Math.min(1, available / image.naturalWidth);
|
|
11
|
+
if (entry.viewport.classList.contains('wireframe-workbench-canvas')) {
|
|
12
|
+
var height = entry.viewport.clientHeight - parseFloat(style.paddingTop) - parseFloat(style.paddingBottom);
|
|
13
|
+
if (height > 0) fitted = Math.min(fitted, height / image.naturalHeight);
|
|
14
|
+
}
|
|
15
|
+
// Default to the whole design at natural scale or smaller; zoom is explicit.
|
|
16
|
+
var scale = entry.mode === 'manual' ? entry.zoom : fitted;
|
|
17
|
+
image.style.width = image.naturalWidth * scale + 'px';
|
|
18
|
+
image.style.maxWidth = 'none';
|
|
19
|
+
image.style.height = 'auto';
|
|
20
|
+
image.style.margin = image.naturalWidth * scale > available ? '0' : '0 auto';
|
|
21
|
+
entry.scale = scale;
|
|
22
|
+
if (!entry.toolbar) return;
|
|
23
|
+
entry.toolbar.querySelector('[data-zoom-value]').textContent = Math.round(scale * 100) + '%';
|
|
24
|
+
entry.toolbar.querySelector('[data-zoom-fit]').setAttribute('aria-pressed', String(entry.mode === 'fit'));
|
|
25
|
+
entry.toolbar.querySelector('[data-zoom-out]').disabled = scale <= 0.25;
|
|
26
|
+
entry.toolbar.querySelector('[data-zoom-in]').disabled = scale >= 4;
|
|
27
|
+
}
|
|
28
|
+
function cleanup() {
|
|
29
|
+
var next = new Map(entries());
|
|
30
|
+
next.forEach(function (entry, viewport) {
|
|
31
|
+
if (viewport.isConnected && entry.image.isConnected) return;
|
|
32
|
+
entry.resize.disconnect(); next.delete(viewport);
|
|
33
|
+
});
|
|
34
|
+
if (next.size !== entries().size) store.set({ wireframeViewports: next });
|
|
35
|
+
if (!next.size) {
|
|
36
|
+
var observer = store.get('wireframeViewportCleanup');
|
|
37
|
+
if (observer) observer.disconnect();
|
|
38
|
+
store.set({ wireframeViewportCleanup: null });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function mountWireframeViewport(viewport, image, toolbar) {
|
|
42
|
+
var previous = entries().get(viewport);
|
|
43
|
+
if (previous) previous.resize.disconnect();
|
|
44
|
+
var entry = { viewport: viewport, image: image, toolbar: toolbar, mode: previous ? previous.mode : 'auto', zoom: previous ? previous.zoom : 1, scale: 1 };
|
|
45
|
+
entry.resize = new ResizeObserver(function () { apply(entry); });
|
|
46
|
+
var next = new Map(entries()); next.set(viewport, entry);
|
|
47
|
+
store.set({ wireframeViewports: next });
|
|
48
|
+
if (toolbar) {
|
|
49
|
+
toolbar.classList.add('wireframe-zoom');
|
|
50
|
+
toolbar.setAttribute('role', 'group'); toolbar.setAttribute('aria-label', 'Sketch zoom');
|
|
51
|
+
toolbar.innerHTML = '<button type="button" data-zoom-out aria-label="Zoom out" title="Zoom out">−</button><button type="button" data-zoom-value aria-label="Reset zoom to 100%" title="Original size">100%</button><button type="button" data-zoom-in aria-label="Zoom in" title="Zoom in">+</button><button type="button" data-zoom-fit aria-pressed="true" title="Fit sketch to available width">Fit</button>';
|
|
52
|
+
function change(mode, zoom) {
|
|
53
|
+
entry.mode = mode; entry.zoom = zoom;
|
|
54
|
+
viewport.scrollLeft = 0; viewport.scrollTop = 0;
|
|
55
|
+
apply(entry);
|
|
56
|
+
}
|
|
57
|
+
toolbar.querySelector('[data-zoom-out]').onclick = function () { change('manual', Math.max(0.25, entry.scale / 1.25)); };
|
|
58
|
+
toolbar.querySelector('[data-zoom-in]').onclick = function () { change('manual', Math.min(4, Math.max(0.25, entry.scale * 1.25))); };
|
|
59
|
+
toolbar.querySelector('[data-zoom-value]').onclick = function () { change('manual', 1); };
|
|
60
|
+
toolbar.querySelector('[data-zoom-fit]').onclick = function () { change('fit', 1); };
|
|
61
|
+
}
|
|
62
|
+
image.addEventListener('load', function () { apply(entry); }, { once: true });
|
|
63
|
+
entry.resize.observe(viewport);
|
|
64
|
+
if (!store.get('wireframeViewportCleanup')) {
|
|
65
|
+
var observer = new MutationObserver(cleanup);
|
|
66
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
67
|
+
store.set({ wireframeViewportCleanup: observer });
|
|
68
|
+
}
|
|
69
|
+
apply(entry);
|
|
70
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { sketchBrand, sketchCredit } from './wireframe-style.js';
|
|
2
|
+
import { store } from './store.js';
|
|
3
|
+
import { claimRightWorkbench, registerRightWorkbench, releaseRightWorkbench } from './right-workbench.js';
|
|
4
|
+
import { renderWireframe, wireframeSource, wireframeImage } from './wireframe-render.js';
|
|
5
|
+
import { refreshIcons } from './icons.js';
|
|
6
|
+
import { mountWireframeViewport } from './wireframe-viewport.js';
|
|
7
|
+
|
|
8
|
+
function panel() { return document.getElementById('wireframe-panel'); }
|
|
9
|
+
export function closeWireframe() {
|
|
10
|
+
releaseRightWorkbench('wireframe');
|
|
11
|
+
if (panel()) panel().remove();
|
|
12
|
+
store.set({ wireframeDraft: null });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function download() {
|
|
16
|
+
var draft = store.get('wireframeDraft');
|
|
17
|
+
if (!draft) return;
|
|
18
|
+
var link = document.createElement('a');
|
|
19
|
+
link.href = URL.createObjectURL(new Blob([draft.source], { type: 'text/plain;charset=utf-8' }));
|
|
20
|
+
link.download = (draft.id || 'wireframe') + '.puml';
|
|
21
|
+
link.click();
|
|
22
|
+
setTimeout(function () { URL.revokeObjectURL(link.href); }, 1000);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function save(event) {
|
|
26
|
+
event.preventDefault();
|
|
27
|
+
var el = panel();
|
|
28
|
+
var draft = store.get('wireframeDraft');
|
|
29
|
+
var form = el.querySelector('form');
|
|
30
|
+
var button = form.querySelector('[type="submit"]');
|
|
31
|
+
var status = el.querySelector('.wireframe-save-status');
|
|
32
|
+
var requested = form.elements.path.value.trim();
|
|
33
|
+
button.disabled = true; status.textContent = 'Saving…';
|
|
34
|
+
fetch('api/wireframe/save', { method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
35
|
+
body: JSON.stringify({ path: requested, source: draft.source }) }).then(function (response) {
|
|
36
|
+
if (!response.ok) return response.text().then(function (text) { throw new Error(text); });
|
|
37
|
+
return response.json();
|
|
38
|
+
}).then(function (result) {
|
|
39
|
+
if (store.get('wireframeDraft') !== draft) return;
|
|
40
|
+
status.textContent = 'Saved to ' + result.path;
|
|
41
|
+
form.hidden = true;
|
|
42
|
+
}).catch(function (error) {
|
|
43
|
+
if (store.get('wireframeDraft') === draft) status.textContent = error.message || 'Unable to save the wireframe.';
|
|
44
|
+
}).finally(function () { button.disabled = false; });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function createPanel() {
|
|
48
|
+
var host = document.getElementById('main-panels');
|
|
49
|
+
if (!host) return null;
|
|
50
|
+
var el = document.createElement('section'); el.id = 'wireframe-panel';
|
|
51
|
+
el.setAttribute('aria-label', 'clay-sketch viewer');
|
|
52
|
+
el.innerHTML = '<header class="wireframe-topbar"><div class="sketch-brand">' + sketchBrand() + '</div><strong class="sketch-title">Sketch viewer</strong><div class="wireframe-window-actions"><button type="button" data-wide title="Widen panel" aria-label="Widen panel" aria-pressed="false"><i data-lucide="chevrons-left-right"></i></button><button type="button" data-full title="Toggle fullscreen" aria-label="Toggle fullscreen" aria-pressed="false"><i data-lucide="maximize-2"></i></button><button type="button" data-close title="Close clay-sketch" aria-label="Close clay-sketch"><i data-lucide="x"></i></button></div></header><div class="wireframe-toolbar"><div class="wireframe-zoom"></div><button type="button" data-save>Save to project</button><button type="button" data-download title="Download sketch source" aria-label="Download sketch source"><i data-lucide="download"></i></button></div><form hidden class="wireframe-save-form"><label>Project file path<input name="path" aria-label="Project file path" required pattern=".*\\.puml" placeholder="wireframe.puml"></label><button type="submit">Save</button><button type="button" data-cancel>Cancel</button></form><p class="wireframe-save-status" role="status"></p><div class="wireframe-workbench-canvas"></div><footer class="sketch-footer">' + sketchCredit() + '<span>Revise through conversation</span></footer>';
|
|
53
|
+
host.appendChild(el);
|
|
54
|
+
el.querySelector('[data-close]').onclick = closeWireframe;
|
|
55
|
+
el.querySelector('[data-download]').onclick = download;
|
|
56
|
+
el.querySelector('[data-save]').onclick = function () {
|
|
57
|
+
var form = el.querySelector('form'); form.hidden = false;
|
|
58
|
+
var draft = store.get('wireframeDraft');
|
|
59
|
+
form.elements.path.value = (draft.id || 'wireframe') + '.puml';
|
|
60
|
+
form.elements.path.focus();
|
|
61
|
+
};
|
|
62
|
+
el.querySelector('[data-cancel]').onclick = function () { el.querySelector('form').hidden = true; el.querySelector('[data-save]').focus(); };
|
|
63
|
+
el.querySelector('form').onsubmit = save;
|
|
64
|
+
el.addEventListener('keydown', function (event) {
|
|
65
|
+
if (event.key !== 'Escape') return;
|
|
66
|
+
event.stopPropagation();
|
|
67
|
+
if (!el.querySelector('form').hidden) el.querySelector('[data-cancel]').click();
|
|
68
|
+
else closeWireframe();
|
|
69
|
+
});
|
|
70
|
+
['wide', 'full'].forEach(function (action) {
|
|
71
|
+
el.querySelector('[data-' + action + ']').onclick = function () {
|
|
72
|
+
var active = el.classList.toggle(action === 'wide' ? 'wireframe-wide' : 'panel-fullscreen');
|
|
73
|
+
this.setAttribute('aria-pressed', String(active));
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
refreshIcons();
|
|
77
|
+
return el;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function openWireframe(message) {
|
|
81
|
+
if (!message || typeof message.source !== 'string' || new TextEncoder().encode(message.source).length > 100000) return;
|
|
82
|
+
if (store.get('paneMode') && window.parent !== window) {
|
|
83
|
+
window.parent.postMessage({ type: 'clay-pane-present-wireframe', message: message, projectSlug: store.get('currentSlug'), sessionId: store.get('activeSessionId') }, window.location.origin);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
claimRightWorkbench('wireframe');
|
|
87
|
+
var el = panel() || createPanel();
|
|
88
|
+
if (!el) return;
|
|
89
|
+
var draft = { source: wireframeSource(message.source, message.language), title: String(message.title || 'clay-sketch').slice(0, 100),
|
|
90
|
+
id: /^[a-zA-Z0-9_-]{1,80}$/.test(message.id || '') ? message.id : 'wireframe' };
|
|
91
|
+
store.set({ wireframeDraft: draft });
|
|
92
|
+
el.querySelector('header strong').textContent = draft.title;
|
|
93
|
+
el.querySelector('.wireframe-save-status').textContent = '';
|
|
94
|
+
el.querySelector('form').hidden = true;
|
|
95
|
+
draw(draft, el).catch(function () {});
|
|
96
|
+
}
|
|
97
|
+
function draw(draft, el) {
|
|
98
|
+
var canvas = el.querySelector('.wireframe-workbench-canvas');
|
|
99
|
+
var active = store.get('wireframeDraft');
|
|
100
|
+
var status = document.createElement('p'); status.setAttribute('role', 'status'); status.textContent = 'Rendering sketch…';
|
|
101
|
+
canvas.replaceChildren(status);
|
|
102
|
+
return renderWireframe(draft.source, canvas).then(function (svg) {
|
|
103
|
+
if (store.get('wireframeDraft') !== active || !canvas.isConnected) throw new DOMException('Sketch changed.', 'AbortError');
|
|
104
|
+
var image = wireframeImage(svg, draft.title);
|
|
105
|
+
canvas.replaceChildren(image);
|
|
106
|
+
mountWireframeViewport(canvas, image, el.querySelector('.wireframe-zoom'));
|
|
107
|
+
}).catch(function (error) {
|
|
108
|
+
if (store.get('wireframeDraft') !== active) throw error;
|
|
109
|
+
status.textContent = error.name === 'AbortError' ? 'Rendering was interrupted. Reopen the wireframe to retry.' : error.message;
|
|
110
|
+
throw error;
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
registerRightWorkbench('wireframe', closeWireframe);
|
|
114
|
+
store.subscribe(function (state, previous) {
|
|
115
|
+
if (state.currentSlug !== previous.currentSlug || state.activeSessionId !== previous.activeSessionId || state.myUserId !== previous.myUserId) closeWireframe();
|
|
116
|
+
});
|
package/lib/public/style.css
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
@import url("css/wireframe.css");
|
|
2
|
+
@import url("css/mcp-connections.css");
|
|
1
3
|
@import url("css/session-github.css");
|
|
2
4
|
@import url("css/vendor-login-browser.css");
|
|
3
5
|
@import url("css/issues.css");
|
|
@@ -68,5 +70,6 @@
|
|
|
68
70
|
@import url("./css/shared-browser.css");
|
|
69
71
|
|
|
70
72
|
@import url("./css/mcp-skills-workbench.css");
|
|
73
|
+
@import url("css/skills-workbench.css");
|
|
71
74
|
|
|
72
75
|
@import url("css/right-workbench.css");
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Diagram decoration geometry adapted from PlantUML 1.2026.8 (GPL-3.0-or-later).
|
|
2
|
+
var text = require('./salt-text');
|
|
3
|
+
function decorate(root, draw, shapes) {
|
|
4
|
+
var options = root.options || {};
|
|
5
|
+
var block = { width: root.width, height: root.height, draw: draw };
|
|
6
|
+
function add(name, top) {
|
|
7
|
+
if (!options[name]) return;
|
|
8
|
+
var small = name === 'header' || name === 'footer';
|
|
9
|
+
var padding = name === 'title' || name === 'legend' ? 5 : 0;
|
|
10
|
+
var margin = name === 'legend' ? 12 : name === 'title' ? 5 : name === 'caption' ? 1 : 0;
|
|
11
|
+
var labels = options.labels[name].map(function (parsed) {
|
|
12
|
+
parsed.runs.forEach(function (run) { run.size = small ? 10 : 14; run.bold = name === 'title' || run.bold; run.color = small ? '#888' : run.color; });
|
|
13
|
+
return text.measureLabel(parsed);
|
|
14
|
+
});
|
|
15
|
+
var textWidth = Math.max.apply(null, labels.map(function (label) { return label.width; }));
|
|
16
|
+
var textHeight = labels.reduce(function (sum, label) { return sum + label.height; }, 0);
|
|
17
|
+
var width = textWidth + 2 * (padding + margin) + 1;
|
|
18
|
+
var height = textHeight + 2 * (padding + margin) + 1;
|
|
19
|
+
var previous = block;
|
|
20
|
+
block = { width: Math.max(previous.width, width), height: previous.height + height, draw: function (x, y) {
|
|
21
|
+
var bx = x + (name === 'header' ? blockWidth - width : (blockWidth - width) / 2);
|
|
22
|
+
var by = y + (top ? 0 : previous.height);
|
|
23
|
+
previous.draw(x + (blockWidth - previous.width) / 2, y + (top ? height : 0));
|
|
24
|
+
if (name === 'legend') shapes.rect(bx + margin, by + margin, textWidth + 2 * padding, textHeight + 2 * padding, '#DDD', '#000', 7.5);
|
|
25
|
+
var yy = by + padding + margin;
|
|
26
|
+
labels.forEach(function (label) {
|
|
27
|
+
var offset = name === 'legend' ? 0 : name === 'header' ? textWidth - label.width : (textWidth - label.width) / 2;
|
|
28
|
+
shapes.label(label, bx + margin + padding + offset, yy); yy += label.height;
|
|
29
|
+
});
|
|
30
|
+
} };
|
|
31
|
+
var blockWidth = block.width;
|
|
32
|
+
}
|
|
33
|
+
add('legend', false); add('title', true); add('caption', false); add('header', true); add('footer', false);
|
|
34
|
+
return block;
|
|
35
|
+
}
|
|
36
|
+
module.exports = { decorate: decorate };
|
package/lib/salt-hand.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Adapted from PlantUML HandJiggle/URectangleHand by Adrian Vogt, GPL-3.0-or-later.
|
|
2
|
+
// See LICENSES/plantuml-salt.txt. Each translated reference graphic resets its seed.
|
|
3
|
+
function jiggle(x, y, variation) {
|
|
4
|
+
var seed = (424242n ^ 25214903917n) & ((1n << 48n) - 1n);
|
|
5
|
+
function bits(n) { seed = (seed * 25214903917n + 11n) & ((1n << 48n) - 1n); return Number(seed >> BigInt(48 - n)); }
|
|
6
|
+
function random() { return (bits(26) * 134217728 + bits(27)) / 9007199254740992; }
|
|
7
|
+
var points = [[x, y]];
|
|
8
|
+
function lineTo(xx, yy) {
|
|
9
|
+
var dx = Math.abs(xx - x); var dy = Math.abs(yy - y); var distance = Math.hypot(dx, dy);
|
|
10
|
+
if (distance < 0.001) return;
|
|
11
|
+
var segments = Math.round(distance / 10); var amount = variation;
|
|
12
|
+
if (segments < 5) { segments = 5; amount /= 3; }
|
|
13
|
+
for (var i = 0; i < segments; i++) {
|
|
14
|
+
var offset = (random() - 0.5) * amount;
|
|
15
|
+
points.push([x + (xx - x) / segments * i - offset * dy / distance, y + (yy - y) / segments * i - offset * dx / distance]);
|
|
16
|
+
}
|
|
17
|
+
points.push([xx, yy]); x = xx; y = yy;
|
|
18
|
+
}
|
|
19
|
+
function arc(a, b, cx, cy, rx, ry) {
|
|
20
|
+
[(a + b) / 2, b].forEach(function (angle) { lineTo(cx + Math.cos(angle) * rx, cy + Math.sin(angle) * ry); });
|
|
21
|
+
}
|
|
22
|
+
return { points: points, lineTo: lineTo, arc: arc };
|
|
23
|
+
}
|
|
24
|
+
function line(dx, dy) { var j = jiggle(0, 0, 2); j.lineTo(dx, dy); return j.points; }
|
|
25
|
+
function rect(w, h, radius) {
|
|
26
|
+
var rx = Math.min(radius || 0, w / 2); var ry = Math.min(radius || 0, h / 2);
|
|
27
|
+
var j = jiggle(rx, 0, 1.5);
|
|
28
|
+
if (!radius) { j.lineTo(w, 0); j.lineTo(w, h); j.lineTo(0, h); j.lineTo(0, 0); }
|
|
29
|
+
else {
|
|
30
|
+
j.lineTo(w - rx, 0); j.arc(-Math.PI / 2, 0, w - rx, ry, rx, ry);
|
|
31
|
+
j.lineTo(w, h - ry); j.arc(0, Math.PI / 2, w - rx, h - ry, rx, ry);
|
|
32
|
+
j.lineTo(rx, h); j.arc(Math.PI / 2, Math.PI, rx, h - ry, rx, ry);
|
|
33
|
+
j.lineTo(0, ry); j.arc(Math.PI, Math.PI * 1.5, rx, ry, rx, ry);
|
|
34
|
+
}
|
|
35
|
+
return j.points;
|
|
36
|
+
}
|
|
37
|
+
module.exports = { line: line, rect: rect };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Resolve explicit raster images only. SVG output contains embedded bytes, never remote URLs.
|
|
2
|
+
var https = require('node:https');
|
|
3
|
+
var dns = require('node:dns').promises;
|
|
4
|
+
var net = require('node:net');
|
|
5
|
+
var MAX_BYTES = 1024 * 1024;
|
|
6
|
+
function failure(message) { return Object.assign(new Error(message), { status: 422 }); }
|
|
7
|
+
function bounded(promise, signal) {
|
|
8
|
+
return new Promise(function (resolve, reject) {
|
|
9
|
+
function aborted() { reject(failure('Wireframe image loading cancelled or timed out.')); }
|
|
10
|
+
if (signal.aborted) { aborted(); return; }
|
|
11
|
+
signal.addEventListener('abort', aborted, { once: true });
|
|
12
|
+
promise.then(resolve, reject).finally(function () { signal.removeEventListener('abort', aborted); });
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
function publicAddress(address) {
|
|
16
|
+
if (net.isIP(address) !== 4) return false;
|
|
17
|
+
var p = address.split('.').map(Number);
|
|
18
|
+
return !(p[0] === 0 || p[0] === 10 || p[0] === 127 || p[0] >= 224 ||
|
|
19
|
+
p[0] === 100 && p[1] >= 64 && p[1] <= 127 || p[0] === 169 && p[1] === 254 ||
|
|
20
|
+
p[0] === 172 && p[1] >= 16 && p[1] <= 31 || p[0] === 192 && (p[1] === 168 || p[1] === 0 || p[1] === 2) ||
|
|
21
|
+
p[0] === 198 && (p[1] === 18 || p[1] === 19 || p[1] === 51 && p[2] === 100) || p[0] === 203 && p[1] === 0 && p[2] === 113);
|
|
22
|
+
}
|
|
23
|
+
function decode(bytes) {
|
|
24
|
+
if (!Buffer.isBuffer(bytes) || bytes.length > MAX_BYTES || bytes.length < 33 ||
|
|
25
|
+
bytes.subarray(0, 8).toString('hex') !== '89504e470d0a1a0a' || bytes.toString('ascii', 12, 16) !== 'IHDR') throw failure('Wireframe images must be PNG files of at most 1 MB.');
|
|
26
|
+
var width = bytes.readUInt32BE(16); var height = bytes.readUInt32BE(20);
|
|
27
|
+
if (!width || !height || width > 2048 || height > 2048) throw failure('Wireframe images must be at most 2048 by 2048 pixels.');
|
|
28
|
+
return { width: width, height: height, data: 'data:image/png;base64,' + bytes.toString('base64') };
|
|
29
|
+
}
|
|
30
|
+
async function download(value, signal) {
|
|
31
|
+
var url;
|
|
32
|
+
try { url = new URL(value); } catch (e) { throw failure('Invalid wireframe image URL.'); }
|
|
33
|
+
if (url.protocol !== 'https:' || url.username || url.password || url.port && url.port !== '443') throw failure('Wireframe images require a public HTTPS URL on port 443.');
|
|
34
|
+
var addresses = await bounded(dns.lookup(url.hostname, { all: true, family: 4 }), signal);
|
|
35
|
+
if (!addresses.length || addresses.some(function (entry) { return !publicAddress(entry.address); })) throw failure('Wireframe images cannot access private or reserved network addresses.');
|
|
36
|
+
if (signal.aborted) throw failure('Wireframe image loading cancelled or timed out.');
|
|
37
|
+
return new Promise(function (resolve, reject) {
|
|
38
|
+
var request = https.get(url, { signal: signal, agent: false, headers: { Accept: 'image/png' },
|
|
39
|
+
lookup: function (_, options, callback) { callback(null, options.all ? [addresses[0]] : addresses[0].address, 4); }
|
|
40
|
+
}, function (response) {
|
|
41
|
+
if (response.statusCode !== 200) { response.resume(); reject(failure('Wireframe image server returned HTTP ' + response.statusCode + '. Redirects are not followed.')); return; }
|
|
42
|
+
var chunks = []; var size = 0;
|
|
43
|
+
response.on('data', function (chunk) {
|
|
44
|
+
size += chunk.length;
|
|
45
|
+
if (size > MAX_BYTES) { var error = failure('Wireframe image exceeds 1 MB.'); reject(error); request.destroy(error); }
|
|
46
|
+
else chunks.push(chunk);
|
|
47
|
+
});
|
|
48
|
+
response.on('error', reject);
|
|
49
|
+
response.on('end', function () { try { resolve(decode(Buffer.concat(chunks))); } catch (e) { reject(e); } });
|
|
50
|
+
});
|
|
51
|
+
request.on('error', function (error) { reject(failure('Unable to load wireframe image: ' + error.message)); });
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
async function resolveImages(root, options) {
|
|
55
|
+
var runs = [];
|
|
56
|
+
function label(node) { if (node) (node.runs || []).forEach(function (run) { if (run.imageUrl) runs.push(run); }); }
|
|
57
|
+
function visit(node) {
|
|
58
|
+
label(node); label(node.titleData);
|
|
59
|
+
(node.options || []).forEach(label);
|
|
60
|
+
(node.rows || []).forEach(function (row) { row.forEach(visit); });
|
|
61
|
+
(node.popups || []).forEach(function (row) { row.forEach(visit); });
|
|
62
|
+
}
|
|
63
|
+
// The root's display options differ from a dropdown's option labels.
|
|
64
|
+
(root.rows || []).forEach(function (row) { row.forEach(visit); }); label(root.titleData);
|
|
65
|
+
(root.popups || []).forEach(function (row) { row.forEach(visit); });
|
|
66
|
+
Object.values(root.options.labels).forEach(function (labels) { labels.forEach(label); });
|
|
67
|
+
var urls = Array.from(new Set(runs.map(function (run) { return run.imageUrl; })));
|
|
68
|
+
if (urls.length > 4) throw failure('Wireframes support at most four distinct images.');
|
|
69
|
+
if (!urls.length) return;
|
|
70
|
+
var controller = new AbortController();
|
|
71
|
+
var signal = controller.signal;
|
|
72
|
+
function cancel() { controller.abort(); }
|
|
73
|
+
if (options.signal) {
|
|
74
|
+
if (options.signal.aborted) cancel();
|
|
75
|
+
else options.signal.addEventListener('abort', cancel, { once: true });
|
|
76
|
+
}
|
|
77
|
+
var timer = setTimeout(cancel, 10000);
|
|
78
|
+
var embeddedBytes = 0;
|
|
79
|
+
try { for (var url of urls) {
|
|
80
|
+
var resource;
|
|
81
|
+
try { resource = options.imageLoader ? decode(await bounded(Promise.resolve(options.imageLoader(url, signal)), signal)) : await download(url, signal); }
|
|
82
|
+
catch (error) { throw Object.assign(failure(error.message), { status: options.signal && options.signal.aborted ? 499 : 422 }); }
|
|
83
|
+
runs.forEach(function (run) { if (run.imageUrl === url) { run.image = resource; embeddedBytes += resource.data.length; } });
|
|
84
|
+
if (embeddedBytes > 6 * 1024 * 1024) throw failure('Embedded wireframe images exceed the 6 MB output budget.');
|
|
85
|
+
} } finally { clearTimeout(timer); if (options.signal) options.signal.removeEventListener('abort', cancel); }
|
|
86
|
+
}
|
|
87
|
+
module.exports = { resolveImages: resolveImages, publicAddress: publicAddress, decode: decode };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Geometry adapted from PlantUML 1.2026.8 Salt, (c) Arnaud Roques, GPL-3.0-or-later.
|
|
2
|
+
// See LICENSES/plantuml-salt.txt. All distances retain reference fractional precision.
|
|
3
|
+
var text = require('./salt-text');
|
|
4
|
+
function measure(node) {
|
|
5
|
+
measureNode(node);
|
|
6
|
+
if (node.width > 8192 || node.height > 8192) throw Object.assign(new Error('Wireframe exceeds the 8192-pixel layout limit. Split it into smaller views.'), { status: 413 });
|
|
7
|
+
}
|
|
8
|
+
function measureNode(node) {
|
|
9
|
+
if (node.type === 'sprite') return;
|
|
10
|
+
if (node.type === 'span') { node.width = 0; node.height = 0; return; }
|
|
11
|
+
if (node.type !== 'container') {
|
|
12
|
+
var managed = /^(button|input|select)$/.test(node.type);
|
|
13
|
+
node.label = text.measureLabel(node, managed);
|
|
14
|
+
node.width = managed ? Math.max(node.label.width, node.charLength * 8) : node.label.width;
|
|
15
|
+
node.height = node.label.height;
|
|
16
|
+
if (node.type === 'button') { node.width += 9; node.height += 9; }
|
|
17
|
+
if (node.type === 'input') { node.width += 6; node.height += 2; }
|
|
18
|
+
if (node.type === 'select') { node.width += 16; node.height += 4; node.optionLabels = (node.options || []).map(function (option) { return text.measureLabel(option); }); }
|
|
19
|
+
if (/^(radio|checkbox)$/.test(node.type)) node.width += 20;
|
|
20
|
+
if (node.type === 'separator') { node.width = 10; node.height = 6; }
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
node.rows.forEach(function (row) { row.forEach(measure); });
|
|
24
|
+
if (node.style[0] === 'T') return require('./salt-tree').measure(node);
|
|
25
|
+
if (node.style === '/' || node.style === '*') {
|
|
26
|
+
node.items = node.rows.flat();
|
|
27
|
+
(node.popups || []).forEach(function (items) { items.slice(1).forEach(function (item) { measure(item); if (item.text === '-') { item.width = 10; item.height = 5; } }); });
|
|
28
|
+
node.vertical = node.style === '/' && (node.verticalTabs || node.rows.length > 1);
|
|
29
|
+
var gap = node.style === '/' ? 15 : 10;
|
|
30
|
+
node.width = node.vertical ? Math.max(0, ...node.items.map(function (c) { return c.width; })) : node.items.reduce(function (sum, c) { return sum + c.width + gap; }, 0);
|
|
31
|
+
node.height = node.vertical ? node.items.reduce(function (sum, c) { return sum + c.height + gap; }, 0) : Math.max(0, ...node.items.map(function (c) { return c.height; }));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
node.titleLabel = node.title ? text.measureLabel(node.titleData || text.parseLabel(node.title)) : { width: 0, height: 0, runs: [] };
|
|
35
|
+
var titleHeight = node.titleLabel.height;
|
|
36
|
+
var cols = Math.max(1, ...node.rows.map(function (r) { return r.length; }));
|
|
37
|
+
node.columns = Array(cols).fill(0);
|
|
38
|
+
node.heights = Array(Math.max(1, node.rows.length)).fill(0);
|
|
39
|
+
node.rows.forEach(function (row, r) {
|
|
40
|
+
row.forEach(function (child, c) {
|
|
41
|
+
if (child.type === 'span') return;
|
|
42
|
+
if (!child.span) node.columns[c] = Math.max(node.columns[c], child.width + 2);
|
|
43
|
+
node.heights[r] = Math.max(node.heights[r], child.height + 2 + (r === 0 ? titleHeight / 2 : 0));
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
node.rows.forEach(function (row) { row.forEach(function (child, c) {
|
|
47
|
+
if (!child.span) return;
|
|
48
|
+
var available = node.columns.slice(c, c + child.span).reduce(function (a, b) { return a + b; }, 0);
|
|
49
|
+
node.columns[c + child.span - 1] += Math.max(0, child.width + 2 - available);
|
|
50
|
+
}); });
|
|
51
|
+
node.xs = [0]; node.ys = [titleHeight / 2];
|
|
52
|
+
node.columns.forEach(function (w) { node.xs.push(node.xs[node.xs.length - 1] + w); });
|
|
53
|
+
node.heights.forEach(function (h) { node.ys.push(node.ys[node.ys.length - 1] + h); });
|
|
54
|
+
node.width = node.xs[node.xs.length - 1];
|
|
55
|
+
node.height = node.ys[node.ys.length - 1] + titleHeight;
|
|
56
|
+
node.contentWidth = node.width; node.contentHeight = node.height;
|
|
57
|
+
if (node.scroll && node.scroll !== 'S-') node.width += 30;
|
|
58
|
+
if (node.scroll && node.scroll !== 'SI') node.height += 30;
|
|
59
|
+
}
|
|
60
|
+
module.exports = { measure: measure };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Read only bounded display directives; never evaluate PlantUML preprocessing.
|
|
2
|
+
var color = require('./salt-text').color;
|
|
3
|
+
function parse(body) {
|
|
4
|
+
var options = { scale: 1, background: '#FFFFFF' };
|
|
5
|
+
body = body.replace(/^[ \t]*legend[ \t]*\n([\s\S]*?)^[ \t]*end[ \t]*legend[ \t]*$/gim, function (_, value) { options.legend = value.trim(); return ''; });
|
|
6
|
+
body = body.replace(/^[ \t]*(title|header|footer|caption)[ \t]+([^\n]+)$/gim, function (_, name, value) { options[name.toLowerCase()] = value.trim(); return ''; });
|
|
7
|
+
body = body.replace(/^\s*!option handwritten (true|false)[ \t]*$/gim, function (_, value) { options.handwritten = value === 'true'; return ''; });
|
|
8
|
+
body = body.replace(/<style>\s*saltDiagram\s*\{([^}]*)\}\s*<\/style>/gi, function (_, contents) {
|
|
9
|
+
contents.trim().split(/\n/).forEach(function (line) { setting(line.trim()); }); return '';
|
|
10
|
+
});
|
|
11
|
+
function setting(line) {
|
|
12
|
+
var m = /^(BackgroundColor|dpi|defaultFontName|Fontname|FontSize|FontStyle|LineThickness|LineColor)\s+(\S+)$/i.exec(line);
|
|
13
|
+
if (!m) throw new Error('Unsupported Salt display setting: ' + line);
|
|
14
|
+
if (/^BackgroundColor$/i.test(m[1])) options.background = color(m[2]);
|
|
15
|
+
if (/^dpi$/i.test(m[1])) options.scale = Number(m[2]) / 96;
|
|
16
|
+
// The reference Salt renderer ignores these font and stroke style settings.
|
|
17
|
+
}
|
|
18
|
+
body = body.replace(/^\s*skinparam[ \t]+([^\n]+)$/gim, function (_, value) { setting(value.trim()); return ''; });
|
|
19
|
+
body = body.replace(/^\s*scale[ \t]+([\d.]+)[ \t]*$/gim, function (_, value) { options.scale = Number(value); return ''; });
|
|
20
|
+
if (!Number.isFinite(options.scale) || options.scale <= 0 || options.scale > 8) throw new Error('Salt scale must be greater than zero and at most 8.');
|
|
21
|
+
return { body: body, options: options };
|
|
22
|
+
}
|
|
23
|
+
module.exports = { parse: parse };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Adapted from PlantUML Salt controls, GPL-3.0-or-later; see LICENSES/plantuml-salt.txt.
|
|
2
|
+
function dropdown(node, x, y, s) {
|
|
3
|
+
var labels = node.optionLabels || [];
|
|
4
|
+
if (!labels.length) return;
|
|
5
|
+
var width = Math.max(node.width - 1, ...labels.map(function (l) { return l.width; }));
|
|
6
|
+
var height = labels.reduce(function (a, l) { return a + l.height; }, 0);
|
|
7
|
+
y += node.height - 1;
|
|
8
|
+
s.rect(x, y, width - 1, height - 1, '#EEE');
|
|
9
|
+
labels.forEach(function (label) { s.label(label, x, y); y += label.height; });
|
|
10
|
+
}
|
|
11
|
+
function menu(node, x, y, s) {
|
|
12
|
+
(node.popups || []).forEach(function (items) {
|
|
13
|
+
var offset = 0;
|
|
14
|
+
for (var entry of node.items) { if (entry.text === items[0].text) break; offset += entry.width + 10; }
|
|
15
|
+
var children = items.slice(1);
|
|
16
|
+
var width = Math.max(0, ...children.map(function (item) { return item.width; }));
|
|
17
|
+
var height = children.reduce(function (a, item) { return a + item.height; }, 0);
|
|
18
|
+
var cy = y + node.height;
|
|
19
|
+
s.rect(x + offset, cy, width, height, '#DDD');
|
|
20
|
+
children.forEach(function (item) {
|
|
21
|
+
if (item.text === '-') s.line(x + offset, cy + item.height / 2, x + offset + width, cy + item.height / 2);
|
|
22
|
+
else s.label(item.label, x + offset, cy);
|
|
23
|
+
cy += item.height;
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function scroll(node, x, y, s) {
|
|
28
|
+
var w = node.contentWidth; var h = node.contentHeight;
|
|
29
|
+
function triangle(xx, yy, points) { s.path(points.map(function (p, i) { return (i ? 'L' : 'M') + (xx + p[0]) + ',' + (yy + p[1]); }).join(' ') + '', '#000', null, xx + 6, yy + 6); }
|
|
30
|
+
if (node.scroll !== 'S-') {
|
|
31
|
+
var vx = x + w + 4;
|
|
32
|
+
s.rect(vx, y, 15, h);
|
|
33
|
+
s.line(vx, y + 12, vx + 15, y + 12); s.line(vx, y + h - 12, vx + 15, y + h - 12);
|
|
34
|
+
triangle(vx + 4, y + 4, [[3,0],[6,5],[0,5],[3,0]]);
|
|
35
|
+
triangle(vx + 4, y + h - 8, [[3,5],[6,0],[0,0],[3,5]]);
|
|
36
|
+
}
|
|
37
|
+
if (node.scroll !== 'SI') {
|
|
38
|
+
var hy = y + h + 4;
|
|
39
|
+
s.rect(x, hy, w, 15);
|
|
40
|
+
s.line(x + 12, hy, x + 12, hy + 15); s.line(x + w - 12, hy, x + w - 12, hy + 15);
|
|
41
|
+
triangle(x + 4, hy + 4, [[0,3],[5,6],[5,0],[0,3]]);
|
|
42
|
+
triangle(x + w - 8, hy + 4, [[5,3],[0,6],[0,0],[5,3]]);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
module.exports = { dropdown: dropdown, menu: menu, scroll: scroll };
|