@twinfinity/printing 6.0.1-ci.28952-beta → 6.0.2-beta

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 (82) hide show
  1. package/README.md +3 -3
  2. package/dist/BuildInfo.js +3 -3
  3. package/dist/BuildInfo.js.map +1 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/layout/analyzeTemplate.js.map +1 -1
  6. package/dist/layout/clipUtils.js.map +1 -1
  7. package/dist/layout/colorConstants.js.map +1 -1
  8. package/dist/layout/elementKinds.js.map +1 -1
  9. package/dist/layout/exportDrawing.js.map +1 -1
  10. package/dist/layout/filters.js.map +1 -1
  11. package/dist/layout/geometryConstants.js.map +1 -1
  12. package/dist/layout/index.js.map +1 -1
  13. package/dist/layout/itemBuilders.js.map +1 -1
  14. package/dist/layout/labelsRender.js.map +1 -1
  15. package/dist/layout/legendConstants.js.map +1 -1
  16. package/dist/layout/legendRender.js.map +1 -1
  17. package/dist/layout/legendRows.js.map +1 -1
  18. package/dist/layout/legendUtils.js.map +1 -1
  19. package/dist/layout/metadata.js.map +1 -1
  20. package/dist/layout/model.js.map +1 -1
  21. package/dist/layout/options.js.map +1 -1
  22. package/dist/layout/presets.js.map +1 -1
  23. package/dist/layout/qrRender.js.map +1 -1
  24. package/dist/layout/renderDrawing.js.map +1 -1
  25. package/dist/layout/renderUtils.js.map +1 -1
  26. package/dist/layout/sectionValidation.js.map +1 -1
  27. package/dist/layout/templateVariables.js.map +1 -1
  28. package/dist/layout/typeGuards.js.map +1 -1
  29. package/dist/layout/types.js.map +1 -1
  30. package/dist/layout/utils.js.map +1 -1
  31. package/dist/layout/validation.js.map +1 -1
  32. package/dist/layout/validationHelpers.js.map +1 -1
  33. package/dist/layout/validationUtils.js.map +1 -1
  34. package/dist/layout/viewportPrep.js.map +1 -1
  35. package/dist/layout/viewportRender.js.map +1 -1
  36. package/dist/layout/viewportUtils.js.map +1 -1
  37. package/dist/layout/warnings.js.map +1 -1
  38. package/dist/printToPdf.js.map +1 -1
  39. package/dist/printToSvg.js.map +1 -1
  40. package/dist/sections.js.map +1 -1
  41. package/dist/types.js.map +1 -1
  42. package/package.json +2 -2
  43. package/src/BuildInfo.ts +41 -41
  44. package/src/index.ts +10 -10
  45. package/src/layout/analyzeTemplate.ts +218 -218
  46. package/src/layout/clipUtils.ts +193 -193
  47. package/src/layout/colorConstants.ts +16 -16
  48. package/src/layout/elementKinds.ts +35 -35
  49. package/src/layout/exportDrawing.ts +206 -206
  50. package/src/layout/filters.ts +80 -80
  51. package/src/layout/geometryConstants.ts +11 -11
  52. package/src/layout/index.ts +671 -671
  53. package/src/layout/itemBuilders.ts +159 -159
  54. package/src/layout/labelsRender.ts +170 -170
  55. package/src/layout/legendConstants.ts +11 -11
  56. package/src/layout/legendRender.ts +543 -543
  57. package/src/layout/legendRows.ts +62 -62
  58. package/src/layout/legendUtils.ts +144 -144
  59. package/src/layout/metadata.ts +210 -210
  60. package/src/layout/model.ts +372 -372
  61. package/src/layout/options.ts +17 -17
  62. package/src/layout/presets.ts +51 -51
  63. package/src/layout/qrRender.ts +126 -126
  64. package/src/layout/qrcode.d.ts +2 -2
  65. package/src/layout/renderDrawing.ts +710 -710
  66. package/src/layout/renderUtils.ts +138 -138
  67. package/src/layout/sectionValidation.ts +2 -2
  68. package/src/layout/templateVariables.ts +317 -317
  69. package/src/layout/typeGuards.ts +9 -9
  70. package/src/layout/types.ts +835 -835
  71. package/src/layout/utils.ts +85 -85
  72. package/src/layout/validation.ts +392 -392
  73. package/src/layout/validationHelpers.ts +59 -59
  74. package/src/layout/validationUtils.ts +6 -6
  75. package/src/layout/viewportPrep.ts +351 -351
  76. package/src/layout/viewportRender.ts +442 -442
  77. package/src/layout/viewportUtils.ts +50 -50
  78. package/src/layout/warnings.ts +3 -3
  79. package/src/printToPdf.ts +108 -108
  80. package/src/printToSvg.ts +188 -188
  81. package/src/sections.ts +1019 -1019
  82. package/src/types.ts +49 -49
