@zeropress/build-core 0.1.14 → 0.1.16
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 +6 -4
- package/package.json +3 -3
- package/src/api/build-site.js +360 -338
- package/src/index.d.ts +2 -18
- package/src/render/control-flow-renderer.js +221 -0
- package/src/render/partial-resolver.js +23 -0
- package/src/render/variable-resolver.js +47 -4
- package/src/render/zeropress-engine.js +30 -2
- package/src/theme/load-theme-dir.js +5 -10
package/src/index.d.ts
CHANGED
|
@@ -1,25 +1,10 @@
|
|
|
1
1
|
import type { PreviewDataV05 } from '@zeropress/preview-data-validator';
|
|
2
|
+
import type { ThemeManifest } from '@zeropress/theme-validator';
|
|
2
3
|
|
|
3
4
|
export interface ThemePackage {
|
|
4
|
-
metadata: {
|
|
5
|
-
name: string;
|
|
6
|
-
version: string;
|
|
7
|
-
author?: string | null;
|
|
8
|
-
description?: string;
|
|
5
|
+
metadata: ThemeManifest & {
|
|
9
6
|
thumbnail?: string;
|
|
10
7
|
settings?: Record<string, unknown>;
|
|
11
|
-
menuSlots?: Record<string, {
|
|
12
|
-
title: string;
|
|
13
|
-
description?: string;
|
|
14
|
-
}>;
|
|
15
|
-
widgetAreas?: Record<string, {
|
|
16
|
-
title: string;
|
|
17
|
-
description?: string;
|
|
18
|
-
}>;
|
|
19
|
-
namespace?: string;
|
|
20
|
-
slug?: string;
|
|
21
|
-
license?: 'MIT' | 'Apache-2.0' | 'BSD-3-Clause' | 'GPL-3.0-only' | 'GPL-3.0-or-later';
|
|
22
|
-
runtime?: '0.3';
|
|
23
8
|
};
|
|
24
9
|
templates: Map<string, string>;
|
|
25
10
|
partials: Map<string, string>;
|
|
@@ -39,7 +24,6 @@ export interface BuildWriter {
|
|
|
39
24
|
export interface BuildOptions {
|
|
40
25
|
assetHashing?: boolean;
|
|
41
26
|
generateSpecialFiles?: boolean;
|
|
42
|
-
injectHtmx?: boolean;
|
|
43
27
|
writeManifest?: boolean;
|
|
44
28
|
}
|
|
45
29
|
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
function isWhitespace(value) {
|
|
2
|
+
return /^[\s\r\n\t]*$/.test(value);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export class ControlFlowRenderer {
|
|
6
|
+
constructor(options = {}) {
|
|
7
|
+
this.resolvePath = typeof options.resolvePath === 'function'
|
|
8
|
+
? options.resolvePath
|
|
9
|
+
: ((data, path) => path.split('.').reduce((current, segment) => {
|
|
10
|
+
if (current == null || typeof current !== 'object') {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
return current[segment];
|
|
14
|
+
}, data));
|
|
15
|
+
this.renderText = typeof options.renderText === 'function'
|
|
16
|
+
? options.renderText
|
|
17
|
+
: ((value) => value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
render(template, data, renderOptions = {}) {
|
|
21
|
+
const nodes = this.parse(template);
|
|
22
|
+
return this.renderNodes(nodes, data, renderOptions);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
parse(template) {
|
|
26
|
+
const source = String(template || '');
|
|
27
|
+
const { nodes, nextIndex, stopTag } = this.parseNodes(source, 0, new Set());
|
|
28
|
+
if (stopTag) {
|
|
29
|
+
throw new Error(`Unexpected closing tag ${stopTag}`);
|
|
30
|
+
}
|
|
31
|
+
if (nextIndex !== source.length) {
|
|
32
|
+
throw new Error('Unexpected parser termination');
|
|
33
|
+
}
|
|
34
|
+
return nodes;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
parseNodes(source, startIndex, stopTags) {
|
|
38
|
+
const nodes = [];
|
|
39
|
+
let index = startIndex;
|
|
40
|
+
|
|
41
|
+
while (index < source.length) {
|
|
42
|
+
const nextDelimiter = source.indexOf('{{', index);
|
|
43
|
+
if (nextDelimiter === -1) {
|
|
44
|
+
nodes.push({ type: 'text', value: source.slice(index) });
|
|
45
|
+
return { nodes, nextIndex: source.length, stopTag: null };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (nextDelimiter > index) {
|
|
49
|
+
nodes.push({ type: 'text', value: source.slice(index, nextDelimiter) });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (source.startsWith('{{!--', nextDelimiter)) {
|
|
53
|
+
const end = source.indexOf('--}}', nextDelimiter + 5);
|
|
54
|
+
if (end === -1) {
|
|
55
|
+
throw new Error('Unclosed block comment');
|
|
56
|
+
}
|
|
57
|
+
const commentBody = source.slice(nextDelimiter + 5, end);
|
|
58
|
+
if (commentBody.includes('{{!--')) {
|
|
59
|
+
throw new Error('Nested block comments are not allowed');
|
|
60
|
+
}
|
|
61
|
+
index = end + 4;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (source.startsWith('{{!', nextDelimiter)) {
|
|
66
|
+
const end = source.indexOf('}}', nextDelimiter + 3);
|
|
67
|
+
if (end === -1) {
|
|
68
|
+
throw new Error('Unclosed inline comment');
|
|
69
|
+
}
|
|
70
|
+
index = end + 2;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const tokenEnd = source.indexOf('}}', nextDelimiter + 2);
|
|
75
|
+
if (tokenEnd === -1) {
|
|
76
|
+
throw new Error('Unclosed template tag');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const rawToken = source.slice(nextDelimiter, tokenEnd + 2);
|
|
80
|
+
const token = source.slice(nextDelimiter + 2, tokenEnd).trim();
|
|
81
|
+
|
|
82
|
+
if (token.startsWith('/')) {
|
|
83
|
+
const tagName = token.slice(1).trim();
|
|
84
|
+
if (!stopTags.has(tagName)) {
|
|
85
|
+
throw new Error(`Unexpected closing tag /${tagName}`);
|
|
86
|
+
}
|
|
87
|
+
return { nodes, nextIndex: tokenEnd + 2, stopTag: tagName };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (token === '#else') {
|
|
91
|
+
if (!stopTags.has('else')) {
|
|
92
|
+
throw new Error('Unexpected else tag');
|
|
93
|
+
}
|
|
94
|
+
return { nodes, nextIndex: tokenEnd + 2, stopTag: 'else' };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (token.startsWith('#if_eq ')) {
|
|
98
|
+
const expression = token.slice('#if_eq '.length).trim();
|
|
99
|
+
const match = /^([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)\s+("(?:[^"\\]|\\.)*")$/.exec(expression);
|
|
100
|
+
if (!match) {
|
|
101
|
+
throw new Error(`Invalid if_eq expression: ${expression}`);
|
|
102
|
+
}
|
|
103
|
+
const [, path, literalSource] = match;
|
|
104
|
+
const literal = JSON.parse(literalSource);
|
|
105
|
+
const ifResult = this.parseNodes(source, tokenEnd + 2, new Set(['else', 'if_eq']));
|
|
106
|
+
let elseNodes = [];
|
|
107
|
+
let nextIndex = ifResult.nextIndex;
|
|
108
|
+
if (ifResult.stopTag === 'else') {
|
|
109
|
+
const elseResult = this.parseNodes(source, ifResult.nextIndex, new Set(['if_eq']));
|
|
110
|
+
if (elseResult.stopTag !== 'if_eq') {
|
|
111
|
+
throw new Error('Unclosed if_eq block after else');
|
|
112
|
+
}
|
|
113
|
+
elseNodes = elseResult.nodes;
|
|
114
|
+
nextIndex = elseResult.nextIndex;
|
|
115
|
+
} else if (ifResult.stopTag !== 'if_eq') {
|
|
116
|
+
throw new Error('Unclosed if_eq block');
|
|
117
|
+
}
|
|
118
|
+
nodes.push({ type: 'if_eq', path, literal, consequent: ifResult.nodes, alternate: elseNodes });
|
|
119
|
+
index = nextIndex;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (token.startsWith('#if ')) {
|
|
124
|
+
const path = token.slice('#if '.length).trim();
|
|
125
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*$/.test(path)) {
|
|
126
|
+
throw new Error(`Invalid if expression: ${path}`);
|
|
127
|
+
}
|
|
128
|
+
const ifResult = this.parseNodes(source, tokenEnd + 2, new Set(['else', 'if']));
|
|
129
|
+
let elseNodes = [];
|
|
130
|
+
let nextIndex = ifResult.nextIndex;
|
|
131
|
+
if (ifResult.stopTag === 'else') {
|
|
132
|
+
const elseResult = this.parseNodes(source, ifResult.nextIndex, new Set(['if']));
|
|
133
|
+
if (elseResult.stopTag !== 'if') {
|
|
134
|
+
throw new Error('Unclosed if block after else');
|
|
135
|
+
}
|
|
136
|
+
elseNodes = elseResult.nodes;
|
|
137
|
+
nextIndex = elseResult.nextIndex;
|
|
138
|
+
} else if (ifResult.stopTag !== 'if') {
|
|
139
|
+
throw new Error('Unclosed if block');
|
|
140
|
+
}
|
|
141
|
+
nodes.push({ type: 'if', path, consequent: ifResult.nodes, alternate: elseNodes });
|
|
142
|
+
index = nextIndex;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (token.startsWith('#for ')) {
|
|
147
|
+
const expression = token.slice('#for '.length).trim();
|
|
148
|
+
const match = /^([a-zA-Z_][a-zA-Z0-9_]*)\s+in\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)$/.exec(expression);
|
|
149
|
+
if (!match) {
|
|
150
|
+
throw new Error(`Invalid for expression: ${expression}`);
|
|
151
|
+
}
|
|
152
|
+
const [, itemName, path] = match;
|
|
153
|
+
const forResult = this.parseNodes(source, tokenEnd + 2, new Set(['for']));
|
|
154
|
+
if (forResult.stopTag !== 'for') {
|
|
155
|
+
throw new Error('Unclosed for block');
|
|
156
|
+
}
|
|
157
|
+
nodes.push({ type: 'for', itemName, path, body: forResult.nodes });
|
|
158
|
+
index = forResult.nextIndex;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
nodes.push({ type: 'text', value: rawToken });
|
|
163
|
+
index = tokenEnd + 2;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (stopTags.size > 0) {
|
|
167
|
+
const expected = Array.from(stopTags).filter((entry) => entry !== 'else').join(', ');
|
|
168
|
+
throw new Error(`Missing closing tag for ${expected}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return { nodes, nextIndex: source.length, stopTag: null };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
renderNodes(nodes, data, renderOptions) {
|
|
175
|
+
return nodes.map((node) => this.renderNode(node, data, renderOptions)).join('');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
renderNode(node, data, renderOptions) {
|
|
179
|
+
switch (node.type) {
|
|
180
|
+
case 'text':
|
|
181
|
+
return this.renderText(node.value, data, renderOptions);
|
|
182
|
+
case 'if': {
|
|
183
|
+
const value = this.resolvePath(data, node.path);
|
|
184
|
+
return this.isTruthy(value)
|
|
185
|
+
? this.renderNodes(node.consequent, data, renderOptions)
|
|
186
|
+
: this.renderNodes(node.alternate, data, renderOptions);
|
|
187
|
+
}
|
|
188
|
+
case 'if_eq': {
|
|
189
|
+
const value = this.resolvePath(data, node.path);
|
|
190
|
+
return value === node.literal
|
|
191
|
+
? this.renderNodes(node.consequent, data, renderOptions)
|
|
192
|
+
: this.renderNodes(node.alternate, data, renderOptions);
|
|
193
|
+
}
|
|
194
|
+
case 'for': {
|
|
195
|
+
const items = this.resolvePath(data, node.path);
|
|
196
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
197
|
+
return '';
|
|
198
|
+
}
|
|
199
|
+
return items
|
|
200
|
+
.map((item, index) => this.renderNodes(node.body, {
|
|
201
|
+
...data,
|
|
202
|
+
[node.itemName]: item,
|
|
203
|
+
$index: index,
|
|
204
|
+
}, renderOptions))
|
|
205
|
+
.join('');
|
|
206
|
+
}
|
|
207
|
+
default:
|
|
208
|
+
return '';
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
isTruthy(value) {
|
|
213
|
+
if (Array.isArray(value)) {
|
|
214
|
+
return value.length > 0;
|
|
215
|
+
}
|
|
216
|
+
if (typeof value === 'string') {
|
|
217
|
+
return value.length > 0;
|
|
218
|
+
}
|
|
219
|
+
return Boolean(value);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export class PartialResolver {
|
|
2
|
+
static PARTIAL_PATTERN = /\{\{partial:([a-zA-Z_][a-zA-Z0-9_-]*(?:\/[a-zA-Z_][a-zA-Z0-9_-]*)*)\}\}/g;
|
|
3
|
+
|
|
4
|
+
resolve(template, partials) {
|
|
5
|
+
return this.resolveTemplate(String(template || ''), partials, []);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
resolveTemplate(template, partials, activeStack) {
|
|
9
|
+
return template.replace(PartialResolver.PARTIAL_PATTERN, (_, partialName) => {
|
|
10
|
+
if (!partials.has(partialName)) {
|
|
11
|
+
throw new Error(`Partial "${partialName}" not found`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (activeStack.includes(partialName)) {
|
|
15
|
+
const cycle = [...activeStack, partialName].join(' -> ');
|
|
16
|
+
throw new Error(`Circular partial reference detected: ${cycle}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const partialContent = partials.get(partialName);
|
|
20
|
+
return this.resolveTemplate(String(partialContent || ''), partials, [...activeStack, partialName]);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -3,14 +3,14 @@ export class VariableResolver {
|
|
|
3
3
|
static VARIABLE_PATTERN = /\{\{([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)+)\}\}/g;
|
|
4
4
|
static STANDALONE_PATTERN = /\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g;
|
|
5
5
|
|
|
6
|
-
resolve(template, data) {
|
|
6
|
+
resolve(template, data, options = {}) {
|
|
7
7
|
let result = template.replace(VariableResolver.MENU_PATTERN, (_, menuId) => {
|
|
8
8
|
return this.renderMenu(data?.menus?.[menuId]);
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
result = result.replace(VariableResolver.VARIABLE_PATTERN, (_, variablePath) => {
|
|
12
12
|
const value = this.resolvePath(data, variablePath);
|
|
13
|
-
return value
|
|
13
|
+
return this.renderValue(value, variablePath, options);
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
result = result.replace(VariableResolver.STANDALONE_PATTERN, (match, variableName) => {
|
|
@@ -19,7 +19,7 @@ export class VariableResolver {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
const value = data[variableName];
|
|
22
|
-
return value
|
|
22
|
+
return this.renderValue(value, variableName, options);
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
return result;
|
|
@@ -64,11 +64,54 @@ export class VariableResolver {
|
|
|
64
64
|
return `<li><a href="${url}" target="${target}"${rel}>${title}</a>${children}</li>`;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
renderValue(value, variablePath, options = {}) {
|
|
68
|
+
if (value == null) {
|
|
69
|
+
return '';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const stringValue = String(value);
|
|
73
|
+
if (options.escapeValues !== true) {
|
|
74
|
+
return stringValue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (this.shouldRenderRaw(variablePath, options)) {
|
|
78
|
+
return stringValue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return this.escapeHtml(stringValue);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
shouldRenderRaw(variablePath, options = {}) {
|
|
85
|
+
if (options.rawPaths instanceof Set && options.rawPaths.has(variablePath)) {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (options.rawPathPrefixes instanceof Set) {
|
|
90
|
+
for (const prefix of options.rawPathPrefixes) {
|
|
91
|
+
if (variablePath === prefix || variablePath.startsWith(`${prefix}.`)) {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const lastSegment = variablePath.split('.').pop();
|
|
98
|
+
if (lastSegment === 'html' || lastSegment?.endsWith('_html')) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (lastSegment?.endsWith('_url')) {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
67
109
|
escapeHtml(value) {
|
|
68
110
|
return value
|
|
69
111
|
.replaceAll('&', '&')
|
|
70
112
|
.replaceAll('<', '<')
|
|
71
113
|
.replaceAll('>', '>')
|
|
72
|
-
.replaceAll('"', '"')
|
|
114
|
+
.replaceAll('"', '"')
|
|
115
|
+
.replaceAll("'", ''');
|
|
73
116
|
}
|
|
74
117
|
}
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { SlotResolver } from './slot-resolver.js';
|
|
2
2
|
import { VariableResolver } from './variable-resolver.js';
|
|
3
|
+
import { ControlFlowRenderer } from './control-flow-renderer.js';
|
|
4
|
+
import { PartialResolver } from './partial-resolver.js';
|
|
3
5
|
|
|
4
6
|
export class ZeroPressEngine {
|
|
5
7
|
constructor() {
|
|
6
8
|
this.slotResolver = new SlotResolver();
|
|
9
|
+
this.partialResolver = new PartialResolver();
|
|
7
10
|
this.variableResolver = new VariableResolver();
|
|
11
|
+
this.controlFlowRenderer = new ControlFlowRenderer({
|
|
12
|
+
resolvePath: (data, path) => this.variableResolver.resolvePath(data, path),
|
|
13
|
+
renderText: (value, data, renderOptions) => this.variableResolver.resolve(value, data, renderOptions),
|
|
14
|
+
});
|
|
8
15
|
this.themePackage = null;
|
|
9
16
|
}
|
|
10
17
|
|
|
@@ -27,9 +34,10 @@ export class ZeroPressEngine {
|
|
|
27
34
|
throw new Error('Template "layout" not found');
|
|
28
35
|
}
|
|
29
36
|
|
|
30
|
-
const
|
|
37
|
+
const renderData = this.combineRenderData(data, context);
|
|
38
|
+
const renderedContent = this.renderTemplate(this.expandPartials(template), renderData);
|
|
31
39
|
const layoutWithSlots = this.slotResolver.resolve(layout, this.themePackage.partials, renderedContent);
|
|
32
|
-
return this.
|
|
40
|
+
return this.renderTemplate(this.expandPartials(layoutWithSlots), renderData);
|
|
33
41
|
}
|
|
34
42
|
|
|
35
43
|
combineRenderData(data, context) {
|
|
@@ -40,4 +48,24 @@ export class ZeroPressEngine {
|
|
|
40
48
|
language: context.language,
|
|
41
49
|
};
|
|
42
50
|
}
|
|
51
|
+
|
|
52
|
+
renderTemplate(template, data) {
|
|
53
|
+
if (this.themePackage?.metadata?.runtime === '0.4') {
|
|
54
|
+
return this.controlFlowRenderer.render(template, data, {
|
|
55
|
+
escapeValues: true,
|
|
56
|
+
rawPaths: new Set(['meta.head_tags']),
|
|
57
|
+
rawPathPrefixes: new Set(['meta']),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return this.variableResolver.resolve(template, data);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
expandPartials(template) {
|
|
65
|
+
if (this.themePackage?.metadata?.runtime !== '0.4') {
|
|
66
|
+
return template;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return this.partialResolver.resolve(template, this.themePackage.partials);
|
|
70
|
+
}
|
|
43
71
|
}
|
|
@@ -16,6 +16,10 @@ export async function loadThemePackageFromDir(themeDir) {
|
|
|
16
16
|
|
|
17
17
|
const rawThemeJson = String(fileMap.get('theme.json'));
|
|
18
18
|
const themeJson = JSON.parse(rawThemeJson);
|
|
19
|
+
const manifest = validation.manifest;
|
|
20
|
+
if (!manifest) {
|
|
21
|
+
throw new Error('Theme validation failed: normalized manifest not available');
|
|
22
|
+
}
|
|
19
23
|
const templates = new Map();
|
|
20
24
|
const partials = new Map();
|
|
21
25
|
const assets = new Map();
|
|
@@ -45,18 +49,9 @@ export async function loadThemePackageFromDir(themeDir) {
|
|
|
45
49
|
|
|
46
50
|
return {
|
|
47
51
|
metadata: {
|
|
48
|
-
|
|
49
|
-
version: themeJson.version,
|
|
50
|
-
author: themeJson.author,
|
|
51
|
-
description: themeJson.description,
|
|
52
|
+
...manifest,
|
|
52
53
|
thumbnail: themeJson.thumbnail,
|
|
53
54
|
settings: themeJson.settings || {},
|
|
54
|
-
menuSlots: themeJson.menuSlots,
|
|
55
|
-
widgetAreas: themeJson.widgetAreas,
|
|
56
|
-
namespace: themeJson.namespace,
|
|
57
|
-
slug: themeJson.slug,
|
|
58
|
-
license: themeJson.license,
|
|
59
|
-
runtime: themeJson.runtime,
|
|
60
55
|
},
|
|
61
56
|
templates,
|
|
62
57
|
partials,
|