@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.
Files changed (116) hide show
  1. package/README.md +425 -0
  2. package/assets/Logo_o2vend.png +0 -0
  3. package/assets/favicon.png +0 -0
  4. package/assets/logo-white.png +0 -0
  5. package/bin/o2vend +42 -0
  6. package/config/widget-map.json +50 -0
  7. package/lib/commands/check.js +201 -0
  8. package/lib/commands/generate.js +33 -0
  9. package/lib/commands/init.js +214 -0
  10. package/lib/commands/optimize.js +216 -0
  11. package/lib/commands/package.js +208 -0
  12. package/lib/commands/serve.js +105 -0
  13. package/lib/commands/validate.js +191 -0
  14. package/lib/lib/api-client.js +357 -0
  15. package/lib/lib/dev-server.js +2618 -0
  16. package/lib/lib/file-watcher.js +80 -0
  17. package/lib/lib/hot-reload.js +106 -0
  18. package/lib/lib/liquid-engine.js +822 -0
  19. package/lib/lib/liquid-filters.js +671 -0
  20. package/lib/lib/mock-api-server.js +989 -0
  21. package/lib/lib/mock-data.js +1468 -0
  22. package/lib/lib/widget-service.js +321 -0
  23. package/package.json +70 -0
  24. package/test-theme/README.md +27 -0
  25. package/test-theme/assets/async-sections.js +446 -0
  26. package/test-theme/assets/cart-drawer.js +463 -0
  27. package/test-theme/assets/cart-manager.js +223 -0
  28. package/test-theme/assets/checkout-price-handler.js +368 -0
  29. package/test-theme/assets/components.css +4629 -0
  30. package/test-theme/assets/delivery-zone.css +299 -0
  31. package/test-theme/assets/delivery-zone.js +396 -0
  32. package/test-theme/assets/logo.png +0 -0
  33. package/test-theme/assets/sections.css +48 -0
  34. package/test-theme/assets/theme.css +3500 -0
  35. package/test-theme/assets/theme.js +3745 -0
  36. package/test-theme/config/settings_data.json +292 -0
  37. package/test-theme/config/settings_schema.json +1050 -0
  38. package/test-theme/layout/theme.liquid +195 -0
  39. package/test-theme/locales/en.default.json +260 -0
  40. package/test-theme/sections/content-fallback.liquid +53 -0
  41. package/test-theme/sections/content.liquid +57 -0
  42. package/test-theme/sections/footer-fallback.liquid +328 -0
  43. package/test-theme/sections/footer.liquid +278 -0
  44. package/test-theme/sections/header-fallback.liquid +1805 -0
  45. package/test-theme/sections/header.liquid +1145 -0
  46. package/test-theme/sections/hero-fallback.liquid +212 -0
  47. package/test-theme/sections/hero.liquid +136 -0
  48. package/test-theme/snippets/account-sidebar.liquid +200 -0
  49. package/test-theme/snippets/add-to-cart-modal.liquid +484 -0
  50. package/test-theme/snippets/breadcrumbs.liquid +134 -0
  51. package/test-theme/snippets/cart-drawer.liquid +467 -0
  52. package/test-theme/snippets/delivery-zone-city-selector.liquid +79 -0
  53. package/test-theme/snippets/delivery-zone-modal.liquid +337 -0
  54. package/test-theme/snippets/delivery-zone-search.liquid +78 -0
  55. package/test-theme/snippets/icon.liquid +105 -0
  56. package/test-theme/snippets/login-modal.liquid +346 -0
  57. package/test-theme/snippets/mega-menu.liquid +812 -0
  58. package/test-theme/snippets/news-thumbnail.liquid +187 -0
  59. package/test-theme/snippets/pagination.liquid +120 -0
  60. package/test-theme/snippets/price.liquid +92 -0
  61. package/test-theme/snippets/product-card-related.liquid +78 -0
  62. package/test-theme/snippets/product-card-simple.liquid +41 -0
  63. package/test-theme/snippets/product-card.liquid +697 -0
  64. package/test-theme/snippets/rating.liquid +85 -0
  65. package/test-theme/snippets/skeleton-collection-grid.liquid +114 -0
  66. package/test-theme/snippets/skeleton-product-card.liquid +124 -0
  67. package/test-theme/snippets/skeleton-product-grid.liquid +34 -0
  68. package/test-theme/snippets/social-sharing.liquid +185 -0
  69. package/test-theme/templates/account/dashboard.liquid +401 -0
  70. package/test-theme/templates/account/loyalty-redemption.liquid +405 -0
  71. package/test-theme/templates/account/loyalty.liquid +588 -0
  72. package/test-theme/templates/account/order-detail.liquid +230 -0
  73. package/test-theme/templates/account/orders.liquid +349 -0
  74. package/test-theme/templates/account/profile.liquid +758 -0
  75. package/test-theme/templates/account/register.liquid +232 -0
  76. package/test-theme/templates/account/return-orders.liquid +348 -0
  77. package/test-theme/templates/account/store-credit.liquid +464 -0
  78. package/test-theme/templates/account/subscriptions.liquid +601 -0
  79. package/test-theme/templates/account/wishlist.liquid +419 -0
  80. package/test-theme/templates/address-book.liquid +1092 -0
  81. package/test-theme/templates/categories.liquid +452 -0
  82. package/test-theme/templates/checkout.liquid +4511 -0
  83. package/test-theme/templates/error.liquid +384 -0
  84. package/test-theme/templates/index.liquid +11 -0
  85. package/test-theme/templates/login.liquid +185 -0
  86. package/test-theme/templates/order-confirmation.liquid +720 -0
  87. package/test-theme/templates/page.liquid +297 -0
  88. package/test-theme/templates/product-detail.liquid +4363 -0
  89. package/test-theme/templates/products.liquid +518 -0
  90. package/test-theme/templates/search.liquid +922 -0
  91. package/test-theme/theme.json.example +19 -0
  92. package/test-theme/widgets/brand-carousel.liquid +676 -0
  93. package/test-theme/widgets/brand.liquid +245 -0
  94. package/test-theme/widgets/carousel.liquid +843 -0
  95. package/test-theme/widgets/category-list-carousel.liquid +656 -0
  96. package/test-theme/widgets/category-list.liquid +340 -0
  97. package/test-theme/widgets/category.liquid +475 -0
  98. package/test-theme/widgets/discount-time.liquid +176 -0
  99. package/test-theme/widgets/footer-menu.liquid +695 -0
  100. package/test-theme/widgets/footer.liquid +179 -0
  101. package/test-theme/widgets/gallery.liquid +271 -0
  102. package/test-theme/widgets/header-menu.liquid +932 -0
  103. package/test-theme/widgets/header.liquid +159 -0
  104. package/test-theme/widgets/html.liquid +214 -0
  105. package/test-theme/widgets/news.liquid +217 -0
  106. package/test-theme/widgets/product-canvas.liquid +235 -0
  107. package/test-theme/widgets/product-carousel.liquid +502 -0
  108. package/test-theme/widgets/product.liquid +45 -0
  109. package/test-theme/widgets/recently-viewed.liquid +26 -0
  110. package/test-theme/widgets/shared/product-grid.liquid +339 -0
  111. package/test-theme/widgets/simple-product.liquid +42 -0
  112. package/test-theme/widgets/single-product.liquid +610 -0
  113. package/test-theme/widgets/spacebar-carousel.liquid +663 -0
  114. package/test-theme/widgets/spacebar.liquid +279 -0
  115. package/test-theme/widgets/splash.liquid +378 -0
  116. package/test-theme/widgets/testimonial-carousel.liquid +709 -0
