@twinfinity/printing 5.0.3-ci.28538-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 (209) hide show
  1. package/README.md +208 -0
  2. package/dist/BuildInfo.d.ts +25 -0
  3. package/dist/BuildInfo.d.ts.map +1 -0
  4. package/dist/BuildInfo.js +15 -0
  5. package/dist/BuildInfo.js.map +1 -0
  6. package/dist/index.d.ts +7 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +9 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/layout/analyzeTemplate.d.ts +29 -0
  11. package/dist/layout/analyzeTemplate.d.ts.map +1 -0
  12. package/dist/layout/analyzeTemplate.js +168 -0
  13. package/dist/layout/analyzeTemplate.js.map +1 -0
  14. package/dist/layout/clipUtils.d.ts +18 -0
  15. package/dist/layout/clipUtils.d.ts.map +1 -0
  16. package/dist/layout/clipUtils.js +165 -0
  17. package/dist/layout/clipUtils.js.map +1 -0
  18. package/dist/layout/colorConstants.d.ts +11 -0
  19. package/dist/layout/colorConstants.d.ts.map +1 -0
  20. package/dist/layout/colorConstants.js +14 -0
  21. package/dist/layout/colorConstants.js.map +1 -0
  22. package/dist/layout/elementKinds.d.ts +21 -0
  23. package/dist/layout/elementKinds.d.ts.map +1 -0
  24. package/dist/layout/elementKinds.js +17 -0
  25. package/dist/layout/elementKinds.js.map +1 -0
  26. package/dist/layout/exportDrawing.d.ts +143 -0
  27. package/dist/layout/exportDrawing.d.ts.map +1 -0
  28. package/dist/layout/exportDrawing.js +146 -0
  29. package/dist/layout/exportDrawing.js.map +1 -0
  30. package/dist/layout/filters.d.ts +33 -0
  31. package/dist/layout/filters.d.ts.map +1 -0
  32. package/dist/layout/filters.js +77 -0
  33. package/dist/layout/filters.js.map +1 -0
  34. package/dist/layout/geometryConstants.d.ts +8 -0
  35. package/dist/layout/geometryConstants.d.ts.map +1 -0
  36. package/dist/layout/geometryConstants.js +10 -0
  37. package/dist/layout/geometryConstants.js.map +1 -0
  38. package/dist/layout/index.d.ts +86 -0
  39. package/dist/layout/index.d.ts.map +1 -0
  40. package/dist/layout/index.js +363 -0
  41. package/dist/layout/index.js.map +1 -0
  42. package/dist/layout/itemBuilders.d.ts +29 -0
  43. package/dist/layout/itemBuilders.d.ts.map +1 -0
  44. package/dist/layout/itemBuilders.js +135 -0
  45. package/dist/layout/itemBuilders.js.map +1 -0
  46. package/dist/layout/labelsRender.d.ts +29 -0
  47. package/dist/layout/labelsRender.d.ts.map +1 -0
  48. package/dist/layout/labelsRender.js +109 -0
  49. package/dist/layout/labelsRender.js.map +1 -0
  50. package/dist/layout/legendConstants.d.ts +12 -0
  51. package/dist/layout/legendConstants.d.ts.map +1 -0
  52. package/dist/layout/legendConstants.js +12 -0
  53. package/dist/layout/legendConstants.js.map +1 -0
  54. package/dist/layout/legendRender.d.ts +13 -0
  55. package/dist/layout/legendRender.d.ts.map +1 -0
  56. package/dist/layout/legendRender.js +424 -0
  57. package/dist/layout/legendRender.js.map +1 -0
  58. package/dist/layout/legendRows.d.ts +26 -0
  59. package/dist/layout/legendRows.d.ts.map +1 -0
  60. package/dist/layout/legendRows.js +45 -0
  61. package/dist/layout/legendRows.js.map +1 -0
  62. package/dist/layout/legendUtils.d.ts +21 -0
  63. package/dist/layout/legendUtils.d.ts.map +1 -0
  64. package/dist/layout/legendUtils.js +110 -0
  65. package/dist/layout/legendUtils.js.map +1 -0
  66. package/dist/layout/metadata.d.ts +43 -0
  67. package/dist/layout/metadata.d.ts.map +1 -0
  68. package/dist/layout/metadata.js +191 -0
  69. package/dist/layout/metadata.js.map +1 -0
  70. package/dist/layout/model.d.ts +15 -0
  71. package/dist/layout/model.d.ts.map +1 -0
  72. package/dist/layout/model.js +310 -0
  73. package/dist/layout/model.js.map +1 -0
  74. package/dist/layout/options.d.ts +17 -0
  75. package/dist/layout/options.d.ts.map +1 -0
  76. package/dist/layout/options.js +2 -0
  77. package/dist/layout/options.js.map +1 -0
  78. package/dist/layout/presets.d.ts +23 -0
  79. package/dist/layout/presets.d.ts.map +1 -0
  80. package/dist/layout/presets.js +41 -0
  81. package/dist/layout/presets.js.map +1 -0
  82. package/dist/layout/qrRender.d.ts +13 -0
  83. package/dist/layout/qrRender.d.ts.map +1 -0
  84. package/dist/layout/qrRender.js +97 -0
  85. package/dist/layout/qrRender.js.map +1 -0
  86. package/dist/layout/renderDrawing.d.ts +62 -0
  87. package/dist/layout/renderDrawing.d.ts.map +1 -0
  88. package/dist/layout/renderDrawing.js +555 -0
  89. package/dist/layout/renderDrawing.js.map +1 -0
  90. package/dist/layout/renderUtils.d.ts +36 -0
  91. package/dist/layout/renderUtils.d.ts.map +1 -0
  92. package/dist/layout/renderUtils.js +117 -0
  93. package/dist/layout/renderUtils.js.map +1 -0
  94. package/dist/layout/sectionValidation.d.ts +3 -0
  95. package/dist/layout/sectionValidation.d.ts.map +1 -0
  96. package/dist/layout/sectionValidation.js +2 -0
  97. package/dist/layout/sectionValidation.js.map +1 -0
  98. package/dist/layout/templateVariables.d.ts +137 -0
  99. package/dist/layout/templateVariables.d.ts.map +1 -0
  100. package/dist/layout/templateVariables.js +295 -0
  101. package/dist/layout/templateVariables.js.map +1 -0
  102. package/dist/layout/typeGuards.d.ts +5 -0
  103. package/dist/layout/typeGuards.d.ts.map +1 -0
  104. package/dist/layout/typeGuards.js +9 -0
  105. package/dist/layout/typeGuards.js.map +1 -0
  106. package/dist/layout/types.d.ts +816 -0
  107. package/dist/layout/types.d.ts.map +1 -0
  108. package/dist/layout/types.js +2 -0
  109. package/dist/layout/types.js.map +1 -0
  110. package/dist/layout/utils.d.ts +9 -0
  111. package/dist/layout/utils.d.ts.map +1 -0
  112. package/dist/layout/utils.js +84 -0
  113. package/dist/layout/utils.js.map +1 -0
  114. package/dist/layout/validation.d.ts +7 -0
  115. package/dist/layout/validation.d.ts.map +1 -0
  116. package/dist/layout/validation.js +352 -0
  117. package/dist/layout/validation.js.map +1 -0
  118. package/dist/layout/validationHelpers.d.ts +15 -0
  119. package/dist/layout/validationHelpers.d.ts.map +1 -0
  120. package/dist/layout/validationHelpers.js +43 -0
  121. package/dist/layout/validationHelpers.js.map +1 -0
  122. package/dist/layout/validationUtils.d.ts +5 -0
  123. package/dist/layout/validationUtils.d.ts.map +1 -0
  124. package/dist/layout/validationUtils.js +5 -0
  125. package/dist/layout/validationUtils.js.map +1 -0
  126. package/dist/layout/viewportPrep.d.ts +47 -0
  127. package/dist/layout/viewportPrep.d.ts.map +1 -0
  128. package/dist/layout/viewportPrep.js +275 -0
  129. package/dist/layout/viewportPrep.js.map +1 -0
  130. package/dist/layout/viewportRender.d.ts +42 -0
  131. package/dist/layout/viewportRender.d.ts.map +1 -0
  132. package/dist/layout/viewportRender.js +337 -0
  133. package/dist/layout/viewportRender.js.map +1 -0
  134. package/dist/layout/viewportUtils.d.ts +9 -0
  135. package/dist/layout/viewportUtils.d.ts.map +1 -0
  136. package/dist/layout/viewportUtils.js +40 -0
  137. package/dist/layout/viewportUtils.js.map +1 -0
  138. package/dist/layout/warnings.d.ts +2 -0
  139. package/dist/layout/warnings.d.ts.map +1 -0
  140. package/dist/layout/warnings.js +4 -0
  141. package/dist/layout/warnings.js.map +1 -0
  142. package/dist/printToPdf.d.ts +38 -0
  143. package/dist/printToPdf.d.ts.map +1 -0
  144. package/dist/printToPdf.js +79 -0
  145. package/dist/printToPdf.js.map +1 -0
  146. package/dist/printToSvg.d.ts +11 -0
  147. package/dist/printToSvg.d.ts.map +1 -0
  148. package/dist/printToSvg.js +168 -0
  149. package/dist/printToSvg.js.map +1 -0
  150. package/dist/sections.d.ts +118 -0
  151. package/dist/sections.d.ts.map +1 -0
  152. package/dist/sections.js +839 -0
  153. package/dist/sections.js.map +1 -0
  154. package/dist/templates/default-template.json +137 -0
  155. package/dist/types.d.ts +54 -0
  156. package/dist/types.d.ts.map +1 -0
  157. package/dist/types.js +11 -0
  158. package/dist/types.js.map +1 -0
  159. package/package.json +47 -0
  160. package/src/BuildInfo.ts +41 -0
  161. package/src/index.ts +10 -0
  162. package/src/layout/analyzeTemplate.ts +218 -0
  163. package/src/layout/clipUtils.ts +193 -0
  164. package/src/layout/colorConstants.ts +16 -0
  165. package/src/layout/elementKinds.ts +35 -0
  166. package/src/layout/exportDrawing.ts +206 -0
  167. package/src/layout/filters.ts +80 -0
  168. package/src/layout/geometryConstants.ts +11 -0
  169. package/src/layout/index.ts +671 -0
  170. package/src/layout/itemBuilders.ts +159 -0
  171. package/src/layout/labelsRender.ts +170 -0
  172. package/src/layout/legendConstants.ts +11 -0
  173. package/src/layout/legendRender.ts +543 -0
  174. package/src/layout/legendRows.ts +62 -0
  175. package/src/layout/legendUtils.ts +144 -0
  176. package/src/layout/metadata.ts +210 -0
  177. package/src/layout/model.ts +372 -0
  178. package/src/layout/options.ts +17 -0
  179. package/src/layout/presets.ts +51 -0
  180. package/src/layout/qrRender.ts +126 -0
  181. package/src/layout/qrcode.d.ts +2 -0
  182. package/src/layout/renderDrawing.ts +710 -0
  183. package/src/layout/renderUtils.ts +138 -0
  184. package/src/layout/sectionValidation.ts +2 -0
  185. package/src/layout/templateVariables.ts +317 -0
  186. package/src/layout/typeGuards.ts +9 -0
  187. package/src/layout/types.ts +835 -0
  188. package/src/layout/utils.ts +85 -0
  189. package/src/layout/validation.ts +392 -0
  190. package/src/layout/validationHelpers.ts +59 -0
  191. package/src/layout/validationUtils.ts +6 -0
  192. package/src/layout/viewportPrep.ts +351 -0
  193. package/src/layout/viewportRender.ts +442 -0
  194. package/src/layout/viewportUtils.ts +50 -0
  195. package/src/layout/warnings.ts +3 -0
  196. package/src/printToPdf.ts +108 -0
  197. package/src/printToSvg.ts +188 -0
  198. package/src/sections.ts +1019 -0
  199. package/src/templates/default-template.json +137 -0
  200. package/src/types.ts +49 -0
  201. package/templates/default-a0-landscape.json +209 -0
  202. package/templates/default-a0-portrait.json +195 -0
  203. package/templates/default-a1-landscape.json +167 -0
  204. package/templates/default-a1-portrait.json +153 -0
  205. package/templates/default-a3-landscape.json +111 -0
  206. package/templates/default-a3-portrait.json +125 -0
  207. package/templates/default-a4-landscape.json +97 -0
  208. package/templates/default-a4-portrait.json +97 -0
  209. package/templates/template-schema.json +1395 -0
