@peter.prib/renderengine 0.13.0 → 0.15.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 (124) hide show
  1. package/README.md +480 -302
  2. package/bin/renderengine.mjs +157 -0
  3. package/dist/renderengine-workspace.umd.js +49217 -37484
  4. package/dist/renderengine.umd.js +24899 -13093
  5. package/documentation/REFERENCE.md +68 -1
  6. package/package.json +13 -3
  7. package/src/CanvasRenderer.js +225 -200
  8. package/src/SVGRenderer.js +476 -454
  9. package/src/archimate.js +22 -2
  10. package/src/archimateArchiFormat.js +1735 -1701
  11. package/src/archimateArchive.js +36 -7
  12. package/src/archimateCSV.js +93 -37
  13. package/src/archimateDrawio.js +2 -0
  14. package/src/archimateExchangeFormat.js +3 -1
  15. package/src/chartCatalog.js +269 -3
  16. package/src/charting/Annotations.js +169 -0
  17. package/src/charting/ArchimateDiagramEditor.js +54 -10
  18. package/src/charting/Axis.js +217 -5
  19. package/src/charting/Chart.js +99 -9
  20. package/src/charting/Chart3D.js +7 -2
  21. package/src/charting/ChartBox.js +8 -1
  22. package/src/charting/ColorPalette.js +42 -4
  23. package/src/charting/DiagramEditor.js +174 -39
  24. package/src/charting/DiagramEditorCommands.js +28 -0
  25. package/src/charting/Edge.js +7 -0
  26. package/src/charting/Hierarchy.js +142 -0
  27. package/src/charting/InteractionManager.js +4 -0
  28. package/src/charting/ModelFileFormat.js +5 -1
  29. package/src/charting/SettingsManager.js +35 -0
  30. package/src/charting/ShapePaths.js +82 -10
  31. package/src/charting/archimate/ArchimateCsvFileFormat.js +51 -12
  32. package/src/charting/archimate/ArchimateElementTemplate.js +235 -231
  33. package/src/charting/ask/AskBar.js +388 -0
  34. package/src/charting/ask/AskView.js +400 -0
  35. package/src/charting/charts/AlluvialDiagram.js +311 -0
  36. package/src/charting/charts/AreaChart.js +69 -15
  37. package/src/charting/charts/BarChart.js +38 -3
  38. package/src/charting/charts/BarChart3D.js +6 -4
  39. package/src/charting/charts/BarChart3DOrbit.js +3 -1
  40. package/src/charting/charts/BarChartBase.js +26 -6
  41. package/src/charting/charts/BeeswarmChart.js +164 -0
  42. package/src/charting/charts/BoxPlotChart.js +25 -3
  43. package/src/charting/charts/BubbleChart.js +4 -1
  44. package/src/charting/charts/CalendarHeatmapChart.js +2 -1
  45. package/src/charting/charts/CandlestickChart.js +172 -4
  46. package/src/charting/charts/ChordDiagram.js +107 -4
  47. package/src/charting/charts/CirclePackChart.js +81 -0
  48. package/src/charting/charts/ComboChart.js +233 -0
  49. package/src/charting/charts/ContourChart.js +310 -0
  50. package/src/charting/charts/DensityChart.js +1 -1
  51. package/src/charting/charts/DonutChart3D.js +13 -0
  52. package/src/charting/charts/DumbbellChart.js +217 -0
  53. package/src/charting/charts/EcdfChart.js +150 -0
  54. package/src/charting/charts/FunnelChart.js +5 -2
  55. package/src/charting/charts/GeoMapChart.js +132 -11
  56. package/src/charting/charts/GroupedBarChart.js +44 -3
  57. package/src/charting/charts/GroupedDistributionChart.js +26 -1
  58. package/src/charting/charts/HeatmapChart.js +6 -3
  59. package/src/charting/charts/HexbinChart.js +137 -0
  60. package/src/charting/charts/HierarchyChart.js +77 -3
  61. package/src/charting/charts/HistogramChart.js +2 -2
  62. package/src/charting/charts/HorizonChart.js +105 -0
  63. package/src/charting/charts/IcicleChart.js +47 -0
  64. package/src/charting/charts/KagiChart.js +167 -0
  65. package/src/charting/charts/LineChart.js +74 -18
  66. package/src/charting/charts/LollipopChart.js +104 -0
  67. package/src/charting/charts/MarimekkoChart.js +75 -0
  68. package/src/charting/charts/OhlcChart.js +79 -0
  69. package/src/charting/charts/OutlineChart.js +10 -8
  70. package/src/charting/charts/ParallelCoordinatesChart.js +11 -3
  71. package/src/charting/charts/ParetoChart.js +92 -0
  72. package/src/charting/charts/PieChart.js +195 -17
  73. package/src/charting/charts/PieChart3D.js +137 -0
  74. package/src/charting/charts/PipeChart.js +5 -2
  75. package/src/charting/charts/PointAndFigureChart.js +198 -0
  76. package/src/charting/charts/PolarAreaChart.js +189 -0
  77. package/src/charting/charts/PopulationPyramidChart.js +4 -4
  78. package/src/charting/charts/RadarChart.js +12 -5
  79. package/src/charting/charts/RadialBarChart.js +6 -3
  80. package/src/charting/charts/RenkoChart.js +178 -0
  81. package/src/charting/charts/RidgelineChart.js +113 -0
  82. package/src/charting/charts/SankeyDiagram.js +129 -6
  83. package/src/charting/charts/ScatterChart.js +19 -3
  84. package/src/charting/charts/ScatterMatrixChart.js +119 -0
  85. package/src/charting/charts/SlopegraphChart.js +205 -0
  86. package/src/charting/charts/SparklineTableChart.js +166 -0
  87. package/src/charting/charts/StackedBarChart.js +125 -11
  88. package/src/charting/charts/StackedBarChart3DOrbit.js +14 -6
  89. package/src/charting/charts/StreamGraphChart.js +18 -3
  90. package/src/charting/charts/SunburstChart.js +28 -8
  91. package/src/charting/charts/SurfaceChart3D.js +157 -0
  92. package/src/charting/charts/TreemapChart.js +31 -11
  93. package/src/charting/charts/ViolinChart.js +20 -2
  94. package/src/charting/charts/WaterfallChart.js +2 -2
  95. package/src/charting/charts/WordCloudChart.js +2 -2
  96. package/src/charting/datamodel/ArchimateDataLink.js +70 -0
  97. package/src/charting/datamodel/DataLineage.js +150 -0
  98. package/src/charting/datamodel/DataModel.js +83 -1
  99. package/src/charting/datamodel/ModelMapping.js +87 -0
  100. package/src/charting/datamodel/SqlDdl.js +110 -85
  101. package/src/charting/datamodel/index.js +3 -0
  102. package/src/charting/decision/DecisionWorkbench.js +295 -0
  103. package/src/charting/diagrams/ArchimateDiagram.js +25 -9
  104. package/src/charting/diagrams/ArchimateModel.js +1382 -1238
  105. package/src/charting/diagrams/GraphDiagram.js +1424 -1350
  106. package/src/charting/diagrams/SketchView.js +121 -0
  107. package/src/charting/diagrams/sketchBoxKinds.js +30 -0
  108. package/src/charting/modelTree/AnalysisPanel.js +101 -23
  109. package/src/charting/modelTree/NavigatorModel.js +410 -0
  110. package/src/charting/modelTree/NavigatorPanel.js +453 -0
  111. package/src/charting/pivot/PivotChart.js +310 -13
  112. package/src/charting/pivot/PivotGridChart.js +322 -34
  113. package/src/charting/pivot/PivotView.js +626 -21
  114. package/src/charting/report/TemplatedReport.js +547 -0
  115. package/src/charting/templates/BoxTemplate.js +28 -27
  116. package/src/charting/templates/PackageTemplate.js +26 -25
  117. package/src/charting/templates/SwimlaneTemplate.js +19 -18
  118. package/src/charting/templates/Template.js +219 -212
  119. package/src/cli.js +146 -0
  120. package/src/index.js +3370 -2899
  121. package/src/workspace/Workspace.js +126 -7
  122. package/src/workspace/archimateServices.js +4 -0
  123. package/src/workspace/parts/ArchimateCanvasPart.js +1 -1
  124. package/src/workspace/styles.js +36 -1
