@peter.prib/renderengine 0.15.2 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/renderengine-workspace.umd.js +6030 -3766
  2. package/dist/renderengine.umd.js +5930 -3772
  3. package/documentation/REFERENCE.md +37 -3
  4. package/package.json +1 -1
  5. package/src/archimateArchiFormat.js +114 -10
  6. package/src/archimateArchive.js +27 -4
  7. package/src/archimateExchangeFormat.js +12 -4
  8. package/src/archimateSpecializations.js +11 -1
  9. package/src/chartCatalog.js +6 -4
  10. package/src/charting/ArchimateDiagramEditor.js +5 -108
  11. package/src/charting/DiagramTypeRegistry.js +20 -1
  12. package/src/charting/DiagramViewPropertiesDialog.js +54 -10
  13. package/src/charting/Legend.js +1 -2
  14. package/src/charting/Statistics.js +14 -0
  15. package/src/charting/charts/BarChart.js +59 -8
  16. package/src/charting/charts/HexbinChart.js +15 -0
  17. package/src/charting/charts/LollipopChart.js +42 -7
  18. package/src/charting/decision/DecisionWorkbench.js +137 -4
  19. package/src/charting/diagramModel/DiagramView.js +7 -2
  20. package/src/charting/diagrams/GraphDiagram3D.js +15 -0
  21. package/src/charting/graph/GraphSource.js +72 -0
  22. package/src/charting/modelTree/NavigatorModel.js +22 -8
  23. package/src/charting/report/TemplatedReport.js +27 -377
  24. package/src/charting/report/ViewsReport.js +118 -0
  25. package/src/charting/report/reportWriters.js +496 -0
  26. package/src/charting/templates/C4BoundaryTemplate.js +34 -0
  27. package/src/charting/templates/C4ComponentTemplate.js +30 -0
  28. package/src/charting/templates/C4ContainerTemplate.js +49 -0
  29. package/src/charting/templates/C4PersonTemplate.js +35 -0
  30. package/src/charting/templates/C4SystemTemplate.js +32 -0
  31. package/src/charting/templates/C4Template.js +95 -0
  32. package/src/charting/templates/index.js +6 -0
  33. package/src/charting/templates/templateRegistry.js +7 -0
  34. package/src/compute/ComputeBackend.js +132 -0
  35. package/src/compute/CpuBackend.js +209 -0
  36. package/src/compute/WebGpuBackend.js +630 -0
  37. package/src/compute/index.js +5 -0
  38. package/src/compute/selectBackend.js +85 -0
  39. package/src/index.js +3403 -3371
  40. package/src/kernelExpr.js +80 -5
  41. package/src/workspace/parts/DiagramEditorPart.js +3 -1
  42. package/src/workspace/parts/ViewsPart.js +106 -0
