@peter.prib/renderengine 0.15.1 → 0.15.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/renderengine-workspace.umd.js +6485 -3992
  2. package/dist/renderengine.umd.js +6251 -3877
  3. package/documentation/REFERENCE.md +384 -28
  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 +7 -109
  11. package/src/charting/DiagramTypeRegistry.js +20 -1
  12. package/src/charting/DiagramViewPropertiesDialog.js +54 -10
  13. package/src/charting/Edge.js +22 -0
  14. package/src/charting/Legend.js +1 -2
  15. package/src/charting/Statistics.js +14 -0
  16. package/src/charting/archimate/ArchimateCanvasEditor.js +21 -5
  17. package/src/charting/archimate/ViewGroupTemplate.js +5 -2
  18. package/src/charting/charts/BarChart.js +59 -8
  19. package/src/charting/charts/HexbinChart.js +15 -0
  20. package/src/charting/charts/LollipopChart.js +42 -7
  21. package/src/charting/decision/DecisionWorkbench.js +71 -4
  22. package/src/charting/diagramModel/DiagramJsonFileFormat.js +32 -0
  23. package/src/charting/diagramModel/DiagramModel.js +39 -0
  24. package/src/charting/diagramModel/DiagramView.js +102 -61
  25. package/src/charting/diagramModel/index.js +11 -10
  26. package/src/charting/diagrams/AddGroupToViewCommand.js +35 -0
  27. package/src/charting/diagrams/ArchimateModel.js +53 -13
  28. package/src/charting/diagrams/GraphDiagram3D.js +15 -0
  29. package/src/charting/diagrams/NestInstanceCommand.js +2 -1
  30. package/src/charting/graph/GraphSource.js +72 -0
  31. package/src/charting/modelTree/NavigatorModel.js +22 -8
  32. package/src/charting/report/TemplatedReport.js +27 -377
  33. package/src/charting/report/ViewsReport.js +118 -0
  34. package/src/charting/report/reportWriters.js +496 -0
  35. package/src/charting/templates/C4BoundaryTemplate.js +34 -0
  36. package/src/charting/templates/C4ComponentTemplate.js +30 -0
  37. package/src/charting/templates/C4ContainerTemplate.js +49 -0
  38. package/src/charting/templates/C4PersonTemplate.js +35 -0
  39. package/src/charting/templates/C4SystemTemplate.js +32 -0
  40. package/src/charting/templates/C4Template.js +95 -0
  41. package/src/charting/templates/index.js +6 -0
  42. package/src/charting/templates/templateRegistry.js +77 -0
  43. package/src/compute/ComputeBackend.js +132 -0
  44. package/src/compute/CpuBackend.js +209 -0
  45. package/src/compute/WebGpuBackend.js +630 -0
  46. package/src/compute/index.js +5 -0
  47. package/src/compute/selectBackend.js +85 -0
  48. package/src/index.js +3403 -3370
  49. package/src/kernelExpr.js +80 -5
  50. package/src/workspace/parts/ArchimateEditorPart.js +2 -2
  51. package/src/workspace/parts/DiagramEditorPart.js +146 -131
  52. package/src/workspace/parts/ViewsPart.js +106 -0
@@ -1,11 +1,24 @@
1
1
  /**
2
- * TemplatedReport — template-driven report generator (PDF, Word, Excel, Markdown, HTML).
2
+ * TemplatedReport — template-driven report generator (HTML, Word, Excel, CSV, Markdown).
3
3
  *
4
4
  * Implements Archi's Jasper-style reporting with customizable templates,
5
5
  * placeholders for model metadata, views, view diagrams (SVG/PNG), elements,
6
6
  * relationships, and properties.
7
7
  */
8
8
 
9
+ import {
10
+ escapeHtml,
11
+ escapeXml,
12
+ escapeCsv,
13
+ formatHtml,
14
+ formatWord,
15
+ formatExcel,
16
+ formatCsv,
17
+ formatMarkdown,
18
+ } from './reportWriters.js';
19
+
20
+ export { escapeHtml, escapeXml, escapeCsv };
21
+
9
22
  /** Built-in template templates */
10
23
  export const REPORT_TEMPLATES = Object.freeze({
11
24
  EXECUTIVE: 'executive',
@@ -13,27 +26,6 @@ export const REPORT_TEMPLATES = Object.freeze({
13
26
  TABULAR: 'tabular',
14
27
  });
15
28
 
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
29
  export class TemplatedReport {
38
30
  /**
39
31
  * @param {Object|ViewSetModel|ArchimateModel} model
@@ -50,14 +42,14 @@ export class TemplatedReport {
50
42
  }
51
43
 
52
44
  get title() {
53
- return this._title || this.model.name || this.model.title || 'Model Report';
45
+ return this._title || this.model?.name || this.model?.title || 'Model Report';
54
46
  }
55
47
 
56
48
  /**
57
49
  * Normalized model data extracted for reporting.
58
50
  */
59
51
  extractData() {
60
- const m = this.model;
52
+ const m = this.model || {};
61
53
  const title = this.title;
62
54
  const doc = m.documentation || '';
63
55
 
@@ -149,6 +141,9 @@ export class TemplatedReport {
149
141
  version: m.version || '1.0',
150
142
  properties,
151
143
  },
144
+ title,
145
+ documentation: doc,
146
+ version: m.version || '1.0',
152
147
  views,
153
148
  elements: normElements,
154
149
  relationships: normRelationships,
@@ -163,279 +158,24 @@ export class TemplatedReport {
163
158
  * @param {boolean} [options.printable=true]
164
159
  * @returns {string}
165
160
  */
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>`;
161
+ toHtml(options = {}) {
162
+ return formatHtml(this.extractData(), options);
261
163
  }
262
164
 
263
165
  /**
264
- * Render the report as Microsoft Word-compatible HTML document (.doc / .docx compatible).
265
- * Word opens this with native tables, headings, and formatting intact.
166
+ * Render the report as Microsoft Word-compatible document (.doc).
266
167
  * @returns {string}
267
168
  */
268
169
  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>`;
170
+ return formatWord(this.extractData());
342
171
  }
343
172
 
344
173
  /**
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.
174
+ * Render the report as Microsoft Excel XML Spreadsheet 2003 format (.xml).
347
175
  * @returns {string}
348
176
  */
349
177
  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>`;
178
+ return formatExcel(this.extractData());
439
179
  }
440
180
 
441
181
  /**
@@ -444,30 +184,7 @@ ${cells.map(c => ` <Cell${isHeader ? ' ss:StyleID="Header"' : ''}><Data ss:T
444
184
  * @returns {string}
445
185
  */
446
186
  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
- }
187
+ return formatCsv(this.extractData(), table);
471
188
  }
472
189
 
473
190
  /**
@@ -475,73 +192,6 @@ ${cells.map(c => ` <Cell${isHeader ? ' ss:StyleID="Header"' : ''}><Data ss:T
475
192
  * @returns {string}
476
193
  */
477
194
  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');
195
+ return formatMarkdown(this.extractData());
546
196
  }
547
197
  }
@@ -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
+ }