@@ -0,0 +1,138 @@
1
+ import { isRecord } from './typeGuards';
2
+
3
+ export const escapeAttr = (value: unknown): string =>
4
+ String(value).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
5
+
6
+ export const escapeText = (value: unknown): string =>
7
+ String(value).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
8
+
9
+ export const getPathValue = (source: unknown, path: string): unknown => {
10
+ const parts = path
11
+ .split('.')
12
+ .map((p) => p.trim())
13
+ .filter(Boolean);
14
+ let current: unknown = source;
15
+ for (const part of parts) {
16
+ if (!isRecord(current)) {
17
+ return undefined;
18
+ }
19
+ current = current[part];
20
+ }
21
+ return current;
22
+ };
23
+
24
+ export const interpolate = (template: string, data: any, warnings?: string[]): string =>
25
+ template.replace(/\{\{\s*([^}]+?)\s*\}\}/g, (match, path) => {
26
+ const value = getPathValue(data, path);
27
+ if (value === undefined || value === null) {
28
+ if (warnings) {
29
+ warnings.push(`Unresolved template variable '${path}' in "${template}".`);
30
+ }
31
+ return '';
32
+ }
33
+ if (typeof value === 'object') {
34
+ if (warnings) {
35
+ warnings.push(
36
+ `Template variable '${path}' resolved to an object instead of a string in "${template}".`
37
+ );
38
+ }
39
+ return '';
40
+ }
41
+ return String(value);
42
+ });
43
+
44
+ export const interpolateWithSources = (template: string, sources: any[]): string =>
45
+ template.replace(/\{\{\s*([^}]+?)\s*\}\}/g, (_match, path) => {
46
+ for (const source of sources) {
47
+ const value = getPathValue(source, path);
48
+ if (value !== undefined && value !== null && typeof value !== 'object') {
49
+ return String(value);
50
+ }
51
+ }
52
+ return '';
53
+ });
54
+
55
+ // colorFromString moved to utils.ts to avoid duplication
56
+ // Import from utils.ts if needed: import { colorFromString } from './utils';
57
+
58
+ /**
59
+ * Resolve a short property path from an item's propertySets.
60
+ *
61
+ * Template paths use the format: SetName.propertyName
62
+ * Example: "SetName.propName" resolves to item.propertySets.SetName.propName.value
63
+ *
64
+ * @param item - An object with propertySets structure
65
+ * @param shortPath - Short path like "SetName.propName"
66
+ * @returns The property value, or undefined if not found
67
+ */
68
+ export const resolveProperty = (item: unknown, shortPath: string): unknown => {
69
+ if (!shortPath || !isRecord(item)) return undefined;
70
+
71
+ const propertySets = (item as Record<string, unknown>).propertySets;
72
+ if (!isRecord(propertySets)) return undefined;
73
+
74
+ const parts = shortPath.split('.');
75
+ if (parts.length < 2) return undefined;
76
+
77
+ const [setName, ...propParts] = parts;
78
+ const propName = propParts.join('.');
79
+
80
+ const propSet = propertySets[setName];
81
+ if (!isRecord(propSet)) return undefined;
82
+
83
+ // New T8 API: T8PropertySet has .properties sub-record
84
+ const innerProps = propSet['properties'];
85
+ const prop = (isRecord(innerProps) ? innerProps[propName] : undefined) ?? propSet[propName];
86
+
87
+ // Handle both wrapped {value} and raw values
88
+ if (isRecord(prop) && 'value' in prop) {
89
+ return prop.value;
90
+ }
91
+ return prop;
92
+ };
93
+
94
+ /**
95
+ * Resolve a property value using either short or full path.
96
+ * Short path: "SetName.propName" → resolves via propertySets
97
+ * Full path: "propertySets.SetName.propName.value" → resolves via direct traversal
98
+ * Also handles top-level properties like "color".
99
+ */
100
+ export const resolvePath = (obj: unknown, path: string): unknown => {
101
+ if (path.startsWith('propertySets.')) {
102
+ return getPathValue(obj, path);
103
+ }
104
+ const direct = getPathValue(obj, path);
105
+ if (direct !== undefined) return direct;
106
+ return resolveProperty(obj, path);
107
+ };
108
+
109
+ /**
110
+ * Check if an item matches a filter condition using short property paths.
111
+ *
112
+ * @param item - An object with propertySets structure
113
+ * @param filter - Filter with path (short format), equals, or in conditions
114
+ * @returns true if item matches the filter
115
+ */
116
+ export const matchesPropertyFilter = (
117
+ item: unknown,
118
+ filter: { path?: string; equals?: unknown; in?: unknown[] } | undefined
119
+ ): boolean => {
120
+ if (!filter || !filter.path) return true;
121
+
122
+ const value = resolveProperty(item, filter.path);
123
+ const normalizedValue = typeof value === 'string' ? value.toLowerCase().trim() : value;
124
+
125
+ if (filter.equals !== undefined) {
126
+ const normalizedEquals = typeof filter.equals === 'string' ? filter.equals.toLowerCase().trim() : filter.equals;
127
+ return normalizedValue === normalizedEquals;
128
+ }
129
+
130
+ if (Array.isArray(filter.in)) {
131
+ return filter.in.some((v) => {
132
+ const normalizedV = typeof v === 'string' ? v.toLowerCase().trim() : v;
133
+ return normalizedValue === normalizedV;
134
+ });
135
+ }
136
+
137
+ return true;
138
+ };
@@ -0,0 +1,2 @@
1
+ export { validateSectionState } from '@twinfinity/geometry';
2
+ export type { SectionStateValidationResult } from '@twinfinity/geometry';
@@ -0,0 +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
+ }
@@ -0,0 +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
+ };