ebag 0.1.0 → 0.1.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 +30 -7
- package/dist/cli/format.d.ts +1 -1
- package/dist/cli/format.js +149 -123
- package/dist/cli/index.js +139 -78
- package/dist/cli/warnings.d.ts +2 -0
- package/dist/cli/warnings.js +14 -0
- package/dist/lib/auth.d.ts +1 -1
- package/dist/lib/auth.js +5 -5
- package/dist/lib/cart.d.ts +1 -1
- package/dist/lib/cart.js +11 -11
- package/dist/lib/client.d.ts +1 -1
- package/dist/lib/client.js +30 -20
- package/dist/lib/config.d.ts +2 -1
- package/dist/lib/config.js +22 -13
- package/dist/lib/cookies.js +10 -7
- package/dist/lib/index.d.ts +9 -9
- package/dist/lib/lists.d.ts +1 -1
- package/dist/lib/lists.js +4 -4
- package/dist/lib/log.d.ts +8 -0
- package/dist/lib/log.js +115 -0
- package/dist/lib/order-status.d.ts +9 -0
- package/dist/lib/order-status.js +34 -0
- package/dist/lib/orders.d.ts +1 -1
- package/dist/lib/orders.js +83 -46
- package/dist/lib/products.d.ts +1 -1
- package/dist/lib/search.d.ts +1 -1
- package/dist/lib/search.js +26 -20
- package/dist/lib/slots.d.ts +1 -1
- package/dist/lib/slots.js +3 -3
- package/dist/lib/types.d.ts +3 -3
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -23,9 +23,13 @@ ebag login --cookie "<cookie>"
|
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
Notes:
|
|
26
|
+
|
|
26
27
|
- Session data is stored in plain text at `~/.config/ebag/session.json` with owner-only permissions (`0600`).
|
|
27
28
|
- List-based search caches product details in `~/.config/ebag/cache.json` for up to 6 hours.
|
|
28
29
|
- Completed orders may be cached locally for faster access.
|
|
30
|
+
- Unknown order statuses warn on stderr and include a link to file a GitHub issue.
|
|
31
|
+
- API `statusText` values are inconsistent with the website and intentionally ignored (including `--json`).
|
|
32
|
+
- `statusDescription` is the supported status label field in `--json` output and is computed at runtime.
|
|
29
33
|
|
|
30
34
|
## Products
|
|
31
35
|
|
|
@@ -73,18 +77,37 @@ ebag list show 753250
|
|
|
73
77
|
npm run build
|
|
74
78
|
```
|
|
75
79
|
|
|
76
|
-
###
|
|
80
|
+
### Unit tests
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
npm run test:unit
|
|
84
|
+
```
|
|
77
85
|
|
|
78
|
-
|
|
79
|
-
You can also override with `EBAG_COOKIE`.
|
|
86
|
+
### End-to-end tests
|
|
80
87
|
|
|
81
|
-
|
|
88
|
+
Requires a valid session in `tests/.config/ebag/session.json` (gitignored):
|
|
82
89
|
|
|
83
90
|
```bash
|
|
84
|
-
|
|
91
|
+
npm run test:e2e
|
|
85
92
|
```
|
|
86
|
-
|
|
93
|
+
|
|
94
|
+
### All tests
|
|
87
95
|
|
|
88
96
|
```bash
|
|
89
|
-
npm
|
|
97
|
+
npm test
|
|
90
98
|
```
|
|
99
|
+
|
|
100
|
+
## Changelog
|
|
101
|
+
|
|
102
|
+
### 0.1.2
|
|
103
|
+
|
|
104
|
+
- Log unknown order statuses in the lib layer and warn in the CLI
|
|
105
|
+
- Add tests for order status logging and CLI warnings
|
|
106
|
+
|
|
107
|
+
### 0.1.1
|
|
108
|
+
|
|
109
|
+
- Add logging in the config directory
|
|
110
|
+
|
|
111
|
+
### 0.1.0
|
|
112
|
+
|
|
113
|
+
- Initial release
|
package/dist/cli/format.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OrderDetail, OrderSummary, ProductSummary } from
|
|
1
|
+
import type { OrderDetail, OrderSummary, ProductSummary } from "../lib/types";
|
|
2
2
|
export declare function outputJson(data: unknown): void;
|
|
3
3
|
export declare function formatHeading(text: string): string;
|
|
4
4
|
export declare function outputProducts(products: ProductSummary[]): void;
|
package/dist/cli/format.js
CHANGED
|
@@ -7,6 +7,7 @@ exports.outputList = outputList;
|
|
|
7
7
|
exports.outputProductDetail = outputProductDetail;
|
|
8
8
|
exports.outputOrdersList = outputOrdersList;
|
|
9
9
|
exports.outputOrderDetail = outputOrderDetail;
|
|
10
|
+
const order_status_1 = require("../lib/order-status");
|
|
10
11
|
function outputJson(data) {
|
|
11
12
|
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
12
13
|
}
|
|
@@ -23,84 +24,95 @@ function formatPrice(product) {
|
|
|
23
24
|
return product.pricePromo;
|
|
24
25
|
if (product.price)
|
|
25
26
|
return product.price;
|
|
26
|
-
return
|
|
27
|
+
return "";
|
|
27
28
|
}
|
|
28
29
|
function outputProducts(products) {
|
|
29
30
|
for (const product of products) {
|
|
30
31
|
const price = formatPrice(product);
|
|
31
|
-
let source =
|
|
32
|
-
if (product.source ===
|
|
32
|
+
let source = "";
|
|
33
|
+
if (product.source === "list") {
|
|
33
34
|
const listNames = (product.listNames || []).filter(Boolean);
|
|
34
35
|
if (listNames.length) {
|
|
35
|
-
source = ` [${listNames.join(
|
|
36
|
+
source = ` [${listNames.join(", ")}]`;
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
|
-
const line = `${product.id} ${product.name}${price ? ` - ${price}` :
|
|
39
|
+
const line = `${product.id} ${product.name}${price ? ` - ${price}` : ""}${source}`;
|
|
39
40
|
process.stdout.write(`${line}\n`);
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
function outputList(items) {
|
|
43
44
|
for (const item of items) {
|
|
44
|
-
const count = item.count !== undefined ? ` (${item.count})` :
|
|
45
|
+
const count = item.count !== undefined ? ` (${item.count})` : "";
|
|
45
46
|
process.stdout.write(`${item.id} ${item.name}${count}\n`);
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
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 ===
|
|
50
|
+
const id = data.id ? String(data.id) : "";
|
|
51
|
+
const name = data.name ? String(data.name) : "";
|
|
52
|
+
const nameEn = data.name_en ? String(data.name_en) : "";
|
|
53
|
+
const brand = typeof data.brand === "string"
|
|
53
54
|
? data.brand
|
|
54
55
|
: data.brand?.name ||
|
|
55
|
-
data.brand
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
'';
|
|
60
|
-
const unit = data.unit_weight_text ? String(data.unit_weight_text) : '';
|
|
56
|
+
data.brand?.name_bg ||
|
|
57
|
+
data.brand?.name_en ||
|
|
58
|
+
"";
|
|
59
|
+
const unit = data.unit_weight_text ? String(data.unit_weight_text) : "";
|
|
61
60
|
const price = data.current_price_eur ?? data.price_promo_eur ?? data.price_eur;
|
|
62
|
-
const priceText = price ? String(price) :
|
|
63
|
-
const currency = priceText ?
|
|
64
|
-
const availability = data.is_available === false
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
61
|
+
const priceText = price ? String(price) : "";
|
|
62
|
+
const currency = priceText ? "EUR" : "";
|
|
63
|
+
const availability = data.is_available === false
|
|
64
|
+
? "No"
|
|
65
|
+
: data.is_available === true
|
|
66
|
+
? "Yes"
|
|
67
|
+
: "";
|
|
68
|
+
const urlSlug = data.url_slug ? String(data.url_slug) : "";
|
|
69
|
+
const origin = data.country_of_origin ? String(data.country_of_origin) : "";
|
|
70
|
+
const expiryRaw = data.expiry_date ? String(data.expiry_date) : "";
|
|
68
71
|
const expiry = formatDate(expiryRaw);
|
|
69
|
-
const description = data.description ? String(data.description) :
|
|
72
|
+
const description = data.description ? String(data.description) : "";
|
|
70
73
|
const kvPairs = [
|
|
71
|
-
[
|
|
72
|
-
[
|
|
73
|
-
[
|
|
74
|
-
[
|
|
75
|
-
[
|
|
76
|
-
[
|
|
77
|
-
[
|
|
78
|
-
[
|
|
74
|
+
["ID", id],
|
|
75
|
+
["Brand", brand],
|
|
76
|
+
["Unit", unit],
|
|
77
|
+
["Price", priceText ? `${priceText}${currency ? ` ${currency}` : ""}` : ""],
|
|
78
|
+
["Available", availability],
|
|
79
|
+
["Origin", origin],
|
|
80
|
+
["Expiry", expiry],
|
|
81
|
+
["Slug", urlSlug],
|
|
79
82
|
];
|
|
80
83
|
const energyPairs = formatEnergyValues(data.energy_values);
|
|
81
|
-
const kvLines = [
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const descriptionMd = description ? htmlToMarkdown(description) :
|
|
84
|
+
const kvLines = [...kvPairs.filter(([, value]) => value), ...energyPairs].map(([key, value]) => `${key}: ${value}`);
|
|
85
|
+
const headerLines = [
|
|
86
|
+
`# ${name || "Product"}`,
|
|
87
|
+
nameEn ? `*${nameEn}*` : "",
|
|
88
|
+
].filter(Boolean);
|
|
89
|
+
const descriptionMd = description ? htmlToMarkdown(description) : "";
|
|
87
90
|
const { descriptionText, ingredientsText } = splitIngredients(descriptionMd);
|
|
88
|
-
const descriptionBlock = descriptionText
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
91
|
+
const descriptionBlock = descriptionText
|
|
92
|
+
? `# Description\n\n${descriptionText}`
|
|
93
|
+
: "";
|
|
94
|
+
const ingredientsBlock = ingredientsText
|
|
95
|
+
? `\n# Ingredients\n\n${ingredientsText}`
|
|
96
|
+
: "";
|
|
97
|
+
const kvBlock = kvLines.length ? ["---", ...kvLines, "---"].join("\n") : "";
|
|
98
|
+
const output = [
|
|
99
|
+
headerLines.join("\n"),
|
|
100
|
+
kvBlock,
|
|
101
|
+
descriptionBlock,
|
|
102
|
+
ingredientsBlock,
|
|
103
|
+
]
|
|
92
104
|
.filter(Boolean)
|
|
93
|
-
.join(
|
|
105
|
+
.join("\n");
|
|
94
106
|
process.stdout.write(`${output}\n`);
|
|
95
107
|
}
|
|
96
108
|
function formatOrderAmount(order) {
|
|
97
|
-
if (
|
|
109
|
+
if ("finalAmountEur" in order && order.finalAmountEur) {
|
|
98
110
|
return `${order.finalAmountEur} EUR`;
|
|
99
111
|
}
|
|
100
|
-
if (
|
|
112
|
+
if ("finalAmount" in order && order.finalAmount) {
|
|
101
113
|
return order.finalAmount;
|
|
102
114
|
}
|
|
103
|
-
if (
|
|
115
|
+
if ("totals" in order) {
|
|
104
116
|
if (order.totals.totalPaidEur)
|
|
105
117
|
return `${order.totals.totalPaidEur} EUR`;
|
|
106
118
|
if (order.totals.totalEur)
|
|
@@ -110,50 +122,52 @@ function formatOrderAmount(order) {
|
|
|
110
122
|
if (order.totals.total)
|
|
111
123
|
return order.totals.total;
|
|
112
124
|
}
|
|
113
|
-
return
|
|
125
|
+
return "";
|
|
114
126
|
}
|
|
115
127
|
function formatDateInTimeZone(date, timeZone) {
|
|
116
|
-
const parts = new Intl.DateTimeFormat(
|
|
128
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
117
129
|
timeZone,
|
|
118
|
-
year:
|
|
119
|
-
month:
|
|
120
|
-
day:
|
|
130
|
+
year: "numeric",
|
|
131
|
+
month: "2-digit",
|
|
132
|
+
day: "2-digit",
|
|
121
133
|
}).formatToParts(date);
|
|
122
134
|
const lookup = new Map(parts.map((part) => [part.type, part.value]));
|
|
123
|
-
const year = lookup.get(
|
|
124
|
-
const month = lookup.get(
|
|
125
|
-
const day = lookup.get(
|
|
135
|
+
const year = lookup.get("year") || "";
|
|
136
|
+
const month = lookup.get("month") || "";
|
|
137
|
+
const day = lookup.get("day") || "";
|
|
126
138
|
return `${year}-${month}-${day}`;
|
|
127
139
|
}
|
|
128
140
|
function formatOrderStatus(order) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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 '';
|
|
141
|
+
if (order.statusDescription)
|
|
142
|
+
return order.statusDescription;
|
|
143
|
+
return (0, order_status_1.describeOrderStatus)(order.status);
|
|
143
144
|
}
|
|
144
145
|
function outputOrdersList(orders) {
|
|
145
146
|
for (const order of orders) {
|
|
146
|
-
const date = order.shippingDate ? formatDate(order.shippingDate) :
|
|
147
|
-
const slot = order.timeSlotDisplay ||
|
|
147
|
+
const date = order.shippingDate ? formatDate(order.shippingDate) : "";
|
|
148
|
+
const slot = order.timeSlotDisplay || "";
|
|
148
149
|
const status = formatOrderStatus(order);
|
|
149
150
|
const total = formatOrderAmount(order);
|
|
150
151
|
const parts = [order.id, date, slot].filter(Boolean);
|
|
151
|
-
const suffix = [status, total].filter(Boolean).join(
|
|
152
|
-
const line = `${parts.join(
|
|
152
|
+
const suffix = [status, total].filter(Boolean).join(" - ");
|
|
153
|
+
const line = `${parts.join(" ")}${suffix ? ` - ${suffix}` : ""}`;
|
|
153
154
|
process.stdout.write(`${line}\n`);
|
|
154
155
|
}
|
|
155
156
|
}
|
|
156
157
|
function outputOrderItems(items) {
|
|
158
|
+
const formatQuantity = (quantity) => {
|
|
159
|
+
if (!quantity)
|
|
160
|
+
return "";
|
|
161
|
+
const normalized = quantity.trim().replace(",", ".");
|
|
162
|
+
if (!normalized)
|
|
163
|
+
return "";
|
|
164
|
+
const parsed = Number(normalized);
|
|
165
|
+
if (!Number.isFinite(parsed))
|
|
166
|
+
return normalized;
|
|
167
|
+
if (Number.isInteger(parsed))
|
|
168
|
+
return String(parsed);
|
|
169
|
+
return String(parsed);
|
|
170
|
+
};
|
|
157
171
|
const byGroup = new Map();
|
|
158
172
|
const ungrouped = [];
|
|
159
173
|
for (const item of items) {
|
|
@@ -171,57 +185,69 @@ function outputOrderItems(items) {
|
|
|
171
185
|
for (const [group, groupItems] of groupEntries) {
|
|
172
186
|
process.stdout.write(`## ${group}\n`);
|
|
173
187
|
for (const item of groupItems) {
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
const
|
|
177
|
-
const
|
|
188
|
+
const qtyValue = formatQuantity(item.quantity);
|
|
189
|
+
const qty = qtyValue ? ` x${qtyValue}` : "";
|
|
190
|
+
const unit = item.unit ? ` (${item.unit})` : "";
|
|
191
|
+
const price = item.priceEur ? `${item.priceEur} EUR` : item.price || "";
|
|
192
|
+
const line = `- ${item.name}${unit}${qty}${price ? ` - ${price}` : ""}`;
|
|
178
193
|
process.stdout.write(`${line}\n`);
|
|
179
194
|
}
|
|
180
|
-
process.stdout.write(
|
|
195
|
+
process.stdout.write("\n");
|
|
181
196
|
}
|
|
182
197
|
}
|
|
183
198
|
if (ungrouped.length) {
|
|
184
199
|
for (const item of ungrouped) {
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
const
|
|
200
|
+
const qtyValue = formatQuantity(item.quantity);
|
|
201
|
+
const qty = qtyValue ? ` x${qtyValue}` : "";
|
|
202
|
+
const unit = item.unit ? ` (${item.unit})` : "";
|
|
203
|
+
const price = item.priceEur ? `${item.priceEur} EUR` : item.price || "";
|
|
204
|
+
const line = `- ${item.name}${unit}${qty}${price ? ` - ${price}` : ""}`;
|
|
189
205
|
process.stdout.write(`${line}\n`);
|
|
190
206
|
}
|
|
191
|
-
process.stdout.write(
|
|
207
|
+
process.stdout.write("\n");
|
|
192
208
|
}
|
|
193
209
|
}
|
|
194
210
|
function outputOrderDetail(detail) {
|
|
195
|
-
const header = `# Order ${detail.id ||
|
|
211
|
+
const header = `# Order ${detail.id || ""}`.trim();
|
|
196
212
|
const status = formatOrderStatus(detail);
|
|
197
|
-
const date = detail.shippingDate ? formatDate(detail.shippingDate) :
|
|
198
|
-
const address = detail.address ||
|
|
213
|
+
const date = detail.shippingDate ? formatDate(detail.shippingDate) : "";
|
|
214
|
+
const address = detail.address || "";
|
|
199
215
|
const total = formatOrderAmount(detail);
|
|
200
216
|
const kvPairs = [
|
|
201
|
-
[
|
|
202
|
-
[
|
|
203
|
-
[
|
|
204
|
-
[
|
|
205
|
-
[
|
|
206
|
-
[
|
|
207
|
-
[
|
|
208
|
-
|
|
217
|
+
["ID", detail.id],
|
|
218
|
+
["Status", status],
|
|
219
|
+
["Date", date],
|
|
220
|
+
["Timeslot", detail.timeSlotDisplay || ""],
|
|
221
|
+
["Address", address],
|
|
222
|
+
["Total", total],
|
|
223
|
+
[
|
|
224
|
+
"Discount",
|
|
225
|
+
detail.totals.discountEur
|
|
226
|
+
? `${detail.totals.discountEur} EUR`
|
|
227
|
+
: detail.totals.discount || "",
|
|
228
|
+
],
|
|
229
|
+
[
|
|
230
|
+
"Tip",
|
|
231
|
+
detail.totals.tipEur
|
|
232
|
+
? `${detail.totals.tipEur} EUR`
|
|
233
|
+
: detail.totals.tip || "",
|
|
234
|
+
],
|
|
209
235
|
].filter(([, value]) => value);
|
|
210
236
|
const kvLines = kvPairs.map(([key, value]) => `${key}: ${value}`);
|
|
211
|
-
const kvBlock = kvLines.length ? [
|
|
237
|
+
const kvBlock = kvLines.length ? ["---", ...kvLines, "---"].join("\n") : "";
|
|
212
238
|
process.stdout.write(`${header}\n`);
|
|
213
239
|
if (kvBlock) {
|
|
214
240
|
process.stdout.write(`${kvBlock}\n`);
|
|
215
241
|
}
|
|
216
|
-
process.stdout.write(
|
|
242
|
+
process.stdout.write("# Items\n");
|
|
217
243
|
if (detail.items.length) {
|
|
218
244
|
outputOrderItems(detail.items);
|
|
219
245
|
}
|
|
220
246
|
else {
|
|
221
|
-
process.stdout.write(
|
|
247
|
+
process.stdout.write("No items found.\n\n");
|
|
222
248
|
}
|
|
223
249
|
if (detail.additionalOrders && detail.additionalOrders.length) {
|
|
224
|
-
process.stdout.write(
|
|
250
|
+
process.stdout.write("# Additional Orders\n");
|
|
225
251
|
for (const additional of detail.additionalOrders) {
|
|
226
252
|
process.stdout.write(`## Order ${additional.id}\n`);
|
|
227
253
|
const additionalTotal = formatOrderAmount(additional);
|
|
@@ -232,14 +258,14 @@ function outputOrderDetail(detail) {
|
|
|
232
258
|
outputOrderItems(additional.items);
|
|
233
259
|
}
|
|
234
260
|
else {
|
|
235
|
-
process.stdout.write(
|
|
261
|
+
process.stdout.write("No items found.\n\n");
|
|
236
262
|
}
|
|
237
263
|
}
|
|
238
264
|
}
|
|
239
265
|
}
|
|
240
266
|
function formatDate(value) {
|
|
241
267
|
if (!value)
|
|
242
|
-
return
|
|
268
|
+
return "";
|
|
243
269
|
const isoMatch = value.match(/(\d{4})-(\d{2})-(\d{2})/);
|
|
244
270
|
if (isoMatch)
|
|
245
271
|
return `${isoMatch[1]}-${isoMatch[2]}-${isoMatch[3]}`;
|
|
@@ -252,7 +278,7 @@ function formatDate(value) {
|
|
|
252
278
|
const parsed = new Date(value);
|
|
253
279
|
if (Number.isNaN(parsed.getTime()))
|
|
254
280
|
return value;
|
|
255
|
-
const pad = (n) => String(n).padStart(2,
|
|
281
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
256
282
|
return `${parsed.getFullYear()}-${pad(parsed.getMonth() + 1)}-${pad(parsed.getDate())}`;
|
|
257
283
|
}
|
|
258
284
|
function formatEnergyValues(value) {
|
|
@@ -261,7 +287,7 @@ function formatEnergyValues(value) {
|
|
|
261
287
|
if (Array.isArray(value)) {
|
|
262
288
|
const pairs = [];
|
|
263
289
|
for (const item of value) {
|
|
264
|
-
if (!item || typeof item !==
|
|
290
|
+
if (!item || typeof item !== "object")
|
|
265
291
|
continue;
|
|
266
292
|
const entry = item;
|
|
267
293
|
const label = entry.name_bg ||
|
|
@@ -279,7 +305,7 @@ function formatEnergyValues(value) {
|
|
|
279
305
|
}
|
|
280
306
|
return pairs;
|
|
281
307
|
}
|
|
282
|
-
if (typeof value ===
|
|
308
|
+
if (typeof value === "object") {
|
|
283
309
|
return Object.entries(value)
|
|
284
310
|
.filter(([, entry]) => entry !== null && entry !== undefined)
|
|
285
311
|
.flatMap(([key, entry]) => splitEnergyValues(`Energy ${key}`, String(entry)));
|
|
@@ -292,7 +318,7 @@ function splitEnergyValues(label, raw) {
|
|
|
292
318
|
const kjMatch = normalized.match(/(\d+(?:\.\d+)?)\s*kJ/i);
|
|
293
319
|
const hasKcalKjLabel = /kcal/i.test(label) && /kJ/i.test(label);
|
|
294
320
|
const splitMatch = normalized.match(/^(\d+(?:\.\d+)?)\/(\d+(?:\.\d+)?)$/);
|
|
295
|
-
const baseLabel = label.replace(/\(?\s*kcal\s*\/\s*kJ\s*\)?/i,
|
|
321
|
+
const baseLabel = label.replace(/\(?\s*kcal\s*\/\s*kJ\s*\)?/i, "").trim();
|
|
296
322
|
const energyLabel = baseLabel || label;
|
|
297
323
|
if (kcalMatch || kjMatch) {
|
|
298
324
|
const items = [];
|
|
@@ -311,40 +337,40 @@ function splitEnergyValues(label, raw) {
|
|
|
311
337
|
return [[label, normalized]];
|
|
312
338
|
}
|
|
313
339
|
function normalizeNumberString(value) {
|
|
314
|
-
if (typeof value ===
|
|
340
|
+
if (typeof value === "number")
|
|
315
341
|
return value.toString();
|
|
316
|
-
if (typeof value !==
|
|
342
|
+
if (typeof value !== "string")
|
|
317
343
|
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(
|
|
344
|
+
const trimmed = value.trim().replace(/\u00a0/g, " ");
|
|
345
|
+
const noSpaces = trimmed.replace(/\s+/g, "");
|
|
346
|
+
if (noSpaces.includes(",")) {
|
|
347
|
+
return noSpaces.replace(",", ".");
|
|
322
348
|
}
|
|
323
349
|
return noSpaces;
|
|
324
350
|
}
|
|
325
351
|
function htmlToMarkdown(html) {
|
|
326
352
|
let text = html;
|
|
327
|
-
text = text.replace(/<\s*br\s*\/?\s*>/gi,
|
|
328
|
-
text = text.replace(/<\s*\/p\s*>/gi,
|
|
329
|
-
text = text.replace(/<\s*p[^>]*>/gi,
|
|
330
|
-
text = text.replace(/<\s*li[^>]*>/gi,
|
|
331
|
-
text = text.replace(/<\s*\/li\s*>/gi,
|
|
332
|
-
text = text.replace(/<\s*ul[^>]*>/gi,
|
|
333
|
-
text = text.replace(/<\s*\/ul\s*>/gi,
|
|
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,
|
|
353
|
+
text = text.replace(/<\s*br\s*\/?\s*>/gi, "\n");
|
|
354
|
+
text = text.replace(/<\s*\/p\s*>/gi, "\n\n");
|
|
355
|
+
text = text.replace(/<\s*p[^>]*>/gi, "");
|
|
356
|
+
text = text.replace(/<\s*li[^>]*>/gi, "- ");
|
|
357
|
+
text = text.replace(/<\s*\/li\s*>/gi, "\n");
|
|
358
|
+
text = text.replace(/<\s*ul[^>]*>/gi, "\n");
|
|
359
|
+
text = text.replace(/<\s*\/ul\s*>/gi, "\n");
|
|
360
|
+
text = text.replace(/<[^>]+>/g, "");
|
|
361
|
+
text = text.replace(/ /g, " ");
|
|
362
|
+
text = text.replace(/&/g, "&");
|
|
363
|
+
text = text.replace(/</g, "<");
|
|
364
|
+
text = text.replace(/>/g, ">");
|
|
365
|
+
text = text.replace(/\n{3,}/g, "\n\n");
|
|
340
366
|
return text.trim();
|
|
341
367
|
}
|
|
342
368
|
function splitIngredients(text) {
|
|
343
369
|
if (!text)
|
|
344
|
-
return { descriptionText:
|
|
370
|
+
return { descriptionText: "", ingredientsText: "" };
|
|
345
371
|
const match = text.match(/(?:^|\n)\s*Съставки\s*:?\s*/i);
|
|
346
372
|
if (!match || match.index === undefined) {
|
|
347
|
-
return { descriptionText: text, ingredientsText:
|
|
373
|
+
return { descriptionText: text, ingredientsText: "" };
|
|
348
374
|
}
|
|
349
375
|
const start = match.index;
|
|
350
376
|
const end = start + match[0].length;
|