mcp-recipes 1.2.0 → 1.3.0
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 +239 -0
- package/README.md +174 -7
- package/dist/index.js +1171 -170
- package/dist/index.js.map +1 -1
- package/dist/recipe/scale.js +185 -38
- package/dist/recipe/scale.js.map +1 -1
- package/dist/sources/client.d.ts +180 -3
- package/dist/sources/client.js +657 -36
- package/dist/sources/client.js.map +1 -1
- package/package.json +3 -3
package/dist/recipe/scale.js
CHANGED
|
@@ -7,7 +7,10 @@ var ENGLISH_FOODS = /\b(?:eggs?|salt|pepper|sugar|flour|butter|milk|cream|oil|ga
|
|
|
7
7
|
var STRUCTURAL = 3;
|
|
8
8
|
var VOCABULARY = 2;
|
|
9
9
|
var NAME = 1;
|
|
10
|
-
function readLanguage(line, evidence = {
|
|
10
|
+
function readLanguage(line, evidence = {
|
|
11
|
+
frenchUnit: false,
|
|
12
|
+
englishUnit: false
|
|
13
|
+
}) {
|
|
11
14
|
let french = 0;
|
|
12
15
|
let english = 0;
|
|
13
16
|
if (FRENCH_WORDS.test(line)) french += STRUCTURAL;
|
|
@@ -330,8 +333,10 @@ var EMBEDDED = {
|
|
|
330
333
|
};
|
|
331
334
|
function buildEmbedded(language) {
|
|
332
335
|
const keys = UNIT_KEYS[language].map((key) => key.replace(/ /g, "\\s+"));
|
|
333
|
-
|
|
336
|
+
const joiner = "(?:(?:de|du|des|d|of|a|an|the)\\s+)?";
|
|
337
|
+
return new RegExp(`\\d[\\d.,/]*\\s*${joiner}(?:${keys.join("|")})\\b`, "i");
|
|
334
338
|
}
|
|
339
|
+
var CONTAINER_NOUN = /^(?:cans?|tins?|jars?|packets?|packages?|packs?|box|boxes|bottles?|cartons?|bags?|tubs?|sachets?|blocks?|blocs?|boites?|pots?|briques?|bocaux|bocals?|barquettes?)\b/i;
|
|
335
340
|
function hasEmbeddedMeasure(text, language) {
|
|
336
341
|
return EMBEDDED[language].test(normalizeUnitKey(text));
|
|
337
342
|
}
|
|
@@ -476,15 +481,24 @@ function chooseReadableUnit(unit, amount) {
|
|
|
476
481
|
current = step.unit;
|
|
477
482
|
}
|
|
478
483
|
const up = PROMOTIONS[normalizeUnitKey(current.canonical)];
|
|
479
|
-
if (up && amount * ratio >= up.per) {
|
|
484
|
+
if (up && amount * ratio >= up.per && writesExactly(amount * ratio / up.per)) {
|
|
480
485
|
const target = lookupUnit(up.to, up.language);
|
|
481
486
|
if (target) {
|
|
482
487
|
ratio /= up.per;
|
|
483
488
|
current = target;
|
|
484
489
|
}
|
|
485
490
|
}
|
|
491
|
+
while (!writesExactly(amount * ratio)) {
|
|
492
|
+
const step = demoteUnit(current);
|
|
493
|
+
if (!step) break;
|
|
494
|
+
ratio *= step.per;
|
|
495
|
+
current = step.unit;
|
|
496
|
+
}
|
|
486
497
|
return { unit: current, ratio };
|
|
487
498
|
}
|
|
499
|
+
function writesExactly(value) {
|
|
500
|
+
return Math.abs(value * 100 - Math.round(value * 100)) < 1e-9;
|
|
501
|
+
}
|
|
488
502
|
function formatUnit(unit, amount, language) {
|
|
489
503
|
if (unit.symbol) return unit.canonical;
|
|
490
504
|
const singular = language === "fr" ? amount < 2 : amount <= 1;
|
|
@@ -568,6 +582,40 @@ function isStatedSize(text) {
|
|
|
568
582
|
if (!measure.unit || measure.unit.kind !== "measured") return false;
|
|
569
583
|
return !CONTAINER_CONTENTS.test(measure.rest);
|
|
570
584
|
}
|
|
585
|
+
function takeCompoundMember(text, unit, language) {
|
|
586
|
+
if (!unit) return null;
|
|
587
|
+
const step = demoteUnit(unit);
|
|
588
|
+
if (!step) return null;
|
|
589
|
+
const joiner = ADDS_UP.exec(text);
|
|
590
|
+
if (joiner) text = text.slice(joiner[0].length);
|
|
591
|
+
const second = parseLeadingQuantity(text, language);
|
|
592
|
+
if (!second || second.amount <= 0 || second.amount >= step.per) return null;
|
|
593
|
+
const after = text.slice(second.length);
|
|
594
|
+
const measure = takeUnit(after.trimStart(), language);
|
|
595
|
+
if (measure.unit) {
|
|
596
|
+
if (measure.unit.canonical !== step.unit.canonical) return null;
|
|
597
|
+
return { adds: second.amount / step.per, rest: measure.rest.trimStart() };
|
|
598
|
+
}
|
|
599
|
+
if (unit.kind !== "measured") return null;
|
|
600
|
+
if (!Number.isInteger(second.amount)) return null;
|
|
601
|
+
const trailing = after.trimStart();
|
|
602
|
+
if (trailing !== "" && !/^(?:de\s|d'|du\s|des\s|,)/i.test(trailing)) return null;
|
|
603
|
+
return { adds: second.amount / step.per, rest: trailing };
|
|
604
|
+
}
|
|
605
|
+
var ADDS_UP = /^\s*(?:\+|et|and|plus)\s+/i;
|
|
606
|
+
var LEADING_DECORATION = /^(?:(?!~|≈|\()[^\p{L}\p{N}\s])+(?=\s)/u;
|
|
607
|
+
function takeDecoration(text) {
|
|
608
|
+
const match = LEADING_DECORATION.exec(text);
|
|
609
|
+
return match ? match[0] : null;
|
|
610
|
+
}
|
|
611
|
+
function namesContainer(word) {
|
|
612
|
+
return word !== void 0 && CONTAINER_NOUN.test(normalizeUnitKey(word));
|
|
613
|
+
}
|
|
614
|
+
var TIME_UNIT = {
|
|
615
|
+
en: /^(?:h|hr|hrs|hours?|mins?|minutes?|secs?|seconds?|days?|nights?|weeks?)\b/i,
|
|
616
|
+
fr: /^(?:h|mn|heures?|mins?|minutes?|secs?|secondes?|jours?|nuits?|semaines?)\b/i
|
|
617
|
+
};
|
|
618
|
+
var ORDINAL_SUFFIX = /^(?:ers?|[eè]res?|[eè]mes?|es?|st|nd|rd|th)\b/i;
|
|
571
619
|
var PER_PERSON = {
|
|
572
620
|
en: /\bper\s+(?:person|head|serving|guest|diner)\b/i,
|
|
573
621
|
fr: /\bpar\s+(?:personne|convive|part|t\u00eate)\b/i
|
|
@@ -690,16 +738,16 @@ function parseLeadingRange(text, language) {
|
|
|
690
738
|
length: low.length + separator[0].length + high.length
|
|
691
739
|
};
|
|
692
740
|
}
|
|
693
|
-
var FRENCH_ARTICLES = { un: 1, une: 1
|
|
741
|
+
var FRENCH_ARTICLES = { un: 1, une: 1 };
|
|
694
742
|
function takeUnit(text, language) {
|
|
695
743
|
const normalized = normalizeUnitKey(text);
|
|
696
744
|
for (const key of unitKeys(language)) {
|
|
697
745
|
if (normalized !== key && !normalized.startsWith(`${key} `)) continue;
|
|
698
746
|
const unit = lookupUnit(key, language);
|
|
699
747
|
if (!unit) continue;
|
|
700
|
-
const
|
|
701
|
-
|
|
702
|
-
return { unit, rest
|
|
748
|
+
const rest = afterKey(text, key);
|
|
749
|
+
if (rest === null) continue;
|
|
750
|
+
return { unit, rest };
|
|
703
751
|
}
|
|
704
752
|
if (language === "en") {
|
|
705
753
|
const words = text.trimStart().split(/\s+/);
|
|
@@ -708,6 +756,14 @@ function takeUnit(text, language) {
|
|
|
708
756
|
}
|
|
709
757
|
return { unit: null, rest: text };
|
|
710
758
|
}
|
|
759
|
+
function afterKey(text, key) {
|
|
760
|
+
const words = text.trim().split(/\s+/);
|
|
761
|
+
for (let count = 1; count <= words.length; count += 1) {
|
|
762
|
+
if (normalizeUnitKey(words.slice(0, count).join(" ")) !== key) continue;
|
|
763
|
+
return words.slice(count).join(" ");
|
|
764
|
+
}
|
|
765
|
+
return null;
|
|
766
|
+
}
|
|
711
767
|
function takeUnitEitherLanguage(text, language) {
|
|
712
768
|
const first = takeUnit(text, language);
|
|
713
769
|
if (first.unit) return first;
|
|
@@ -723,13 +779,16 @@ function readEvidence(line) {
|
|
|
723
779
|
}
|
|
724
780
|
function parseIngredient(line, choice = "auto") {
|
|
725
781
|
const original = line;
|
|
726
|
-
const
|
|
782
|
+
const marked = readable(line);
|
|
783
|
+
const decoration = takeDecoration(marked);
|
|
784
|
+
const text = decoration ? marked.slice(decoration.length).trimStart() : marked;
|
|
727
785
|
const evidence = readEvidence(text);
|
|
728
786
|
const language = choice === "auto" ? evidence.language : choice;
|
|
729
787
|
const empty = (heldBack) => ({
|
|
730
788
|
original,
|
|
731
789
|
language,
|
|
732
790
|
heldBack,
|
|
791
|
+
decoration,
|
|
733
792
|
approximation: null,
|
|
734
793
|
measureAdjective: null,
|
|
735
794
|
amount: null,
|
|
@@ -738,6 +797,8 @@ function parseIngredient(line, choice = "auto") {
|
|
|
738
797
|
unit: null,
|
|
739
798
|
alternates: [],
|
|
740
799
|
alternateStyle: null,
|
|
800
|
+
alternateIntro: null,
|
|
801
|
+
capacity: null,
|
|
741
802
|
item: text,
|
|
742
803
|
articleWord: null,
|
|
743
804
|
countMultiplier: null
|
|
@@ -749,7 +810,10 @@ function parseIngredient(line, choice = "auto") {
|
|
|
749
810
|
const article = range ? null : readArticle(stated, language);
|
|
750
811
|
const quantity = range ?? parseLeadingQuantity(stated, language) ?? article;
|
|
751
812
|
if (!quantity) return empty(null);
|
|
752
|
-
|
|
813
|
+
const behindFigure = stated.slice(quantity.length);
|
|
814
|
+
if (new RegExp("^-\\p{L}", "u").test(behindFigure)) return empty("sizeQualifier");
|
|
815
|
+
if (quantity !== article && ORDINAL_SUFFIX.test(behindFigure)) return empty(null);
|
|
816
|
+
if (TIME_UNIT[language].test(behindFigure.trimStart())) return empty("duration");
|
|
753
817
|
let rest = stated.slice(quantity.length).trimStart();
|
|
754
818
|
if (language === "en") rest = rest.replace(/^(?:of\s+)?an?\s+/i, "");
|
|
755
819
|
const multiplier = readCountMultiplier(rest);
|
|
@@ -761,26 +825,44 @@ function parseIngredient(line, choice = "auto") {
|
|
|
761
825
|
const behind = described.adjective ? takeLeadingUnit(described.rest, language, fromArticle) : null;
|
|
762
826
|
const leading = direct.unit ? direct : behind?.unit ? behind : direct;
|
|
763
827
|
rest = leading.rest;
|
|
764
|
-
const
|
|
765
|
-
rest =
|
|
828
|
+
const compound = range ? null : takeCompoundMember(rest, leading.unit, language);
|
|
829
|
+
if (compound) rest = compound.rest;
|
|
830
|
+
const whole = quantity.amount + (compound?.adds ?? 0);
|
|
831
|
+
const introducedBracket = /^(?:de\s+|du\s+|des\s+|d'|of\s+)(?=\()/i.exec(rest);
|
|
832
|
+
const bracketed = takeAlternates(
|
|
833
|
+
introducedBracket ? rest.slice(introducedBracket[0].length) : rest,
|
|
834
|
+
language
|
|
835
|
+
);
|
|
836
|
+
rest = bracketed.measures.length > 0 ? bracketed.rest : rest;
|
|
766
837
|
const slashed = bracketed.measures.length > 0 ? null : takeSlashAlternates(rest, language);
|
|
767
838
|
if (slashed) rest = slashed.rest;
|
|
768
|
-
|
|
839
|
+
let item = stripItemLead(rest, language);
|
|
840
|
+
const trailing = bracketed.measures.length === 0 && !slashed ? takeTrailingAlternates(item, language) : null;
|
|
841
|
+
if (trailing) item = trailing.item;
|
|
842
|
+
const group = trailing ?? (bracketed.measures.length > 0 ? bracketed : null);
|
|
843
|
+
const capacity = group !== null && group.measures.every((measure) => measure.unit?.kind === "measured") && (namesContainer(leading.unit?.canonical) || namesContainer(item)) ? group.published : null;
|
|
769
844
|
if (language === "fr" && !leading.unit && statesItemSize(item)) return empty("itemSize");
|
|
770
845
|
if (language === "fr" && leading.partitive && isStatedSize(item)) return empty("itemSize");
|
|
846
|
+
if (leading.unit?.kind === "measured" && CONTAINER_NOUN.test(normalizeUnitKey(item))) {
|
|
847
|
+
return empty("containerSize");
|
|
848
|
+
}
|
|
849
|
+
const alternates = capacity !== null ? [] : slashed?.measures ?? group?.measures ?? [];
|
|
771
850
|
return {
|
|
772
851
|
original,
|
|
773
852
|
language,
|
|
774
853
|
heldBack: PER_PERSON[language].test(text) ? "perPerson" : null,
|
|
854
|
+
decoration,
|
|
775
855
|
approximation: loose ? loose[0] : null,
|
|
776
856
|
measureAdjective: behind?.unit ? described.adjective : null,
|
|
777
|
-
amount:
|
|
857
|
+
amount: whole * times,
|
|
778
858
|
amountMax: range === null ? null : range.max * times,
|
|
779
859
|
rangeSeparator: range?.separator ?? null,
|
|
780
860
|
unit: leading.unit,
|
|
781
|
-
alternates
|
|
782
|
-
alternateStyle: slashed ? "slash" :
|
|
783
|
-
|
|
861
|
+
alternates,
|
|
862
|
+
alternateStyle: alternates.length === 0 ? null : slashed ? "slash" : trailing ? "trailing" : "bracket",
|
|
863
|
+
alternateIntro: capacity !== null ? null : group?.intro ?? null,
|
|
864
|
+
capacity,
|
|
865
|
+
item,
|
|
784
866
|
articleWord: fromArticle ? article?.word ?? null : null,
|
|
785
867
|
countMultiplier: multiplier?.times ?? null
|
|
786
868
|
};
|
|
@@ -796,7 +878,7 @@ function unreadableComma(text, language, evidence) {
|
|
|
796
878
|
function readArticle(text, language) {
|
|
797
879
|
const counts = (rest) => takeLeadingUnit(rest, language, true).unit !== null || readCountMultiplier(rest) !== null;
|
|
798
880
|
if (language === "fr") {
|
|
799
|
-
const match = /^\s*(un|une
|
|
881
|
+
const match = /^\s*(un|une)\b\s*/i.exec(text);
|
|
800
882
|
if (!match) return null;
|
|
801
883
|
if (!counts(text.slice(match[0].length))) return null;
|
|
802
884
|
const word = match[1];
|
|
@@ -836,18 +918,35 @@ function stripItemLead(text, language) {
|
|
|
836
918
|
return text.replace(/^of\s+/i, "").replace(/^an?\s+/i, "").trim();
|
|
837
919
|
}
|
|
838
920
|
function takeAlternates(text, language) {
|
|
839
|
-
|
|
921
|
+
const none = { measures: [], intro: null, published: "", rest: text };
|
|
922
|
+
if (!text.startsWith("(")) return none;
|
|
840
923
|
const close = text.indexOf(")");
|
|
841
|
-
if (close < 0) return
|
|
842
|
-
const
|
|
843
|
-
|
|
924
|
+
if (close < 0) return none;
|
|
925
|
+
const read = readBracket(text.slice(0, close + 1), language);
|
|
926
|
+
if (!read) return none;
|
|
927
|
+
return { ...read, rest: text.slice(close + 1).trimStart() };
|
|
928
|
+
}
|
|
929
|
+
var EQUIVALENT_INTRODUCER = /^(?:soit|environ|about|approx\.?|approximately|or|ou|=)\s*/i;
|
|
930
|
+
var MEASURE_PARTITIVE = /^(?:de\s+la\s+|de\s+l'|d'|de\s+|du\s+|des\s+|of\s+)/i;
|
|
931
|
+
function takeMeasureAfterQuantity(text, language) {
|
|
932
|
+
const direct = takeUnitEitherLanguage(text, language);
|
|
933
|
+
if (direct.unit) return direct;
|
|
934
|
+
const partitive = MEASURE_PARTITIVE.exec(text);
|
|
935
|
+
if (!partitive) return direct;
|
|
936
|
+
return takeUnitEitherLanguage(text.slice(partitive[0].length), language);
|
|
937
|
+
}
|
|
938
|
+
function readBracket(bracket, language) {
|
|
939
|
+
const inside = bracket.slice(1, -1);
|
|
940
|
+
const intro = EQUIVALENT_INTRODUCER.exec(inside);
|
|
941
|
+
const body = intro ? inside.slice(intro[0].length) : inside;
|
|
942
|
+
const parts = body.split(/(?<!\d)\/|\/(?!\d)/).map((part) => part.trim());
|
|
844
943
|
const measures = [];
|
|
845
944
|
for (const part of parts) {
|
|
846
945
|
const range = parseLeadingRange(part, language);
|
|
847
946
|
const quantity = range ?? parseLeadingQuantity(part, language);
|
|
848
|
-
if (!quantity) return
|
|
849
|
-
const after =
|
|
850
|
-
if (!after.unit || after.rest.trim() !== "") return
|
|
947
|
+
if (!quantity) return null;
|
|
948
|
+
const after = takeMeasureAfterQuantity(part.slice(quantity.length).trimStart(), language);
|
|
949
|
+
if (!after.unit || after.rest.trim() !== "") return null;
|
|
851
950
|
measures.push({
|
|
852
951
|
amount: quantity.amount,
|
|
853
952
|
amountMax: range?.max ?? null,
|
|
@@ -855,8 +954,17 @@ function takeAlternates(text, language) {
|
|
|
855
954
|
unit: after.unit
|
|
856
955
|
});
|
|
857
956
|
}
|
|
858
|
-
if (measures.length === 0) return
|
|
859
|
-
return { measures,
|
|
957
|
+
if (measures.length === 0) return null;
|
|
958
|
+
return { measures, intro: intro ? intro[0].trim() : null, published: bracket };
|
|
959
|
+
}
|
|
960
|
+
function takeTrailingAlternates(item, language) {
|
|
961
|
+
const closing = /\s*(\([^()]*\))\s*$/.exec(item);
|
|
962
|
+
if (!closing) return null;
|
|
963
|
+
const read = readBracket(closing[1], language);
|
|
964
|
+
if (!read) return null;
|
|
965
|
+
const head = item.slice(0, closing.index).trim();
|
|
966
|
+
if (head === "") return null;
|
|
967
|
+
return { ...read, item: head };
|
|
860
968
|
}
|
|
861
969
|
function takeSlashAlternates(text, language) {
|
|
862
970
|
const measures = [];
|
|
@@ -907,6 +1015,7 @@ function roundTo(value, step) {
|
|
|
907
1015
|
return Math.round(Math.round(value / step) * step * 100) / 100;
|
|
908
1016
|
}
|
|
909
1017
|
function roundMeasured(value) {
|
|
1018
|
+
if (Number.isInteger(value)) return value;
|
|
910
1019
|
if (value >= 100) return roundTo(value, 5);
|
|
911
1020
|
if (value >= 10) return roundTo(value, 1);
|
|
912
1021
|
if (value >= 1) return roundTo(value, 0.1);
|
|
@@ -980,6 +1089,10 @@ function landedExactly(exact, amount) {
|
|
|
980
1089
|
if (gap > EXACT_WITHIN) return false;
|
|
981
1090
|
return exact === 0 || gap / Math.abs(exact) <= EXACT_SHARE;
|
|
982
1091
|
}
|
|
1092
|
+
function boundLandedExactly(bound) {
|
|
1093
|
+
const fine = bound.ratio >= 1 ? bound.ratio : 1;
|
|
1094
|
+
return landedExactly(bound.raw * fine, bound.amount * (fine / bound.ratio));
|
|
1095
|
+
}
|
|
983
1096
|
function scaleMeasure(low, high, unit, factor, divisibility) {
|
|
984
1097
|
const raws = high === null ? [low * factor] : [low * factor, high * factor];
|
|
985
1098
|
const sources = high === null ? [low] : [low, high];
|
|
@@ -995,7 +1108,8 @@ function scaleMeasure(low, high, unit, factor, divisibility) {
|
|
|
995
1108
|
amount: Math.min(usable, ceiling),
|
|
996
1109
|
exact,
|
|
997
1110
|
clamped: false,
|
|
998
|
-
raw
|
|
1111
|
+
raw,
|
|
1112
|
+
ratio
|
|
999
1113
|
};
|
|
1000
1114
|
}),
|
|
1001
1115
|
unit: target
|
|
@@ -1010,14 +1124,14 @@ function scaleMeasure(low, high, unit, factor, divisibility) {
|
|
|
1010
1124
|
const bounds2 = raws.map((raw, index) => {
|
|
1011
1125
|
const ceiling = factor < 1 ? sources[index] : Number.POSITIVE_INFINITY;
|
|
1012
1126
|
const rounded = roundSpoon(raw, ceiling);
|
|
1013
|
-
return { amount: rounded.value, exact: raw, clamped: rounded.clamped, raw };
|
|
1127
|
+
return { amount: rounded.value, exact: raw, clamped: rounded.clamped, raw, ratio: 1 };
|
|
1014
1128
|
});
|
|
1015
1129
|
return { bounds: bounds2, unit };
|
|
1016
1130
|
}
|
|
1017
1131
|
const bounds = raws.map((raw, index) => {
|
|
1018
1132
|
const ceiling = factor < 1 ? sources[index] : Number.POSITIVE_INFINITY;
|
|
1019
1133
|
const rounded = roundCountable(raw, divisibility, ceiling);
|
|
1020
|
-
return { amount: rounded.value, exact: raw, clamped: rounded.clamped, raw };
|
|
1134
|
+
return { amount: rounded.value, exact: raw, clamped: rounded.clamped, raw, ratio: 1 };
|
|
1021
1135
|
});
|
|
1022
1136
|
return { bounds, unit };
|
|
1023
1137
|
}
|
|
@@ -1146,6 +1260,8 @@ var IRREGULAR_SINGULAR = Object.fromEntries(
|
|
|
1146
1260
|
Object.entries(IRREGULAR_PLURAL).map(([one, many]) => [many, one])
|
|
1147
1261
|
);
|
|
1148
1262
|
function matchCase(source, replacement) {
|
|
1263
|
+
const hasLetter = new RegExp("\\p{L}", "u").test(source);
|
|
1264
|
+
if (hasLetter && source === source.toUpperCase()) return replacement.toUpperCase();
|
|
1149
1265
|
if (source[0] === source[0]?.toUpperCase() && source.slice(1) === source.slice(1).toLowerCase()) {
|
|
1150
1266
|
return replacement[0].toUpperCase() + replacement.slice(1);
|
|
1151
1267
|
}
|
|
@@ -1194,6 +1310,15 @@ function agreeInEnglish(item, amount) {
|
|
|
1194
1310
|
else return item;
|
|
1195
1311
|
return `${words.join(" ")}${tail}`;
|
|
1196
1312
|
}
|
|
1313
|
+
var FRENCH_OU_PLURAL_IN_X = /* @__PURE__ */ new Set([
|
|
1314
|
+
"bijou",
|
|
1315
|
+
"caillou",
|
|
1316
|
+
"chou",
|
|
1317
|
+
"genou",
|
|
1318
|
+
"hibou",
|
|
1319
|
+
"joujou",
|
|
1320
|
+
"pou"
|
|
1321
|
+
]);
|
|
1197
1322
|
var INVARIABLE_FRENCH_NOUN = /* @__PURE__ */ new Set([
|
|
1198
1323
|
"ananas",
|
|
1199
1324
|
"anis",
|
|
@@ -1271,15 +1396,18 @@ function agreeInFrench(item, amount) {
|
|
|
1271
1396
|
const head = words[0] ?? "";
|
|
1272
1397
|
if (head.length <= 3) return item;
|
|
1273
1398
|
const wantsPlural = amount >= 2;
|
|
1274
|
-
const
|
|
1399
|
+
const ouPlural = /x$/i.test(head) && FRENCH_OU_PLURAL_IN_X.has(foldWord(head.slice(0, -1)));
|
|
1400
|
+
const isPlural = /s$|eaux$|aux$/i.test(head) || ouPlural;
|
|
1275
1401
|
if (wantsPlural && !isPlural) {
|
|
1276
1402
|
if (/[sxz]$/i.test(head)) {
|
|
1277
1403
|
} else if (/eau$/i.test(head)) words[0] = `${head}x`;
|
|
1404
|
+
else if (FRENCH_OU_PLURAL_IN_X.has(foldWord(head))) words[0] = `${head}x`;
|
|
1278
1405
|
else if (/al$/i.test(head)) words[0] = `${head.slice(0, -2)}aux`;
|
|
1279
1406
|
else words[0] = `${head}s`;
|
|
1280
1407
|
} else if (!wantsPlural && isPlural) {
|
|
1281
1408
|
if (/eaux$/i.test(head)) words[0] = head.slice(0, -1);
|
|
1282
1409
|
else if (/aux$/i.test(head)) words[0] = `${head.slice(0, -3)}al`;
|
|
1410
|
+
else if (ouPlural) words[0] = head.slice(0, -1);
|
|
1283
1411
|
else if (INVARIABLE_FRENCH_NOUN.has(foldWord(head))) {
|
|
1284
1412
|
} else words[0] = head.slice(0, -1);
|
|
1285
1413
|
}
|
|
@@ -1303,6 +1431,12 @@ function agreeLeadingAdjective(word, amount, language) {
|
|
|
1303
1431
|
function agreeWithAmount(item, amount, language) {
|
|
1304
1432
|
return language === "fr" ? agreeInFrench(item, amount) : agreeInEnglish(item, amount);
|
|
1305
1433
|
}
|
|
1434
|
+
function agreeCountedContainer(item, amount, language) {
|
|
1435
|
+
if (language === "fr") return agreeInFrench(item, amount);
|
|
1436
|
+
const space = item.indexOf(" ");
|
|
1437
|
+
if (space < 0) return agreeInEnglish(item, amount);
|
|
1438
|
+
return `${agreeInEnglish(item.slice(0, space), amount)}${item.slice(space)}`;
|
|
1439
|
+
}
|
|
1306
1440
|
var MUTE_H_WORDS = /^(?:huile|huiles|huitre|huitres|huître|huîtres|herbe|herbes|hysope)\b/i;
|
|
1307
1441
|
function joinItem(item, language) {
|
|
1308
1442
|
if (!item) return "";
|
|
@@ -1361,7 +1495,9 @@ function scaleAlternative(tail, options) {
|
|
|
1361
1495
|
var HELD_BACK_NOTE = {
|
|
1362
1496
|
sizeQualifier: "The figures here give the size of one item rather than how many, so the line is left as published.",
|
|
1363
1497
|
itemSize: "The measure standing behind the item gives the size of one of them rather than how many, so the line is left as published. Serving more people is a matter of taking a bigger one, and serving fewer a smaller one.",
|
|
1498
|
+
containerSize: "The measure here gives what one container holds rather than how many of them the recipe uses, and the line states no count, so it is left as published. Scaling it is a matter of opening that many containers of the size named.",
|
|
1364
1499
|
perPerson: "This line already states an amount for one person, and the factor is what changes how many people the recipe serves, so the line is left as published.",
|
|
1500
|
+
duration: "This line states a length of time rather than an amount of an ingredient, so it is left as published. A rest, a proof or a bake takes as long for a large batch as for a small one, and the factor has nothing to say about it.",
|
|
1365
1501
|
ambiguousDecimal: "The comma in this number marks thousands in one convention and the decimal point in another, and the line gives no sign which was meant, so it is left as published."
|
|
1366
1502
|
};
|
|
1367
1503
|
function scaleSingleLine(line, options) {
|
|
@@ -1385,10 +1521,12 @@ function scaleSingleLine(line, options) {
|
|
|
1385
1521
|
const alternates = parsed.alternates.map((measure) => renderMeasure(measure, factor, language));
|
|
1386
1522
|
const primaryBounds = primary.bounds;
|
|
1387
1523
|
const alternateBounds = alternates.flatMap((entry) => entry.bounds);
|
|
1388
|
-
const movedPrimary = primaryBounds.some((
|
|
1389
|
-
const movedAlternate = alternateBounds.some((
|
|
1524
|
+
const movedPrimary = primaryBounds.some((bound) => !boundLandedExactly(bound));
|
|
1525
|
+
const movedAlternate = alternateBounds.some((bound) => !boundLandedExactly(bound));
|
|
1390
1526
|
const clamped = [...primaryBounds, ...alternateBounds].find((bound) => bound.clamped) ?? null;
|
|
1391
1527
|
const restated = parsed.alternateStyle === "slash";
|
|
1528
|
+
const carriesMore = hasEmbeddedMeasure(parsed.item, language);
|
|
1529
|
+
const leftover = carriesMore && parsed.unit?.kind === "measured";
|
|
1392
1530
|
const low = primaryBounds[0];
|
|
1393
1531
|
const high = primaryBounds[1] ?? null;
|
|
1394
1532
|
const unit = primary.unit;
|
|
@@ -1404,13 +1542,17 @@ function scaleSingleLine(line, options) {
|
|
|
1404
1542
|
const adjective = named && parsed.measureAdjective ? ` ${agreeLeadingAdjective(parsed.measureAdjective, shown, language)}` : "";
|
|
1405
1543
|
const unitLabel = named ? `${adjective} ${formatUnit(named, shown, language)}` : "";
|
|
1406
1544
|
const alternateTexts = alternates.map((entry) => entry.text);
|
|
1407
|
-
const
|
|
1408
|
-
const
|
|
1545
|
+
const introduced = parsed.alternateIntro ? `${parsed.alternateIntro} ` : "";
|
|
1546
|
+
const bracketed = alternates.length === 0 ? "" : ` (${introduced}${alternateTexts.join(" / ")})`;
|
|
1547
|
+
const altLabel = alternates.length === 0 ? "" : parsed.alternateStyle === "slash" ? ` / ${alternateTexts.join(" / ")}` : parsed.alternateStyle === "trailing" ? "" : bracketed;
|
|
1548
|
+
const trailingLabel = parsed.alternateStyle === "trailing" ? bracketed : "";
|
|
1549
|
+
const capacityLabel = parsed.capacity ? ` ${parsed.capacity}` : "";
|
|
1550
|
+
const counted = parsed.capacity ? agreeCountedContainer(parsed.item, shown, language) : agreeWithAmount(parsed.item, shown, language);
|
|
1409
1551
|
const itemLabel = named ? joinItem(parsed.item, language) : counted ? ` ${counted}` : "";
|
|
1410
1552
|
const result = {
|
|
1411
|
-
text: `${parsed.approximation ?? ""}${amountText}${unitLabel}${altLabel}${itemLabel}`.trim(),
|
|
1553
|
+
text: `${parsed.decoration ? `${parsed.decoration} ` : ""}${parsed.approximation ?? ""}${amountText}${unitLabel}${capacityLabel}${altLabel}${itemLabel}${trailingLabel}`.trim(),
|
|
1412
1554
|
original: parsed.original,
|
|
1413
|
-
scaling: movedPrimary || movedAlternate || restated ? "rounded" : "scaled",
|
|
1555
|
+
scaling: movedPrimary || movedAlternate || restated || leftover ? "rounded" : "scaled",
|
|
1414
1556
|
amount: low.amount,
|
|
1415
1557
|
amountMax: collapsed ? null : high?.amount ?? null,
|
|
1416
1558
|
unit: named?.canonical ?? null,
|
|
@@ -1423,7 +1565,7 @@ function scaleSingleLine(line, options) {
|
|
|
1423
1565
|
`Clamped up to ${formatAmount(clamped.amount, language)} from ${formatAmount(Math.round(clamped.raw * 1e3) / 1e3, language)}, the smallest amount worth measuring. This line no longer holds its share of the recipe.`
|
|
1424
1566
|
);
|
|
1425
1567
|
} else if (movedPrimary) {
|
|
1426
|
-
const moved = primaryBounds.filter((bound) => !
|
|
1568
|
+
const moved = primaryBounds.filter((bound) => !boundLandedExactly(bound));
|
|
1427
1569
|
sentences.push(
|
|
1428
1570
|
moved.map(
|
|
1429
1571
|
(bound) => `Rounded ${bound.amount > bound.exact ? "up" : "down"} from ${asPublished(bound.raw, parsed.unit)}.`
|
|
@@ -1438,11 +1580,16 @@ function scaleSingleLine(line, options) {
|
|
|
1438
1580
|
"This line states one quantity twice, and both readings were scaled. They agree as closely as the page wrote them, and no closer."
|
|
1439
1581
|
);
|
|
1440
1582
|
}
|
|
1441
|
-
if (
|
|
1583
|
+
if (carriesMore) {
|
|
1442
1584
|
sentences.push(
|
|
1443
1585
|
"This line carries a further quantity after the first one, and only the first was scaled. Read the rest as published."
|
|
1444
1586
|
);
|
|
1445
1587
|
}
|
|
1588
|
+
if (parsed.capacity) {
|
|
1589
|
+
sentences.push(
|
|
1590
|
+
`${parsed.capacity} is what one container holds, so it stays as published and the count is what the factor moved. Scaling this line is a matter of opening that many containers of the size named.`
|
|
1591
|
+
);
|
|
1592
|
+
}
|
|
1446
1593
|
if (collapsed) {
|
|
1447
1594
|
sentences.push("The page gave a range, and at this size both ends come to the same amount.");
|
|
1448
1595
|
}
|