@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.
- package/dist/renderengine-workspace.umd.js +6030 -3766
- package/dist/renderengine.umd.js +5930 -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 +137 -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,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, '&')
|
|
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
|
+
}
|