@@ -0,0 +1,547 @@
1
+ /**
2
+ * TemplatedReport — template-driven report generator (PDF, Word, Excel, Markdown, HTML).
3
+ *
4
+ * Implements Archi's Jasper-style reporting with customizable templates,
5
+ * placeholders for model metadata, views, view diagrams (SVG/PNG), elements,
6
+ * relationships, and properties.
7
+ */
8
+
9
+ /** Built-in template templates */
10
+ export const REPORT_TEMPLATES = Object.freeze({
11
+ EXECUTIVE: 'executive',
12
+ DETAILED: 'detailed',
13
+ TABULAR: 'tabular',
14
+ });
15
+
16
+ function escapeHtml(str) {
17
+ return String(str ?? '')
18
+ .replace(/&/g, '&')
19
+ .replace(/</g, '&lt;')
20
+ .replace(/>/g, '&gt;')
21
+ .replace(/"/g, '&quot;')
22
+ .replace(/'/g, '&#39;');
23
+ }
24
+
25
+ function escapeXml(str) {
26
+ return escapeHtml(str);
27
+ }
28
+
29
+ function escapeCsv(val) {
30
+ const s = String(val ?? '');
31
+ if (/[",\n\r]/.test(s)) {
32
+ return `"${s.replace(/"/g, '""')}"`;
33
+ }
34
+ return s;
35
+ }
36
+
37
+ export class TemplatedReport {
38
+ /**
39
+ * @param {Object|ViewSetModel|ArchimateModel} model
40
+ * @param {Object} [options]
41
+ * @param {Object<string, string>} [options.pictures] - View SVG markup by view id
42
+ * @param {string} [options.title] - Override title
43
+ * @param {string} [options.template] - 'executive' | 'detailed' | 'tabular'
44
+ */
45
+ constructor(model, { pictures = {}, title = null, template = REPORT_TEMPLATES.DETAILED } = {}) {
46
+ this.model = model;
47
+ this.pictures = pictures;
48
+ this._title = title;
49
+ this.templateKind = template;
50
+ }
51
+
52
+ get title() {
53
+ return this._title || this.model.name || this.model.title || 'Model Report';
54
+ }
55
+
56
+ /**
57
+ * Normalized model data extracted for reporting.
58
+ */
59
+ extractData() {
60
+ const m = this.model;
61
+ const title = this.title;
62
+ const doc = m.documentation || '';
63
+
64
+ // Extract views
65
+ const viewsList = typeof m.views === 'function' ? m.views() : (m.views || []);
66
+ const views = viewsList.map(v => {
67
+ const vDoc = v.documentation || '';
68
+ const pic = this.pictures[v.id] || v.picture?.svg || (typeof v.toSvg === 'function' ? v.toSvg() : null) || '';
69
+ // Get view elements
70
+ let vElements = [];
71
+ if (Array.isArray(v.elements)) {
72
+ vElements = v.elements.map(e => ({
73
+ id: e.id,
74
+ name: e.name || e.label || '',
75
+ type: e.type || e.archiVisualType || 'Element',
76
+ }));
77
+ } else if (typeof v.elementInstancesFor === 'function' && Array.isArray(v.elementIds)) {
78
+ vElements = v.elementIds.map(id => {
79
+ const el = typeof m.getElement === 'function' ? m.getElement(id) : null;
80
+ return {
81
+ id,
82
+ name: el?.name || id,
83
+ type: el?.type || 'Element',
84
+ };
85
+ });
86
+ }
87
+
88
+ const vProps = Array.isArray(v.properties) ? v.properties : [];
89
+
90
+ return {
91
+ id: v.id,
92
+ name: v.name || v.title || 'View',
93
+ documentation: vDoc,
94
+ picture: pic,
95
+ elements: vElements,
96
+ properties: vProps,
97
+ };
98
+ });
99
+
100
+ // Extract elements
101
+ let elements = [];
102
+ if (typeof m.allElements === 'function') {
103
+ elements = m.allElements();
104
+ } else if (typeof m.elements === 'function') {
105
+ elements = m.elements();
106
+ } else if (Array.isArray(m.elements)) {
107
+ elements = m.elements;
108
+ }
109
+ const normElements = elements.map(e => ({
110
+ id: e.id,
111
+ name: e.name || '',
112
+ type: e.type || 'Element',
113
+ layer: e.layer || (typeof e.type === 'string' && e.type.split('-')[0]) || '',
114
+ documentation: e.documentation || '',
115
+ properties: Array.isArray(e.properties) ? e.properties : [],
116
+ }));
117
+
118
+ // Extract relationships
119
+ let relationships = [];
120
+ if (typeof m.relationships === 'function') {
121
+ relationships = m.relationships();
122
+ } else if (Array.isArray(m.relationships)) {
123
+ relationships = m.relationships;
124
+ }
125
+ const normRelationships = relationships.map(r => {
126
+ const src = typeof m.getElement === 'function' ? m.getElement(r.source ?? r.sourceId) : null;
127
+ const tgt = typeof m.getElement === 'function' ? m.getElement(r.target ?? r.targetId) : null;
128
+ return {
129
+ id: r.id,
130
+ name: r.name || '',
131
+ type: r.type || r.kind || 'Relationship',
132
+ source: src?.name || r.source || r.sourceId || '',
133
+ sourceId: r.source || r.sourceId || '',
134
+ target: tgt?.name || r.target || r.targetId || '',
135
+ targetId: r.target || r.targetId || '',
136
+ documentation: r.documentation || '',
137
+ properties: Array.isArray(r.properties) ? r.properties : [],
138
+ };
139
+ });
140
+
141
+ // Extract properties
142
+ const properties = Array.isArray(m.properties) ? m.properties : [];
143
+
144
+ return {
145
+ model: {
146
+ id: m.id || 'model',
147
+ name: title,
148
+ documentation: doc,
149
+ version: m.version || '1.0',
150
+ properties,
151
+ },
152
+ views,
153
+ elements: normElements,
154
+ relationships: normRelationships,
155
+ properties,
156
+ date: new Date().toISOString().split('T')[0],
157
+ };
158
+ }
159
+
160
+ /**
161
+ * Render the report as HTML with print-to-PDF styles and vector SVG view diagrams.
162
+ * @param {Object} [options]
163
+ * @param {boolean} [options.printable=true]
164
+ * @returns {string}
165
+ */
166
+ toHtml({ printable = true } = {}) {
167
+ const data = this.extractData();
168
+ const { model, views, elements, relationships, properties, date } = data;
169
+
170
+ const printCss = printable ? `
171
+ @media print {
172
+ body { background: #fff; margin: 0; padding: 0; font-size: 11pt; color: #000; }
173
+ header, footer { position: fixed; width: 100%; left: 0; font-size: 9pt; color: #777; }
174
+ header { top: 0; border-bottom: 1px solid #ccc; padding-bottom: 4px; }
175
+ footer { bottom: 0; border-top: 1px solid #ccc; padding-top: 4px; display: flex; justify-content: space-between; }
176
+ .page-break { page-break-after: always; }
177
+ .no-break { page-break-inside: avoid; }
178
+ .picture { max-width: 100% !important; page-break-inside: avoid; }
179
+ table { page-break-inside: auto; }
180
+ tr { page-break-inside: avoid; page-break-after: auto; }
181
+ }
182
+ @page {
183
+ size: A4;
184
+ margin: 20mm;
185
+ }` : '';
186
+
187
+ return `<!DOCTYPE html>
188
+ <html lang="en">
189
+ <head>
190
+ <meta charset="utf-8">
191
+ <meta name="viewport" content="width=device-width, initial-scale=1">
192
+ <title>${escapeHtml(model.name)}</title>
193
+ <style>
194
+ 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; }
195
+ h1 { font-size: 26px; border-bottom: 2px solid #2563eb; padding-bottom: 6px; margin-top: 24px; color: #1e3a8a; }
196
+ h2 { font-size: 20px; border-bottom: 1px solid #cbd5e1; padding-bottom: 4px; margin-top: 24px; color: #1e40af; }
197
+ h3 { font-size: 16px; margin-top: 18px; color: #334155; }
198
+ .meta { font-size: 13px; color: #64748b; margin-bottom: 20px; }
199
+ .doc { white-space: pre-wrap; background: #f8fafc; border-left: 4px solid #3b82f6; padding: 10px 14px; margin: 12px 0; font-size: 14px; }
200
+ table { width: 100%; border-collapse: collapse; margin: 16px 0; font-size: 13px; }
201
+ th, td { border: 1px solid #e2e8f0; padding: 6px 10px; text-align: left; vertical-align: top; }
202
+ th { background: #f1f5f9; font-weight: 600; color: #334155; }
203
+ tr:nth-child(even) { background: #f8fafc; }
204
+ .picture { margin: 16px 0; border: 1px solid #e2e8f0; border-radius: 4px; padding: 8px; background: #fff; overflow-x: auto; text-align: center; }
205
+ .picture svg { max-width: 100%; height: auto; }
206
+ .badge { display: inline-block; padding: 2px 6px; font-size: 11px; font-weight: 500; border-radius: 3px; background: #e0f2fe; color: #0369a1; }
207
+ ${printCss}
208
+ </style>
209
+ </head>
210
+ <body>
211
+ <h1>${escapeHtml(model.name)}</h1>
212
+ <div class="meta">Generated: ${escapeHtml(date)} | Version: ${escapeHtml(model.version)}</div>
213
+ ${model.documentation ? `<div class="doc">${escapeHtml(model.documentation)}</div>` : ''}
214
+
215
+ ${properties.length ? `
216
+ <h2>Model Properties</h2>
217
+ <table>
218
+ <thead><tr><th>Property</th><th>Value</th></tr></thead>
219
+ <tbody>
220
+ ${properties.map(p => `<tr><td>${escapeHtml(p.name)}</td><td>${escapeHtml(p.value)}</td></tr>`).join('')}
221
+ </tbody>
222
+ </table>` : ''}
223
+
224
+ <h2>Views (${views.length})</h2>
225
+ ${views.map(v => `
226
+ <div class="no-break">
227
+ <h3>${escapeHtml(v.name)}</h3>
228
+ ${v.documentation ? `<div class="doc">${escapeHtml(v.documentation)}</div>` : ''}
229
+ ${v.picture ? `<div class="picture">${v.picture}</div>` : ''}
230
+ ${v.elements.length ? `
231
+ <table>
232
+ <thead><tr><th>Element</th><th>Type</th></tr></thead>
233
+ <tbody>
234
+ ${v.elements.map(e => `<tr><td>${escapeHtml(e.name)}</td><td><span class="badge">${escapeHtml(e.type)}</span></td></tr>`).join('')}
235
+ </tbody>
236
+ </table>` : ''}
237
+ </div>
238
+ <div class="page-break"></div>
239
+ `).join('')}
240
+
241
+ ${elements.length ? `
242
+ <h2>Elements (${elements.length})</h2>
243
+ <table>
244
+ <thead><tr><th>Name</th><th>Type</th><th>Layer</th><th>Documentation</th></tr></thead>
245
+ <tbody>
246
+ ${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('')}
247
+ </tbody>
248
+ </table>` : ''}
249
+
250
+ ${relationships.length ? `
251
+ <h2>Relationships (${relationships.length})</h2>
252
+ <table>
253
+ <thead><tr><th>Type</th><th>Source</th><th>Target</th><th>Name</th><th>Documentation</th></tr></thead>
254
+ <tbody>
255
+ ${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('')}
256
+ </tbody>
257
+ </table>` : ''}
258
+
259
+ </body>
260
+ </html>`;
261
+ }
262
+
263
+ /**
264
+ * Render the report as Microsoft Word-compatible HTML document (.doc / .docx compatible).
265
+ * Word opens this with native tables, headings, and formatting intact.
266
+ * @returns {string}
267
+ */
268
+ toWord() {
269
+ const data = this.extractData();
270
+ const { model, views, elements, relationships, properties, date } = data;
271
+
272
+ return `<html xmlns:o="urn:schemas-microsoft-com:office:office"
273
+ xmlns:w="urn:schemas-microsoft-com:office:word"
274
+ xmlns="http://www.w3.org/TR/REC-html40">
275
+ <head>
276
+ <meta charset="utf-8">
277
+ <title>${escapeXml(model.name)}</title>
278
+ <!--[if gte mso 9]>
279
+ <xml>
280
+ <w:WordDocument>
281
+ <w:View>Print</w:View>
282
+ <w:Zoom>100</w:Zoom>
283
+ <w:DoNotOptimizeForBrowser/>
284
+ </w:WordDocument>
285
+ </xml>
286
+ <![endif]-->
287
+ <style>
288
+ body { font-family: Calibri, Arial, sans-serif; font-size: 11pt; line-height: 1.4; color: #333333; }
289
+ h1 { font-size: 22pt; color: #1f4e79; border-bottom: 2pt solid #1f4e79; margin-bottom: 12pt; }
290
+ h2 { font-size: 16pt; color: #2e75b6; margin-top: 18pt; margin-bottom: 8pt; }
291
+ h3 { font-size: 13pt; color: #595959; margin-top: 12pt; }
292
+ p.doc { font-style: italic; background-color: #f2f2f2; padding: 6pt; border-left: 3pt solid #2e75b6; }
293
+ table { width: 100%; border-collapse: collapse; margin-top: 8pt; margin-bottom: 12pt; }
294
+ th, td { border: 0.5pt solid #d9d9d9; padding: 4pt 6pt; font-size: 10pt; }
295
+ th { background-color: #f2f2f2; font-weight: bold; color: #1f4e79; }
296
+ .page-break { page-break-before: always; }
297
+ </style>
298
+ </head>
299
+ <body>
300
+ <h1>${escapeXml(model.name)}</h1>
301
+ <p><strong>Generated:</strong> ${escapeXml(date)} | <strong>Version:</strong> ${escapeXml(model.version)}</p>
302
+ ${model.documentation ? `<p class="doc">${escapeXml(model.documentation)}</p>` : ''}
303
+
304
+ ${properties.length ? `
305
+ <h2>Properties</h2>
306
+ <table>
307
+ <tr><th>Property</th><th>Value</th></tr>
308
+ ${properties.map(p => `<tr><td>${escapeXml(p.name)}</td><td>${escapeXml(p.value)}</td></tr>`).join('')}
309
+ </table>` : ''}
310
+
311
+ <h2>Views</h2>
312
+ ${views.map(v => `
313
+ <div class="page-break"></div>
314
+ <h3>${escapeXml(v.name)}</h3>
315
+ ${v.documentation ? `<p class="doc">${escapeXml(v.documentation)}</p>` : ''}
316
+ ${v.picture ? `<div>${v.picture}</div>` : ''}
317
+ ${v.elements.length ? `
318
+ <table>
319
+ <tr><th>Element</th><th>Type</th></tr>
320
+ ${v.elements.map(e => `<tr><td>${escapeXml(e.name)}</td><td>${escapeXml(e.type)}</td></tr>`).join('')}
321
+ </table>` : ''}
322
+ `).join('')}
323
+
324
+ ${elements.length ? `
325
+ <div class="page-break"></div>
326
+ <h2>Elements (${elements.length})</h2>
327
+ <table>
328
+ <tr><th>Name</th><th>Type</th><th>Layer</th><th>Documentation</th></tr>
329
+ ${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('')}
330
+ </table>` : ''}
331
+
332
+ ${relationships.length ? `
333
+ <div class="page-break"></div>
334
+ <h2>Relationships (${relationships.length})</h2>
335
+ <table>
336
+ <tr><th>Type</th><th>Source</th><th>Target</th><th>Name</th></tr>
337
+ ${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('')}
338
+ </table>` : ''}
339
+
340
+ </body>
341
+ </html>`;
342
+ }
343
+
344
+ /**
345
+ * Render the report as Microsoft Excel XML Spreadsheet 2003 format (.xml / .xls).
346
+ * Opens in Excel with native worksheets: Model, Views, Elements, Relationships, Properties.
347
+ * @returns {string}
348
+ */
349
+ toExcel() {
350
+ const data = this.extractData();
351
+ const { model, views, elements, relationships, properties, date } = data;
352
+
353
+ const makeRow = (cells, isHeader = false) => `
354
+ <Row>
355
+ ${cells.map(c => ` <Cell${isHeader ? ' ss:StyleID="Header"' : ''}><Data ss:Type="String">${escapeXml(c)}</Data></Cell>`).join('\n')}
356
+ </Row>`;
357
+
358
+ return `<?xml version="1.0"?>
359
+ <?mso-application progid="Excel.Sheet"?>
360
+ <Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"
361
+ xmlns:o="urn:schemas-microsoft-com:office:office"
362
+ xmlns:x="urn:schemas-microsoft-com:office:excel"
363
+ xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet">
364
+ <Styles>
365
+ <Style ss:ID="Default" ss:Name="Normal">
366
+ <Alignment ss:Vertical="Center"/>
367
+ <Font ss:FontName="Calibri" ss:Size="11" ss:Color="#000000"/>
368
+ </Style>
369
+ <Style ss:ID="Header">
370
+ <Font ss:FontName="Calibri" ss:Size="11" ss:Color="#FFFFFF" ss:Bold="1"/>
371
+ <Interior ss:Color="#2563EB" ss:Pattern="Solid"/>
372
+ </Style>
373
+ <Style ss:ID="Title">
374
+ <Font ss:FontName="Calibri" ss:Size="16" ss:Color="#1E3A8A" ss:Bold="1"/>
375
+ </Style>
376
+ </Styles>
377
+
378
+ <Worksheet ss:Name="Model Summary">
379
+ <Table>
380
+ <Column ss:Width="160"/>
381
+ <Column ss:Width="400"/>
382
+ <Row>
383
+ <Cell ss:StyleID="Title"><Data ss:Type="String">${escapeXml(model.name)}</Data></Cell>
384
+ </Row>
385
+ ${makeRow(['Item', 'Details'], true)}
386
+ ${makeRow(['Title', model.name])}
387
+ ${makeRow(['Version', model.version])}
388
+ ${makeRow(['Generated', date])}
389
+ ${makeRow(['Documentation', model.documentation])}
390
+ ${makeRow(['Total Views', String(views.length)])}
391
+ ${makeRow(['Total Elements', String(elements.length)])}
392
+ ${makeRow(['Total Relationships', String(relationships.length)])}
393
+ </Table>
394
+ </Worksheet>
395
+
396
+ <Worksheet ss:Name="Views">
397
+ <Table>
398
+ <Column ss:Width="180"/>
399
+ <Column ss:Width="80"/>
400
+ <Column ss:Width="300"/>
401
+ ${makeRow(['View Name', 'Elements Count', 'Documentation'], true)}
402
+ ${views.map(v => makeRow([v.name, String(v.elements.length), v.documentation])).join('')}
403
+ </Table>
404
+ </Worksheet>
405
+
406
+ <Worksheet ss:Name="Elements">
407
+ <Table>
408
+ <Column ss:Width="140"/>
409
+ <Column ss:Width="160"/>
410
+ <Column ss:Width="100"/>
411
+ <Column ss:Width="280"/>
412
+ ${makeRow(['Name', 'Type', 'Layer', 'Documentation'], true)}
413
+ ${elements.map(e => makeRow([e.name, e.type, e.layer, e.documentation])).join('')}
414
+ </Table>
415
+ </Worksheet>
416
+
417
+ <Worksheet ss:Name="Relationships">
418
+ <Table>
419
+ <Column ss:Width="140"/>
420
+ <Column ss:Width="160"/>
421
+ <Column ss:Width="160"/>
422
+ <Column ss:Width="100"/>
423
+ <Column ss:Width="200"/>
424
+ ${makeRow(['Type', 'Source', 'Target', 'Name', 'Documentation'], true)}
425
+ ${relationships.map(r => makeRow([r.type, r.source, r.target, r.name, r.documentation])).join('')}
426
+ </Table>
427
+ </Worksheet>
428
+
429
+ ${properties.length ? `
430
+ <Worksheet ss:Name="Properties">
431
+ <Table>
432
+ <Column ss:Width="160"/>
433
+ <Column ss:Width="300"/>
434
+ ${makeRow(['Name', 'Value'], true)}
435
+ ${properties.map(p => makeRow([p.name, p.value])).join('')}
436
+ </Table>
437
+ </Worksheet>` : ''}
438
+ </Workbook>`;
439
+ }
440
+
441
+ /**
442
+ * Export a model table as standard CSV.
443
+ * @param {'elements'|'relationships'|'views'|'properties'} [table='elements']
444
+ * @returns {string}
445
+ */
446
+ toCsv(table = 'elements') {
447
+ const data = this.extractData();
448
+ switch (table) {
449
+ case 'views': {
450
+ const header = ['ID', 'Name', 'Elements', 'Documentation'];
451
+ const rows = data.views.map(v => [v.id, v.name, v.elements.length, v.documentation]);
452
+ return [header, ...rows].map(r => r.map(escapeCsv).join(',')).join('\r\n');
453
+ }
454
+ case 'relationships': {
455
+ const header = ['ID', 'Type', 'Source', 'Target', 'Name', 'Documentation'];
456
+ const rows = data.relationships.map(r => [r.id, r.type, r.source, r.target, r.name, r.documentation]);
457
+ return [header, ...rows].map(r => r.map(escapeCsv).join(',')).join('\r\n');
458
+ }
459
+ case 'properties': {
460
+ const header = ['Name', 'Value'];
461
+ const rows = data.properties.map(p => [p.name, p.value]);
462
+ return [header, ...rows].map(r => r.map(escapeCsv).join(',')).join('\r\n');
463
+ }
464
+ case 'elements':
465
+ default: {
466
+ const header = ['ID', 'Name', 'Type', 'Layer', 'Documentation'];
467
+ const rows = data.elements.map(e => [e.id, e.name, e.type, e.layer, e.documentation]);
468
+ return [header, ...rows].map(r => r.map(escapeCsv).join(',')).join('\r\n');
469
+ }
470
+ }
471
+ }
472
+
473
+ /**
474
+ * Render the report as GitHub-flavored Markdown.
475
+ * @returns {string}
476
+ */
477
+ toMarkdown() {
478
+ const data = this.extractData();
479
+ const { model, views, elements, relationships, properties, date } = data;
480
+ const lines = [];
481
+
482
+ lines.push(`# ${model.name}`);
483
+ lines.push(`*Generated: ${date} | Version: ${model.version}*`);
484
+ lines.push('');
485
+
486
+ if (model.documentation) {
487
+ lines.push(model.documentation);
488
+ lines.push('');
489
+ }
490
+
491
+ if (properties.length) {
492
+ lines.push('## Properties');
493
+ lines.push('');
494
+ lines.push('| Property | Value |');
495
+ lines.push('| --- | --- |');
496
+ for (const p of properties) {
497
+ lines.push(`| ${p.name} | ${p.value} |`);
498
+ }
499
+ lines.push('');
500
+ }
501
+
502
+ if (views.length) {
503
+ lines.push(`## Views (${views.length})`);
504
+ lines.push('');
505
+ for (const v of views) {
506
+ lines.push(`### ${v.name}`);
507
+ if (v.documentation) {
508
+ lines.push('');
509
+ lines.push(v.documentation);
510
+ }
511
+ if (v.elements.length) {
512
+ lines.push('');
513
+ lines.push('| Element | Type |');
514
+ lines.push('| --- | --- |');
515
+ for (const e of v.elements) {
516
+ lines.push(`| ${e.name} | ${e.type} |`);
517
+ }
518
+ }
519
+ lines.push('');
520
+ }
521
+ }
522
+
523
+ if (elements.length) {
524
+ lines.push(`## Elements (${elements.length})`);
525
+ lines.push('');
526
+ lines.push('| Name | Type | Layer | Documentation |');
527
+ lines.push('| --- | --- | --- | --- |');
528
+ for (const e of elements) {
529
+ lines.push(`| **${e.name}** | ${e.type} | ${e.layer} | ${e.documentation.replace(/\n/g, ' ')} |`);
530
+ }
531
+ lines.push('');
532
+ }
533
+
534
+ if (relationships.length) {
535
+ lines.push(`## Relationships (${relationships.length})`);
536
+ lines.push('');
537
+ lines.push('| Type | Source | Target | Name |');
538
+ lines.push('| --- | --- | --- | --- |');
539
+ for (const r of relationships) {
540
+ lines.push(`| ${r.type} | ${r.source} | ${r.target} | ${r.name} |`);
541
+ }
542
+ lines.push('');
543
+ }
544
+
545
+ return lines.join('\n');
546
+ }
547
+ }
@@ -1,27 +1,28 @@
1
- import { Template } from './Template.js';
2
- import { svgEl, pointsAttr } from './svg.js';
3
-
4
- /** BoxTemplate — 3D box (UML deployment node, server, device). */
5
- export class BoxTemplate extends Template {
6
- static defaults = { width: 140, height: 90 };
7
-
8
- get depth() { return Math.min(14, this.width * 0.15, this.height * 0.15); }
9
-
10
- get labelAnchor() { return { x: this.x - this.depth / 2, y: this.y + this.depth / 2 - 8 }; }
11
-
12
- outline() {
13
- const { left: L, right: R, top: T, bottom: B, depth: d } = this;
14
- return [[L, T + d], [L + d, T], [R, T], [R, B - d], [R - d, B], [L, B]];
15
- }
16
-
17
- shapeElements(doc) {
18
- const { left: L, right: R, top: T, bottom: B } = this;
19
- const d = this.depth;
20
- const face = (pts, shade) => svgEl('polygon', this.paint({ points: pointsAttr(pts), 'fill-opacity': shade }), doc);
21
- return [
22
- face([[L, T + d], [L + d, T], [R, T], [R - d, T + d]], 0.7),
23
- face([[R - d, T + d], [R, T], [R, B - d], [R - d, B]], 0.5),
24
- svgEl('rect', this.paint({ x: L, y: T + d, width: this.width - d, height: this.height - d }), doc),
25
- ];
26
- }
27
- }
1
+ import { Template } from './Template.js';
2
+ import { svgEl, pointsAttr } from './svg.js';
3
+
4
+ /** BoxTemplate — 3D box (UML deployment node, server, device). */
5
+ export class BoxTemplate extends Template {
6
+ static isContainer = true;
7
+ static defaults = { width: 140, height: 90 };
8
+
9
+ get depth() { return Math.min(14, this.width * 0.15, this.height * 0.15); }
10
+
11
+ get labelAnchor() { return { x: this.x - this.depth / 2, y: this.y + this.depth / 2 - 8 }; }
12
+
13
+ outline() {
14
+ const { left: L, right: R, top: T, bottom: B, depth: d } = this;
15
+ return [[L, T + d], [L + d, T], [R, T], [R, B - d], [R - d, B], [L, B]];
16
+ }
17
+
18
+ shapeElements(doc) {
19
+ const { left: L, right: R, top: T, bottom: B } = this;
20
+ const d = this.depth;
21
+ const face = (pts, shade) => svgEl('polygon', this.paint({ points: pointsAttr(pts), 'fill-opacity': shade }), doc);
22
+ return [
23
+ face([[L, T + d], [L + d, T], [R, T], [R - d, T + d]], 0.7),
24
+ face([[R - d, T + d], [R, T], [R, B - d], [R - d, B]], 0.5),
25
+ svgEl('rect', this.paint({ x: L, y: T + d, width: this.width - d, height: this.height - d }), doc),
26
+ ];
27
+ }
28
+ }
@@ -1,25 +1,26 @@
1
- import { Template } from './Template.js';
2
- import { svgEl } from './svg.js';
3
-
4
- /** PackageTemplate — UML package / folder: body with a name tab on the top-left. */
5
- export class PackageTemplate extends Template {
6
- static defaults = { width: 160, height: 110 };
7
-
8
- get tabHeight() { return Math.min(16, this.height / 5); }
9
-
10
- get labelAnchor() { return { x: this.x, y: this.y + this.tabHeight / 2 - 8 }; }
11
-
12
- outline() {
13
- const { left: L, right: R, top: T, bottom: B, tabHeight: t } = this;
14
- const tabRight = L + this.width * 0.4;
15
- return [[L, T], [tabRight, T], [tabRight, T + t], [R, T + t], [R, B], [L, B]];
16
- }
17
-
18
- shapeElements(doc) {
19
- const t = this.tabHeight;
20
- return [
21
- svgEl('rect', this.paint({ x: this.left, y: this.top, width: this.width * 0.4, height: t }), doc),
22
- svgEl('rect', this.paint({ x: this.left, y: this.top + t, width: this.width, height: this.height - t }), doc),
23
- ];
24
- }
25
- }
1
+ import { Template } from './Template.js';
2
+ import { svgEl } from './svg.js';
3
+
4
+ /** PackageTemplate — UML package / folder: body with a name tab on the top-left. */
5
+ export class PackageTemplate extends Template {
6
+ static isContainer = true;
7
+ static defaults = { width: 160, height: 110 };
8
+
9
+ get tabHeight() { return Math.min(16, this.height / 5); }
10
+
11
+ get labelAnchor() { return { x: this.x, y: this.y + this.tabHeight / 2 - 8 }; }
12
+
13
+ outline() {
14
+ const { left: L, right: R, top: T, bottom: B, tabHeight: t } = this;
15
+ const tabRight = L + this.width * 0.4;
16
+ return [[L, T], [tabRight, T], [tabRight, T + t], [R, T + t], [R, B], [L, B]];
17
+ }
18
+
19
+ shapeElements(doc) {
20
+ const t = this.tabHeight;
21
+ return [
22
+ svgEl('rect', this.paint({ x: this.left, y: this.top, width: this.width * 0.4, height: t }), doc),
23
+ svgEl('rect', this.paint({ x: this.left, y: this.top + t, width: this.width, height: this.height - t }), doc),
24
+ ];
25
+ }
26
+ }