@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/units/convert.ts"],"sourcesContent":["import { RecipeBookError } from '../errors'\nimport {\n checkQuantity,\n checkResult,\n mapAmount,\n type SameAmount,\n} from '../quantity/amount'\nimport type { Amount, Quantity } from '../quantity/types'\nimport { getUnit, unknownUnitError } from './lookup'\nimport {\n lookupUnit,\n type System,\n type UnitDefinition,\n type UnitId,\n} from './registry'\nimport { checkSystem, inSystem } from './system'\n\n/**\n * Converts a quantity to another unit of the same dimension. Both ends of a\n * range convert. Nothing is rounded, so the amount can be off in its last\n * digit: 5 tbsp is 14.999999999999998 tsp.\n *\n * @throws {RecipeBookError} `incompatible_units` across dimensions (cup to g),\n * between a count unit and a different unit, or for a bare count. Check with\n * {@link canConvert} first when the units come from data.\n * @throws {RecipeBookError} `unknown_unit` when either unit is not a unit id.\n * @throws {RecipeBookError} `invalid_argument` when `quantity` isn't a quantity object, the amount is not a finite number greater than 0 or a range of them, a range's `min` isn't less than its `max`, the quantity has `max` set, as a `Servings` or `Yield` has, or the result overflows to `Infinity` or underflows to 0, which only an amount above about 1e300 or below about 1e-300 can do.\n *\n * @example\n * convert({ amount: 1, unit: 'cup' }, 'ml') // { amount: 236.5882365, unit: 'ml' }\n * convert({ amount: { min: 2, max: 3 }, unit: 'tbsp' }, 'tsp') // { amount: { min: 6, max: 9 }, unit: 'tsp' }\n */\nexport function convert<A extends Amount, To extends UnitId>(\n quantity: Quantity & { amount: A },\n to: To,\n): { amount: SameAmount<A>; unit: To } {\n const amount = checkQuantity('convert', quantity)\n const target = getUnit(to)\n const source =\n quantity.unit === undefined ? undefined : getUnit(quantity.unit)\n\n if (source?.id === target.id) {\n return { amount: amount as SameAmount<A>, unit: to }\n }\n if (\n source?.toBase === undefined ||\n target.toBase === undefined ||\n source.dimension !== target.dimension\n ) {\n throw incompatibleUnits(source, target)\n }\n\n const fromBase = source.toBase\n const toBase = target.toBase\n return {\n amount: checkResult(\n 'convert',\n mapAmount(amount, (value) => (value * fromBase) / toBase),\n ) as SameAmount<A>,\n unit: to,\n }\n}\n\n/**\n * True when {@link convert} can convert between the two units: same unit, or\n * mass to mass, or volume to volume. Never throws, so it is safe on untrusted data.\n *\n * @example\n * canConvert('cup', 'ml') // true\n * canConvert('cup', 'g') // false\n * canConvert(undefined, 'g') // false: a bare count has no size\n */\nexport function canConvert(from: UnitId | undefined, to: UnitId): boolean {\n const source = lookupUnit(from)\n const target = lookupUnit(to)\n\n if (!source || !target) {\n return false\n }\n\n // Only count units lack `toBase`, so a target with the source's dimension has one.\n return (\n source.id === target.id ||\n (source.toBase !== undefined && source.dimension === target.dimension)\n )\n}\n\n/**\n * Expresses a quantity in a measurement system, choosing the display unit\n * from that system's ladder: g and kg, ml and l, oz and lb, or tsp, tbsp, and\n * cup. Nothing is rounded, so the amount can be off in its last digit, as with\n * {@link convert}.\n *\n * Returns the quantity unchanged when its unit is already native to `system`\n * (tsp and tbsp are native to both), when it is a count unit, or when it has\n * no unit.\n *\n * @throws {RecipeBookError} `invalid_argument` for an unknown system, an invalid amount, a quantity with `max` set, as a `Servings` or `Yield` has, or a result that overflows to `Infinity` or underflows to 0, which only an amount above about 1e300 or below about 1e-300 can do.\n * @throws {RecipeBookError} `unknown_unit` when the unit is not a unit id.\n *\n * @example\n * toSystem({ amount: 250, unit: 'g' }, 'us') // { amount: 8.818…, unit: 'oz' }\n * toSystem({ amount: 0.5, unit: 'cup' }, 'metric') // { amount: 118.29…, unit: 'ml' }\n * toSystem({ amount: 3, unit: 'tsp' }, 'metric') // unchanged\n */\nexport function toSystem<A extends Amount>(\n quantity: Quantity & { amount: A },\n system: System,\n): { amount: SameAmount<A>; unit?: UnitId | undefined } {\n const amount = checkQuantity('toSystem', quantity)\n checkSystem('toSystem', system)\n const { unit } = quantity\n\n if (unit !== undefined && !lookupUnit(unit)) {\n throw unknownUnitError(unit)\n }\n\n const checked = { amount, unit }\n const result = inSystem(checked, system)\n checkResult('toSystem', result.amount)\n return (result === checked ? quantity : result) as {\n amount: SameAmount<A>\n unit?: UnitId | undefined\n }\n}\n\nfunction incompatibleUnits(\n source: UnitDefinition | undefined,\n target: UnitDefinition,\n): RecipeBookError {\n const example =\n target.dimension === 'mass'\n ? '120 g'\n : target.dimension === 'volume'\n ? '250 ml'\n : `1 ${target.id}`\n const fix = `Add an equivalent to the ingredient, e.g. equivalents: [\"${example}\"].`\n\n if (!source) {\n return new RecipeBookError(\n 'incompatible_units',\n `Cannot convert a quantity without a unit to ${target.id}; a bare count such as \"2 eggs\" has no fixed size. ${fix}`,\n )\n }\n\n const countUnit = [source, target].find((unit) => unit.toBase === undefined)\n if (countUnit) {\n return new RecipeBookError(\n 'incompatible_units',\n `Cannot convert ${source.id} to ${target.id}; ${countUnit.id} is a count unit with no fixed size. ${fix}`,\n )\n }\n\n return new RecipeBookError(\n 'incompatible_units',\n `Cannot convert ${source.id} (${source.dimension}) to ${target.id} (${target.dimension}). ${fix}`,\n )\n}\n"],"mappings":";AAAA,SAAS,uBAAuB;AAChC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAEP,SAAS,SAAS,wBAAwB;AAC1C;AAAA,EACE;AAAA,OAIK;AACP,SAAS,aAAa,gBAAgB;AAiB/B,SAAS,QACd,UACA,IACqC;AACrC,QAAM,SAAS,cAAc,WAAW,QAAQ;AAChD,QAAM,SAAS,QAAQ,EAAE;AACzB,QAAM,SACJ,SAAS,SAAS,SAAY,SAAY,QAAQ,SAAS,IAAI;AAEjE,MAAI,QAAQ,OAAO,OAAO,IAAI;AAC5B,WAAO,EAAE,QAAiC,MAAM,GAAG;AAAA,EACrD;AACA,MACE,QAAQ,WAAW,UACnB,OAAO,WAAW,UAClB,OAAO,cAAc,OAAO,WAC5B;AACA,UAAM,kBAAkB,QAAQ,MAAM;AAAA,EACxC;AAEA,QAAM,WAAW,OAAO;AACxB,QAAM,SAAS,OAAO;AACtB,SAAO;AAAA,IACL,QAAQ;AAAA,MACN;AAAA,MACA,UAAU,QAAQ,CAAC,UAAW,QAAQ,WAAY,MAAM;AAAA,IAC1D;AAAA,IACA,MAAM;AAAA,EACR;AACF;AAWO,SAAS,WAAW,MAA0B,IAAqB;AACxE,QAAM,SAAS,WAAW,IAAI;AAC9B,QAAM,SAAS,WAAW,EAAE;AAE5B,MAAI,CAAC,UAAU,CAAC,QAAQ;AACtB,WAAO;AAAA,EACT;AAGA,SACE,OAAO,OAAO,OAAO,MACpB,OAAO,WAAW,UAAa,OAAO,cAAc,OAAO;AAEhE;AAoBO,SAAS,SACd,UACA,QACsD;AACtD,QAAM,SAAS,cAAc,YAAY,QAAQ;AACjD,cAAY,YAAY,MAAM;AAC9B,QAAM,EAAE,KAAK,IAAI;AAEjB,MAAI,SAAS,UAAa,CAAC,WAAW,IAAI,GAAG;AAC3C,UAAM,iBAAiB,IAAI;AAAA,EAC7B;AAEA,QAAM,UAAU,EAAE,QAAQ,KAAK;AAC/B,QAAM,SAAS,SAAS,SAAS,MAAM;AACvC,cAAY,YAAY,OAAO,MAAM;AACrC,SAAQ,WAAW,UAAU,WAAW;AAI1C;AAEA,SAAS,kBACP,QACA,QACiB;AACjB,QAAM,UACJ,OAAO,cAAc,SACjB,UACA,OAAO,cAAc,WACnB,WACA,KAAK,OAAO,EAAE;AACtB,QAAM,MAAM,4DAA4D,OAAO;AAE/E,MAAI,CAAC,QAAQ;AACX,WAAO,IAAI;AAAA,MACT;AAAA,MACA,+CAA+C,OAAO,EAAE,sDAAsD,GAAG;AAAA,IACnH;AAAA,EACF;AAEA,QAAM,YAAY,CAAC,QAAQ,MAAM,EAAE,KAAK,CAAC,SAAS,KAAK,WAAW,MAAS;AAC3E,MAAI,WAAW;AACb,WAAO,IAAI;AAAA,MACT;AAAA,MACA,kBAAkB,OAAO,EAAE,OAAO,OAAO,EAAE,KAAK,UAAU,EAAE,wCAAwC,GAAG;AAAA,IACzG;AAAA,EACF;AAEA,SAAO,IAAI;AAAA,IACT;AAAA,IACA,kBAAkB,OAAO,EAAE,KAAK,OAAO,SAAS,QAAQ,OAAO,EAAE,KAAK,OAAO,SAAS,MAAM,GAAG;AAAA,EACjG;AACF;","names":[]}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// src/units/lookup.ts
|
|
2
|
+
import { describeValue, quote, RecipeBookError } from "../errors.js";
|
|
3
|
+
import { hasOwn } from "../issues.js";
|
|
4
|
+
import { closestMatch } from "../suggest.js";
|
|
5
|
+
import {
|
|
6
|
+
lookupUnit,
|
|
7
|
+
units
|
|
8
|
+
} from "./registry.js";
|
|
9
|
+
var aliasIndex;
|
|
10
|
+
var CASE_SENSITIVE = /^[Ttc]$/;
|
|
11
|
+
function foldKey(key) {
|
|
12
|
+
const lower = key.toLowerCase();
|
|
13
|
+
return lower === "t" || lower === "c" ? key : lower;
|
|
14
|
+
}
|
|
15
|
+
function findUnit(text) {
|
|
16
|
+
if (typeof text !== "string") {
|
|
17
|
+
return void 0;
|
|
18
|
+
}
|
|
19
|
+
const key = unitKey(text);
|
|
20
|
+
if (!aliasIndex) {
|
|
21
|
+
const index = /* @__PURE__ */ new Map();
|
|
22
|
+
for (const unit of Object.values(units)) {
|
|
23
|
+
for (const alias of unit.aliases) {
|
|
24
|
+
index.set(foldKey(unitKey(alias)), unit.id);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
aliasIndex = index;
|
|
28
|
+
}
|
|
29
|
+
return aliasIndex.get(key) ?? aliasIndex.get(foldKey(key));
|
|
30
|
+
}
|
|
31
|
+
function unitKey(text) {
|
|
32
|
+
return text.trim().replace(/\s+/g, " ").replace(/(\p{L}) ?\.(?= |$)/gu, "$1");
|
|
33
|
+
}
|
|
34
|
+
function getUnit(id) {
|
|
35
|
+
if (!lookupUnit(id)) {
|
|
36
|
+
throw unknownUnitError(id);
|
|
37
|
+
}
|
|
38
|
+
return units[id];
|
|
39
|
+
}
|
|
40
|
+
function unknownUnitError(value) {
|
|
41
|
+
if (typeof value !== "string") {
|
|
42
|
+
return new RecipeBookError(
|
|
43
|
+
"unknown_unit",
|
|
44
|
+
`Expected a unit id such as "g", "ml", or "cup", received ${describeValue(value)}.`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
const name = value.toLowerCase();
|
|
48
|
+
if (hasOwn(SYSTEM_NAMES, name)) {
|
|
49
|
+
return new RecipeBookError(
|
|
50
|
+
"unknown_unit",
|
|
51
|
+
`${quote(value)} is a measurement system, not a unit. To convert a quantity into a system, use toSystem(quantity, "${SYSTEM_NAMES[name]}").`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
const id = findUnit(value);
|
|
55
|
+
if (id) {
|
|
56
|
+
return new RecipeBookError(
|
|
57
|
+
"unknown_unit",
|
|
58
|
+
`${quote(value)} is not a unit id. Use the id "${id}"; findUnit(${quote(value)}) converts author text to ids.`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
const suggestion = suggestUnit(value);
|
|
62
|
+
return new RecipeBookError(
|
|
63
|
+
"unknown_unit",
|
|
64
|
+
suggestion ? (
|
|
65
|
+
// The caller wants an id, so "cupz" suggests "cup" rather than the alias "cups".
|
|
66
|
+
`Unknown unit ${quote(value)}. Did you mean "${findUnit(suggestion)}"?`
|
|
67
|
+
) : `Unknown unit ${quote(value)}. Use a unit id such as "g", "ml", or "cup".`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
var SYSTEM_NAMES = {
|
|
71
|
+
metric: "metric",
|
|
72
|
+
us: "us"
|
|
73
|
+
};
|
|
74
|
+
function suggestUnit(text) {
|
|
75
|
+
return closestMatch(
|
|
76
|
+
unitKey(text).replace(/(^|\P{P})\p{P}+$/u, "$1"),
|
|
77
|
+
Object.values(units).flatMap((unit) => unit.aliases).filter((alias) => !CASE_SENSITIVE.test(alias))
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
export {
|
|
81
|
+
findUnit,
|
|
82
|
+
getUnit,
|
|
83
|
+
suggestUnit,
|
|
84
|
+
unknownUnitError
|
|
85
|
+
};
|
|
86
|
+
//# sourceMappingURL=lookup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/units/lookup.ts"],"sourcesContent":["import { describeValue, quote, RecipeBookError } from '../errors'\nimport { hasOwn } from '../issues'\nimport { closestMatch } from '../suggest'\nimport {\n lookupUnit,\n type UnitAlias,\n type UnitDefinition,\n type UnitId,\n type UnitIdOf,\n units,\n} from './registry'\n\nlet aliasIndex: ReadonlyMap<string, UnitId> | undefined\n\n// 'T' (tablespoon) and 't' (teaspoon) differ only by case, and 'C' is more\n// often Celsius than cup.\nconst CASE_SENSITIVE = /^[Ttc]$/\n\nfunction foldKey(key: string): string {\n const lower = key.toLowerCase()\n return lower === 't' || lower === 'c' ? key : lower\n}\n\n/**\n * Finds the unit that author text names. Surrounding whitespace is ignored,\n * inner whitespace is collapsed, a period that ends a word is dropped, and\n * case is ignored except for `'T'` (tbsp), `'t'` (tsp), and `'c'` (cup).\n *\n * @returns The unit id, or `undefined` when the text names no known unit.\n *\n * @example\n * findUnit('Tablespoons') // 'tbsp'\n * findUnit('T') // 'tbsp'\n * findUnit('fl. oz.') // 'fl-oz'\n * findUnit('knob') // undefined\n */\nexport function findUnit<Alias extends UnitAlias>(text: Alias): UnitIdOf<Alias>\nexport function findUnit<Alias extends UnitAlias>(\n text: `${Alias}.`,\n): UnitIdOf<Alias>\nexport function findUnit(text: string): UnitId | undefined\nexport function findUnit(text: string): UnitId | undefined {\n if (typeof text !== 'string') {\n return undefined\n }\n\n const key = unitKey(text)\n if (!aliasIndex) {\n const index = new Map<string, UnitId>()\n for (const unit of Object.values(units)) {\n for (const alias of unit.aliases) {\n index.set(foldKey(unitKey(alias)), unit.id)\n }\n }\n aliasIndex = index\n }\n\n // Folding alone finds the same unit. Trying the text as written first skips\n // lower-casing it for the usual spellings.\n return aliasIndex.get(key) ?? aliasIndex.get(foldKey(key))\n}\n\nfunction unitKey(text: string): string {\n return text\n .trim()\n .replace(/\\s+/g, ' ')\n .replace(/(\\p{L}) ?\\.(?= |$)/gu, '$1')\n}\n\n/**\n * Returns the registry entry for a unit id.\n *\n * @throws {RecipeBookError} `unknown_unit` when `id` is not a unit id.\n *\n * @example\n * getUnit('cup').toBase // 236.5882365\n */\nexport function getUnit<Id extends UnitId>(\n id: Id,\n): UnitDefinition & { readonly id: Id } {\n if (!lookupUnit(id)) {\n throw unknownUnitError(id)\n }\n\n return units[id]\n}\n\n/** Builds the `unknown_unit` error for a value that should have been a unit id. */\nexport function unknownUnitError(value: unknown): RecipeBookError {\n if (typeof value !== 'string') {\n return new RecipeBookError(\n 'unknown_unit',\n `Expected a unit id such as \"g\", \"ml\", or \"cup\", received ${describeValue(value)}.`,\n )\n }\n\n const name = value.toLowerCase()\n if (hasOwn(SYSTEM_NAMES, name)) {\n return new RecipeBookError(\n 'unknown_unit',\n `${quote(value)} is a measurement system, not a unit. To convert a quantity into a system, use toSystem(quantity, \"${SYSTEM_NAMES[name]}\").`,\n )\n }\n\n const id = findUnit(value)\n if (id) {\n return new RecipeBookError(\n 'unknown_unit',\n `${quote(value)} is not a unit id. Use the id \"${id}\"; findUnit(${quote(value)}) converts author text to ids.`,\n )\n }\n\n const suggestion = suggestUnit(value)\n return new RecipeBookError(\n 'unknown_unit',\n suggestion\n ? // The caller wants an id, so \"cupz\" suggests \"cup\" rather than the alias \"cups\".\n `Unknown unit ${quote(value)}. Did you mean \"${findUnit(suggestion)}\"?`\n : `Unknown unit ${quote(value)}. Use a unit id such as \"g\", \"ml\", or \"cup\".`,\n )\n}\n\nconst SYSTEM_NAMES: Readonly<Record<string, string>> = {\n metric: 'metric',\n us: 'us',\n}\n\n/** The closest known alias to a misspelled unit, for \"did you mean\" hints. */\nexport function suggestUnit(text: string): UnitAlias | undefined {\n // A case-sensitive alias would only be suggested for another case of it,\n // which is the spelling it rejects.\n return closestMatch(\n unitKey(text).replace(/(^|\\P{P})\\p{P}+$/u, '$1'),\n Object.values(units)\n .flatMap((unit) => unit.aliases)\n .filter((alias) => !CASE_SENSITIVE.test(alias)),\n )\n}\n"],"mappings":";AAAA,SAAS,eAAe,OAAO,uBAAuB;AACtD,SAAS,cAAc;AACvB,SAAS,oBAAoB;AAC7B;AAAA,EACE;AAAA,EAKA;AAAA,OACK;AAEP,IAAI;AAIJ,IAAM,iBAAiB;AAEvB,SAAS,QAAQ,KAAqB;AACpC,QAAM,QAAQ,IAAI,YAAY;AAC9B,SAAO,UAAU,OAAO,UAAU,MAAM,MAAM;AAChD;AAoBO,SAAS,SAAS,MAAkC;AACzD,MAAI,OAAO,SAAS,UAAU;AAC5B,WAAO;AAAA,EACT;AAEA,QAAM,MAAM,QAAQ,IAAI;AACxB,MAAI,CAAC,YAAY;AACf,UAAM,QAAQ,oBAAI,IAAoB;AACtC,eAAW,QAAQ,OAAO,OAAO,KAAK,GAAG;AACvC,iBAAW,SAAS,KAAK,SAAS;AAChC,cAAM,IAAI,QAAQ,QAAQ,KAAK,CAAC,GAAG,KAAK,EAAE;AAAA,MAC5C;AAAA,IACF;AACA,iBAAa;AAAA,EACf;AAIA,SAAO,WAAW,IAAI,GAAG,KAAK,WAAW,IAAI,QAAQ,GAAG,CAAC;AAC3D;AAEA,SAAS,QAAQ,MAAsB;AACrC,SAAO,KACJ,KAAK,EACL,QAAQ,QAAQ,GAAG,EACnB,QAAQ,wBAAwB,IAAI;AACzC;AAUO,SAAS,QACd,IACsC;AACtC,MAAI,CAAC,WAAW,EAAE,GAAG;AACnB,UAAM,iBAAiB,EAAE;AAAA,EAC3B;AAEA,SAAO,MAAM,EAAE;AACjB;AAGO,SAAS,iBAAiB,OAAiC;AAChE,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO,IAAI;AAAA,MACT;AAAA,MACA,4DAA4D,cAAc,KAAK,CAAC;AAAA,IAClF;AAAA,EACF;AAEA,QAAM,OAAO,MAAM,YAAY;AAC/B,MAAI,OAAO,cAAc,IAAI,GAAG;AAC9B,WAAO,IAAI;AAAA,MACT;AAAA,MACA,GAAG,MAAM,KAAK,CAAC,sGAAsG,aAAa,IAAI,CAAC;AAAA,IACzI;AAAA,EACF;AAEA,QAAM,KAAK,SAAS,KAAK;AACzB,MAAI,IAAI;AACN,WAAO,IAAI;AAAA,MACT;AAAA,MACA,GAAG,MAAM,KAAK,CAAC,kCAAkC,EAAE,eAAe,MAAM,KAAK,CAAC;AAAA,IAChF;AAAA,EACF;AAEA,QAAM,aAAa,YAAY,KAAK;AACpC,SAAO,IAAI;AAAA,IACT;AAAA,IACA;AAAA;AAAA,MAEI,gBAAgB,MAAM,KAAK,CAAC,mBAAmB,SAAS,UAAU,CAAC;AAAA,QACnE,gBAAgB,MAAM,KAAK,CAAC;AAAA,EAClC;AACF;AAEA,IAAM,eAAiD;AAAA,EACrD,QAAQ;AAAA,EACR,IAAI;AACN;AAGO,SAAS,YAAY,MAAqC;AAG/D,SAAO;AAAA,IACL,QAAQ,IAAI,EAAE,QAAQ,qBAAqB,IAAI;AAAA,IAC/C,OAAO,OAAO,KAAK,EAChB,QAAQ,CAAC,SAAS,KAAK,OAAO,EAC9B,OAAO,CAAC,UAAU,CAAC,eAAe,KAAK,KAAK,CAAC;AAAA,EAClD;AACF;","names":[]}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// src/units/registry.ts
|
|
2
|
+
import { hasOwn } from "../issues.js";
|
|
3
|
+
var TSP_ML = 4.92892159375;
|
|
4
|
+
var OZ_G = 28.349523125;
|
|
5
|
+
function definitions() {
|
|
6
|
+
return {
|
|
7
|
+
mg: {
|
|
8
|
+
dimension: "mass",
|
|
9
|
+
systems: ["metric"],
|
|
10
|
+
toBase: 1e-3,
|
|
11
|
+
aliases: ["milligram", "milligrams", "milligramme", "milligrammes"],
|
|
12
|
+
labels: { short: invariable("mg"), long: label("milligram") }
|
|
13
|
+
},
|
|
14
|
+
g: {
|
|
15
|
+
dimension: "mass",
|
|
16
|
+
systems: ["metric"],
|
|
17
|
+
toBase: 1,
|
|
18
|
+
aliases: ["gram", "grams", "gramme", "grammes"],
|
|
19
|
+
labels: { short: invariable("g"), long: label("gram") }
|
|
20
|
+
},
|
|
21
|
+
kg: {
|
|
22
|
+
dimension: "mass",
|
|
23
|
+
systems: ["metric"],
|
|
24
|
+
toBase: 1e3,
|
|
25
|
+
aliases: [
|
|
26
|
+
"kgs",
|
|
27
|
+
"kilo",
|
|
28
|
+
"kilos",
|
|
29
|
+
"kilogram",
|
|
30
|
+
"kilograms",
|
|
31
|
+
"kilogramme",
|
|
32
|
+
"kilogrammes"
|
|
33
|
+
],
|
|
34
|
+
labels: { short: invariable("kg"), long: label("kilogram") }
|
|
35
|
+
},
|
|
36
|
+
oz: {
|
|
37
|
+
dimension: "mass",
|
|
38
|
+
systems: ["us"],
|
|
39
|
+
toBase: OZ_G,
|
|
40
|
+
aliases: ["ounce", "ounces"],
|
|
41
|
+
labels: { short: invariable("oz"), long: label("ounce") }
|
|
42
|
+
},
|
|
43
|
+
lb: {
|
|
44
|
+
dimension: "mass",
|
|
45
|
+
systems: ["us"],
|
|
46
|
+
toBase: 16 * OZ_G,
|
|
47
|
+
aliases: ["lbs", "pound", "pounds"],
|
|
48
|
+
labels: { short: invariable("lb"), long: label("pound") }
|
|
49
|
+
},
|
|
50
|
+
ml: {
|
|
51
|
+
dimension: "volume",
|
|
52
|
+
systems: ["metric"],
|
|
53
|
+
toBase: 1,
|
|
54
|
+
aliases: ["mL", "milliliter", "milliliters", "millilitre", "millilitres"],
|
|
55
|
+
labels: { short: invariable("mL"), long: label("milliliter") }
|
|
56
|
+
},
|
|
57
|
+
cl: {
|
|
58
|
+
dimension: "volume",
|
|
59
|
+
systems: ["metric"],
|
|
60
|
+
toBase: 10,
|
|
61
|
+
aliases: ["cL", "centiliter", "centiliters", "centilitre", "centilitres"],
|
|
62
|
+
labels: { short: invariable("cL"), long: label("centiliter") }
|
|
63
|
+
},
|
|
64
|
+
dl: {
|
|
65
|
+
dimension: "volume",
|
|
66
|
+
systems: ["metric"],
|
|
67
|
+
toBase: 100,
|
|
68
|
+
aliases: ["dL", "deciliter", "deciliters", "decilitre", "decilitres"],
|
|
69
|
+
labels: { short: invariable("dL"), long: label("deciliter") }
|
|
70
|
+
},
|
|
71
|
+
l: {
|
|
72
|
+
dimension: "volume",
|
|
73
|
+
systems: ["metric"],
|
|
74
|
+
toBase: 1e3,
|
|
75
|
+
aliases: ["L", "liter", "liters", "litre", "litres"],
|
|
76
|
+
labels: { short: invariable("L"), long: label("liter") }
|
|
77
|
+
},
|
|
78
|
+
tsp: {
|
|
79
|
+
dimension: "volume",
|
|
80
|
+
systems: ["metric", "us"],
|
|
81
|
+
toBase: TSP_ML,
|
|
82
|
+
aliases: ["t", "tsps", "teaspoon", "teaspoons"],
|
|
83
|
+
labels: { short: invariable("tsp"), long: label("teaspoon") }
|
|
84
|
+
},
|
|
85
|
+
tbsp: {
|
|
86
|
+
dimension: "volume",
|
|
87
|
+
systems: ["metric", "us"],
|
|
88
|
+
toBase: 3 * TSP_ML,
|
|
89
|
+
aliases: ["T", "tbs", "tbsps", "tablespoon", "tablespoons"],
|
|
90
|
+
labels: { short: invariable("tbsp"), long: label("tablespoon") }
|
|
91
|
+
},
|
|
92
|
+
"fl-oz": {
|
|
93
|
+
dimension: "volume",
|
|
94
|
+
systems: ["us"],
|
|
95
|
+
toBase: 6 * TSP_ML,
|
|
96
|
+
aliases: ["fl oz", "fluid ounce", "fluid ounces"],
|
|
97
|
+
labels: { short: invariable("fl oz"), long: label("fluid ounce") }
|
|
98
|
+
},
|
|
99
|
+
cup: {
|
|
100
|
+
dimension: "volume",
|
|
101
|
+
systems: ["us"],
|
|
102
|
+
toBase: 48 * TSP_ML,
|
|
103
|
+
aliases: ["cups", "c"],
|
|
104
|
+
labels: { short: label("cup"), long: label("cup") }
|
|
105
|
+
},
|
|
106
|
+
pint: {
|
|
107
|
+
dimension: "volume",
|
|
108
|
+
systems: ["us"],
|
|
109
|
+
toBase: 96 * TSP_ML,
|
|
110
|
+
aliases: ["pt", "pts", "pints"],
|
|
111
|
+
labels: { short: invariable("pt"), long: label("pint") }
|
|
112
|
+
},
|
|
113
|
+
quart: {
|
|
114
|
+
dimension: "volume",
|
|
115
|
+
systems: ["us"],
|
|
116
|
+
toBase: 192 * TSP_ML,
|
|
117
|
+
aliases: ["qt", "qts", "quarts"],
|
|
118
|
+
labels: { short: invariable("qt"), long: label("quart") }
|
|
119
|
+
},
|
|
120
|
+
gallon: {
|
|
121
|
+
dimension: "volume",
|
|
122
|
+
systems: ["us"],
|
|
123
|
+
toBase: 768 * TSP_ML,
|
|
124
|
+
aliases: ["gal", "gals", "gallons"],
|
|
125
|
+
labels: { short: invariable("gal"), long: label("gallon") }
|
|
126
|
+
},
|
|
127
|
+
pinch: count("pinch", ["pinches"]),
|
|
128
|
+
dash: count("dash", ["dashes"]),
|
|
129
|
+
clove: count("clove", ["cloves"]),
|
|
130
|
+
slice: count("slice", ["slices"]),
|
|
131
|
+
piece: count("piece", ["pieces", "pc", "pcs"]),
|
|
132
|
+
can: count("can", ["cans"]),
|
|
133
|
+
package: count("package", ["packages", "pkg", "pkgs"]),
|
|
134
|
+
stick: count("stick", ["sticks"]),
|
|
135
|
+
sprig: count("sprig", ["sprigs"]),
|
|
136
|
+
bunch: count("bunch", ["bunches"]),
|
|
137
|
+
head: count("head", ["heads"])
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function label(one, other = `${one}s`) {
|
|
141
|
+
return { one, other };
|
|
142
|
+
}
|
|
143
|
+
function invariable(one) {
|
|
144
|
+
return { one, other: one };
|
|
145
|
+
}
|
|
146
|
+
function count(one, aliases) {
|
|
147
|
+
return {
|
|
148
|
+
dimension: "count",
|
|
149
|
+
systems: ["metric", "us"],
|
|
150
|
+
aliases,
|
|
151
|
+
labels: { short: label(one, aliases[0]), long: label(one, aliases[0]) }
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
var units = /* @__PURE__ */ buildRegistry();
|
|
155
|
+
function lookupUnit(id) {
|
|
156
|
+
return typeof id === "string" && hasOwn(units, id) ? units[id] : void 0;
|
|
157
|
+
}
|
|
158
|
+
function buildRegistry() {
|
|
159
|
+
return Object.freeze(
|
|
160
|
+
Object.fromEntries(
|
|
161
|
+
Object.entries(definitions()).map(
|
|
162
|
+
([id, spec]) => [
|
|
163
|
+
id,
|
|
164
|
+
Object.freeze({
|
|
165
|
+
id,
|
|
166
|
+
...spec,
|
|
167
|
+
systems: Object.freeze(spec.systems),
|
|
168
|
+
aliases: Object.freeze([id, ...spec.aliases]),
|
|
169
|
+
labels: Object.freeze({
|
|
170
|
+
short: Object.freeze(spec.labels.short),
|
|
171
|
+
long: Object.freeze(spec.labels.long)
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
]
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
export {
|
|
180
|
+
lookupUnit,
|
|
181
|
+
units
|
|
182
|
+
};
|
|
183
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/units/registry.ts"],"sourcesContent":["import { hasOwn } from '../issues'\n\n/** A measurement system. `'us'` is US customary. */\nexport type System = 'metric' | 'us'\n\n/** What a unit measures. Count units, such as `clove` or `pinch`, have no fixed size. */\nexport type Dimension = 'mass' | 'volume' | 'count'\n\ntype Label = { one: string; other: string }\n\ntype UnitSpec = {\n dimension: Dimension\n systems: readonly System[]\n toBase?: number\n aliases: readonly string[]\n labels: { short: Label; long: Label }\n}\n\n// Exact definitions (NIST Handbook 44, Appendix C): 1 lb = 453.59237 g, and\n// 1 tsp = 1/768 US gallon, where 1 gallon = 231 in³ = 3785.411784 mL.\nconst TSP_ML = 4.92892159375\nconst OZ_G = 28.349523125\n\n// A function rather than a module-level object, so bundlers can drop the whole\n// registry when nothing imports it: the labels are built with calls.\nfunction definitions() {\n return {\n mg: {\n dimension: 'mass',\n systems: ['metric'],\n toBase: 0.001,\n aliases: ['milligram', 'milligrams', 'milligramme', 'milligrammes'],\n labels: { short: invariable('mg'), long: label('milligram') },\n },\n g: {\n dimension: 'mass',\n systems: ['metric'],\n toBase: 1,\n aliases: ['gram', 'grams', 'gramme', 'grammes'],\n labels: { short: invariable('g'), long: label('gram') },\n },\n kg: {\n dimension: 'mass',\n systems: ['metric'],\n toBase: 1000,\n aliases: [\n 'kgs',\n 'kilo',\n 'kilos',\n 'kilogram',\n 'kilograms',\n 'kilogramme',\n 'kilogrammes',\n ],\n labels: { short: invariable('kg'), long: label('kilogram') },\n },\n oz: {\n dimension: 'mass',\n systems: ['us'],\n toBase: OZ_G,\n aliases: ['ounce', 'ounces'],\n labels: { short: invariable('oz'), long: label('ounce') },\n },\n lb: {\n dimension: 'mass',\n systems: ['us'],\n toBase: 16 * OZ_G,\n aliases: ['lbs', 'pound', 'pounds'],\n labels: { short: invariable('lb'), long: label('pound') },\n },\n ml: {\n dimension: 'volume',\n systems: ['metric'],\n toBase: 1,\n aliases: ['mL', 'milliliter', 'milliliters', 'millilitre', 'millilitres'],\n labels: { short: invariable('mL'), long: label('milliliter') },\n },\n cl: {\n dimension: 'volume',\n systems: ['metric'],\n toBase: 10,\n aliases: ['cL', 'centiliter', 'centiliters', 'centilitre', 'centilitres'],\n labels: { short: invariable('cL'), long: label('centiliter') },\n },\n dl: {\n dimension: 'volume',\n systems: ['metric'],\n toBase: 100,\n aliases: ['dL', 'deciliter', 'deciliters', 'decilitre', 'decilitres'],\n labels: { short: invariable('dL'), long: label('deciliter') },\n },\n l: {\n dimension: 'volume',\n systems: ['metric'],\n toBase: 1000,\n aliases: ['L', 'liter', 'liters', 'litre', 'litres'],\n labels: { short: invariable('L'), long: label('liter') },\n },\n tsp: {\n dimension: 'volume',\n systems: ['metric', 'us'],\n toBase: TSP_ML,\n aliases: ['t', 'tsps', 'teaspoon', 'teaspoons'],\n labels: { short: invariable('tsp'), long: label('teaspoon') },\n },\n tbsp: {\n dimension: 'volume',\n systems: ['metric', 'us'],\n toBase: 3 * TSP_ML,\n aliases: ['T', 'tbs', 'tbsps', 'tablespoon', 'tablespoons'],\n labels: { short: invariable('tbsp'), long: label('tablespoon') },\n },\n 'fl-oz': {\n dimension: 'volume',\n systems: ['us'],\n toBase: 6 * TSP_ML,\n aliases: ['fl oz', 'fluid ounce', 'fluid ounces'],\n labels: { short: invariable('fl oz'), long: label('fluid ounce') },\n },\n cup: {\n dimension: 'volume',\n systems: ['us'],\n toBase: 48 * TSP_ML,\n aliases: ['cups', 'c'],\n labels: { short: label('cup'), long: label('cup') },\n },\n pint: {\n dimension: 'volume',\n systems: ['us'],\n toBase: 96 * TSP_ML,\n aliases: ['pt', 'pts', 'pints'],\n labels: { short: invariable('pt'), long: label('pint') },\n },\n quart: {\n dimension: 'volume',\n systems: ['us'],\n toBase: 192 * TSP_ML,\n aliases: ['qt', 'qts', 'quarts'],\n labels: { short: invariable('qt'), long: label('quart') },\n },\n gallon: {\n dimension: 'volume',\n systems: ['us'],\n toBase: 768 * TSP_ML,\n aliases: ['gal', 'gals', 'gallons'],\n labels: { short: invariable('gal'), long: label('gallon') },\n },\n pinch: count('pinch', ['pinches']),\n dash: count('dash', ['dashes']),\n clove: count('clove', ['cloves']),\n slice: count('slice', ['slices']),\n piece: count('piece', ['pieces', 'pc', 'pcs']),\n can: count('can', ['cans']),\n package: count('package', ['packages', 'pkg', 'pkgs']),\n stick: count('stick', ['sticks']),\n sprig: count('sprig', ['sprigs']),\n bunch: count('bunch', ['bunches']),\n head: count('head', ['heads']),\n } as const satisfies Record<string, UnitSpec>\n}\n\nfunction label(one: string, other = `${one}s`): Label {\n return { one, other }\n}\n\nfunction invariable(one: string): Label {\n return { one, other: one }\n}\n\n/**\n * A count unit, native to both systems. `aliases` starts with the plural, which\n * the labels use. Aliases are one tuple argument so their literal types are\n * inferred from it rather than widened by the `satisfies` context.\n */\nfunction count<const Aliases extends readonly [string, ...string[]]>(\n one: string,\n aliases: Aliases,\n) {\n return {\n dimension: 'count' as const,\n systems: ['metric', 'us'] as const,\n aliases,\n labels: { short: label(one, aliases[0]), long: label(one, aliases[0]) },\n }\n}\n\ntype Definitions = ReturnType<typeof definitions>\n\n/**\n * A unit id, such as `'g'`, `'ml'`, `'cup'`, or `'fl-oz'`. Quantities always\n * store ids; use {@link findUnit} to turn author text into one. May grow in minor releases.\n */\nexport type UnitId = keyof Definitions\n\ntype AliasOf<Id extends UnitId> = Id | Definitions[Id]['aliases'][number]\n\n/**\n * A spelling of a unit in the registry, such as `'cups'`, `'T'`, or `'litre'`.\n * {@link findUnit} also accepts it in any case (except `'T'`, `'t'`, and\n * `'c'`) and with a trailing period.\n */\nexport type UnitAlias = { [Id in UnitId]: AliasOf<Id> }[UnitId]\n\n/** The id of the unit an alias names: `UnitIdOf<'tablespoons'>` is `'tbsp'`. */\nexport type UnitIdOf<Alias extends UnitAlias> = {\n [Id in UnitId]: Alias extends AliasOf<Id> ? Id : never\n}[UnitId]\n\n/** One entry in the {@link units} registry. */\nexport type UnitDefinition = {\n readonly id: UnitId\n readonly dimension: Dimension\n /** Systems where the unit is native. tsp, tbsp, and count units are native to both. */\n readonly systems: readonly System[]\n /** Exact size in grams (mass) or millilitres (volume). Absent for count units. */\n readonly toBase?: number\n /**\n * Every accepted spelling, starting with the id. Matched case-insensitively,\n * except `'T'` (tbsp), `'t'` (tsp), and `'c'` (cup).\n */\n readonly aliases: readonly UnitAlias[]\n /** English labels for one and for many, in short (`'tbsp'`) and long (`'tablespoon'`) form. */\n readonly labels: {\n readonly short: Readonly<Label>\n readonly long: Readonly<Label>\n }\n}\n\ntype Registry = {\n readonly [Id in UnitId]: UnitDefinition & { readonly id: Id }\n}\n\n/**\n * The read-only unit registry, keyed by id. Volume and mass units carry exact\n * factors to millilitres and grams.\n *\n * @example\n * units.cup.toBase // 236.5882365\n * Object.values(units).filter((unit) => unit.dimension === 'mass')\n */\nexport const units: Registry = /* @__PURE__ */ buildRegistry()\n\n/** The registry entry for a unit id, or `undefined` for anything else. */\nexport function lookupUnit(id: unknown): UnitDefinition | undefined {\n return typeof id === 'string' && hasOwn(units, id)\n ? units[id as UnitId]\n : undefined\n}\n\nfunction buildRegistry(): Registry {\n return Object.freeze(\n Object.fromEntries(\n Object.entries<UnitSpec>(definitions()).map(\n ([id, spec]): [string, UnitDefinition] => [\n id,\n Object.freeze({\n id: id as UnitId,\n ...spec,\n systems: Object.freeze(spec.systems),\n aliases: Object.freeze([id, ...spec.aliases] as UnitAlias[]),\n labels: Object.freeze({\n short: Object.freeze(spec.labels.short),\n long: Object.freeze(spec.labels.long),\n }),\n }),\n ],\n ),\n ) as Registry,\n )\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// src/units/system.ts
|
|
2
|
+
import { describeValue, RecipeBookError } from "../errors.js";
|
|
3
|
+
import { amountEnds, mapAmount } from "../quantity/amount.js";
|
|
4
|
+
import {
|
|
5
|
+
lookupUnit
|
|
6
|
+
} from "./registry.js";
|
|
7
|
+
var STEP_TOLERANCE = 0.05;
|
|
8
|
+
var FLOAT_TOLERANCE = 1e-9;
|
|
9
|
+
var LADDERS = {
|
|
10
|
+
metric: {
|
|
11
|
+
mass: [{ unit: "kg", min: 1 }, { unit: "g" }],
|
|
12
|
+
volume: [{ unit: "l", min: 1 }, { unit: "ml" }]
|
|
13
|
+
},
|
|
14
|
+
us: {
|
|
15
|
+
mass: [{ unit: "lb", min: 1 }, { unit: "oz" }],
|
|
16
|
+
volume: [
|
|
17
|
+
{ unit: "cup", min: 0.25, steps: [4, 3], stepsBelow: 1 },
|
|
18
|
+
{ unit: "tbsp", min: 1, steps: [2] },
|
|
19
|
+
{ unit: "tsp" }
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
function inSystem(quantity, system) {
|
|
24
|
+
const source = lookupUnit(quantity.unit);
|
|
25
|
+
if (source?.toBase === void 0 || source.dimension === "count" || source.systems.includes(system)) {
|
|
26
|
+
return quantity;
|
|
27
|
+
}
|
|
28
|
+
return onLadder(
|
|
29
|
+
quantity,
|
|
30
|
+
source.toBase,
|
|
31
|
+
LADDERS[system][source.dimension],
|
|
32
|
+
system
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
function rebalance(quantity, system) {
|
|
36
|
+
const source = lookupUnit(quantity.unit);
|
|
37
|
+
if (source?.toBase === void 0 || source.dimension === "count" || !source.systems.includes(system)) {
|
|
38
|
+
return quantity;
|
|
39
|
+
}
|
|
40
|
+
for (const owner of source.systems) {
|
|
41
|
+
const ladder = LADDERS[owner][source.dimension];
|
|
42
|
+
if (ladder.some((rung) => rung.unit === source.id)) {
|
|
43
|
+
return onLadder(quantity, source.toBase, ladder, system);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return quantity;
|
|
47
|
+
}
|
|
48
|
+
function isSystem(value) {
|
|
49
|
+
return value === "metric" || value === "us";
|
|
50
|
+
}
|
|
51
|
+
function checkSystem(fn, system) {
|
|
52
|
+
if (!isSystem(system)) {
|
|
53
|
+
throw new RecipeBookError(
|
|
54
|
+
"invalid_argument",
|
|
55
|
+
`${fn}: system must be "metric" or "us", received ${describeValue(system)}.`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function onLadder(quantity, fromBase, ladder, system) {
|
|
60
|
+
const base = mapAmount(quantity.amount, (value) => value * fromBase);
|
|
61
|
+
for (const rung of ladder) {
|
|
62
|
+
const unit = lookupUnit(rung.unit);
|
|
63
|
+
if (!unit.systems.includes(system)) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const amount = mapAmount(base, (value) => value / unit.toBase);
|
|
67
|
+
const ends = amountEnds(amount);
|
|
68
|
+
const low = ends[0];
|
|
69
|
+
const tolerance = rung.steps ? STEP_TOLERANCE : FLOAT_TOLERANCE;
|
|
70
|
+
const meetsMin = rung.min === void 0 || low >= rung.min * (1 - tolerance);
|
|
71
|
+
const fitsSteps = !rung.steps || ends.every(
|
|
72
|
+
(value) => rung.stepsBelow !== void 0 && value >= rung.stepsBelow || rung.steps.some(
|
|
73
|
+
(denominator) => nearMultiple(value, 1 / denominator)
|
|
74
|
+
)
|
|
75
|
+
);
|
|
76
|
+
if (meetsMin && fitsSteps) {
|
|
77
|
+
return unit.id === quantity.unit ? quantity : { amount, unit: unit.id };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return quantity;
|
|
81
|
+
}
|
|
82
|
+
function nearMultiple(value, step) {
|
|
83
|
+
return Math.abs(value - Math.round(value / step) * step) <= STEP_TOLERANCE * value;
|
|
84
|
+
}
|
|
85
|
+
export {
|
|
86
|
+
checkSystem,
|
|
87
|
+
inSystem,
|
|
88
|
+
isSystem,
|
|
89
|
+
rebalance
|
|
90
|
+
};
|
|
91
|
+
//# sourceMappingURL=system.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/units/system.ts"],"sourcesContent":["import { describeValue, RecipeBookError } from '../errors'\nimport { amountEnds, mapAmount } from '../quantity/amount'\nimport type { Quantity } from '../quantity/types'\nimport {\n lookupUnit,\n type System,\n type UnitDefinition,\n type UnitId,\n} from './registry'\n\ntype Rung = {\n unit: UnitId\n /** Smallest amount, in this unit, that may use it. */\n min?: number\n /** Measuring-set fractions the amount must land near, within 5%, by denominator: 4 for quarters. */\n steps?: readonly number[]\n /** From this amount up, the steps no longer apply. */\n stepsBelow?: number\n}\n\nconst STEP_TOLERANCE = 0.05\nconst FLOAT_TOLERANCE = 1e-9\n\n// Largest unit first. The last rung has no conditions, so it always matches.\nconst LADDERS: Record<System, Record<'mass' | 'volume', readonly Rung[]>> = {\n metric: {\n mass: [{ unit: 'kg', min: 1 }, { unit: 'g' }],\n volume: [{ unit: 'l', min: 1 }, { unit: 'ml' }],\n },\n us: {\n mass: [{ unit: 'lb', min: 1 }, { unit: 'oz' }],\n volume: [\n { unit: 'cup', min: 0.25, steps: [4, 3], stepsBelow: 1 },\n { unit: 'tbsp', min: 1, steps: [2] },\n { unit: 'tsp' },\n ],\n },\n}\n\n/** {@link toSystem} without argument checks. Unknown units are returned unchanged. */\nexport function inSystem(quantity: Quantity, system: System): Quantity {\n const source = lookupUnit(quantity.unit)\n\n if (\n source?.toBase === undefined ||\n source.dimension === 'count' ||\n source.systems.includes(system)\n ) {\n return quantity\n }\n\n return onLadder(\n quantity,\n source.toBase,\n LADDERS[system][source.dimension],\n system,\n )\n}\n\n/**\n * Moves a quantity to the best unit on its own ladder that is native to\n * `system`: 6 tsp becomes 2 tbsp, and 1000 g becomes 1 kg. Quantities whose\n * unit is on no ladder, or not native to `system`, are returned unchanged.\n */\nexport function rebalance(quantity: Quantity, system: System): Quantity {\n const source = lookupUnit(quantity.unit)\n\n if (\n source?.toBase === undefined ||\n source.dimension === 'count' ||\n !source.systems.includes(system)\n ) {\n return quantity\n }\n\n for (const owner of source.systems) {\n const ladder = LADDERS[owner][source.dimension]\n if (ladder.some((rung) => rung.unit === source.id)) {\n return onLadder(quantity, source.toBase, ladder, system)\n }\n }\n\n return quantity\n}\n\nexport function isSystem(value: unknown): value is System {\n return value === 'metric' || value === 'us'\n}\n\n/** Throws `invalid_argument`, naming `fn`, unless `system` is `'metric'` or `'us'`. */\nexport function checkSystem(fn: string, system: unknown): void {\n if (!isSystem(system)) {\n throw new RecipeBookError(\n 'invalid_argument',\n `${fn}: system must be \"metric\" or \"us\", received ${describeValue(system)}.`,\n )\n }\n}\n\nfunction onLadder(\n quantity: Quantity,\n fromBase: number,\n ladder: readonly Rung[],\n system: System,\n): Quantity {\n const base = mapAmount(quantity.amount, (value) => value * fromBase)\n\n for (const rung of ladder) {\n const unit = lookupUnit(rung.unit) as UnitDefinition & { toBase: number }\n if (!unit.systems.includes(system)) {\n continue\n }\n\n const amount = mapAmount(base, (value) => value / unit.toBase)\n const ends = amountEnds(amount)\n const low = ends[0]!\n\n const tolerance = rung.steps ? STEP_TOLERANCE : FLOAT_TOLERANCE\n const meetsMin = rung.min === undefined || low >= rung.min * (1 - tolerance)\n const fitsSteps =\n !rung.steps ||\n ends.every(\n (value) =>\n (rung.stepsBelow !== undefined && value >= rung.stepsBelow) ||\n rung.steps!.some((denominator) =>\n nearMultiple(value, 1 / denominator),\n ),\n )\n\n if (meetsMin && fitsSteps) {\n return unit.id === quantity.unit ? quantity : { amount, unit: unit.id }\n }\n }\n\n return quantity\n}\n\nfunction nearMultiple(value: number, step: number): boolean {\n return (\n Math.abs(value - Math.round(value / step) * step) <= STEP_TOLERANCE * value\n )\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// src/units/temperature.ts
|
|
2
|
+
import { describeValue, RecipeBookError } from "../errors.js";
|
|
3
|
+
function convertTemperature(temperature, to) {
|
|
4
|
+
const { value, unit } = temperature ?? {};
|
|
5
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
6
|
+
throw invalidArgument(
|
|
7
|
+
`value must be a finite number, received ${describeValue(value)}`
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
if (unit !== "C" && unit !== "F") {
|
|
11
|
+
throw invalidArgument(
|
|
12
|
+
`temperature unit must be "C" or "F", received ${describeValue(unit)}`
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
if (to !== "C" && to !== "F") {
|
|
16
|
+
throw invalidArgument(
|
|
17
|
+
`target unit must be "C" or "F", received ${describeValue(to)}`
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
if (unit === to) {
|
|
21
|
+
return { value, unit };
|
|
22
|
+
}
|
|
23
|
+
const result = to === "C" ? (value - 32) * 5 / 9 : value * 9 / 5 + 32;
|
|
24
|
+
if (!Number.isFinite(result)) {
|
|
25
|
+
throw invalidArgument(
|
|
26
|
+
`the result must be a finite number, but it is ${result}`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return { value: result, unit: to };
|
|
30
|
+
}
|
|
31
|
+
function invalidArgument(detail) {
|
|
32
|
+
return new RecipeBookError(
|
|
33
|
+
"invalid_argument",
|
|
34
|
+
`convertTemperature: ${detail}.`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
export {
|
|
38
|
+
convertTemperature
|
|
39
|
+
};
|
|
40
|
+
//# sourceMappingURL=temperature.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/units/temperature.ts"],"sourcesContent":["import { describeValue, RecipeBookError } from '../errors'\n\n/** Degrees Celsius or Fahrenheit. */\nexport type TemperatureUnit = 'C' | 'F'\n\n/**\n * An oven or food temperature. Temperatures are not quantities: converting\n * them is affine, and scaling a recipe never changes them.\n */\nexport type Temperature = { value: number; unit: TemperatureUnit }\n\n/**\n * Converts a temperature between Celsius and Fahrenheit. Nothing is rounded,\n * so the value can be off in its last digits: −18 °C is\n * −0.3999999999999986 °F.\n *\n * @throws {RecipeBookError} `invalid_argument` for a non-finite value, an unknown unit, or a result that overflows to `Infinity` or `-Infinity`, which only a value beyond about ±2e307 can do.\n *\n * @example\n * convertTemperature({ value: 350, unit: 'F' }, 'C') // { value: 176.66…, unit: 'C' }\n * convertTemperature({ value: 180, unit: 'C' }, 'F') // { value: 356, unit: 'F' }\n */\nexport function convertTemperature(\n temperature: Temperature,\n to: TemperatureUnit,\n): Temperature {\n const { value, unit } = temperature ?? {}\n\n if (typeof value !== 'number' || !Number.isFinite(value)) {\n throw invalidArgument(\n `value must be a finite number, received ${describeValue(value)}`,\n )\n }\n if (unit !== 'C' && unit !== 'F') {\n throw invalidArgument(\n `temperature unit must be \"C\" or \"F\", received ${describeValue(unit)}`,\n )\n }\n if (to !== 'C' && to !== 'F') {\n throw invalidArgument(\n `target unit must be \"C\" or \"F\", received ${describeValue(to)}`,\n )\n }\n\n if (unit === to) {\n return { value, unit }\n }\n\n const result = to === 'C' ? ((value - 32) * 5) / 9 : (value * 9) / 5 + 32\n if (!Number.isFinite(result)) {\n throw invalidArgument(\n `the result must be a finite number, but it is ${result}`,\n )\n }\n return { value: result, unit: to }\n}\n\nfunction invalidArgument(detail: string): RecipeBookError {\n return new RecipeBookError(\n 'invalid_argument',\n `convertTemperature: ${detail}.`,\n )\n}\n"],"mappings":";AAAA,SAAS,eAAe,uBAAuB;AAsBxC,SAAS,mBACd,aACA,IACa;AACb,QAAM,EAAE,OAAO,KAAK,IAAI,eAAe,CAAC;AAExC,MAAI,OAAO,UAAU,YAAY,CAAC,OAAO,SAAS,KAAK,GAAG;AACxD,UAAM;AAAA,MACJ,2CAA2C,cAAc,KAAK,CAAC;AAAA,IACjE;AAAA,EACF;AACA,MAAI,SAAS,OAAO,SAAS,KAAK;AAChC,UAAM;AAAA,MACJ,iDAAiD,cAAc,IAAI,CAAC;AAAA,IACtE;AAAA,EACF;AACA,MAAI,OAAO,OAAO,OAAO,KAAK;AAC5B,UAAM;AAAA,MACJ,4CAA4C,cAAc,EAAE,CAAC;AAAA,IAC/D;AAAA,EACF;AAEA,MAAI,SAAS,IAAI;AACf,WAAO,EAAE,OAAO,KAAK;AAAA,EACvB;AAEA,QAAM,SAAS,OAAO,OAAQ,QAAQ,MAAM,IAAK,IAAK,QAAQ,IAAK,IAAI;AACvE,MAAI,CAAC,OAAO,SAAS,MAAM,GAAG;AAC5B,UAAM;AAAA,MACJ,iDAAiD,MAAM;AAAA,IACzD;AAAA,EACF;AACA,SAAO,EAAE,OAAO,QAAQ,MAAM,GAAG;AACnC;AAEA,SAAS,gBAAgB,QAAiC;AACxD,SAAO,IAAI;AAAA,IACT;AAAA,IACA,uBAAuB,MAAM;AAAA,EAC/B;AACF;","names":[]}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// src/validation.ts
|
|
2
|
+
import {
|
|
3
|
+
quote,
|
|
4
|
+
RecipeBookError,
|
|
5
|
+
truncate
|
|
6
|
+
} from "./errors.js";
|
|
7
|
+
import { hasOwn, isRecord, issue, warning } from "./issues.js";
|
|
8
|
+
import { closestMatch } from "./suggest.js";
|
|
9
|
+
var RecipeValidationError = class extends RecipeBookError {
|
|
10
|
+
/**
|
|
11
|
+
* @param subject - What was validated, shown as given at the start of the
|
|
12
|
+
* message, e.g. `'"Buttermilk pancakes"'`. Recipe Book's own errors quote
|
|
13
|
+
* the recipe's name or the input text like a JSON string, cut with "…"
|
|
14
|
+
* when longer than 60 UTF-16 code units.
|
|
15
|
+
*/
|
|
16
|
+
constructor(code, issues, subject = code === "invalid_recipe" ? "Recipe" : code === "invalid_quantity" ? "Quantity" : "Ingredient") {
|
|
17
|
+
super(code, summarize(subject, issues));
|
|
18
|
+
Object.assign(this, { name: "RecipeValidationError" });
|
|
19
|
+
this.issues = issues;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
var MAX_LISTED = 10;
|
|
23
|
+
var IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
24
|
+
function summarize(subject, issues) {
|
|
25
|
+
const count = issues.length;
|
|
26
|
+
const header = `${subject} has ${count} ${count === 1 ? "problem" : "problems"}:`;
|
|
27
|
+
const listed = issues.slice(0, MAX_LISTED);
|
|
28
|
+
const paths = listed.map((issue2) => formatPath(issue2.path));
|
|
29
|
+
const width = Math.max(0, ...paths.map((path) => path.length));
|
|
30
|
+
const lines = listed.map(
|
|
31
|
+
(issue2, i) => ` ${issue2.severity === "error" ? "\u2716" : "\u26A0"} ${paths[i].padEnd(width)} ${issue2.message}`
|
|
32
|
+
);
|
|
33
|
+
if (count > listed.length) {
|
|
34
|
+
lines.push(
|
|
35
|
+
` \u2026and ${count - listed.length} more. error.issues lists them all.`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
return [header, ...lines].join("\n");
|
|
39
|
+
}
|
|
40
|
+
function formatPath(path) {
|
|
41
|
+
if (path.length === 0) {
|
|
42
|
+
return "(root)";
|
|
43
|
+
}
|
|
44
|
+
return path.reduce(
|
|
45
|
+
(text, segment) => typeof segment === "number" ? `${text}[${segment}]` : IDENTIFIER.test(segment) ? text ? `${text}.${truncate(segment)}` : truncate(segment) : `${text}[${quote(segment)}]`,
|
|
46
|
+
""
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
function error(issues, code, path, message, suggestion) {
|
|
50
|
+
issues.push(issue("error", code, path, message, suggestion));
|
|
51
|
+
return void 0;
|
|
52
|
+
}
|
|
53
|
+
function toResult(code, data, issues, subject) {
|
|
54
|
+
if (data === void 0 || issues.some((issue2) => issue2.severity === "error")) {
|
|
55
|
+
return {
|
|
56
|
+
success: false,
|
|
57
|
+
error: new RecipeValidationError(code, issues, subject)
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return { success: true, data, warnings: issues };
|
|
61
|
+
}
|
|
62
|
+
function asRecord(value) {
|
|
63
|
+
return isRecord(value) ? Object.assign(/* @__PURE__ */ Object.create(null), value) : void 0;
|
|
64
|
+
}
|
|
65
|
+
function checkFields(issues, record, fields, path) {
|
|
66
|
+
for (const key of Object.keys(record)) {
|
|
67
|
+
if (hasOwn(fields, key)) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const suggestion = closestMatch(
|
|
71
|
+
key.replace(/Content$/, ""),
|
|
72
|
+
Object.keys(fields)
|
|
73
|
+
);
|
|
74
|
+
warning(
|
|
75
|
+
issues,
|
|
76
|
+
"unknown_field",
|
|
77
|
+
[...path, key],
|
|
78
|
+
suggestion ? `Unknown field. Did you mean "${suggestion}"?` : "Unknown field. It is ignored.",
|
|
79
|
+
suggestion
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export {
|
|
84
|
+
RecipeValidationError,
|
|
85
|
+
asRecord,
|
|
86
|
+
checkFields,
|
|
87
|
+
error,
|
|
88
|
+
toResult
|
|
89
|
+
};
|
|
90
|
+
//# sourceMappingURL=validation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/validation.ts"],"sourcesContent":["import {\n type ParseResult,\n quote,\n RecipeBookError,\n type RecipeIssue,\n type RecipeIssueCode,\n truncate,\n type ValidationErrorCode,\n} from './errors'\nimport { hasOwn, isRecord, issue, type Path, warning } from './issues'\nimport { closestMatch } from './suggest'\n\n// Only parsing and fromJsonLd use this module. Nothing React's imports from core\n// load may import it, or every page with React parts loads it once any page\n// parses. scripts/check-parser-split.js checks this.\n\n/** Thrown, or returned by the `parse*` functions, when data fails validation. */\nexport class RecipeValidationError extends RecipeBookError {\n declare readonly code: ValidationErrorCode\n /**\n * Every problem found, errors and warnings, in a stable order: fields in\n * model order (recipe fields, ingredients, steps, notes, equipment, then\n * nutrition), list items in input order, and unknown fields last within\n * their object.\n */\n declare readonly issues: readonly RecipeIssue[]\n\n /**\n * @param subject - What was validated, shown as given at the start of the\n * message, e.g. `'\"Buttermilk pancakes\"'`. Recipe Book's own errors quote\n * the recipe's name or the input text like a JSON string, cut with \"…\"\n * when longer than 60 UTF-16 code units.\n */\n constructor(\n code: ValidationErrorCode,\n issues: readonly RecipeIssue[],\n subject = code === 'invalid_recipe'\n ? 'Recipe'\n : code === 'invalid_quantity'\n ? 'Quantity'\n : 'Ingredient',\n ) {\n super(code, summarize(subject, issues))\n // The base class declares `name` readonly.\n Object.assign(this, { name: 'RecipeValidationError' })\n this.issues = issues\n }\n}\n\n// Messages end up in logs, so they stay bounded however large the input is.\nconst MAX_LISTED = 10\n// Keys in paths come from input, so only ASCII identifiers print bare. Others\n// print quoted, so `[\"a.b\"]` and look-alikes such as `bakeꓸtime` can't pass for nesting.\nconst IDENTIFIER = /^[A-Za-z_$][\\w$]*$/\n\nfunction summarize(subject: string, issues: readonly RecipeIssue[]): string {\n const count = issues.length\n const header = `${subject} has ${count} ${count === 1 ? 'problem' : 'problems'}:`\n const listed = issues.slice(0, MAX_LISTED)\n const paths = listed.map((issue) => formatPath(issue.path))\n const width = Math.max(0, ...paths.map((path) => path.length))\n const lines = listed.map(\n (issue, i) =>\n ` ${issue.severity === 'error' ? '✖' : '⚠'} ${paths[i]!.padEnd(width)} ${issue.message}`,\n )\n\n if (count > listed.length) {\n lines.push(\n ` …and ${count - listed.length} more. error.issues lists them all.`,\n )\n }\n\n return [header, ...lines].join('\\n')\n}\n\nfunction formatPath(path: readonly (string | number)[]): string {\n if (path.length === 0) {\n return '(root)'\n }\n\n return path.reduce<string>(\n (text, segment) =>\n typeof segment === 'number'\n ? `${text}[${segment}]`\n : IDENTIFIER.test(segment)\n ? text\n ? `${text}.${truncate(segment)}`\n : truncate(segment)\n : `${text}[${quote(segment)}]`,\n '',\n )\n}\n\n/** Records an error and returns `undefined`, so readers can `return error(...)`. */\nexport function error(\n issues: RecipeIssue[],\n code: RecipeIssueCode,\n path: Path,\n message: string,\n suggestion?: string,\n): undefined {\n issues.push(issue('error', code, path, message, suggestion))\n return undefined\n}\n\nexport function toResult<T>(\n code: RecipeValidationError['code'],\n data: T | undefined,\n issues: RecipeIssue[],\n subject?: string,\n): ParseResult<T> {\n if (\n data === undefined ||\n issues.some((issue) => issue.severity === 'error')\n ) {\n return {\n success: false,\n error: new RecipeValidationError(code, issues, subject),\n }\n }\n\n return { success: true, data, warnings: issues }\n}\n\n/**\n * A copy of an object's own enumerable fields on a null prototype, so fields\n * inherited from a prototype, including a polluted `Object.prototype`, are never read.\n */\nexport function asRecord(value: unknown): Record<string, unknown> | undefined {\n return isRecord(value)\n ? Object.assign(Object.create(null) as Record<string, unknown>, value)\n : undefined\n}\n\n/** The known keys of `T`. The type makes the compiler check that none is missing or extra. */\nexport type Fields<T> = { readonly [K in keyof T]-?: true }\n\n/**\n * Warns about each key of `record` that isn't in `fields`. Suggestions ignore\n * a trailing `Content`, so schema.org names such as `fatContent` suggest `fat`.\n */\nexport function checkFields(\n issues: RecipeIssue[],\n record: Record<string, unknown>,\n fields: Readonly<Record<string, unknown>>,\n path: Path,\n): void {\n for (const key of Object.keys(record)) {\n if (hasOwn(fields, key)) {\n continue\n }\n\n const suggestion = closestMatch(\n key.replace(/Content$/, ''),\n Object.keys(fields),\n )\n\n warning(\n issues,\n 'unknown_field',\n [...path, key],\n suggestion\n ? `Unknown field. Did you mean \"${suggestion}\"?`\n : 'Unknown field. It is ignored.',\n suggestion,\n )\n }\n}\n"],"mappings":";AAAA;AAAA,EAEE;AAAA,EACA;AAAA,EAGA;AAAA,OAEK;AACP,SAAS,QAAQ,UAAU,OAAkB,eAAe;AAC5D,SAAS,oBAAoB;AAOtB,IAAM,wBAAN,cAAoC,gBAAgB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgBzD,YACE,MACA,QACA,UAAU,SAAS,mBACf,WACA,SAAS,qBACP,aACA,cACN;AACA,UAAM,MAAM,UAAU,SAAS,MAAM,CAAC;AAEtC,WAAO,OAAO,MAAM,EAAE,MAAM,wBAAwB,CAAC;AACrD,SAAK,SAAS;AAAA,EAChB;AACF;AAGA,IAAM,aAAa;AAGnB,IAAM,aAAa;AAEnB,SAAS,UAAU,SAAiB,QAAwC;AAC1E,QAAM,QAAQ,OAAO;AACrB,QAAM,SAAS,GAAG,OAAO,QAAQ,KAAK,IAAI,UAAU,IAAI,YAAY,UAAU;AAC9E,QAAM,SAAS,OAAO,MAAM,GAAG,UAAU;AACzC,QAAM,QAAQ,OAAO,IAAI,CAACA,WAAU,WAAWA,OAAM,IAAI,CAAC;AAC1D,QAAM,QAAQ,KAAK,IAAI,GAAG,GAAG,MAAM,IAAI,CAAC,SAAS,KAAK,MAAM,CAAC;AAC7D,QAAM,QAAQ,OAAO;AAAA,IACnB,CAACA,QAAO,MACN,KAAKA,OAAM,aAAa,UAAU,WAAM,QAAG,IAAI,MAAM,CAAC,EAAG,OAAO,KAAK,CAAC,KAAKA,OAAM,OAAO;AAAA,EAC5F;AAEA,MAAI,QAAQ,OAAO,QAAQ;AACzB,UAAM;AAAA,MACJ,eAAU,QAAQ,OAAO,MAAM;AAAA,IACjC;AAAA,EACF;AAEA,SAAO,CAAC,QAAQ,GAAG,KAAK,EAAE,KAAK,IAAI;AACrC;AAEA,SAAS,WAAW,MAA4C;AAC9D,MAAI,KAAK,WAAW,GAAG;AACrB,WAAO;AAAA,EACT;AAEA,SAAO,KAAK;AAAA,IACV,CAAC,MAAM,YACL,OAAO,YAAY,WACf,GAAG,IAAI,IAAI,OAAO,MAClB,WAAW,KAAK,OAAO,IACrB,OACE,GAAG,IAAI,IAAI,SAAS,OAAO,CAAC,KAC5B,SAAS,OAAO,IAClB,GAAG,IAAI,IAAI,MAAM,OAAO,CAAC;AAAA,IACjC;AAAA,EACF;AACF;AAGO,SAAS,MACd,QACA,MACA,MACA,SACA,YACW;AACX,SAAO,KAAK,MAAM,SAAS,MAAM,MAAM,SAAS,UAAU,CAAC;AAC3D,SAAO;AACT;AAEO,SAAS,SACd,MACA,MACA,QACA,SACgB;AAChB,MACE,SAAS,UACT,OAAO,KAAK,CAACA,WAAUA,OAAM,aAAa,OAAO,GACjD;AACA,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO,IAAI,sBAAsB,MAAM,QAAQ,OAAO;AAAA,IACxD;AAAA,EACF;AAEA,SAAO,EAAE,SAAS,MAAM,MAAM,UAAU,OAAO;AACjD;AAMO,SAAS,SAAS,OAAqD;AAC5E,SAAO,SAAS,KAAK,IACjB,OAAO,OAAO,uBAAO,OAAO,IAAI,GAA8B,KAAK,IACnE;AACN;AASO,SAAS,YACd,QACA,QACA,QACA,MACM;AACN,aAAW,OAAO,OAAO,KAAK,MAAM,GAAG;AACrC,QAAI,OAAO,QAAQ,GAAG,GAAG;AACvB;AAAA,IACF;AAEA,UAAM,aAAa;AAAA,MACjB,IAAI,QAAQ,YAAY,EAAE;AAAA,MAC1B,OAAO,KAAK,MAAM;AAAA,IACpB;AAEA;AAAA,MACE;AAAA,MACA;AAAA,MACA,CAAC,GAAG,MAAM,GAAG;AAAA,MACb,aACI,gCAAgC,UAAU,OAC1C;AAAA,MACJ;AAAA,IACF;AAAA,EACF;AACF;","names":["issue"]}
|