@brixter/brix-builder 0.0.1
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/LICENSE +21 -0
- package/README.md +25 -0
- package/dist/core.d.ts +103 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +758 -0
- package/dist/core.js.map +1 -0
- package/dist/editor/BuilderApp.svelte +1299 -0
- package/dist/editor/BuilderFieldEditor.svelte +274 -0
- package/dist/editor/BuilderInspector.svelte +123 -0
- package/dist/editor/BuilderPreviewFrame.svelte +661 -0
- package/dist/editor/ComponentPreviewThumbnail.svelte +197 -0
- package/dist/editor/PageFlowSidebar.svelte +198 -0
- package/dist/editor/PreviewBlockInserter.svelte +35 -0
- package/dist/editor/PreviewIconEditor.svelte +213 -0
- package/dist/editor/PreviewImageEditor.svelte +221 -0
- package/dist/editor/PreviewTextEditor.svelte +246 -0
- package/dist/editor/RichTextEditor.svelte +234 -0
- package/dist/editor/contracts.d.ts +57 -0
- package/dist/editor/contracts.d.ts.map +1 -0
- package/dist/editor/contracts.js +2 -0
- package/dist/editor/contracts.js.map +1 -0
- package/dist/editor/index.d.ts +3 -0
- package/dist/editor/index.d.ts.map +1 -0
- package/dist/editor/index.js +2 -0
- package/dist/editor/index.js.map +1 -0
- package/dist/editor/shortcuts.d.ts +28 -0
- package/dist/editor/shortcuts.d.ts.map +1 -0
- package/dist/editor/shortcuts.js +28 -0
- package/dist/editor/shortcuts.js.map +1 -0
- package/dist/editor-controller.d.ts +50 -0
- package/dist/editor-controller.d.ts.map +1 -0
- package/dist/editor-controller.js +157 -0
- package/dist/editor-controller.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/preview/field-edit-debug.d.ts +5 -0
- package/dist/preview/field-edit-debug.d.ts.map +1 -0
- package/dist/preview/field-edit-debug.js +36 -0
- package/dist/preview/field-edit-debug.js.map +1 -0
- package/dist/preview/interactive-content.d.ts +8 -0
- package/dist/preview/interactive-content.d.ts.map +1 -0
- package/dist/preview/interactive-content.js +62 -0
- package/dist/preview/interactive-content.js.map +1 -0
- package/dist/preview-dom.d.ts +67 -0
- package/dist/preview-dom.d.ts.map +1 -0
- package/dist/preview-dom.js +191 -0
- package/dist/preview-dom.js.map +1 -0
- package/dist/svelte/SveltePreviewRenderer.svelte +490 -0
- package/dist/svelte/adapter.d.ts +7 -0
- package/dist/svelte/adapter.d.ts.map +1 -0
- package/dist/svelte/adapter.js +66 -0
- package/dist/svelte/adapter.js.map +1 -0
- package/dist/svelte/index.d.ts +3 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +3 -0
- package/dist/svelte/index.js.map +1 -0
- package/dist/svelte/markup-schema.d.ts +5 -0
- package/dist/svelte/markup-schema.d.ts.map +1 -0
- package/dist/svelte/markup-schema.js +177 -0
- package/dist/svelte/markup-schema.js.map +1 -0
- package/package.json +56 -0
package/dist/core.js
ADDED
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
import yaml from 'yaml';
|
|
2
|
+
const { parse: parseYaml, stringify: stringifyYaml } = yaml;
|
|
3
|
+
export function createRichTextValue(mode, html = '') {
|
|
4
|
+
return {
|
|
5
|
+
kind: 'richtext',
|
|
6
|
+
mode,
|
|
7
|
+
html,
|
|
8
|
+
json: null
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function isRichTextValue(value) {
|
|
12
|
+
return (isRecord(value) &&
|
|
13
|
+
value.kind === 'richtext' &&
|
|
14
|
+
(value.mode === 'inline' || value.mode === 'block') &&
|
|
15
|
+
typeof value.html === 'string' &&
|
|
16
|
+
(value.json === null || isRecord(value.json)));
|
|
17
|
+
}
|
|
18
|
+
export function inferBuilderFieldKind(field) {
|
|
19
|
+
if (field.kind) {
|
|
20
|
+
return field.kind;
|
|
21
|
+
}
|
|
22
|
+
if (field.fields) {
|
|
23
|
+
return 'object';
|
|
24
|
+
}
|
|
25
|
+
if (field.item) {
|
|
26
|
+
return 'array';
|
|
27
|
+
}
|
|
28
|
+
if (typeof field.default === 'boolean') {
|
|
29
|
+
return 'boolean';
|
|
30
|
+
}
|
|
31
|
+
if (typeof field.default === 'number') {
|
|
32
|
+
return 'number';
|
|
33
|
+
}
|
|
34
|
+
return 'text';
|
|
35
|
+
}
|
|
36
|
+
export function createBuilderDefaultsFromFields(fields) {
|
|
37
|
+
return Object.fromEntries(Object.entries(fields).map(([key, field]) => [key, createBuilderFieldDefault(field)]));
|
|
38
|
+
}
|
|
39
|
+
export function createBuilderFallbackProps(definition, props = definition.defaults) {
|
|
40
|
+
return mergeFallbackValues(createBuilderFallbackPropsFromFields(definition.fields), props);
|
|
41
|
+
}
|
|
42
|
+
function createBuilderFallbackPropsFromFields(fields) {
|
|
43
|
+
return Object.fromEntries(Object.entries(fields).map(([key, field]) => [key, createBuilderFallbackValue(field, key)]));
|
|
44
|
+
}
|
|
45
|
+
function createBuilderFallbackValue(field, key) {
|
|
46
|
+
const kind = inferBuilderFieldKind(field);
|
|
47
|
+
if (kind === 'richtext-inline') {
|
|
48
|
+
return createRichTextValue('inline', getFallbackText(key));
|
|
49
|
+
}
|
|
50
|
+
if (kind === 'richtext-block') {
|
|
51
|
+
return createRichTextValue('block', `<p>${getFallbackText(key)}</p>`);
|
|
52
|
+
}
|
|
53
|
+
if (kind === 'object') {
|
|
54
|
+
return field.fields ? createBuilderFallbackPropsFromFields(field.fields) : {};
|
|
55
|
+
}
|
|
56
|
+
if (kind === 'array') {
|
|
57
|
+
const item = field.item ? createBuilderFallbackValue(field.item, field.itemLabel ?? key) : {};
|
|
58
|
+
return [item, cloneValue(item), cloneValue(item)];
|
|
59
|
+
}
|
|
60
|
+
if (kind === 'boolean') {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
if (kind === 'number') {
|
|
64
|
+
return 3;
|
|
65
|
+
}
|
|
66
|
+
if (kind === 'image') {
|
|
67
|
+
return createImageFallback();
|
|
68
|
+
}
|
|
69
|
+
if (kind === 'icon') {
|
|
70
|
+
return createIconFallback();
|
|
71
|
+
}
|
|
72
|
+
if (isHrefKey(key)) {
|
|
73
|
+
return '#';
|
|
74
|
+
}
|
|
75
|
+
return getFallbackText(key);
|
|
76
|
+
}
|
|
77
|
+
function mergeFallbackValues(fallbackValue, value) {
|
|
78
|
+
if (!hasRenderableValue(value)) {
|
|
79
|
+
return fallbackValue;
|
|
80
|
+
}
|
|
81
|
+
if (isRichTextValue(value)) {
|
|
82
|
+
return value.html.trim() ? value : fallbackValue;
|
|
83
|
+
}
|
|
84
|
+
if (Array.isArray(value)) {
|
|
85
|
+
return value.length > 0 ? value : fallbackValue;
|
|
86
|
+
}
|
|
87
|
+
if (isRecord(value) && isRecord(fallbackValue)) {
|
|
88
|
+
return {
|
|
89
|
+
...value,
|
|
90
|
+
...Object.fromEntries(Object.entries(fallbackValue).map(([key, fallbackEntry]) => [
|
|
91
|
+
key,
|
|
92
|
+
mergeFallbackValues(fallbackEntry, value[key])
|
|
93
|
+
]))
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
function hasRenderableValue(value) {
|
|
99
|
+
if (isRichTextValue(value)) {
|
|
100
|
+
return value.html.trim().length > 0;
|
|
101
|
+
}
|
|
102
|
+
if (Array.isArray(value)) {
|
|
103
|
+
return value.length > 0;
|
|
104
|
+
}
|
|
105
|
+
if (typeof value === 'string') {
|
|
106
|
+
return value.trim().length > 0;
|
|
107
|
+
}
|
|
108
|
+
return value !== null && value !== undefined;
|
|
109
|
+
}
|
|
110
|
+
export function createBuilderCollectionsFromFields(fields, basePath = '') {
|
|
111
|
+
const collections = [];
|
|
112
|
+
for (const [name, field] of Object.entries(fields)) {
|
|
113
|
+
const path = basePath ? `${basePath}.${name}` : name;
|
|
114
|
+
const kind = inferBuilderFieldKind(field);
|
|
115
|
+
if (kind === 'array' && field.item) {
|
|
116
|
+
const defaultItem = createBuilderFieldDefault(field.item);
|
|
117
|
+
const previewField = findPreviewImageField(field.item);
|
|
118
|
+
if (isRecord(defaultItem)) {
|
|
119
|
+
const summaryField = field.summaryField ?? inferSummaryField(field.item);
|
|
120
|
+
collections.push({
|
|
121
|
+
path,
|
|
122
|
+
label: field.label ?? humanizeKey(name),
|
|
123
|
+
itemLabel: field.itemLabel ?? humanizeKey(name).replace(/i$/i, 'o'),
|
|
124
|
+
defaultItem,
|
|
125
|
+
summaryField,
|
|
126
|
+
imageField: field.imageField ?? previewField?.path,
|
|
127
|
+
previewSelector: field.previewSelector ?? getCollectionPreviewSelector(path)
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (kind === 'object' && field.fields) {
|
|
133
|
+
collections.push(...createBuilderCollectionsFromFields(field.fields, path));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return collections;
|
|
137
|
+
}
|
|
138
|
+
export function createBuilderPreviewBindingsFromFields(fields, basePath = '') {
|
|
139
|
+
const bindings = [];
|
|
140
|
+
for (const [name, field] of Object.entries(fields)) {
|
|
141
|
+
const kind = inferBuilderFieldKind(field);
|
|
142
|
+
const path = basePath ? `${basePath}.${name}` : name;
|
|
143
|
+
const selector = getFieldPreviewSelector(field, path);
|
|
144
|
+
if (kind === 'image' && selector) {
|
|
145
|
+
bindings.push({
|
|
146
|
+
type: 'image',
|
|
147
|
+
selector,
|
|
148
|
+
path,
|
|
149
|
+
label: field.previewLabel ??
|
|
150
|
+
(field.label ? `Sostituisci ${field.label.toLowerCase()}` : undefined)
|
|
151
|
+
});
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (kind === 'icon' && selector) {
|
|
155
|
+
bindings.push({
|
|
156
|
+
type: 'icon',
|
|
157
|
+
selector,
|
|
158
|
+
path,
|
|
159
|
+
label: field.previewLabel ?? field.label
|
|
160
|
+
});
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if ((kind === 'richtext-inline' || kind === 'richtext-block') && selector) {
|
|
164
|
+
bindings.push({
|
|
165
|
+
type: 'richtext',
|
|
166
|
+
selector,
|
|
167
|
+
path,
|
|
168
|
+
label: field.previewLabel ?? field.label,
|
|
169
|
+
richTextMode: kind === 'richtext-inline' ? 'inline' : 'block'
|
|
170
|
+
});
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (kind === 'text' && selector) {
|
|
174
|
+
bindings.push({
|
|
175
|
+
type: 'text',
|
|
176
|
+
selector,
|
|
177
|
+
path,
|
|
178
|
+
label: field.previewLabel ?? field.label
|
|
179
|
+
});
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (kind === 'object' && field.fields) {
|
|
183
|
+
bindings.push(...createBuilderPreviewBindingsFromFields(field.fields, path));
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (kind === 'array' && field.item) {
|
|
187
|
+
const itemPath = `${path}[]`;
|
|
188
|
+
if (field.item.fields) {
|
|
189
|
+
bindings.push(...createBuilderPreviewBindingsFromFields(field.item.fields, itemPath));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return bindings;
|
|
194
|
+
}
|
|
195
|
+
export function createInspectorFieldsFromFields(fields) {
|
|
196
|
+
return Object.fromEntries(Object.entries(fields)
|
|
197
|
+
.map(([key, field]) => [key, createInspectorField(field)])
|
|
198
|
+
.filter((entry) => Boolean(entry[1])));
|
|
199
|
+
}
|
|
200
|
+
function createInspectorField(field) {
|
|
201
|
+
const kind = inferBuilderFieldKind(field);
|
|
202
|
+
const isPreviewEditable = Boolean(field.previewSelector || field.previewInMarkup) &&
|
|
203
|
+
(kind === 'text' ||
|
|
204
|
+
kind === 'image' ||
|
|
205
|
+
kind === 'icon' ||
|
|
206
|
+
kind === 'richtext-inline' ||
|
|
207
|
+
kind === 'richtext-block');
|
|
208
|
+
if (isPreviewEditable) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
if (kind === 'object' && field.fields) {
|
|
212
|
+
const nestedFields = createInspectorFieldsFromFields(field.fields);
|
|
213
|
+
if (Object.keys(nestedFields).length === 0) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
...field,
|
|
218
|
+
fields: nestedFields
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
if (kind === 'array' && field.item?.fields) {
|
|
222
|
+
const nestedItemFields = createInspectorFieldsFromFields(field.item.fields);
|
|
223
|
+
if (Object.keys(nestedItemFields).length === 0) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
...field,
|
|
228
|
+
item: {
|
|
229
|
+
...field.item,
|
|
230
|
+
fields: nestedItemFields
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
return cloneValue(field);
|
|
235
|
+
}
|
|
236
|
+
export function normalizeBuilderPropsForRender(value) {
|
|
237
|
+
if (isRichTextValue(value)) {
|
|
238
|
+
return value.html;
|
|
239
|
+
}
|
|
240
|
+
if (Array.isArray(value)) {
|
|
241
|
+
return value.map((entry) => normalizeBuilderPropsForRender(entry));
|
|
242
|
+
}
|
|
243
|
+
if (isRecord(value)) {
|
|
244
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, normalizeBuilderPropsForRender(entry)]));
|
|
245
|
+
}
|
|
246
|
+
return value;
|
|
247
|
+
}
|
|
248
|
+
export function createBuilderDocument(definitions) {
|
|
249
|
+
const markdownBlock = definitions.find((definition) => definition.mode === 'markdown');
|
|
250
|
+
const firstComponentBlock = definitions.find((definition) => definition.mode === 'component');
|
|
251
|
+
const initialBlocks = [];
|
|
252
|
+
if (markdownBlock) {
|
|
253
|
+
initialBlocks.push(markdownBlock);
|
|
254
|
+
}
|
|
255
|
+
if (firstComponentBlock) {
|
|
256
|
+
initialBlocks.push(firstComponentBlock);
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
title: 'Pagina Brixter',
|
|
260
|
+
description: 'Bozza generata da Brixter.',
|
|
261
|
+
metadata: {},
|
|
262
|
+
blocks: initialBlocks.map((definition) => createBlock(definition.type, definitions))
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
export function createBlock(type, definitions) {
|
|
266
|
+
const definition = getDefinition(type, definitions);
|
|
267
|
+
return {
|
|
268
|
+
id: createId(),
|
|
269
|
+
type: definition.type,
|
|
270
|
+
props: cloneValue(createBuilderFallbackProps(definition))
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
export function getDefinition(type, definitions) {
|
|
274
|
+
const definition = definitions.find((entry) => entry.type === type);
|
|
275
|
+
if (!definition) {
|
|
276
|
+
throw new Error(`Unknown brik type: ${type}`);
|
|
277
|
+
}
|
|
278
|
+
return definition;
|
|
279
|
+
}
|
|
280
|
+
export function getPropsDraft(block) {
|
|
281
|
+
return JSON.stringify(block.props, null, 2);
|
|
282
|
+
}
|
|
283
|
+
export function parsePropsDraft(value) {
|
|
284
|
+
if (!value.trim()) {
|
|
285
|
+
return {};
|
|
286
|
+
}
|
|
287
|
+
const parsed = JSON.parse(value);
|
|
288
|
+
if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object') {
|
|
289
|
+
throw new Error('Le props devono essere un oggetto JSON.');
|
|
290
|
+
}
|
|
291
|
+
return parsed;
|
|
292
|
+
}
|
|
293
|
+
export function updatePropsAtPath(props, path, value) {
|
|
294
|
+
const clone = cloneValue(props);
|
|
295
|
+
const segments = parsePath(path);
|
|
296
|
+
if (segments.length === 0) {
|
|
297
|
+
throw new Error('Path non valido.');
|
|
298
|
+
}
|
|
299
|
+
let current = clone;
|
|
300
|
+
for (let index = 0; index < segments.length - 1; index++) {
|
|
301
|
+
const segment = segments[index];
|
|
302
|
+
const nextSegment = segments[index + 1];
|
|
303
|
+
if (typeof segment === 'number') {
|
|
304
|
+
if (!Array.isArray(current)) {
|
|
305
|
+
throw new Error(`Il segmento ${segment} richiede un array.`);
|
|
306
|
+
}
|
|
307
|
+
if (current[segment] === undefined) {
|
|
308
|
+
current[segment] = typeof nextSegment === 'number' ? [] : {};
|
|
309
|
+
}
|
|
310
|
+
current = current[segment];
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (!current || typeof current !== 'object' || Array.isArray(current)) {
|
|
314
|
+
throw new Error(`Il segmento "${segment}" richiede un oggetto.`);
|
|
315
|
+
}
|
|
316
|
+
const record = current;
|
|
317
|
+
if (record[segment] === undefined) {
|
|
318
|
+
record[segment] = typeof nextSegment === 'number' ? [] : {};
|
|
319
|
+
}
|
|
320
|
+
current = record[segment];
|
|
321
|
+
}
|
|
322
|
+
const lastSegment = segments.at(-1);
|
|
323
|
+
if (lastSegment === undefined) {
|
|
324
|
+
throw new Error('Path non valido.');
|
|
325
|
+
}
|
|
326
|
+
if (typeof lastSegment === 'number') {
|
|
327
|
+
if (!Array.isArray(current)) {
|
|
328
|
+
throw new Error(`Il segmento ${lastSegment} richiede un array.`);
|
|
329
|
+
}
|
|
330
|
+
current[lastSegment] = value;
|
|
331
|
+
return clone;
|
|
332
|
+
}
|
|
333
|
+
if (!current || typeof current !== 'object' || Array.isArray(current)) {
|
|
334
|
+
throw new Error(`Il segmento "${lastSegment}" richiede un oggetto.`);
|
|
335
|
+
}
|
|
336
|
+
current[lastSegment] = value;
|
|
337
|
+
return clone;
|
|
338
|
+
}
|
|
339
|
+
export function getValueAtPath(props, path) {
|
|
340
|
+
const segments = parsePath(path);
|
|
341
|
+
let current = props;
|
|
342
|
+
for (const segment of segments) {
|
|
343
|
+
if (typeof segment === 'number') {
|
|
344
|
+
if (!Array.isArray(current)) {
|
|
345
|
+
return undefined;
|
|
346
|
+
}
|
|
347
|
+
current = current[segment];
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (!current || typeof current !== 'object' || Array.isArray(current)) {
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
current = current[segment];
|
|
354
|
+
}
|
|
355
|
+
return current;
|
|
356
|
+
}
|
|
357
|
+
export function getCollectionItems(props, collection) {
|
|
358
|
+
const value = getValueAtPath(props, collection.path);
|
|
359
|
+
if (!Array.isArray(value)) {
|
|
360
|
+
return [];
|
|
361
|
+
}
|
|
362
|
+
return value.filter(isRecord);
|
|
363
|
+
}
|
|
364
|
+
export function addCollectionItem(props, collection) {
|
|
365
|
+
const items = getCollectionItems(props, collection);
|
|
366
|
+
return updatePropsAtPath(props, collection.path, [...items, cloneValue(collection.defaultItem)]);
|
|
367
|
+
}
|
|
368
|
+
export function removeCollectionItem(props, collection, index) {
|
|
369
|
+
const items = getCollectionItems(props, collection);
|
|
370
|
+
if (index < 0 || index >= items.length) {
|
|
371
|
+
return props;
|
|
372
|
+
}
|
|
373
|
+
return updatePropsAtPath(props, collection.path, items.filter((_, itemIndex) => itemIndex !== index));
|
|
374
|
+
}
|
|
375
|
+
export function moveCollectionItem(props, collection, index, direction) {
|
|
376
|
+
return reorderCollectionItem(props, collection, index, index + direction);
|
|
377
|
+
}
|
|
378
|
+
export function reorderCollectionItem(props, collection, fromIndex, toIndex) {
|
|
379
|
+
const items = [...getCollectionItems(props, collection)];
|
|
380
|
+
if (fromIndex < 0 ||
|
|
381
|
+
fromIndex >= items.length ||
|
|
382
|
+
toIndex < 0 ||
|
|
383
|
+
toIndex >= items.length ||
|
|
384
|
+
fromIndex === toIndex) {
|
|
385
|
+
return props;
|
|
386
|
+
}
|
|
387
|
+
const [item] = items.splice(fromIndex, 1);
|
|
388
|
+
items.splice(toIndex, 0, item);
|
|
389
|
+
return updatePropsAtPath(props, collection.path, items);
|
|
390
|
+
}
|
|
391
|
+
export function getCollectionItemSummary(item, collection, index) {
|
|
392
|
+
if (collection.summaryField) {
|
|
393
|
+
const summary = getValueAtPath(item, collection.summaryField);
|
|
394
|
+
if (typeof summary === 'string' && summary.trim()) {
|
|
395
|
+
return summary;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return `${collection.itemLabel} ${index + 1}`;
|
|
399
|
+
}
|
|
400
|
+
export function getCollectionItemImagePath(collection, index) {
|
|
401
|
+
if (!collection.imageField) {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
return `${collection.path}[${index}].${collection.imageField}`;
|
|
405
|
+
}
|
|
406
|
+
export function serializeToMdsvex(document, definitions) {
|
|
407
|
+
const sections = [
|
|
408
|
+
'---',
|
|
409
|
+
`title: ${document.title}`,
|
|
410
|
+
`description: ${document.description}`,
|
|
411
|
+
'---'
|
|
412
|
+
];
|
|
413
|
+
const componentBlocks = document.blocks
|
|
414
|
+
.map((block, index) => ({ block, index, definition: getDefinition(block.type, definitions) }))
|
|
415
|
+
.filter((entry) => entry.definition.mode === 'component');
|
|
416
|
+
if (componentBlocks.length > 0) {
|
|
417
|
+
const scriptLines = ['<script>'];
|
|
418
|
+
const importedTypes = new Set();
|
|
419
|
+
for (const { definition } of componentBlocks) {
|
|
420
|
+
if (importedTypes.has(definition.type))
|
|
421
|
+
continue;
|
|
422
|
+
scriptLines.push(`\timport ${definition.type} from '${definition.path}';`);
|
|
423
|
+
importedTypes.add(definition.type);
|
|
424
|
+
}
|
|
425
|
+
for (const { block, index } of componentBlocks) {
|
|
426
|
+
scriptLines.push(`\tconst blockProps${index + 1} = ${JSON.stringify(normalizeBuilderPropsForRender(block.props), null, 2)};`);
|
|
427
|
+
}
|
|
428
|
+
scriptLines.push('</script>');
|
|
429
|
+
sections.push(scriptLines.join('\n'));
|
|
430
|
+
}
|
|
431
|
+
for (const [index, block] of document.blocks.entries()) {
|
|
432
|
+
const definition = getDefinition(block.type, definitions);
|
|
433
|
+
if (definition.mode === 'markdown') {
|
|
434
|
+
const content = typeof block.props.content === 'string' ? block.props.content : '';
|
|
435
|
+
sections.push(content.trim() || '<!-- Brik markdown vuoto -->');
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
sections.push(`<${definition.type} {...blockProps${index + 1}} />`);
|
|
439
|
+
}
|
|
440
|
+
return sections.join('\n\n');
|
|
441
|
+
}
|
|
442
|
+
export function parseBrixYamlDocument(source, definitions) {
|
|
443
|
+
const parsed = parseYaml(source);
|
|
444
|
+
const rawDocument = isRecord(parsed) ? parsed : {};
|
|
445
|
+
const components = Array.isArray(rawDocument.components) ? rawDocument.components : [];
|
|
446
|
+
const metadata = getBrixYamlMetadata(rawDocument);
|
|
447
|
+
const title = typeof rawDocument.title === 'string' ? rawDocument.title : 'Pagina Brixter';
|
|
448
|
+
const description = typeof rawDocument.description === 'string'
|
|
449
|
+
? rawDocument.description
|
|
450
|
+
: 'Bozza generata da Brixter.';
|
|
451
|
+
const layout = typeof rawDocument.layout === 'string' && rawDocument.layout.trim()
|
|
452
|
+
? rawDocument.layout.trim()
|
|
453
|
+
: undefined;
|
|
454
|
+
const blocks = [];
|
|
455
|
+
for (const component of components) {
|
|
456
|
+
if (!isRecord(component) || typeof component.type !== 'string') {
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
const definition = findDefinitionForBrixType(component.type, definitions);
|
|
460
|
+
if (!definition || definition.mode !== 'component') {
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
blocks.push({
|
|
464
|
+
id: createId(),
|
|
465
|
+
type: definition.type,
|
|
466
|
+
props: hydrateBuilderProps(isRecord(component.props) ? component.props : {}, definition.fields, definition.defaults)
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
return {
|
|
470
|
+
title,
|
|
471
|
+
description,
|
|
472
|
+
layout,
|
|
473
|
+
metadata,
|
|
474
|
+
blocks
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
export function serializeToBrixYaml(document, definitions) {
|
|
478
|
+
const output = {
|
|
479
|
+
title: document.title,
|
|
480
|
+
description: document.description
|
|
481
|
+
};
|
|
482
|
+
for (const [key, value] of Object.entries(document.metadata ?? {})) {
|
|
483
|
+
if (key === 'title' || key === 'description' || key === 'layout' || key === 'components') {
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
output[key] = cloneValue(value);
|
|
487
|
+
}
|
|
488
|
+
if (document.layout) {
|
|
489
|
+
output.layout = document.layout;
|
|
490
|
+
}
|
|
491
|
+
output.components = document.blocks
|
|
492
|
+
.map((block) => ({ block, definition: getDefinition(block.type, definitions) }))
|
|
493
|
+
.filter((entry) => entry.definition.mode === 'component')
|
|
494
|
+
.map(({ block }) => ({
|
|
495
|
+
type: block.type,
|
|
496
|
+
props: normalizeBuilderPropsForRender(block.props)
|
|
497
|
+
}));
|
|
498
|
+
return stringifyYaml(output).trimEnd() + '\n';
|
|
499
|
+
}
|
|
500
|
+
function cloneValue(value) {
|
|
501
|
+
return JSON.parse(JSON.stringify(value));
|
|
502
|
+
}
|
|
503
|
+
function getBrixYamlMetadata(document) {
|
|
504
|
+
return Object.fromEntries(Object.entries(document)
|
|
505
|
+
.filter(([key]) => key !== 'title' && key !== 'description' && key !== 'layout' && key !== 'components')
|
|
506
|
+
.map(([key, value]) => [key, cloneValue(value)]));
|
|
507
|
+
}
|
|
508
|
+
function findDefinitionForBrixType(type, definitions) {
|
|
509
|
+
const normalized = toComponentName(type);
|
|
510
|
+
return definitions.find((definition) => definition.type === type || definition.type === normalized);
|
|
511
|
+
}
|
|
512
|
+
function toComponentName(value) {
|
|
513
|
+
const normalized = value.trim().replace(/\.(svelte|ts|js)$/i, '');
|
|
514
|
+
if (/^[A-Z][A-Za-z0-9]*$/.test(normalized))
|
|
515
|
+
return normalized;
|
|
516
|
+
return normalized
|
|
517
|
+
.split(/[-_\s/]+/)
|
|
518
|
+
.filter(Boolean)
|
|
519
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
520
|
+
.join('');
|
|
521
|
+
}
|
|
522
|
+
function hydrateBuilderProps(props, fields, defaults) {
|
|
523
|
+
const hydrated = cloneValue(defaults);
|
|
524
|
+
for (const [key, value] of Object.entries(props)) {
|
|
525
|
+
const field = fields[key];
|
|
526
|
+
hydrated[key] = field ? hydrateBuilderValue(value, field, defaults[key]) : cloneValue(value);
|
|
527
|
+
}
|
|
528
|
+
return hydrated;
|
|
529
|
+
}
|
|
530
|
+
function hydrateBuilderValue(value, field, defaultValue) {
|
|
531
|
+
const kind = inferBuilderFieldKind(field);
|
|
532
|
+
if (kind === 'richtext-inline' || kind === 'richtext-block') {
|
|
533
|
+
if (isRichTextValue(value)) {
|
|
534
|
+
return cloneValue(value);
|
|
535
|
+
}
|
|
536
|
+
return createRichTextValue(kind === 'richtext-inline' ? 'inline' : 'block', asString(value));
|
|
537
|
+
}
|
|
538
|
+
if (kind === 'object') {
|
|
539
|
+
const nestedDefaults = isRecord(defaultValue)
|
|
540
|
+
? defaultValue
|
|
541
|
+
: field.fields
|
|
542
|
+
? createBuilderDefaultsFromFields(field.fields)
|
|
543
|
+
: {};
|
|
544
|
+
if (!isRecord(value)) {
|
|
545
|
+
return cloneValue(nestedDefaults);
|
|
546
|
+
}
|
|
547
|
+
return field.fields
|
|
548
|
+
? hydrateBuilderProps(value, field.fields, nestedDefaults)
|
|
549
|
+
: cloneValue(value);
|
|
550
|
+
}
|
|
551
|
+
if (kind === 'array') {
|
|
552
|
+
if (!Array.isArray(value)) {
|
|
553
|
+
return [];
|
|
554
|
+
}
|
|
555
|
+
return value.map((entry) => field.item ? hydrateBuilderValue(entry, field.item, undefined) : cloneValue(entry));
|
|
556
|
+
}
|
|
557
|
+
return cloneValue(value);
|
|
558
|
+
}
|
|
559
|
+
function createBuilderFieldDefault(field, defaultValue = field.default) {
|
|
560
|
+
const kind = inferBuilderFieldKind(field);
|
|
561
|
+
if (kind === 'richtext-inline' || kind === 'richtext-block') {
|
|
562
|
+
return createRichTextValue(kind === 'richtext-inline' ? 'inline' : 'block', asString(defaultValue));
|
|
563
|
+
}
|
|
564
|
+
if (kind === 'object') {
|
|
565
|
+
const nestedDefaults = field.fields ? createBuilderDefaultsFromFields(field.fields) : {};
|
|
566
|
+
if (!isRecord(defaultValue)) {
|
|
567
|
+
return nestedDefaults;
|
|
568
|
+
}
|
|
569
|
+
const mergedDefaults = { ...nestedDefaults };
|
|
570
|
+
for (const [key, value] of Object.entries(defaultValue)) {
|
|
571
|
+
const nestedField = field.fields?.[key];
|
|
572
|
+
mergedDefaults[key] = nestedField
|
|
573
|
+
? createBuilderFieldDefault(nestedField, value)
|
|
574
|
+
: cloneValue(value);
|
|
575
|
+
}
|
|
576
|
+
return mergedDefaults;
|
|
577
|
+
}
|
|
578
|
+
if (kind === 'array') {
|
|
579
|
+
if (!Array.isArray(defaultValue)) {
|
|
580
|
+
return [];
|
|
581
|
+
}
|
|
582
|
+
return defaultValue.map((entry) => field.item ? createBuilderFieldDefault(field.item, entry) : cloneValue(entry));
|
|
583
|
+
}
|
|
584
|
+
return cloneValue(defaultValue ?? getPrimitiveDefault(kind));
|
|
585
|
+
}
|
|
586
|
+
function findPreviewImageField(field, basePath = '') {
|
|
587
|
+
const kind = inferBuilderFieldKind(field);
|
|
588
|
+
if (kind === 'image') {
|
|
589
|
+
return {
|
|
590
|
+
path: basePath,
|
|
591
|
+
previewSelector: getFieldPreviewSelector(field, basePath),
|
|
592
|
+
hasPreviewBinding: Boolean(field.previewSelector || field.previewInMarkup)
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
if (kind === 'object' && field.fields) {
|
|
596
|
+
for (const [name, nestedField] of Object.entries(field.fields)) {
|
|
597
|
+
const nestedPath = basePath ? `${basePath}.${name}` : name;
|
|
598
|
+
const result = findPreviewImageField(nestedField, nestedPath);
|
|
599
|
+
if (result) {
|
|
600
|
+
return result;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
function isRecord(value) {
|
|
607
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
608
|
+
}
|
|
609
|
+
function getPrimitiveDefault(kind) {
|
|
610
|
+
if (kind === 'boolean') {
|
|
611
|
+
return false;
|
|
612
|
+
}
|
|
613
|
+
if (kind === 'number') {
|
|
614
|
+
return 0;
|
|
615
|
+
}
|
|
616
|
+
return '';
|
|
617
|
+
}
|
|
618
|
+
export function getFallbackText(key) {
|
|
619
|
+
const normalized = key.toLowerCase();
|
|
620
|
+
if (normalized.includes('eyebrow'))
|
|
621
|
+
return 'Launch smarter';
|
|
622
|
+
if (normalized.includes('headline') || normalized.includes('title')) {
|
|
623
|
+
return 'Build better pages faster';
|
|
624
|
+
}
|
|
625
|
+
if (normalized.includes('subtitle') ||
|
|
626
|
+
normalized.includes('description') ||
|
|
627
|
+
normalized.includes('text')) {
|
|
628
|
+
return 'A focused preview with realistic content so you can recognize this brik.';
|
|
629
|
+
}
|
|
630
|
+
if (normalized.includes('label'))
|
|
631
|
+
return 'Get started';
|
|
632
|
+
if (normalized.includes('note'))
|
|
633
|
+
return 'No setup required.';
|
|
634
|
+
if (normalized.includes('author') || normalized.includes('name'))
|
|
635
|
+
return 'Alex Morgan';
|
|
636
|
+
if (normalized.includes('role'))
|
|
637
|
+
return 'Product lead';
|
|
638
|
+
if (normalized.includes('quote'))
|
|
639
|
+
return 'This section gives the page structure immediately.';
|
|
640
|
+
if (normalized.includes('claim'))
|
|
641
|
+
return 'Simple pages, edited visually.';
|
|
642
|
+
if (normalized.includes('brand'))
|
|
643
|
+
return 'Brixter';
|
|
644
|
+
return humanizeKey(key);
|
|
645
|
+
}
|
|
646
|
+
function isHrefKey(key) {
|
|
647
|
+
return key.toLowerCase() === 'href' || key.toLowerCase().endsWith('url');
|
|
648
|
+
}
|
|
649
|
+
function createImageFallback() {
|
|
650
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 240" role="img" aria-hidden="true">
|
|
651
|
+
<g transform="translate(160 120)" stroke="#a3a3a3" fill="none" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
652
|
+
<g transform="translate(-12 -12)">
|
|
653
|
+
<rect width="18" height="18" x="3" y="3" rx="2" ry="2"/>
|
|
654
|
+
<circle cx="9" cy="9" r="2"/>
|
|
655
|
+
<path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/>
|
|
656
|
+
</g>
|
|
657
|
+
</g>
|
|
658
|
+
</svg>`;
|
|
659
|
+
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
|
|
660
|
+
}
|
|
661
|
+
function parsePath(path) {
|
|
662
|
+
const segments = [];
|
|
663
|
+
for (const part of path.split('.')) {
|
|
664
|
+
const matches = part.match(/([^\[\]]+)|\[(\d+)\]/g) ?? [];
|
|
665
|
+
for (const match of matches) {
|
|
666
|
+
if (match.startsWith('[') && match.endsWith(']')) {
|
|
667
|
+
segments.push(Number(match.slice(1, -1)));
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
segments.push(match);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return segments;
|
|
674
|
+
}
|
|
675
|
+
function asString(value) {
|
|
676
|
+
return typeof value === 'string' ? value : '';
|
|
677
|
+
}
|
|
678
|
+
function getFieldPreviewSelector(field, path) {
|
|
679
|
+
return (field.previewSelector ??
|
|
680
|
+
(field.previewInMarkup ? inferFieldPreviewSelector(field, path) : undefined));
|
|
681
|
+
}
|
|
682
|
+
function inferFieldPreviewSelector(field, path) {
|
|
683
|
+
const kind = inferBuilderFieldKind(field);
|
|
684
|
+
if (kind === 'text' ||
|
|
685
|
+
kind === 'image' ||
|
|
686
|
+
kind === 'icon' ||
|
|
687
|
+
kind === 'richtext-inline' ||
|
|
688
|
+
kind === 'richtext-block') {
|
|
689
|
+
return `[data-builder-field="${path}"]`;
|
|
690
|
+
}
|
|
691
|
+
return undefined;
|
|
692
|
+
}
|
|
693
|
+
function getCollectionPreviewSelector(path) {
|
|
694
|
+
return `[data-builder-collection-item="${path}"]`;
|
|
695
|
+
}
|
|
696
|
+
function inferSummaryField(field) {
|
|
697
|
+
if (!field?.fields) {
|
|
698
|
+
return undefined;
|
|
699
|
+
}
|
|
700
|
+
for (const candidate of ['title', 'name', 'label', 'alt']) {
|
|
701
|
+
if (field.fields[candidate]) {
|
|
702
|
+
return candidate;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return Object.entries(field.fields).find(([, nestedField]) => {
|
|
706
|
+
const kind = inferBuilderFieldKind(nestedField);
|
|
707
|
+
return kind === 'text' || kind === 'richtext-inline' || kind === 'richtext-block';
|
|
708
|
+
})?.[0];
|
|
709
|
+
}
|
|
710
|
+
function humanizeKey(value) {
|
|
711
|
+
return value
|
|
712
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
713
|
+
.replace(/[-_]/g, ' ')
|
|
714
|
+
.replace(/\s+/g, ' ')
|
|
715
|
+
.trim()
|
|
716
|
+
.replace(/^\w/, (match) => match.toUpperCase());
|
|
717
|
+
}
|
|
718
|
+
function createId() {
|
|
719
|
+
if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) {
|
|
720
|
+
return crypto.randomUUID();
|
|
721
|
+
}
|
|
722
|
+
return `block-${Math.random().toString(36).slice(2, 10)}`;
|
|
723
|
+
}
|
|
724
|
+
function createIconFallback() {
|
|
725
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-help-circle"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" x2="12.01" y1="17" y2="17"/></svg>`;
|
|
726
|
+
}
|
|
727
|
+
export function getFieldByRawPath(fields, rawPath) {
|
|
728
|
+
const segments = rawPath.split('.');
|
|
729
|
+
let currentFields = fields;
|
|
730
|
+
let currentField = null;
|
|
731
|
+
for (const segment of segments) {
|
|
732
|
+
if (!currentFields) {
|
|
733
|
+
return null;
|
|
734
|
+
}
|
|
735
|
+
const isArray = segment.endsWith('[]');
|
|
736
|
+
const name = isArray ? segment.slice(0, -2) : segment;
|
|
737
|
+
const field = currentFields[name];
|
|
738
|
+
if (!field) {
|
|
739
|
+
return null;
|
|
740
|
+
}
|
|
741
|
+
currentField = field;
|
|
742
|
+
if (isArray && field.item?.fields) {
|
|
743
|
+
currentFields = field.item.fields;
|
|
744
|
+
}
|
|
745
|
+
else if (field.fields) {
|
|
746
|
+
currentFields = field.fields;
|
|
747
|
+
}
|
|
748
|
+
else {
|
|
749
|
+
currentFields = undefined;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return currentField;
|
|
753
|
+
}
|
|
754
|
+
export function getFieldByPath(fields, path) {
|
|
755
|
+
const rawPath = path.replace(/\[\d+\]/g, '[]');
|
|
756
|
+
return getFieldByRawPath(fields, rawPath);
|
|
757
|
+
}
|
|
758
|
+
//# sourceMappingURL=core.js.map
|