@recipe-book/core 0.0.3 → 0.1.1
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/CHANGELOG.md +116 -0
- package/LICENSE +21 -0
- package/README.md +50 -7
- package/dist/chunk-D5CV3UF7.cjs +2754 -0
- package/dist/chunk-D5CV3UF7.cjs.map +1 -0
- package/dist/errors.js +76 -0
- package/dist/errors.js.map +1 -0
- package/dist/format/duration.js +36 -0
- package/dist/format/duration.js.map +1 -0
- package/dist/format/ingredient.js +49 -0
- package/dist/format/ingredient.js.map +1 -0
- package/dist/format/number.js +120 -0
- package/dist/format/number.js.map +1 -0
- package/dist/format/options.js +31 -0
- package/dist/format/options.js.map +1 -0
- package/dist/format/quantity.js +144 -0
- package/dist/format/quantity.js.map +1 -0
- package/dist/format/servings.js +68 -0
- package/dist/format/servings.js.map +1 -0
- package/dist/format/temperature.js +35 -0
- package/dist/format/temperature.js.map +1 -0
- package/dist/index.cjs +476 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +994 -0
- package/dist/index.d.ts +994 -5
- package/dist/index.js +48 -1
- package/dist/index.js.map +1 -0
- package/dist/ingredient/parse.js +412 -0
- package/dist/ingredient/parse.js.map +1 -0
- package/dist/issues.js +26 -0
- package/dist/issues.js.map +1 -0
- package/dist/quantity/amount.js +78 -0
- package/dist/quantity/amount.js.map +1 -0
- package/dist/quantity/parse.js +255 -0
- package/dist/quantity/parse.js.map +1 -0
- package/dist/quantity/scale.js +23 -0
- package/dist/quantity/scale.js.map +1 -0
- package/dist/recipe/duration.js +28 -0
- package/dist/recipe/duration.js.map +1 -0
- package/dist/recipe/ids.js +68 -0
- package/dist/recipe/ids.js.map +1 -0
- package/dist/recipe/infer.js +24 -0
- package/dist/recipe/infer.js.map +1 -0
- package/dist/recipe/nutrient-units.js +18 -0
- package/dist/recipe/nutrient-units.js.map +1 -0
- package/dist/recipe/nutrition.js +26 -0
- package/dist/recipe/nutrition.js.map +1 -0
- package/dist/recipe/parse.js +854 -0
- package/dist/recipe/parse.js.map +1 -0
- package/dist/recipe/schema.js +32 -0
- package/dist/recipe/schema.js.map +1 -0
- package/dist/recipe/url.js +9 -0
- package/dist/recipe/url.js.map +1 -0
- package/dist/resolve.js +120 -0
- package/dist/resolve.js.map +1 -0
- package/dist/schema-org/find.js +158 -0
- package/dist/schema-org/find.js.map +1 -0
- package/dist/schema-org/from-json-ld.js +647 -0
- package/dist/schema-org/from-json-ld.js.map +1 -0
- package/dist/schema-org/nutrients.js +19 -0
- package/dist/schema-org/nutrients.js.map +1 -0
- package/dist/schema-org/text.js +125 -0
- package/dist/schema-org/text.js.map +1 -0
- package/dist/schema-org/to-json-ld.js +151 -0
- package/dist/schema-org/to-json-ld.js.map +1 -0
- package/dist/schema-org.cjs +1062 -0
- package/dist/schema-org.cjs.map +1 -0
- package/dist/schema-org.d.cts +223 -0
- package/dist/schema-org.d.ts +223 -0
- package/dist/schema-org.js +10 -0
- package/dist/schema-org.js.map +1 -0
- package/dist/steps/segment.js +45 -0
- package/dist/steps/segment.js.map +1 -0
- package/dist/steps/temperature.js +174 -0
- package/dist/steps/temperature.js.map +1 -0
- package/dist/suggest.js +50 -0
- package/dist/suggest.js.map +1 -0
- package/dist/types-BknM8KOd.d.cts +628 -0
- package/dist/types-BknM8KOd.d.ts +628 -0
- package/dist/units/convert.js +79 -0
- package/dist/units/convert.js.map +1 -0
- package/dist/units/lookup.js +86 -0
- package/dist/units/lookup.js.map +1 -0
- package/dist/units/registry.js +183 -0
- package/dist/units/registry.js.map +1 -0
- package/dist/units/system.js +91 -0
- package/dist/units/system.js.map +1 -0
- package/dist/units/temperature.js +40 -0
- package/dist/units/temperature.js.map +1 -0
- package/dist/validation.js +90 -0
- package/dist/validation.js.map +1 -0
- package/package.json +53 -8
- package/schema-org/package.json +5 -0
- package/dist/chunk-22RBQQWH.mjs +0 -1
- package/dist/chunk-3CSP4UZG.mjs +0 -1
- package/dist/chunk-3FFMHRB7.mjs +0 -26
- package/dist/chunk-4EB3HG6B.mjs +0 -50
- package/dist/chunk-4EHNJLGI.mjs +0 -50
- package/dist/chunk-4IA5EEKI.mjs +0 -81
- package/dist/chunk-4TATADPT.mjs +0 -51
- package/dist/chunk-6GI5KLI4.mjs +0 -22
- package/dist/chunk-6QJZOZ6W.mjs +0 -49
- package/dist/chunk-6YYQ4RYZ.mjs +0 -26
- package/dist/chunk-7BBUDI2L.mjs +0 -0
- package/dist/chunk-7KEMNCV7.mjs +0 -26
- package/dist/chunk-7MMJZZ7Z.mjs +0 -75
- package/dist/chunk-BES4V7DM.mjs +0 -26
- package/dist/chunk-BRDXRIAH.mjs +0 -50
- package/dist/chunk-C45UD45A.mjs +0 -1
- package/dist/chunk-CBLJXA6P.mjs +0 -1
- package/dist/chunk-CFR2ZBPB.mjs +0 -77
- package/dist/chunk-DYMNVFL2.mjs +0 -78
- package/dist/chunk-F6AQKXJU.mjs +0 -51
- package/dist/chunk-FW4M2R43.mjs +0 -51
- package/dist/chunk-GCCEDUST.mjs +0 -75
- package/dist/chunk-GE7KBYZR.mjs +0 -26
- package/dist/chunk-GN7ZDGYC.mjs +0 -51
- package/dist/chunk-GOO2SCQX.mjs +0 -75
- package/dist/chunk-HAVKBQLH.mjs +0 -51
- package/dist/chunk-HCBIPANU.mjs +0 -50
- package/dist/chunk-IEBAGBS7.mjs +0 -51
- package/dist/chunk-IQ6WN2V4.mjs +0 -75
- package/dist/chunk-JB4Z2FR7.mjs +0 -77
- package/dist/chunk-JMTQOTBM.mjs +0 -75
- package/dist/chunk-JRO6JUES.mjs +0 -51
- package/dist/chunk-KH3SRXET.mjs +0 -52
- package/dist/chunk-KNZMCAQZ.mjs +0 -51
- package/dist/chunk-KXFUCO3R.mjs +0 -1
- package/dist/chunk-LEBGCVCJ.mjs +0 -75
- package/dist/chunk-MAHWFCIL.mjs +0 -51
- package/dist/chunk-MPH76SLC.mjs +0 -51
- package/dist/chunk-MQGMFBJQ.mjs +0 -26
- package/dist/chunk-NMIVG7T4.mjs +0 -6
- package/dist/chunk-Q3TT3AZ4.mjs +0 -26
- package/dist/chunk-QY5HJQIL.mjs +0 -51
- package/dist/chunk-RCPLSNKM.mjs +0 -1
- package/dist/chunk-SR2KFBT3.mjs +0 -1
- package/dist/chunk-ULDLBAEG.mjs +0 -1
- package/dist/chunk-UQ3EUYFR.mjs +0 -1
- package/dist/chunk-V26OCCTL.mjs +0 -82
- package/dist/chunk-WBQAMGXK.mjs +0 -0
- package/dist/chunk-WSW6FMUL.mjs +0 -51
- package/dist/chunk-WU55SBIV.mjs +0 -75
- package/dist/chunk-XINK45P3.mjs +0 -51
- package/dist/chunk-XNOHIWI3.mjs +0 -1
- package/dist/chunk-XV4QN4LA.mjs +0 -6
- package/dist/chunk-YHBXEVUX.mjs +0 -51
- package/dist/chunk-YSZSCEI7.mjs +0 -75
- package/dist/chunk-YY3SUQ7P.mjs +0 -16
- package/dist/chunk-YZB3IG47.mjs +0 -80
- package/dist/chunk-ZPZT4LMQ.mjs +0 -25
- package/dist/const/conversion.d.mts +0 -7
- package/dist/const/conversion.d.ts +0 -7
- package/dist/const/conversion.js +0 -1
- package/dist/const/conversion.mjs +0 -1
- package/dist/const/unit.d.mts +0 -119
- package/dist/const/unit.d.ts +0 -119
- package/dist/const/unit.js +0 -1
- package/dist/const/unit.mjs +0 -1
- package/dist/index.d.mts +0 -5
- package/dist/index.mjs +0 -1
- package/dist/lib/assert.d.mts +0 -18
- package/dist/lib/assert.d.ts +0 -18
- package/dist/lib/assert.js +0 -1
- package/dist/lib/assert.mjs +0 -1
- package/dist/lib/ingredient.d.mts +0 -9
- package/dist/lib/ingredient.d.ts +0 -9
- package/dist/lib/ingredient.js +0 -1
- package/dist/lib/ingredient.mjs +0 -1
- package/dist/lib/recipe.d.mts +0 -12
- package/dist/lib/recipe.d.ts +0 -12
- package/dist/lib/recipe.js +0 -1
- package/dist/lib/recipe.mjs +0 -1
- package/dist/lib/runtime.d.mts +0 -3
- package/dist/lib/runtime.d.ts +0 -3
- package/dist/lib/runtime.js +0 -1
- package/dist/lib/runtime.mjs +0 -1
- package/dist/lib/unit.d.mts +0 -19
- package/dist/lib/unit.d.ts +0 -19
- package/dist/lib/unit.js +0 -1
- package/dist/lib/unit.mjs +0 -1
- package/dist/types.d.mts +0 -36
- package/dist/types.d.ts +0 -36
- package/dist/types.js +0 -1
- package/dist/types.mjs +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/quantity/amount.ts"],"sourcesContent":["import { describeValue, RecipeBookError } from '../errors'\nimport type { Amount, Range } from './types'\n\n/** The amount type an operation returns for an input amount type. */\nexport type SameAmount<A extends Amount> = A extends number ? number : Range\n\nexport function mapAmount<A extends Amount>(\n amount: A,\n fn: (value: number) => number,\n): SameAmount<A> {\n return (\n typeof amount === 'number'\n ? fn(amount)\n : { min: fn(amount.min), max: fn(amount.max) }\n ) as SameAmount<A>\n}\n\nexport function amountEnds(amount: Amount): readonly number[] {\n return typeof amount === 'number' ? [amount] : [amount.min, amount.max]\n}\n\n/**\n * Validates a quantity passed to a public function.\n *\n * @throws {RecipeBookError} `invalid_argument`, naming `fn` and the fix.\n */\nexport function checkQuantity(fn: string, quantity: unknown): Amount {\n if (\n typeof quantity !== 'object' ||\n quantity === null ||\n !('amount' in quantity)\n ) {\n throw invalidArgument(\n `${fn}: expected a quantity such as { amount: 2, unit: \"cup\" }, received ${describeValue(quantity)}.`,\n )\n }\n\n if ((quantity as { max?: unknown }).max !== undefined) {\n throw invalidArgument(\n `${fn}: a quantity has no max; use amount: { min, max }, or formatServings or formatYield.`,\n )\n }\n\n const { amount } = quantity\n\n if (typeof amount === 'number') {\n if (!isPositive(amount)) {\n throw invalidArgument(\n `${fn}: amount must be a finite number greater than 0, received ${describeValue(amount)}.`,\n )\n }\n return amount\n }\n\n if (\n typeof amount === 'object' &&\n amount !== null &&\n 'min' in amount &&\n 'max' in amount\n ) {\n const { min, max } = amount\n if (!isPositive(min) || !isPositive(max)) {\n throw invalidArgument(\n `${fn}: range ends must be finite numbers greater than 0, received { min: ${describeValue(min)}, max: ${describeValue(max)} }.`,\n )\n }\n if (min >= max) {\n throw invalidArgument(\n `${fn}: range minimum (${min}) must be less than maximum (${max}). Use a plain number when both ends are equal.`,\n )\n }\n return { min, max }\n }\n\n throw invalidArgument(\n `${fn}: amount must be a number or a { min, max } range, received ${describeValue(amount)}.`,\n )\n}\n\n/**\n * Returns `amount`, a result of `fn`, unless an end overflowed to `Infinity`\n * or underflowed to 0.\n *\n * @throws {RecipeBookError} `invalid_argument`, naming `fn`.\n */\nexport function checkResult<A extends Amount>(fn: string, amount: A): A {\n for (const value of amountEnds(amount)) {\n if (!isPositive(value)) {\n throw invalidArgument(\n `${fn}: the result must be a finite number greater than 0, but it is ${describeValue(value)}.`,\n )\n }\n }\n return amount\n}\n\n/**\n * Validates a multiplier passed to a public function.\n *\n * @throws {RecipeBookError} `invalid_argument`, naming `fn` and the parameter.\n */\nexport function checkFactor(fn: string, name: string, value: unknown): void {\n if (!isPositive(value)) {\n throw invalidArgument(\n `${fn}: ${name} must be a finite number greater than 0, received ${describeValue(value)}.`,\n )\n }\n}\n\nexport function isPositive(value: unknown): value is number {\n return typeof value === 'number' && Number.isFinite(value) && value > 0\n}\n\nfunction invalidArgument(message: string): RecipeBookError {\n return new RecipeBookError('invalid_argument', message)\n}\n"],"mappings":";AAAA,SAAS,eAAe,uBAAuB;AAMxC,SAAS,UACd,QACA,IACe;AACf,SACE,OAAO,WAAW,WACd,GAAG,MAAM,IACT,EAAE,KAAK,GAAG,OAAO,GAAG,GAAG,KAAK,GAAG,OAAO,GAAG,EAAE;AAEnD;AAEO,SAAS,WAAW,QAAmC;AAC5D,SAAO,OAAO,WAAW,WAAW,CAAC,MAAM,IAAI,CAAC,OAAO,KAAK,OAAO,GAAG;AACxE;AAOO,SAAS,cAAc,IAAY,UAA2B;AACnE,MACE,OAAO,aAAa,YACpB,aAAa,QACb,EAAE,YAAY,WACd;AACA,UAAM;AAAA,MACJ,GAAG,EAAE,sEAAsE,cAAc,QAAQ,CAAC;AAAA,IACpG;AAAA,EACF;AAEA,MAAK,SAA+B,QAAQ,QAAW;AACrD,UAAM;AAAA,MACJ,GAAG,EAAE;AAAA,IACP;AAAA,EACF;AAEA,QAAM,EAAE,OAAO,IAAI;AAEnB,MAAI,OAAO,WAAW,UAAU;AAC9B,QAAI,CAAC,WAAW,MAAM,GAAG;AACvB,YAAM;AAAA,QACJ,GAAG,EAAE,6DAA6D,cAAc,MAAM,CAAC;AAAA,MACzF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,MACE,OAAO,WAAW,YAClB,WAAW,QACX,SAAS,UACT,SAAS,QACT;AACA,UAAM,EAAE,KAAK,IAAI,IAAI;AACrB,QAAI,CAAC,WAAW,GAAG,KAAK,CAAC,WAAW,GAAG,GAAG;AACxC,YAAM;AAAA,QACJ,GAAG,EAAE,uEAAuE,cAAc,GAAG,CAAC,UAAU,cAAc,GAAG,CAAC;AAAA,MAC5H;AAAA,IACF;AACA,QAAI,OAAO,KAAK;AACd,YAAM;AAAA,QACJ,GAAG,EAAE,oBAAoB,GAAG,gCAAgC,GAAG;AAAA,MACjE;AAAA,IACF;AACA,WAAO,EAAE,KAAK,IAAI;AAAA,EACpB;AAEA,QAAM;AAAA,IACJ,GAAG,EAAE,+DAA+D,cAAc,MAAM,CAAC;AAAA,EAC3F;AACF;AAQO,SAAS,YAA8B,IAAY,QAAc;AACtE,aAAW,SAAS,WAAW,MAAM,GAAG;AACtC,QAAI,CAAC,WAAW,KAAK,GAAG;AACtB,YAAM;AAAA,QACJ,GAAG,EAAE,kEAAkE,cAAc,KAAK,CAAC;AAAA,MAC7F;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAOO,SAAS,YAAY,IAAY,MAAc,OAAsB;AAC1E,MAAI,CAAC,WAAW,KAAK,GAAG;AACtB,UAAM;AAAA,MACJ,GAAG,EAAE,KAAK,IAAI,qDAAqD,cAAc,KAAK,CAAC;AAAA,IACzF;AAAA,EACF;AACF;AAEO,SAAS,WAAW,OAAiC;AAC1D,SAAO,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,KAAK,QAAQ;AACxE;AAEA,SAAS,gBAAgB,SAAkC;AACzD,SAAO,IAAI,gBAAgB,oBAAoB,OAAO;AACxD;","names":[]}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// src/quantity/parse.ts
|
|
2
|
+
import {
|
|
3
|
+
describeValue,
|
|
4
|
+
MAX_ECHOED,
|
|
5
|
+
quote
|
|
6
|
+
} from "../errors.js";
|
|
7
|
+
import { findUnit, suggestUnit } from "../units/lookup.js";
|
|
8
|
+
import { units } from "../units/registry.js";
|
|
9
|
+
import { asRecord, checkFields, error, toResult } from "../validation.js";
|
|
10
|
+
import { isPositive } from "./amount.js";
|
|
11
|
+
var VULGAR = "[\xBC-\xBE\u2150-\u215E]";
|
|
12
|
+
var ARTICLE = /^an?\s+(?=\D)/i;
|
|
13
|
+
var amountPattern;
|
|
14
|
+
function parseQuantity(text) {
|
|
15
|
+
const issues = [];
|
|
16
|
+
if (typeof text !== "string") {
|
|
17
|
+
error(
|
|
18
|
+
issues,
|
|
19
|
+
"invalid_type",
|
|
20
|
+
[],
|
|
21
|
+
`Expected quantity text such as "1 \xBD cups", received ${describeValue(text)}.`
|
|
22
|
+
);
|
|
23
|
+
return toResult("invalid_quantity", void 0, issues);
|
|
24
|
+
}
|
|
25
|
+
const quantity = readQuantityText(issues, text, []);
|
|
26
|
+
return toResult("invalid_quantity", quantity, issues, quote(text));
|
|
27
|
+
}
|
|
28
|
+
function readQuantity(issues, value, path) {
|
|
29
|
+
if (typeof value === "string") {
|
|
30
|
+
const quantity = readQuantityText(issues, value, path);
|
|
31
|
+
return quantity && readTextAmount(issues, quantity.amount, path) ? quantity : void 0;
|
|
32
|
+
}
|
|
33
|
+
if (typeof value === "number") {
|
|
34
|
+
const amount = readPositive(issues, value, path);
|
|
35
|
+
return amount === void 0 ? void 0 : { amount };
|
|
36
|
+
}
|
|
37
|
+
const record = asRecord(value);
|
|
38
|
+
if (record) {
|
|
39
|
+
checkFields(issues, record, { amount: true, unit: true }, path);
|
|
40
|
+
const amount = readAmount(issues, record.amount, [...path, "amount"]);
|
|
41
|
+
const { unit } = record;
|
|
42
|
+
if (unit === void 0) {
|
|
43
|
+
return amount === void 0 ? void 0 : { amount };
|
|
44
|
+
}
|
|
45
|
+
if (typeof unit !== "string") {
|
|
46
|
+
return error(
|
|
47
|
+
issues,
|
|
48
|
+
"invalid_type",
|
|
49
|
+
[...path, "unit"],
|
|
50
|
+
`Expected a unit such as "g" or "cup", received ${describeValue(unit)}.`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
const id = findUnit(unit);
|
|
54
|
+
if (!id) {
|
|
55
|
+
const problem = unknownUnit(
|
|
56
|
+
unit,
|
|
57
|
+
amount === void 0 ? void 0 : typeof amount === "number" ? String(amount) : `{ amount: { min: ${amount.min}, max: ${amount.max} } }`
|
|
58
|
+
);
|
|
59
|
+
return error(
|
|
60
|
+
issues,
|
|
61
|
+
problem.code,
|
|
62
|
+
[...path, "unit"],
|
|
63
|
+
problem.message,
|
|
64
|
+
problem.suggestion
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return amount === void 0 ? void 0 : { amount, unit: id };
|
|
68
|
+
}
|
|
69
|
+
return error(
|
|
70
|
+
issues,
|
|
71
|
+
"invalid_type",
|
|
72
|
+
path,
|
|
73
|
+
`Expected a quantity such as "2 cups", 2, or { amount: 2, unit: "cup" }, received ${describeValue(value)}.`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
function readQuantityText(issues, text, path) {
|
|
77
|
+
const result = parseText(text.replace(/\s+/g, " ").trim());
|
|
78
|
+
if ("code" in result) {
|
|
79
|
+
return error(issues, result.code, path, result.message, result.suggestion);
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
function parseText(text) {
|
|
84
|
+
const match = matchAmount(text);
|
|
85
|
+
if (!match) {
|
|
86
|
+
const unit2 = findUnit(text);
|
|
87
|
+
return unit2 ? {
|
|
88
|
+
code: "invalid_quantity",
|
|
89
|
+
message: `${quote(text)} has no amount. Write a full quantity, e.g. "1 ${units[unit2].labels.short.one}".`
|
|
90
|
+
} : unreadable(text);
|
|
91
|
+
}
|
|
92
|
+
if ("code" in match) {
|
|
93
|
+
return match;
|
|
94
|
+
}
|
|
95
|
+
const { amount, amountText, rest: unitText } = match;
|
|
96
|
+
if (unitText === "") {
|
|
97
|
+
return { amount };
|
|
98
|
+
}
|
|
99
|
+
if (/\d/.test(unitText) || !/\p{L}/u.test(unitText)) {
|
|
100
|
+
return unreadable(text);
|
|
101
|
+
}
|
|
102
|
+
const unit = findUnit(unitText);
|
|
103
|
+
if (unit) {
|
|
104
|
+
return { amount, unit };
|
|
105
|
+
}
|
|
106
|
+
return unknownUnit(
|
|
107
|
+
unitText,
|
|
108
|
+
amountText.length <= MAX_ECHOED && /^\d+(?:\.\d+)?$/.test(amountText) ? amountText : quote(amountText)
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
function matchAmount(text, allowZero) {
|
|
112
|
+
if (/\d,\d/.test(text)) {
|
|
113
|
+
return {
|
|
114
|
+
code: "invalid_quantity",
|
|
115
|
+
message: `Couldn't read ${quote(text)}. Write decimals with a period, e.g. "1.5", and leave out thousands separators.`
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const article = ARTICLE.exec(text);
|
|
119
|
+
if (article) {
|
|
120
|
+
return { amount: 1, amountText: "1", rest: text.slice(article[0].length) };
|
|
121
|
+
}
|
|
122
|
+
if (!amountPattern) {
|
|
123
|
+
const number = String.raw`(?:\d+\s+)?\d+\s*[/⁄]\s*\d+|(?:\d+\s*)?${VULGAR}|\d+(?:\.\d+)?|\.\d+`;
|
|
124
|
+
amountPattern = new RegExp(
|
|
125
|
+
String.raw`^(${number})(?:(?:\s*[-–—]\s*|\s+to\s+)(${number}))?`,
|
|
126
|
+
"i"
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
const match = amountPattern.exec(text);
|
|
130
|
+
if (!match) {
|
|
131
|
+
return void 0;
|
|
132
|
+
}
|
|
133
|
+
const [matched, lowText = "", highText] = match;
|
|
134
|
+
const low = toNumber(lowText);
|
|
135
|
+
const high = highText === void 0 ? low : toNumber(highText);
|
|
136
|
+
if (low === Infinity || high === Infinity) {
|
|
137
|
+
return {
|
|
138
|
+
code: "invalid_amount",
|
|
139
|
+
message: `${quote(text)} has an amount too large to read. Check it for extra digits.`
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
if (!(isPositive(low) || allowZero && low === 0) || !(isPositive(high) || allowZero && high === 0)) {
|
|
143
|
+
return {
|
|
144
|
+
code: "invalid_amount",
|
|
145
|
+
message: `Expected a positive amount, received ${quote(text)}.`
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
if (low > high) {
|
|
149
|
+
const mixedHint = /^\d+$/.test(lowText) && high < 1 ? ` To write a mixed number, use ${quote(`${lowText} ${highText}`)}.` : "";
|
|
150
|
+
return {
|
|
151
|
+
code: "invalid_range",
|
|
152
|
+
message: `Range minimum (${truncateNumber(lowText)}) must be less than maximum (${truncateNumber(highText)}).${mixedHint}`
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
amount: low === high ? low : { min: low, max: high },
|
|
157
|
+
amountText: matched,
|
|
158
|
+
rest: text.slice(matched.length).trim()
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function unknownUnit(unitText, quantity) {
|
|
162
|
+
const suggestion = suggestUnit(unitText);
|
|
163
|
+
const move = quantity === void 0 ? "move the word into the name" : `move the word into the name: { name: ${quote(unitText)}, quantity: ${quantity} }`;
|
|
164
|
+
return {
|
|
165
|
+
code: "unknown_unit",
|
|
166
|
+
message: suggestion ? `Unknown unit ${quote(unitText)}. Did you mean "${suggestion}"?` : `Unknown unit ${quote(unitText)}. Use a known unit, or ${move}.`,
|
|
167
|
+
...suggestion && { suggestion }
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function truncateNumber(text) {
|
|
171
|
+
return text.length > 20 ? `${text.slice(0, 19)}\u2026` : text;
|
|
172
|
+
}
|
|
173
|
+
function unreadable(text) {
|
|
174
|
+
return {
|
|
175
|
+
code: "invalid_quantity",
|
|
176
|
+
message: text === "" ? 'Expected a quantity such as "1 \xBD cups", received an empty string.' : `Couldn't read ${quote(text)}. Use a number or fraction, e.g. "1 \xBD cups".`
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
var fractions = /* @__PURE__ */ new Map();
|
|
180
|
+
function toNumber(text) {
|
|
181
|
+
const ascii = /^(?:(\d+)\s+)?(\d+)\s*[/⁄]\s*(\d+)$/.exec(text);
|
|
182
|
+
if (ascii) {
|
|
183
|
+
const [, whole = "0", numerator, denominator] = ascii;
|
|
184
|
+
return Number(whole) + Number(numerator) / (Number(denominator) || NaN);
|
|
185
|
+
}
|
|
186
|
+
const last = text.slice(-1);
|
|
187
|
+
if (last > "9") {
|
|
188
|
+
let fraction = fractions.get(last);
|
|
189
|
+
if (fraction === void 0) {
|
|
190
|
+
const [numerator, denominator] = last.normalize("NFKD").split("\u2044");
|
|
191
|
+
fraction = Number(numerator) / Number(denominator);
|
|
192
|
+
fractions.set(last, fraction);
|
|
193
|
+
}
|
|
194
|
+
return Number(text.slice(0, -1)) + fraction;
|
|
195
|
+
}
|
|
196
|
+
return Number(text);
|
|
197
|
+
}
|
|
198
|
+
function readAmount(issues, value, path) {
|
|
199
|
+
if (value === void 0) {
|
|
200
|
+
return error(
|
|
201
|
+
issues,
|
|
202
|
+
"missing_field",
|
|
203
|
+
path,
|
|
204
|
+
'Add an amount, e.g. { amount: 2, unit: "cup" }.'
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
if (typeof value === "number") {
|
|
208
|
+
return readPositive(issues, value, path);
|
|
209
|
+
}
|
|
210
|
+
const range = asRecord(value);
|
|
211
|
+
if (range && "min" in range && "max" in range) {
|
|
212
|
+
checkFields(issues, range, { min: true, max: true }, path);
|
|
213
|
+
const min = readPositive(issues, range.min, [...path, "min"]);
|
|
214
|
+
const max = readPositive(issues, range.max, [...path, "max"]);
|
|
215
|
+
if (min === void 0 || max === void 0) {
|
|
216
|
+
return void 0;
|
|
217
|
+
}
|
|
218
|
+
if (min > max) {
|
|
219
|
+
return error(
|
|
220
|
+
issues,
|
|
221
|
+
"invalid_range",
|
|
222
|
+
path,
|
|
223
|
+
`Range minimum (${min}) must be less than maximum (${max}).`
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
return min === max ? min : { min, max };
|
|
227
|
+
}
|
|
228
|
+
return error(
|
|
229
|
+
issues,
|
|
230
|
+
"invalid_type",
|
|
231
|
+
path,
|
|
232
|
+
`Expected a number or { min, max }, received ${describeValue(value)}.`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
function readPositive(issues, value, path) {
|
|
236
|
+
if (isPositive(value) && value >= 1e-300 && value <= 1e290) {
|
|
237
|
+
return value;
|
|
238
|
+
}
|
|
239
|
+
return error(
|
|
240
|
+
issues,
|
|
241
|
+
typeof value === "number" ? "invalid_amount" : "invalid_type",
|
|
242
|
+
path,
|
|
243
|
+
isPositive(value) ? `Expected a number from 1e-300 to 1e290, received ${describeValue(value)}.` : `Expected a positive number, received ${describeValue(value)}.`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
function readTextAmount(issues, amount, path) {
|
|
247
|
+
return (typeof amount === "number" ? [amount] : [amount.min, amount.max]).every((end) => readPositive(issues, end, path) !== void 0);
|
|
248
|
+
}
|
|
249
|
+
export {
|
|
250
|
+
matchAmount,
|
|
251
|
+
parseQuantity,
|
|
252
|
+
readPositive,
|
|
253
|
+
readQuantity
|
|
254
|
+
};
|
|
255
|
+
//# sourceMappingURL=parse.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/quantity/parse.ts"],"sourcesContent":["import {\n describeValue,\n MAX_ECHOED,\n type ParseResult,\n quote,\n type RecipeIssue,\n type RecipeIssueCode,\n} from '../errors'\nimport type { Path } from '../issues'\nimport { findUnit, suggestUnit } from '../units/lookup'\nimport { units } from '../units/registry'\nimport { asRecord, checkFields, error, toResult } from '../validation'\nimport { isPositive } from './amount'\nimport type { Amount, Quantity } from './types'\n\n// ¼, ½, ¾ and ⅐ to ⅞, which decompose to their fractions, such as '½' to\n// '1⁄2'. ⅟ has no denominator, and ↉ is zero.\nconst VULGAR = '[¼-¾⅐-⅞]'\n\nconst ARTICLE = /^an?\\s+(?=\\D)/i\n\nlet amountPattern: RegExp | undefined\n\nexport type Problem = {\n code: RecipeIssueCode\n message: string\n suggestion?: string\n}\n\n/**\n * Parses quantity text, such as `'1 ½ cups'`, `'2-3 tbsp'`, `'a pinch'`, or\n * `'3'` for a bare count. Never throws.\n *\n * Accepts whole numbers, decimals, fractions (`1/2`, `½`, also with the\n * fraction slash `⁄`), mixed numbers (`1 1/2`, `1½`) and ranges (`2-3`, `2–3`,\n * `2 to 3`), each followed by an optional unit alias, and `a` or `an` for 1\n * before a unit alias, as in `'a pinch'`.\n *\n * @example\n * parseQuantity('1 ½ cups') // { success: true, data: { amount: 1.5, unit: 'cup' }, warnings: [] }\n *\n * const result = parseQuantity('2 cupz')\n * if (!result.success) {\n * result.error.message // '\"2 cupz\" has 1 problem:\\n ✖ (root) Unknown unit \"cupz\". Did you mean \"cups\"?\\nSee https://recipe-book.siraj.page/errors/invalid_quantity'\n * }\n */\nexport function parseQuantity(text: string): ParseResult<Quantity> {\n const issues: RecipeIssue[] = []\n\n if (typeof text !== 'string') {\n error(\n issues,\n 'invalid_type',\n [],\n `Expected quantity text such as \"1 ½ cups\", received ${describeValue(text)}.`,\n )\n return toResult<Quantity>('invalid_quantity', undefined, issues)\n }\n\n const quantity = readQuantityText(issues, text, [])\n return toResult('invalid_quantity', quantity, issues, quote(text))\n}\n\n/** Reads a quantity in any input form: text, a number, or `{ amount, unit }`. */\nexport function readQuantity(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): Quantity | undefined {\n if (typeof value === 'string') {\n const quantity = readQuantityText(issues, value, path)\n return quantity && readTextAmount(issues, quantity.amount, path)\n ? quantity\n : undefined\n }\n\n if (typeof value === 'number') {\n const amount = readPositive(issues, value, path)\n return amount === undefined ? undefined : { amount }\n }\n\n const record = asRecord(value)\n if (record) {\n checkFields(issues, record, { amount: true, unit: true }, path)\n\n const amount = readAmount(issues, record.amount, [...path, 'amount'])\n const { unit } = record\n\n if (unit === undefined) {\n return amount === undefined ? undefined : { amount }\n }\n\n if (typeof unit !== 'string') {\n return error(\n issues,\n 'invalid_type',\n [...path, 'unit'],\n `Expected a unit such as \"g\" or \"cup\", received ${describeValue(unit)}.`,\n )\n }\n\n const id = findUnit(unit)\n\n if (!id) {\n const problem = unknownUnit(\n unit,\n amount === undefined\n ? undefined\n : typeof amount === 'number'\n ? String(amount)\n : `{ amount: { min: ${amount.min}, max: ${amount.max} } }`,\n )\n return error(\n issues,\n problem.code,\n [...path, 'unit'],\n problem.message,\n problem.suggestion,\n )\n }\n\n return amount === undefined ? undefined : { amount, unit: id }\n }\n\n return error(\n issues,\n 'invalid_type',\n path,\n `Expected a quantity such as \"2 cups\", 2, or { amount: 2, unit: \"cup\" }, received ${describeValue(value)}.`,\n )\n}\n\nfunction readQuantityText(\n issues: RecipeIssue[],\n text: string,\n path: Path,\n): Quantity | undefined {\n const result = parseText(text.replace(/\\s+/g, ' ').trim())\n\n if ('code' in result) {\n return error(issues, result.code, path, result.message, result.suggestion)\n }\n\n return result\n}\n\nfunction parseText(text: string): Quantity | Problem {\n const match = matchAmount(text)\n\n if (!match) {\n const unit = findUnit(text)\n return unit\n ? {\n code: 'invalid_quantity',\n message: `${quote(text)} has no amount. Write a full quantity, e.g. \"1 ${units[unit].labels.short.one}\".`,\n }\n : unreadable(text)\n }\n\n if ('code' in match) {\n return match\n }\n\n const { amount, amountText, rest: unitText } = match\n\n if (unitText === '') {\n return { amount }\n }\n\n if (/\\d/.test(unitText) || !/\\p{L}/u.test(unitText)) {\n return unreadable(text)\n }\n\n const unit = findUnit(unitText)\n if (unit) {\n return { amount, unit }\n }\n\n return unknownUnit(\n unitText,\n amountText.length <= MAX_ECHOED && /^\\d+(?:\\.\\d+)?$/.test(amountText)\n ? amountText\n : quote(amountText),\n )\n}\n\n/**\n * Reads the amount that starts `text`, which is trimmed with single spaces: a\n * number, fraction, mixed number, range, or \"a\" or \"an\" for 1. A comma between\n * digits anywhere in the text, as in \"1,5\", is a problem. Otherwise returns\n * `undefined` when the text doesn't start with an amount, and a problem when\n * the amount is too large to read, isn't greater than 0 (0 is allowed with\n * `allowZero`), or is a range whose low end is above its high end. When it\n * reads one, it returns the amount, its text as written, and the text after it.\n */\nexport function matchAmount(\n text: string,\n allowZero?: boolean,\n): { amount: Amount; amountText: string; rest: string } | Problem | undefined {\n // \"1,5\" is a decimal in much of Europe and \"1,500\" a thousands separator in\n // the US, so neither is guessed.\n if (/\\d,\\d/.test(text)) {\n return {\n code: 'invalid_quantity',\n message: `Couldn't read ${quote(text)}. Write decimals with a period, e.g. \"1.5\", and leave out thousands separators.`,\n }\n }\n\n const article = ARTICLE.exec(text)\n if (article) {\n return { amount: 1, amountText: '1', rest: text.slice(article[0].length) }\n }\n\n if (!amountPattern) {\n // Alternatives are tried in order, so a fraction with an optional whole\n // number, as in \"1 1/2\", \"1/2\", \"1½\" or \"½\", comes before decimals and\n // integers.\n const number = String.raw`(?:\\d+\\s+)?\\d+\\s*[/⁄]\\s*\\d+|(?:\\d+\\s*)?${VULGAR}|\\d+(?:\\.\\d+)?|\\.\\d+`\n amountPattern = new RegExp(\n String.raw`^(${number})(?:(?:\\s*[-–—]\\s*|\\s+to\\s+)(${number}))?`,\n 'i',\n )\n }\n\n const match = amountPattern.exec(text)\n if (!match) {\n return undefined\n }\n\n const [matched, lowText = '', highText] = match\n const low = toNumber(lowText)\n const high = highText === undefined ? low : toNumber(highText)\n\n if (low === Infinity || high === Infinity) {\n return {\n code: 'invalid_amount',\n message: `${quote(text)} has an amount too large to read. Check it for extra digits.`,\n }\n }\n\n if (\n !(isPositive(low) || (allowZero && low === 0)) ||\n !(isPositive(high) || (allowZero && high === 0))\n ) {\n return {\n code: 'invalid_amount',\n message: `Expected a positive amount, received ${quote(text)}.`,\n }\n }\n\n if (low > high) {\n const mixedHint =\n /^\\d+$/.test(lowText) && high < 1\n ? ` To write a mixed number, use ${quote(`${lowText} ${highText}`)}.`\n : ''\n return {\n code: 'invalid_range',\n message: `Range minimum (${truncateNumber(lowText)}) must be less than maximum (${truncateNumber(highText!)}).${mixedHint}`,\n }\n }\n\n return {\n amount: low === high ? low : { min: low, max: high },\n amountText: matched,\n rest: text.slice(matched.length).trim(),\n }\n}\n\n/**\n * The `unknown_unit` problem. `quantity` is the line's quantity without the\n * unit, as it would be written in `RecipeInput`, for the \"move the word into\n * the name\" fix.\n */\nfunction unknownUnit(unitText: string, quantity: string | undefined): Problem {\n const suggestion = suggestUnit(unitText)\n const move =\n quantity === undefined\n ? 'move the word into the name'\n : `move the word into the name: { name: ${quote(unitText)}, quantity: ${quantity} }`\n\n return {\n code: 'unknown_unit',\n message: suggestion\n ? `Unknown unit ${quote(unitText)}. Did you mean \"${suggestion}\"?`\n : `Unknown unit ${quote(unitText)}. Use a known unit, or ${move}.`,\n ...(suggestion && { suggestion }),\n }\n}\n\nfunction truncateNumber(text: string): string {\n return text.length > 20 ? `${text.slice(0, 19)}…` : text\n}\n\nfunction unreadable(text: string): Problem {\n return {\n code: 'invalid_quantity',\n message:\n text === ''\n ? 'Expected a quantity such as \"1 ½ cups\", received an empty string.'\n : `Couldn't read ${quote(text)}. Use a number or fraction, e.g. \"1 ½ cups\".`,\n }\n}\n\n// Decomposing a glyph is slow, so each glyph's value is read once.\nconst fractions = new Map<string, number>()\n\nfunction toNumber(text: string): number {\n // The `[/⁄]` must match the one in `amountPattern`.\n const ascii = /^(?:(\\d+)\\s+)?(\\d+)\\s*[/⁄]\\s*(\\d+)$/.exec(text)\n if (ascii) {\n const [, whole = '0', numerator, denominator] = ascii\n // A zero denominator reads as NaN, not Infinity, so \"1/0\" isn't too large.\n return Number(whole) + Number(numerator) / (Number(denominator) || NaN)\n }\n\n // Amounts end in a digit or a glyph.\n const last = text.slice(-1)\n if (last > '9') {\n let fraction = fractions.get(last)\n if (fraction === undefined) {\n const [numerator, denominator] = last.normalize('NFKD').split('⁄')\n fraction = Number(numerator) / Number(denominator)\n fractions.set(last, fraction)\n }\n // Before a glyph is a whole number and any spaces, or nothing, which\n // Number reads as 0.\n return Number(text.slice(0, -1)) + fraction\n }\n\n return Number(text)\n}\n\nfunction readAmount(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): Amount | undefined {\n if (value === undefined) {\n return error(\n issues,\n 'missing_field',\n path,\n 'Add an amount, e.g. { amount: 2, unit: \"cup\" }.',\n )\n }\n\n if (typeof value === 'number') {\n return readPositive(issues, value, path)\n }\n\n const range = asRecord(value)\n if (range && 'min' in range && 'max' in range) {\n checkFields(issues, range, { min: true, max: true }, path)\n\n const min = readPositive(issues, range.min, [...path, 'min'])\n const max = readPositive(issues, range.max, [...path, 'max'])\n\n if (min === undefined || max === undefined) {\n return undefined\n }\n\n if (min > max) {\n return error(\n issues,\n 'invalid_range',\n path,\n `Range minimum (${min}) must be less than maximum (${max}).`,\n )\n }\n\n return min === max ? min : { min, max }\n }\n\n return error(\n issues,\n 'invalid_type',\n path,\n `Expected a number or { min, max }, received ${describeValue(value)}.`,\n )\n}\n\n/**\n * Reads a number from 1e-300 to 1e290. Scaling by 1e-6 to 1e6 and converting\n * between any two units in the registry keeps an amount in that range finite\n * and greater than 0.\n */\nexport function readPositive(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): number | undefined {\n if (isPositive(value) && value >= 1e-300 && value <= 1e290) {\n return value\n }\n\n return error(\n issues,\n typeof value === 'number' ? 'invalid_amount' : 'invalid_type',\n path,\n isPositive(value)\n ? `Expected a number from 1e-300 to 1e290, received ${describeValue(value)}.`\n : `Expected a positive number, received ${describeValue(value)}.`,\n )\n}\n\n/** Checks each end of an amount read from text with `readPositive`. */\nfunction readTextAmount(\n issues: RecipeIssue[],\n amount: Amount,\n path: Path,\n): boolean {\n return (\n typeof amount === 'number' ? [amount] : [amount.min, amount.max]\n ).every((end) => readPositive(issues, end, path) !== undefined)\n}\n"],"mappings":"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","names":["unit"]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// src/quantity/scale.ts
|
|
2
|
+
import {
|
|
3
|
+
checkFactor,
|
|
4
|
+
checkQuantity,
|
|
5
|
+
checkResult,
|
|
6
|
+
mapAmount
|
|
7
|
+
} from "./amount.js";
|
|
8
|
+
function scaleQuantity(quantity, factor) {
|
|
9
|
+
const amount = checkQuantity("scaleQuantity", quantity);
|
|
10
|
+
checkFactor("scaleQuantity", "factor", factor);
|
|
11
|
+
const result = multiply({ amount, unit: quantity.unit }, factor);
|
|
12
|
+
checkResult("scaleQuantity", result.amount);
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
15
|
+
function multiply(quantity, factor) {
|
|
16
|
+
const amount = mapAmount(quantity.amount, (value) => value * factor);
|
|
17
|
+
return quantity.unit === void 0 ? { amount } : { amount, unit: quantity.unit };
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
multiply,
|
|
21
|
+
scaleQuantity
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=scale.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/quantity/scale.ts"],"sourcesContent":["import type { UnitId } from '../units/registry'\nimport {\n checkFactor,\n checkQuantity,\n checkResult,\n mapAmount,\n type SameAmount,\n} from './amount'\nimport type { Amount, Quantity } from './types'\n\n/**\n * Multiplies a quantity by `factor`. Both ends of a range scale, the unit is\n * kept, and nothing is rounded.\n *\n * @throws {RecipeBookError} `invalid_argument` when `factor` is not a finite\n * number greater than 0, `quantity` isn't a quantity object, the amount is\n * not a finite number greater than 0 or a range of them, a range's `min`\n * isn't less than its `max`, or the quantity has `max` set, as a `Servings`\n * or `Yield` has. `resolveRecipe` scales those. The unit isn't checked.\n * Also thrown when an end times `factor` overflows to `Infinity` or\n * underflows to 0: past about 1.8e308, or below about 5e-324.\n *\n * @example\n * scaleQuantity({ amount: 1.5, unit: 'cup' }, 2) // { amount: 3, unit: 'cup' }\n * scaleQuantity({ amount: { min: 2, max: 3 }, unit: 'tbsp' }, 2) // { amount: { min: 4, max: 6 }, unit: 'tbsp' }\n */\nexport function scaleQuantity<A extends Amount>(\n quantity: Quantity & { amount: A },\n factor: number,\n): { amount: SameAmount<A>; unit?: UnitId | undefined } {\n const amount = checkQuantity('scaleQuantity', quantity) as A\n checkFactor('scaleQuantity', 'factor', factor)\n\n const result = multiply({ amount, unit: quantity.unit }, factor)\n checkResult('scaleQuantity', result.amount)\n return result\n}\n\n/** {@link scaleQuantity} without argument checks. */\nexport function multiply<A extends Amount>(\n quantity: Quantity & { amount: A },\n factor: number,\n): { amount: SameAmount<A>; unit?: UnitId | undefined } {\n const amount = mapAmount(quantity.amount, (value) => value * factor)\n return quantity.unit === undefined\n ? { amount }\n : { amount, unit: quantity.unit }\n}\n"],"mappings":";AACA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAmBA,SAAS,cACd,UACA,QACsD;AACtD,QAAM,SAAS,cAAc,iBAAiB,QAAQ;AACtD,cAAY,iBAAiB,UAAU,MAAM;AAE7C,QAAM,SAAS,SAAS,EAAE,QAAQ,MAAM,SAAS,KAAK,GAAG,MAAM;AAC/D,cAAY,iBAAiB,OAAO,MAAM;AAC1C,SAAO;AACT;AAGO,SAAS,SACd,UACA,QACsD;AACtD,QAAM,SAAS,UAAU,SAAS,QAAQ,CAAC,UAAU,QAAQ,MAAM;AACnE,SAAO,SAAS,SAAS,SACrB,EAAE,OAAO,IACT,EAAE,QAAQ,MAAM,SAAS,KAAK;AACpC;","names":[]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// src/recipe/duration.ts
|
|
2
|
+
var ISO_DURATION = /^P(?!$)(?:(\d+(?:[.,]\d+)?)W)?(?:(\d+(?:[.,]\d+)?)D)?(?:T(?=\d)(?:(\d+(?:[.,]\d+)?)H)?(?:(\d+(?:[.,]\d+)?)M)?(?:(\d+(?:[.,]\d+)?)S)?)?$/i;
|
|
3
|
+
var SECONDS = [604800, 86400, 3600, 60, 1];
|
|
4
|
+
function parseIsoDuration(text) {
|
|
5
|
+
const match = ISO_DURATION.exec(text.trim());
|
|
6
|
+
if (!match) {
|
|
7
|
+
return void 0;
|
|
8
|
+
}
|
|
9
|
+
let seconds = 0;
|
|
10
|
+
let fractional = false;
|
|
11
|
+
let group = 0;
|
|
12
|
+
for (const unitSeconds of SECONDS) {
|
|
13
|
+
const part = match[++group];
|
|
14
|
+
if (part === void 0) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (fractional) {
|
|
18
|
+
return void 0;
|
|
19
|
+
}
|
|
20
|
+
fractional = /[.,]/.test(part);
|
|
21
|
+
seconds += Number(part.replace(",", ".")) * unitSeconds;
|
|
22
|
+
}
|
|
23
|
+
return Number.isFinite(seconds) ? seconds / 60 : void 0;
|
|
24
|
+
}
|
|
25
|
+
export {
|
|
26
|
+
parseIsoDuration
|
|
27
|
+
};
|
|
28
|
+
//# sourceMappingURL=duration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/recipe/duration.ts"],"sourcesContent":["// Years and months are left out: their length in minutes isn't fixed.\nconst ISO_DURATION =\n /^P(?!$)(?:(\\d+(?:[.,]\\d+)?)W)?(?:(\\d+(?:[.,]\\d+)?)D)?(?:T(?=\\d)(?:(\\d+(?:[.,]\\d+)?)H)?(?:(\\d+(?:[.,]\\d+)?)M)?(?:(\\d+(?:[.,]\\d+)?)S)?)?$/i\n\nconst SECONDS = [604_800, 86_400, 3600, 60, 1]\n\n/** Minutes in an ISO 8601 duration such as `'PT1H30M'`, or `undefined` if it isn't one. */\nexport function parseIsoDuration(text: string): number | undefined {\n const match = ISO_DURATION.exec(text.trim())\n\n if (!match) {\n return undefined\n }\n\n let seconds = 0\n let fractional = false\n let group = 0\n\n for (const unitSeconds of SECONDS) {\n const part = match[++group]\n if (part === undefined) {\n continue\n }\n\n // Only the smallest component may have a fraction: PT1.5H, not PT1.5H30M.\n if (fractional) {\n return undefined\n }\n\n fractional = /[.,]/.test(part)\n seconds += Number(part.replace(',', '.')) * unitSeconds\n }\n\n return Number.isFinite(seconds) ? seconds / 60 : undefined\n}\n"],"mappings":";AACA,IAAM,eACJ;AAEF,IAAM,UAAU,CAAC,QAAS,OAAQ,MAAM,IAAI,CAAC;AAGtC,SAAS,iBAAiB,MAAkC;AACjE,QAAM,QAAQ,aAAa,KAAK,KAAK,KAAK,CAAC;AAE3C,MAAI,CAAC,OAAO;AACV,WAAO;AAAA,EACT;AAEA,MAAI,UAAU;AACd,MAAI,aAAa;AACjB,MAAI,QAAQ;AAEZ,aAAW,eAAe,SAAS;AACjC,UAAM,OAAO,MAAM,EAAE,KAAK;AAC1B,QAAI,SAAS,QAAW;AACtB;AAAA,IACF;AAGA,QAAI,YAAY;AACd,aAAO;AAAA,IACT;AAEA,iBAAa,OAAO,KAAK,IAAI;AAC7B,eAAW,OAAO,KAAK,QAAQ,KAAK,GAAG,CAAC,IAAI;AAAA,EAC9C;AAEA,SAAO,OAAO,SAAS,OAAO,IAAI,UAAU,KAAK;AACnD;","names":[]}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// src/recipe/ids.ts
|
|
2
|
+
import { quote } from "../errors.js";
|
|
3
|
+
import { issue } from "../issues.js";
|
|
4
|
+
function slugify(text) {
|
|
5
|
+
return text.normalize("NFKD").replace(/(\p{Script=Latin})\p{M}+/gu, "$1").toLowerCase().normalize("NFC").replace(/(^|[^\p{L}\p{M}])\p{M}+/gu, "$1").replace(/[^\p{L}\p{M}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "");
|
|
6
|
+
}
|
|
7
|
+
function assignIds(requests, pending) {
|
|
8
|
+
const used = /* @__PURE__ */ new Set();
|
|
9
|
+
const nextSuffix = /* @__PURE__ */ new Map();
|
|
10
|
+
const ids = requests.map(({ authored }) => {
|
|
11
|
+
if (authored === void 0 || used.has(authored)) {
|
|
12
|
+
return void 0;
|
|
13
|
+
}
|
|
14
|
+
used.add(authored);
|
|
15
|
+
return authored;
|
|
16
|
+
});
|
|
17
|
+
return requests.map((request, i) => {
|
|
18
|
+
const claimed = ids[i];
|
|
19
|
+
if (claimed !== void 0) {
|
|
20
|
+
return claimed;
|
|
21
|
+
}
|
|
22
|
+
const base = request.authored ?? request.derived;
|
|
23
|
+
let id = base;
|
|
24
|
+
if (used.has(id)) {
|
|
25
|
+
let n = nextSuffix.get(base) ?? 2;
|
|
26
|
+
while (used.has(id = `${base}-${n}`)) {
|
|
27
|
+
n++;
|
|
28
|
+
}
|
|
29
|
+
nextSuffix.set(base, n + 1);
|
|
30
|
+
}
|
|
31
|
+
used.add(id);
|
|
32
|
+
if (request.authored !== void 0) {
|
|
33
|
+
pending.push({
|
|
34
|
+
position: request.position,
|
|
35
|
+
issue: issue(
|
|
36
|
+
"warning",
|
|
37
|
+
"duplicate_id",
|
|
38
|
+
[...request.path, "id"],
|
|
39
|
+
`${quote(request.authored)} is already used; renamed to ${quote(id)}.`
|
|
40
|
+
)
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return id;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function insertPending(issues, pending) {
|
|
47
|
+
if (pending.length === 0) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const sorted = [...pending].sort((a, b) => a.position - b.position);
|
|
51
|
+
const original = issues.splice(0);
|
|
52
|
+
let next = 0;
|
|
53
|
+
for (const { position, issue: issue2 } of sorted) {
|
|
54
|
+
while (next < position) {
|
|
55
|
+
issues.push(original[next++]);
|
|
56
|
+
}
|
|
57
|
+
issues.push(issue2);
|
|
58
|
+
}
|
|
59
|
+
while (next < original.length) {
|
|
60
|
+
issues.push(original[next++]);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export {
|
|
64
|
+
assignIds,
|
|
65
|
+
insertPending,
|
|
66
|
+
slugify
|
|
67
|
+
};
|
|
68
|
+
//# sourceMappingURL=ids.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/recipe/ids.ts"],"sourcesContent":["import { quote, type RecipeIssue } from '../errors'\nimport { issue, type Path } from '../issues'\n\n/**\n * An id to assign. `position` is where the item's issues start in the issue\n * list, so a `duplicate_id` warning can be placed in input order.\n */\nexport type IdRequest = {\n authored: string | undefined\n derived: string\n path: Path\n position: number\n}\n\nexport type PendingIssue = { position: number; issue: RecipeIssue }\n\n/**\n * Lowercase words joined by hyphens, without accents on Latin letters:\n * \"Crème fraîche\" → \"creme-fraiche\", \"わさび\" → \"わさび\".\n */\nexport function slugify(text: string): string {\n return (\n text\n .normalize('NFKD')\n // Marks in other scripts are part of the letter: dropping them turns わさび into わさひ.\n .replace(/(\\p{Script=Latin})\\p{M}+/gu, '$1')\n // Lowercase before NFC: some Greek capitals with marks, such as Ϊ́, have no precomposed form but their lowercase does.\n .toLowerCase()\n .normalize('NFC')\n // A mark with no letter before it, such as the variation selector in ❤️, would be invisible.\n .replace(/(^|[^\\p{L}\\p{M}])\\p{M}+/gu, '$1')\n .replace(/[^\\p{L}\\p{M}\\p{N}]+/gu, '-')\n .replace(/^-+|-+$/g, '')\n )\n}\n\n/**\n * Gives each request a unique id. Authored ids are claimed first, so a derived\n * id never takes one; a repeated authored id is renamed with a warning. Taken\n * ids get the next free suffix: `egg`, `egg-2`, `egg-3`.\n */\nexport function assignIds(\n requests: readonly IdRequest[],\n pending: PendingIssue[],\n): string[] {\n const used = new Set<string>()\n const nextSuffix = new Map<string, number>()\n const ids = requests.map(({ authored }) => {\n if (authored === undefined || used.has(authored)) {\n return undefined\n }\n used.add(authored)\n return authored\n })\n\n return requests.map((request, i) => {\n const claimed = ids[i]\n if (claimed !== undefined) {\n return claimed\n }\n\n const base = request.authored ?? request.derived\n let id = base\n if (used.has(id)) {\n // Resuming from the last suffix keeps many repeats of one id linear.\n let n = nextSuffix.get(base) ?? 2\n while (used.has((id = `${base}-${n}`))) {\n n++\n }\n nextSuffix.set(base, n + 1)\n }\n used.add(id)\n\n if (request.authored !== undefined) {\n pending.push({\n position: request.position,\n issue: issue(\n 'warning',\n 'duplicate_id',\n [...request.path, 'id'],\n `${quote(request.authored)} is already used; renamed to ${quote(id)}.`,\n ),\n })\n }\n\n return id\n })\n}\n\n/** Inserts pending issues at their positions, keeping input order. */\nexport function insertPending(\n issues: RecipeIssue[],\n pending: readonly PendingIssue[],\n): void {\n if (pending.length === 0) {\n return\n }\n\n const sorted = [...pending].sort((a, b) => a.position - b.position)\n const original = issues.splice(0)\n let next = 0\n for (const { position, issue } of sorted) {\n while (next < position) {\n issues.push(original[next++]!)\n }\n issues.push(issue)\n }\n while (next < original.length) {\n issues.push(original[next++]!)\n }\n}\n"],"mappings":";AAAA,SAAS,aAA+B;AACxC,SAAS,aAAwB;AAmB1B,SAAS,QAAQ,MAAsB;AAC5C,SACE,KACG,UAAU,MAAM,EAEhB,QAAQ,8BAA8B,IAAI,EAE1C,YAAY,EACZ,UAAU,KAAK,EAEf,QAAQ,6BAA6B,IAAI,EACzC,QAAQ,yBAAyB,GAAG,EACpC,QAAQ,YAAY,EAAE;AAE7B;AAOO,SAAS,UACd,UACA,SACU;AACV,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,aAAa,oBAAI,IAAoB;AAC3C,QAAM,MAAM,SAAS,IAAI,CAAC,EAAE,SAAS,MAAM;AACzC,QAAI,aAAa,UAAa,KAAK,IAAI,QAAQ,GAAG;AAChD,aAAO;AAAA,IACT;AACA,SAAK,IAAI,QAAQ;AACjB,WAAO;AAAA,EACT,CAAC;AAED,SAAO,SAAS,IAAI,CAAC,SAAS,MAAM;AAClC,UAAM,UAAU,IAAI,CAAC;AACrB,QAAI,YAAY,QAAW;AACzB,aAAO;AAAA,IACT;AAEA,UAAM,OAAO,QAAQ,YAAY,QAAQ;AACzC,QAAI,KAAK;AACT,QAAI,KAAK,IAAI,EAAE,GAAG;AAEhB,UAAI,IAAI,WAAW,IAAI,IAAI,KAAK;AAChC,aAAO,KAAK,IAAK,KAAK,GAAG,IAAI,IAAI,CAAC,EAAG,GAAG;AACtC;AAAA,MACF;AACA,iBAAW,IAAI,MAAM,IAAI,CAAC;AAAA,IAC5B;AACA,SAAK,IAAI,EAAE;AAEX,QAAI,QAAQ,aAAa,QAAW;AAClC,cAAQ,KAAK;AAAA,QACX,UAAU,QAAQ;AAAA,QAClB,OAAO;AAAA,UACL;AAAA,UACA;AAAA,UACA,CAAC,GAAG,QAAQ,MAAM,IAAI;AAAA,UACtB,GAAG,MAAM,QAAQ,QAAQ,CAAC,gCAAgC,MAAM,EAAE,CAAC;AAAA,QACrE;AAAA,MACF,CAAC;AAAA,IACH;AAEA,WAAO;AAAA,EACT,CAAC;AACH;AAGO,SAAS,cACd,QACA,SACM;AACN,MAAI,QAAQ,WAAW,GAAG;AACxB;AAAA,EACF;AAEA,QAAM,SAAS,CAAC,GAAG,OAAO,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,WAAW,EAAE,QAAQ;AAClE,QAAM,WAAW,OAAO,OAAO,CAAC;AAChC,MAAI,OAAO;AACX,aAAW,EAAE,UAAU,OAAAA,OAAM,KAAK,QAAQ;AACxC,WAAO,OAAO,UAAU;AACtB,aAAO,KAAK,SAAS,MAAM,CAAE;AAAA,IAC/B;AACA,WAAO,KAAKA,MAAK;AAAA,EACnB;AACA,SAAO,OAAO,SAAS,QAAQ;AAC7B,WAAO,KAAK,SAAS,MAAM,CAAE;AAAA,EAC/B;AACF;","names":["issue"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// src/recipe/infer.ts
|
|
2
|
+
import { units } from "../units/registry.js";
|
|
3
|
+
function inferSystem(sections) {
|
|
4
|
+
const counts = { metric: 0, us: 0 };
|
|
5
|
+
let first;
|
|
6
|
+
for (const { items } of sections) {
|
|
7
|
+
for (const { quantity } of items) {
|
|
8
|
+
const systems = quantity?.unit ? units[quantity.unit].systems : [];
|
|
9
|
+
if (systems.length === 1) {
|
|
10
|
+
const system = systems[0];
|
|
11
|
+
counts[system]++;
|
|
12
|
+
first ?? (first = system);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (counts.metric !== counts.us) {
|
|
17
|
+
return counts.metric > counts.us ? "metric" : "us";
|
|
18
|
+
}
|
|
19
|
+
return first;
|
|
20
|
+
}
|
|
21
|
+
export {
|
|
22
|
+
inferSystem
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=infer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/recipe/infer.ts"],"sourcesContent":["import { units, type System } from '../units/registry'\nimport type { IngredientSection } from './types'\n\n/**\n * The system a recipe was written in. Counts the ingredients whose primary\n * quantity has a unit native to exactly one system; the system with more\n * ingredients wins, and on a tie the first such ingredient decides.\n * Equivalents don't count.\n *\n * @returns `undefined` for a system-neutral recipe: only counts, tsp, and tbsp.\n */\nexport function inferSystem(\n sections: readonly IngredientSection[],\n): System | undefined {\n const counts: Record<System, number> = { metric: 0, us: 0 }\n let first: System | undefined\n\n for (const { items } of sections) {\n for (const { quantity } of items) {\n const systems: readonly System[] = quantity?.unit\n ? units[quantity.unit].systems\n : []\n\n if (systems.length === 1) {\n const system = systems[0]!\n counts[system]++\n first ??= system\n }\n }\n }\n\n if (counts.metric !== counts.us) {\n return counts.metric > counts.us ? 'metric' : 'us'\n }\n\n return first\n}\n"],"mappings":";AAAA,SAAS,aAA0B;AAW5B,SAAS,YACd,UACoB;AACpB,QAAM,SAAiC,EAAE,QAAQ,GAAG,IAAI,EAAE;AAC1D,MAAI;AAEJ,aAAW,EAAE,MAAM,KAAK,UAAU;AAChC,eAAW,EAAE,SAAS,KAAK,OAAO;AAChC,YAAM,UAA6B,UAAU,OACzC,MAAM,SAAS,IAAI,EAAE,UACrB,CAAC;AAEL,UAAI,QAAQ,WAAW,GAAG;AACxB,cAAM,SAAS,QAAQ,CAAC;AACxB,eAAO,MAAM;AACb,0BAAU;AAAA,MACZ;AAAA,IACF;AAAA,EACF;AAEA,MAAI,OAAO,WAAW,OAAO,IAAI;AAC/B,WAAO,OAAO,SAAS,OAAO,KAAK,WAAW;AAAA,EAChD;AAEA,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// src/recipe/nutrient-units.ts
|
|
2
|
+
var NUTRIENT_UNITS = {
|
|
3
|
+
calories: "kcal",
|
|
4
|
+
fat: "g",
|
|
5
|
+
saturatedFat: "g",
|
|
6
|
+
transFat: "g",
|
|
7
|
+
unsaturatedFat: "g",
|
|
8
|
+
carbohydrates: "g",
|
|
9
|
+
sugar: "g",
|
|
10
|
+
fiber: "g",
|
|
11
|
+
protein: "g",
|
|
12
|
+
sodium: "mg",
|
|
13
|
+
cholesterol: "mg"
|
|
14
|
+
};
|
|
15
|
+
export {
|
|
16
|
+
NUTRIENT_UNITS
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=nutrient-units.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/recipe/nutrient-units.ts"],"sourcesContent":["import type { NutritionInput } from './input'\nimport type { NutrientUnit } from './nutrition'\n\nexport type NutrientKey = Exclude<keyof NutritionInput, 'servingSize'>\n\n// Kept out of parse.ts: toJsonLd reads it and React's JsonLd part imports\n// toJsonLd, so any page with React parts can load this module, and bundlers\n// that include whole modules would load the parser with it.\nexport const NUTRIENT_UNITS: Readonly<Record<NutrientKey, NutrientUnit>> = {\n calories: 'kcal',\n fat: 'g',\n saturatedFat: 'g',\n transFat: 'g',\n unsaturatedFat: 'g',\n carbohydrates: 'g',\n sugar: 'g',\n fiber: 'g',\n protein: 'g',\n sodium: 'mg',\n cholesterol: 'mg',\n}\n"],"mappings":";AAQO,IAAM,iBAA8D;AAAA,EACzE,UAAU;AAAA,EACV,KAAK;AAAA,EACL,cAAc;AAAA,EACd,UAAU;AAAA,EACV,gBAAgB;AAAA,EAChB,eAAe;AAAA,EACf,OAAO;AAAA,EACP,OAAO;AAAA,EACP,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,aAAa;AACf;","names":[]}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// src/recipe/nutrition.ts
|
|
2
|
+
import { matchAmount } from "../quantity/parse.js";
|
|
3
|
+
import { findUnit } from "../units/lookup.js";
|
|
4
|
+
import { lookupUnit } from "../units/registry.js";
|
|
5
|
+
function parseNutrient(text, unit) {
|
|
6
|
+
const match = matchAmount(text.replace(/\s+/g, " "), true);
|
|
7
|
+
if (match && "code" in match) {
|
|
8
|
+
return match.code === "invalid_quantity" ? match : void 0;
|
|
9
|
+
}
|
|
10
|
+
const amount = match && typeof match.amount === "number" ? convert(match.amount, match.rest, unit) : void 0;
|
|
11
|
+
return Number.isFinite(amount) ? amount : void 0;
|
|
12
|
+
}
|
|
13
|
+
function convert(amount, unitText, unit) {
|
|
14
|
+
if (unitText === "") {
|
|
15
|
+
return amount;
|
|
16
|
+
}
|
|
17
|
+
if (unit === "kcal") {
|
|
18
|
+
return /^(?:k?cals?|(?:kilo)?calories?)\.?$/i.test(unitText) ? amount : /^(?:kj|kilojoules?)\.?$/i.test(unitText) ? amount / 4.184 : void 0;
|
|
19
|
+
}
|
|
20
|
+
const mass = lookupUnit(findUnit(unitText));
|
|
21
|
+
return mass?.dimension === "mass" && mass.toBase !== void 0 ? amount * (unit === "mg" ? mass.toBase * 1e3 : mass.toBase) : void 0;
|
|
22
|
+
}
|
|
23
|
+
export {
|
|
24
|
+
parseNutrient
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=nutrition.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/recipe/nutrition.ts"],"sourcesContent":["import { matchAmount, type Problem } from '../quantity/parse'\nimport { findUnit } from '../units/lookup'\nimport { lookupUnit } from '../units/registry'\n\nexport type NutrientUnit = 'kcal' | 'g' | 'mg'\n\n/**\n * The amount in `unit` that nutrient text such as `'250 kcal'` or `'1.2 g'`\n * names, a problem if its number has a comma in it, or `undefined` if it isn't\n * readable as one. A number without a unit is already in `unit`. kJ convert to\n * kcal at 4.184 kJ per kcal, and masses convert through the unit registry.\n */\nexport function parseNutrient(\n text: string,\n unit: NutrientUnit,\n): number | Problem | undefined {\n const match = matchAmount(text.replace(/\\s+/g, ' '), true)\n\n if (match && 'code' in match) {\n // Keep only the comma problem. The others are worded for positive amounts\n // and ranges, which don't fit a nutrient.\n return match.code === 'invalid_quantity' ? match : undefined\n }\n\n const amount =\n match && typeof match.amount === 'number'\n ? convert(match.amount, match.rest, unit)\n : undefined\n\n return Number.isFinite(amount) ? amount : undefined\n}\n\nfunction convert(\n amount: number,\n unitText: string,\n unit: NutrientUnit,\n): number | undefined {\n if (unitText === '') {\n return amount\n }\n\n if (unit === 'kcal') {\n return /^(?:k?cals?|(?:kilo)?calories?)\\.?$/i.test(unitText)\n ? amount\n : /^(?:kj|kilojoules?)\\.?$/i.test(unitText)\n ? amount / 4.184\n : undefined\n }\n\n const mass = lookupUnit(findUnit(unitText))\n\n return mass?.dimension === 'mass' && mass.toBase !== undefined\n ? amount * (unit === 'mg' ? mass.toBase * 1000 : mass.toBase)\n : undefined\n}\n"],"mappings":";AAAA,SAAS,mBAAiC;AAC1C,SAAS,gBAAgB;AACzB,SAAS,kBAAkB;AAUpB,SAAS,cACd,MACA,MAC8B;AAC9B,QAAM,QAAQ,YAAY,KAAK,QAAQ,QAAQ,GAAG,GAAG,IAAI;AAEzD,MAAI,SAAS,UAAU,OAAO;AAG5B,WAAO,MAAM,SAAS,qBAAqB,QAAQ;AAAA,EACrD;AAEA,QAAM,SACJ,SAAS,OAAO,MAAM,WAAW,WAC7B,QAAQ,MAAM,QAAQ,MAAM,MAAM,IAAI,IACtC;AAEN,SAAO,OAAO,SAAS,MAAM,IAAI,SAAS;AAC5C;AAEA,SAAS,QACP,QACA,UACA,MACoB;AACpB,MAAI,aAAa,IAAI;AACnB,WAAO;AAAA,EACT;AAEA,MAAI,SAAS,QAAQ;AACnB,WAAO,uCAAuC,KAAK,QAAQ,IACvD,SACA,2BAA2B,KAAK,QAAQ,IACtC,SAAS,QACT;AAAA,EACR;AAEA,QAAM,OAAO,WAAW,SAAS,QAAQ,CAAC;AAE1C,SAAO,MAAM,cAAc,UAAU,KAAK,WAAW,SACjD,UAAU,SAAS,OAAO,KAAK,SAAS,MAAO,KAAK,UACpD;AACN;","names":[]}
|