@@ -0,0 +1,671 @@
1
+ /**
2
+ * O2VEND Liquid Filters
3
+ * Standalone implementation - copied and adapted from webstore solution
4
+ * Update manually when webstore solution changes
5
+ *
6
+ * Source: o2vend-webstore/services/liquid-helper-service.js
7
+ */
8
+
9
+ const moment = require('moment');
10
+ const _ = require('lodash');
11
+
12
+ /**
13
+ * Liquid Helper Service
14
+ * Provides additional filters and functions for liquid templates
15
+ */
16
+ class LiquidHelperService {
17
+ constructor() {
18
+ this.filters = {
19
+ // Date filters
20
+ date: this.dateFilter.bind(this),
21
+ time_ago: this.timeAgoFilter.bind(this),
22
+ time_until: this.timeUntilFilter.bind(this),
23
+
24
+ // String filters
25
+ truncate: this.truncateFilter.bind(this),
26
+ capitalize: this.capitalizeFilter.bind(this),
27
+ upcase: this.upcaseFilter.bind(this),
28
+ downcase: this.downcaseFilter.bind(this),
29
+ strip: this.stripFilter.bind(this),
30
+ lstrip: this.lstripFilter.bind(this),
31
+ rstrip: this.rstripFilter.bind(this),
32
+ split: this.splitFilter.bind(this),
33
+ join: this.joinFilter.bind(this),
34
+ replace: this.replaceFilter.bind(this),
35
+ remove: this.removeFilter.bind(this),
36
+ prepend: this.prependFilter.bind(this),
37
+ append: this.appendFilter.bind(this),
38
+
39
+ // Number filters
40
+ plus: this.plusFilter.bind(this),
41
+ minus: this.minusFilter.bind(this),
42
+ times: this.timesFilter.bind(this),
43
+ divided_by: this.dividedByFilter.bind(this),
44
+ modulo: this.moduloFilter.bind(this),
45
+ round: this.roundFilter.bind(this),
46
+ ceil: this.ceilFilter.bind(this),
47
+ floor: this.floorFilter.bind(this),
48
+ abs: this.absFilter.bind(this),
49
+
50
+ // Array filters
51
+ first: this.firstFilter.bind(this),
52
+ last: this.lastFilter.bind(this),
53
+ sort: this.sortFilter.bind(this),
54
+ reverse: this.reverseFilter.bind(this),
55
+ uniq: this.uniqFilter.bind(this),
56
+ map: this.mapFilter.bind(this),
57
+ where: this.whereFilter.bind(this),
58
+ size: this.sizeFilter.bind(this),
59
+ slice: this.sliceFilter.bind(this),
60
+
61
+ // Object filters
62
+ default: this.defaultFilter.bind(this),
63
+ json: this.jsonFilter.bind(this),
64
+
65
+ // Money filters
66
+ money: this.moneyFilter.bind(this),
67
+ money_with_currency: this.moneyWithCurrencyFilter.bind(this),
68
+ money_with_settings: this.moneyWithSettingsFilter.bind(this),
69
+
70
+ // URL filters
71
+ url_encode: this.urlEncodeFilter.bind(this),
72
+ url_decode: this.urlDecodeFilter.bind(this),
73
+
74
+ // HTML filters
75
+ escape: this.escapeFilter.bind(this),
76
+ strip_html: this.stripHtmlFilter.bind(this),
77
+ strip_newlines: this.stripNewlinesFilter.bind(this),
78
+
79
+ // Image filters
80
+ img_url: this.imgUrlFilter.bind(this),
81
+ img_tag: this.imgTagFilter.bind(this),
82
+
83
+ // Product filters
84
+ product_url: this.productUrlFilter.bind(this),
85
+ collection_url: this.collectionUrlFilter.bind(this),
86
+ page_url: this.pageUrlFilter.bind(this),
87
+
88
+ // Utility filters
89
+ pluralize: this.pluralizeFilter.bind(this),
90
+ handleize: this.handleizeFilter.bind(this),
91
+ highlight: this.highlightFilter.bind(this),
92
+ truncatewords: this.truncateWordsFilter.bind(this),
93
+ newline_to_br: this.newlineToBrFilter.bind(this),
94
+
95
+ // Asset versioning filter (adds version query parameter)
96
+ asset_url: this.assetUrlFilter.bind(this),
97
+
98
+ // HTML tag filters
99
+ stylesheet_tag: this.stylesheetTagFilter.bind(this),
100
+ script_tag: this.scriptTagFilter.bind(this),
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Get all available filters
106
+ * @returns {Object} Filters object
107
+ */
108
+ getFilters() {
109
+ return this.filters;
110
+ }
111
+
112
+ // Date filters
113
+ dateFilter(input, format) {
114
+ if (!input) return '';
115
+
116
+ let date;
117
+ if (typeof input === 'string') {
118
+ const normalizedInput = input.trim();
119
+ const lowerInput = normalizedInput.toLowerCase();
120
+
121
+ if (lowerInput === 'now' || lowerInput === 'today') {
122
+ date = moment();
123
+ } else if (normalizedInput.match(/^\d{4}-\d{2}-\d{2}t\d{2}:\d{2}:\d{2}/i)) {
124
+ date = moment(normalizedInput);
125
+ } else if (normalizedInput.match(/^\d{4}-\d{2}-\d{2}/)) {
126
+ date = moment(normalizedInput, 'YYYY-MM-DD');
127
+ } else {
128
+ if (normalizedInput.match(/[©a-zA-Z]/) && !normalizedInput.match(/^\d{4}/)) {
129
+ if (format && (format.includes('%Y') || format.includes('YYYY'))) {
130
+ return moment().format('YYYY');
131
+ }
132
+ return input;
133
+ }
134
+
135
+ date = moment(normalizedInput, moment.ISO_8601, true);
136
+ if (!date.isValid()) {
137
+ try {
138
+ date = moment(normalizedInput);
139
+ } catch (e) {
140
+ if (format && (format.includes('%Y') || format.includes('YYYY'))) {
141
+ return moment().format('YYYY');
142
+ }
143
+ return input;
144
+ }
145
+ }
146
+ }
147
+ } else if (input instanceof Date) {
148
+ date = moment(input);
149
+ } else if (typeof input === 'number') {
150
+ date = moment.unix(input);
151
+ } else {
152
+ try {
153
+ date = moment(input);
154
+ } catch (e) {
155
+ if (format && (format.includes('%Y') || format.includes('YYYY'))) {
156
+ return moment().format('YYYY');
157
+ }
158
+ return input;
159
+ }
160
+ }
161
+
162
+ if (!date || !date.isValid()) {
163
+ if (format && (format.includes('%Y') || format.includes('YYYY'))) {
164
+ return moment().format('YYYY');
165
+ }
166
+ return input;
167
+ }
168
+
169
+ const momentFormat = (format || 'YYYY-MM-DD')
170
+ .replace(/%Y/g, 'YYYY')
171
+ .replace(/%m/g, 'MM')
172
+ .replace(/%d/g, 'DD')
173
+ .replace(/%H/g, 'HH')
174
+ .replace(/%M/g, 'mm')
175
+ .replace(/%S/g, 'ss')
176
+ .replace(/%y/g, 'YY');
177
+
178
+ return date.format(momentFormat);
179
+ }
180
+
181
+ timeAgoFilter(input) {
182
+ if (!input) return '';
183
+
184
+ let date;
185
+ if (typeof input === 'string') {
186
+ const normalizedInput = input.trim().toLowerCase();
187
+ if (normalizedInput === 'now' || normalizedInput === 'today') {
188
+ date = moment();
189
+ } else if (normalizedInput.match(/^\d{4}-\d{2}-\d{2}t\d{2}:\d{2}:\d{2}/)) {
190
+ date = moment(normalizedInput);
191
+ } else if (normalizedInput.match(/^\d{4}-\d{2}-\d{2}/)) {
192
+ date = moment(normalizedInput, 'YYYY-MM-DD');
193
+ } else {
194
+ date = moment(normalizedInput, moment.ISO_8601, true);
195
+ if (!date.isValid()) {
196
+ date = moment(normalizedInput);
197
+ }
198
+ }
199
+ } else if (input instanceof Date) {
200
+ date = moment(input);
201
+ } else {
202
+ date = moment(input);
203
+ }
204
+
205
+ return date.isValid() ? date.fromNow() : input;
206
+ }
207
+
208
+ timeUntilFilter(input) {
209
+ if (!input) return '';
210
+
211
+ let date;
212
+ if (typeof input === 'string') {
213
+ const normalizedInput = input.trim().toLowerCase();
214
+ if (normalizedInput === 'now' || normalizedInput === 'today') {
215
+ date = moment();
216
+ } else if (normalizedInput.match(/^\d{4}-\d{2}-\d{2}t\d{2}:\d{2}:\d{2}/)) {
217
+ date = moment(normalizedInput);
218
+ } else if (normalizedInput.match(/^\d{4}-\d{2}-\d{2}/)) {
219
+ date = moment(normalizedInput, 'YYYY-MM-DD');
220
+ } else {
221
+ date = moment(normalizedInput, moment.ISO_8601, true);
222
+ if (!date.isValid()) {
223
+ date = moment(normalizedInput);
224
+ }
225
+ }
226
+ } else if (input instanceof Date) {
227
+ date = moment(input);
228
+ } else {
229
+ date = moment(input);
230
+ }
231
+
232
+ return date.isValid() ? date.toNow() : input;
233
+ }
234
+
235
+ // String filters
236
+ truncateFilter(input, length = 50, truncateString = '...') {
237
+ if (!input || typeof input !== 'string') return input;
238
+ return input.length > length ?
239
+ input.substring(0, length) + truncateString :
240
+ input;
241
+ }
242
+
243
+ capitalizeFilter(input) {
244
+ if (!input || typeof input !== 'string') return input;
245
+ return input.charAt(0).toUpperCase() + input.slice(1).toLowerCase();
246
+ }
247
+
248
+ upcaseFilter(input) {
249
+ return input && typeof input === 'string' ? input.toUpperCase() : input;
250
+ }
251
+
252
+ downcaseFilter(input) {
253
+ return input && typeof input === 'string' ? input.toLowerCase() : input;
254
+ }
255
+
256
+ stripFilter(input) {
257
+ return input && typeof input === 'string' ? input.trim() : input;
258
+ }
259
+
260
+ lstripFilter(input) {
261
+ return input && typeof input === 'string' ? input.replace(/^\s+/, '') : input;
262
+ }
263
+
264
+ rstripFilter(input) {
265
+ return input && typeof input === 'string' ? input.replace(/\s+$/, '') : input;
266
+ }
267
+
268
+ splitFilter(input, delimiter) {
269
+ if (!input || typeof input !== 'string') return [];
270
+ return input.split(delimiter || ' ');
271
+ }
272
+
273
+ joinFilter(input, delimiter) {
274
+ if (!Array.isArray(input)) return input;
275
+ return input.join(delimiter || ' ');
276
+ }
277
+
278
+ replaceFilter(input, search, replace) {
279
+ if (!input || typeof input !== 'string') return input;
280
+ return input.replace(new RegExp(search, 'g'), replace);
281
+ }
282
+
283
+ removeFilter(input, search) {
284
+ if (!input || typeof input !== 'string') return input;
285
+ return input.replace(new RegExp(search, 'g'), '');
286
+ }
287
+
288
+ prependFilter(input, prepend) {
289
+ if (!input) return prepend || '';
290
+ return (prepend || '') + input;
291
+ }
292
+
293
+ appendFilter(input, append) {
294
+ if (!input) return append || '';
295
+ return input + (append || '');
296
+ }
297
+
298
+ // Number filters
299
+ plusFilter(input, number) {
300
+ const num = parseFloat(input);
301
+ const add = parseFloat(number);
302
+ return isNaN(num) || isNaN(add) ? input : num + add;
303
+ }
304
+
305
+ minusFilter(input, number) {
306
+ const num = parseFloat(input);
307
+ const sub = parseFloat(number);
308
+ return isNaN(num) || isNaN(sub) ? input : num - sub;
309
+ }
310
+
311
+ timesFilter(input, number) {
312
+ const num = parseFloat(input);
313
+ const mult = parseFloat(number);
314
+ return isNaN(num) || isNaN(mult) ? input : num * mult;
315
+ }
316
+
317
+ dividedByFilter(input, number) {
318
+ const num = parseFloat(input);
319
+ const div = parseFloat(number);
320
+ return isNaN(num) || isNaN(div) || div === 0 ? input : num / div;
321
+ }
322
+
323
+ moduloFilter(input, number) {
324
+ const num = parseFloat(input);
325
+ const mod = parseFloat(number);
326
+ return isNaN(num) || isNaN(mod) || mod === 0 ? input : num % mod;
327
+ }
328
+
329
+ roundFilter(input, decimals = 0) {
330
+ const num = parseFloat(input);
331
+ return isNaN(num) ? input : Math.round(num * Math.pow(10, decimals)) / Math.pow(10, decimals);
332
+ }
333
+
334
+ ceilFilter(input) {
335
+ const num = parseFloat(input);
336
+ return isNaN(num) ? input : Math.ceil(num);
337
+ }
338
+
339
+ floorFilter(input) {
340
+ const num = parseFloat(input);
341
+ return isNaN(num) ? input : Math.floor(num);
342
+ }
343
+
344
+ absFilter(input) {
345
+ const num = parseFloat(input);
346
+ return isNaN(num) ? input : Math.abs(num);
347
+ }
348
+
349
+ // Array filters
350
+ firstFilter(input) {
351
+ return Array.isArray(input) && input.length > 0 ? input[0] : input;
352
+ }
353
+
354
+ lastFilter(input) {
355
+ return Array.isArray(input) && input.length > 0 ? input[input.length - 1] : input;
356
+ }
357
+
358
+ sortFilter(input, property) {
359
+ if (!Array.isArray(input)) return input;
360
+ if (property) {
361
+ return input.sort((a, b) => {
362
+ const aVal = _.get(a, property);
363
+ const bVal = _.get(b, property);
364
+ return aVal > bVal ? 1 : aVal < bVal ? -1 : 0;
365
+ });
366
+ }
367
+ return input.sort();
368
+ }
369
+
370
+ reverseFilter(input) {
371
+ return Array.isArray(input) ? input.slice().reverse() : input;
372
+ }
373
+
374
+ uniqFilter(input, property) {
375
+ if (!Array.isArray(input)) return input;
376
+ if (property) {
377
+ const seen = new Set();
378
+ return input.filter(item => {
379
+ const val = _.get(item, property);
380
+ if (seen.has(val)) return false;
381
+ seen.add(val);
382
+ return true;
383
+ });
384
+ }
385
+ return [...new Set(input)];
386
+ }
387
+
388
+ mapFilter(input, property) {
389
+ if (!Array.isArray(input)) return input;
390
+ return input.map(item => _.get(item, property));
391
+ }
392
+
393
+ whereFilter(input, property, value) {
394
+ if (!Array.isArray(input)) return input;
395
+ return input.filter(item => _.get(item, property) === value);
396
+ }
397
+
398
+ sizeFilter(input) {
399
+ if (Array.isArray(input)) return input.length;
400
+ if (typeof input === 'string') return input.length;
401
+ if (typeof input === 'object' && input !== null) return Object.keys(input).length;
402
+ return 0;
403
+ }
404
+
405
+ sliceFilter(input, start, length) {
406
+ if (!Array.isArray(input)) return input;
407
+ return input.slice(start, start + length);
408
+ }
409
+
410
+ // Object filters
411
+ defaultFilter(input, defaultValue) {
412
+ return input || defaultValue;
413
+ }
414
+
415
+ jsonFilter(input) {
416
+ try {
417
+ return JSON.stringify(input);
418
+ } catch (error) {
419
+ return input;
420
+ }
421
+ }
422
+
423
+ // Money filters
424
+ moneyFilter(input, currency = 'USD') {
425
+ const num = parseFloat(input);
426
+ if (isNaN(num)) return input;
427
+
428
+ const formatter = new Intl.NumberFormat('en-US', {
429
+ style: 'currency',
430
+ currency: currency
431
+ });
432
+
433
+ return formatter.format(num);
434
+ }
435
+
436
+ moneyWithCurrencyFilter(input, currency = 'USD') {
437
+ return this.moneyFilter(input, currency);
438
+ }
439
+
440
+ moneyWithSettingsFilter(input, settings = {}) {
441
+ const num = parseFloat(input);
442
+ if (isNaN(num)) return input;
443
+
444
+ const {
445
+ currencySymbol = '₹',
446
+ currencyFormat = '#,##0.00',
447
+ currencyDecimalDigits = 2,
448
+ currencyGroupSeparator = ',',
449
+ currencyDecimalSeparator = '.',
450
+ currencyGroupSizes = [3]
451
+ } = settings;
452
+
453
+ const formatNumberWithSettings = (num, settings) => {
454
+ const {
455
+ decimalDigits = 2,
456
+ groupSeparator = ',',
457
+ decimalSeparator = '.',
458
+ groupSizes = [3]
459
+ } = settings;
460
+
461
+ let formatted = num.toFixed(decimalDigits);
462
+ const parts = formatted.split('.');
463
+ let integerPart = parts[0];
464
+ const decimalPart = parts[1];
465
+
466
+ if (groupSizes.length > 0) {
467
+ const groupSize = groupSizes[0];
468
+ const regex = new RegExp(`\\B(?=(\\d{${groupSize}})+(?!\\d))`, 'g');
469
+ integerPart = integerPart.replace(regex, groupSeparator);
470
+ }
471
+
472
+ if (decimalPart && decimalDigits > 0) {
473
+ return `${integerPart}${decimalSeparator}${decimalPart}`;
474
+ }
475
+
476
+ return integerPart;
477
+ };
478
+
479
+ const formatted = formatNumberWithSettings(num, {
480
+ decimalDigits: currencyDecimalDigits,
481
+ groupSeparator: currencyGroupSeparator,
482
+ decimalSeparator: currencyDecimalSeparator,
483
+ groupSizes: currencyGroupSizes
484
+ });
485
+
486
+ return `${currencySymbol}${formatted}`;
487
+ }
488
+
489
+ // URL filters
490
+ urlEncodeFilter(input) {
491
+ return input && typeof input === 'string' ? encodeURIComponent(input) : input;
492
+ }
493
+
494
+ urlDecodeFilter(input) {
495
+ return input && typeof input === 'string' ? decodeURIComponent(input) : input;
496
+ }
497
+
498
+ // HTML filters
499
+ escapeFilter(input) {
500
+ if (!input || typeof input !== 'string') return input;
501
+ return input
502
+ .replace(/&/g, '&amp;')
503
+ .replace(/</g, '&lt;')
504
+ .replace(/>/g, '&gt;')
505
+ .replace(/"/g, '&quot;')
506
+ .replace(/'/g, '&#39;');
507
+ }
508
+
509
+ stripHtmlFilter(input) {
510
+ if (!input || typeof input !== 'string') return input;
511
+ return input.replace(/<[^>]*>/g, '');
512
+ }
513
+
514
+ stripNewlinesFilter(input) {
515
+ if (!input || typeof input !== 'string') return input;
516
+ return input.replace(/\n/g, ' ').replace(/\r/g, '');
517
+ }
518
+
519
+ // Image filters
520
+ imgUrlFilter(input, size = 'original') {
521
+ if (!input) return '';
522
+ // In a real implementation, this would generate proper image URLs
523
+ return input;
524
+ }
525
+
526
+ imgTagFilter(input, alt = '', attributes = '') {
527
+ if (!input) return '';
528
+ return `<img src="${input}" alt="${alt}" ${attributes}>`;
529
+ }
530
+
531
+ // Product filters
532
+ productUrlFilter(product) {
533
+ if (!product) return '#';
534
+ // Use handle, slug, or id - ensure it's a string and handle URL encoding
535
+ const identifier = product.handle || product.slug || product.id;
536
+ if (!identifier) return '#';
537
+ // Ensure it's a string and remove any leading slashes
538
+ const handle = String(identifier).replace(/^\//, '');
539
+ return `/products/${handle}`;
540
+ }
541
+
542
+ collectionUrlFilter(collection) {
543
+ if (!collection) return '#';
544
+ return `/collections/${collection.slug || collection.handle || collection.id}`;
545
+ }
546
+
547
+ pageUrlFilter(page) {
548
+ if (!page) return '#';
549
+ return `/pages/${page.slug || page.handle || page.id}`;
550
+ }
551
+
552
+ // Utility filters
553
+ pluralizeFilter(input, singular, plural) {
554
+ const count = parseInt(input);
555
+ if (isNaN(count)) return input;
556
+ return count === 1 ? singular : plural;
557
+ }
558
+
559
+ handleizeFilter(input) {
560
+ if (!input || typeof input !== 'string') return input;
561
+ return input
562
+ .toLowerCase()
563
+ .replace(/[^a-z0-9\s-]/g, '')
564
+ .replace(/\s+/g, '-')
565
+ .replace(/-+/g, '-')
566
+ .replace(/^-|-$/g, '');
567
+ }
568
+
569
+ highlightFilter(input, search, className = 'highlight') {
570
+ if (!input || !search || typeof input !== 'string') return input;
571
+ const regex = new RegExp(`(${search})`, 'gi');
572
+ return input.replace(regex, `<span class="${className}">$1</span>`);
573
+ }
574
+
575
+ truncateWordsFilter(input, words = 15, truncateString = '...') {
576
+ if (!input || typeof input !== 'string') return input;
577
+ const wordArray = input.split(' ');
578
+ if (wordArray.length <= words) return input;
579
+ return wordArray.slice(0, words).join(' ') + truncateString;
580
+ }
581
+
582
+ newlineToBrFilter(input) {
583
+ if (!input || typeof input !== 'string') return input;
584
+ return input.replace(/\n/g, '<br>');
585
+ }
586
+
587
+ /**
588
+ * Asset URL filter - removes .min extensions and ensures /assets/ prefix
589
+ * @param {string} input - Asset URL path
590
+ * @returns {string} Asset URL with .min removed and /assets/ prefix
591
+ */
592
+ assetUrlFilter(input) {
593
+ if (!input || typeof input !== 'string') return input;
594
+
595
+ // Remove .min extension if present
596
+ let url = input.replace(/\.min\.(css|js|jpg|jpeg|png|gif|svg|webp)$/, (match, ext) => `.${ext}`);
597
+
598
+ // Remove /themes/default/ or /themes/theme-name/ prefixes if present
599
+ // Handle both "/themes/default/assets/logo.png" and "themes/default/assets/logo.png"
600
+ url = url.replace(/^\/?themes\/[^\/]+\//, '/');
601
+
602
+ // Extract just the filename if path contains assets
603
+ // Handle paths like "themes/default/assets/logo.png" -> "/assets/logo.png"
604
+ // or "/themes/default/assets/logo.png" -> "/assets/logo.png"
605
+ const assetsMatch = url.match(/assets\/(.+)$/);
606
+ if (assetsMatch) {
607
+ url = '/assets/' + assetsMatch[1];
608
+ } else if (!url.startsWith('/assets/') && !url.startsWith('http://') && !url.startsWith('https://') && !url.startsWith('data:')) {
609
+ // Ensure /assets/ prefix if not already present (skip if already absolute URL or data URI)
610
+ url = '/assets/' + url.replace(/^\/+/, '');
611
+ }
612
+
613
+ return url;
614
+ }
615
+
616
+ /**
617
+ * Stylesheet tag filter - generates <link> tag for CSS
618
+ * @param {string} input - CSS file path
619
+ * @returns {string} HTML <link> tag
620
+ */
621
+ stylesheetTagFilter(input) {
622
+ if (!input || typeof input !== 'string') return '';
623
+
624
+ // Remove .min extension if present
625
+ let url = input.replace(/\.min\.css$/, '.css');
626
+
627
+ // Remove /themes/default/ or /themes/theme-name/ prefixes if present
628
+ // Handle both "/themes/default/assets/theme.css" and "themes/default/assets/theme.css"
629
+ url = url.replace(/^\/?themes\/[^\/]+\//, '/');
630
+
631
+ // Extract just the filename if path contains assets
632
+ const assetsMatch = url.match(/assets\/(.+)$/);
633
+ if (assetsMatch) {
634
+ url = '/assets/' + assetsMatch[1];
635
+ } else if (!url.startsWith('/assets/') && !url.startsWith('http://') && !url.startsWith('https://') && !url.startsWith('data:')) {
636
+ // Ensure /assets/ prefix if not already present
637
+ url = '/assets/' + url.replace(/^\/+/, '');
638
+ }
639
+
640
+ return `<link rel="stylesheet" href="${url}">`;
641
+ }
642
+
643
+ /**
644
+ * Script tag filter - generates <script> tag for JS
645
+ * @param {string} input - JS file path
646
+ * @returns {string} HTML <script> tag
647
+ */
648
+ scriptTagFilter(input) {
649
+ if (!input || typeof input !== 'string') return '';
650
+
651
+ // Remove .min extension if present
652
+ let url = input.replace(/\.min\.js$/, '.js');
653
+
654
+ // Remove /themes/default/ or /themes/theme-name/ prefixes if present
655
+ // Handle both "/themes/default/assets/theme.js" and "themes/default/assets/theme.js"
656
+ url = url.replace(/^\/?themes\/[^\/]+\//, '/');
657
+
658
+ // Extract just the filename if path contains assets
659
+ const assetsMatch = url.match(/assets\/(.+)$/);
660
+ if (assetsMatch) {
661
+ url = '/assets/' + assetsMatch[1];
662
+ } else if (!url.startsWith('/assets/') && !url.startsWith('http://') && !url.startsWith('https://') && !url.startsWith('data:')) {
663
+ // Ensure /assets/ prefix if not already present
664
+ url = '/assets/' + url.replace(/^\/+/, '');
665
+ }
666
+
667
+ return `<script src="${url}"></script>`;
668
+ }
669
+ }
670
+
671
+ module.exports = LiquidHelperService;