@@ -1,317 +1,317 @@
1
- /**
2
- * Template variable extraction, validation, and resolution.
3
- *
4
- * This module provides utilities for working with parameterized templates:
5
- * - Extract variable definitions for UI generation
6
- * - Get default values (fail-safe fallback)
7
- * - Resolve user-provided values with validation
8
- * - Apply variables to template (substitute {{vars.*}} references)
9
- *
10
- * @module templateVariables
11
- */
12
-
13
- import type { LayoutTemplate, TemplateVariableDef, TemplateVariableInfo } from './types';
14
-
15
- /**
16
- * Extract all variable definitions from a template.
17
- *
18
- * Returns an array of variable info objects suitable for UI generation.
19
- * Returns empty array if template has no variables.
20
- *
21
- * @param template - Layout template (may or may not have variables)
22
- * @returns Array of variable definitions with metadata
23
- *
24
- * @example
25
- * ```typescript
26
- * const vars = getTemplateVariables(template);
27
- * // [
28
- * // { name: 'hyresobjektsnummer', type: 'string', default: '2022-F2008', label: 'Hyresobjekt', ... },
29
- * // { name: 'showLegend', type: 'boolean', default: true, label: 'Show Legend', ... }
30
- * // ]
31
- *
32
- * // Use for UI generation
33
- * vars.forEach(v => {
34
- * if (v.type === 'string') {
35
- * renderTextField(v.label, v.default);
36
- * } else if (v.type === 'boolean') {
37
- * renderCheckbox(v.label, v.default);
38
- * }
39
- * });
40
- * ```
41
- */
42
- export function getTemplateVariables(template: LayoutTemplate): TemplateVariableInfo[] {
43
- if (!template.variables) return [];
44
-
45
- return Object.entries(template.variables).map(([name, def]) => ({
46
- name,
47
- type: def.type,
48
- default: def.default,
49
- label: def.label ?? name,
50
- description: def.description,
51
- format: def.format,
52
- min: def.min,
53
- max: def.max,
54
- step: def.step,
55
- minLength: def.minLength,
56
- maxLength: def.maxLength,
57
- pattern: def.pattern,
58
- enum: def.enum,
59
- items: def.items
60
- }));
61
- }
62
-
63
- /**
64
- * Extract all default values from a template.
65
- *
66
- * Returns an object with all variable names mapped to their default values.
67
- * These defaults are guaranteed to work - template can always render with them.
68
- *
69
- * Returns empty object if template has no variables.
70
- *
71
- * @param template - Layout template
72
- * @returns Object mapping variable names to default values
73
- *
74
- * @example
75
- * ```typescript
76
- * const defaults = getTemplateDefaults(template);
77
- * // { hyresobjektsnummer: '2022-F2008', showLegend: true, scaleDenom: 100 }
78
- *
79
- * // Can always render with defaults (fail-safe)
80
- * const { svg } = await renderDrawing({
81
- * template,
82
- * models,
83
- * variables: defaults // Guaranteed to work
84
- * });
85
- * ```
86
- */
87
- export function getTemplateDefaults(template: LayoutTemplate): Record<string, any> {
88
- if (!template.variables) return {};
89
-
90
- const defaults: Record<string, any> = {};
91
- for (const [name, def] of Object.entries(template.variables)) {
92
- defaults[name] = def.default;
93
- }
94
- return defaults;
95
- }
96
-
97
- /**
98
- * Validate a value against a variable definition.
99
- *
100
- * Checks type and constraint validation (min/max, pattern, enum, etc.).
101
- *
102
- * @param value - Value to validate
103
- * @param varDef - Variable definition with constraints
104
- * @returns Validation result with ok flag and optional error message
105
- */
106
- function validateVariable(value: any, varDef: TemplateVariableDef): { ok: boolean; error?: string } {
107
- // Type check
108
- const actualType = Array.isArray(value) ? 'array' : typeof value;
109
- if (varDef.type === 'number' && actualType !== 'number') {
110
- return { ok: false, error: `Expected number, got ${actualType}` };
111
- }
112
- if (varDef.type === 'string' && actualType !== 'string') {
113
- return { ok: false, error: `Expected string, got ${actualType}` };
114
- }
115
- if (varDef.type === 'boolean' && actualType !== 'boolean') {
116
- return { ok: false, error: `Expected boolean, got ${actualType}` };
117
- }
118
- if (varDef.type === 'array' && actualType !== 'array') {
119
- return { ok: false, error: `Expected array, got ${actualType}` };
120
- }
121
-
122
- // String validation
123
- if (varDef.type === 'string' && typeof value === 'string') {
124
- if (varDef.minLength !== undefined && value.length < varDef.minLength) {
125
- return { ok: false, error: `String too short (min ${varDef.minLength})` };
126
- }
127
- if (varDef.maxLength !== undefined && value.length > varDef.maxLength) {
128
- return { ok: false, error: `String too long (max ${varDef.maxLength})` };
129
- }
130
- if (varDef.pattern) {
131
- try {
132
- const regex = new RegExp(varDef.pattern);
133
- if (!regex.test(value)) {
134
- return { ok: false, error: `Does not match pattern ${varDef.pattern}` };
135
- }
136
- } catch {
137
- return { ok: false, error: `Invalid pattern: ${varDef.pattern}` };
138
- }
139
- }
140
- if (varDef.enum && !varDef.enum.includes(value)) {
141
- return { ok: false, error: `Not in allowed values: ${varDef.enum.join(', ')}` };
142
- }
143
- }
144
-
145
- // Number validation
146
- if (varDef.type === 'number' && typeof value === 'number') {
147
- if (!Number.isFinite(value)) {
148
- return { ok: false, error: 'Must be a finite number' };
149
- }
150
- if (varDef.min !== undefined && value < varDef.min) {
151
- return { ok: false, error: `Too small (min ${varDef.min})` };
152
- }
153
- if (varDef.max !== undefined && value > varDef.max) {
154
- return { ok: false, error: `Too large (max ${varDef.max})` };
155
- }
156
- }
157
-
158
- return { ok: true };
159
- }
160
-
161
- /**
162
- * Resolve template variables by merging user values with defaults.
163
- *
164
- * This is a **fail-safe** function:
165
- * - Missing values → use default
166
- * - Invalid values → use default + warn
167
- * - Unknown variables → warn + ignore
168
- *
169
- * The result is ALWAYS valid and complete (safe to use for rendering).
170
- *
171
- * @param template - Layout template with variable definitions
172
- * @param userValues - Optional user-provided values (can be partial or undefined)
173
- * @returns Object with resolved values and any validation warnings
174
- *
175
- * @example
176
- * ```typescript
177
- * // User provides partial values
178
- * const { values, warnings } = resolveTemplateValues(template, {
179
- * hyresobjektsnummer: '2022-F2009' // Valid
180
- * // Other vars use defaults
181
- * });
182
- * // values = { hyresobjektsnummer: '2022-F2009', showLegend: true, scaleDenom: 100 }
183
- * // warnings = []
184
- *
185
- * // User provides invalid value
186
- * const { values, warnings } = resolveTemplateValues(template, {
187
- * scaleDenom: 9999 // Out of range (max: 500)
188
- * });
189
- * // values = { scaleDenom: 100, ... } ← Fell back to default
190
- * // warnings = ["Invalid 'scaleDenom': Too large (max 500) - using default '100'"]
191
- *
192
- * // Always safe to render
193
- * const resolvedTemplate = applyTemplateVariables(template, values);
194
- * ```
195
- */
196
- export function resolveTemplateValues(
197
- template: LayoutTemplate,
198
- userValues?: Record<string, any>
199
- ): { values: Record<string, any>; warnings: string[] } {
200
- const defaults = getTemplateDefaults(template);
201
- const warnings: string[] = [];
202
-
203
- // Start with all defaults (fail-safe!)
204
- const values = { ...defaults };
205
-
206
- // No user values or no variables → return defaults
207
- if (!userValues || !template.variables) {
208
- return { values, warnings };
209
- }
210
-
211
- // Merge and validate user values
212
- for (const [name, value] of Object.entries(userValues)) {
213
- const varDef = template.variables[name];
214
-
215
- // Unknown variable
216
- if (!varDef) {
217
- warnings.push(`Unknown variable '${name}' - ignoring`);
218
- continue;
219
- }
220
-
221
- // Validate value
222
- const validation = validateVariable(value, varDef);
223
- if (!validation.ok) {
224
- warnings.push(`Invalid '${name}': ${validation.error} - using default '${defaults[name]}'`);
225
- continue;
226
- }
227
-
228
- // Valid → use it
229
- values[name] = value;
230
- }
231
-
232
- return { values, warnings };
233
- }
234
-
235
- /**
236
- * Apply variables to a template by substituting {{vars.*}} references.
237
- *
238
- * Recursively scans the template and replaces all {{vars.variableName}} references
239
- * with the corresponding values from the provided values object.
240
- *
241
- * Returns a new template with all variables resolved (does not mutate input).
242
- *
243
- * @param template - Original template with {{vars.*}} references
244
- * @param values - Resolved variable values (from resolveTemplateValues)
245
- * @returns New template with all {{vars.*}} replaced by actual values
246
- *
247
- * @example
248
- * ```typescript
249
- * const template = {
250
- * elements: [
251
- * { kind: 'text', text: 'Hyresobjekt: {{vars.hyresobjektsnummer}}' },
252
- * { kind: 'viewport', clip: { where: { 'BIP.id': '{{vars.hyresobjektsnummer}}' } } }
253
- * ]
254
- * };
255
- *
256
- * const values = { hyresobjektsnummer: '2022-F2009' };
257
- * const resolved = applyTemplateVariables(template, values);
258
- * // {
259
- * // elements: [
260
- * // { kind: 'text', text: 'Hyresobjekt: 2022-F2009' },
261
- * // { kind: 'viewport', clip: { where: { 'BIP.id': '2022-F2009' } } }
262
- * // ]
263
- * // }
264
- * ```
265
- */
266
- export function applyTemplateVariables(template: LayoutTemplate, values: Record<string, any>): LayoutTemplate {
267
- // Deep clone to avoid mutation
268
- const resolved: LayoutTemplate = JSON.parse(JSON.stringify(template));
269
-
270
- // Recursively replace {{vars.*}} in all string values
271
- function replaceInValue(obj: any): any {
272
- if (typeof obj === 'string') {
273
- return interpolateVariables(obj, values);
274
- }
275
- if (Array.isArray(obj)) {
276
- return obj.map(replaceInValue);
277
- }
278
- if (obj && typeof obj === 'object') {
279
- const result: any = {};
280
- for (const [key, val] of Object.entries(obj)) {
281
- result[key] = replaceInValue(val);
282
- }
283
- return result;
284
- }
285
- return obj;
286
- }
287
-
288
- // Apply to all template properties that might contain variable references
289
- resolved.elements = replaceInValue(resolved.elements);
290
- if (resolved.assets) {
291
- resolved.assets = replaceInValue(resolved.assets);
292
- }
293
-
294
- return resolved;
295
- }
296
-
297
- /**
298
- * Interpolate {{vars.*}} references in a string.
299
- *
300
- * Replaces all occurrences of {{vars.variableName}} with the corresponding value.
301
- * If variable is not found, keeps the placeholder and logs a warning.
302
- *
303
- * @param str - String potentially containing {{vars.*}} references
304
- * @param values - Variable values
305
- * @returns String with all {{vars.*}} replaced
306
- */
307
- function interpolateVariables(str: string, values: Record<string, any>): string {
308
- return str.replace(/\{\{vars\.(\w+)\}\}/g, (match, varName) => {
309
- if (varName in values) {
310
- const value = values[varName];
311
- // Convert to string (handles numbers, booleans, etc.)
312
- return String(value);
313
- }
314
- console.warn(`[templateVariables] Variable '${varName}' not found - keeping placeholder`);
315
- return match; // Keep {{vars.varName}} as-is
316
- });
317
- }
1
+ /**
2
+ * Template variable extraction, validation, and resolution.
3
+ *
4
+ * This module provides utilities for working with parameterized templates:
5
+ * - Extract variable definitions for UI generation
6
+ * - Get default values (fail-safe fallback)
7
+ * - Resolve user-provided values with validation
8
+ * - Apply variables to template (substitute {{vars.*}} references)
9
+ *
10
+ * @module templateVariables
11
+ */
12
+
13
+ import type { LayoutTemplate, TemplateVariableDef, TemplateVariableInfo } from './types';
14
+
15
+ /**
16
+ * Extract all variable definitions from a template.
17
+ *
18
+ * Returns an array of variable info objects suitable for UI generation.
19
+ * Returns empty array if template has no variables.
20
+ *
21
+ * @param template - Layout template (may or may not have variables)
22
+ * @returns Array of variable definitions with metadata
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * const vars = getTemplateVariables(template);
27
+ * // [
28
+ * // { name: 'hyresobjektsnummer', type: 'string', default: '2022-F2008', label: 'Hyresobjekt', ... },
29
+ * // { name: 'showLegend', type: 'boolean', default: true, label: 'Show Legend', ... }
30
+ * // ]
31
+ *
32
+ * // Use for UI generation
33
+ * vars.forEach(v => {
34
+ * if (v.type === 'string') {
35
+ * renderTextField(v.label, v.default);
36
+ * } else if (v.type === 'boolean') {
37
+ * renderCheckbox(v.label, v.default);
38
+ * }
39
+ * });
40
+ * ```
41
+ */
42
+ export function getTemplateVariables(template: LayoutTemplate): TemplateVariableInfo[] {
43
+ if (!template.variables) return [];
44
+
45
+ return Object.entries(template.variables).map(([name, def]) => ({
46
+ name,
47
+ type: def.type,
48
+ default: def.default,
49
+ label: def.label ?? name,
50
+ description: def.description,
51
+ format: def.format,
52
+ min: def.min,
53
+ max: def.max,
54
+ step: def.step,
55
+ minLength: def.minLength,
56
+ maxLength: def.maxLength,
57
+ pattern: def.pattern,
58
+ enum: def.enum,
59
+ items: def.items
60
+ }));
61
+ }
62
+
63
+ /**
64
+ * Extract all default values from a template.
65
+ *
66
+ * Returns an object with all variable names mapped to their default values.
67
+ * These defaults are guaranteed to work - template can always render with them.
68
+ *
69
+ * Returns empty object if template has no variables.
70
+ *
71
+ * @param template - Layout template
72
+ * @returns Object mapping variable names to default values
73
+ *
74
+ * @example
75
+ * ```typescript
76
+ * const defaults = getTemplateDefaults(template);
77
+ * // { hyresobjektsnummer: '2022-F2008', showLegend: true, scaleDenom: 100 }
78
+ *
79
+ * // Can always render with defaults (fail-safe)
80
+ * const { svg } = await renderDrawing({
81
+ * template,
82
+ * models,
83
+ * variables: defaults // Guaranteed to work
84
+ * });
85
+ * ```
86
+ */
87
+ export function getTemplateDefaults(template: LayoutTemplate): Record<string, any> {
88
+ if (!template.variables) return {};
89
+
90
+ const defaults: Record<string, any> = {};
91
+ for (const [name, def] of Object.entries(template.variables)) {
92
+ defaults[name] = def.default;
93
+ }
94
+ return defaults;
95
+ }
96
+
97
+ /**
98
+ * Validate a value against a variable definition.
99
+ *
100
+ * Checks type and constraint validation (min/max, pattern, enum, etc.).
101
+ *
102
+ * @param value - Value to validate
103
+ * @param varDef - Variable definition with constraints
104
+ * @returns Validation result with ok flag and optional error message
105
+ */
106
+ function validateVariable(value: any, varDef: TemplateVariableDef): { ok: boolean; error?: string } {
107
+ // Type check
108
+ const actualType = Array.isArray(value) ? 'array' : typeof value;
109
+ if (varDef.type === 'number' && actualType !== 'number') {
110
+ return { ok: false, error: `Expected number, got ${actualType}` };
111
+ }
112
+ if (varDef.type === 'string' && actualType !== 'string') {
113
+ return { ok: false, error: `Expected string, got ${actualType}` };
114
+ }
115
+ if (varDef.type === 'boolean' && actualType !== 'boolean') {
116
+ return { ok: false, error: `Expected boolean, got ${actualType}` };
117
+ }
118
+ if (varDef.type === 'array' && actualType !== 'array') {
119
+ return { ok: false, error: `Expected array, got ${actualType}` };
120
+ }
121
+
122
+ // String validation
123
+ if (varDef.type === 'string' && typeof value === 'string') {
124
+ if (varDef.minLength !== undefined && value.length < varDef.minLength) {
125
+ return { ok: false, error: `String too short (min ${varDef.minLength})` };
126
+ }
127
+ if (varDef.maxLength !== undefined && value.length > varDef.maxLength) {
128
+ return { ok: false, error: `String too long (max ${varDef.maxLength})` };
129
+ }
130
+ if (varDef.pattern) {
131
+ try {
132
+ const regex = new RegExp(varDef.pattern);
133
+ if (!regex.test(value)) {
134
+ return { ok: false, error: `Does not match pattern ${varDef.pattern}` };
135
+ }
136
+ } catch {
137
+ return { ok: false, error: `Invalid pattern: ${varDef.pattern}` };
138
+ }
139
+ }
140
+ if (varDef.enum && !varDef.enum.includes(value)) {
141
+ return { ok: false, error: `Not in allowed values: ${varDef.enum.join(', ')}` };
142
+ }
143
+ }
144
+
145
+ // Number validation
146
+ if (varDef.type === 'number' && typeof value === 'number') {
147
+ if (!Number.isFinite(value)) {
148
+ return { ok: false, error: 'Must be a finite number' };
149
+ }
150
+ if (varDef.min !== undefined && value < varDef.min) {
151
+ return { ok: false, error: `Too small (min ${varDef.min})` };
152
+ }
153
+ if (varDef.max !== undefined && value > varDef.max) {
154
+ return { ok: false, error: `Too large (max ${varDef.max})` };
155
+ }
156
+ }
157
+
158
+ return { ok: true };
159
+ }
160
+
161
+ /**
162
+ * Resolve template variables by merging user values with defaults.
163
+ *
164
+ * This is a **fail-safe** function:
165
+ * - Missing values → use default
166
+ * - Invalid values → use default + warn
167
+ * - Unknown variables → warn + ignore
168
+ *
169
+ * The result is ALWAYS valid and complete (safe to use for rendering).
170
+ *
171
+ * @param template - Layout template with variable definitions
172
+ * @param userValues - Optional user-provided values (can be partial or undefined)
173
+ * @returns Object with resolved values and any validation warnings
174
+ *
175
+ * @example
176
+ * ```typescript
177
+ * // User provides partial values
178
+ * const { values, warnings } = resolveTemplateValues(template, {
179
+ * hyresobjektsnummer: '2022-F2009' // Valid
180
+ * // Other vars use defaults
181
+ * });
182
+ * // values = { hyresobjektsnummer: '2022-F2009', showLegend: true, scaleDenom: 100 }
183
+ * // warnings = []
184
+ *
185
+ * // User provides invalid value
186
+ * const { values, warnings } = resolveTemplateValues(template, {
187
+ * scaleDenom: 9999 // Out of range (max: 500)
188
+ * });
189
+ * // values = { scaleDenom: 100, ... } ← Fell back to default
190
+ * // warnings = ["Invalid 'scaleDenom': Too large (max 500) - using default '100'"]
191
+ *
192
+ * // Always safe to render
193
+ * const resolvedTemplate = applyTemplateVariables(template, values);
194
+ * ```
195
+ */
196
+ export function resolveTemplateValues(
197
+ template: LayoutTemplate,
198
+ userValues?: Record<string, any>
199
+ ): { values: Record<string, any>; warnings: string[] } {
200
+ const defaults = getTemplateDefaults(template);
201
+ const warnings: string[] = [];
202
+
203
+ // Start with all defaults (fail-safe!)
204
+ const values = { ...defaults };
205
+
206
+ // No user values or no variables → return defaults
207
+ if (!userValues || !template.variables) {
208
+ return { values, warnings };
209
+ }
210
+
211
+ // Merge and validate user values
212
+ for (const [name, value] of Object.entries(userValues)) {
213
+ const varDef = template.variables[name];
214
+
215
+ // Unknown variable
216
+ if (!varDef) {
217
+ warnings.push(`Unknown variable '${name}' - ignoring`);
218
+ continue;
219
+ }
220
+
221
+ // Validate value
222
+ const validation = validateVariable(value, varDef);
223
+ if (!validation.ok) {
224
+ warnings.push(`Invalid '${name}': ${validation.error} - using default '${defaults[name]}'`);
225
+ continue;
226
+ }
227
+
228
+ // Valid → use it
229
+ values[name] = value;
230
+ }
231
+
232
+ return { values, warnings };
233
+ }
234
+
235
+ /**
236
+ * Apply variables to a template by substituting {{vars.*}} references.
237
+ *
238
+ * Recursively scans the template and replaces all {{vars.variableName}} references
239
+ * with the corresponding values from the provided values object.
240
+ *
241
+ * Returns a new template with all variables resolved (does not mutate input).
242
+ *
243
+ * @param template - Original template with {{vars.*}} references
244
+ * @param values - Resolved variable values (from resolveTemplateValues)
245
+ * @returns New template with all {{vars.*}} replaced by actual values
246
+ *
247
+ * @example
248
+ * ```typescript
249
+ * const template = {
250
+ * elements: [
251
+ * { kind: 'text', text: 'Hyresobjekt: {{vars.hyresobjektsnummer}}' },
252
+ * { kind: 'viewport', clip: { where: { 'BIP.id': '{{vars.hyresobjektsnummer}}' } } }
253
+ * ]
254
+ * };
255
+ *
256
+ * const values = { hyresobjektsnummer: '2022-F2009' };
257
+ * const resolved = applyTemplateVariables(template, values);
258
+ * // {
259
+ * // elements: [
260
+ * // { kind: 'text', text: 'Hyresobjekt: 2022-F2009' },
261
+ * // { kind: 'viewport', clip: { where: { 'BIP.id': '2022-F2009' } } }
262
+ * // ]
263
+ * // }
264
+ * ```
265
+ */
266
+ export function applyTemplateVariables(template: LayoutTemplate, values: Record<string, any>): LayoutTemplate {
267
+ // Deep clone to avoid mutation
268
+ const resolved: LayoutTemplate = JSON.parse(JSON.stringify(template));
269
+
270
+ // Recursively replace {{vars.*}} in all string values
271
+ function replaceInValue(obj: any): any {
272
+ if (typeof obj === 'string') {
273
+ return interpolateVariables(obj, values);
274
+ }
275
+ if (Array.isArray(obj)) {
276
+ return obj.map(replaceInValue);
277
+ }
278
+ if (obj && typeof obj === 'object') {
279
+ const result: any = {};
280
+ for (const [key, val] of Object.entries(obj)) {
281
+ result[key] = replaceInValue(val);
282
+ }
283
+ return result;
284
+ }
285
+ return obj;
286
+ }
287
+
288
+ // Apply to all template properties that might contain variable references
289
+ resolved.elements = replaceInValue(resolved.elements);
290
+ if (resolved.assets) {
291
+ resolved.assets = replaceInValue(resolved.assets);
292
+ }
293
+
294
+ return resolved;
295
+ }
296
+
297
+ /**
298
+ * Interpolate {{vars.*}} references in a string.
299
+ *
300
+ * Replaces all occurrences of {{vars.variableName}} with the corresponding value.
301
+ * If variable is not found, keeps the placeholder and logs a warning.
302
+ *
303
+ * @param str - String potentially containing {{vars.*}} references
304
+ * @param values - Variable values
305
+ * @returns String with all {{vars.*}} replaced
306
+ */
307
+ function interpolateVariables(str: string, values: Record<string, any>): string {
308
+ return str.replace(/\{\{vars\.(\w+)\}\}/g, (match, varName) => {
309
+ if (varName in values) {
310
+ const value = values[varName];
311
+ // Convert to string (handles numbers, booleans, etc.)
312
+ return String(value);
313
+ }
314
+ console.warn(`[templateVariables] Variable '${varName}' not found - keeping placeholder`);
315
+ return match; // Keep {{vars.varName}} as-is
316
+ });
317
+ }
@@ -1,9 +1,9 @@
1
- /** Type guard: true for plain objects, false for arrays, null, and primitives. */
2
- export const isRecord = (value: unknown): value is Record<string, unknown> => {
3
- return value !== null && typeof value === 'object' && !Array.isArray(value);
4
- };
5
-
6
- /** Safely read a property from an unknown value. Returns undefined for non-objects. */
7
- export const getProp = (obj: unknown, key: string): unknown => {
8
- return isRecord(obj) ? obj[key] : undefined;
9
- };
1
+ /** Type guard: true for plain objects, false for arrays, null, and primitives. */
2
+ export const isRecord = (value: unknown): value is Record<string, unknown> => {
3
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
4
+ };
5
+
6
+ /** Safely read a property from an unknown value. Returns undefined for non-objects. */
7
+ export const getProp = (obj: unknown, key: string): unknown => {
8
+ return isRecord(obj) ? obj[key] : undefined;
9
+ };