@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,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, '&')
|
|
503
|
+
.replace(/</g, '<')
|
|
504
|
+
.replace(/>/g, '>')
|
|
505
|
+
.replace(/"/g, '"')
|
|
506
|
+
.replace(/'/g, ''');
|
|
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;
|