@@ -0,0 +1,118 @@
1
+ /**
2
+ * ViewsReport — a templated report for any set of chart views (ViewsPart, DecisionWorkbench).
3
+ *
4
+ * Normalizes views containing charts (SVGs, titles, data) and notes/documentation into
5
+ * the report structure used by formatHtml, formatWord, formatExcel, formatCsv, and formatMarkdown.
6
+ */
7
+
8
+ import { TemplatedReport, REPORT_TEMPLATES } from './TemplatedReport.js';
9
+
10
+ export class ViewsReport extends TemplatedReport {
11
+ /**
12
+ * @param {Array<Object>|Object} views - array of view objects or document holding a views list
13
+ * @param {Object} [options]
14
+ * @param {string} [options.title] - Report title (default 'Views Report')
15
+ * @param {string} [options.documentation] - Overall documentation/summary
16
+ * @param {string} [options.version='1.0'] - Version
17
+ * @param {Array<Object>} [options.properties=[]] - Overall properties
18
+ * @param {Object<string, string>} [options.pictures={}] - SVG markup mapped by viewId, viewId:index, or chart title
19
+ * @param {string} [options.template] - executive | detailed | tabular
20
+ */
21
+ constructor(views = [], options = {}) {
22
+ let viewList = [];
23
+ let docTitle = null;
24
+ let docDocumentation = null;
25
+ let docProperties = [];
26
+
27
+ if (Array.isArray(views)) {
28
+ viewList = views;
29
+ } else if (views && typeof views === 'object') {
30
+ viewList = Array.isArray(views.views) ? views.views : (typeof views.views === 'function' ? views.views() : []);
31
+ docTitle = views.title || views.name || null;
32
+ docDocumentation = views.documentation || views.notes || null;
33
+ docProperties = Array.isArray(views.properties) ? views.properties : [];
34
+ }
35
+
36
+ super(null, {
37
+ title: options.title || docTitle || 'Views Report',
38
+ pictures: options.pictures || {},
39
+ template: options.template || REPORT_TEMPLATES.DETAILED,
40
+ });
41
+
42
+ this.rawViews = viewList;
43
+ this.options = options;
44
+ this.documentation = options.documentation ?? (typeof docDocumentation === 'string' ? docDocumentation : '') ?? '';
45
+ this.version = options.version ?? '1.0';
46
+ this.properties = options.properties ?? docProperties ?? [];
47
+ }
48
+
49
+ get title() {
50
+ return this._title || 'Views Report';
51
+ }
52
+
53
+ /**
54
+ * Extract normalized report data from views.
55
+ */
56
+ extractData() {
57
+ const title = this.title;
58
+ const views = this.rawViews.map((v, vIndex) => {
59
+ const vId = v.id || `view-${vIndex + 1}`;
60
+ const vName = v.title || v.name || vId;
61
+ const notes = Array.isArray(v.notes) ? v.notes : (v.notes ? [String(v.notes)] : []);
62
+ const doc = v.documentation || (notes.length ? notes.join('\n\n') : '');
63
+
64
+ const charts = (v.charts || []).map((c, cIndex) => {
65
+ const picKey = `${vId}:${cIndex}`;
66
+ const pic = this.pictures[picKey]
67
+ || this.pictures[c.title]
68
+ || this.pictures[c.id]
69
+ || (cIndex === 0 ? this.pictures[vId] : null)
70
+ || c.picture
71
+ || c.svg
72
+ || '';
73
+ return {
74
+ id: c.id || `${vId}-chart-${cIndex + 1}`,
75
+ title: c.title || `Chart ${cIndex + 1}`,
76
+ chartType: c.chartType || '',
77
+ picture: pic,
78
+ data: c.data,
79
+ };
80
+ });
81
+
82
+ // Single picture if charts pictures exist or view picture
83
+ const chartPictures = charts.map(c => c.picture).filter(Boolean);
84
+ const primaryPic = chartPictures.length
85
+ ? chartPictures.join('\n')
86
+ : (this.pictures[vId] || v.picture || (typeof v.toSvg === 'function' ? v.toSvg() : ''));
87
+
88
+ return {
89
+ id: vId,
90
+ name: vName,
91
+ documentation: doc,
92
+ notes,
93
+ picture: primaryPic,
94
+ charts,
95
+ elements: Array.isArray(v.elements) ? v.elements : [],
96
+ properties: Array.isArray(v.properties) ? v.properties : [],
97
+ };
98
+ });
99
+
100
+ return {
101
+ model: {
102
+ id: 'views-report',
103
+ name: title,
104
+ documentation: this.documentation,
105
+ version: this.version,
106
+ properties: this.properties,
107
+ },
108
+ title,
109
+ documentation: this.documentation,
110
+ version: this.version,
111
+ views,
112
+ elements: [],
113
+ relationships: [],
114
+ properties: this.properties,
115
+ date: new Date().toISOString().split('T')[0],
116
+ };
117
+ }
118
+ }
@@ -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, '&amp;')
12
+ .replace(/</g, '&lt;')
13
+ .replace(/>/g, '&gt;')
14
+ .replace(/"/g, '&quot;')
15
+ .replace(/'/g, '&#39;');
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
+ }