clay-server 4.8.2 → 4.9.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/plantuml-renderer.js +43 -0
- package/lib/project-file-http.js +2 -0
- package/lib/project-plantuml.js +48 -0
- package/lib/project-session-document.js +17 -1
- package/lib/project-wireframe-http.js +88 -0
- package/lib/public/app.js +6 -0
- package/lib/public/css/filebrowser.css +5 -0
- package/lib/public/css/wireframe.css +95 -0
- package/lib/public/modules/app-messages.js +8 -1
- package/lib/public/modules/filebrowser.js +29 -3
- package/lib/public/modules/markdown.js +3 -1
- package/lib/public/modules/plantuml-preview.js +76 -0
- package/lib/public/modules/split-view.js +14 -0
- 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 +1 -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 +2 -1
- package/lib/session-document-mcp-server.js +9 -0
- package/package.json +1 -1
|
@@ -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 };
|
|
@@ -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 { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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 };
|