@o2vend/theme-cli 1.0.0

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.

Potentially problematic release.


This version of @o2vend/theme-cli might be problematic. Click here for more details.

@@ -0,0 +1,589 @@
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
+ }
99
+
100
+ /**
101
+ * Get all available filters
102
+ * @returns {Object} Filters object
103
+ */
104
+ getFilters() {
105
+ return this.filters;
106
+ }
107
+
108
+ // Date filters
109
+ dateFilter(input, format) {
110
+ if (!input) return '';
111
+
112
+ let date;
113
+ if (typeof input === 'string') {
114
+ const normalizedInput = input.trim();
115
+ const lowerInput = normalizedInput.toLowerCase();
116
+
117
+ if (lowerInput === 'now' || lowerInput === 'today') {
118
+ date = moment();
119
+ } else if (normalizedInput.match(/^\d{4}-\d{2}-\d{2}t\d{2}:\d{2}:\d{2}/i)) {
120
+ date = moment(normalizedInput);
121
+ } else if (normalizedInput.match(/^\d{4}-\d{2}-\d{2}/)) {
122
+ date = moment(normalizedInput, 'YYYY-MM-DD');
123
+ } else {
124
+ if (normalizedInput.match(/[©a-zA-Z]/) && !normalizedInput.match(/^\d{4}/)) {
125
+ if (format && (format.includes('%Y') || format.includes('YYYY'))) {
126
+ return moment().format('YYYY');
127
+ }
128
+ return input;
129
+ }
130
+
131
+ date = moment(normalizedInput, moment.ISO_8601, true);
132
+ if (!date.isValid()) {
133
+ try {
134
+ date = moment(normalizedInput);
135
+ } catch (e) {
136
+ if (format && (format.includes('%Y') || format.includes('YYYY'))) {
137
+ return moment().format('YYYY');
138
+ }
139
+ return input;
140
+ }
141
+ }
142
+ }
143
+ } else if (input instanceof Date) {
144
+ date = moment(input);
145
+ } else if (typeof input === 'number') {
146
+ date = moment.unix(input);
147
+ } else {
148
+ try {
149
+ date = moment(input);
150
+ } catch (e) {
151
+ if (format && (format.includes('%Y') || format.includes('YYYY'))) {
152
+ return moment().format('YYYY');
153
+ }
154
+ return input;
155
+ }
156
+ }
157
+
158
+ if (!date || !date.isValid()) {
159
+ if (format && (format.includes('%Y') || format.includes('YYYY'))) {
160
+ return moment().format('YYYY');
161
+ }
162
+ return input;
163
+ }
164
+
165
+ const momentFormat = (format || 'YYYY-MM-DD')
166
+ .replace(/%Y/g, 'YYYY')
167
+ .replace(/%m/g, 'MM')
168
+ .replace(/%d/g, 'DD')
169
+ .replace(/%H/g, 'HH')
170
+ .replace(/%M/g, 'mm')
171
+ .replace(/%S/g, 'ss')
172
+ .replace(/%y/g, 'YY');
173
+
174
+ return date.format(momentFormat);
175
+ }
176
+
177
+ timeAgoFilter(input) {
178
+ if (!input) return '';
179
+
180
+ let date;
181
+ if (typeof input === 'string') {
182
+ const normalizedInput = input.trim().toLowerCase();
183
+ if (normalizedInput === 'now' || normalizedInput === 'today') {
184
+ date = moment();
185
+ } else if (normalizedInput.match(/^\d{4}-\d{2}-\d{2}t\d{2}:\d{2}:\d{2}/)) {
186
+ date = moment(normalizedInput);
187
+ } else if (normalizedInput.match(/^\d{4}-\d{2}-\d{2}/)) {
188
+ date = moment(normalizedInput, 'YYYY-MM-DD');
189
+ } else {
190
+ date = moment(normalizedInput, moment.ISO_8601, true);
191
+ if (!date.isValid()) {
192
+ date = moment(normalizedInput);
193
+ }
194
+ }
195
+ } else if (input instanceof Date) {
196
+ date = moment(input);
197
+ } else {
198
+ date = moment(input);
199
+ }
200
+
201
+ return date.isValid() ? date.fromNow() : input;
202
+ }
203
+
204
+ timeUntilFilter(input) {
205
+ if (!input) return '';
206
+
207
+ let date;
208
+ if (typeof input === 'string') {
209
+ const normalizedInput = input.trim().toLowerCase();
210
+ if (normalizedInput === 'now' || normalizedInput === 'today') {
211
+ date = moment();
212
+ } else if (normalizedInput.match(/^\d{4}-\d{2}-\d{2}t\d{2}:\d{2}:\d{2}/)) {
213
+ date = moment(normalizedInput);
214
+ } else if (normalizedInput.match(/^\d{4}-\d{2}-\d{2}/)) {
215
+ date = moment(normalizedInput, 'YYYY-MM-DD');
216
+ } else {
217
+ date = moment(normalizedInput, moment.ISO_8601, true);
218
+ if (!date.isValid()) {
219
+ date = moment(normalizedInput);
220
+ }
221
+ }
222
+ } else if (input instanceof Date) {
223
+ date = moment(input);
224
+ } else {
225
+ date = moment(input);
226
+ }
227
+
228
+ return date.isValid() ? date.toNow() : input;
229
+ }
230
+
231
+ // String filters
232
+ truncateFilter(input, length = 50, truncateString = '...') {
233
+ if (!input || typeof input !== 'string') return input;
234
+ return input.length > length ?
235
+ input.substring(0, length) + truncateString :
236
+ input;
237
+ }
238
+
239
+ capitalizeFilter(input) {
240
+ if (!input || typeof input !== 'string') return input;
241
+ return input.charAt(0).toUpperCase() + input.slice(1).toLowerCase();
242
+ }
243
+
244
+ upcaseFilter(input) {
245
+ return input && typeof input === 'string' ? input.toUpperCase() : input;
246
+ }
247
+
248
+ downcaseFilter(input) {
249
+ return input && typeof input === 'string' ? input.toLowerCase() : input;
250
+ }
251
+
252
+ stripFilter(input) {
253
+ return input && typeof input === 'string' ? input.trim() : input;
254
+ }
255
+
256
+ lstripFilter(input) {
257
+ return input && typeof input === 'string' ? input.replace(/^\s+/, '') : input;
258
+ }
259
+
260
+ rstripFilter(input) {
261
+ return input && typeof input === 'string' ? input.replace(/\s+$/, '') : input;
262
+ }
263
+
264
+ splitFilter(input, delimiter) {
265
+ if (!input || typeof input !== 'string') return [];
266
+ return input.split(delimiter || ' ');
267
+ }
268
+
269
+ joinFilter(input, delimiter) {
270
+ if (!Array.isArray(input)) return input;
271
+ return input.join(delimiter || ' ');
272
+ }
273
+
274
+ replaceFilter(input, search, replace) {
275
+ if (!input || typeof input !== 'string') return input;
276
+ return input.replace(new RegExp(search, 'g'), replace);
277
+ }
278
+
279
+ removeFilter(input, search) {
280
+ if (!input || typeof input !== 'string') return input;
281
+ return input.replace(new RegExp(search, 'g'), '');
282
+ }
283
+
284
+ prependFilter(input, prepend) {
285
+ if (!input) return prepend || '';
286
+ return (prepend || '') + input;
287
+ }
288
+
289
+ appendFilter(input, append) {
290
+ if (!input) return append || '';
291
+ return input + (append || '');
292
+ }
293
+
294
+ // Number filters
295
+ plusFilter(input, number) {
296
+ const num = parseFloat(input);
297
+ const add = parseFloat(number);
298
+ return isNaN(num) || isNaN(add) ? input : num + add;
299
+ }
300
+
301
+ minusFilter(input, number) {
302
+ const num = parseFloat(input);
303
+ const sub = parseFloat(number);
304
+ return isNaN(num) || isNaN(sub) ? input : num - sub;
305
+ }
306
+
307
+ timesFilter(input, number) {
308
+ const num = parseFloat(input);
309
+ const mult = parseFloat(number);
310
+ return isNaN(num) || isNaN(mult) ? input : num * mult;
311
+ }
312
+
313
+ dividedByFilter(input, number) {
314
+ const num = parseFloat(input);
315
+ const div = parseFloat(number);
316
+ return isNaN(num) || isNaN(div) || div === 0 ? input : num / div;
317
+ }
318
+
319
+ moduloFilter(input, number) {
320
+ const num = parseFloat(input);
321
+ const mod = parseFloat(number);
322
+ return isNaN(num) || isNaN(mod) || mod === 0 ? input : num % mod;
323
+ }
324
+
325
+ roundFilter(input, decimals = 0) {
326
+ const num = parseFloat(input);
327
+ return isNaN(num) ? input : Math.round(num * Math.pow(10, decimals)) / Math.pow(10, decimals);
328
+ }
329
+
330
+ ceilFilter(input) {
331
+ const num = parseFloat(input);
332
+ return isNaN(num) ? input : Math.ceil(num);
333
+ }
334
+
335
+ floorFilter(input) {
336
+ const num = parseFloat(input);
337
+ return isNaN(num) ? input : Math.floor(num);
338
+ }
339
+
340
+ absFilter(input) {
341
+ const num = parseFloat(input);
342
+ return isNaN(num) ? input : Math.abs(num);
343
+ }
344
+
345
+ // Array filters
346
+ firstFilter(input) {
347
+ return Array.isArray(input) && input.length > 0 ? input[0] : input;
348
+ }
349
+
350
+ lastFilter(input) {
351
+ return Array.isArray(input) && input.length > 0 ? input[input.length - 1] : input;
352
+ }
353
+
354
+ sortFilter(input, property) {
355
+ if (!Array.isArray(input)) return input;
356
+ if (property) {
357
+ return input.sort((a, b) => {
358
+ const aVal = _.get(a, property);
359
+ const bVal = _.get(b, property);
360
+ return aVal > bVal ? 1 : aVal < bVal ? -1 : 0;
361
+ });
362
+ }
363
+ return input.sort();
364
+ }
365
+
366
+ reverseFilter(input) {
367
+ return Array.isArray(input) ? input.slice().reverse() : input;
368
+ }
369
+
370
+ uniqFilter(input, property) {
371
+ if (!Array.isArray(input)) return input;
372
+ if (property) {
373
+ const seen = new Set();
374
+ return input.filter(item => {
375
+ const val = _.get(item, property);
376
+ if (seen.has(val)) return false;
377
+ seen.add(val);
378
+ return true;
379
+ });
380
+ }
381
+ return [...new Set(input)];
382
+ }
383
+
384
+ mapFilter(input, property) {
385
+ if (!Array.isArray(input)) return input;
386
+ return input.map(item => _.get(item, property));
387
+ }
388
+
389
+ whereFilter(input, property, value) {
390
+ if (!Array.isArray(input)) return input;
391
+ return input.filter(item => _.get(item, property) === value);
392
+ }
393
+
394
+ sizeFilter(input) {
395
+ if (Array.isArray(input)) return input.length;
396
+ if (typeof input === 'string') return input.length;
397
+ if (typeof input === 'object' && input !== null) return Object.keys(input).length;
398
+ return 0;
399
+ }
400
+
401
+ sliceFilter(input, start, length) {
402
+ if (!Array.isArray(input)) return input;
403
+ return input.slice(start, start + length);
404
+ }
405
+
406
+ // Object filters
407
+ defaultFilter(input, defaultValue) {
408
+ return input || defaultValue;
409
+ }
410
+
411
+ jsonFilter(input) {
412
+ try {
413
+ return JSON.stringify(input);
414
+ } catch (error) {
415
+ return input;
416
+ }
417
+ }
418
+
419
+ // Money filters
420
+ moneyFilter(input, currency = 'USD') {
421
+ const num = parseFloat(input);
422
+ if (isNaN(num)) return input;
423
+
424
+ const formatter = new Intl.NumberFormat('en-US', {
425
+ style: 'currency',
426
+ currency: currency
427
+ });
428
+
429
+ return formatter.format(num);
430
+ }
431
+
432
+ moneyWithCurrencyFilter(input, currency = 'USD') {
433
+ return this.moneyFilter(input, currency);
434
+ }
435
+
436
+ moneyWithSettingsFilter(input, settings = {}) {
437
+ const num = parseFloat(input);
438
+ if (isNaN(num)) return input;
439
+
440
+ const {
441
+ currencySymbol = '₹',
442
+ currencyFormat = '#,##0.00',
443
+ currencyDecimalDigits = 2,
444
+ currencyGroupSeparator = ',',
445
+ currencyDecimalSeparator = '.',
446
+ currencyGroupSizes = [3]
447
+ } = settings;
448
+
449
+ const formatNumberWithSettings = (num, settings) => {
450
+ const {
451
+ decimalDigits = 2,
452
+ groupSeparator = ',',
453
+ decimalSeparator = '.',
454
+ groupSizes = [3]
455
+ } = settings;
456
+
457
+ let formatted = num.toFixed(decimalDigits);
458
+ const parts = formatted.split('.');
459
+ let integerPart = parts[0];
460
+ const decimalPart = parts[1];
461
+
462
+ if (groupSizes.length > 0) {
463
+ const groupSize = groupSizes[0];
464
+ const regex = new RegExp(`\\B(?=(\\d{${groupSize}})+(?!\\d))`, 'g');
465
+ integerPart = integerPart.replace(regex, groupSeparator);
466
+ }
467
+
468
+ if (decimalPart && decimalDigits > 0) {
469
+ return `${integerPart}${decimalSeparator}${decimalPart}`;
470
+ }
471
+
472
+ return integerPart;
473
+ };
474
+
475
+ const formatted = formatNumberWithSettings(num, {
476
+ decimalDigits: currencyDecimalDigits,
477
+ groupSeparator: currencyGroupSeparator,
478
+ decimalSeparator: currencyDecimalSeparator,
479
+ groupSizes: currencyGroupSizes
480
+ });
481
+
482
+ return `${currencySymbol}${formatted}`;
483
+ }
484
+
485
+ // URL filters
486
+ urlEncodeFilter(input) {
487
+ return input && typeof input === 'string' ? encodeURIComponent(input) : input;
488
+ }
489
+
490
+ urlDecodeFilter(input) {
491
+ return input && typeof input === 'string' ? decodeURIComponent(input) : input;
492
+ }
493
+
494
+ // HTML filters
495
+ escapeFilter(input) {
496
+ if (!input || typeof input !== 'string') return input;
497
+ return input
498
+ .replace(/&/g, '&amp;')
499
+ .replace(/</g, '&lt;')
500
+ .replace(/>/g, '&gt;')
501
+ .replace(/"/g, '&quot;')
502
+ .replace(/'/g, '&#39;');
503
+ }
504
+
505
+ stripHtmlFilter(input) {
506
+ if (!input || typeof input !== 'string') return input;
507
+ return input.replace(/<[^>]*>/g, '');
508
+ }
509
+
510
+ stripNewlinesFilter(input) {
511
+ if (!input || typeof input !== 'string') return input;
512
+ return input.replace(/\n/g, ' ').replace(/\r/g, '');
513
+ }
514
+
515
+ // Image filters
516
+ imgUrlFilter(input, size = 'original') {
517
+ if (!input) return '';
518
+ // In a real implementation, this would generate proper image URLs
519
+ return input;
520
+ }
521
+
522
+ imgTagFilter(input, alt = '', attributes = '') {
523
+ if (!input) return '';
524
+ return `<img src="${input}" alt="${alt}" ${attributes}>`;
525
+ }
526
+
527
+ // Product filters
528
+ productUrlFilter(product) {
529
+ if (!product) return '#';
530
+ return `/products/${product.slug || product.handle || product.id}`;
531
+ }
532
+
533
+ collectionUrlFilter(collection) {
534
+ if (!collection) return '#';
535
+ return `/collections/${collection.slug || collection.handle || collection.id}`;
536
+ }
537
+
538
+ pageUrlFilter(page) {
539
+ if (!page) return '#';
540
+ return `/pages/${page.slug || page.handle || page.id}`;
541
+ }
542
+
543
+ // Utility filters
544
+ pluralizeFilter(input, singular, plural) {
545
+ const count = parseInt(input);
546
+ if (isNaN(count)) return input;
547
+ return count === 1 ? singular : plural;
548
+ }
549
+
550
+ handleizeFilter(input) {
551
+ if (!input || typeof input !== 'string') return input;
552
+ return input
553
+ .toLowerCase()
554
+ .replace(/[^a-z0-9\s-]/g, '')
555
+ .replace(/\s+/g, '-')
556
+ .replace(/-+/g, '-')
557
+ .replace(/^-|-$/g, '');
558
+ }
559
+
560
+ highlightFilter(input, search, className = 'highlight') {
561
+ if (!input || !search || typeof input !== 'string') return input;
562
+ const regex = new RegExp(`(${search})`, 'gi');
563
+ return input.replace(regex, `<span class="${className}">$1</span>`);
564
+ }
565
+
566
+ truncateWordsFilter(input, words = 15, truncateString = '...') {
567
+ if (!input || typeof input !== 'string') return input;
568
+ const wordArray = input.split(' ');
569
+ if (wordArray.length <= words) return input;
570
+ return wordArray.slice(0, words).join(' ') + truncateString;
571
+ }
572
+
573
+ newlineToBrFilter(input) {
574
+ if (!input || typeof input !== 'string') return input;
575
+ return input.replace(/\n/g, '<br>');
576
+ }
577
+
578
+ /**
579
+ * Asset URL filter - returns asset URL as-is
580
+ * @param {string} input - Asset URL path
581
+ * @returns {string} Asset URL (unchanged)
582
+ */
583
+ assetUrlFilter(input) {
584
+ if (!input || typeof input !== 'string') return input;
585
+ return input;
586
+ }
587
+ }
588
+
589
+ module.exports = LiquidHelperService;