@peter.prib/renderengine 0.15.2 → 0.15.3
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/dist/renderengine-workspace.umd.js +5963 -3766
- package/dist/renderengine.umd.js +5863 -3772
- package/documentation/REFERENCE.md +37 -3
- package/package.json +1 -1
- package/src/archimateArchiFormat.js +114 -10
- package/src/archimateArchive.js +27 -4
- package/src/archimateExchangeFormat.js +12 -4
- package/src/archimateSpecializations.js +11 -1
- package/src/chartCatalog.js +6 -4
- package/src/charting/ArchimateDiagramEditor.js +5 -108
- package/src/charting/DiagramTypeRegistry.js +20 -1
- package/src/charting/DiagramViewPropertiesDialog.js +54 -10
- package/src/charting/Legend.js +1 -2
- package/src/charting/Statistics.js +14 -0
- package/src/charting/charts/BarChart.js +59 -8
- package/src/charting/charts/HexbinChart.js +15 -0
- package/src/charting/charts/LollipopChart.js +42 -7
- package/src/charting/decision/DecisionWorkbench.js +71 -4
- package/src/charting/diagramModel/DiagramView.js +7 -2
- package/src/charting/diagrams/GraphDiagram3D.js +15 -0
- package/src/charting/graph/GraphSource.js +72 -0
- package/src/charting/modelTree/NavigatorModel.js +22 -8
- package/src/charting/report/TemplatedReport.js +27 -377
- package/src/charting/report/ViewsReport.js +118 -0
- package/src/charting/report/reportWriters.js +496 -0
- package/src/charting/templates/C4BoundaryTemplate.js +34 -0
- package/src/charting/templates/C4ComponentTemplate.js +30 -0
- package/src/charting/templates/C4ContainerTemplate.js +49 -0
- package/src/charting/templates/C4PersonTemplate.js +35 -0
- package/src/charting/templates/C4SystemTemplate.js +32 -0
- package/src/charting/templates/C4Template.js +95 -0
- package/src/charting/templates/index.js +6 -0
- package/src/charting/templates/templateRegistry.js +7 -0
- package/src/compute/ComputeBackend.js +132 -0
- package/src/compute/CpuBackend.js +209 -0
- package/src/compute/WebGpuBackend.js +630 -0
- package/src/compute/index.js +5 -0
- package/src/compute/selectBackend.js +85 -0
- package/src/index.js +3403 -3371
- package/src/kernelExpr.js +80 -5
- package/src/workspace/parts/DiagramEditorPart.js +3 -1
- package/src/workspace/parts/ViewsPart.js +106 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure format writers for templated reports (HTML, Word, Excel, CSV, Markdown).
|
|
3
|
+
*
|
|
4
|
+
* Separates document generation formats from model / view data extraction,
|
|
5
|
+
* allowing any report producer (ArchiMate models, ViewsPart, DecisionWorkbench)
|
|
6
|
+
* to output to all 5 formats cleanly.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export function escapeHtml(str) {
|
|
10
|
+
return String(str ?? '')
|
|
11
|
+
.replace(/&/g, '&')
|
|
12
|
+
.replace(/</g, '<')
|
|
13
|
+
.replace(/>/g, '>')
|
|
14
|
+
.replace(/"/g, '"')
|
|
15
|
+
.replace(/'/g, ''');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function escapeXml(str) {
|
|
19
|
+
return escapeHtml(str);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function escapeCsv(val) {
|
|
23
|
+
const s = String(val ?? '');
|
|
24
|
+
if (/[",\n\r]/.test(s)) {
|
|
25
|
+
return `"${s.replace(/"/g, '""')}"`;
|
|
26
|
+
}
|
|
27
|
+
return s;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Render report data as HTML with print-to-PDF styles and vector SVG view diagrams.
|
|
32
|
+
* @param {Object} data - normalized report data
|
|
33
|
+
* @param {Object} [options]
|
|
34
|
+
* @param {boolean} [options.printable=true]
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
export function formatHtml(data, { printable = true } = {}) {
|
|
38
|
+
const { model, views = [], elements = [], relationships = [], properties = [], date = new Date().toISOString().split('T')[0] } = data;
|
|
39
|
+
const modelName = model?.name || data.title || 'Report';
|
|
40
|
+
const modelVersion = model?.version || data.version || '1.0';
|
|
41
|
+
const modelDoc = model?.documentation || data.documentation || '';
|
|
42
|
+
|
|
43
|
+
const printCss = printable ? `
|
|
44
|
+
@media print {
|
|
45
|
+
body { background: #fff; margin: 0; padding: 0; font-size: 11pt; color: #000; }
|
|
46
|
+
header, footer { position: fixed; width: 100%; left: 0; font-size: 9pt; color: #777; }
|
|
47
|
+
header { top: 0; border-bottom: 1px solid #ccc; padding-bottom: 4px; }
|
|
48
|
+
footer { bottom: 0; border-top: 1px solid #ccc; padding-top: 4px; display: flex; justify-content: space-between; }
|
|
49
|
+
.page-break { page-break-after: always; }
|
|
50
|
+
.no-break { page-break-inside: avoid; }
|
|
51
|
+
.picture { max-width: 100% !important; page-break-inside: avoid; }
|
|
52
|
+
table { page-break-inside: auto; }
|
|
53
|
+
tr { page-break-inside: avoid; page-break-after: auto; }
|
|
54
|
+
}
|
|
55
|
+
@page {
|
|
56
|
+
size: A4;
|
|
57
|
+
margin: 20mm;
|
|
58
|
+
}` : '';
|
|
59
|
+
|
|
60
|
+
return `<!DOCTYPE html>
|
|
61
|
+
<html lang="en">
|
|
62
|
+
<head>
|
|
63
|
+
<meta charset="utf-8">
|
|
64
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
65
|
+
<title>${escapeHtml(modelName)}</title>
|
|
66
|
+
<style>
|
|
67
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; line-height: 1.5; color: #222; margin: 24px auto; max-width: 960px; padding: 0 16px; }
|
|
68
|
+
h1 { font-size: 26px; border-bottom: 2px solid #2563eb; padding-bottom: 6px; margin-top: 24px; color: #1e3a8a; }
|
|
69
|
+
h2 { font-size: 20px; border-bottom: 1px solid #cbd5e1; padding-bottom: 4px; margin-top: 24px; color: #1e40af; }
|
|
70
|
+
h3 { font-size: 16px; margin-top: 18px; color: #334155; }
|
|
71
|
+
h4 { font-size: 14px; margin-top: 12px; color: #475569; }
|
|
72
|
+
.meta { font-size: 13px; color: #64748b; margin-bottom: 20px; }
|
|
73
|
+
.doc { white-space: pre-wrap; background: #f8fafc; border-left: 4px solid #3b82f6; padding: 10px 14px; margin: 12px 0; font-size: 14px; }
|
|
74
|
+
.notes { margin: 8px 0; padding-left: 20px; }
|
|
75
|
+
.notes li { margin-bottom: 4px; }
|
|
76
|
+
.chart-block { margin: 16px 0; }
|
|
77
|
+
table { width: 100%; border-collapse: collapse; margin: 16px 0; font-size: 13px; }
|
|
78
|
+
th, td { border: 1px solid #e2e8f0; padding: 6px 10px; text-align: left; vertical-align: top; }
|
|
79
|
+
th { background: #f1f5f9; font-weight: 600; color: #334155; }
|
|
80
|
+
tr:nth-child(even) { background: #f8fafc; }
|
|
81
|
+
.picture { margin: 16px 0; border: 1px solid #e2e8f0; border-radius: 4px; padding: 8px; background: #fff; overflow-x: auto; text-align: center; }
|
|
82
|
+
.picture svg { max-width: 100%; height: auto; }
|
|
83
|
+
.badge { display: inline-block; padding: 2px 6px; font-size: 11px; font-weight: 500; border-radius: 3px; background: #e0f2fe; color: #0369a1; }
|
|
84
|
+
${printCss}
|
|
85
|
+
</style>
|
|
86
|
+
</head>
|
|
87
|
+
<body>
|
|
88
|
+
<h1>${escapeHtml(modelName)}</h1>
|
|
89
|
+
<div class="meta">Generated: ${escapeHtml(date)} | Version: ${escapeHtml(modelVersion)}</div>
|
|
90
|
+
${modelDoc ? `<div class="doc">${escapeHtml(modelDoc)}</div>` : ''}
|
|
91
|
+
|
|
92
|
+
${properties.length ? `
|
|
93
|
+
<h2>Properties</h2>
|
|
94
|
+
<table>
|
|
95
|
+
<thead><tr><th>Property</th><th>Value</th></tr></thead>
|
|
96
|
+
<tbody>
|
|
97
|
+
${properties.map(p => `<tr><td>${escapeHtml(p.name)}</td><td>${escapeHtml(p.value)}</td></tr>`).join('')}
|
|
98
|
+
</tbody>
|
|
99
|
+
</table>` : ''}
|
|
100
|
+
|
|
101
|
+
${views.length ? `
|
|
102
|
+
<h2>Views (${views.length})</h2>
|
|
103
|
+
${views.map(v => `
|
|
104
|
+
<div class="no-break">
|
|
105
|
+
<h3>${escapeHtml(v.name || v.title || v.id)}</h3>
|
|
106
|
+
${v.documentation ? `<div class="doc">${escapeHtml(v.documentation)}</div>` : ''}
|
|
107
|
+
${v.notes?.length && v.notes.join('\n\n') !== v.documentation ? `
|
|
108
|
+
<ul class="notes">
|
|
109
|
+
${v.notes.map(n => `<li>${escapeHtml(n)}</li>`).join('')}
|
|
110
|
+
</ul>` : ''}
|
|
111
|
+
${v.charts?.length ? v.charts.map(c => `
|
|
112
|
+
<div class="chart-block">
|
|
113
|
+
${c.title ? `<h4>${escapeHtml(c.title)}</h4>` : ''}
|
|
114
|
+
${c.picture ? `<div class="picture">${c.picture}</div>` : ''}
|
|
115
|
+
</div>
|
|
116
|
+
`).join('') : (v.picture ? `<div class="picture">${v.picture}</div>` : '')}
|
|
117
|
+
${v.elements?.length ? `
|
|
118
|
+
<table>
|
|
119
|
+
<thead><tr><th>Element</th><th>Type</th></tr></thead>
|
|
120
|
+
<tbody>
|
|
121
|
+
${v.elements.map(e => `<tr><td>${escapeHtml(e.name)}</td><td><span class="badge">${escapeHtml(e.type)}</span></td></tr>`).join('')}
|
|
122
|
+
</tbody>
|
|
123
|
+
</table>` : ''}
|
|
124
|
+
</div>
|
|
125
|
+
<div class="page-break"></div>
|
|
126
|
+
`).join('')}` : ''}
|
|
127
|
+
|
|
128
|
+
${elements.length ? `
|
|
129
|
+
<h2>Elements (${elements.length})</h2>
|
|
130
|
+
<table>
|
|
131
|
+
<thead><tr><th>Name</th><th>Type</th><th>Layer</th><th>Documentation</th></tr></thead>
|
|
132
|
+
<tbody>
|
|
133
|
+
${elements.map(e => `<tr><td><strong>${escapeHtml(e.name)}</strong></td><td><span class="badge">${escapeHtml(e.type)}</span></td><td>${escapeHtml(e.layer)}</td><td>${escapeHtml(e.documentation)}</td></tr>`).join('')}
|
|
134
|
+
</tbody>
|
|
135
|
+
</table>` : ''}
|
|
136
|
+
|
|
137
|
+
${relationships.length ? `
|
|
138
|
+
<h2>Relationships (${relationships.length})</h2>
|
|
139
|
+
<table>
|
|
140
|
+
<thead><tr><th>Type</th><th>Source</th><th>Target</th><th>Name</th><th>Documentation</th></tr></thead>
|
|
141
|
+
<tbody>
|
|
142
|
+
${relationships.map(r => `<tr><td><span class="badge">${escapeHtml(r.type)}</span></td><td>${escapeHtml(r.source)}</td><td>${escapeHtml(r.target)}</td><td>${escapeHtml(r.name)}</td><td>${escapeHtml(r.documentation)}</td></tr>`).join('')}
|
|
143
|
+
</tbody>
|
|
144
|
+
</table>` : ''}
|
|
145
|
+
|
|
146
|
+
</body>
|
|
147
|
+
</html>`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Render report data as Microsoft Word-compatible document (.doc).
|
|
152
|
+
* @param {Object} data - normalized report data
|
|
153
|
+
* @returns {string}
|
|
154
|
+
*/
|
|
155
|
+
export function formatWord(data) {
|
|
156
|
+
const { model, views = [], elements = [], relationships = [], properties = [], date = new Date().toISOString().split('T')[0] } = data;
|
|
157
|
+
const modelName = model?.name || data.title || 'Report';
|
|
158
|
+
const modelVersion = model?.version || data.version || '1.0';
|
|
159
|
+
const modelDoc = model?.documentation || data.documentation || '';
|
|
160
|
+
|
|
161
|
+
return `<html xmlns:o="urn:schemas-microsoft-com:office:office"
|
|
162
|
+
xmlns:w="urn:schemas-microsoft-com:office:word"
|
|
163
|
+
xmlns="http://www.w3.org/TR/REC-html40">
|
|
164
|
+
<head>
|
|
165
|
+
<meta charset="utf-8">
|
|
166
|
+
<title>${escapeXml(modelName)}</title>
|
|
167
|
+
<!--[if gte mso 9]>
|
|
168
|
+
<xml>
|
|
169
|
+
<w:WordDocument>
|
|
170
|
+
<w:View>Print</w:View>
|
|
171
|
+
<w:Zoom>100</w:Zoom>
|
|
172
|
+
<w:DoNotOptimizeForBrowser/>
|
|
173
|
+
</w:WordDocument>
|
|
174
|
+
</xml>
|
|
175
|
+
<![endif]-->
|
|
176
|
+
<style>
|
|
177
|
+
body { font-family: Calibri, Arial, sans-serif; font-size: 11pt; line-height: 1.4; color: #333333; }
|
|
178
|
+
h1 { font-size: 22pt; color: #1f4e79; border-bottom: 2pt solid #1f4e79; margin-bottom: 12pt; }
|
|
179
|
+
h2 { font-size: 16pt; color: #2e75b6; margin-top: 18pt; margin-bottom: 8pt; }
|
|
180
|
+
h3 { font-size: 13pt; color: #595959; margin-top: 12pt; }
|
|
181
|
+
h4 { font-size: 11pt; color: #595959; margin-top: 8pt; font-weight: bold; }
|
|
182
|
+
p.doc { font-style: italic; background-color: #f2f2f2; padding: 6pt; border-left: 3pt solid #2e75b6; }
|
|
183
|
+
table { width: 100%; border-collapse: collapse; margin-top: 8pt; margin-bottom: 12pt; }
|
|
184
|
+
th, td { border: 0.5pt solid #d9d9d9; padding: 4pt 6pt; font-size: 10pt; }
|
|
185
|
+
th { background-color: #f2f2f2; font-weight: bold; color: #1f4e79; }
|
|
186
|
+
.page-break { page-break-before: always; }
|
|
187
|
+
</style>
|
|
188
|
+
</head>
|
|
189
|
+
<body>
|
|
190
|
+
<h1>${escapeXml(modelName)}</h1>
|
|
191
|
+
<p><strong>Generated:</strong> ${escapeXml(date)} | <strong>Version:</strong> ${escapeXml(modelVersion)}</p>
|
|
192
|
+
${modelDoc ? `<p class="doc">${escapeXml(modelDoc)}</p>` : ''}
|
|
193
|
+
|
|
194
|
+
${properties.length ? `
|
|
195
|
+
<h2>Properties</h2>
|
|
196
|
+
<table>
|
|
197
|
+
<tr><th>Property</th><th>Value</th></tr>
|
|
198
|
+
${properties.map(p => `<tr><td>${escapeXml(p.name)}</td><td>${escapeXml(p.value)}</td></tr>`).join('')}
|
|
199
|
+
</table>` : ''}
|
|
200
|
+
|
|
201
|
+
${views.length ? `
|
|
202
|
+
<h2>Views</h2>
|
|
203
|
+
${views.map(v => `
|
|
204
|
+
<div class="page-break"></div>
|
|
205
|
+
<h3>${escapeXml(v.name || v.title || v.id)}</h3>
|
|
206
|
+
${v.documentation ? `<p class="doc">${escapeXml(v.documentation)}</p>` : ''}
|
|
207
|
+
${v.charts?.length ? v.charts.map(c => `
|
|
208
|
+
<div>
|
|
209
|
+
${c.title ? `<h4>${escapeXml(c.title)}</h4>` : ''}
|
|
210
|
+
${c.picture ? `<div>${c.picture}</div>` : ''}
|
|
211
|
+
</div>
|
|
212
|
+
`).join('') : (v.picture ? `<div>${v.picture}</div>` : '')}
|
|
213
|
+
${v.elements?.length ? `
|
|
214
|
+
<table>
|
|
215
|
+
<tr><th>Element</th><th>Type</th></tr>
|
|
216
|
+
${v.elements.map(e => `<tr><td>${escapeXml(e.name)}</td><td>${escapeXml(e.type)}</td></tr>`).join('')}
|
|
217
|
+
</table>` : ''}
|
|
218
|
+
`).join('')}` : ''}
|
|
219
|
+
|
|
220
|
+
${elements.length ? `
|
|
221
|
+
<div class="page-break"></div>
|
|
222
|
+
<h2>Elements (${elements.length})</h2>
|
|
223
|
+
<table>
|
|
224
|
+
<tr><th>Name</th><th>Type</th><th>Layer</th><th>Documentation</th></tr>
|
|
225
|
+
${elements.map(e => `<tr><td><strong>${escapeXml(e.name)}</strong></td><td>${escapeXml(e.type)}</td><td>${escapeXml(e.layer)}</td><td>${escapeXml(e.documentation)}</td></tr>`).join('')}
|
|
226
|
+
</table>` : ''}
|
|
227
|
+
|
|
228
|
+
${relationships.length ? `
|
|
229
|
+
<div class="page-break"></div>
|
|
230
|
+
<h2>Relationships (${relationships.length})</h2>
|
|
231
|
+
<table>
|
|
232
|
+
<tr><th>Type</th><th>Source</th><th>Target</th><th>Name</th></tr>
|
|
233
|
+
${relationships.map(r => `<tr><td>${escapeXml(r.type)}</td><td>${escapeXml(r.source)}</td><td>${escapeXml(r.target)}</td><td>${escapeXml(r.name)}</td></tr>`).join('')}
|
|
234
|
+
</table>` : ''}
|
|
235
|
+
|
|
236
|
+
</body>
|
|
237
|
+
</html>`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Render report data as Microsoft Excel XML Spreadsheet 2003 (.xml).
|
|
242
|
+
* @param {Object} data - normalized report data
|
|
243
|
+
* @returns {string}
|
|
244
|
+
*/
|
|
245
|
+
export function formatExcel(data) {
|
|
246
|
+
const { model, views = [], elements = [], relationships = [], properties = [], date = new Date().toISOString().split('T')[0] } = data;
|
|
247
|
+
const modelName = model?.name || data.title || 'Report';
|
|
248
|
+
const modelVersion = model?.version || data.version || '1.0';
|
|
249
|
+
const modelDoc = model?.documentation || data.documentation || '';
|
|
250
|
+
|
|
251
|
+
const makeRow = (cells, isHeader = false) => `
|
|
252
|
+
<Row>
|
|
253
|
+
${cells.map(c => ` <Cell${isHeader ? ' ss:StyleID="Header"' : ''}><Data ss:Type="String">${escapeXml(c)}</Data></Cell>`).join('\n')}
|
|
254
|
+
</Row>`;
|
|
255
|
+
|
|
256
|
+
const hasCharts = views.some(v => v.charts?.length);
|
|
257
|
+
|
|
258
|
+
return `<?xml version="1.0"?>
|
|
259
|
+
<?mso-application progid="Excel.Sheet"?>
|
|
260
|
+
<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"
|
|
261
|
+
xmlns:o="urn:schemas-microsoft-com:office:office"
|
|
262
|
+
xmlns:x="urn:schemas-microsoft-com:office:excel"
|
|
263
|
+
xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet">
|
|
264
|
+
<Styles>
|
|
265
|
+
<Style ss:ID="Default" ss:Name="Normal">
|
|
266
|
+
<Alignment ss:Vertical="Center"/>
|
|
267
|
+
<Font ss:FontName="Calibri" ss:Size="11" ss:Color="#000000"/>
|
|
268
|
+
</Style>
|
|
269
|
+
<Style ss:ID="Header">
|
|
270
|
+
<Font ss:FontName="Calibri" ss:Size="11" ss:Color="#FFFFFF" ss:Bold="1"/>
|
|
271
|
+
<Interior ss:Color="#2563EB" ss:Pattern="Solid"/>
|
|
272
|
+
</Style>
|
|
273
|
+
<Style ss:ID="Title">
|
|
274
|
+
<Font ss:FontName="Calibri" ss:Size="16" ss:Color="#1E3A8A" ss:Bold="1"/>
|
|
275
|
+
</Style>
|
|
276
|
+
</Styles>
|
|
277
|
+
|
|
278
|
+
<Worksheet ss:Name="Model Summary">
|
|
279
|
+
<Table>
|
|
280
|
+
<Column ss:Width="160"/>
|
|
281
|
+
<Column ss:Width="400"/>
|
|
282
|
+
<Row>
|
|
283
|
+
<Cell ss:StyleID="Title"><Data ss:Type="String">${escapeXml(modelName)}</Data></Cell>
|
|
284
|
+
</Row>
|
|
285
|
+
${makeRow(['Item', 'Details'], true)}
|
|
286
|
+
${makeRow(['Title', modelName])}
|
|
287
|
+
${makeRow(['Version', modelVersion])}
|
|
288
|
+
${makeRow(['Generated', date])}
|
|
289
|
+
${makeRow(['Documentation', modelDoc])}
|
|
290
|
+
${makeRow(['Total Views', String(views.length)])}
|
|
291
|
+
${elements.length ? makeRow(['Total Elements', String(elements.length)]) : ''}
|
|
292
|
+
${relationships.length ? makeRow(['Total Relationships', String(relationships.length)]) : ''}
|
|
293
|
+
</Table>
|
|
294
|
+
</Worksheet>
|
|
295
|
+
|
|
296
|
+
<Worksheet ss:Name="Views">
|
|
297
|
+
<Table>
|
|
298
|
+
<Column ss:Width="180"/>
|
|
299
|
+
<Column ss:Width="80"/>
|
|
300
|
+
<Column ss:Width="300"/>
|
|
301
|
+
${makeRow(['View Name', hasCharts ? 'Charts Count' : 'Elements Count', 'Documentation'], true)}
|
|
302
|
+
${views.map(v => makeRow([v.name || v.title || v.id, String(hasCharts ? (v.charts?.length ?? 0) : (v.elements?.length ?? 0)), v.documentation || v.notes?.join('; ') || ''])).join('')}
|
|
303
|
+
</Table>
|
|
304
|
+
</Worksheet>
|
|
305
|
+
|
|
306
|
+
${hasCharts ? `
|
|
307
|
+
<Worksheet ss:Name="Charts">
|
|
308
|
+
<Table>
|
|
309
|
+
<Column ss:Width="180"/>
|
|
310
|
+
<Column ss:Width="200"/>
|
|
311
|
+
<Column ss:Width="120"/>
|
|
312
|
+
${makeRow(['View', 'Chart Title', 'Chart Type'], true)}
|
|
313
|
+
${views.flatMap(v => (v.charts || []).map(c => makeRow([v.name || v.title || v.id, c.title || '', c.chartType || '']))).join('')}
|
|
314
|
+
</Table>
|
|
315
|
+
</Worksheet>` : ''}
|
|
316
|
+
|
|
317
|
+
${elements.length ? `
|
|
318
|
+
<Worksheet ss:Name="Elements">
|
|
319
|
+
<Table>
|
|
320
|
+
<Column ss:Width="140"/>
|
|
321
|
+
<Column ss:Width="160"/>
|
|
322
|
+
<Column ss:Width="100"/>
|
|
323
|
+
<Column ss:Width="280"/>
|
|
324
|
+
${makeRow(['Name', 'Type', 'Layer', 'Documentation'], true)}
|
|
325
|
+
${elements.map(e => makeRow([e.name, e.type, e.layer, e.documentation])).join('')}
|
|
326
|
+
</Table>
|
|
327
|
+
</Worksheet>` : ''}
|
|
328
|
+
|
|
329
|
+
${relationships.length ? `
|
|
330
|
+
<Worksheet ss:Name="Relationships">
|
|
331
|
+
<Table>
|
|
332
|
+
<Column ss:Width="140"/>
|
|
333
|
+
<Column ss:Width="160"/>
|
|
334
|
+
<Column ss:Width="160"/>
|
|
335
|
+
<Column ss:Width="100"/>
|
|
336
|
+
<Column ss:Width="200"/>
|
|
337
|
+
${makeRow(['Type', 'Source', 'Target', 'Name', 'Documentation'], true)}
|
|
338
|
+
${relationships.map(r => makeRow([r.type, r.source, r.target, r.name, r.documentation])).join('')}
|
|
339
|
+
</Table>
|
|
340
|
+
</Worksheet>` : ''}
|
|
341
|
+
|
|
342
|
+
${properties.length ? `
|
|
343
|
+
<Worksheet ss:Name="Properties">
|
|
344
|
+
<Table>
|
|
345
|
+
<Column ss:Width="160"/>
|
|
346
|
+
<Column ss:Width="300"/>
|
|
347
|
+
${makeRow(['Name', 'Value'], true)}
|
|
348
|
+
${properties.map(p => makeRow([p.name, p.value])).join('')}
|
|
349
|
+
</Table>
|
|
350
|
+
</Worksheet>` : ''}
|
|
351
|
+
</Workbook>`;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Export a report table as standard CSV.
|
|
356
|
+
* @param {Object} data - normalized report data
|
|
357
|
+
* @param {'elements'|'relationships'|'views'|'charts'|'properties'} [table='elements']
|
|
358
|
+
* @returns {string}
|
|
359
|
+
*/
|
|
360
|
+
export function formatCsv(data, table = 'elements') {
|
|
361
|
+
switch (table) {
|
|
362
|
+
case 'views': {
|
|
363
|
+
const hasCharts = data.views?.some(v => v.charts?.length);
|
|
364
|
+
const countCol = hasCharts ? 'Charts' : 'Elements';
|
|
365
|
+
const header = ['ID', 'Name', countCol, 'Documentation'];
|
|
366
|
+
const rows = (data.views || []).map(v => [
|
|
367
|
+
v.id,
|
|
368
|
+
v.name || v.title || v.id,
|
|
369
|
+
hasCharts ? (v.charts?.length ?? 0) : (v.elements?.length ?? 0),
|
|
370
|
+
v.documentation || v.notes?.join('; ') || '',
|
|
371
|
+
]);
|
|
372
|
+
return [header, ...rows].map(r => r.map(escapeCsv).join(',')).join('\r\n');
|
|
373
|
+
}
|
|
374
|
+
case 'charts': {
|
|
375
|
+
const header = ['View ID', 'View Name', 'Chart Title', 'Chart Type'];
|
|
376
|
+
const rows = (data.views || []).flatMap(v => (v.charts || []).map(c => [
|
|
377
|
+
v.id,
|
|
378
|
+
v.name || v.title || v.id,
|
|
379
|
+
c.title || '',
|
|
380
|
+
c.chartType || '',
|
|
381
|
+
]));
|
|
382
|
+
return [header, ...rows].map(r => r.map(escapeCsv).join(',')).join('\r\n');
|
|
383
|
+
}
|
|
384
|
+
case 'relationships': {
|
|
385
|
+
const header = ['ID', 'Type', 'Source', 'Target', 'Name', 'Documentation'];
|
|
386
|
+
const rows = (data.relationships || []).map(r => [r.id, r.type, r.source, r.target, r.name, r.documentation]);
|
|
387
|
+
return [header, ...rows].map(r => r.map(escapeCsv).join(',')).join('\r\n');
|
|
388
|
+
}
|
|
389
|
+
case 'properties': {
|
|
390
|
+
const header = ['Name', 'Value'];
|
|
391
|
+
const rows = (data.properties || []).map(p => [p.name, p.value]);
|
|
392
|
+
return [header, ...rows].map(r => r.map(escapeCsv).join(',')).join('\r\n');
|
|
393
|
+
}
|
|
394
|
+
case 'elements':
|
|
395
|
+
default: {
|
|
396
|
+
if (!data.elements?.length && data.views?.length) {
|
|
397
|
+
return formatCsv(data, 'views');
|
|
398
|
+
}
|
|
399
|
+
const header = ['ID', 'Name', 'Type', 'Layer', 'Documentation'];
|
|
400
|
+
const rows = (data.elements || []).map(e => [e.id, e.name, e.type, e.layer, e.documentation]);
|
|
401
|
+
return [header, ...rows].map(r => r.map(escapeCsv).join(',')).join('\r\n');
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Render report data as GitHub-flavored Markdown.
|
|
408
|
+
* @param {Object} data - normalized report data
|
|
409
|
+
* @returns {string}
|
|
410
|
+
*/
|
|
411
|
+
export function formatMarkdown(data) {
|
|
412
|
+
const { model, views = [], elements = [], relationships = [], properties = [], date = new Date().toISOString().split('T')[0] } = data;
|
|
413
|
+
const modelName = model?.name || data.title || 'Report';
|
|
414
|
+
const modelVersion = model?.version || data.version || '1.0';
|
|
415
|
+
const modelDoc = model?.documentation || data.documentation || '';
|
|
416
|
+
const lines = [];
|
|
417
|
+
|
|
418
|
+
lines.push(`# ${modelName}`);
|
|
419
|
+
lines.push(`*Generated: ${date} | Version: ${modelVersion}*`);
|
|
420
|
+
lines.push('');
|
|
421
|
+
|
|
422
|
+
if (modelDoc) {
|
|
423
|
+
lines.push(modelDoc);
|
|
424
|
+
lines.push('');
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (properties.length) {
|
|
428
|
+
lines.push('## Properties');
|
|
429
|
+
lines.push('');
|
|
430
|
+
lines.push('| Property | Value |');
|
|
431
|
+
lines.push('| --- | --- |');
|
|
432
|
+
for (const p of properties) {
|
|
433
|
+
lines.push(`| ${p.name} | ${p.value} |`);
|
|
434
|
+
}
|
|
435
|
+
lines.push('');
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (views.length) {
|
|
439
|
+
lines.push(`## Views (${views.length})`);
|
|
440
|
+
lines.push('');
|
|
441
|
+
for (const v of views) {
|
|
442
|
+
const vName = v.name || v.title || v.id;
|
|
443
|
+
lines.push(`### ${vName}`);
|
|
444
|
+
if (v.notes?.length) {
|
|
445
|
+
lines.push('');
|
|
446
|
+
for (const note of v.notes) {
|
|
447
|
+
lines.push(`- ${note}`);
|
|
448
|
+
}
|
|
449
|
+
} else if (v.documentation) {
|
|
450
|
+
lines.push('');
|
|
451
|
+
lines.push(v.documentation);
|
|
452
|
+
}
|
|
453
|
+
if (v.charts?.length) {
|
|
454
|
+
lines.push('');
|
|
455
|
+
for (const c of v.charts) {
|
|
456
|
+
lines.push(`#### ${c.title || 'Chart'}`);
|
|
457
|
+
if (c.chartType) lines.push(`*Type: ${c.chartType}*`);
|
|
458
|
+
lines.push('');
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (v.elements?.length) {
|
|
462
|
+
lines.push('');
|
|
463
|
+
lines.push('| Element | Type |');
|
|
464
|
+
lines.push('| --- | --- |');
|
|
465
|
+
for (const e of v.elements) {
|
|
466
|
+
lines.push(`| ${e.name} | ${e.type} |`);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
lines.push('');
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (elements.length) {
|
|
474
|
+
lines.push(`## Elements (${elements.length})`);
|
|
475
|
+
lines.push('');
|
|
476
|
+
lines.push('| Name | Type | Layer | Documentation |');
|
|
477
|
+
lines.push('| --- | --- | --- | --- |');
|
|
478
|
+
for (const e of elements) {
|
|
479
|
+
lines.push(`| **${e.name}** | ${e.type} | ${e.layer} | ${e.documentation.replace(/\n/g, ' ')} |`);
|
|
480
|
+
}
|
|
481
|
+
lines.push('');
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (relationships.length) {
|
|
485
|
+
lines.push(`## Relationships (${relationships.length})`);
|
|
486
|
+
lines.push('');
|
|
487
|
+
lines.push('| Type | Source | Target | Name |');
|
|
488
|
+
lines.push('| --- | --- | --- | --- |');
|
|
489
|
+
for (const r of relationships) {
|
|
490
|
+
lines.push(`| ${r.type} | ${r.source} | ${r.target} | ${r.name} |`);
|
|
491
|
+
}
|
|
492
|
+
lines.push('');
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return lines.join('\n');
|
|
496
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { C4Template } from './C4Template.js';
|
|
2
|
+
|
|
3
|
+
/** C4BoundaryTemplate — An enterprise, system, or container boundary grouping elements in C4 model. */
|
|
4
|
+
export class C4BoundaryTemplate extends C4Template {
|
|
5
|
+
static isContainer = true;
|
|
6
|
+
static defaults = {
|
|
7
|
+
width: 440,
|
|
8
|
+
height: 260,
|
|
9
|
+
fillColor: 'none',
|
|
10
|
+
strokeColor: '#707070',
|
|
11
|
+
strokeWidth: 2,
|
|
12
|
+
textColor: '#334155',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
_initState(name, options = {}) {
|
|
16
|
+
super._initState(name, options);
|
|
17
|
+
this.boundaryType = options.boundaryType ?? 'System';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get cornerRadius() { return 6; }
|
|
21
|
+
get isBoundary() { return true; }
|
|
22
|
+
get c4Tag() { return `[${this.boundaryType} Boundary]`; }
|
|
23
|
+
|
|
24
|
+
paint(extra = {}) {
|
|
25
|
+
return super.paint({ 'stroke-dasharray': '6,4', ...extra });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
serialize() {
|
|
29
|
+
return {
|
|
30
|
+
...super.serialize(),
|
|
31
|
+
...(this.boundaryType ? { boundaryType: this.boundaryType } : {}),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { C4Template } from './C4Template.js';
|
|
2
|
+
import { svgEl } from './svg.js';
|
|
3
|
+
|
|
4
|
+
/** C4ComponentTemplate — A controller, service, or modular component within a container. */
|
|
5
|
+
export class C4ComponentTemplate extends C4Template {
|
|
6
|
+
static defaults = {
|
|
7
|
+
width: 160,
|
|
8
|
+
height: 100,
|
|
9
|
+
fillColor: '#85bbf0',
|
|
10
|
+
strokeColor: '#5d82a8',
|
|
11
|
+
strokeWidth: 2,
|
|
12
|
+
textColor: '#000000',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
get cornerRadius() { return 0; }
|
|
16
|
+
|
|
17
|
+
get c4Tag() {
|
|
18
|
+
return this.technology ? `[Component: ${this.technology}]` : '[Component]';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
shapeElements(doc) {
|
|
22
|
+
const ix = this.right - 22, iy = this.top + 8;
|
|
23
|
+
const icon = [
|
|
24
|
+
svgEl('rect', this.paint({ x: ix, y: iy, width: 14, height: 16, 'stroke-width': 1 }), doc),
|
|
25
|
+
svgEl('rect', this.paint({ x: ix - 4, y: iy + 3, width: 8, height: 4, 'stroke-width': 1 }), doc),
|
|
26
|
+
svgEl('rect', this.paint({ x: ix - 4, y: iy + 9, width: 8, height: 4, 'stroke-width': 1 }), doc),
|
|
27
|
+
];
|
|
28
|
+
return [...super.shapeElements(doc), ...icon];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { C4Template } from './C4Template.js';
|
|
2
|
+
import { svgEl } from './svg.js';
|
|
3
|
+
|
|
4
|
+
/** C4ContainerTemplate — An application, service, web app, or data store in C4 model. */
|
|
5
|
+
export class C4ContainerTemplate extends C4Template {
|
|
6
|
+
static defaults = {
|
|
7
|
+
width: 180,
|
|
8
|
+
height: 120,
|
|
9
|
+
fillColor: '#438dd5',
|
|
10
|
+
strokeColor: '#2e6295',
|
|
11
|
+
strokeWidth: 2,
|
|
12
|
+
textColor: '#ffffff',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
_initState(name, options = {}) {
|
|
16
|
+
super._initState(name, options);
|
|
17
|
+
this.containerType = options.containerType ?? 'application';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get cornerRadius() { return 8; }
|
|
21
|
+
|
|
22
|
+
get c4Tag() {
|
|
23
|
+
if (this.technology) return `[Container: ${this.technology}]`;
|
|
24
|
+
return this.containerType === 'database' ? '[Container: Database]' : '[Container]';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
shapeElements(doc) {
|
|
28
|
+
const elements = super.shapeElements(doc);
|
|
29
|
+
if (this.containerType === 'database') {
|
|
30
|
+
const ix = this.right - 24, iy = this.top + 10;
|
|
31
|
+
const w = 14, h = 16;
|
|
32
|
+
const stroke = { stroke: '#ffffff', 'stroke-width': 1.5, fill: 'none', 'stroke-opacity': 0.9 };
|
|
33
|
+
// Database cylinder icon (top ellipse, middle body, bottom curve)
|
|
34
|
+
elements.push(
|
|
35
|
+
svgEl('ellipse', { cx: ix + w / 2, cy: iy + 3, rx: w / 2, ry: 2.5, ...stroke }, doc),
|
|
36
|
+
svgEl('path', { d: `M ${ix} ${iy + 3} V ${iy + h} C ${ix} ${iy + h + 2.5}, ${ix + w} ${iy + h + 2.5}, ${ix + w} ${iy + h} V ${iy + 3}`, ...stroke }, doc),
|
|
37
|
+
svgEl('path', { d: `M ${ix} ${iy + 8} C ${ix} ${iy + 10.5}, ${ix + w} ${iy + 10.5}, ${ix + w} ${iy + 8}`, ...stroke }, doc),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return elements;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
serialize() {
|
|
44
|
+
return {
|
|
45
|
+
...super.serialize(),
|
|
46
|
+
...(this.containerType && this.containerType !== 'application' ? { containerType: this.containerType } : {}),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { C4Template } from './C4Template.js';
|
|
2
|
+
import { svgEl } from './svg.js';
|
|
3
|
+
|
|
4
|
+
/** C4PersonTemplate — A human user, actor, persona, or stakeholder in C4 model. */
|
|
5
|
+
export class C4PersonTemplate extends C4Template {
|
|
6
|
+
static defaults = {
|
|
7
|
+
width: 160,
|
|
8
|
+
height: 120,
|
|
9
|
+
fillColor: '#08427b',
|
|
10
|
+
strokeColor: '#052e56',
|
|
11
|
+
strokeWidth: 2,
|
|
12
|
+
textColor: '#ffffff',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
get cornerRadius() { return 10; }
|
|
16
|
+
get c4Tag() { return '[Person]'; }
|
|
17
|
+
get hasTopIcon() { return true; }
|
|
18
|
+
|
|
19
|
+
shapeElements(doc) {
|
|
20
|
+
const elements = super.shapeElements(doc);
|
|
21
|
+
const cx = this.x;
|
|
22
|
+
const cy = this.top + 18;
|
|
23
|
+
const headR = 8;
|
|
24
|
+
const head = svgEl('circle', {
|
|
25
|
+
cx, cy: cy - 2, r: headR, fill: '#ffffff', 'fill-opacity': '0.9', stroke: 'none',
|
|
26
|
+
}, doc);
|
|
27
|
+
const shoulderY = cy + headR + 1;
|
|
28
|
+
const shoulders = svgEl('path', {
|
|
29
|
+
d: `M ${cx - 12} ${shoulderY + 10} C ${cx - 12} ${shoulderY + 2}, ${cx - 6} ${shoulderY}, ${cx} ${shoulderY} C ${cx + 6} ${shoulderY}, ${cx + 12} ${shoulderY + 2}, ${cx + 12} ${shoulderY + 10} Z`,
|
|
30
|
+
fill: '#ffffff', 'fill-opacity': '0.9', stroke: 'none',
|
|
31
|
+
}, doc);
|
|
32
|
+
elements.push(head, shoulders);
|
|
33
|
+
return elements;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { C4Template } from './C4Template.js';
|
|
2
|
+
|
|
3
|
+
/** C4SystemTemplate — Software System in focus or external software system. */
|
|
4
|
+
export class C4SystemTemplate extends C4Template {
|
|
5
|
+
static defaults = {
|
|
6
|
+
width: 180,
|
|
7
|
+
height: 120,
|
|
8
|
+
fillColor: '#1168bd',
|
|
9
|
+
strokeColor: '#0b4884',
|
|
10
|
+
strokeWidth: 2,
|
|
11
|
+
textColor: '#ffffff',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
_initState(name, options = {}) {
|
|
15
|
+
super._initState(name, options);
|
|
16
|
+
this.external = Boolean(options.external);
|
|
17
|
+
if (this.external && !options.fillColor) {
|
|
18
|
+
this.fillColor = '#999999';
|
|
19
|
+
this.strokeColor = '#666666';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get cornerRadius() { return 8; }
|
|
24
|
+
get c4Tag() { return this.external ? '[Software System: External]' : '[Software System]'; }
|
|
25
|
+
|
|
26
|
+
serialize() {
|
|
27
|
+
return {
|
|
28
|
+
...super.serialize(),
|
|
29
|
+
...(this.external ? { external: true } : {}),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|