@o2vend/theme-cli 1.0.32
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 +425 -0
- package/assets/Logo_o2vend.png +0 -0
- package/assets/favicon.png +0 -0
- package/assets/logo-white.png +0 -0
- package/bin/o2vend +42 -0
- package/config/widget-map.json +50 -0
- package/lib/commands/check.js +201 -0
- package/lib/commands/generate.js +33 -0
- package/lib/commands/init.js +214 -0
- package/lib/commands/optimize.js +216 -0
- package/lib/commands/package.js +208 -0
- package/lib/commands/serve.js +105 -0
- package/lib/commands/validate.js +191 -0
- package/lib/lib/api-client.js +357 -0
- package/lib/lib/dev-server.js +2618 -0
- package/lib/lib/file-watcher.js +80 -0
- package/lib/lib/hot-reload.js +106 -0
- package/lib/lib/liquid-engine.js +822 -0
- package/lib/lib/liquid-filters.js +671 -0
- package/lib/lib/mock-api-server.js +989 -0
- package/lib/lib/mock-data.js +1468 -0
- package/lib/lib/widget-service.js +321 -0
- package/package.json +70 -0
- package/test-theme/README.md +27 -0
- package/test-theme/assets/async-sections.js +446 -0
- package/test-theme/assets/cart-drawer.js +463 -0
- package/test-theme/assets/cart-manager.js +223 -0
- package/test-theme/assets/checkout-price-handler.js +368 -0
- package/test-theme/assets/components.css +4629 -0
- package/test-theme/assets/delivery-zone.css +299 -0
- package/test-theme/assets/delivery-zone.js +396 -0
- package/test-theme/assets/logo.png +0 -0
- package/test-theme/assets/sections.css +48 -0
- package/test-theme/assets/theme.css +3500 -0
- package/test-theme/assets/theme.js +3745 -0
- package/test-theme/config/settings_data.json +292 -0
- package/test-theme/config/settings_schema.json +1050 -0
- package/test-theme/layout/theme.liquid +195 -0
- package/test-theme/locales/en.default.json +260 -0
- package/test-theme/sections/content-fallback.liquid +53 -0
- package/test-theme/sections/content.liquid +57 -0
- package/test-theme/sections/footer-fallback.liquid +328 -0
- package/test-theme/sections/footer.liquid +278 -0
- package/test-theme/sections/header-fallback.liquid +1805 -0
- package/test-theme/sections/header.liquid +1145 -0
- package/test-theme/sections/hero-fallback.liquid +212 -0
- package/test-theme/sections/hero.liquid +136 -0
- package/test-theme/snippets/account-sidebar.liquid +200 -0
- package/test-theme/snippets/add-to-cart-modal.liquid +484 -0
- package/test-theme/snippets/breadcrumbs.liquid +134 -0
- package/test-theme/snippets/cart-drawer.liquid +467 -0
- package/test-theme/snippets/delivery-zone-city-selector.liquid +79 -0
- package/test-theme/snippets/delivery-zone-modal.liquid +337 -0
- package/test-theme/snippets/delivery-zone-search.liquid +78 -0
- package/test-theme/snippets/icon.liquid +105 -0
- package/test-theme/snippets/login-modal.liquid +346 -0
- package/test-theme/snippets/mega-menu.liquid +812 -0
- package/test-theme/snippets/news-thumbnail.liquid +187 -0
- package/test-theme/snippets/pagination.liquid +120 -0
- package/test-theme/snippets/price.liquid +92 -0
- package/test-theme/snippets/product-card-related.liquid +78 -0
- package/test-theme/snippets/product-card-simple.liquid +41 -0
- package/test-theme/snippets/product-card.liquid +697 -0
- package/test-theme/snippets/rating.liquid +85 -0
- package/test-theme/snippets/skeleton-collection-grid.liquid +114 -0
- package/test-theme/snippets/skeleton-product-card.liquid +124 -0
- package/test-theme/snippets/skeleton-product-grid.liquid +34 -0
- package/test-theme/snippets/social-sharing.liquid +185 -0
- package/test-theme/templates/account/dashboard.liquid +401 -0
- package/test-theme/templates/account/loyalty-redemption.liquid +405 -0
- package/test-theme/templates/account/loyalty.liquid +588 -0
- package/test-theme/templates/account/order-detail.liquid +230 -0
- package/test-theme/templates/account/orders.liquid +349 -0
- package/test-theme/templates/account/profile.liquid +758 -0
- package/test-theme/templates/account/register.liquid +232 -0
- package/test-theme/templates/account/return-orders.liquid +348 -0
- package/test-theme/templates/account/store-credit.liquid +464 -0
- package/test-theme/templates/account/subscriptions.liquid +601 -0
- package/test-theme/templates/account/wishlist.liquid +419 -0
- package/test-theme/templates/address-book.liquid +1092 -0
- package/test-theme/templates/categories.liquid +452 -0
- package/test-theme/templates/checkout.liquid +4511 -0
- package/test-theme/templates/error.liquid +384 -0
- package/test-theme/templates/index.liquid +11 -0
- package/test-theme/templates/login.liquid +185 -0
- package/test-theme/templates/order-confirmation.liquid +720 -0
- package/test-theme/templates/page.liquid +297 -0
- package/test-theme/templates/product-detail.liquid +4363 -0
- package/test-theme/templates/products.liquid +518 -0
- package/test-theme/templates/search.liquid +922 -0
- package/test-theme/theme.json.example +19 -0
- package/test-theme/widgets/brand-carousel.liquid +676 -0
- package/test-theme/widgets/brand.liquid +245 -0
- package/test-theme/widgets/carousel.liquid +843 -0
- package/test-theme/widgets/category-list-carousel.liquid +656 -0
- package/test-theme/widgets/category-list.liquid +340 -0
- package/test-theme/widgets/category.liquid +475 -0
- package/test-theme/widgets/discount-time.liquid +176 -0
- package/test-theme/widgets/footer-menu.liquid +695 -0
- package/test-theme/widgets/footer.liquid +179 -0
- package/test-theme/widgets/gallery.liquid +271 -0
- package/test-theme/widgets/header-menu.liquid +932 -0
- package/test-theme/widgets/header.liquid +159 -0
- package/test-theme/widgets/html.liquid +214 -0
- package/test-theme/widgets/news.liquid +217 -0
- package/test-theme/widgets/product-canvas.liquid +235 -0
- package/test-theme/widgets/product-carousel.liquid +502 -0
- package/test-theme/widgets/product.liquid +45 -0
- package/test-theme/widgets/recently-viewed.liquid +26 -0
- package/test-theme/widgets/shared/product-grid.liquid +339 -0
- package/test-theme/widgets/simple-product.liquid +42 -0
- package/test-theme/widgets/single-product.liquid +610 -0
- package/test-theme/widgets/spacebar-carousel.liquid +663 -0
- package/test-theme/widgets/spacebar.liquid +279 -0
- package/test-theme/widgets/splash.liquid +378 -0
- package/test-theme/widgets/testimonial-carousel.liquid +709 -0
|
@@ -0,0 +1,822 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LiquidJS Engine Setup
|
|
3
|
+
* Configures LiquidJS with all O2VEND filters and custom tags
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { Liquid } = require('liquidjs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const LiquidHelperService = require('./liquid-filters');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const chalk = require('chalk');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create and configure LiquidJS engine
|
|
14
|
+
* @param {string} themePath - Path to theme directory
|
|
15
|
+
* @param {Object} options - Engine options
|
|
16
|
+
* @returns {Liquid} Configured LiquidJS engine
|
|
17
|
+
*/
|
|
18
|
+
// Store the current rendering context globally so sections can access it
|
|
19
|
+
let currentRenderingContext = null;
|
|
20
|
+
|
|
21
|
+
// Track widgets currently being rendered to prevent infinite recursion
|
|
22
|
+
const renderingStack = new Set();
|
|
23
|
+
const MAX_RENDER_DEPTH = 50; // Maximum number of nested renders allowed
|
|
24
|
+
let currentRenderDepth = 0;
|
|
25
|
+
|
|
26
|
+
function createLiquidEngine(themePath, options = {}) {
|
|
27
|
+
const {
|
|
28
|
+
cache = false, // Disable cache in dev mode
|
|
29
|
+
extname = '.liquid'
|
|
30
|
+
} = options;
|
|
31
|
+
|
|
32
|
+
// Create Liquid engine
|
|
33
|
+
// NOTE: Do NOT use outputEscape: 'escape' as it breaks JSON output in script tags
|
|
34
|
+
// Templates should use explicit escaping when needed (e.g., {{ variable | escape }})
|
|
35
|
+
const liquid = new Liquid({
|
|
36
|
+
root: themePath,
|
|
37
|
+
extname: extname,
|
|
38
|
+
cache: cache,
|
|
39
|
+
strictFilters: false,
|
|
40
|
+
strictVariables: false,
|
|
41
|
+
ownPropertyOnly: false,
|
|
42
|
+
// Enable dynamic properties to access array methods
|
|
43
|
+
dynamicPartials: true,
|
|
44
|
+
// Ensure arrays are properly handled
|
|
45
|
+
jsTruthy: true,
|
|
46
|
+
// Enable async filters
|
|
47
|
+
async: true
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Register all filters from LiquidHelperService
|
|
51
|
+
const liquidHelperService = new LiquidHelperService();
|
|
52
|
+
const filters = liquidHelperService.getFilters();
|
|
53
|
+
|
|
54
|
+
Object.keys(filters).forEach(filterName => {
|
|
55
|
+
liquid.registerFilter(filterName, filters[filterName]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Register custom filters for JSON output compatibility
|
|
59
|
+
// These help handle edge cases where templates output JSON directly
|
|
60
|
+
|
|
61
|
+
// json_value: Outputs a JSON-safe value (handles null, undefined, objects)
|
|
62
|
+
liquid.registerFilter('json_value', (value) => {
|
|
63
|
+
if (value === null || value === undefined) {
|
|
64
|
+
return 'null';
|
|
65
|
+
}
|
|
66
|
+
if (typeof value === 'object') {
|
|
67
|
+
return JSON.stringify(value);
|
|
68
|
+
}
|
|
69
|
+
if (typeof value === 'string') {
|
|
70
|
+
return JSON.stringify(value);
|
|
71
|
+
}
|
|
72
|
+
return String(value);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// safe_json: Ensures proper JSON encoding without double-encoding strings
|
|
76
|
+
liquid.registerFilter('safe_json', (value) => {
|
|
77
|
+
if (value === null || value === undefined) {
|
|
78
|
+
return 'null';
|
|
79
|
+
}
|
|
80
|
+
// If it's already a JSON string, don't double-encode
|
|
81
|
+
if (typeof value === 'string') {
|
|
82
|
+
try {
|
|
83
|
+
JSON.parse(value);
|
|
84
|
+
return value; // Already valid JSON
|
|
85
|
+
} catch {
|
|
86
|
+
return JSON.stringify(value); // Encode the string
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return JSON.stringify(value);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Register custom tags
|
|
93
|
+
registerCustomTags(liquid, themePath);
|
|
94
|
+
|
|
95
|
+
return liquid;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Register custom Liquid tags (section, hook, schema)
|
|
100
|
+
* @param {Liquid} liquid - LiquidJS engine instance
|
|
101
|
+
* @param {string} themePath - Theme path
|
|
102
|
+
*/
|
|
103
|
+
function registerCustomTags(liquid, themePath) {
|
|
104
|
+
// Register section tag
|
|
105
|
+
liquid.registerTag('section', {
|
|
106
|
+
parse: function(tagToken, remainTokens) {
|
|
107
|
+
this.sectionPath = tagToken.args.trim().replace(/^['"]|['"]$/g, '');
|
|
108
|
+
},
|
|
109
|
+
render: async function(scope, hash) {
|
|
110
|
+
let sectionPath = this.sectionPath;
|
|
111
|
+
if (!sectionPath) {
|
|
112
|
+
console.error('[SECTION] No section path provided');
|
|
113
|
+
return '';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
// Normalize section path - remove 'sections/' prefix if present
|
|
118
|
+
// Handle both 'hero' and 'sections/hero' formats
|
|
119
|
+
sectionPath = sectionPath.replace(/^sections\//, '').replace(/^sections\\/, '');
|
|
120
|
+
|
|
121
|
+
const sectionFile = path.join(themePath, 'sections', `${sectionPath}.liquid`);
|
|
122
|
+
if (fs.existsSync(sectionFile)) {
|
|
123
|
+
let sectionContent = fs.readFileSync(sectionFile, 'utf8');
|
|
124
|
+
|
|
125
|
+
// Pre-process section to convert render_widget filter to tag
|
|
126
|
+
const originalContent = sectionContent;
|
|
127
|
+
try {
|
|
128
|
+
sectionContent = preprocessTemplate(sectionContent);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error(chalk.red(`[SECTION] ❌ Error preprocessing ${sectionPath}: ${error.message}`));
|
|
131
|
+
sectionContent = originalContent;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const scopeContexts = Array.isArray(scope?.contexts) ? scope.contexts : [];
|
|
135
|
+
const primaryScope = scopeContexts.length > 0
|
|
136
|
+
? scopeContexts[0]
|
|
137
|
+
: (scope?.environments || scope?.context || {});
|
|
138
|
+
|
|
139
|
+
// Merge with current rendering context to ensure all data is available
|
|
140
|
+
const fullContext = currentRenderingContext
|
|
141
|
+
? { ...currentRenderingContext, ...primaryScope }
|
|
142
|
+
: primaryScope;
|
|
143
|
+
|
|
144
|
+
const context = {
|
|
145
|
+
...fullContext,
|
|
146
|
+
section: fullContext.section || {}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// Render section
|
|
150
|
+
try {
|
|
151
|
+
const rendered = await liquid.parseAndRender(sectionContent, context);
|
|
152
|
+
return rendered;
|
|
153
|
+
} catch (renderError) {
|
|
154
|
+
console.error(chalk.red(`[SECTION] ❌ Error rendering ${sectionPath}: ${renderError.message}`));
|
|
155
|
+
throw renderError;
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
console.warn(`[SECTION] Section file not found: ${sectionFile}`);
|
|
159
|
+
return '';
|
|
160
|
+
}
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error(`[SECTION] Error rendering section ${sectionPath}:`, error.message);
|
|
163
|
+
return '';
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Register hook tag (simplified for CLI - plugins not supported)
|
|
169
|
+
liquid.registerTag('hook', {
|
|
170
|
+
parse: function(tagToken, remainTokens) {
|
|
171
|
+
const args = tagToken.args;
|
|
172
|
+
if (typeof args === 'string') {
|
|
173
|
+
this.hookName = args.trim().replace(/^['"]|['"]$/g, '');
|
|
174
|
+
} else {
|
|
175
|
+
this.hookName = String(args || '').trim().replace(/^['"]|['"]$/g, '');
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
render: async function(scope, hash) {
|
|
179
|
+
// In CLI, hooks are not supported (no plugin system)
|
|
180
|
+
// Return empty string
|
|
181
|
+
return '';
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Register schema tag as a block tag
|
|
186
|
+
// Schema blocks contain JSON configuration and should not be rendered
|
|
187
|
+
liquid.registerTag('schema', {
|
|
188
|
+
parse: function(tagToken, remainTokens) {
|
|
189
|
+
// Consume all tokens until endschema
|
|
190
|
+
this.tokens = [];
|
|
191
|
+
let token;
|
|
192
|
+
while ((token = remainTokens.shift())) {
|
|
193
|
+
if (token.name === 'endschema') {
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
this.tokens.push(token);
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
render: async function(scope, hash) {
|
|
200
|
+
// Schema blocks are for configuration only - don't render anything
|
|
201
|
+
return '';
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Register endschema tag (closing tag for schema)
|
|
206
|
+
liquid.registerTag('endschema', {
|
|
207
|
+
parse: function(tagToken, remainTokens) {
|
|
208
|
+
// No-op - just marks the end of schema block
|
|
209
|
+
},
|
|
210
|
+
render: async function(scope, hash) {
|
|
211
|
+
return '';
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Register render_widget tag as an alternative to the filter
|
|
216
|
+
// This allows templates to use: {% render_widget widget %}
|
|
217
|
+
liquid.registerTag('render_widget', {
|
|
218
|
+
parse: function(tagToken, remainTokens) {
|
|
219
|
+
const args = tagToken.args.trim();
|
|
220
|
+
this.widgetVar = args || 'widget';
|
|
221
|
+
},
|
|
222
|
+
render: async function(scope, hash) {
|
|
223
|
+
try {
|
|
224
|
+
// Get widget from scope using scope.get() for proper variable resolution
|
|
225
|
+
let widget = await scope.get(this.widgetVar.split('.'));
|
|
226
|
+
|
|
227
|
+
if (!widget || !widget.type) {
|
|
228
|
+
return '';
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Render widget template
|
|
232
|
+
if (widget.template_path) {
|
|
233
|
+
const widgetPath = path.join(themePath, `${widget.template_path}.liquid`);
|
|
234
|
+
const altPath = path.join(themePath, widget.template_path);
|
|
235
|
+
|
|
236
|
+
const finalPath = fs.existsSync(widgetPath) ? widgetPath :
|
|
237
|
+
(fs.existsSync(altPath) ? altPath : null);
|
|
238
|
+
|
|
239
|
+
if (finalPath) {
|
|
240
|
+
const fullContext = currentRenderingContext
|
|
241
|
+
? { ...currentRenderingContext, widget: widget }
|
|
242
|
+
: { widget: widget };
|
|
243
|
+
const widgetContent = fs.readFileSync(finalPath, 'utf8');
|
|
244
|
+
return await liquid.parseAndRender(widgetContent, fullContext);
|
|
245
|
+
}
|
|
246
|
+
return `<div class="widget-error">Widget template not found: ${widget.template_path}</div>`;
|
|
247
|
+
}
|
|
248
|
+
return `<div class="widget-error">Widget missing template_path</div>`;
|
|
249
|
+
} catch (error) {
|
|
250
|
+
console.error(chalk.red(`[RENDER_WIDGET] Error: ${error.message}`));
|
|
251
|
+
return `<div class="widget-error">Error: ${error.message}</div>`;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Override include/render tags to handle widget paths
|
|
257
|
+
// This allows sections to use {% include 'widgets/banner' %} or {% render 'widgets/banner' %}
|
|
258
|
+
const originalRenderFile = liquid.renderFile.bind(liquid);
|
|
259
|
+
|
|
260
|
+
// Override renderFile to handle widget paths
|
|
261
|
+
liquid.renderFile = async function(file, ctx, opts) {
|
|
262
|
+
// If the file path starts with 'widgets/', resolve it correctly
|
|
263
|
+
if (typeof file === 'string' && file.startsWith('widgets/')) {
|
|
264
|
+
const widgetName = file.replace(/^widgets\//, '').replace(/\.liquid$/, '');
|
|
265
|
+
const widgetPath = path.join(themePath, 'widgets', `${widgetName}.liquid`);
|
|
266
|
+
if (fs.existsSync(widgetPath)) {
|
|
267
|
+
return originalRenderFile(widgetPath, ctx, opts);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// Also handle snippets
|
|
271
|
+
if (typeof file === 'string' && file.startsWith('snippets/')) {
|
|
272
|
+
const snippetName = file.replace(/^snippets\//, '').replace(/\.liquid$/, '');
|
|
273
|
+
const snippetPath = path.join(themePath, 'snippets', `${snippetName}.liquid`);
|
|
274
|
+
if (fs.existsSync(snippetPath)) {
|
|
275
|
+
return originalRenderFile(snippetPath, ctx, opts);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// Default behavior
|
|
279
|
+
return originalRenderFile(file, ctx, opts);
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
// Override the include tag to handle widget and snippet paths
|
|
283
|
+
liquid.registerTag('include', {
|
|
284
|
+
parse: function(tagToken, remainTokens) {
|
|
285
|
+
// Parse arguments - handle both 'snippets/pagination' and 'snippets/pagination', pagination formats
|
|
286
|
+
const args = tagToken.args.trim();
|
|
287
|
+
// Extract file path (first argument before comma if present)
|
|
288
|
+
const fileMatch = args.match(/^['"]([^'"]+)['"]/);
|
|
289
|
+
this.file = fileMatch ? fileMatch[1] : args.split(',')[0].trim().replace(/^['"]|['"]$/g, '');
|
|
290
|
+
},
|
|
291
|
+
render: async function(scope, hash) {
|
|
292
|
+
let filePath = this.file;
|
|
293
|
+
if (!filePath) {
|
|
294
|
+
console.warn('[INCLUDE] No file path provided');
|
|
295
|
+
return '';
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Clean up file path
|
|
299
|
+
filePath = filePath.trim().replace(/^['"]|['"]$/g, '');
|
|
300
|
+
|
|
301
|
+
// Get full context for snippets/widgets
|
|
302
|
+
const scopeContexts = Array.isArray(scope?.contexts) ? scope.contexts : [];
|
|
303
|
+
const primaryScope = scopeContexts.length > 0
|
|
304
|
+
? scopeContexts[0]
|
|
305
|
+
: (scope?.environments || scope?.context || {});
|
|
306
|
+
|
|
307
|
+
// Merge with current rendering context to ensure all data is available
|
|
308
|
+
const fullContext = currentRenderingContext
|
|
309
|
+
? { ...currentRenderingContext, ...primaryScope }
|
|
310
|
+
: primaryScope;
|
|
311
|
+
|
|
312
|
+
// Handle widget paths
|
|
313
|
+
if (filePath.startsWith('widgets/')) {
|
|
314
|
+
const widgetName = filePath.replace(/^widgets\//, '').replace(/\.liquid$/, '');
|
|
315
|
+
const widgetPath = path.join(themePath, 'widgets', `${widgetName}.liquid`);
|
|
316
|
+
if (fs.existsSync(widgetPath)) {
|
|
317
|
+
return await liquid.parseAndRender(fs.readFileSync(widgetPath, 'utf8'), fullContext);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Handle snippet paths
|
|
322
|
+
if (filePath.startsWith('snippets/')) {
|
|
323
|
+
const snippetName = filePath.replace(/^snippets\//, '').replace(/\.liquid$/, '');
|
|
324
|
+
const snippetPath = path.join(themePath, 'snippets', `${snippetName}.liquid`);
|
|
325
|
+
if (fs.existsSync(snippetPath)) {
|
|
326
|
+
return await liquid.parseAndRender(fs.readFileSync(snippetPath, 'utf8'), fullContext);
|
|
327
|
+
} else {
|
|
328
|
+
console.warn(`[INCLUDE] Snippet not found: ${snippetPath}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Try to resolve file in snippets directory (default location)
|
|
333
|
+
const snippetPath = path.join(themePath, 'snippets', `${filePath}.liquid`);
|
|
334
|
+
if (fs.existsSync(snippetPath)) {
|
|
335
|
+
return await liquid.parseAndRender(fs.readFileSync(snippetPath, 'utf8'), fullContext);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Try direct path
|
|
339
|
+
const resolvedPath = path.join(themePath, filePath);
|
|
340
|
+
if (fs.existsSync(resolvedPath)) {
|
|
341
|
+
return await liquid.parseAndRender(fs.readFileSync(resolvedPath, 'utf8'), fullContext);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Try with .liquid extension
|
|
345
|
+
const resolvedPathWithExt = `${resolvedPath}.liquid`;
|
|
346
|
+
if (fs.existsSync(resolvedPathWithExt)) {
|
|
347
|
+
return await liquid.parseAndRender(fs.readFileSync(resolvedPathWithExt, 'utf8'), fullContext);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
console.warn(`[INCLUDE] File not found: ${filePath} (tried: ${snippetPath}, ${resolvedPath}, ${resolvedPathWithExt})`);
|
|
351
|
+
return '';
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// Override the render tag (similar to include but with isolated scope)
|
|
356
|
+
liquid.registerTag('render', {
|
|
357
|
+
parse: function(tagToken, remainTokens) {
|
|
358
|
+
// Parse the tag arguments
|
|
359
|
+
// Format: {% render 'path' %} or {% render variable.path, param: value %}
|
|
360
|
+
const args = tagToken.args.trim();
|
|
361
|
+
|
|
362
|
+
// Split by comma to separate file path from hash parameters
|
|
363
|
+
const commaIndex = args.indexOf(',');
|
|
364
|
+
if (commaIndex > 0) {
|
|
365
|
+
this.fileArg = args.substring(0, commaIndex).trim();
|
|
366
|
+
this.hashArgs = args.substring(commaIndex + 1).trim();
|
|
367
|
+
} else {
|
|
368
|
+
this.fileArg = args;
|
|
369
|
+
this.hashArgs = '';
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
render: async function(scope, hash) {
|
|
373
|
+
// RECURSION GUARD: Check render depth to prevent infinite loops
|
|
374
|
+
currentRenderDepth++;
|
|
375
|
+
if (currentRenderDepth > MAX_RENDER_DEPTH) {
|
|
376
|
+
console.error(chalk.red(`[RENDER TAG] ❌ Maximum render depth (${MAX_RENDER_DEPTH}) exceeded - possible infinite loop`));
|
|
377
|
+
console.error(chalk.red(`[RENDER TAG] Current rendering stack: ${Array.from(renderingStack).join(' -> ')}`));
|
|
378
|
+
currentRenderDepth--;
|
|
379
|
+
return `<div class="widget-error">Error: Maximum render depth exceeded</div>`;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// CRITICAL: Use scope.get() to properly resolve variables from the LiquidJS scope chain
|
|
383
|
+
// This correctly handles loop variables like 'widget' in {% for widget in widgets.hero %}
|
|
384
|
+
|
|
385
|
+
// Build context starting with currentRenderingContext
|
|
386
|
+
let fullContext = {};
|
|
387
|
+
if (currentRenderingContext) {
|
|
388
|
+
fullContext = { ...currentRenderingContext };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Resolve file path - could be a string literal or a variable
|
|
392
|
+
let filePath = this.fileArg;
|
|
393
|
+
|
|
394
|
+
// If it's wrapped in quotes, it's a string literal
|
|
395
|
+
if ((filePath.startsWith('"') && filePath.endsWith('"')) ||
|
|
396
|
+
(filePath.startsWith("'") && filePath.endsWith("'"))) {
|
|
397
|
+
filePath = filePath.slice(1, -1);
|
|
398
|
+
} else {
|
|
399
|
+
// It's a variable - use scope.get() to resolve it properly (this handles loop variables!)
|
|
400
|
+
try {
|
|
401
|
+
// For paths like 'widget.template_path', we need to resolve via scope.get()
|
|
402
|
+
const resolvedValue = await scope.get(filePath.split('.'));
|
|
403
|
+
if (resolvedValue && typeof resolvedValue === 'string') {
|
|
404
|
+
filePath = resolvedValue;
|
|
405
|
+
} else {
|
|
406
|
+
currentRenderDepth--;
|
|
407
|
+
return '';
|
|
408
|
+
}
|
|
409
|
+
} catch (error) {
|
|
410
|
+
currentRenderDepth--;
|
|
411
|
+
return '';
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Parse hash parameters (e.g., widget: widget, settings: settings)
|
|
416
|
+
const renderContext = { ...fullContext };
|
|
417
|
+
if (this.hashArgs) {
|
|
418
|
+
// Parse hash args like "widget: widget, settings: settings"
|
|
419
|
+
const hashPairs = this.hashArgs.split(',').map(pair => pair.trim());
|
|
420
|
+
for (const pair of hashPairs) {
|
|
421
|
+
const colonIndex = pair.indexOf(':');
|
|
422
|
+
if (colonIndex > 0) {
|
|
423
|
+
const key = pair.substring(0, colonIndex).trim();
|
|
424
|
+
const valueVar = pair.substring(colonIndex + 1).trim();
|
|
425
|
+
if (key && valueVar) {
|
|
426
|
+
// Use scope.get() to resolve the value variable (handles loop variables correctly!)
|
|
427
|
+
try {
|
|
428
|
+
const value = await scope.get(valueVar.split('.'));
|
|
429
|
+
if (value !== undefined && value !== null) {
|
|
430
|
+
renderContext[key] = value;
|
|
431
|
+
}
|
|
432
|
+
} catch (error) {
|
|
433
|
+
// Silently skip unresolved hash params
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Handle widget paths
|
|
441
|
+
if (filePath.startsWith('widgets/')) {
|
|
442
|
+
const widgetName = filePath.replace(/^widgets\//, '').replace(/\.liquid$/, '');
|
|
443
|
+
const widgetPath = path.join(themePath, 'widgets', `${widgetName}.liquid`);
|
|
444
|
+
|
|
445
|
+
// Create unique render key based on widget ID (if available) or path
|
|
446
|
+
const widgetId = renderContext.widget?.id || widgetName;
|
|
447
|
+
const renderKey = `${widgetName}:${widgetId}`;
|
|
448
|
+
|
|
449
|
+
// Check for recursive rendering of the same widget
|
|
450
|
+
if (renderingStack.has(renderKey)) {
|
|
451
|
+
// Only log in debug mode to reduce noise - the guard is working correctly
|
|
452
|
+
if (process.env.DEBUG) {
|
|
453
|
+
console.warn(chalk.yellow(`[RENDER TAG] Skipping duplicate render of ${renderKey}`));
|
|
454
|
+
}
|
|
455
|
+
currentRenderDepth--;
|
|
456
|
+
return ''; // Skip duplicate render
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Add to rendering stack
|
|
460
|
+
renderingStack.add(renderKey);
|
|
461
|
+
|
|
462
|
+
try {
|
|
463
|
+
if (fs.existsSync(widgetPath)) {
|
|
464
|
+
// Render with isolated scope (render tag behavior) - use renderContext which includes hash params
|
|
465
|
+
const widgetContent = fs.readFileSync(widgetPath, 'utf8');
|
|
466
|
+
const rendered = await liquid.parseAndRender(widgetContent, renderContext);
|
|
467
|
+
return rendered;
|
|
468
|
+
} else {
|
|
469
|
+
// Try alternative paths
|
|
470
|
+
const altPath = path.join(themePath, 'widgets', 'shared', `${widgetName}.liquid`);
|
|
471
|
+
if (fs.existsSync(altPath)) {
|
|
472
|
+
const widgetContent = fs.readFileSync(altPath, 'utf8');
|
|
473
|
+
const rendered = await liquid.parseAndRender(widgetContent, renderContext);
|
|
474
|
+
return rendered;
|
|
475
|
+
}
|
|
476
|
+
console.error(chalk.red(`[RENDER TAG] ❌ Widget template not found: ${widgetPath}`));
|
|
477
|
+
}
|
|
478
|
+
} finally {
|
|
479
|
+
// Remove from rendering stack
|
|
480
|
+
renderingStack.delete(renderKey);
|
|
481
|
+
currentRenderDepth--;
|
|
482
|
+
}
|
|
483
|
+
return '';
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Handle snippet paths
|
|
487
|
+
if (filePath.startsWith('snippets/')) {
|
|
488
|
+
const snippetName = filePath.replace(/^snippets\//, '').replace(/\.liquid$/, '');
|
|
489
|
+
const snippetPath = path.join(themePath, 'snippets', `${snippetName}.liquid`);
|
|
490
|
+
try {
|
|
491
|
+
if (fs.existsSync(snippetPath)) {
|
|
492
|
+
const rendered = await liquid.parseAndRender(fs.readFileSync(snippetPath, 'utf8'), renderContext);
|
|
493
|
+
return rendered;
|
|
494
|
+
}
|
|
495
|
+
} finally {
|
|
496
|
+
currentRenderDepth--;
|
|
497
|
+
}
|
|
498
|
+
return '';
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Default behavior - try to resolve file
|
|
502
|
+
try {
|
|
503
|
+
const resolvedPath = path.join(themePath, filePath);
|
|
504
|
+
if (fs.existsSync(resolvedPath)) {
|
|
505
|
+
const rendered = await liquid.parseAndRender(fs.readFileSync(resolvedPath, 'utf8'), renderContext);
|
|
506
|
+
currentRenderDepth--;
|
|
507
|
+
return rendered;
|
|
508
|
+
}
|
|
509
|
+
// Try with .liquid extension
|
|
510
|
+
const resolvedPathWithExt = `${resolvedPath}.liquid`;
|
|
511
|
+
if (fs.existsSync(resolvedPathWithExt)) {
|
|
512
|
+
const rendered = await liquid.parseAndRender(fs.readFileSync(resolvedPathWithExt, 'utf8'), renderContext);
|
|
513
|
+
currentRenderDepth--;
|
|
514
|
+
return rendered;
|
|
515
|
+
}
|
|
516
|
+
} catch (error) {
|
|
517
|
+
console.warn(`[RENDER] Error rendering file ${filePath}:`, error.message);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
currentRenderDepth--;
|
|
521
|
+
return '';
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Render a single widget to HTML
|
|
528
|
+
* @param {Object} widget - Widget object
|
|
529
|
+
* @param {Object} context - Rendering context
|
|
530
|
+
* @param {string} themePath - Theme path
|
|
531
|
+
* @returns {Promise<string>} Rendered widget HTML
|
|
532
|
+
*/
|
|
533
|
+
async function renderWidget(widget, context, themePath) {
|
|
534
|
+
if (!widget || !widget.template_path) {
|
|
535
|
+
return `<div class="widget-error">Widget missing template_path</div>`;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
try {
|
|
539
|
+
const widgetPath = path.join(themePath, `${widget.template_path}.liquid`);
|
|
540
|
+
if (!fs.existsSync(widgetPath)) {
|
|
541
|
+
// Try without .liquid extension
|
|
542
|
+
const altPath = path.join(themePath, widget.template_path);
|
|
543
|
+
if (!fs.existsSync(altPath)) {
|
|
544
|
+
return `<div class="widget-error">Widget template not found: ${widget.template_path}</div>`;
|
|
545
|
+
}
|
|
546
|
+
// Use alt path
|
|
547
|
+
const widgetContent = fs.readFileSync(altPath, 'utf8');
|
|
548
|
+
const widgetContext = {
|
|
549
|
+
...context,
|
|
550
|
+
widget: widget
|
|
551
|
+
};
|
|
552
|
+
return await liquid.parseAndRender(widgetContent, widgetContext);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const widgetContent = fs.readFileSync(widgetPath, 'utf8');
|
|
556
|
+
const widgetContext = {
|
|
557
|
+
...context,
|
|
558
|
+
widget: widget
|
|
559
|
+
};
|
|
560
|
+
const rendered = await liquid.parseAndRender(widgetContent, widgetContext);
|
|
561
|
+
return rendered;
|
|
562
|
+
} catch (error) {
|
|
563
|
+
console.error(chalk.red(`[WIDGET RENDER] ❌ Error rendering widget ${widget.type}: ${error.message}`));
|
|
564
|
+
return `<div class="widget-error">Error rendering widget: ${error.message}</div>`;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Render template with layout support
|
|
570
|
+
* @param {Liquid} liquid - LiquidJS engine
|
|
571
|
+
* @param {string} templatePath - Template path (relative to theme, e.g., 'templates/index')
|
|
572
|
+
* @param {Object} context - Template context
|
|
573
|
+
* @param {string} themePath - Theme path
|
|
574
|
+
* @returns {Promise<string>} Rendered HTML
|
|
575
|
+
*/
|
|
576
|
+
/**
|
|
577
|
+
* Pre-process template content to transform render_widget filter calls into tag calls
|
|
578
|
+
* This allows the filter syntax {{ widget | render_widget }} to work without modifying theme files
|
|
579
|
+
*/
|
|
580
|
+
function preprocessTemplate(content) {
|
|
581
|
+
if (!content || typeof content !== 'string') {
|
|
582
|
+
return content;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Replace {{ widget | render_widget }} with {% render_widget widget %}
|
|
586
|
+
const filterPattern = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_.]*)\s*\|\s*render_widget\s*\}\}/gs;
|
|
587
|
+
|
|
588
|
+
// Pattern for widget loops that might need rendering code injected
|
|
589
|
+
const widgetLoopPattern = /(\{%\s*for\s+widget\s+in\s+[\w.]+\s*%\})([\s\S]*?)(\{%\s*endfor\s*%\})/g;
|
|
590
|
+
|
|
591
|
+
// Transform filter syntax to tag syntax
|
|
592
|
+
let transformed = content.replace(filterPattern, (match, variable) => {
|
|
593
|
+
return `{% render_widget ${variable} %}`;
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
// Check for widget loops with empty wrappers and inject rendering code
|
|
597
|
+
widgetLoopPattern.lastIndex = 0;
|
|
598
|
+
let loopMatch;
|
|
599
|
+
while ((loopMatch = widgetLoopPattern.exec(transformed)) !== null) {
|
|
600
|
+
const [fullMatch, forTag, loopBody, endforTag] = loopMatch;
|
|
601
|
+
const hasWrapper = loopBody.includes('theme-widget-wrapper');
|
|
602
|
+
const hasRendering = loopBody.includes('render_widget') || loopBody.includes('{% render');
|
|
603
|
+
|
|
604
|
+
if (hasWrapper && !hasRendering) {
|
|
605
|
+
const wrapperDivPattern = /(<div[^>]*theme-widget-wrapper[^>]*>)([\s\S]*?)(<\/div>)/;
|
|
606
|
+
const wrapperMatch = loopBody.match(wrapperDivPattern);
|
|
607
|
+
|
|
608
|
+
if (wrapperMatch) {
|
|
609
|
+
const [wrapperFull, openingDiv, innerContent, closingDiv] = wrapperMatch;
|
|
610
|
+
if (!innerContent.trim() || innerContent.trim().length < 10) {
|
|
611
|
+
const injectedContent = `${openingDiv}
|
|
612
|
+
{% if widget and widget.template_path %}
|
|
613
|
+
{% render widget.template_path, widget: widget, settings: settings %}
|
|
614
|
+
{% else %}
|
|
615
|
+
<div class="widget-error">Widget template not found</div>
|
|
616
|
+
{% endif %}
|
|
617
|
+
${closingDiv}`;
|
|
618
|
+
transformed = transformed.replace(wrapperFull, injectedContent);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return transformed;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Render a single widget to HTML
|
|
629
|
+
* @param {Object} widget - Widget object
|
|
630
|
+
* @param {Object} context - Rendering context
|
|
631
|
+
* @param {string} themePath - Theme path
|
|
632
|
+
* @returns {Promise<string>} Rendered widget HTML
|
|
633
|
+
*/
|
|
634
|
+
async function renderWidget(widget, context, themePath) {
|
|
635
|
+
if (!widget || !widget.template_path) {
|
|
636
|
+
return `<div class="widget-error">Widget missing template_path</div>`;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
try {
|
|
640
|
+
const widgetPath = path.join(themePath, `${widget.template_path}.liquid`);
|
|
641
|
+
if (!fs.existsSync(widgetPath)) {
|
|
642
|
+
// Try without .liquid extension
|
|
643
|
+
const altPath = path.join(themePath, widget.template_path);
|
|
644
|
+
if (!fs.existsSync(altPath)) {
|
|
645
|
+
return `<div class="widget-error">Widget template not found: ${widget.template_path}</div>`;
|
|
646
|
+
}
|
|
647
|
+
// Use alt path
|
|
648
|
+
const widgetContent = fs.readFileSync(altPath, 'utf8');
|
|
649
|
+
const widgetContext = {
|
|
650
|
+
...context,
|
|
651
|
+
widget: widget
|
|
652
|
+
};
|
|
653
|
+
return await liquid.parseAndRender(widgetContent, widgetContext);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const widgetContent = fs.readFileSync(widgetPath, 'utf8');
|
|
657
|
+
const widgetContext = {
|
|
658
|
+
...context,
|
|
659
|
+
widget: widget
|
|
660
|
+
};
|
|
661
|
+
const rendered = await liquid.parseAndRender(widgetContent, widgetContext);
|
|
662
|
+
return rendered;
|
|
663
|
+
} catch (error) {
|
|
664
|
+
console.error(chalk.red(`[WIDGET RENDER] ❌ Error rendering widget ${widget.type}: ${error.message}`));
|
|
665
|
+
return `<div class="widget-error">Error rendering widget: ${error.message}</div>`;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
async function renderWithLayout(liquid, templatePath, context, themePath) {
|
|
670
|
+
try {
|
|
671
|
+
// Store context globally so sections can access it
|
|
672
|
+
const previousContext = currentRenderingContext;
|
|
673
|
+
currentRenderingContext = context;
|
|
674
|
+
|
|
675
|
+
try {
|
|
676
|
+
const fullTemplatePath = path.join(themePath, `${templatePath}.liquid`);
|
|
677
|
+
|
|
678
|
+
if (!fs.existsSync(fullTemplatePath)) {
|
|
679
|
+
throw new Error(`Template not found: ${fullTemplatePath}`);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Debug: Log context data for templates
|
|
683
|
+
const contextSummary = {
|
|
684
|
+
products: context.products?.length || 0,
|
|
685
|
+
widgets: Object.keys(context.widgets || {}).map(s => `${s}:${context.widgets[s]?.length || 0}`).join(', '),
|
|
686
|
+
categories: context.categories?.length || 0,
|
|
687
|
+
brands: context.brands?.length || 0,
|
|
688
|
+
menus: context.menus?.length || 0,
|
|
689
|
+
cart: context.cart?.itemCount || 0
|
|
690
|
+
};
|
|
691
|
+
console.log(`[RENDER] ${templatePath} - Context:`, JSON.stringify(contextSummary));
|
|
692
|
+
|
|
693
|
+
let templateContent = fs.readFileSync(fullTemplatePath, 'utf-8');
|
|
694
|
+
|
|
695
|
+
// Pre-process template to convert render_widget filter to tag
|
|
696
|
+
templateContent = preprocessTemplate(templateContent);
|
|
697
|
+
|
|
698
|
+
// DEBUG: Check if template uses render_widget filter
|
|
699
|
+
const usesRenderWidgetFilter = templateContent.includes('render_widget');
|
|
700
|
+
if (usesRenderWidgetFilter) {
|
|
701
|
+
console.log(chalk.yellow(`[RENDER] ⚠️ Template uses render_widget filter - checking if filter is available...`));
|
|
702
|
+
const hasFilter = liquid.filters && liquid.filters['render_widget'];
|
|
703
|
+
console.log(chalk.yellow(`[RENDER] Filter available: ${hasFilter ? '✅' : '❌'}`));
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Check for layout directive
|
|
707
|
+
const layoutMatch = templateContent.match(/\{%\s*layout\s+['"](.+?)['"]\s*%\}/);
|
|
708
|
+
|
|
709
|
+
if (layoutMatch) {
|
|
710
|
+
let layoutPath = layoutMatch[1]; // e.g., 'layout/theme' or 'theme'
|
|
711
|
+
|
|
712
|
+
// Remove layout directive from template
|
|
713
|
+
const contentWithoutLayout = templateContent.replace(/\{%\s*layout\s+['"](.+?)['"]\s*%\}/, '').trim();
|
|
714
|
+
|
|
715
|
+
// Render template content first
|
|
716
|
+
// Wrap in try-catch to catch any filter-related errors
|
|
717
|
+
let renderedContent;
|
|
718
|
+
try {
|
|
719
|
+
renderedContent = await liquid.parseAndRender(contentWithoutLayout, context);
|
|
720
|
+
} catch (renderError) {
|
|
721
|
+
console.error(chalk.red(`[RENDER] ❌ Error rendering template content:`));
|
|
722
|
+
console.error(chalk.red(` Error: ${renderError.message}`));
|
|
723
|
+
if (renderError.stack) {
|
|
724
|
+
console.error(chalk.red(` Stack: ${renderError.stack.split('\n').slice(0, 5).join('\n')}`));
|
|
725
|
+
}
|
|
726
|
+
// Check if error is related to render_widget filter
|
|
727
|
+
if (renderError.message && renderError.message.includes('render_widget')) {
|
|
728
|
+
console.error(chalk.yellow(`[RENDER] ⚠️ This error is related to render_widget filter - filter may not be working correctly`));
|
|
729
|
+
}
|
|
730
|
+
throw renderError;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Render layout with content
|
|
734
|
+
const layoutContext = {
|
|
735
|
+
...context,
|
|
736
|
+
content: renderedContent
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
// Resolve layout path
|
|
740
|
+
// If layoutPath doesn't include 'layout/', assume it's just the layout name
|
|
741
|
+
// and prepend 'layout/'
|
|
742
|
+
if (!layoutPath.includes('/')) {
|
|
743
|
+
layoutPath = `layout/${layoutPath}`;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Build full layout file path
|
|
747
|
+
const layoutFile = path.join(themePath, `${layoutPath}.liquid`);
|
|
748
|
+
|
|
749
|
+
if (fs.existsSync(layoutFile)) {
|
|
750
|
+
let layoutContent = fs.readFileSync(layoutFile, 'utf-8');
|
|
751
|
+
|
|
752
|
+
// Set content_for_layout in context (Shopify-compatible approach)
|
|
753
|
+
// This is the proper way to pass content to layouts
|
|
754
|
+
layoutContext.content_for_layout = renderedContent;
|
|
755
|
+
layoutContext.content = renderedContent;
|
|
756
|
+
|
|
757
|
+
// Parse and render the layout with content in context
|
|
758
|
+
// The layout should use {{ content_for_layout }} or {{ content }} to output
|
|
759
|
+
return await liquid.parseAndRender(layoutContent, layoutContext);
|
|
760
|
+
} else {
|
|
761
|
+
// Try to create default layout if it doesn't exist
|
|
762
|
+
const layoutDir = path.dirname(layoutFile);
|
|
763
|
+
const layoutName = path.basename(layoutPath);
|
|
764
|
+
|
|
765
|
+
if (layoutName === 'theme' && !fs.existsSync(layoutFile)) {
|
|
766
|
+
console.warn(`[Liquid Engine] Layout not found: ${layoutFile}`);
|
|
767
|
+
console.warn(`[Liquid Engine] Creating default theme layout...`);
|
|
768
|
+
fs.ensureDirSync(layoutDir);
|
|
769
|
+
|
|
770
|
+
const defaultLayout = `<!DOCTYPE html>
|
|
771
|
+
<html lang="en">
|
|
772
|
+
<head>
|
|
773
|
+
<meta charset="UTF-8">
|
|
774
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
775
|
+
<title>{{ shop.name }}{% if page.title %} - {{ page.title }}{% endif %}</title>
|
|
776
|
+
</head>
|
|
777
|
+
<body>
|
|
778
|
+
{{ content }}
|
|
779
|
+
</body>
|
|
780
|
+
</html>`;
|
|
781
|
+
fs.writeFileSync(layoutFile, defaultLayout);
|
|
782
|
+
|
|
783
|
+
// Read the newly created layout
|
|
784
|
+
let layoutContent = fs.readFileSync(layoutFile, 'utf-8');
|
|
785
|
+
// Set content in context
|
|
786
|
+
layoutContext.content_for_layout = renderedContent;
|
|
787
|
+
layoutContext.content = renderedContent;
|
|
788
|
+
return await liquid.parseAndRender(layoutContent, layoutContext);
|
|
789
|
+
} else {
|
|
790
|
+
throw new Error(`Layout not found: ${layoutFile}`);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
} else {
|
|
794
|
+
// No layout directive, render template directly
|
|
795
|
+
return await liquid.parseAndRender(templateContent, context);
|
|
796
|
+
}
|
|
797
|
+
} finally {
|
|
798
|
+
// Restore previous context
|
|
799
|
+
currentRenderingContext = previousContext;
|
|
800
|
+
}
|
|
801
|
+
} catch (error) {
|
|
802
|
+
console.error('Error rendering template with layout:', error);
|
|
803
|
+
throw error;
|
|
804
|
+
} finally {
|
|
805
|
+
// Ensure context is cleared on error
|
|
806
|
+
currentRenderingContext = null;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Get current rendering context (for use in filters)
|
|
812
|
+
* @returns {Object|null} Current rendering context
|
|
813
|
+
*/
|
|
814
|
+
function getCurrentContext() {
|
|
815
|
+
return currentRenderingContext;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
module.exports = {
|
|
819
|
+
createLiquidEngine,
|
|
820
|
+
renderWithLayout,
|
|
821
|
+
getCurrentContext
|
|
822
|
+
};
|