@sneat/structured-data 0.0.0-stage → 0.27.29
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/README.md +29 -2
- package/assets/hcl-worker.mjs +135 -0
- package/assets/tree-sitter-hcl.LICENSE +201 -0
- package/assets/tree-sitter-hcl.wasm +0 -0
- package/assets/web-tree-sitter.LICENSE +21 -0
- package/assets/web-tree-sitter.js +4081 -0
- package/assets/web-tree-sitter.wasm +0 -0
- package/fesm2022/sneat-structured-data-core.mjs +692 -0
- package/fesm2022/sneat-structured-data-core.mjs.map +1 -0
- package/fesm2022/sneat-structured-data.mjs +1163 -0
- package/fesm2022/sneat-structured-data.mjs.map +1 -0
- package/package.json +32 -3
- package/tsconfig.lib.prod.tsbuildinfo +1 -0
- package/types/sneat-structured-data-core.d.ts +253 -0
- package/types/sneat-structured-data.d.ts +327 -0
|
@@ -0,0 +1,1163 @@
|
|
|
1
|
+
import { parseTree } from 'jsonc-parser';
|
|
2
|
+
import { parseDocument, isAlias, isScalar, isSeq, isMap } from 'yaml';
|
|
3
|
+
import * as i1 from '@angular/common';
|
|
4
|
+
import { CommonModule } from '@angular/common';
|
|
5
|
+
import * as i0 from '@angular/core';
|
|
6
|
+
import { EventEmitter, Output, Input, Component, forwardRef, inject, ElementRef } from '@angular/core';
|
|
7
|
+
import * as i2 from 'primeng/accordion';
|
|
8
|
+
import { AccordionModule } from 'primeng/accordion';
|
|
9
|
+
import * as i3 from 'primeng/button';
|
|
10
|
+
import { ButtonModule } from 'primeng/button';
|
|
11
|
+
import * as i4 from 'primeng/table';
|
|
12
|
+
import { TableModule } from 'primeng/table';
|
|
13
|
+
|
|
14
|
+
const DEFAULT_LIMITS = {
|
|
15
|
+
maxSourceBytes: 2_000_000,
|
|
16
|
+
maxNodes: 30_000,
|
|
17
|
+
maxDepth: 64,
|
|
18
|
+
maxRows: 2_000,
|
|
19
|
+
maxConfigBytes: 64_000,
|
|
20
|
+
maxRules: 100,
|
|
21
|
+
maxSections: 300,
|
|
22
|
+
maxSelectorLength: 256,
|
|
23
|
+
maxSelectorDepth: 16,
|
|
24
|
+
maxYamlAliases: 0,
|
|
25
|
+
timeoutMs: 2_000,
|
|
26
|
+
};
|
|
27
|
+
function effectiveLimits(overrides) {
|
|
28
|
+
const result = { ...DEFAULT_LIMITS };
|
|
29
|
+
if (overrides) {
|
|
30
|
+
for (const key of Object.keys(DEFAULT_LIMITS)) {
|
|
31
|
+
const value = overrides[key];
|
|
32
|
+
if (value !== undefined) {
|
|
33
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > DEFAULT_LIMITS[key]) {
|
|
34
|
+
throw new Error(`Invalid ${key} limit`);
|
|
35
|
+
}
|
|
36
|
+
result[key] = value;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
function sourceByteLength(text) {
|
|
43
|
+
return new TextEncoder().encode(text).length;
|
|
44
|
+
}
|
|
45
|
+
function exceedsSourceLimit(text, maxBytes) {
|
|
46
|
+
return text.length > maxBytes || sourceByteLength(text) > maxBytes;
|
|
47
|
+
}
|
|
48
|
+
function diagnostic(code, message, range, source) {
|
|
49
|
+
return { code, message, severity: 'error', ...(range ? { range } : {}), ...(source ? { source } : {}) };
|
|
50
|
+
}
|
|
51
|
+
/** Tree-sitter and JS parser offsets are UTF-16 code-unit offsets. */
|
|
52
|
+
class SourceLocator {
|
|
53
|
+
constructor(source) {
|
|
54
|
+
this.source = source;
|
|
55
|
+
this.lineStarts = [0];
|
|
56
|
+
for (let index = 0; index < source.length; index++) {
|
|
57
|
+
if (source[index] === '\r') {
|
|
58
|
+
if (source[index + 1] === '\n')
|
|
59
|
+
index++;
|
|
60
|
+
this.lineStarts.push(index + 1);
|
|
61
|
+
}
|
|
62
|
+
else if (source[index] === '\n') {
|
|
63
|
+
this.lineStarts.push(index + 1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
range(startOffset, endOffset) {
|
|
68
|
+
return {
|
|
69
|
+
startOffset,
|
|
70
|
+
endOffset,
|
|
71
|
+
start: this.position(startOffset),
|
|
72
|
+
end: this.position(endOffset),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
position(offset) {
|
|
76
|
+
let low = 0;
|
|
77
|
+
let high = this.lineStarts.length;
|
|
78
|
+
while (low + 1 < high) {
|
|
79
|
+
const middle = (low + high) >>> 1;
|
|
80
|
+
if (this.lineStarts[middle] <= offset)
|
|
81
|
+
low = middle;
|
|
82
|
+
else
|
|
83
|
+
high = middle;
|
|
84
|
+
}
|
|
85
|
+
return { line: low, column: offset - this.lineStarts[low] };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function parseJsonText(sourceText, limits) {
|
|
90
|
+
let bounded;
|
|
91
|
+
try {
|
|
92
|
+
bounded = effectiveLimits(limits);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
return { ok: false, diagnostics: [diagnostic('limit_exceeded', String(error))] };
|
|
96
|
+
}
|
|
97
|
+
if (exceedsSourceLimit(sourceText, bounded.maxSourceBytes)) {
|
|
98
|
+
return { ok: false, diagnostics: [diagnostic('limit_exceeded', `JSON source exceeds ${bounded.maxSourceBytes} bytes`)] };
|
|
99
|
+
}
|
|
100
|
+
const errors = [];
|
|
101
|
+
let root;
|
|
102
|
+
try {
|
|
103
|
+
root = parseTree(sourceText, errors, { disallowComments: true, allowTrailingComma: false });
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return { ok: false, diagnostics: [diagnostic('limit_exceeded', 'JSON parser could not process this source safely')] };
|
|
107
|
+
}
|
|
108
|
+
const locator = new SourceLocator(sourceText);
|
|
109
|
+
if (!root || errors.length) {
|
|
110
|
+
return { ok: false, diagnostics: errors.length
|
|
111
|
+
? errors.map(error => diagnostic('invalid_syntax', `Invalid JSON (${error.error})`, locator.range(error.offset, error.offset + error.length)))
|
|
112
|
+
: [diagnostic('invalid_syntax', 'JSON document is empty')] };
|
|
113
|
+
}
|
|
114
|
+
let count = 0;
|
|
115
|
+
try {
|
|
116
|
+
const convert = (node, depth) => {
|
|
117
|
+
if (++count > bounded.maxNodes || depth > bounded.maxDepth)
|
|
118
|
+
throw new Error('JSON node or depth limit exceeded');
|
|
119
|
+
const range = locator.range(node.offset, node.offset + node.length);
|
|
120
|
+
switch (node.type) {
|
|
121
|
+
case 'null': return { kind: 'null', range };
|
|
122
|
+
case 'boolean': return { kind: 'boolean', value: node.value, range };
|
|
123
|
+
case 'string': return { kind: 'string', value: node.value, range };
|
|
124
|
+
case 'number': {
|
|
125
|
+
const sourceNumber = sourceText.slice(node.offset, node.offset + node.length);
|
|
126
|
+
const parsedNumber = Number(sourceNumber);
|
|
127
|
+
const losslessInteger = !Number.isInteger(parsedNumber) || Number.isSafeInteger(parsedNumber);
|
|
128
|
+
return { kind: 'number', sourceText: sourceNumber,
|
|
129
|
+
...(Number.isFinite(parsedNumber) && losslessInteger ? { value: parsedNumber } : {}), range };
|
|
130
|
+
}
|
|
131
|
+
case 'array': return { kind: 'array', items: (node.children ?? []).map(child => convert(child, depth + 1)), range };
|
|
132
|
+
case 'object': {
|
|
133
|
+
const entries = [];
|
|
134
|
+
const seen = new Set();
|
|
135
|
+
for (const property of node.children ?? []) {
|
|
136
|
+
const keyNode = property.children?.[0];
|
|
137
|
+
const valueNode = property.children?.[1];
|
|
138
|
+
if (!keyNode || !valueNode || typeof keyNode.value !== 'string')
|
|
139
|
+
throw new Error('Invalid JSON property');
|
|
140
|
+
const key = keyNode.value;
|
|
141
|
+
if (seen.has(key))
|
|
142
|
+
throw new Error(`Duplicate JSON key: ${key}`);
|
|
143
|
+
seen.add(key);
|
|
144
|
+
entries.push({ key, value: convert(valueNode, depth + 1), range: locator.range(property.offset, property.offset + property.length) });
|
|
145
|
+
}
|
|
146
|
+
return { kind: 'object', entries, range };
|
|
147
|
+
}
|
|
148
|
+
default: throw new Error(`Unsupported JSON node: ${node.type}`);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
return { ok: true, document: { format: 'json', root: convert(root, 0), sourceText }, diagnostics: [] };
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
const message = String(error);
|
|
155
|
+
return { ok: false, diagnostics: [diagnostic(message.includes('limit') ? 'limit_exceeded' : 'invalid_syntax', message)] };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function parseYamlText(sourceText, limits) {
|
|
160
|
+
let bounded;
|
|
161
|
+
try {
|
|
162
|
+
bounded = effectiveLimits(limits);
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
return { ok: false, diagnostics: [diagnostic('limit_exceeded', String(error))] };
|
|
166
|
+
}
|
|
167
|
+
if (exceedsSourceLimit(sourceText, bounded.maxSourceBytes)) {
|
|
168
|
+
return { ok: false, diagnostics: [diagnostic('limit_exceeded', `YAML source exceeds ${bounded.maxSourceBytes} bytes`)] };
|
|
169
|
+
}
|
|
170
|
+
const locator = new SourceLocator(sourceText);
|
|
171
|
+
let yaml;
|
|
172
|
+
try {
|
|
173
|
+
yaml = parseDocument(sourceText, { uniqueKeys: true, strict: true });
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return { ok: false, diagnostics: [diagnostic('limit_exceeded', 'YAML parser could not process this source safely')] };
|
|
177
|
+
}
|
|
178
|
+
if (yaml.errors.length) {
|
|
179
|
+
return { ok: false, diagnostics: yaml.errors.map(error => diagnostic('invalid_syntax', error.message, error.pos ? locator.range(error.pos[0], error.pos[1]) : undefined)) };
|
|
180
|
+
}
|
|
181
|
+
let count = 0;
|
|
182
|
+
const rangeOf = (node) => node?.range
|
|
183
|
+
? locator.range(node.range[0], node.range[1]) : undefined;
|
|
184
|
+
try {
|
|
185
|
+
const convert = (node, depth) => {
|
|
186
|
+
if (++count > bounded.maxNodes || depth > bounded.maxDepth)
|
|
187
|
+
throw new Error('YAML node or depth limit exceeded');
|
|
188
|
+
if (node === null)
|
|
189
|
+
return { kind: 'null' };
|
|
190
|
+
if (isAlias(node))
|
|
191
|
+
throw new Error('YAML aliases are unsupported in structured view');
|
|
192
|
+
if (node.tag && !node.tag.startsWith('tag:yaml.org,2002:'))
|
|
193
|
+
throw new Error(`Unsupported YAML tag: ${node.tag}`);
|
|
194
|
+
const range = rangeOf(node);
|
|
195
|
+
if (isScalar(node)) {
|
|
196
|
+
const value = node.value;
|
|
197
|
+
if (value === null || value === undefined)
|
|
198
|
+
return { kind: 'null', range };
|
|
199
|
+
if (typeof value === 'boolean')
|
|
200
|
+
return { kind: 'boolean', value, range };
|
|
201
|
+
if (typeof value === 'number') {
|
|
202
|
+
const sourceNumber = range ? sourceText.slice(range.startOffset, range.endOffset) : String(value);
|
|
203
|
+
return { kind: 'number', sourceText: sourceNumber,
|
|
204
|
+
...(Number.isFinite(value) && (!Number.isInteger(value) || Number.isSafeInteger(value)) ? { value } : {}), range };
|
|
205
|
+
}
|
|
206
|
+
if (typeof value === 'string')
|
|
207
|
+
return { kind: 'string', value, range };
|
|
208
|
+
throw new Error('Unsupported YAML scalar');
|
|
209
|
+
}
|
|
210
|
+
if (isSeq(node))
|
|
211
|
+
return { kind: 'array', items: node.items.map(item => convert(item, depth + 1)), range };
|
|
212
|
+
if (isMap(node)) {
|
|
213
|
+
const entries = [];
|
|
214
|
+
const seen = new Set();
|
|
215
|
+
for (const pair of node.items) {
|
|
216
|
+
if (!isScalar(pair.key) || typeof pair.key.value !== 'string')
|
|
217
|
+
throw new Error('YAML mapping keys must be strings');
|
|
218
|
+
const key = pair.key.value;
|
|
219
|
+
if (seen.has(key))
|
|
220
|
+
throw new Error(`Duplicate YAML key: ${key}`);
|
|
221
|
+
seen.add(key);
|
|
222
|
+
entries.push({ key, value: convert(pair.value, depth + 1), range: rangeOf(pair.key) });
|
|
223
|
+
}
|
|
224
|
+
return { kind: 'object', entries, range };
|
|
225
|
+
}
|
|
226
|
+
throw new Error('Unsupported YAML node');
|
|
227
|
+
};
|
|
228
|
+
return { ok: true, document: { format: 'yaml', root: convert(yaml.contents, 0), sourceText }, diagnostics: [] };
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
const message = String(error);
|
|
232
|
+
const code = message.includes('limit') ? 'limit_exceeded' : 'unsupported_construct';
|
|
233
|
+
return { ok: false, diagnostics: [diagnostic(code, message)] };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Loads only same-origin packaged assets, and only when invoked in a browser. */
|
|
238
|
+
async function parseHclText(sourceText, limits) {
|
|
239
|
+
let bounded;
|
|
240
|
+
try {
|
|
241
|
+
bounded = effectiveLimits(limits);
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
return { ok: false, diagnostics: [diagnostic('limit_exceeded', String(error))] };
|
|
245
|
+
}
|
|
246
|
+
if (exceedsSourceLimit(sourceText, bounded.maxSourceBytes)) {
|
|
247
|
+
return { ok: false, diagnostics: [diagnostic('limit_exceeded', `HCL source exceeds ${bounded.maxSourceBytes} bytes`)] };
|
|
248
|
+
}
|
|
249
|
+
if (typeof Worker === 'undefined') {
|
|
250
|
+
return { ok: false, diagnostics: [diagnostic('parser_unavailable', 'HCL parsing requires a browser Worker')] };
|
|
251
|
+
}
|
|
252
|
+
let worker;
|
|
253
|
+
try {
|
|
254
|
+
worker = new Worker(new URL('../assets/hcl-worker.mjs', import.meta.url), { type: 'module' });
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
return { ok: false, diagnostics: [diagnostic('parser_unavailable', `HCL Worker could not start: ${String(error)}`)] };
|
|
258
|
+
}
|
|
259
|
+
return new Promise(resolve => {
|
|
260
|
+
let settled = false;
|
|
261
|
+
const finish = (result) => {
|
|
262
|
+
if (settled)
|
|
263
|
+
return;
|
|
264
|
+
settled = true;
|
|
265
|
+
clearTimeout(timer);
|
|
266
|
+
worker.terminate();
|
|
267
|
+
resolve(result);
|
|
268
|
+
};
|
|
269
|
+
const timer = setTimeout(() => finish({ ok: false,
|
|
270
|
+
diagnostics: [diagnostic('limit_exceeded', 'HCL parsing exceeded its hard deadline')] }), bounded.timeoutMs + 1_000);
|
|
271
|
+
worker.onmessage = (event) => {
|
|
272
|
+
const reply = event.data;
|
|
273
|
+
if (!reply || reply.id !== 1)
|
|
274
|
+
return;
|
|
275
|
+
if (reply.ok && reply.root)
|
|
276
|
+
finish({ ok: true,
|
|
277
|
+
document: { format: 'hcl', root: reply.root, sourceText }, diagnostics: [] });
|
|
278
|
+
else
|
|
279
|
+
finish({ ok: false, diagnostics: reply.diagnostics?.length
|
|
280
|
+
? reply.diagnostics : [diagnostic('invalid_syntax', 'HCL parser returned no document')] });
|
|
281
|
+
};
|
|
282
|
+
worker.onerror = () => finish({ ok: false,
|
|
283
|
+
diagnostics: [diagnostic('parser_unavailable', 'HCL Worker failed to load its packaged assets')] });
|
|
284
|
+
worker.postMessage({ id: 1, sourceText, limits: bounded });
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** RFC 6901 without wildcards, evaluation, or prototype traversal. */
|
|
289
|
+
function parseRelativePointer(pointer, maxLength = DEFAULT_LIMITS.maxSelectorLength, maxDepth = DEFAULT_LIMITS.maxSelectorDepth) {
|
|
290
|
+
if (pointer === '')
|
|
291
|
+
return [];
|
|
292
|
+
if (!pointer.startsWith('/') || pointer.length > maxLength)
|
|
293
|
+
return null;
|
|
294
|
+
const parts = pointer.slice(1).split('/');
|
|
295
|
+
if (parts.length > maxDepth)
|
|
296
|
+
return null;
|
|
297
|
+
const decoded = [];
|
|
298
|
+
for (const part of parts) {
|
|
299
|
+
if (/~(?![01])/.test(part))
|
|
300
|
+
return null;
|
|
301
|
+
const value = part.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
302
|
+
if (value === '__proto__' || value === 'prototype' || value === 'constructor')
|
|
303
|
+
return null;
|
|
304
|
+
decoded.push(value);
|
|
305
|
+
}
|
|
306
|
+
return decoded;
|
|
307
|
+
}
|
|
308
|
+
const missing = { found: false };
|
|
309
|
+
function selectRelative(node, pointer) {
|
|
310
|
+
const parts = parseRelativePointer(pointer);
|
|
311
|
+
if (parts === null)
|
|
312
|
+
return missing;
|
|
313
|
+
let current = node;
|
|
314
|
+
for (let index = 0; index < parts.length; index++) {
|
|
315
|
+
const part = parts[index];
|
|
316
|
+
if (current.kind === 'hcl-block')
|
|
317
|
+
current = current.body;
|
|
318
|
+
if (current.kind === 'hcl-attribute')
|
|
319
|
+
current = current.value;
|
|
320
|
+
if (current.kind === 'hcl-body') {
|
|
321
|
+
const body = current;
|
|
322
|
+
if (part === 'attributes' && index + 1 < parts.length) {
|
|
323
|
+
const name = parts[++index];
|
|
324
|
+
const found = body.members.find(member => member.kind === 'hcl-attribute' && member.name === name);
|
|
325
|
+
if (!found || found.kind !== 'hcl-attribute')
|
|
326
|
+
return missing;
|
|
327
|
+
current = found.value;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (part === 'blocks' && index + 1 < parts.length) {
|
|
331
|
+
const position = Number(parts[++index]);
|
|
332
|
+
if (!Number.isSafeInteger(position) || position < 0 || String(position) !== parts[index])
|
|
333
|
+
return missing;
|
|
334
|
+
const found = body.members.filter(member => member.kind === 'hcl-block')[position];
|
|
335
|
+
if (!found)
|
|
336
|
+
return missing;
|
|
337
|
+
current = found;
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
return missing;
|
|
341
|
+
}
|
|
342
|
+
if (current.kind === 'object') {
|
|
343
|
+
const found = current.entries.find(entry => entry.key === part);
|
|
344
|
+
if (!found)
|
|
345
|
+
return missing;
|
|
346
|
+
current = found.value;
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (current.kind === 'array') {
|
|
350
|
+
const position = Number(part);
|
|
351
|
+
if (!Number.isSafeInteger(position) || position < 0 || String(position) !== part)
|
|
352
|
+
return missing;
|
|
353
|
+
const found = current.items[position];
|
|
354
|
+
if (!found)
|
|
355
|
+
return missing;
|
|
356
|
+
current = found;
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
return missing;
|
|
360
|
+
}
|
|
361
|
+
return { found: true, value: current };
|
|
362
|
+
}
|
|
363
|
+
function selectViewPath(context, path) {
|
|
364
|
+
if (path === '$key')
|
|
365
|
+
return context.key === undefined ? missing
|
|
366
|
+
: { found: true, value: { kind: 'string', value: context.key } };
|
|
367
|
+
if (path.startsWith('$label/')) {
|
|
368
|
+
const position = Number(path.slice('$label/'.length));
|
|
369
|
+
if (!Number.isSafeInteger(position) || position < 0 || String(position) !== path.slice('$label/'.length))
|
|
370
|
+
return missing;
|
|
371
|
+
const value = context.labels?.[position];
|
|
372
|
+
return value === undefined ? missing : { found: true, value: { kind: 'string', value } };
|
|
373
|
+
}
|
|
374
|
+
return selectRelative(context.node, path);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function record(value) {
|
|
378
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
379
|
+
return null;
|
|
380
|
+
const prototype = Object.getPrototypeOf(value);
|
|
381
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
382
|
+
return null;
|
|
383
|
+
for (const property of Object.values(Object.getOwnPropertyDescriptors(value))) {
|
|
384
|
+
if (!('value' in property))
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
return value;
|
|
388
|
+
}
|
|
389
|
+
function exactKeys(value, allowed) {
|
|
390
|
+
return Object.keys(value).every(key => allowed.includes(key));
|
|
391
|
+
}
|
|
392
|
+
function text(value, max = 256) {
|
|
393
|
+
return typeof value === 'string' && value.length > 0 && value.length <= max;
|
|
394
|
+
}
|
|
395
|
+
function pointer(value, limits, allowSpecial = false) {
|
|
396
|
+
if (typeof value !== 'string')
|
|
397
|
+
return false;
|
|
398
|
+
if (allowSpecial && (value === '$key' || /^\$label\/(0|[1-9]\d{0,2})$/.test(value)))
|
|
399
|
+
return true;
|
|
400
|
+
return parseRelativePointer(value, limits.maxSelectorLength, limits.maxSelectorDepth) !== null;
|
|
401
|
+
}
|
|
402
|
+
function safeGlob(value, limits) {
|
|
403
|
+
return text(value, limits.maxSelectorLength) && !value.startsWith('/') && !value.includes('..')
|
|
404
|
+
&& /^[A-Za-z0-9_./*?@-]+$/.test(value);
|
|
405
|
+
}
|
|
406
|
+
function parseTitle(value, limits) {
|
|
407
|
+
const source = record(value);
|
|
408
|
+
if (!source || !exactKeys(source, ['label', 'path']) || !text(source['label']) || !pointer(source['path'], limits))
|
|
409
|
+
return null;
|
|
410
|
+
return { label: source['label'], path: source['path'] };
|
|
411
|
+
}
|
|
412
|
+
function parseFields(value, limits) {
|
|
413
|
+
if (!Array.isArray(value) || value.length > 64)
|
|
414
|
+
return null;
|
|
415
|
+
const result = [];
|
|
416
|
+
for (const item of value) {
|
|
417
|
+
const field = record(item);
|
|
418
|
+
if (!field || !exactKeys(field, ['label', 'path', 'scalarListLayout']) || !text(field['label'])
|
|
419
|
+
|| !pointer(field['path'], limits) || (field['scalarListLayout'] !== undefined
|
|
420
|
+
&& field['scalarListLayout'] !== 'inline' && field['scalarListLayout'] !== 'list'))
|
|
421
|
+
return null;
|
|
422
|
+
result.push({ label: field['label'], path: field['path'],
|
|
423
|
+
...(field['scalarListLayout'] ? { scalarListLayout: field['scalarListLayout'] } : {}) });
|
|
424
|
+
}
|
|
425
|
+
return result;
|
|
426
|
+
}
|
|
427
|
+
function parseColumns(value, limits) {
|
|
428
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > 64)
|
|
429
|
+
return null;
|
|
430
|
+
const result = [];
|
|
431
|
+
for (const item of value) {
|
|
432
|
+
const column = record(item);
|
|
433
|
+
if (!column || !exactKeys(column, ['label', 'path']) || !text(column['label'])
|
|
434
|
+
|| !pointer(column['path'], limits, true))
|
|
435
|
+
return null;
|
|
436
|
+
result.push({ label: column['label'], path: column['path'] });
|
|
437
|
+
}
|
|
438
|
+
return result;
|
|
439
|
+
}
|
|
440
|
+
function parseSections(value, limits, counter, depth = 0) {
|
|
441
|
+
if (!Array.isArray(value) || depth > limits.maxDepth || value.length > limits.maxSections)
|
|
442
|
+
return null;
|
|
443
|
+
const result = [];
|
|
444
|
+
const ids = new Set();
|
|
445
|
+
for (const item of value) {
|
|
446
|
+
if (++counter.count > limits.maxSections)
|
|
447
|
+
return null;
|
|
448
|
+
const section = record(item);
|
|
449
|
+
if (!section || !text(section['id'], 100) || !/^[A-Za-z0-9_-]+$/.test(section['id'])
|
|
450
|
+
|| ids.has(section['id']) || !text(section['title']) || !pointer(section['path'], limits))
|
|
451
|
+
return null;
|
|
452
|
+
ids.add(section['id']);
|
|
453
|
+
const base = { id: section['id'], title: section['title'], path: section['path'] };
|
|
454
|
+
if (section['kind'] === 'fields') {
|
|
455
|
+
if (!exactKeys(section, ['id', 'title', 'kind', 'path', 'fields']))
|
|
456
|
+
return null;
|
|
457
|
+
const fields = parseFields(section['fields'], limits);
|
|
458
|
+
if (!fields)
|
|
459
|
+
return null;
|
|
460
|
+
result.push({ ...base, kind: 'fields', fields });
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
if (section['kind'] === 'value') {
|
|
464
|
+
if (!exactKeys(section, ['id', 'title', 'kind', 'path', 'scalarListLayout'])
|
|
465
|
+
|| (section['scalarListLayout'] !== undefined && section['scalarListLayout'] !== 'inline'
|
|
466
|
+
&& section['scalarListLayout'] !== 'list'))
|
|
467
|
+
return null;
|
|
468
|
+
result.push({ ...base, kind: 'value',
|
|
469
|
+
...(section['scalarListLayout'] ? { scalarListLayout: section['scalarListLayout'] } : {}) });
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
if (section['kind'] === 'table') {
|
|
473
|
+
if (!exactKeys(section, ['id', 'title', 'kind', 'path', 'rows', 'blockType', 'columns'])
|
|
474
|
+
|| !['dictionary', 'list', 'blocks'].includes(String(section['rows']))
|
|
475
|
+
|| (section['blockType'] !== undefined && !text(section['blockType'], 100)))
|
|
476
|
+
return null;
|
|
477
|
+
const columns = parseColumns(section['columns'], limits);
|
|
478
|
+
if (!columns)
|
|
479
|
+
return null;
|
|
480
|
+
result.push({ ...base, kind: 'table', rows: section['rows'], columns,
|
|
481
|
+
...(section['blockType'] ? { blockType: section['blockType'] } : {}) });
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
if (section['kind'] === 'entries' || section['kind'] === 'blocks') {
|
|
485
|
+
const blocks = section['kind'] === 'blocks';
|
|
486
|
+
if (!exactKeys(section, blocks
|
|
487
|
+
? ['id', 'title', 'kind', 'path', 'blockType', 'presentation', 'allowLayoutSwitch', 'initiallyExpanded', 'entry']
|
|
488
|
+
: ['id', 'title', 'kind', 'path', 'presentation', 'allowLayoutSwitch', 'initiallyExpanded', 'entry']))
|
|
489
|
+
return null;
|
|
490
|
+
if (blocks && !text(section['blockType'], 100))
|
|
491
|
+
return null;
|
|
492
|
+
if (section['presentation'] !== undefined && section['presentation'] !== 'list' && section['presentation'] !== 'accordion')
|
|
493
|
+
return null;
|
|
494
|
+
if (section['allowLayoutSwitch'] !== undefined && typeof section['allowLayoutSwitch'] !== 'boolean')
|
|
495
|
+
return null;
|
|
496
|
+
if (section['initiallyExpanded'] !== undefined && !['first', 'all', 'none'].includes(String(section['initiallyExpanded'])))
|
|
497
|
+
return null;
|
|
498
|
+
let entry;
|
|
499
|
+
if (section['entry'] !== undefined) {
|
|
500
|
+
const rawEntry = record(section['entry']);
|
|
501
|
+
if (!rawEntry || !exactKeys(rawEntry, ['heading', 'sections']) || !text(rawEntry['heading'])
|
|
502
|
+
|| !pointer(rawEntry['heading'], limits, true))
|
|
503
|
+
return null;
|
|
504
|
+
const nested = rawEntry['sections'] === undefined ? undefined
|
|
505
|
+
: parseSections(rawEntry['sections'], limits, counter, depth + 1);
|
|
506
|
+
if (rawEntry['sections'] !== undefined && !nested)
|
|
507
|
+
return null;
|
|
508
|
+
entry = { heading: rawEntry['heading'], ...(nested ? { sections: nested } : {}) };
|
|
509
|
+
}
|
|
510
|
+
const common = { ...base,
|
|
511
|
+
...(section['presentation'] ? { presentation: section['presentation'] } : {}),
|
|
512
|
+
...(section['allowLayoutSwitch'] !== undefined ? { allowLayoutSwitch: section['allowLayoutSwitch'] } : {}),
|
|
513
|
+
...(section['initiallyExpanded'] ? { initiallyExpanded: section['initiallyExpanded'] } : {}),
|
|
514
|
+
...(entry ? { entry } : {}) };
|
|
515
|
+
result.push(blocks ? { ...common, kind: 'blocks', blockType: section['blockType'] }
|
|
516
|
+
: { ...common, kind: 'entries' });
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
return result;
|
|
522
|
+
}
|
|
523
|
+
function validateViewConfig(input, limits) {
|
|
524
|
+
let bounded;
|
|
525
|
+
try {
|
|
526
|
+
bounded = effectiveLimits(limits);
|
|
527
|
+
}
|
|
528
|
+
catch (error) {
|
|
529
|
+
return { ok: false, diagnostics: [diagnostic('limit_exceeded', String(error))] };
|
|
530
|
+
}
|
|
531
|
+
const source = record(input);
|
|
532
|
+
if (!source || !exactKeys(source, ['version', 'rules']) || source['version'] !== 1
|
|
533
|
+
|| !Array.isArray(source['rules']) || source['rules'].length > bounded.maxRules) {
|
|
534
|
+
return { ok: false, diagnostics: [diagnostic('invalid_config', 'Expected version 1 and a bounded rules array')] };
|
|
535
|
+
}
|
|
536
|
+
const rules = [];
|
|
537
|
+
const ids = new Set();
|
|
538
|
+
const counter = { count: 0 };
|
|
539
|
+
for (const item of source['rules']) {
|
|
540
|
+
const rule = record(item);
|
|
541
|
+
if (!rule || !exactKeys(rule, ['id', 'match', 'parser', 'title', 'sections']) || !text(rule['id'], 100)
|
|
542
|
+
|| !/^[A-Za-z0-9_-]+$/.test(rule['id']) || ids.has(rule['id'])
|
|
543
|
+
|| !safeGlob(rule['match'], bounded) || !['json', 'yaml', 'hcl'].includes(String(rule['parser']))) {
|
|
544
|
+
return { ok: false, diagnostics: [diagnostic('invalid_config', 'Invalid or duplicate view rule')] };
|
|
545
|
+
}
|
|
546
|
+
ids.add(rule['id']);
|
|
547
|
+
const title = rule['title'] === undefined ? undefined : parseTitle(rule['title'], bounded);
|
|
548
|
+
if (rule['title'] !== undefined && !title)
|
|
549
|
+
return { ok: false, diagnostics: [diagnostic('invalid_config', `Invalid title in ${rule['id']}`)] };
|
|
550
|
+
const sections = rule['sections'] === undefined ? undefined : parseSections(rule['sections'], bounded, counter);
|
|
551
|
+
if (rule['sections'] !== undefined && !sections)
|
|
552
|
+
return { ok: false, diagnostics: [diagnostic('invalid_config', `Invalid sections in ${rule['id']}`)] };
|
|
553
|
+
rules.push({ id: rule['id'], match: rule['match'], parser: rule['parser'],
|
|
554
|
+
...(title ? { title } : {}), ...(sections ? { sections } : {}) });
|
|
555
|
+
}
|
|
556
|
+
return { ok: true, config: { version: 1, rules } };
|
|
557
|
+
}
|
|
558
|
+
function plain(node) {
|
|
559
|
+
switch (node.kind) {
|
|
560
|
+
case 'null': return null;
|
|
561
|
+
case 'boolean':
|
|
562
|
+
case 'string': return node.value;
|
|
563
|
+
case 'number': return node.value ?? Number(node.sourceText);
|
|
564
|
+
case 'array': return node.items.map(plain);
|
|
565
|
+
case 'object': return Object.fromEntries(node.entries.map(entry => [entry.key, plain(entry.value)]));
|
|
566
|
+
default: throw new Error('Configuration cannot contain HCL or expressions');
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
function parseViewConfigText(textSource, format, sourcePath, limits) {
|
|
570
|
+
let bounded;
|
|
571
|
+
try {
|
|
572
|
+
bounded = effectiveLimits(limits);
|
|
573
|
+
}
|
|
574
|
+
catch (error) {
|
|
575
|
+
return { ok: false, diagnostics: [diagnostic('limit_exceeded', String(error))] };
|
|
576
|
+
}
|
|
577
|
+
if (exceedsSourceLimit(textSource, bounded.maxConfigBytes)) {
|
|
578
|
+
return { ok: false, diagnostics: [diagnostic('limit_exceeded', `View config exceeds ${bounded.maxConfigBytes} bytes`, undefined, sourcePath)] };
|
|
579
|
+
}
|
|
580
|
+
const parsed = format === 'json' ? parseJsonText(textSource, { ...bounded, maxSourceBytes: bounded.maxConfigBytes })
|
|
581
|
+
: parseYamlText(textSource, { ...bounded, maxSourceBytes: bounded.maxConfigBytes });
|
|
582
|
+
if (!parsed.ok)
|
|
583
|
+
return { ok: false, diagnostics: parsed.diagnostics.map(item => ({ ...item, source: sourcePath })) };
|
|
584
|
+
try {
|
|
585
|
+
return validateViewConfig(plain(parsed.document.root), bounded);
|
|
586
|
+
}
|
|
587
|
+
catch (error) {
|
|
588
|
+
return { ok: false, diagnostics: [diagnostic('invalid_config', String(error), undefined, sourcePath)] };
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
function matchGlob(glob, path) {
|
|
592
|
+
if (path.length > 4_096)
|
|
593
|
+
return false;
|
|
594
|
+
const width = path.length + 1;
|
|
595
|
+
const table = new Uint8Array((glob.length + 1) * width);
|
|
596
|
+
const at = (pattern, position) => table[pattern * width + position];
|
|
597
|
+
table[glob.length * width + path.length] = 1;
|
|
598
|
+
for (let pattern = glob.length - 1; pattern >= 0; pattern--) {
|
|
599
|
+
let consumedDirectory = false;
|
|
600
|
+
for (let position = path.length; position >= 0; position--) {
|
|
601
|
+
const character = glob[pattern];
|
|
602
|
+
const remaining = position < path.length;
|
|
603
|
+
let matches = false;
|
|
604
|
+
if (character === '*' && glob[pattern + 1] === '*' && glob[pattern + 2] === '/') {
|
|
605
|
+
consumedDirectory = remaining && ((path[position] === '/' && !!at(pattern + 3, position + 1)) || consumedDirectory);
|
|
606
|
+
matches = !!at(pattern + 3, position) || consumedDirectory;
|
|
607
|
+
}
|
|
608
|
+
else if (character === '*' && glob[pattern + 1] === '*') {
|
|
609
|
+
matches = !!at(pattern + 2, position) || (remaining && !!at(pattern, position + 1));
|
|
610
|
+
}
|
|
611
|
+
else if (character === '*') {
|
|
612
|
+
matches = !!at(pattern + 1, position) || (remaining && path[position] !== '/' && !!at(pattern, position + 1));
|
|
613
|
+
}
|
|
614
|
+
else if (character === '?') {
|
|
615
|
+
matches = remaining && path[position] !== '/' && !!at(pattern + 1, position + 1);
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
matches = remaining && character === path[position] && !!at(pattern + 1, position + 1);
|
|
619
|
+
}
|
|
620
|
+
table[pattern * width + position] = matches ? 1 : 0;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
return !!at(0, 0);
|
|
624
|
+
}
|
|
625
|
+
function resolveViewRule(options) {
|
|
626
|
+
let bounded;
|
|
627
|
+
try {
|
|
628
|
+
bounded = effectiveLimits(options.limits);
|
|
629
|
+
}
|
|
630
|
+
catch (error) {
|
|
631
|
+
return { status: 'error', diagnostics: [diagnostic('limit_exceeded', String(error))] };
|
|
632
|
+
}
|
|
633
|
+
const pathSegments = options.repositoryPath.split('/');
|
|
634
|
+
if (options.repositoryPath.length > 4_096 || options.repositoryPath.startsWith('/')
|
|
635
|
+
|| options.repositoryPath.includes('\\')
|
|
636
|
+
|| [...options.repositoryPath].some(character => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127)
|
|
637
|
+
|| pathSegments.some(segment => segment === '.' || segment === '..' || segment === '')) {
|
|
638
|
+
return { status: 'error', diagnostics: [diagnostic('invalid_config', 'Invalid repository path for view resolution')] };
|
|
639
|
+
}
|
|
640
|
+
const limits = options.limits;
|
|
641
|
+
const merged = [];
|
|
642
|
+
const append = (rule) => {
|
|
643
|
+
const previous = merged.findIndex(candidate => candidate.id === rule.id);
|
|
644
|
+
if (previous >= 0)
|
|
645
|
+
merged.splice(previous, 1);
|
|
646
|
+
merged.push(rule);
|
|
647
|
+
};
|
|
648
|
+
if (options.builtInRules?.length) {
|
|
649
|
+
const checked = validateViewConfig({ version: 1, rules: options.builtInRules }, limits);
|
|
650
|
+
if (!checked.ok)
|
|
651
|
+
return { status: 'error', diagnostics: checked.diagnostics };
|
|
652
|
+
checked.config.rules.forEach(append);
|
|
653
|
+
if (merged.length > bounded.maxRules) {
|
|
654
|
+
return { status: 'error', diagnostics: [diagnostic('limit_exceeded', 'Too many combined view rules')] };
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
const levels = new Set();
|
|
658
|
+
for (const document of options.orderedDocuments) {
|
|
659
|
+
const level = document.sourcePath.replace(/\/[^/]+$/, '');
|
|
660
|
+
if (levels.has(level))
|
|
661
|
+
return { status: 'error', diagnostics: [diagnostic('invalid_config', `Multiple view config files at ${level}`)] };
|
|
662
|
+
levels.add(level);
|
|
663
|
+
const checked = validateViewConfig(document.config, limits);
|
|
664
|
+
if (!checked.ok)
|
|
665
|
+
return { status: 'error', diagnostics: checked.diagnostics.map(item => ({ ...item, source: document.sourcePath })) };
|
|
666
|
+
checked.config.rules.forEach(append);
|
|
667
|
+
if (merged.length > bounded.maxRules) {
|
|
668
|
+
return { status: 'error', diagnostics: [diagnostic('limit_exceeded', 'Too many combined view rules')] };
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
const path = options.repositoryPath.replace(/^\/+/, '');
|
|
672
|
+
let matchingWork = 0;
|
|
673
|
+
for (let index = merged.length - 1; index >= 0; index--) {
|
|
674
|
+
matchingWork += (merged[index].match.length + 1) * (path.length + 1);
|
|
675
|
+
if (matchingWork > 8_000_000) {
|
|
676
|
+
return { status: 'error', diagnostics: [diagnostic('limit_exceeded', 'View rule matching work limit exceeded')] };
|
|
677
|
+
}
|
|
678
|
+
if (matchGlob(merged[index].match, path))
|
|
679
|
+
return { status: 'matched', parser: merged[index].parser, view: merged[index] };
|
|
680
|
+
}
|
|
681
|
+
const suffix = path.toLowerCase().split('.').pop();
|
|
682
|
+
if (suffix === 'json' || suffix === 'yaml' || suffix === 'hcl')
|
|
683
|
+
return { status: 'generic', parser: suffix };
|
|
684
|
+
if (suffix === 'yml')
|
|
685
|
+
return { status: 'generic', parser: 'yaml' };
|
|
686
|
+
return { status: 'unsupported', diagnostics: [diagnostic('unsupported_format', 'No JSON, YAML, or HCL view applies')] };
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function parseStructuredText(sourceText, format, limits) {
|
|
690
|
+
if (format === 'json')
|
|
691
|
+
return Promise.resolve(parseJsonText(sourceText, limits));
|
|
692
|
+
if (format === 'yaml')
|
|
693
|
+
return Promise.resolve(parseYamlText(sourceText, limits));
|
|
694
|
+
return parseHclText(sourceText, limits);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/** Internal recursive, read-only value renderer. The host's source remains authoritative. */
|
|
698
|
+
class StructuredNodeComponent {
|
|
699
|
+
constructor() {
|
|
700
|
+
this.depth = 0;
|
|
701
|
+
this.maxDepth = 64;
|
|
702
|
+
this.maxRows = 2_000;
|
|
703
|
+
this.scalarListLayout = 'inline';
|
|
704
|
+
this.sourcePositionSelected = new EventEmitter();
|
|
705
|
+
this.expanded = false;
|
|
706
|
+
this.inlineLimit = 12;
|
|
707
|
+
}
|
|
708
|
+
scalarText(node) {
|
|
709
|
+
switch (node.kind) {
|
|
710
|
+
case 'null': return 'null';
|
|
711
|
+
case 'boolean': return String(node.value);
|
|
712
|
+
case 'number': return node.sourceText;
|
|
713
|
+
case 'string': return JSON.stringify(node.value);
|
|
714
|
+
case 'expression': return node.sourceText;
|
|
715
|
+
default: return '';
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
isInlineArray(node) {
|
|
719
|
+
return node.kind === 'array' && this.scalarListLayout === 'inline'
|
|
720
|
+
&& node.items.every(item => ['null', 'boolean', 'number', 'string', 'expression'].includes(item.kind));
|
|
721
|
+
}
|
|
722
|
+
inlineArrayText(node) {
|
|
723
|
+
return `[${this.limitedArray(node).map(item => this.inlineScalarText(item)).join(', ')}]`;
|
|
724
|
+
}
|
|
725
|
+
inlineScalarText(node) {
|
|
726
|
+
if (node.kind !== 'string')
|
|
727
|
+
return this.scalarText(node);
|
|
728
|
+
const safe = /^[\p{L}_][\p{L}\p{N}_.-]*$/u.test(node.value)
|
|
729
|
+
&& !['true', 'false', 'null', 'nan', 'infinity'].includes(node.value.toLowerCase());
|
|
730
|
+
return safe ? node.value : JSON.stringify(node.value);
|
|
731
|
+
}
|
|
732
|
+
limitedArray(node) { return node.items.slice(0, this.maxRows); }
|
|
733
|
+
limitedEntries(node) { return node.entries.slice(0, this.maxRows); }
|
|
734
|
+
limitedMembers(node) { return node.members.slice(0, this.maxRows); }
|
|
735
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: StructuredNodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
736
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: StructuredNodeComponent, isStandalone: true, selector: "sneat-structured-node", inputs: { node: "node", depth: "depth", maxDepth: "maxDepth", maxRows: "maxRows", scalarListLayout: "scalarListLayout" }, outputs: { sourcePositionSelected: "sourcePositionSelected" }, ngImport: i0, template: `
|
|
737
|
+
@if (depth > maxDepth) {
|
|
738
|
+
<span class="sd-limit">Nested value limit reached</span>
|
|
739
|
+
} @else {
|
|
740
|
+
@switch (node.kind) {
|
|
741
|
+
@case ('null') { <span class="sd-scalar sd-null">null</span> }
|
|
742
|
+
@case ('boolean') { <span class="sd-scalar">{{ scalarText(node) }}</span> }
|
|
743
|
+
@case ('number') { <span class="sd-scalar">{{ scalarText(node) }}</span> }
|
|
744
|
+
@case ('string') { <span class="sd-scalar">{{ scalarText(node) }}</span> }
|
|
745
|
+
@case ('expression') {
|
|
746
|
+
<span class="sd-expression" aria-label="Unevaluated HCL expression">{{ scalarText(node) }}</span>
|
|
747
|
+
}
|
|
748
|
+
@case ('array') {
|
|
749
|
+
@if (isInlineArray(node) && (node.items.length <= inlineLimit || expanded)) {
|
|
750
|
+
<span class="sd-inline-array" aria-label="List of {{node.items.length}} values">{{inlineArrayText(node)}}</span>
|
|
751
|
+
@if (node.items.length > maxRows) { <span class="sd-limit">{{node.items.length - maxRows}} more values; inspect Raw source</span> }
|
|
752
|
+
} @else if (isInlineArray(node) && !expanded) {
|
|
753
|
+
<button type="button" class="sd-link" (click)="expanded = true">Show {{node.items.length}} values</button>
|
|
754
|
+
} @else {
|
|
755
|
+
<ol class="sd-list">
|
|
756
|
+
@for (item of limitedArray(node); track $index) {
|
|
757
|
+
<li><sneat-structured-node [node]="item" [depth]="depth + 1" [maxDepth]="maxDepth"
|
|
758
|
+
[maxRows]="maxRows" (sourcePositionSelected)="sourcePositionSelected.emit($event)" /></li>
|
|
759
|
+
}
|
|
760
|
+
</ol>
|
|
761
|
+
@if (node.items.length > maxRows) { <span class="sd-limit">{{node.items.length - maxRows}} more values; inspect Raw source</span> }
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
@case ('object') {
|
|
765
|
+
@if (node.entries.length === 0) { <span class="sd-empty">Empty object</span> }
|
|
766
|
+
@else {
|
|
767
|
+
<dl class="sd-fields">
|
|
768
|
+
@for (entry of limitedEntries(node); track entry.key) {
|
|
769
|
+
<dt>{{entry.key}}</dt>
|
|
770
|
+
<dd><sneat-structured-node [node]="entry.value" [depth]="depth + 1" [maxDepth]="maxDepth"
|
|
771
|
+
[maxRows]="maxRows" (sourcePositionSelected)="sourcePositionSelected.emit($event)" /></dd>
|
|
772
|
+
}
|
|
773
|
+
</dl>
|
|
774
|
+
@if (node.entries.length > maxRows) { <span class="sd-limit">{{node.entries.length - maxRows}} more fields; inspect Raw source</span> }
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
@case ('hcl-body') {
|
|
778
|
+
@if (node.members.length === 0) { <span class="sd-empty">Empty body</span> }
|
|
779
|
+
@for (member of limitedMembers(node); track $index) {
|
|
780
|
+
@if (member.kind === 'hcl-attribute') {
|
|
781
|
+
<div class="sd-hcl-member"><strong>{{member.name}}</strong>:
|
|
782
|
+
<sneat-structured-node [node]="member.value" [depth]="depth + 1" [maxDepth]="maxDepth"
|
|
783
|
+
[maxRows]="maxRows" (sourcePositionSelected)="sourcePositionSelected.emit($event)" />
|
|
784
|
+
<button type="button" class="sd-source" (click)="sourcePositionSelected.emit(member.range)"
|
|
785
|
+
[attr.aria-label]="'Open source for ' + member.name">Source</button>
|
|
786
|
+
</div>
|
|
787
|
+
} @else {
|
|
788
|
+
<details class="sd-hcl-block">
|
|
789
|
+
<summary>{{member.blockType}} @for (label of member.labels; track $index) { <span>{{label}} </span> }</summary>
|
|
790
|
+
<sneat-structured-node [node]="member.body" [depth]="depth + 1" [maxDepth]="maxDepth"
|
|
791
|
+
[maxRows]="maxRows" (sourcePositionSelected)="sourcePositionSelected.emit($event)" />
|
|
792
|
+
<button type="button" class="sd-source" (click)="sourcePositionSelected.emit(member.range)"
|
|
793
|
+
[attr.aria-label]="'Open source for ' + member.blockType">Source</button>
|
|
794
|
+
</details>
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
@if (node.members.length > maxRows) { <span class="sd-limit">{{node.members.length - maxRows}} more members; inspect Raw source</span> }
|
|
798
|
+
}
|
|
799
|
+
@case ('hcl-block') {
|
|
800
|
+
<strong>{{node.blockType}} @for (label of node.labels; track $index) { <span>{{label}} </span> }</strong>
|
|
801
|
+
<sneat-structured-node [node]="node.body" [depth]="depth + 1" [maxDepth]="maxDepth"
|
|
802
|
+
[maxRows]="maxRows" (sourcePositionSelected)="sourcePositionSelected.emit($event)" />
|
|
803
|
+
}
|
|
804
|
+
@case ('hcl-attribute') {
|
|
805
|
+
<strong>{{node.name}}</strong>:
|
|
806
|
+
<sneat-structured-node [node]="node.value" [depth]="depth + 1" [maxDepth]="maxDepth"
|
|
807
|
+
[maxRows]="maxRows" (sourcePositionSelected)="sourcePositionSelected.emit($event)" />
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
@if (node.range && node.kind !== 'hcl-body' && node.kind !== 'hcl-block' && node.kind !== 'hcl-attribute') {
|
|
811
|
+
<button type="button" class="sd-source" (click)="sourcePositionSelected.emit(node.range)"
|
|
812
|
+
aria-label="Open source for value">Source</button>
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
`, isInline: true, styles: [":host{display:inline-block;max-width:100%;overflow-wrap:anywhere}.sd-inline-array{display:inline-flex;flex-wrap:wrap;gap:.2rem;max-width:100%}.sd-inline-item{white-space:normal}.sd-fields{display:grid;grid-template-columns:minmax(6rem,auto) minmax(0,1fr);gap:.3rem .8rem}.sd-fields dt{font-weight:600}.sd-fields dd{margin:0;min-width:0}.sd-list{margin:.2rem 0;padding-inline-start:1.5rem}.sd-expression{font-family:monospace}.sd-source,.sd-link{border:0;background:transparent;color:inherit;text-decoration:underline;cursor:pointer}.sd-source{font-size:.75em;margin-inline-start:.5rem}.sd-empty,.sd-null,.sd-limit{opacity:.75}.sd-hcl-member,.sd-hcl-block{display:block;margin-block:.25rem}\n"], dependencies: [{ kind: "component", type: i0.forwardRef(() => StructuredNodeComponent), selector: "sneat-structured-node", inputs: ["node", "depth", "maxDepth", "maxRows", "scalarListLayout"], outputs: ["sourcePositionSelected"] }, { kind: "ngmodule", type: i0.forwardRef(() => CommonModule) }] }); }
|
|
816
|
+
}
|
|
817
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: StructuredNodeComponent, decorators: [{
|
|
818
|
+
type: Component,
|
|
819
|
+
args: [{ selector: 'sneat-structured-node', standalone: true, imports: [CommonModule, forwardRef(() => StructuredNodeComponent)], template: `
|
|
820
|
+
@if (depth > maxDepth) {
|
|
821
|
+
<span class="sd-limit">Nested value limit reached</span>
|
|
822
|
+
} @else {
|
|
823
|
+
@switch (node.kind) {
|
|
824
|
+
@case ('null') { <span class="sd-scalar sd-null">null</span> }
|
|
825
|
+
@case ('boolean') { <span class="sd-scalar">{{ scalarText(node) }}</span> }
|
|
826
|
+
@case ('number') { <span class="sd-scalar">{{ scalarText(node) }}</span> }
|
|
827
|
+
@case ('string') { <span class="sd-scalar">{{ scalarText(node) }}</span> }
|
|
828
|
+
@case ('expression') {
|
|
829
|
+
<span class="sd-expression" aria-label="Unevaluated HCL expression">{{ scalarText(node) }}</span>
|
|
830
|
+
}
|
|
831
|
+
@case ('array') {
|
|
832
|
+
@if (isInlineArray(node) && (node.items.length <= inlineLimit || expanded)) {
|
|
833
|
+
<span class="sd-inline-array" aria-label="List of {{node.items.length}} values">{{inlineArrayText(node)}}</span>
|
|
834
|
+
@if (node.items.length > maxRows) { <span class="sd-limit">{{node.items.length - maxRows}} more values; inspect Raw source</span> }
|
|
835
|
+
} @else if (isInlineArray(node) && !expanded) {
|
|
836
|
+
<button type="button" class="sd-link" (click)="expanded = true">Show {{node.items.length}} values</button>
|
|
837
|
+
} @else {
|
|
838
|
+
<ol class="sd-list">
|
|
839
|
+
@for (item of limitedArray(node); track $index) {
|
|
840
|
+
<li><sneat-structured-node [node]="item" [depth]="depth + 1" [maxDepth]="maxDepth"
|
|
841
|
+
[maxRows]="maxRows" (sourcePositionSelected)="sourcePositionSelected.emit($event)" /></li>
|
|
842
|
+
}
|
|
843
|
+
</ol>
|
|
844
|
+
@if (node.items.length > maxRows) { <span class="sd-limit">{{node.items.length - maxRows}} more values; inspect Raw source</span> }
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
@case ('object') {
|
|
848
|
+
@if (node.entries.length === 0) { <span class="sd-empty">Empty object</span> }
|
|
849
|
+
@else {
|
|
850
|
+
<dl class="sd-fields">
|
|
851
|
+
@for (entry of limitedEntries(node); track entry.key) {
|
|
852
|
+
<dt>{{entry.key}}</dt>
|
|
853
|
+
<dd><sneat-structured-node [node]="entry.value" [depth]="depth + 1" [maxDepth]="maxDepth"
|
|
854
|
+
[maxRows]="maxRows" (sourcePositionSelected)="sourcePositionSelected.emit($event)" /></dd>
|
|
855
|
+
}
|
|
856
|
+
</dl>
|
|
857
|
+
@if (node.entries.length > maxRows) { <span class="sd-limit">{{node.entries.length - maxRows}} more fields; inspect Raw source</span> }
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
@case ('hcl-body') {
|
|
861
|
+
@if (node.members.length === 0) { <span class="sd-empty">Empty body</span> }
|
|
862
|
+
@for (member of limitedMembers(node); track $index) {
|
|
863
|
+
@if (member.kind === 'hcl-attribute') {
|
|
864
|
+
<div class="sd-hcl-member"><strong>{{member.name}}</strong>:
|
|
865
|
+
<sneat-structured-node [node]="member.value" [depth]="depth + 1" [maxDepth]="maxDepth"
|
|
866
|
+
[maxRows]="maxRows" (sourcePositionSelected)="sourcePositionSelected.emit($event)" />
|
|
867
|
+
<button type="button" class="sd-source" (click)="sourcePositionSelected.emit(member.range)"
|
|
868
|
+
[attr.aria-label]="'Open source for ' + member.name">Source</button>
|
|
869
|
+
</div>
|
|
870
|
+
} @else {
|
|
871
|
+
<details class="sd-hcl-block">
|
|
872
|
+
<summary>{{member.blockType}} @for (label of member.labels; track $index) { <span>{{label}} </span> }</summary>
|
|
873
|
+
<sneat-structured-node [node]="member.body" [depth]="depth + 1" [maxDepth]="maxDepth"
|
|
874
|
+
[maxRows]="maxRows" (sourcePositionSelected)="sourcePositionSelected.emit($event)" />
|
|
875
|
+
<button type="button" class="sd-source" (click)="sourcePositionSelected.emit(member.range)"
|
|
876
|
+
[attr.aria-label]="'Open source for ' + member.blockType">Source</button>
|
|
877
|
+
</details>
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
@if (node.members.length > maxRows) { <span class="sd-limit">{{node.members.length - maxRows}} more members; inspect Raw source</span> }
|
|
881
|
+
}
|
|
882
|
+
@case ('hcl-block') {
|
|
883
|
+
<strong>{{node.blockType}} @for (label of node.labels; track $index) { <span>{{label}} </span> }</strong>
|
|
884
|
+
<sneat-structured-node [node]="node.body" [depth]="depth + 1" [maxDepth]="maxDepth"
|
|
885
|
+
[maxRows]="maxRows" (sourcePositionSelected)="sourcePositionSelected.emit($event)" />
|
|
886
|
+
}
|
|
887
|
+
@case ('hcl-attribute') {
|
|
888
|
+
<strong>{{node.name}}</strong>:
|
|
889
|
+
<sneat-structured-node [node]="node.value" [depth]="depth + 1" [maxDepth]="maxDepth"
|
|
890
|
+
[maxRows]="maxRows" (sourcePositionSelected)="sourcePositionSelected.emit($event)" />
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
@if (node.range && node.kind !== 'hcl-body' && node.kind !== 'hcl-block' && node.kind !== 'hcl-attribute') {
|
|
894
|
+
<button type="button" class="sd-source" (click)="sourcePositionSelected.emit(node.range)"
|
|
895
|
+
aria-label="Open source for value">Source</button>
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
`, styles: [":host{display:inline-block;max-width:100%;overflow-wrap:anywhere}.sd-inline-array{display:inline-flex;flex-wrap:wrap;gap:.2rem;max-width:100%}.sd-inline-item{white-space:normal}.sd-fields{display:grid;grid-template-columns:minmax(6rem,auto) minmax(0,1fr);gap:.3rem .8rem}.sd-fields dt{font-weight:600}.sd-fields dd{margin:0;min-width:0}.sd-list{margin:.2rem 0;padding-inline-start:1.5rem}.sd-expression{font-family:monospace}.sd-source,.sd-link{border:0;background:transparent;color:inherit;text-decoration:underline;cursor:pointer}.sd-source{font-size:.75em;margin-inline-start:.5rem}.sd-empty,.sd-null,.sd-limit{opacity:.75}.sd-hcl-member,.sd-hcl-block{display:block;margin-block:.25rem}\n"] }]
|
|
899
|
+
}], propDecorators: { node: [{
|
|
900
|
+
type: Input,
|
|
901
|
+
args: [{ required: true }]
|
|
902
|
+
}], depth: [{
|
|
903
|
+
type: Input
|
|
904
|
+
}], maxDepth: [{
|
|
905
|
+
type: Input
|
|
906
|
+
}], maxRows: [{
|
|
907
|
+
type: Input
|
|
908
|
+
}], scalarListLayout: [{
|
|
909
|
+
type: Input
|
|
910
|
+
}], sourcePositionSelected: [{
|
|
911
|
+
type: Output
|
|
912
|
+
}] } });
|
|
913
|
+
|
|
914
|
+
class StructuredDataViewerComponent {
|
|
915
|
+
constructor() {
|
|
916
|
+
this.host = inject(ElementRef);
|
|
917
|
+
this.document = null;
|
|
918
|
+
this.view = null;
|
|
919
|
+
this.activeSectionId = null;
|
|
920
|
+
this.diagnostics = [];
|
|
921
|
+
this.sourceText = '';
|
|
922
|
+
this.showRawFallback = false;
|
|
923
|
+
this.limits = {};
|
|
924
|
+
this.sourcePositionSelected = new EventEmitter();
|
|
925
|
+
this.sectionSelected = new EventEmitter();
|
|
926
|
+
this.expanded = new Map();
|
|
927
|
+
this.layouts = new Map();
|
|
928
|
+
this.lastNavigatedId = null;
|
|
929
|
+
}
|
|
930
|
+
ngOnChanges(changes) {
|
|
931
|
+
if (changes['document'] || changes['view']) {
|
|
932
|
+
this.expanded.clear();
|
|
933
|
+
this.layouts.clear();
|
|
934
|
+
this.lastNavigatedId = null;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
ngAfterViewChecked() {
|
|
938
|
+
if (!this.activeSectionId || this.lastNavigatedId === this.activeSectionId || typeof document === 'undefined')
|
|
939
|
+
return;
|
|
940
|
+
const target = [...this.host.nativeElement.querySelectorAll('[id]')]
|
|
941
|
+
.find(element => element.id === this.activeSectionId);
|
|
942
|
+
if (!target)
|
|
943
|
+
return;
|
|
944
|
+
target.focus({ preventScroll: true });
|
|
945
|
+
target.scrollIntoView?.({ block: 'nearest' });
|
|
946
|
+
this.lastNavigatedId = this.activeSectionId;
|
|
947
|
+
}
|
|
948
|
+
get maxRows() { return Math.max(0, Math.min(2_000, this.limits.maxRows ?? 2_000)); }
|
|
949
|
+
get maxDepth() { return Math.max(0, Math.min(64, this.limits.maxDepth ?? 64)); }
|
|
950
|
+
get sections() { return this.view?.sections ?? []; }
|
|
951
|
+
get rootContext() { return this.document ? { node: this.document.root, pathId: '' } : null; }
|
|
952
|
+
get rawFallbackText() { return this.sourceText || this.document?.sourceText || ''; }
|
|
953
|
+
titleText() {
|
|
954
|
+
if (!this.document || !this.view?.title)
|
|
955
|
+
return null;
|
|
956
|
+
const value = selectRelative(this.document.root, this.view.title.path);
|
|
957
|
+
return value.found ? `${this.view.title.label} ${this.shortText(value.value)}` : this.view.title.label;
|
|
958
|
+
}
|
|
959
|
+
selected(context, path) {
|
|
960
|
+
const result = selectViewPath(context, path);
|
|
961
|
+
return result.found ? result.value : null;
|
|
962
|
+
}
|
|
963
|
+
shortText(node) {
|
|
964
|
+
switch (node.kind) {
|
|
965
|
+
case 'null': return 'null';
|
|
966
|
+
case 'boolean': return String(node.value);
|
|
967
|
+
case 'number': return node.sourceText;
|
|
968
|
+
case 'string': return node.value;
|
|
969
|
+
case 'expression': return node.sourceText;
|
|
970
|
+
case 'array': return `[${node.items.length} values]`;
|
|
971
|
+
case 'object': return `{${node.entries.length} fields}`;
|
|
972
|
+
case 'hcl-body': return `${node.members.length} members`;
|
|
973
|
+
case 'hcl-attribute': return node.name;
|
|
974
|
+
case 'hcl-block': return `${node.blockType} ${node.labels.join(' ')}`;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
selectSection(sectionId) { this.sectionSelected.emit({ sectionId }); }
|
|
978
|
+
sectionId(section, context) {
|
|
979
|
+
return context.pathId ? `${context.pathId}/${section.id}` : section.id;
|
|
980
|
+
}
|
|
981
|
+
rowActivated(row) { this.selectSection(row.pathId || row.id); }
|
|
982
|
+
rowPath(section, parent, id) {
|
|
983
|
+
return `${this.sectionId(section, parent)}/${id}`;
|
|
984
|
+
}
|
|
985
|
+
rowsFor(section, context) {
|
|
986
|
+
const selected = this.selected(context, section.path);
|
|
987
|
+
if (!selected || this.maxRows === 0)
|
|
988
|
+
return [];
|
|
989
|
+
const rows = [];
|
|
990
|
+
if (section.kind === 'blocks' || (section.kind === 'table' && section.rows === 'blocks')) {
|
|
991
|
+
const body = selected.kind === 'hcl-block' ? selected.body : selected;
|
|
992
|
+
if (body.kind !== 'hcl-body')
|
|
993
|
+
return [];
|
|
994
|
+
for (const member of body.members) {
|
|
995
|
+
if (member.kind !== 'hcl-block' || (section.blockType && member.blockType !== section.blockType))
|
|
996
|
+
continue;
|
|
997
|
+
const rowContext = { node: member, labels: member.labels };
|
|
998
|
+
const id = `${member.blockType}-${member.range.startOffset}-${encodeURIComponent(member.labels.join('|'))}`;
|
|
999
|
+
rows.push({ ...rowContext, id, pathId: this.rowPath(section, context, id),
|
|
1000
|
+
title: this.entryHeading(section.kind === 'blocks' ? section.entry : undefined, rowContext, `${member.blockType} ${member.labels.join(' ')}`) });
|
|
1001
|
+
if (rows.length >= this.maxRows)
|
|
1002
|
+
break;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
else if (selected.kind === 'object' && (section.kind === 'entries' || (section.kind === 'table' && section.rows === 'dictionary'))) {
|
|
1006
|
+
for (const entry of selected.entries) {
|
|
1007
|
+
const rowContext = { node: entry.value, key: entry.key };
|
|
1008
|
+
const id = encodeURIComponent(entry.key);
|
|
1009
|
+
rows.push({ ...rowContext, id, pathId: this.rowPath(section, context, id),
|
|
1010
|
+
title: this.entryHeading(section.kind === 'entries' ? section.entry : undefined, rowContext, entry.key) });
|
|
1011
|
+
if (rows.length >= this.maxRows)
|
|
1012
|
+
break;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
else if (selected.kind === 'array' && (section.kind === 'entries' || (section.kind === 'table' && section.rows === 'list'))) {
|
|
1016
|
+
selected.items.slice(0, this.maxRows).forEach((item, index) => {
|
|
1017
|
+
const rowContext = { node: item };
|
|
1018
|
+
const id = String(index);
|
|
1019
|
+
rows.push({ ...rowContext, id, pathId: this.rowPath(section, context, id),
|
|
1020
|
+
title: this.entryHeading(section.kind === 'entries' ? section.entry : undefined, rowContext, String(index + 1)) });
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
return rows;
|
|
1024
|
+
}
|
|
1025
|
+
totalRows(section, context) {
|
|
1026
|
+
const selected = this.selected(context, section.path);
|
|
1027
|
+
if (!selected)
|
|
1028
|
+
return 0;
|
|
1029
|
+
if (selected.kind === 'object')
|
|
1030
|
+
return selected.entries.length;
|
|
1031
|
+
if (selected.kind === 'array')
|
|
1032
|
+
return selected.items.length;
|
|
1033
|
+
const body = selected.kind === 'hcl-block' ? selected.body : selected;
|
|
1034
|
+
return body.kind === 'hcl-body' ? body.members.filter(member => member.kind === 'hcl-block'
|
|
1035
|
+
&& (!('blockType' in section) || !section.blockType || member.blockType === section.blockType)).length : 0;
|
|
1036
|
+
}
|
|
1037
|
+
rowLimitNotice(section, context) {
|
|
1038
|
+
const remaining = this.totalRows(section, context) - this.maxRows;
|
|
1039
|
+
return remaining > 0 ? `${remaining} more rows; inspect Raw source` : null;
|
|
1040
|
+
}
|
|
1041
|
+
entryHeading(entry, context, fallback) {
|
|
1042
|
+
if (!entry)
|
|
1043
|
+
return fallback;
|
|
1044
|
+
const selected = selectViewPath(context, entry.heading);
|
|
1045
|
+
return selected.found ? this.shortText(selected.value) : fallback;
|
|
1046
|
+
}
|
|
1047
|
+
entrySections(section) {
|
|
1048
|
+
return section.kind === 'entries' || section.kind === 'blocks' ? section.entry?.sections ?? [] : [];
|
|
1049
|
+
}
|
|
1050
|
+
layoutFor(section, context) {
|
|
1051
|
+
return this.layouts.get(this.sectionId(section, context)) ?? ('presentation' in section ? section.presentation || 'list' : 'list');
|
|
1052
|
+
}
|
|
1053
|
+
setLayout(section, context, layout) {
|
|
1054
|
+
this.layouts.set(this.sectionId(section, context), layout);
|
|
1055
|
+
}
|
|
1056
|
+
expandedValues(section, context, rows) {
|
|
1057
|
+
const pathId = this.sectionId(section, context);
|
|
1058
|
+
let current = this.expanded.get(pathId);
|
|
1059
|
+
if (!current) {
|
|
1060
|
+
const initial = 'initiallyExpanded' in section ? section.initiallyExpanded : undefined;
|
|
1061
|
+
current = initial === 'all' ? rows.map(row => row.id) : initial === 'first' && rows.length ? [rows[0].id] : [];
|
|
1062
|
+
this.expanded.set(pathId, current);
|
|
1063
|
+
}
|
|
1064
|
+
if (this.activeSectionId?.startsWith(`${pathId}/`)) {
|
|
1065
|
+
const requested = this.activeSectionId.slice(pathId.length + 1).split('/')[0];
|
|
1066
|
+
if (rows.some(row => row.id === requested) && !current.includes(requested)) {
|
|
1067
|
+
current = [...current, requested];
|
|
1068
|
+
this.expanded.set(pathId, current);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
return current;
|
|
1072
|
+
}
|
|
1073
|
+
setExpandedValues(section, context, values) {
|
|
1074
|
+
this.expanded.set(this.sectionId(section, context), values == null ? [] : Array.isArray(values) ? values.map(String) : [String(values)]);
|
|
1075
|
+
}
|
|
1076
|
+
expandAll(section, context, rows) {
|
|
1077
|
+
this.expanded.set(this.sectionId(section, context), rows.map(row => row.id));
|
|
1078
|
+
}
|
|
1079
|
+
collapseAll(section, context) { this.expanded.set(this.sectionId(section, context), []); }
|
|
1080
|
+
limitedFields(fields) {
|
|
1081
|
+
return fields.slice(0, this.maxRows);
|
|
1082
|
+
}
|
|
1083
|
+
omittedFields(fields) {
|
|
1084
|
+
return Math.max(0, fields.length - this.maxRows);
|
|
1085
|
+
}
|
|
1086
|
+
otherFields(node, sections) {
|
|
1087
|
+
const current = node.kind === 'hcl-block' ? node.body : node;
|
|
1088
|
+
const paths = sections.flatMap(section => section.kind === 'fields' && section.path === ''
|
|
1089
|
+
? section.fields.map(field => field.path) : [section.path]);
|
|
1090
|
+
if (current.kind === 'object') {
|
|
1091
|
+
const consumed = new Set(paths.flatMap(path => {
|
|
1092
|
+
const segments = parseRelativePointer(path);
|
|
1093
|
+
return segments?.length === 1 ? [segments[0]] : [];
|
|
1094
|
+
}));
|
|
1095
|
+
return current.entries.filter(entry => !consumed.has(entry.key)).map(entry => ({ label: entry.key, node: entry.value }));
|
|
1096
|
+
}
|
|
1097
|
+
if (current.kind === 'hcl-body') {
|
|
1098
|
+
const attributes = new Set(paths.flatMap(path => {
|
|
1099
|
+
const segments = parseRelativePointer(path);
|
|
1100
|
+
return segments?.length === 2 && segments[0] === 'attributes' ? [segments[1]] : [];
|
|
1101
|
+
}));
|
|
1102
|
+
const blocks = new Set(sections.flatMap(section => {
|
|
1103
|
+
if (section.kind === 'blocks' || (section.kind === 'table' && section.rows === 'blocks')) {
|
|
1104
|
+
return section.blockType ? [section.blockType] : [];
|
|
1105
|
+
}
|
|
1106
|
+
return [];
|
|
1107
|
+
}));
|
|
1108
|
+
return current.members.filter(member => member.kind === 'hcl-attribute' ? !attributes.has(member.name) : !blocks.has(member.blockType))
|
|
1109
|
+
.map(member => ({ label: member.kind === 'hcl-attribute' ? member.name : `${member.blockType} ${member.labels.join(' ')}`, node: member }));
|
|
1110
|
+
}
|
|
1111
|
+
return [];
|
|
1112
|
+
}
|
|
1113
|
+
fieldOther(node, section) {
|
|
1114
|
+
if (section.kind !== 'fields')
|
|
1115
|
+
return [];
|
|
1116
|
+
// A root fields section shares its value with the top-level disclosure.
|
|
1117
|
+
// Show root leftovers once there; nested row sections retain their own details.
|
|
1118
|
+
if (node === this.document?.root)
|
|
1119
|
+
return [];
|
|
1120
|
+
return this.otherFields(node, section.fields.map((field, index) => ({ kind: 'value',
|
|
1121
|
+
id: `${section.id}-${index}`, title: field.label, path: field.path })));
|
|
1122
|
+
}
|
|
1123
|
+
tableOtherFields(node, section) {
|
|
1124
|
+
if (section.kind !== 'table')
|
|
1125
|
+
return [];
|
|
1126
|
+
return this.otherFields(node, section.columns.map((column, index) => ({ kind: 'value',
|
|
1127
|
+
id: `${section.id}-${index}`, title: column.label, path: column.path })));
|
|
1128
|
+
}
|
|
1129
|
+
tableFallbackNode(node) {
|
|
1130
|
+
return node.kind !== 'object' && node.kind !== 'hcl-body' && node.kind !== 'hcl-block';
|
|
1131
|
+
}
|
|
1132
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: StructuredDataViewerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1133
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: StructuredDataViewerComponent, isStandalone: true, selector: "sneat-structured-data-viewer", inputs: { document: "document", view: "view", activeSectionId: "activeSectionId", diagnostics: "diagnostics", sourceText: "sourceText", showRawFallback: "showRawFallback", limits: "limits" }, outputs: { sourcePositionSelected: "sourcePositionSelected", sectionSelected: "sectionSelected" }, usesOnChanges: true, ngImport: i0, template: "<article class=\"sd-viewer\" aria-label=\"Structured data viewer\">\n @if (diagnostics.length) {\n <div class=\"sd-diagnostics\" role=\"alert\">\n @for (item of diagnostics; track $index) { <p>{{item.message}}</p> }\n </div>\n }\n\n @if (document; as parsed) {\n @if (titleText(); as title) { <h2>{{title}}</h2> }\n @if (rootContext; as root) {\n @if (sections.length) {\n <ng-container *ngTemplateOutlet=\"sectionList; context: { $implicit: root, sectionItems: sections }\" />\n @let remaining = otherFields(root.node, sections);\n @if (remaining.length) {\n <section class=\"sd-section\" aria-label=\"Other fields\">\n <h3>Other fields</h3>\n @for (field of limitedFields(remaining); track $index) {\n <div class=\"sd-field\"><strong>{{field.label}}</strong>\n <sneat-structured-node [node]=\"field.node\" [maxRows]=\"maxRows\" [maxDepth]=\"maxDepth\"\n (sourcePositionSelected)=\"sourcePositionSelected.emit($event)\" />\n </div>\n }\n @if (omittedFields(remaining); as omitted) { <p class=\"sd-limit\">{{omitted}} more fields; inspect Raw source</p> }\n </section>\n }\n } @else {\n <sneat-structured-node [node]=\"parsed.root\" [maxRows]=\"maxRows\" [maxDepth]=\"maxDepth\"\n (sourcePositionSelected)=\"sourcePositionSelected.emit($event)\" />\n }\n }\n } @else if (showRawFallback && rawFallbackText) {\n <pre class=\"sd-raw\">{{rawFallbackText}}</pre>\n }\n</article>\n\n<ng-template #sectionList let-context let-sectionItems=\"sectionItems\">\n @for (section of sectionItems; track section.id) {\n <section class=\"sd-section\" [id]=\"sectionId(section, context)\" tabindex=\"-1\">\n <h3><button type=\"button\" class=\"sd-section-link\" (click)=\"selectSection(sectionId(section, context))\">{{section.title}}</button></h3>\n @switch (section.kind) {\n @case ('value') {\n @if (selected(context, section.path); as node) {\n <sneat-structured-node [node]=\"node\" [scalarListLayout]=\"section.scalarListLayout || 'inline'\"\n [maxRows]=\"maxRows\" [maxDepth]=\"maxDepth\" (sourcePositionSelected)=\"sourcePositionSelected.emit($event)\" />\n } @else { <span class=\"sd-missing\">Missing</span> }\n }\n @case ('fields') {\n @if (selected(context, section.path); as node) {\n <dl class=\"sd-fields\">\n @for (field of section.fields; track field.label) {\n <dt>{{field.label}}</dt>\n <dd>\n @if (selected({node: node}, field.path); as fieldNode) {\n <sneat-structured-node [node]=\"fieldNode\" [scalarListLayout]=\"field.scalarListLayout || 'inline'\"\n [maxRows]=\"maxRows\" [maxDepth]=\"maxDepth\" (sourcePositionSelected)=\"sourcePositionSelected.emit($event)\" />\n } @else { <span class=\"sd-missing\">Missing</span> }\n </dd>\n }\n </dl>\n @let other = fieldOther(node, section);\n @if (other.length) {\n <details><summary>Other fields ({{other.length}})</summary>\n @for (field of limitedFields(other); track $index) {\n <div class=\"sd-field\"><strong>{{field.label}}</strong>\n <sneat-structured-node [node]=\"field.node\" [maxRows]=\"maxRows\" [maxDepth]=\"maxDepth\"\n (sourcePositionSelected)=\"sourcePositionSelected.emit($event)\" />\n </div>\n }\n @if (omittedFields(other); as omitted) { <p class=\"sd-limit\">{{omitted}} more fields; inspect Raw source</p> }\n </details>\n }\n } @else { <span class=\"sd-missing\">Missing</span> }\n }\n @case ('table') {\n @let rows = rowsFor(section, context);\n @if (rows.length) {\n <p-table [value]=\"rows\" [scrollable]=\"true\" styleClass=\"sd-table\">\n <ng-template #header><tr>\n @for (column of section.columns; track column.label) { <th scope=\"col\">{{column.label}}</th> }\n <th scope=\"col\">Other fields</th>\n </tr></ng-template>\n <ng-template #body let-row><tr [id]=\"row.pathId\" tabindex=\"-1\">\n @for (column of section.columns; track column.label) {\n <td>\n @if (selected(row, column.path); as cell) {\n <sneat-structured-node [node]=\"cell\" [maxRows]=\"maxRows\" [maxDepth]=\"maxDepth\"\n (sourcePositionSelected)=\"sourcePositionSelected.emit($event)\" />\n } @else { <span class=\"sd-missing\">Missing</span> }\n </td>\n }\n <td>\n @let other = tableOtherFields(row.node, section);\n @if (other.length) {\n <details><summary>Other fields ({{other.length}})</summary>\n @for (field of limitedFields(other); track $index) {\n <div class=\"sd-field\"><strong>{{field.label}}</strong>\n <sneat-structured-node [node]=\"field.node\" [maxRows]=\"maxRows\" [maxDepth]=\"maxDepth\"\n (sourcePositionSelected)=\"sourcePositionSelected.emit($event)\" />\n </div>\n }\n @if (omittedFields(other); as omitted) { <p class=\"sd-limit\">{{omitted}} more fields; inspect Raw source</p> }\n </details>\n } @else if (tableFallbackNode(row.node)) {\n <sneat-structured-node [node]=\"row.node\" [maxRows]=\"maxRows\" [maxDepth]=\"maxDepth\"\n (sourcePositionSelected)=\"sourcePositionSelected.emit($event)\" />\n } @else { <span class=\"sd-empty\">None</span> }\n </td>\n </tr></ng-template>\n </p-table>\n @if (rowLimitNotice(section, context); as notice) { <p class=\"sd-limit\">{{notice}}</p> }\n } @else { <span class=\"sd-empty\">No rows</span> }\n }\n @case ('entries') {\n <ng-container *ngTemplateOutlet=\"collection; context: { $implicit: section, parent: context }\" />\n }\n @case ('blocks') {\n <ng-container *ngTemplateOutlet=\"collection; context: { $implicit: section, parent: context }\" />\n }\n }\n </section>\n }\n</ng-template>\n\n<ng-template #collection let-section let-parent=\"parent\">\n @let rows = rowsFor(section, parent);\n @if (rows.length) {\n <div class=\"sd-controls\" [attr.aria-label]=\"section.title + ' controls'\">\n @if (section.allowLayoutSwitch) {\n <button pButton type=\"button\" [attr.aria-pressed]=\"layoutFor(section, parent) === 'accordion'\"\n (click)=\"setLayout(section, parent, 'accordion')\">Accordion</button>\n <button pButton type=\"button\" [attr.aria-pressed]=\"layoutFor(section, parent) === 'list'\"\n (click)=\"setLayout(section, parent, 'list')\">List</button>\n }\n @if (layoutFor(section, parent) === 'accordion') {\n <p-button label=\"Expand all\" [text]=\"true\" (onClick)=\"expandAll(section, parent, rows)\" />\n <p-button label=\"Collapse all\" [text]=\"true\" (onClick)=\"collapseAll(section, parent)\" />\n }\n </div>\n @if (layoutFor(section, parent) === 'accordion') {\n <p-accordion [multiple]=\"true\" [value]=\"expandedValues(section, parent, rows)\"\n (valueChange)=\"setExpandedValues(section, parent, $event)\">\n @for (row of rows; track row.id) {\n <p-accordion-panel [value]=\"row.id\" [id]=\"row.pathId\" tabindex=\"-1\">\n <p-accordion-header>\n <span>{{row.title}}</span>\n </p-accordion-header>\n <p-accordion-content>\n <button type=\"button\" class=\"sd-section-link\" (click)=\"rowActivated(row)\"\n [attr.aria-label]=\"'Link to ' + row.title\">Link to section</button>\n @if (entrySections(section).length) {\n <ng-container *ngTemplateOutlet=\"sectionList; context: { $implicit: row, sectionItems: entrySections(section) }\" />\n @let other = otherFields(row.node, entrySections(section));\n @if (other.length) {\n <details><summary>Other fields ({{other.length}})</summary>\n @for (field of limitedFields(other); track $index) {\n <div class=\"sd-field\"><strong>{{field.label}}</strong>\n <sneat-structured-node [node]=\"field.node\" [maxRows]=\"maxRows\" [maxDepth]=\"maxDepth\"\n (sourcePositionSelected)=\"sourcePositionSelected.emit($event)\" />\n </div>\n }\n @if (omittedFields(other); as omitted) { <p class=\"sd-limit\">{{omitted}} more fields; inspect Raw source</p> }\n </details>\n }\n } @else {\n <sneat-structured-node [node]=\"row.node\" [maxRows]=\"maxRows\" [maxDepth]=\"maxDepth\"\n (sourcePositionSelected)=\"sourcePositionSelected.emit($event)\" />\n }\n </p-accordion-content>\n </p-accordion-panel>\n }\n </p-accordion>\n } @else {\n <div class=\"sd-full-list\">\n @for (row of rows; track row.id) {\n <article class=\"sd-entry\" [id]=\"row.pathId\" tabindex=\"-1\">\n <h4><button type=\"button\" class=\"sd-section-link\" (click)=\"rowActivated(row)\">{{row.title}}</button></h4>\n @if (entrySections(section).length) {\n <ng-container *ngTemplateOutlet=\"sectionList; context: { $implicit: row, sectionItems: entrySections(section) }\" />\n @let other = otherFields(row.node, entrySections(section));\n @if (other.length) {\n <details><summary>Other fields ({{other.length}})</summary>\n @for (field of limitedFields(other); track $index) {\n <div class=\"sd-field\"><strong>{{field.label}}</strong>\n <sneat-structured-node [node]=\"field.node\" [maxRows]=\"maxRows\" [maxDepth]=\"maxDepth\"\n (sourcePositionSelected)=\"sourcePositionSelected.emit($event)\" />\n </div>\n }\n @if (omittedFields(other); as omitted) { <p class=\"sd-limit\">{{omitted}} more fields; inspect Raw source</p> }\n </details>\n }\n } @else {\n <sneat-structured-node [node]=\"row.node\" [maxRows]=\"maxRows\" [maxDepth]=\"maxDepth\"\n (sourcePositionSelected)=\"sourcePositionSelected.emit($event)\" />\n }\n </article>\n }\n </div>\n }\n @if (rowLimitNotice(section, parent); as notice) { <p class=\"sd-limit\">{{notice}}</p> }\n } @else { <span class=\"sd-empty\">No entries</span> }\n</ng-template>\n", styles: [":host{display:block;min-width:0}.sd-viewer{max-width:100%;min-width:0;overflow-wrap:anywhere}.sd-section{margin-block:1rem}.sd-section h3,.sd-entry h4{margin:.5rem 0}.sd-section-link{border:0;padding:0;color:inherit;background:transparent;font:inherit;text-align:start;text-decoration:underline;cursor:pointer}.sd-controls{display:flex;flex-wrap:wrap;gap:.25rem;margin-block:.4rem}.sd-fields{display:grid;grid-template-columns:minmax(6rem,auto) minmax(0,1fr);gap:.4rem .8rem}.sd-fields dt{font-weight:600}.sd-fields dd{margin:0;min-width:0}.sd-field{display:flex;flex-wrap:wrap;gap:.4rem;margin-block:.3rem}.sd-entry{border-block-end:1px solid currentColor;padding-block:.5rem}.sd-entry:last-child{border:0}.sd-empty,.sd-missing,.sd-limit{opacity:.75}.sd-diagnostics{border-inline-start:.25rem solid currentColor;padding-inline-start:.7rem}.sd-raw{white-space:pre-wrap;overflow-wrap:anywhere;max-height:32rem;overflow:auto}@media(max-width:40rem){.sd-fields{display:block}.sd-fields dd{margin-block-end:.6rem}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: AccordionModule }, { kind: "component", type: i2.Accordion, selector: "p-accordion", inputs: ["value", "multiple", "expandIcon", "collapseIcon", "selectOnFocus", "motionOptions"], outputs: ["valueChange", "onClose", "onOpen"] }, { kind: "component", type: i2.AccordionPanel, selector: "p-accordion-panel, p-accordionpanel", inputs: ["value", "disabled"], outputs: ["valueChange"] }, { kind: "component", type: i2.AccordionHeader, selector: "p-accordion-header, p-accordionheader" }, { kind: "component", type: i2.AccordionContent, selector: "p-accordion-content, p-accordioncontent" }, { kind: "ngmodule", type: ButtonModule }, { kind: "directive", type: i3.ButtonDirective, selector: "[pButton]", inputs: ["pButton", "pButtonPT", "pButtonUnstyled", "hostName", "text", "plain", "raised", "size", "outlined", "link", "rounded", "fluid", "variant", "iconOnly", "loading", "severity"] }, { kind: "component", type: i3.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid", "iconOnly"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: TableModule }, { kind: "component", type: i4.Table, selector: "p-table", inputs: ["frozenColumns", "frozenValue", "tableStyle", "tableStyleClass", "paginator", "pageLinks", "rowsPerPageOptions", "alwaysShowPaginator", "paginatorPosition", "paginatorStyleClass", "paginatorDropdownAppendTo", "paginatorDropdownScrollHeight", "currentPageReportTemplate", "showCurrentPageReport", "showJumpToPageDropdown", "showJumpToPageInput", "showFirstLastIcon", "showPageLinks", "defaultSortOrder", "sortMode", "resetPageOnSort", "selectionMode", "selectionPageOnly", "contextMenuSelection", "dataKey", "metaKeySelection", "rowSelectable", "rowTrackBy", "lazy", "lazyLoadOnInit", "compareSelectionBy", "csvSeparator", "exportFilename", "filters", "globalFilterFields", "filterDelay", "filterLocale", "expandedRowKeys", "editingRowKeys", "rowExpandMode", "scrollable", "rowGroupMode", "scrollHeight", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "virtualScrollDelay", "frozenWidth", "contextMenu", "resizableColumns", "columnResizeMode", "reorderableColumns", "loading", "loadingIcon", "showLoader", "rowHover", "customSort", "showInitialSortBadge", "exportFunction", "exportHeader", "stateKey", "stateStorage", "editMode", "groupRowsBy", "size", "showGridlines", "stripedRows", "groupRowsByOrder", "paginatorLocale", "value", "columns", "first", "rows", "totalRecords", "sortField", "sortOrder", "multiSortMeta", "selection", "selectAll"], outputs: ["contextMenuSelectionChange", "firstChange", "rowsChange", "totalRecordsChange", "selectionChange", "selectAllChange", "onRowSelect", "onRowUnselect", "onPage", "onSort", "onFilter", "onLazyLoad", "onRowExpand", "onRowCollapse", "onContextMenuSelect", "onColResize", "onColReorder", "onRowReorder", "onEditInit", "onEditComplete", "onEditCancel", "onHeaderCheckboxToggle", "sortFunction", "onStateSave", "onStateRestore"] }, { kind: "component", type: StructuredNodeComponent, selector: "sneat-structured-node", inputs: ["node", "depth", "maxDepth", "maxRows", "scalarListLayout"], outputs: ["sourcePositionSelected"] }] }); }
|
|
1134
|
+
}
|
|
1135
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: StructuredDataViewerComponent, decorators: [{
|
|
1136
|
+
type: Component,
|
|
1137
|
+
args: [{ selector: 'sneat-structured-data-viewer', standalone: true, imports: [CommonModule, AccordionModule, ButtonModule, TableModule, StructuredNodeComponent], template: "<article class=\"sd-viewer\" aria-label=\"Structured data viewer\">\n @if (diagnostics.length) {\n <div class=\"sd-diagnostics\" role=\"alert\">\n @for (item of diagnostics; track $index) { <p>{{item.message}}</p> }\n </div>\n }\n\n @if (document; as parsed) {\n @if (titleText(); as title) { <h2>{{title}}</h2> }\n @if (rootContext; as root) {\n @if (sections.length) {\n <ng-container *ngTemplateOutlet=\"sectionList; context: { $implicit: root, sectionItems: sections }\" />\n @let remaining = otherFields(root.node, sections);\n @if (remaining.length) {\n <section class=\"sd-section\" aria-label=\"Other fields\">\n <h3>Other fields</h3>\n @for (field of limitedFields(remaining); track $index) {\n <div class=\"sd-field\"><strong>{{field.label}}</strong>\n <sneat-structured-node [node]=\"field.node\" [maxRows]=\"maxRows\" [maxDepth]=\"maxDepth\"\n (sourcePositionSelected)=\"sourcePositionSelected.emit($event)\" />\n </div>\n }\n @if (omittedFields(remaining); as omitted) { <p class=\"sd-limit\">{{omitted}} more fields; inspect Raw source</p> }\n </section>\n }\n } @else {\n <sneat-structured-node [node]=\"parsed.root\" [maxRows]=\"maxRows\" [maxDepth]=\"maxDepth\"\n (sourcePositionSelected)=\"sourcePositionSelected.emit($event)\" />\n }\n }\n } @else if (showRawFallback && rawFallbackText) {\n <pre class=\"sd-raw\">{{rawFallbackText}}</pre>\n }\n</article>\n\n<ng-template #sectionList let-context let-sectionItems=\"sectionItems\">\n @for (section of sectionItems; track section.id) {\n <section class=\"sd-section\" [id]=\"sectionId(section, context)\" tabindex=\"-1\">\n <h3><button type=\"button\" class=\"sd-section-link\" (click)=\"selectSection(sectionId(section, context))\">{{section.title}}</button></h3>\n @switch (section.kind) {\n @case ('value') {\n @if (selected(context, section.path); as node) {\n <sneat-structured-node [node]=\"node\" [scalarListLayout]=\"section.scalarListLayout || 'inline'\"\n [maxRows]=\"maxRows\" [maxDepth]=\"maxDepth\" (sourcePositionSelected)=\"sourcePositionSelected.emit($event)\" />\n } @else { <span class=\"sd-missing\">Missing</span> }\n }\n @case ('fields') {\n @if (selected(context, section.path); as node) {\n <dl class=\"sd-fields\">\n @for (field of section.fields; track field.label) {\n <dt>{{field.label}}</dt>\n <dd>\n @if (selected({node: node}, field.path); as fieldNode) {\n <sneat-structured-node [node]=\"fieldNode\" [scalarListLayout]=\"field.scalarListLayout || 'inline'\"\n [maxRows]=\"maxRows\" [maxDepth]=\"maxDepth\" (sourcePositionSelected)=\"sourcePositionSelected.emit($event)\" />\n } @else { <span class=\"sd-missing\">Missing</span> }\n </dd>\n }\n </dl>\n @let other = fieldOther(node, section);\n @if (other.length) {\n <details><summary>Other fields ({{other.length}})</summary>\n @for (field of limitedFields(other); track $index) {\n <div class=\"sd-field\"><strong>{{field.label}}</strong>\n <sneat-structured-node [node]=\"field.node\" [maxRows]=\"maxRows\" [maxDepth]=\"maxDepth\"\n (sourcePositionSelected)=\"sourcePositionSelected.emit($event)\" />\n </div>\n }\n @if (omittedFields(other); as omitted) { <p class=\"sd-limit\">{{omitted}} more fields; inspect Raw source</p> }\n </details>\n }\n } @else { <span class=\"sd-missing\">Missing</span> }\n }\n @case ('table') {\n @let rows = rowsFor(section, context);\n @if (rows.length) {\n <p-table [value]=\"rows\" [scrollable]=\"true\" styleClass=\"sd-table\">\n <ng-template #header><tr>\n @for (column of section.columns; track column.label) { <th scope=\"col\">{{column.label}}</th> }\n <th scope=\"col\">Other fields</th>\n </tr></ng-template>\n <ng-template #body let-row><tr [id]=\"row.pathId\" tabindex=\"-1\">\n @for (column of section.columns; track column.label) {\n <td>\n @if (selected(row, column.path); as cell) {\n <sneat-structured-node [node]=\"cell\" [maxRows]=\"maxRows\" [maxDepth]=\"maxDepth\"\n (sourcePositionSelected)=\"sourcePositionSelected.emit($event)\" />\n } @else { <span class=\"sd-missing\">Missing</span> }\n </td>\n }\n <td>\n @let other = tableOtherFields(row.node, section);\n @if (other.length) {\n <details><summary>Other fields ({{other.length}})</summary>\n @for (field of limitedFields(other); track $index) {\n <div class=\"sd-field\"><strong>{{field.label}}</strong>\n <sneat-structured-node [node]=\"field.node\" [maxRows]=\"maxRows\" [maxDepth]=\"maxDepth\"\n (sourcePositionSelected)=\"sourcePositionSelected.emit($event)\" />\n </div>\n }\n @if (omittedFields(other); as omitted) { <p class=\"sd-limit\">{{omitted}} more fields; inspect Raw source</p> }\n </details>\n } @else if (tableFallbackNode(row.node)) {\n <sneat-structured-node [node]=\"row.node\" [maxRows]=\"maxRows\" [maxDepth]=\"maxDepth\"\n (sourcePositionSelected)=\"sourcePositionSelected.emit($event)\" />\n } @else { <span class=\"sd-empty\">None</span> }\n </td>\n </tr></ng-template>\n </p-table>\n @if (rowLimitNotice(section, context); as notice) { <p class=\"sd-limit\">{{notice}}</p> }\n } @else { <span class=\"sd-empty\">No rows</span> }\n }\n @case ('entries') {\n <ng-container *ngTemplateOutlet=\"collection; context: { $implicit: section, parent: context }\" />\n }\n @case ('blocks') {\n <ng-container *ngTemplateOutlet=\"collection; context: { $implicit: section, parent: context }\" />\n }\n }\n </section>\n }\n</ng-template>\n\n<ng-template #collection let-section let-parent=\"parent\">\n @let rows = rowsFor(section, parent);\n @if (rows.length) {\n <div class=\"sd-controls\" [attr.aria-label]=\"section.title + ' controls'\">\n @if (section.allowLayoutSwitch) {\n <button pButton type=\"button\" [attr.aria-pressed]=\"layoutFor(section, parent) === 'accordion'\"\n (click)=\"setLayout(section, parent, 'accordion')\">Accordion</button>\n <button pButton type=\"button\" [attr.aria-pressed]=\"layoutFor(section, parent) === 'list'\"\n (click)=\"setLayout(section, parent, 'list')\">List</button>\n }\n @if (layoutFor(section, parent) === 'accordion') {\n <p-button label=\"Expand all\" [text]=\"true\" (onClick)=\"expandAll(section, parent, rows)\" />\n <p-button label=\"Collapse all\" [text]=\"true\" (onClick)=\"collapseAll(section, parent)\" />\n }\n </div>\n @if (layoutFor(section, parent) === 'accordion') {\n <p-accordion [multiple]=\"true\" [value]=\"expandedValues(section, parent, rows)\"\n (valueChange)=\"setExpandedValues(section, parent, $event)\">\n @for (row of rows; track row.id) {\n <p-accordion-panel [value]=\"row.id\" [id]=\"row.pathId\" tabindex=\"-1\">\n <p-accordion-header>\n <span>{{row.title}}</span>\n </p-accordion-header>\n <p-accordion-content>\n <button type=\"button\" class=\"sd-section-link\" (click)=\"rowActivated(row)\"\n [attr.aria-label]=\"'Link to ' + row.title\">Link to section</button>\n @if (entrySections(section).length) {\n <ng-container *ngTemplateOutlet=\"sectionList; context: { $implicit: row, sectionItems: entrySections(section) }\" />\n @let other = otherFields(row.node, entrySections(section));\n @if (other.length) {\n <details><summary>Other fields ({{other.length}})</summary>\n @for (field of limitedFields(other); track $index) {\n <div class=\"sd-field\"><strong>{{field.label}}</strong>\n <sneat-structured-node [node]=\"field.node\" [maxRows]=\"maxRows\" [maxDepth]=\"maxDepth\"\n (sourcePositionSelected)=\"sourcePositionSelected.emit($event)\" />\n </div>\n }\n @if (omittedFields(other); as omitted) { <p class=\"sd-limit\">{{omitted}} more fields; inspect Raw source</p> }\n </details>\n }\n } @else {\n <sneat-structured-node [node]=\"row.node\" [maxRows]=\"maxRows\" [maxDepth]=\"maxDepth\"\n (sourcePositionSelected)=\"sourcePositionSelected.emit($event)\" />\n }\n </p-accordion-content>\n </p-accordion-panel>\n }\n </p-accordion>\n } @else {\n <div class=\"sd-full-list\">\n @for (row of rows; track row.id) {\n <article class=\"sd-entry\" [id]=\"row.pathId\" tabindex=\"-1\">\n <h4><button type=\"button\" class=\"sd-section-link\" (click)=\"rowActivated(row)\">{{row.title}}</button></h4>\n @if (entrySections(section).length) {\n <ng-container *ngTemplateOutlet=\"sectionList; context: { $implicit: row, sectionItems: entrySections(section) }\" />\n @let other = otherFields(row.node, entrySections(section));\n @if (other.length) {\n <details><summary>Other fields ({{other.length}})</summary>\n @for (field of limitedFields(other); track $index) {\n <div class=\"sd-field\"><strong>{{field.label}}</strong>\n <sneat-structured-node [node]=\"field.node\" [maxRows]=\"maxRows\" [maxDepth]=\"maxDepth\"\n (sourcePositionSelected)=\"sourcePositionSelected.emit($event)\" />\n </div>\n }\n @if (omittedFields(other); as omitted) { <p class=\"sd-limit\">{{omitted}} more fields; inspect Raw source</p> }\n </details>\n }\n } @else {\n <sneat-structured-node [node]=\"row.node\" [maxRows]=\"maxRows\" [maxDepth]=\"maxDepth\"\n (sourcePositionSelected)=\"sourcePositionSelected.emit($event)\" />\n }\n </article>\n }\n </div>\n }\n @if (rowLimitNotice(section, parent); as notice) { <p class=\"sd-limit\">{{notice}}</p> }\n } @else { <span class=\"sd-empty\">No entries</span> }\n</ng-template>\n", styles: [":host{display:block;min-width:0}.sd-viewer{max-width:100%;min-width:0;overflow-wrap:anywhere}.sd-section{margin-block:1rem}.sd-section h3,.sd-entry h4{margin:.5rem 0}.sd-section-link{border:0;padding:0;color:inherit;background:transparent;font:inherit;text-align:start;text-decoration:underline;cursor:pointer}.sd-controls{display:flex;flex-wrap:wrap;gap:.25rem;margin-block:.4rem}.sd-fields{display:grid;grid-template-columns:minmax(6rem,auto) minmax(0,1fr);gap:.4rem .8rem}.sd-fields dt{font-weight:600}.sd-fields dd{margin:0;min-width:0}.sd-field{display:flex;flex-wrap:wrap;gap:.4rem;margin-block:.3rem}.sd-entry{border-block-end:1px solid currentColor;padding-block:.5rem}.sd-entry:last-child{border:0}.sd-empty,.sd-missing,.sd-limit{opacity:.75}.sd-diagnostics{border-inline-start:.25rem solid currentColor;padding-inline-start:.7rem}.sd-raw{white-space:pre-wrap;overflow-wrap:anywhere;max-height:32rem;overflow:auto}@media(max-width:40rem){.sd-fields{display:block}.sd-fields dd{margin-block-end:.6rem}}\n"] }]
|
|
1138
|
+
}], propDecorators: { document: [{
|
|
1139
|
+
type: Input
|
|
1140
|
+
}], view: [{
|
|
1141
|
+
type: Input
|
|
1142
|
+
}], activeSectionId: [{
|
|
1143
|
+
type: Input
|
|
1144
|
+
}], diagnostics: [{
|
|
1145
|
+
type: Input
|
|
1146
|
+
}], sourceText: [{
|
|
1147
|
+
type: Input
|
|
1148
|
+
}], showRawFallback: [{
|
|
1149
|
+
type: Input
|
|
1150
|
+
}], limits: [{
|
|
1151
|
+
type: Input
|
|
1152
|
+
}], sourcePositionSelected: [{
|
|
1153
|
+
type: Output
|
|
1154
|
+
}], sectionSelected: [{
|
|
1155
|
+
type: Output
|
|
1156
|
+
}] } });
|
|
1157
|
+
|
|
1158
|
+
/**
|
|
1159
|
+
* Generated bundle index. Do not edit.
|
|
1160
|
+
*/
|
|
1161
|
+
|
|
1162
|
+
export { DEFAULT_LIMITS, StructuredDataViewerComponent, parseHclText, parseJsonText, parseRelativePointer, parseStructuredText, parseViewConfigText, parseYamlText, resolveViewRule, selectRelative, selectViewPath, validateViewConfig };
|
|
1163
|
+
//# sourceMappingURL=sneat-structured-data.mjs.map
|