ebag 0.0.2
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 +89 -0
- package/dist/cli/format.d.ts +12 -0
- package/dist/cli/format.js +354 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +447 -0
- package/dist/lib/auth.d.ts +3 -0
- package/dist/lib/auth.js +17 -0
- package/dist/lib/cart.d.ts +4 -0
- package/dist/lib/cart.js +46 -0
- package/dist/lib/client.d.ts +13 -0
- package/dist/lib/client.js +96 -0
- package/dist/lib/config.d.ts +11 -0
- package/dist/lib/config.js +90 -0
- package/dist/lib/cookies.d.ts +2 -0
- package/dist/lib/cookies.js +30 -0
- package/dist/lib/index.d.ts +9 -0
- package/dist/lib/index.js +25 -0
- package/dist/lib/lists.d.ts +4 -0
- package/dist/lib/lists.js +41 -0
- package/dist/lib/orders.d.ts +14 -0
- package/dist/lib/orders.js +266 -0
- package/dist/lib/products.d.ts +2 -0
- package/dist/lib/products.js +8 -0
- package/dist/lib/search.d.ts +7 -0
- package/dist/lib/search.js +181 -0
- package/dist/lib/slots.d.ts +6 -0
- package/dist/lib/slots.js +55 -0
- package/dist/lib/types.d.ts +115 -0
- package/dist/lib/types.js +2 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# ebag
|
|
2
|
+
|
|
3
|
+
CLI for interacting with ebag.bg (unofficial).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g ebag
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or run without installing:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx ebag --help
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Login
|
|
18
|
+
|
|
19
|
+
Get the cookie header from your browser and store it:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
ebag login --cookie "<cookie>"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Notes:
|
|
26
|
+
- Session data is stored in plain text at `~/.config/ebag/session.json` with owner-only permissions (`0600`).
|
|
27
|
+
- List-based search caches product details in `~/.config/ebag/cache.json` for up to 6 hours.
|
|
28
|
+
- Completed orders may be cached locally for faster access.
|
|
29
|
+
|
|
30
|
+
## Search
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
ebag search "lindt"
|
|
34
|
+
ebag search "lindt" --json
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Delivery slots
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
ebag slots
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Orders
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
ebag order list
|
|
47
|
+
ebag order list --from 2024-02-01 --to 2024-03-31
|
|
48
|
+
ebag order show DE9CD146FECD05BF
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Cart
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
ebag cart add 5128 --qty 1
|
|
55
|
+
ebag cart update 5128 --qty 2
|
|
56
|
+
ebag cart show
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Lists
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
ebag list show
|
|
63
|
+
ebag list add 753250 5128 --qty 1
|
|
64
|
+
ebag list show 753250
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Contributing
|
|
68
|
+
|
|
69
|
+
### Build
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npm run build
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### End-to-end tests
|
|
76
|
+
|
|
77
|
+
By default, `npm run test:e2e` reads cookies from `tests/.secrets/ebag-cookies` (gitignored).
|
|
78
|
+
You can also override with `EBAG_COOKIE`.
|
|
79
|
+
|
|
80
|
+
Optional query override:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
EBAG_COOKIE="<cookie>" EBAG_TEST_QUERY="lindt" npm run test:e2e
|
|
84
|
+
```
|
|
85
|
+
Store test cookies in `tests/.secrets/ebag-cookies` (gitignored), then run:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npm run test:e2e
|
|
89
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { OrderDetail, OrderSummary, ProductSummary } from '../lib/types';
|
|
2
|
+
export declare function outputJson(data: unknown): void;
|
|
3
|
+
export declare function formatHeading(text: string): string;
|
|
4
|
+
export declare function outputProducts(products: ProductSummary[]): void;
|
|
5
|
+
export declare function outputList(items: {
|
|
6
|
+
id: number;
|
|
7
|
+
name: string;
|
|
8
|
+
count?: number;
|
|
9
|
+
}[]): void;
|
|
10
|
+
export declare function outputProductDetail(data: Record<string, unknown>): void;
|
|
11
|
+
export declare function outputOrdersList(orders: OrderSummary[]): void;
|
|
12
|
+
export declare function outputOrderDetail(detail: OrderDetail): void;
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.outputJson = outputJson;
|
|
4
|
+
exports.formatHeading = formatHeading;
|
|
5
|
+
exports.outputProducts = outputProducts;
|
|
6
|
+
exports.outputList = outputList;
|
|
7
|
+
exports.outputProductDetail = outputProductDetail;
|
|
8
|
+
exports.outputOrdersList = outputOrdersList;
|
|
9
|
+
exports.outputOrderDetail = outputOrderDetail;
|
|
10
|
+
function outputJson(data) {
|
|
11
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
12
|
+
}
|
|
13
|
+
function formatHeading(text) {
|
|
14
|
+
if (process.stdout.isTTY && !process.env.NO_COLOR) {
|
|
15
|
+
return `\u001b[1m${text}\u001b[0m`;
|
|
16
|
+
}
|
|
17
|
+
return text;
|
|
18
|
+
}
|
|
19
|
+
function formatPrice(product) {
|
|
20
|
+
if (product.currentPrice)
|
|
21
|
+
return product.currentPrice;
|
|
22
|
+
if (product.pricePromo)
|
|
23
|
+
return product.pricePromo;
|
|
24
|
+
if (product.price)
|
|
25
|
+
return product.price;
|
|
26
|
+
return '';
|
|
27
|
+
}
|
|
28
|
+
function outputProducts(products) {
|
|
29
|
+
for (const product of products) {
|
|
30
|
+
const price = formatPrice(product);
|
|
31
|
+
let source = '';
|
|
32
|
+
if (product.source === 'list') {
|
|
33
|
+
const listNames = (product.listNames || []).filter(Boolean);
|
|
34
|
+
if (listNames.length) {
|
|
35
|
+
source = ` [${listNames.join(', ')}]`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const line = `${product.id} ${product.name}${price ? ` - ${price}` : ''}${source}`;
|
|
39
|
+
process.stdout.write(`${line}\n`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function outputList(items) {
|
|
43
|
+
for (const item of items) {
|
|
44
|
+
const count = item.count !== undefined ? ` (${item.count})` : '';
|
|
45
|
+
process.stdout.write(`${item.id} ${item.name}${count}\n`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function outputProductDetail(data) {
|
|
49
|
+
const id = data.id ? String(data.id) : '';
|
|
50
|
+
const name = data.name ? String(data.name) : '';
|
|
51
|
+
const nameEn = data.name_en ? String(data.name_en) : '';
|
|
52
|
+
const brand = typeof data.brand === 'string'
|
|
53
|
+
? data.brand
|
|
54
|
+
: data.brand?.name ||
|
|
55
|
+
data.brand
|
|
56
|
+
?.name_bg ||
|
|
57
|
+
data.brand
|
|
58
|
+
?.name_en ||
|
|
59
|
+
'';
|
|
60
|
+
const unit = data.unit_weight_text ? String(data.unit_weight_text) : '';
|
|
61
|
+
const price = data.current_price_eur ?? data.price_promo_eur ?? data.price_eur;
|
|
62
|
+
const priceText = price ? String(price) : '';
|
|
63
|
+
const currency = priceText ? 'EUR' : '';
|
|
64
|
+
const availability = data.is_available === false ? 'No' : data.is_available === true ? 'Yes' : '';
|
|
65
|
+
const urlSlug = data.url_slug ? String(data.url_slug) : '';
|
|
66
|
+
const origin = data.country_of_origin ? String(data.country_of_origin) : '';
|
|
67
|
+
const expiryRaw = data.expiry_date ? String(data.expiry_date) : '';
|
|
68
|
+
const expiry = formatDate(expiryRaw);
|
|
69
|
+
const description = data.description ? String(data.description) : '';
|
|
70
|
+
const kvPairs = [
|
|
71
|
+
['ID', id],
|
|
72
|
+
['Brand', brand],
|
|
73
|
+
['Unit', unit],
|
|
74
|
+
['Price', priceText ? `${priceText}${currency ? ` ${currency}` : ''}` : ''],
|
|
75
|
+
['Available', availability],
|
|
76
|
+
['Origin', origin],
|
|
77
|
+
['Expiry', expiry],
|
|
78
|
+
['Slug', urlSlug],
|
|
79
|
+
];
|
|
80
|
+
const energyPairs = formatEnergyValues(data.energy_values);
|
|
81
|
+
const kvLines = [
|
|
82
|
+
...kvPairs.filter(([, value]) => value),
|
|
83
|
+
...energyPairs,
|
|
84
|
+
].map(([key, value]) => `${key}: ${value}`);
|
|
85
|
+
const headerLines = [`# ${name || 'Product'}`, nameEn ? `*${nameEn}*` : ''].filter(Boolean);
|
|
86
|
+
const descriptionMd = description ? htmlToMarkdown(description) : '';
|
|
87
|
+
const { descriptionText, ingredientsText } = splitIngredients(descriptionMd);
|
|
88
|
+
const descriptionBlock = descriptionText ? `# Description\n\n${descriptionText}` : '';
|
|
89
|
+
const ingredientsBlock = ingredientsText ? `\n# Ingredients\n\n${ingredientsText}` : '';
|
|
90
|
+
const kvBlock = kvLines.length ? ['---', ...kvLines, '---'].join('\n') : '';
|
|
91
|
+
const output = [headerLines.join('\n'), kvBlock, descriptionBlock, ingredientsBlock]
|
|
92
|
+
.filter(Boolean)
|
|
93
|
+
.join('\n');
|
|
94
|
+
process.stdout.write(`${output}\n`);
|
|
95
|
+
}
|
|
96
|
+
function formatOrderAmount(order) {
|
|
97
|
+
if ('finalAmountEur' in order && order.finalAmountEur) {
|
|
98
|
+
return `${order.finalAmountEur} EUR`;
|
|
99
|
+
}
|
|
100
|
+
if ('finalAmount' in order && order.finalAmount) {
|
|
101
|
+
return order.finalAmount;
|
|
102
|
+
}
|
|
103
|
+
if ('totals' in order) {
|
|
104
|
+
if (order.totals.totalPaidEur)
|
|
105
|
+
return `${order.totals.totalPaidEur} EUR`;
|
|
106
|
+
if (order.totals.totalEur)
|
|
107
|
+
return `${order.totals.totalEur} EUR`;
|
|
108
|
+
if (order.totals.totalPaid)
|
|
109
|
+
return order.totals.totalPaid;
|
|
110
|
+
if (order.totals.total)
|
|
111
|
+
return order.totals.total;
|
|
112
|
+
}
|
|
113
|
+
return '';
|
|
114
|
+
}
|
|
115
|
+
function formatDateInTimeZone(date, timeZone) {
|
|
116
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
117
|
+
timeZone,
|
|
118
|
+
year: 'numeric',
|
|
119
|
+
month: '2-digit',
|
|
120
|
+
day: '2-digit',
|
|
121
|
+
}).formatToParts(date);
|
|
122
|
+
const lookup = new Map(parts.map((part) => [part.type, part.value]));
|
|
123
|
+
const year = lookup.get('year') || '';
|
|
124
|
+
const month = lookup.get('month') || '';
|
|
125
|
+
const day = lookup.get('day') || '';
|
|
126
|
+
return `${year}-${month}-${day}`;
|
|
127
|
+
}
|
|
128
|
+
function formatOrderStatus(order) {
|
|
129
|
+
const status = order.status;
|
|
130
|
+
if (status === 3)
|
|
131
|
+
return 'Отказана';
|
|
132
|
+
if (status === 4) {
|
|
133
|
+
const date = order.shippingDate ? formatDate(order.shippingDate) : '';
|
|
134
|
+
const todayDate = formatDateInTimeZone(new Date(), 'Europe/Sofia');
|
|
135
|
+
if (date && date >= todayDate)
|
|
136
|
+
return 'Нова';
|
|
137
|
+
return 'Завършена';
|
|
138
|
+
}
|
|
139
|
+
if (status !== undefined) {
|
|
140
|
+
return `Status ${status}`;
|
|
141
|
+
}
|
|
142
|
+
return '';
|
|
143
|
+
}
|
|
144
|
+
function outputOrdersList(orders) {
|
|
145
|
+
for (const order of orders) {
|
|
146
|
+
const date = order.shippingDate ? formatDate(order.shippingDate) : '';
|
|
147
|
+
const slot = order.timeSlotDisplay || '';
|
|
148
|
+
const status = formatOrderStatus(order);
|
|
149
|
+
const total = formatOrderAmount(order);
|
|
150
|
+
const parts = [order.id, date, slot].filter(Boolean);
|
|
151
|
+
const suffix = [status, total].filter(Boolean).join(' - ');
|
|
152
|
+
const line = `${parts.join(' ')}${suffix ? ` - ${suffix}` : ''}`;
|
|
153
|
+
process.stdout.write(`${line}\n`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function outputOrderItems(items) {
|
|
157
|
+
const byGroup = new Map();
|
|
158
|
+
const ungrouped = [];
|
|
159
|
+
for (const item of items) {
|
|
160
|
+
if (item.group) {
|
|
161
|
+
const groupItems = byGroup.get(item.group) || [];
|
|
162
|
+
groupItems.push(item);
|
|
163
|
+
byGroup.set(item.group, groupItems);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
ungrouped.push(item);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const groupEntries = [...byGroup.entries()];
|
|
170
|
+
if (groupEntries.length) {
|
|
171
|
+
for (const [group, groupItems] of groupEntries) {
|
|
172
|
+
process.stdout.write(`## ${group}\n`);
|
|
173
|
+
for (const item of groupItems) {
|
|
174
|
+
const qty = item.quantity ? ` x${item.quantity}` : '';
|
|
175
|
+
const unit = item.unit ? ` (${item.unit})` : '';
|
|
176
|
+
const price = item.priceEur ? `${item.priceEur} EUR` : item.price || '';
|
|
177
|
+
const line = `- ${item.name}${unit}${qty}${price ? ` - ${price}` : ''}`;
|
|
178
|
+
process.stdout.write(`${line}\n`);
|
|
179
|
+
}
|
|
180
|
+
process.stdout.write('\n');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (ungrouped.length) {
|
|
184
|
+
for (const item of ungrouped) {
|
|
185
|
+
const qty = item.quantity ? ` x${item.quantity}` : '';
|
|
186
|
+
const unit = item.unit ? ` (${item.unit})` : '';
|
|
187
|
+
const price = item.priceEur ? `${item.priceEur} EUR` : item.price || '';
|
|
188
|
+
const line = `- ${item.name}${unit}${qty}${price ? ` - ${price}` : ''}`;
|
|
189
|
+
process.stdout.write(`${line}\n`);
|
|
190
|
+
}
|
|
191
|
+
process.stdout.write('\n');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function outputOrderDetail(detail) {
|
|
195
|
+
const header = `# Order ${detail.id || ''}`.trim();
|
|
196
|
+
const status = formatOrderStatus(detail);
|
|
197
|
+
const date = detail.shippingDate ? formatDate(detail.shippingDate) : '';
|
|
198
|
+
const address = detail.address || '';
|
|
199
|
+
const total = formatOrderAmount(detail);
|
|
200
|
+
const kvPairs = [
|
|
201
|
+
['ID', detail.id],
|
|
202
|
+
['Status', status],
|
|
203
|
+
['Date', date],
|
|
204
|
+
['Timeslot', detail.timeSlotDisplay || ''],
|
|
205
|
+
['Address', address],
|
|
206
|
+
['Total', total],
|
|
207
|
+
['Discount', detail.totals.discountEur ? `${detail.totals.discountEur} EUR` : detail.totals.discount || ''],
|
|
208
|
+
['Tip', detail.totals.tipEur ? `${detail.totals.tipEur} EUR` : detail.totals.tip || ''],
|
|
209
|
+
].filter(([, value]) => value);
|
|
210
|
+
const kvLines = kvPairs.map(([key, value]) => `${key}: ${value}`);
|
|
211
|
+
const kvBlock = kvLines.length ? ['---', ...kvLines, '---'].join('\n') : '';
|
|
212
|
+
process.stdout.write(`${header}\n`);
|
|
213
|
+
if (kvBlock) {
|
|
214
|
+
process.stdout.write(`${kvBlock}\n`);
|
|
215
|
+
}
|
|
216
|
+
process.stdout.write('# Items\n');
|
|
217
|
+
if (detail.items.length) {
|
|
218
|
+
outputOrderItems(detail.items);
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
process.stdout.write('No items found.\n\n');
|
|
222
|
+
}
|
|
223
|
+
if (detail.additionalOrders && detail.additionalOrders.length) {
|
|
224
|
+
process.stdout.write('# Additional Orders\n');
|
|
225
|
+
for (const additional of detail.additionalOrders) {
|
|
226
|
+
process.stdout.write(`## Order ${additional.id}\n`);
|
|
227
|
+
const additionalTotal = formatOrderAmount(additional);
|
|
228
|
+
if (additionalTotal) {
|
|
229
|
+
process.stdout.write(`Total: ${additionalTotal}\n`);
|
|
230
|
+
}
|
|
231
|
+
if (additional.items.length) {
|
|
232
|
+
outputOrderItems(additional.items);
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
process.stdout.write('No items found.\n\n');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function formatDate(value) {
|
|
241
|
+
if (!value)
|
|
242
|
+
return '';
|
|
243
|
+
const isoMatch = value.match(/(\d{4})-(\d{2})-(\d{2})/);
|
|
244
|
+
if (isoMatch)
|
|
245
|
+
return `${isoMatch[1]}-${isoMatch[2]}-${isoMatch[3]}`;
|
|
246
|
+
const ymdAlt = value.match(/(\d{4})[./](\d{2})[./](\d{2})/);
|
|
247
|
+
if (ymdAlt)
|
|
248
|
+
return `${ymdAlt[1]}-${ymdAlt[2]}-${ymdAlt[3]}`;
|
|
249
|
+
const dmyMatch = value.match(/(\d{2})[./-](\d{2})[./-](\d{4})/);
|
|
250
|
+
if (dmyMatch)
|
|
251
|
+
return `${dmyMatch[3]}-${dmyMatch[2]}-${dmyMatch[1]}`;
|
|
252
|
+
const parsed = new Date(value);
|
|
253
|
+
if (Number.isNaN(parsed.getTime()))
|
|
254
|
+
return value;
|
|
255
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
256
|
+
return `${parsed.getFullYear()}-${pad(parsed.getMonth() + 1)}-${pad(parsed.getDate())}`;
|
|
257
|
+
}
|
|
258
|
+
function formatEnergyValues(value) {
|
|
259
|
+
if (!value)
|
|
260
|
+
return [];
|
|
261
|
+
if (Array.isArray(value)) {
|
|
262
|
+
const pairs = [];
|
|
263
|
+
for (const item of value) {
|
|
264
|
+
if (!item || typeof item !== 'object')
|
|
265
|
+
continue;
|
|
266
|
+
const entry = item;
|
|
267
|
+
const label = entry.name_bg ||
|
|
268
|
+
entry.name_en ||
|
|
269
|
+
entry.name;
|
|
270
|
+
const amount = entry.value ??
|
|
271
|
+
entry.amount ??
|
|
272
|
+
entry.quantity ??
|
|
273
|
+
entry.value_bg ??
|
|
274
|
+
entry.value_en ??
|
|
275
|
+
entry.energy;
|
|
276
|
+
if (!label || amount === undefined || amount === null)
|
|
277
|
+
continue;
|
|
278
|
+
pairs.push(...splitEnergyValues(`Energy ${label}`, String(amount)));
|
|
279
|
+
}
|
|
280
|
+
return pairs;
|
|
281
|
+
}
|
|
282
|
+
if (typeof value === 'object') {
|
|
283
|
+
return Object.entries(value)
|
|
284
|
+
.filter(([, entry]) => entry !== null && entry !== undefined)
|
|
285
|
+
.flatMap(([key, entry]) => splitEnergyValues(`Energy ${key}`, String(entry)));
|
|
286
|
+
}
|
|
287
|
+
return [];
|
|
288
|
+
}
|
|
289
|
+
function splitEnergyValues(label, raw) {
|
|
290
|
+
const normalized = normalizeNumberString(raw);
|
|
291
|
+
const kcalMatch = normalized.match(/(\d+(?:\.\d+)?)\s*kcal/i);
|
|
292
|
+
const kjMatch = normalized.match(/(\d+(?:\.\d+)?)\s*kJ/i);
|
|
293
|
+
const hasKcalKjLabel = /kcal/i.test(label) && /kJ/i.test(label);
|
|
294
|
+
const splitMatch = normalized.match(/^(\d+(?:\.\d+)?)\/(\d+(?:\.\d+)?)$/);
|
|
295
|
+
const baseLabel = label.replace(/\(?\s*kcal\s*\/\s*kJ\s*\)?/i, '').trim();
|
|
296
|
+
const energyLabel = baseLabel || label;
|
|
297
|
+
if (kcalMatch || kjMatch) {
|
|
298
|
+
const items = [];
|
|
299
|
+
if (kcalMatch)
|
|
300
|
+
items.push([`${energyLabel} kcal`, kcalMatch[1]]);
|
|
301
|
+
if (kjMatch)
|
|
302
|
+
items.push([`${energyLabel} kJ`, kjMatch[1]]);
|
|
303
|
+
return items;
|
|
304
|
+
}
|
|
305
|
+
if (splitMatch && hasKcalKjLabel) {
|
|
306
|
+
return [
|
|
307
|
+
[`${energyLabel} kcal`, splitMatch[1]],
|
|
308
|
+
[`${energyLabel} kJ`, splitMatch[2]],
|
|
309
|
+
];
|
|
310
|
+
}
|
|
311
|
+
return [[label, normalized]];
|
|
312
|
+
}
|
|
313
|
+
function normalizeNumberString(value) {
|
|
314
|
+
if (typeof value === 'number')
|
|
315
|
+
return value.toString();
|
|
316
|
+
if (typeof value !== 'string')
|
|
317
|
+
return String(value);
|
|
318
|
+
const trimmed = value.trim().replace(/\u00a0/g, ' ');
|
|
319
|
+
const noSpaces = trimmed.replace(/\s+/g, '');
|
|
320
|
+
if (noSpaces.includes(',')) {
|
|
321
|
+
return noSpaces.replace(',', '.');
|
|
322
|
+
}
|
|
323
|
+
return noSpaces;
|
|
324
|
+
}
|
|
325
|
+
function htmlToMarkdown(html) {
|
|
326
|
+
let text = html;
|
|
327
|
+
text = text.replace(/<\s*br\s*\/?\s*>/gi, '\n');
|
|
328
|
+
text = text.replace(/<\s*\/p\s*>/gi, '\n\n');
|
|
329
|
+
text = text.replace(/<\s*p[^>]*>/gi, '');
|
|
330
|
+
text = text.replace(/<\s*li[^>]*>/gi, '- ');
|
|
331
|
+
text = text.replace(/<\s*\/li\s*>/gi, '\n');
|
|
332
|
+
text = text.replace(/<\s*ul[^>]*>/gi, '\n');
|
|
333
|
+
text = text.replace(/<\s*\/ul\s*>/gi, '\n');
|
|
334
|
+
text = text.replace(/<[^>]+>/g, '');
|
|
335
|
+
text = text.replace(/ /g, ' ');
|
|
336
|
+
text = text.replace(/&/g, '&');
|
|
337
|
+
text = text.replace(/</g, '<');
|
|
338
|
+
text = text.replace(/>/g, '>');
|
|
339
|
+
text = text.replace(/\n{3,}/g, '\n\n');
|
|
340
|
+
return text.trim();
|
|
341
|
+
}
|
|
342
|
+
function splitIngredients(text) {
|
|
343
|
+
if (!text)
|
|
344
|
+
return { descriptionText: '', ingredientsText: '' };
|
|
345
|
+
const match = text.match(/(?:^|\n)\s*Съставки\s*:?\s*/i);
|
|
346
|
+
if (!match || match.index === undefined) {
|
|
347
|
+
return { descriptionText: text, ingredientsText: '' };
|
|
348
|
+
}
|
|
349
|
+
const start = match.index;
|
|
350
|
+
const end = start + match[0].length;
|
|
351
|
+
const descriptionText = text.slice(0, start).trim();
|
|
352
|
+
const ingredientsText = text.slice(end).trim();
|
|
353
|
+
return { descriptionText, ingredientsText };
|
|
354
|
+
}
|