@cyberismo/data-handler 0.0.19 → 0.0.21
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/command-handler.js +5 -1
- package/dist/command-handler.js.map +1 -1
- package/dist/commands/create.js +16 -5
- package/dist/commands/create.js.map +1 -1
- package/dist/macros/include/index.js +19 -2
- package/dist/macros/include/index.js.map +1 -1
- package/dist/macros/include/types.d.ts +21 -12
- package/dist/macros/index.js +1 -0
- package/dist/macros/index.js.map +1 -1
- package/dist/resources/create-defaults.js +1 -1
- package/dist/resources/create-defaults.js.map +1 -1
- package/dist/resources/field-type-resource.js +10 -3
- package/dist/resources/field-type-resource.js.map +1 -1
- package/dist/resources/folder-resource.js +20 -8
- package/dist/resources/folder-resource.js.map +1 -1
- package/dist/resources/workflow-resource.d.ts +6 -0
- package/dist/resources/workflow-resource.js +28 -12
- package/dist/resources/workflow-resource.js.map +1 -1
- package/dist/svg/percentage.js +7 -3
- package/dist/svg/percentage.js.map +1 -1
- package/dist/svg/scoreCard.js +10 -4
- package/dist/svg/scoreCard.js.map +1 -1
- package/dist/types/queries.d.ts +1 -1
- package/dist/utils/csv.d.ts +8 -0
- package/dist/utils/csv.js +11 -0
- package/dist/utils/csv.js.map +1 -1
- package/dist/utils/json.d.ts +17 -10
- package/dist/utils/json.js +21 -12
- package/dist/utils/json.js.map +1 -1
- package/dist/utils/report.js +4 -0
- package/dist/utils/report.js.map +1 -1
- package/dist/utils/resource-utils.js +1 -0
- package/dist/utils/resource-utils.js.map +1 -1
- package/package.json +10 -10
- package/src/command-handler.ts +4 -1
- package/src/commands/create.ts +15 -5
- package/src/macros/include/index.ts +22 -2
- package/src/macros/include/types.ts +21 -12
- package/src/macros/index.ts +1 -0
- package/src/resources/create-defaults.ts +1 -1
- package/src/resources/field-type-resource.ts +13 -6
- package/src/resources/folder-resource.ts +21 -7
- package/src/resources/workflow-resource.ts +44 -15
- package/src/svg/percentage.ts +7 -3
- package/src/svg/scoreCard.ts +10 -4
- package/src/types/queries.ts +1 -1
- package/src/utils/csv.ts +12 -0
- package/src/utils/json.ts +23 -13
- package/src/utils/report.ts +4 -0
- package/src/utils/resource-utils.ts +1 -0
|
@@ -12,21 +12,30 @@
|
|
|
12
12
|
License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Include macro options.
|
|
17
|
+
* @param cardKey Card key of the card being included
|
|
18
|
+
* @param levelOffset A positive number will increase the level of headings.
|
|
19
|
+
* A negative value will decrease the level of headings in the included content.
|
|
20
|
+
* @param title Determines behaviour with the title that is in card metadata
|
|
21
|
+
* include --> includes the title
|
|
22
|
+
* exclude --> excludes the title
|
|
23
|
+
* only --> includes title but does not import content
|
|
24
|
+
* @param whitespace Whether to trim leading and trailing whitespace
|
|
25
|
+
* @param escape Type of escaping to apply to the included content
|
|
26
|
+
* json --> escapes for JSON strings
|
|
27
|
+
* csv --> escapes for CSV fields
|
|
28
|
+
*/
|
|
15
29
|
export interface IncludeMacroOptions {
|
|
16
|
-
/**
|
|
17
|
-
* Card key of the card being included
|
|
18
|
-
*/
|
|
19
30
|
cardKey: string;
|
|
20
|
-
/**
|
|
21
|
-
* A positive number wil increase the level of headings and a negative alue will the level of headings in the included content
|
|
22
|
-
*/
|
|
23
31
|
levelOffset?: string;
|
|
24
|
-
/**
|
|
25
|
-
* Determines behaviour with the title that is in card metadata
|
|
26
|
-
* include --> includes the title
|
|
27
|
-
* exclude --> excludes the title
|
|
28
|
-
* only --> includes title but does not import content
|
|
29
|
-
*/
|
|
30
32
|
title?: 'include' | 'exclude' | 'only';
|
|
31
33
|
pageTitles?: 'normal' | 'discrete';
|
|
34
|
+
/**
|
|
35
|
+
* Whether to trim initial and final whitespace and newlines from the output.
|
|
36
|
+
* Useful for generating non-AsciiDoc content such as JSON.
|
|
37
|
+
* Default is 'keep'.
|
|
38
|
+
*/
|
|
39
|
+
whitespace?: 'keep' | 'trim';
|
|
40
|
+
escape?: 'json' | 'csv';
|
|
32
41
|
}
|
package/src/macros/index.ts
CHANGED
|
@@ -115,7 +115,7 @@ export abstract class DefaultContent {
|
|
|
115
115
|
displayName: '',
|
|
116
116
|
dataType: dataType,
|
|
117
117
|
} as FieldType;
|
|
118
|
-
if (dataType === 'enum') {
|
|
118
|
+
if (dataType === 'enum' || dataType === 'list') {
|
|
119
119
|
value.enumValues = [{ enumValue: 'value1' }, { enumValue: 'value2' }];
|
|
120
120
|
}
|
|
121
121
|
return value;
|
|
@@ -188,13 +188,16 @@ export class FieldTypeResource extends FileResource<FieldType> {
|
|
|
188
188
|
);
|
|
189
189
|
}
|
|
190
190
|
const newValue = (op as ChangeOperation<EnumDefinition>).to;
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
throw new Error(
|
|
196
|
-
`Cannot perform operation on 'enumValues'. Enum with value '${(op.to as EnumDefinition).enumValue}' already exists`,
|
|
191
|
+
// Only check for duplicates if the enumValue itself is being changed
|
|
192
|
+
if (newValue.enumValue !== targetValue.enumValue) {
|
|
193
|
+
const foundTo = values.find(
|
|
194
|
+
(item) => (item as EnumDefinition).enumValue === newValue.enumValue,
|
|
197
195
|
);
|
|
196
|
+
if (foundTo) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
`Cannot perform operation on 'enumValues'. Enum with value '${(op.to as EnumDefinition).enumValue}' already exists`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
198
201
|
}
|
|
199
202
|
}
|
|
200
203
|
// Return the whole object; caller can just provide 'enumValue'.
|
|
@@ -431,6 +434,10 @@ export class FieldTypeResource extends FileResource<FieldType> {
|
|
|
431
434
|
}
|
|
432
435
|
content.dataType = super.handleScalar(op) as DataType;
|
|
433
436
|
} else if (key === 'enumValues') {
|
|
437
|
+
// Initialize enumValues array if it doesn't exist
|
|
438
|
+
if (!content.enumValues) {
|
|
439
|
+
content.enumValues = [];
|
|
440
|
+
}
|
|
434
441
|
if (op.name === 'add' || op.name === 'change' || op.name === 'remove') {
|
|
435
442
|
const existingValue = this.enumValueExists<EnumDefinition>(
|
|
436
443
|
op as Operation<EnumDefinition>,
|
|
@@ -19,6 +19,7 @@ import { isContentKey } from '../interfaces/resource-interfaces.js';
|
|
|
19
19
|
import {
|
|
20
20
|
filename,
|
|
21
21
|
contentPropertyName,
|
|
22
|
+
ALL_FILE_MAPPINGS,
|
|
22
23
|
} from '../interfaces/folder-content-interfaces.js';
|
|
23
24
|
import { formatJson } from '../utils/json.js';
|
|
24
25
|
import { VALID_FOLDER_RESOURCE_FILES } from '../utils/constants.js';
|
|
@@ -95,7 +96,7 @@ export abstract class FolderResource<
|
|
|
95
96
|
for (const [fileName, fileContent] of contentFiles.entries()) {
|
|
96
97
|
const key = contentPropertyName(fileName);
|
|
97
98
|
if (key) {
|
|
98
|
-
const isJson = key === '
|
|
99
|
+
const isJson = key === ALL_FILE_MAPPINGS['parameterSchema.json'];
|
|
99
100
|
content[key] = isJson ? JSON.parse(fileContent) : fileContent;
|
|
100
101
|
}
|
|
101
102
|
}
|
|
@@ -143,15 +144,28 @@ export abstract class FolderResource<
|
|
|
143
144
|
throw new Error(`File '${fileName}' is not allowed to be updated`);
|
|
144
145
|
}
|
|
145
146
|
|
|
146
|
-
|
|
147
|
+
// TODO: Updates should either use valid strings or allow for objects
|
|
148
|
+
const key = contentPropertyName(fileName);
|
|
149
|
+
const isJson = key === ALL_FILE_MAPPINGS['parameterSchema.json'];
|
|
150
|
+
let parsedContent: unknown = changedContent;
|
|
151
|
+
if (isJson) {
|
|
152
|
+
try {
|
|
153
|
+
parsedContent = JSON.parse(changedContent);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
const message =
|
|
156
|
+
error instanceof Error ? error.message : 'Unknown error';
|
|
157
|
+
throw new Error(`Invalid JSON content for '${key}' update: ${message}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const contentToWrite = isJson
|
|
161
|
+
? formatJson(parsedContent as object)
|
|
162
|
+
: changedContent;
|
|
163
|
+
|
|
164
|
+
await writeFileSafe(filePath, contentToWrite, { flag: 'w' });
|
|
147
165
|
|
|
148
166
|
// Update this resource's content
|
|
149
|
-
const key = contentPropertyName(fileName);
|
|
150
167
|
if (key) {
|
|
151
|
-
|
|
152
|
-
(this.resourceContent as Record<string, unknown>)[key] = isJson
|
|
153
|
-
? JSON.parse(changedContent)
|
|
154
|
-
: changedContent;
|
|
168
|
+
(this.resourceContent as Record<string, unknown>)[key] = parsedContent;
|
|
155
169
|
}
|
|
156
170
|
}
|
|
157
171
|
|
|
@@ -78,16 +78,11 @@ export class WorkflowResource extends FileResource<Workflow> {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
// Handle change of workflow state.
|
|
81
|
-
private async handleStateChange(
|
|
82
|
-
|
|
81
|
+
private async handleStateChange(
|
|
82
|
+
content: Workflow,
|
|
83
|
+
op: ChangeOperation<WorkflowState>,
|
|
84
|
+
) {
|
|
83
85
|
const stateName = this.targetName(op) as string;
|
|
84
|
-
// Check that state can be changed to
|
|
85
|
-
content.transitions = content.transitions.filter(
|
|
86
|
-
(t) => t.toState !== stateName,
|
|
87
|
-
);
|
|
88
|
-
content.transitions.forEach((t) => {
|
|
89
|
-
t.fromState = t.fromState.filter((state) => state !== stateName);
|
|
90
|
-
});
|
|
91
86
|
// validate that new state contains 'name' and 'category'
|
|
92
87
|
if (op.to.name === undefined || op.to.category === undefined) {
|
|
93
88
|
throw new Error(
|
|
@@ -95,16 +90,26 @@ export class WorkflowResource extends FileResource<Workflow> {
|
|
|
95
90
|
Updated state must have 'name' and 'category' properties.`,
|
|
96
91
|
);
|
|
97
92
|
}
|
|
98
|
-
//
|
|
93
|
+
// Rename transitions to use the new state name
|
|
99
94
|
const toStateName = op.to.name;
|
|
100
|
-
|
|
95
|
+
content.transitions.forEach((t) => {
|
|
96
|
+
if (t.toState === stateName) {
|
|
97
|
+
t.toState = toStateName;
|
|
98
|
+
}
|
|
99
|
+
t.fromState = t.fromState.map((state) =>
|
|
100
|
+
state === stateName ? toStateName : state,
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
// Update all cards that use this state.
|
|
101
104
|
await this.updateCardStates(stateName, toStateName);
|
|
102
105
|
}
|
|
103
106
|
|
|
104
107
|
// Handle removal of workflow state.
|
|
105
108
|
// State can be removed with or without replacement.
|
|
106
|
-
private async handleStateRemoval(
|
|
107
|
-
|
|
109
|
+
private async handleStateRemoval(
|
|
110
|
+
content: Workflow,
|
|
111
|
+
op: RemoveOperation<WorkflowState>,
|
|
112
|
+
) {
|
|
108
113
|
const stateName = this.targetName(op) as string;
|
|
109
114
|
|
|
110
115
|
// If there is no replacement value, remove all transitions "to" and "from" this state.
|
|
@@ -232,6 +237,28 @@ export class WorkflowResource extends FileResource<Workflow> {
|
|
|
232
237
|
return super.create(newContent);
|
|
233
238
|
}
|
|
234
239
|
|
|
240
|
+
/**
|
|
241
|
+
* Validates the content of the workflow resource.
|
|
242
|
+
* @param content Content to be validated.
|
|
243
|
+
* @throws if content is invalid.
|
|
244
|
+
*/
|
|
245
|
+
public async validate(content?: Workflow) {
|
|
246
|
+
// Base class run basic schema checks
|
|
247
|
+
await super.validate(content);
|
|
248
|
+
|
|
249
|
+
const workflowContent = content ?? this.content;
|
|
250
|
+
|
|
251
|
+
const newCardTransitions = workflowContent.transitions.filter(
|
|
252
|
+
(t) => t.fromState.includes('') || t.fromState.length === 0,
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
if (newCardTransitions.length !== 1) {
|
|
256
|
+
throw new Error(
|
|
257
|
+
`Workflow '${workflowContent.name}' must have exactly one transition from "New Card" (empty fromState), found ${newCardTransitions.length}.`,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
235
262
|
/**
|
|
236
263
|
* Renames the object and the file.
|
|
237
264
|
* @param newName New name for the resource.
|
|
@@ -329,11 +356,13 @@ export class WorkflowResource extends FileResource<Workflow> {
|
|
|
329
356
|
removeOp = {
|
|
330
357
|
name: 'remove',
|
|
331
358
|
target: toBeRemovedState as WorkflowState,
|
|
359
|
+
replacementValue: (op as RemoveOperation<unknown>)
|
|
360
|
+
.replacementValue as WorkflowState,
|
|
332
361
|
};
|
|
333
362
|
} else {
|
|
334
363
|
removeOp = op as RemoveOperation<WorkflowState>;
|
|
335
364
|
}
|
|
336
|
-
await this.handleStateRemoval(removeOp);
|
|
365
|
+
await this.handleStateRemoval(content, removeOp);
|
|
337
366
|
} else if (key === 'states' && op.name === 'change') {
|
|
338
367
|
// If workflow state is renamed, replace all transitions "to" and "from" the old state with new state.
|
|
339
368
|
let changeOp: ChangeOperation<WorkflowState>;
|
|
@@ -349,7 +378,7 @@ export class WorkflowResource extends FileResource<Workflow> {
|
|
|
349
378
|
} else {
|
|
350
379
|
changeOp = op as ChangeOperation<WorkflowState>;
|
|
351
380
|
}
|
|
352
|
-
await this.handleStateChange(changeOp);
|
|
381
|
+
await this.handleStateChange(content, changeOp);
|
|
353
382
|
}
|
|
354
383
|
|
|
355
384
|
await super.postUpdate(content, updateKey, op);
|
package/src/svg/percentage.ts
CHANGED
|
@@ -71,10 +71,14 @@ export function percentage(options: PercentageOptions): string {
|
|
|
71
71
|
const dynamicSVGHeight = donutYOffset + SIZE / 2 + R + 20;
|
|
72
72
|
return `
|
|
73
73
|
<svg width="${SVG_WIDTH}" height="${dynamicSVGHeight}" viewBox="0 0 ${SVG_WIDTH} ${dynamicSVGHeight}" xmlns="http://www.w3.org/2000/svg">
|
|
74
|
+
<style>
|
|
75
|
+
.percentage-text { fill: var(--joy-palette-text-primary, #333); }
|
|
76
|
+
.percentage-legend { fill: var(--joy-palette-text-secondary, #666); }
|
|
77
|
+
</style>
|
|
74
78
|
<title>${title}</title>
|
|
75
79
|
|
|
76
80
|
<!-- Visible Title (wrapped) -->
|
|
77
|
-
<text x="${SVG_WIDTH / 2}" y="${TITLE_Y}" text-anchor="middle" font-size="${TITLE_FONT_SIZE}" font-weight="bold">
|
|
81
|
+
<text class="percentage-text" x="${SVG_WIDTH / 2}" y="${TITLE_Y}" text-anchor="middle" font-size="${TITLE_FONT_SIZE}" font-weight="bold">
|
|
78
82
|
${titleLines.map((line, i) => `<tspan x='${SVG_WIDTH / 2}' dy='${i === 0 ? 0 : LINE_SPACING}em'>${line}</tspan>`).join('')}
|
|
79
83
|
</text>
|
|
80
84
|
|
|
@@ -90,8 +94,8 @@ export function percentage(options: PercentageOptions): string {
|
|
|
90
94
|
transform="rotate(-90 ${SVG_WIDTH / 2} ${donutCenterY})" />
|
|
91
95
|
|
|
92
96
|
<!-- Numbers -->
|
|
93
|
-
<text x="${SVG_WIDTH / 2}" y="${donutCenterY - 8}" text-anchor="middle" font-size="${VALUE_FONT_SIZE}" font-weight="bold">${value}%</text>
|
|
94
|
-
<text x="${SVG_WIDTH / 2}" y="${donutCenterY + 20}" text-anchor="middle" font-size="${LEGEND_FONT_SIZE}">${legend}</text>
|
|
97
|
+
<text class="percentage-text" x="${SVG_WIDTH / 2}" y="${donutCenterY - 8}" text-anchor="middle" font-size="${VALUE_FONT_SIZE}" font-weight="bold">${value}%</text>
|
|
98
|
+
<text class="percentage-legend" x="${SVG_WIDTH / 2}" y="${donutCenterY + 20}" text-anchor="middle" font-size="${LEGEND_FONT_SIZE}">${legend}</text>
|
|
95
99
|
</svg>
|
|
96
100
|
`;
|
|
97
101
|
}
|
package/src/svg/scoreCard.ts
CHANGED
|
@@ -76,11 +76,17 @@ export function scoreCard(options: ScoreCardOptions): string {
|
|
|
76
76
|
const height = Math.ceil(textBlockHeight + PADDING * 2);
|
|
77
77
|
|
|
78
78
|
const svgContent = `<svg class="card" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg">
|
|
79
|
-
<
|
|
79
|
+
<style>
|
|
80
|
+
.scorecard-bg { fill: var(--joy-palette-background-surface, #fff); stroke: var(--joy-palette-divider, #dfe4ea); }
|
|
81
|
+
.scorecard-title { fill: var(--joy-palette-text-primary, #001829); }
|
|
82
|
+
.scorecard-value { fill: var(--joy-palette-text-primary, #333); }
|
|
83
|
+
.scorecard-caption { fill: var(--joy-palette-text-tertiary, #999); }
|
|
84
|
+
</style>
|
|
85
|
+
<rect class="scorecard-bg" rx="8" ry="8" stroke-width="3" width="${width}" height="${height}"/>
|
|
80
86
|
<g text-anchor="middle">
|
|
81
|
-
<text class="title" x="${width / 2}" y="${TITLE_SIZE / 2 + PADDING}" font-size="${TITLE_SIZE}" font-weight="400"
|
|
82
|
-
<text class="value" x="${width / 2}" y="${titleHeight + VALUE_SIZE / 2 + PADDING}" font-size="${VALUE_SIZE}" font-weight="700"
|
|
83
|
-
<text class="caption" x="${width / 2}" y="${titleHeight + valueHeight + LINE_GAP_CAPTION + CAPTION_SIZE / 2 + PADDING}" font-size="${CAPTION_SIZE}" font-weight="400"
|
|
87
|
+
<text class="scorecard-title" x="${width / 2}" y="${TITLE_SIZE / 2 + PADDING}" font-size="${TITLE_SIZE}" font-weight="400" dominant-baseline="middle">${title}</text>
|
|
88
|
+
<text class="scorecard-value" x="${width / 2}" y="${titleHeight + VALUE_SIZE / 2 + PADDING}" font-size="${VALUE_SIZE}" font-weight="700" dominant-baseline="middle">${value}<tspan class="unit" font-size="${UNIT_SIZE}" font-weight="400" dx="${UNIT_OFFSET}">${unit}</tspan></text>
|
|
89
|
+
<text class="scorecard-caption" x="${width / 2}" y="${titleHeight + valueHeight + LINE_GAP_CAPTION + CAPTION_SIZE / 2 + PADDING}" font-size="${CAPTION_SIZE}" font-weight="400" dominant-baseline="middle">${legend}</text>
|
|
84
90
|
</g>
|
|
85
91
|
</svg>`;
|
|
86
92
|
|
package/src/types/queries.ts
CHANGED
package/src/utils/csv.ts
CHANGED
|
@@ -14,6 +14,18 @@ import { readFile } from 'node:fs/promises';
|
|
|
14
14
|
import { parse } from 'csv-parse/sync';
|
|
15
15
|
import type { CSVRowRaw } from '../interfaces/project-interfaces.js';
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Escapes a string for use as a CSV field.
|
|
19
|
+
* Escapes double quotes by doubling them. The caller is responsible for
|
|
20
|
+
* wrapping the result in quotes.
|
|
21
|
+
* @param str The string to escape
|
|
22
|
+
* @returns The escaped string (without surrounding quotes)
|
|
23
|
+
*/
|
|
24
|
+
export function escapeCsvField(str: string): string {
|
|
25
|
+
// Escape double quotes by doubling them
|
|
26
|
+
return str.replace(/"/g, '""');
|
|
27
|
+
}
|
|
28
|
+
|
|
17
29
|
/**
|
|
18
30
|
* Reads a CSV file and returns its content as an array of objects.
|
|
19
31
|
* @param file Path to the CSV file.
|
package/src/utils/json.ts
CHANGED
|
@@ -15,6 +15,29 @@
|
|
|
15
15
|
import { readFileSync } from 'node:fs';
|
|
16
16
|
import { type FileHandle, readFile, writeFile } from 'node:fs/promises';
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Escapes a string for use as a JSON string value.
|
|
20
|
+
* Escapes double quotes, backslashes, and control characters.
|
|
21
|
+
* @param content The string to escape
|
|
22
|
+
* @returns The escaped string suitable for use in JSON
|
|
23
|
+
*/
|
|
24
|
+
export function escapeJsonString(content: string): string {
|
|
25
|
+
return JSON.stringify(content).slice(1, -1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Format an object with JSON.stringify
|
|
30
|
+
*
|
|
31
|
+
* The purpose of this function is to format the JSON output in a centralised function
|
|
32
|
+
* so that the format can be controlled in a single location.
|
|
33
|
+
*
|
|
34
|
+
* @param json JSON object to format.
|
|
35
|
+
* @returns Formatted JSON string
|
|
36
|
+
*/
|
|
37
|
+
export function formatJson(json: object) {
|
|
38
|
+
return JSON.stringify(json, trimReplacer, 4);
|
|
39
|
+
}
|
|
40
|
+
|
|
18
41
|
/**
|
|
19
42
|
* Handles reading of a JSON file.
|
|
20
43
|
* @param file file name (and path) to read.
|
|
@@ -79,19 +102,6 @@ export function trimReplacer(_: string, value: unknown) {
|
|
|
79
102
|
return value;
|
|
80
103
|
}
|
|
81
104
|
|
|
82
|
-
/**
|
|
83
|
-
* Format an object with JSON.stringify
|
|
84
|
-
*
|
|
85
|
-
* The purpose of this function is to format the JSON output in a centralised function
|
|
86
|
-
* so that the format can be controlled in a single location.
|
|
87
|
-
*
|
|
88
|
-
* @param json JSON object to format.
|
|
89
|
-
* @returns Formatted JSON string
|
|
90
|
-
*/
|
|
91
|
-
export function formatJson(json: object) {
|
|
92
|
-
return JSON.stringify(json, trimReplacer, 4);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
105
|
/**
|
|
96
106
|
* Writes and formats a JSON file.
|
|
97
107
|
* @param filename file name (and path) to write.
|
package/src/utils/report.ts
CHANGED
|
@@ -14,6 +14,8 @@ import type { CalculationEngine } from '../containers/project/calculation-engine
|
|
|
14
14
|
import { registerEmptyMacros } from '../macros/index.js';
|
|
15
15
|
import type { Context } from '../interfaces/project-interfaces.js';
|
|
16
16
|
import { resourceName } from './resource-utils.js';
|
|
17
|
+
import { escapeJsonString } from './json.js';
|
|
18
|
+
import { escapeCsvField } from './csv.js';
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* Formats a value from a logic program for use to graphviz
|
|
@@ -125,6 +127,8 @@ export async function generateReportContent(
|
|
|
125
127
|
}
|
|
126
128
|
handlebars.registerHelper('isCustomField', isCustomField);
|
|
127
129
|
handlebars.registerHelper('formatValue', formatValue);
|
|
130
|
+
handlebars.registerHelper('jsonEscape', escapeJsonString);
|
|
131
|
+
handlebars.registerHelper('csvEscape', escapeCsvField);
|
|
128
132
|
|
|
129
133
|
return handlebars.compile(contentTemplate)({
|
|
130
134
|
...options,
|