mcp-recipes 1.3.1 → 2.0.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 +14 -0
- package/README.md +38 -2
- package/dist/index.js +1298 -552
- package/dist/index.js.map +1 -1
- package/dist/recipe/scale.js +861 -326
- package/dist/recipe/scale.js.map +1 -1
- package/dist/sources/client.d.ts +13 -133
- package/dist/sources/client.js +217 -153
- package/dist/sources/client.js.map +1 -1
- package/package.json +18 -9
- package/server.json +78 -0
package/dist/recipe/scale.js
CHANGED
|
@@ -13,17 +13,41 @@ function readLanguage(line, evidence = {
|
|
|
13
13
|
}) {
|
|
14
14
|
let french = 0;
|
|
15
15
|
let english = 0;
|
|
16
|
-
if (FRENCH_WORDS.test(line))
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (
|
|
16
|
+
if (FRENCH_WORDS.test(line)) {
|
|
17
|
+
french += STRUCTURAL;
|
|
18
|
+
}
|
|
19
|
+
if (FRENCH_LETTERS.test(line)) {
|
|
20
|
+
french += STRUCTURAL;
|
|
21
|
+
}
|
|
22
|
+
if (ENGLISH_WORDS.test(line)) {
|
|
23
|
+
english += STRUCTURAL;
|
|
24
|
+
}
|
|
25
|
+
if (evidence.frenchUnit) {
|
|
26
|
+
french += VOCABULARY;
|
|
27
|
+
}
|
|
28
|
+
if (evidence.englishUnit) {
|
|
29
|
+
english += VOCABULARY;
|
|
30
|
+
}
|
|
31
|
+
if (FRENCH_FOODS.test(line)) {
|
|
32
|
+
french += NAME;
|
|
33
|
+
}
|
|
34
|
+
if (ENGLISH_FOODS.test(line)) {
|
|
35
|
+
english += NAME;
|
|
36
|
+
}
|
|
23
37
|
return { language: french > english ? "fr" : "en", french, english };
|
|
24
38
|
}
|
|
25
39
|
|
|
26
40
|
// src/recipe/units.ts
|
|
41
|
+
var AL_ENDING = /al$/i;
|
|
42
|
+
var AUX_ENDING = /aux$/i;
|
|
43
|
+
var EAUX_ENDING = /eaux$/i;
|
|
44
|
+
var EAU_ENDING = /eau$/i;
|
|
45
|
+
var FUL_MEASURE = /^[a-z]{3,}fuls?$/;
|
|
46
|
+
var HEAD_BEFORE_DE = new RegExp("^\\s*(\\p{L}+)\\s+(?=(?:de|du|des)\\s|d')", "u");
|
|
47
|
+
var HOUSEHOLD_MEASURE = /^(cup|tablespoon|Tbsp|teaspoon|tsp|cuillère à soupe|cuillère à café|tasse)$/;
|
|
48
|
+
var SIBILANT_LETTER = /[sxz]$/i;
|
|
49
|
+
var TRAILING_S_ANY_CASE = /s$/i;
|
|
50
|
+
var VOWEL_THEN_S = /[aiou]s$/i;
|
|
27
51
|
var SHARED_UNITS = {
|
|
28
52
|
mg: { canonical: "mg", kind: "measured", system: "metric", symbol: true },
|
|
29
53
|
g: { canonical: "g", kind: "measured", system: "metric", symbol: true },
|
|
@@ -357,12 +381,20 @@ var NOT_A_MEASURE = /* @__PURE__ */ new Set([
|
|
|
357
381
|
"ensemble"
|
|
358
382
|
]);
|
|
359
383
|
function readPartitiveMeasure(text) {
|
|
360
|
-
const match =
|
|
361
|
-
if (!match)
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
if (
|
|
384
|
+
const match = HEAD_BEFORE_DE.exec(text);
|
|
385
|
+
if (!match) {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
const [word = ""] = match.slice(1);
|
|
389
|
+
if (word.length < 3) {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
if (NOT_A_MEASURE.has(normalizeUnitKey(word))) {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
if (lookupUnit(word, "fr")) {
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
366
398
|
const canonical = frenchSingular(word);
|
|
367
399
|
return {
|
|
368
400
|
unit: {
|
|
@@ -376,21 +408,37 @@ function readPartitiveMeasure(text) {
|
|
|
376
408
|
}
|
|
377
409
|
function readContainerLoad(word) {
|
|
378
410
|
const key = normalizeUnitKey(word);
|
|
379
|
-
if (
|
|
411
|
+
if (!FUL_MEASURE.test(key)) {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
380
414
|
const canonical = key.endsWith("fuls") ? key.slice(0, -1) : key;
|
|
381
415
|
return { canonical, kind: "approximate", system: "none", plural: `${canonical}s` };
|
|
382
416
|
}
|
|
383
417
|
function frenchSingular(word) {
|
|
384
|
-
if (
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
if (
|
|
418
|
+
if (EAUX_ENDING.test(word)) {
|
|
419
|
+
return word.slice(0, -1);
|
|
420
|
+
}
|
|
421
|
+
if (AUX_ENDING.test(word)) {
|
|
422
|
+
return `${word.slice(0, -3)}al`;
|
|
423
|
+
}
|
|
424
|
+
if (VOWEL_THEN_S.test(word)) {
|
|
425
|
+
return word;
|
|
426
|
+
}
|
|
427
|
+
if (TRAILING_S_ANY_CASE.test(word) && word.length > 3) {
|
|
428
|
+
return word.slice(0, -1);
|
|
429
|
+
}
|
|
388
430
|
return word;
|
|
389
431
|
}
|
|
390
432
|
function frenchPlural(word) {
|
|
391
|
-
if (
|
|
392
|
-
|
|
393
|
-
|
|
433
|
+
if (SIBILANT_LETTER.test(word)) {
|
|
434
|
+
return word;
|
|
435
|
+
}
|
|
436
|
+
if (EAU_ENDING.test(word)) {
|
|
437
|
+
return `${word}x`;
|
|
438
|
+
}
|
|
439
|
+
if (AL_ENDING.test(word)) {
|
|
440
|
+
return `${word.slice(0, -2)}aux`;
|
|
441
|
+
}
|
|
394
442
|
return `${word}s`;
|
|
395
443
|
}
|
|
396
444
|
var APPROXIMATE_EQUIVALENT = {
|
|
@@ -451,17 +499,19 @@ var DEMOTIONS = {
|
|
|
451
499
|
};
|
|
452
500
|
function demoteUnit(unit) {
|
|
453
501
|
const step = DEMOTIONS[normalizeUnitKey(unit.canonical)];
|
|
454
|
-
if (!step)
|
|
502
|
+
if (!step) {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
455
505
|
const target = lookupUnit(step.to, step.language);
|
|
456
506
|
return target ? { unit: target, per: step.per } : null;
|
|
457
507
|
}
|
|
458
508
|
function isSpoonMeasure(unit) {
|
|
459
|
-
return
|
|
460
|
-
unit.canonical
|
|
461
|
-
);
|
|
509
|
+
return HOUSEHOLD_MEASURE.test(unit.canonical);
|
|
462
510
|
}
|
|
463
511
|
function unitDivisibility(unit) {
|
|
464
|
-
if (unit.kind === "approximate")
|
|
512
|
+
if (unit.kind === "approximate") {
|
|
513
|
+
return "whole";
|
|
514
|
+
}
|
|
465
515
|
return QUARTERED_MEASURE.test(unit.canonical) ? "quarter" : "half";
|
|
466
516
|
}
|
|
467
517
|
var QUARTERED_MEASURE = /\b(pots?|bouteilles?|bottles?|jars?|blocs?|blocks?|tranches?|slices?)\b/i;
|
|
@@ -476,7 +526,9 @@ function chooseReadableUnit(unit, amount) {
|
|
|
476
526
|
let ratio = 1;
|
|
477
527
|
while (amount * ratio < 1) {
|
|
478
528
|
const step = demoteUnit(current);
|
|
479
|
-
if (!step)
|
|
529
|
+
if (!step) {
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
480
532
|
ratio *= step.per;
|
|
481
533
|
current = step.unit;
|
|
482
534
|
}
|
|
@@ -490,7 +542,9 @@ function chooseReadableUnit(unit, amount) {
|
|
|
490
542
|
}
|
|
491
543
|
while (!writesExactly(amount * ratio)) {
|
|
492
544
|
const step = demoteUnit(current);
|
|
493
|
-
if (!step)
|
|
545
|
+
if (!step) {
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
494
548
|
ratio *= step.per;
|
|
495
549
|
current = step.unit;
|
|
496
550
|
}
|
|
@@ -500,13 +554,64 @@ function writesExactly(value) {
|
|
|
500
554
|
return Math.abs(value * 100 - Math.round(value * 100)) < 1e-9;
|
|
501
555
|
}
|
|
502
556
|
function formatUnit(unit, amount, language) {
|
|
503
|
-
if (unit.symbol)
|
|
557
|
+
if (unit.symbol) {
|
|
558
|
+
return unit.canonical;
|
|
559
|
+
}
|
|
504
560
|
const singular = language === "fr" ? amount < 2 : amount <= 1;
|
|
505
|
-
if (singular)
|
|
561
|
+
if (singular) {
|
|
562
|
+
return unit.canonical;
|
|
563
|
+
}
|
|
506
564
|
return unit.plural ?? `${unit.canonical}s`;
|
|
507
565
|
}
|
|
508
566
|
|
|
509
567
|
// src/recipe/quantity.ts
|
|
568
|
+
var BRACKETED_TAIL = /\s*(\([^()]*\))\s*$/;
|
|
569
|
+
var LEADING_ARTICLE = /^(?:of\s+)?an?\s+/i;
|
|
570
|
+
var LEADING_ARTICLE_FRENCH = /^(?:une|un)\s+/i;
|
|
571
|
+
var LEADING_INDEFINITE_WITH_SPACE = /^an?\s+/i;
|
|
572
|
+
var LEADING_PARTITIVE = /^(?:de\s+la\s+|de\s+l'|d'|de\s+|du\s+|des\s+)/i;
|
|
573
|
+
var OF_OPENING = /^of\s+/i;
|
|
574
|
+
var OPENING_ARTICLE_FRENCH = /^\s*(un|une)\b\s*/i;
|
|
575
|
+
var REPEATED_COMMA_GROUPS = /^\s*\d+,\d+,\d/;
|
|
576
|
+
var SLASH_OUTSIDE_A_FRACTION = /(?<!\d)\/|\/(?!\d)/;
|
|
577
|
+
var ATTACHED_DE = /\s+de\s+(?=\d)/i;
|
|
578
|
+
var DECIMAL = /^(\d+(?:[.,]\d+)?)/;
|
|
579
|
+
var DIGIT = /\d/;
|
|
580
|
+
var ENGLISH_RANGE_JOINER = /^\s+(to|or)\s+/i;
|
|
581
|
+
var FRACTION_WORD = /^(?:(a|an|one|two|three)[\s-]+)?(halves|half|thirds|third|quarters|quarter|fourths|fourth)\b/i;
|
|
582
|
+
var FRENCH_RANGE_JOINER = /^\s+(à|a|ou)\s+/i;
|
|
583
|
+
var GROUPED_DECIMAL = /^(\d{1,3}(?:,\d{3})+(?:\.\d+)?|\d+(?:\.\d+)?)/;
|
|
584
|
+
var HYPHENATED_TAIL = new RegExp("^-\\p{L}", "u");
|
|
585
|
+
var LEADING_DASH = /^\s*(–|—|-)\s*/;
|
|
586
|
+
var LEADING_INDEFINITE = /^an?\s/i;
|
|
587
|
+
var LEADING_OF = /^\s*of\s+/i;
|
|
588
|
+
var LEADING_QUANTITY = /^[\s\d.,/½⅓⅔¼¾⅕⅖⅗⅘⅙⅚⅛⅜⅝⅞\-–—]+/u;
|
|
589
|
+
var LEADING_WORD = new RegExp("^\\s*(\\p{L}+)\\s+", "u");
|
|
590
|
+
var MIXED_FRACTION = /^(\d+)\s+(\d+)\s*\/\s*(\d+)/;
|
|
591
|
+
var PARTITIVE_BEFORE_BRACKET = /^(?:de\s+|du\s+|des\s+|d'|of\s+)(?=\()/i;
|
|
592
|
+
var PARTITIVE_OPENING = /^(?:de\s|d'|du\s|des\s|,)/i;
|
|
593
|
+
var SIMPLE_FRACTION = /^(\d+)\s*\/\s*(\d+)/;
|
|
594
|
+
var TRAILING_S = /s$/;
|
|
595
|
+
var VAGUE_ARTICLE = /^(?:un|une|quelques|of\s+an?|an?)\s+/iu;
|
|
596
|
+
var WHITESPACE = /\s+/;
|
|
597
|
+
function leadingMeasure(direct, behind) {
|
|
598
|
+
if (direct.unit) {
|
|
599
|
+
return direct;
|
|
600
|
+
}
|
|
601
|
+
return behind?.unit ? behind : direct;
|
|
602
|
+
}
|
|
603
|
+
function alternateStyleOf(count, slashed, trailing) {
|
|
604
|
+
if (count === 0) {
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
if (slashed) {
|
|
608
|
+
return "slash";
|
|
609
|
+
}
|
|
610
|
+
if (trailing) {
|
|
611
|
+
return "trailing";
|
|
612
|
+
}
|
|
613
|
+
return "bracket";
|
|
614
|
+
}
|
|
510
615
|
var VULGAR_FRACTIONS = {
|
|
511
616
|
"\xBD": 0.5,
|
|
512
617
|
"\u2153": 1 / 3,
|
|
@@ -546,13 +651,18 @@ var NAMED_ENTITIES = {
|
|
|
546
651
|
mdash: "\u2014"
|
|
547
652
|
};
|
|
548
653
|
function decodeEntities(text) {
|
|
549
|
-
return text.replace(
|
|
654
|
+
return text.replace(
|
|
655
|
+
/&#x([0-9a-f]+);/gi,
|
|
656
|
+
(whole, hex) => codePoint(Number.parseInt(hex, 16), whole)
|
|
657
|
+
).replace(/&#(\d+);/g, (whole, digits) => codePoint(Number(digits), whole)).replace(
|
|
550
658
|
/&([a-z][a-z0-9]*);/gi,
|
|
551
659
|
(whole, name) => NAMED_ENTITIES[name.toLowerCase()] ?? whole
|
|
552
660
|
);
|
|
553
661
|
}
|
|
554
662
|
function codePoint(value, published) {
|
|
555
|
-
if (!Number.isInteger(value) || value < 32 || value > 1114111)
|
|
663
|
+
if (!Number.isInteger(value) || value < 32 || value > 1114111) {
|
|
664
|
+
return published;
|
|
665
|
+
}
|
|
556
666
|
try {
|
|
557
667
|
return String.fromCodePoint(value);
|
|
558
668
|
} catch {
|
|
@@ -569,37 +679,59 @@ var APPROXIMATION_PREFIX = {
|
|
|
569
679
|
};
|
|
570
680
|
var CONTAINER_CONTENTS = /^\s*(?:de\s|d'|du\s|des\s)/i;
|
|
571
681
|
function statesItemSize(item) {
|
|
572
|
-
const attached =
|
|
573
|
-
if (!attached)
|
|
682
|
+
const attached = ATTACHED_DE.exec(item);
|
|
683
|
+
if (!attached) {
|
|
684
|
+
return false;
|
|
685
|
+
}
|
|
574
686
|
const named = item.slice(0, attached.index).trim();
|
|
575
|
-
if (!named ||
|
|
687
|
+
if (!named || DIGIT.test(named)) {
|
|
688
|
+
return false;
|
|
689
|
+
}
|
|
576
690
|
return isStatedSize(item.slice(attached.index + attached[0].length));
|
|
577
691
|
}
|
|
578
692
|
function isStatedSize(text) {
|
|
579
693
|
const size = parseLeadingQuantity(text, "fr");
|
|
580
|
-
if (!size)
|
|
694
|
+
if (!size) {
|
|
695
|
+
return false;
|
|
696
|
+
}
|
|
581
697
|
const measure = takeUnit(text.slice(size.length).trimStart(), "fr");
|
|
582
|
-
if (
|
|
698
|
+
if (measure.unit?.kind !== "measured") {
|
|
699
|
+
return false;
|
|
700
|
+
}
|
|
583
701
|
return !CONTAINER_CONTENTS.test(measure.rest);
|
|
584
702
|
}
|
|
585
703
|
function takeCompoundMember(text, unit, language) {
|
|
586
|
-
if (!unit)
|
|
704
|
+
if (!unit) {
|
|
705
|
+
return null;
|
|
706
|
+
}
|
|
587
707
|
const step = demoteUnit(unit);
|
|
588
|
-
if (!step)
|
|
708
|
+
if (!step) {
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
589
711
|
const joiner = ADDS_UP.exec(text);
|
|
590
|
-
|
|
591
|
-
const second = parseLeadingQuantity(
|
|
592
|
-
if (!second || second.amount <= 0 || second.amount >= step.per)
|
|
593
|
-
|
|
712
|
+
const behindTheJoiner = joiner ? text.slice(joiner[0].length) : text;
|
|
713
|
+
const second = parseLeadingQuantity(behindTheJoiner, language);
|
|
714
|
+
if (!second || second.amount <= 0 || second.amount >= step.per) {
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
const after = behindTheJoiner.slice(second.length);
|
|
594
718
|
const measure = takeUnit(after.trimStart(), language);
|
|
595
719
|
if (measure.unit) {
|
|
596
|
-
if (measure.unit.canonical !== step.unit.canonical)
|
|
720
|
+
if (measure.unit.canonical !== step.unit.canonical) {
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
597
723
|
return { adds: second.amount / step.per, rest: measure.rest.trimStart() };
|
|
598
724
|
}
|
|
599
|
-
if (unit.kind !== "measured")
|
|
600
|
-
|
|
725
|
+
if (unit.kind !== "measured") {
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
if (!Number.isInteger(second.amount)) {
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
601
731
|
const trailing = after.trimStart();
|
|
602
|
-
if (trailing !== "" &&
|
|
732
|
+
if (trailing !== "" && !PARTITIVE_OPENING.test(trailing)) {
|
|
733
|
+
return null;
|
|
734
|
+
}
|
|
603
735
|
return { adds: second.amount / step.per, rest: trailing };
|
|
604
736
|
}
|
|
605
737
|
var ADDS_UP = /^\s*(?:\+|et|and|plus)\s+/i;
|
|
@@ -647,12 +779,17 @@ var MEASURE_ADJECTIVES = {
|
|
|
647
779
|
])
|
|
648
780
|
};
|
|
649
781
|
function takeMeasureAdjective(text, language) {
|
|
650
|
-
const match =
|
|
651
|
-
if (!match)
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
782
|
+
const match = LEADING_WORD.exec(text);
|
|
783
|
+
if (!match) {
|
|
784
|
+
return { adjective: null, rest: text };
|
|
785
|
+
}
|
|
786
|
+
const [adjective = ""] = match.slice(1);
|
|
787
|
+
const folded = normalizeUnitKey(adjective);
|
|
788
|
+
const listed = MEASURE_ADJECTIVES[language].has(folded) || MEASURE_ADJECTIVES[language].has(folded.replace(TRAILING_S, ""));
|
|
789
|
+
if (!listed) {
|
|
790
|
+
return { adjective: null, rest: text };
|
|
791
|
+
}
|
|
792
|
+
return { adjective, rest: text.slice(match[0].length) };
|
|
656
793
|
}
|
|
657
794
|
var COMMA_GROUPED = /^\s*\d{1,3}(?:,\d{3})+(?!\d)/;
|
|
658
795
|
var COMMA_DECIMAL = /^\s*\d+,\d+/;
|
|
@@ -662,10 +799,10 @@ function parseLeadingQuantity(text, language) {
|
|
|
662
799
|
const mixedGlyph = new RegExp(`^(\\d+)\\s*([${VULGAR_CLASS}])`).exec(trimmed);
|
|
663
800
|
if (mixedGlyph) {
|
|
664
801
|
const whole = Number(mixedGlyph[1]);
|
|
665
|
-
const fraction2 = VULGAR_FRACTIONS[mixedGlyph[2]];
|
|
802
|
+
const fraction2 = VULGAR_FRACTIONS[mixedGlyph[2] ?? ""] ?? Number.NaN;
|
|
666
803
|
return { amount: whole + fraction2, length: offset + mixedGlyph[0].length };
|
|
667
804
|
}
|
|
668
|
-
const mixed =
|
|
805
|
+
const mixed = MIXED_FRACTION.exec(trimmed);
|
|
669
806
|
if (mixed) {
|
|
670
807
|
const denominator = Number(mixed[3]);
|
|
671
808
|
if (denominator !== 0) {
|
|
@@ -675,27 +812,33 @@ function parseLeadingQuantity(text, language) {
|
|
|
675
812
|
};
|
|
676
813
|
}
|
|
677
814
|
}
|
|
678
|
-
const fraction =
|
|
815
|
+
const fraction = SIMPLE_FRACTION.exec(trimmed);
|
|
679
816
|
if (fraction) {
|
|
680
817
|
const denominator = Number(fraction[2]);
|
|
681
|
-
if (denominator === 0)
|
|
818
|
+
if (denominator === 0) {
|
|
819
|
+
return null;
|
|
820
|
+
}
|
|
682
821
|
return { amount: Number(fraction[1]) / denominator, length: offset + fraction[0].length };
|
|
683
822
|
}
|
|
684
823
|
const glyph = trimmed[0];
|
|
685
824
|
if (glyph && glyph in VULGAR_FRACTIONS) {
|
|
686
|
-
return { amount: VULGAR_FRACTIONS[glyph], length: offset + 1 };
|
|
825
|
+
return { amount: VULGAR_FRACTIONS[glyph] ?? Number.NaN, length: offset + 1 };
|
|
687
826
|
}
|
|
688
|
-
const decimal = (language === "fr" ?
|
|
827
|
+
const decimal = (language === "fr" ? DECIMAL : GROUPED_DECIMAL).exec(trimmed);
|
|
689
828
|
if (decimal) {
|
|
690
|
-
const written = decimal
|
|
829
|
+
const [written = ""] = decimal.slice(1);
|
|
691
830
|
const amount = Number(
|
|
692
831
|
language === "fr" ? written.replace(",", ".") : written.replace(/,/g, "")
|
|
693
832
|
);
|
|
694
|
-
if (Number.isFinite(amount))
|
|
833
|
+
if (Number.isFinite(amount)) {
|
|
834
|
+
return { amount, length: offset + decimal[0].length };
|
|
835
|
+
}
|
|
695
836
|
}
|
|
696
837
|
if (language === "en") {
|
|
697
838
|
const written = parseWrittenFraction(trimmed);
|
|
698
|
-
if (written)
|
|
839
|
+
if (written) {
|
|
840
|
+
return { amount: written.amount, length: offset + written.length };
|
|
841
|
+
}
|
|
699
842
|
}
|
|
700
843
|
return null;
|
|
701
844
|
}
|
|
@@ -711,30 +854,40 @@ var WRITTEN_DENOMINATORS = {
|
|
|
711
854
|
fourths: 4
|
|
712
855
|
};
|
|
713
856
|
function parseWrittenFraction(text) {
|
|
714
|
-
const match =
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
857
|
+
const match = FRACTION_WORD.exec(text);
|
|
858
|
+
if (!match) {
|
|
859
|
+
return null;
|
|
860
|
+
}
|
|
718
861
|
const numerator = match[1] ? WRITTEN_NUMERATORS[match[1].toLowerCase()] : 1;
|
|
719
|
-
const denominator = WRITTEN_DENOMINATORS[match[2].toLowerCase()];
|
|
720
|
-
if (!numerator
|
|
721
|
-
|
|
722
|
-
|
|
862
|
+
const denominator = WRITTEN_DENOMINATORS[(match[2] ?? "").toLowerCase()];
|
|
863
|
+
if (!(numerator && denominator)) {
|
|
864
|
+
return null;
|
|
865
|
+
}
|
|
866
|
+
const rest = text.slice(match[0].length).replace(LEADING_OF, "").trimStart();
|
|
867
|
+
if (!(LEADING_INDEFINITE.test(rest) || takeUnit(rest, "en").unit)) {
|
|
868
|
+
return null;
|
|
869
|
+
}
|
|
723
870
|
return { amount: numerator / denominator, length: match[0].length };
|
|
724
871
|
}
|
|
725
872
|
function parseLeadingRange(text, language) {
|
|
726
873
|
const low = parseLeadingQuantity(text, language);
|
|
727
|
-
if (!low)
|
|
874
|
+
if (!low) {
|
|
875
|
+
return null;
|
|
876
|
+
}
|
|
728
877
|
const after = text.slice(low.length);
|
|
729
|
-
const written = language === "fr" ?
|
|
730
|
-
const separator = written.exec(after) ??
|
|
731
|
-
if (!separator)
|
|
878
|
+
const written = language === "fr" ? FRENCH_RANGE_JOINER : ENGLISH_RANGE_JOINER;
|
|
879
|
+
const separator = written.exec(after) ?? LEADING_DASH.exec(after);
|
|
880
|
+
if (!separator) {
|
|
881
|
+
return null;
|
|
882
|
+
}
|
|
732
883
|
const high = parseLeadingQuantity(after.slice(separator[0].length), language);
|
|
733
|
-
if (!high || high.amount <= low.amount)
|
|
884
|
+
if (!high || high.amount <= low.amount) {
|
|
885
|
+
return null;
|
|
886
|
+
}
|
|
734
887
|
return {
|
|
735
888
|
amount: low.amount,
|
|
736
889
|
max: high.amount,
|
|
737
|
-
separator: separator[1],
|
|
890
|
+
separator: separator[1] ?? "",
|
|
738
891
|
length: low.length + separator[0].length + high.length
|
|
739
892
|
};
|
|
740
893
|
}
|
|
@@ -742,41 +895,122 @@ var FRENCH_ARTICLES = { un: 1, une: 1 };
|
|
|
742
895
|
function takeUnit(text, language) {
|
|
743
896
|
const normalized = normalizeUnitKey(text);
|
|
744
897
|
for (const key of unitKeys(language)) {
|
|
745
|
-
if (normalized !== key && !normalized.startsWith(`${key} `))
|
|
898
|
+
if (normalized !== key && !normalized.startsWith(`${key} `)) {
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
746
901
|
const unit = lookupUnit(key, language);
|
|
747
|
-
if (!unit)
|
|
902
|
+
if (!unit) {
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
748
905
|
const rest = afterKey(text, key);
|
|
749
|
-
if (rest === null)
|
|
906
|
+
if (rest === null) {
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
750
909
|
return { unit, rest };
|
|
751
910
|
}
|
|
752
911
|
if (language === "en") {
|
|
753
|
-
const words = text.trimStart().split(
|
|
912
|
+
const words = text.trimStart().split(WHITESPACE);
|
|
754
913
|
const load = words[0] ? readContainerLoad(words[0]) : null;
|
|
755
|
-
if (load)
|
|
914
|
+
if (load) {
|
|
915
|
+
return { unit: load, rest: words.slice(1).join(" ") };
|
|
916
|
+
}
|
|
756
917
|
}
|
|
757
918
|
return { unit: null, rest: text };
|
|
758
919
|
}
|
|
759
920
|
function afterKey(text, key) {
|
|
760
|
-
const words = text.trim().split(
|
|
921
|
+
const words = text.trim().split(WHITESPACE);
|
|
761
922
|
for (let count = 1; count <= words.length; count += 1) {
|
|
762
|
-
if (normalizeUnitKey(words.slice(0, count).join(" ")) !== key)
|
|
923
|
+
if (normalizeUnitKey(words.slice(0, count).join(" ")) !== key) {
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
763
926
|
return words.slice(count).join(" ");
|
|
764
927
|
}
|
|
765
928
|
return null;
|
|
766
929
|
}
|
|
767
930
|
function takeUnitEitherLanguage(text, language) {
|
|
768
931
|
const first = takeUnit(text, language);
|
|
769
|
-
if (first.unit)
|
|
932
|
+
if (first.unit) {
|
|
933
|
+
return first;
|
|
934
|
+
}
|
|
770
935
|
return takeUnit(text, language === "fr" ? "en" : "fr");
|
|
771
936
|
}
|
|
772
937
|
function readEvidence(line) {
|
|
773
938
|
const text = line.trim();
|
|
774
|
-
const afterFigures = text.replace(
|
|
775
|
-
const afterArticle = afterFigures.replace(
|
|
939
|
+
const afterFigures = text.replace(LEADING_QUANTITY, "").trimStart();
|
|
940
|
+
const afterArticle = afterFigures.replace(VAGUE_ARTICLE, "");
|
|
776
941
|
const frenchUnit = takeUnit(afterArticle, "fr").unit !== null;
|
|
777
942
|
const englishUnit = takeUnit(afterArticle, "en").unit !== null;
|
|
778
943
|
return readLanguage(text, { frenchUnit, englishUnit });
|
|
779
944
|
}
|
|
945
|
+
function readMeasure(rest, language, fromArticle) {
|
|
946
|
+
const direct = takeLeadingUnit(rest, language, fromArticle);
|
|
947
|
+
const described = direct.unit ? { adjective: null, rest } : takeMeasureAdjective(rest, language);
|
|
948
|
+
const behind = described.adjective ? takeLeadingUnit(described.rest, language, fromArticle) : null;
|
|
949
|
+
return { leading: leadingMeasure(direct, behind), described, behind };
|
|
950
|
+
}
|
|
951
|
+
function whatTheFigureIsNot(behindFigure, couldBeARank, language) {
|
|
952
|
+
if (HYPHENATED_TAIL.test(behindFigure)) {
|
|
953
|
+
return "sizeQualifier";
|
|
954
|
+
}
|
|
955
|
+
if (couldBeARank && ORDINAL_SUFFIX.test(behindFigure)) {
|
|
956
|
+
return null;
|
|
957
|
+
}
|
|
958
|
+
if (TIME_UNIT[language].test(behindFigure.trimStart())) {
|
|
959
|
+
return "duration";
|
|
960
|
+
}
|
|
961
|
+
return false;
|
|
962
|
+
}
|
|
963
|
+
function takeEquivalents(text, language) {
|
|
964
|
+
const introducedBracket = PARTITIVE_BEFORE_BRACKET.exec(text);
|
|
965
|
+
const bracketed = takeAlternates(
|
|
966
|
+
introducedBracket ? text.slice(introducedBracket[0].length) : text,
|
|
967
|
+
language
|
|
968
|
+
);
|
|
969
|
+
let rest = bracketed.measures.length > 0 ? bracketed.rest : text;
|
|
970
|
+
const slashed = bracketed.measures.length > 0 ? null : takeSlashAlternates(rest, language);
|
|
971
|
+
if (slashed) {
|
|
972
|
+
rest = slashed.rest;
|
|
973
|
+
}
|
|
974
|
+
let item = stripItemLead(rest, language);
|
|
975
|
+
const trailing = bracketed.measures.length === 0 && !slashed ? takeTrailingAlternates(item, language) : null;
|
|
976
|
+
if (trailing) {
|
|
977
|
+
item = trailing.item;
|
|
978
|
+
}
|
|
979
|
+
const group = trailing ?? (bracketed.measures.length > 0 ? bracketed : null);
|
|
980
|
+
return { rest, item, bracketed, slashed, trailing, group };
|
|
981
|
+
}
|
|
982
|
+
function whatTheFiguresSize(leading, item, language) {
|
|
983
|
+
if (language === "fr" && !leading.unit && statesItemSize(item)) {
|
|
984
|
+
return "itemSize";
|
|
985
|
+
}
|
|
986
|
+
if (language === "fr" && leading.partitive && isStatedSize(item)) {
|
|
987
|
+
return "itemSize";
|
|
988
|
+
}
|
|
989
|
+
if (leading.unit?.kind === "measured" && CONTAINER_NOUN.test(normalizeUnitKey(item))) {
|
|
990
|
+
return "containerSize";
|
|
991
|
+
}
|
|
992
|
+
return null;
|
|
993
|
+
}
|
|
994
|
+
function whatTheCountIsOf(behindFigure, language) {
|
|
995
|
+
let rest = behindFigure.trimStart();
|
|
996
|
+
if (language === "en") {
|
|
997
|
+
rest = rest.replace(LEADING_ARTICLE, "");
|
|
998
|
+
}
|
|
999
|
+
const multiplier = readCountMultiplier(rest);
|
|
1000
|
+
if (multiplier) {
|
|
1001
|
+
rest = multiplier.rest;
|
|
1002
|
+
}
|
|
1003
|
+
return { rest, multiplier };
|
|
1004
|
+
}
|
|
1005
|
+
function capacityOfTheContainer(group, measureName, item) {
|
|
1006
|
+
if (!group?.measures.every((measure) => measure.unit?.kind === "measured")) {
|
|
1007
|
+
return null;
|
|
1008
|
+
}
|
|
1009
|
+
if (!(namesContainer(measureName) || namesContainer(item))) {
|
|
1010
|
+
return null;
|
|
1011
|
+
}
|
|
1012
|
+
return group.published;
|
|
1013
|
+
}
|
|
780
1014
|
function parseIngredient(line, choice = "auto") {
|
|
781
1015
|
const original = line;
|
|
782
1016
|
const marked = readable(line);
|
|
@@ -803,50 +1037,48 @@ function parseIngredient(line, choice = "auto") {
|
|
|
803
1037
|
articleWord: null,
|
|
804
1038
|
countMultiplier: null
|
|
805
1039
|
});
|
|
806
|
-
if (unreadableComma(text, language, evidence))
|
|
1040
|
+
if (unreadableComma(text, language, evidence)) {
|
|
1041
|
+
return empty("ambiguousDecimal");
|
|
1042
|
+
}
|
|
807
1043
|
const loose = APPROXIMATION_PREFIX[language].exec(text);
|
|
808
1044
|
const stated = loose ? text.slice(loose[0].length) : text;
|
|
809
1045
|
const range = parseLeadingRange(stated, language);
|
|
810
1046
|
const article = range ? null : readArticle(stated, language);
|
|
811
1047
|
const quantity = range ?? parseLeadingQuantity(stated, language) ?? article;
|
|
812
|
-
if (!quantity)
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
if (
|
|
1048
|
+
if (!quantity) {
|
|
1049
|
+
return empty(null);
|
|
1050
|
+
}
|
|
1051
|
+
const notAnAmount = whatTheFigureIsNot(
|
|
1052
|
+
stated.slice(quantity.length),
|
|
1053
|
+
quantity !== article,
|
|
1054
|
+
language
|
|
1055
|
+
);
|
|
1056
|
+
if (notAnAmount !== false) {
|
|
1057
|
+
return empty(notAnAmount);
|
|
1058
|
+
}
|
|
1059
|
+
const counted = whatTheCountIsOf(stated.slice(quantity.length), language);
|
|
1060
|
+
let rest = counted.rest;
|
|
1061
|
+
const multiplier = counted.multiplier;
|
|
821
1062
|
const times = multiplier?.times ?? 1;
|
|
822
1063
|
const fromArticle = quantity === article;
|
|
823
|
-
const
|
|
824
|
-
const
|
|
825
|
-
const behind = described.adjective ? takeLeadingUnit(described.rest, language, fromArticle) : null;
|
|
826
|
-
const leading = direct.unit ? direct : behind?.unit ? behind : direct;
|
|
1064
|
+
const measure = readMeasure(rest, language, fromArticle);
|
|
1065
|
+
const { leading, described, behind } = measure;
|
|
827
1066
|
rest = leading.rest;
|
|
828
1067
|
const compound = range ? null : takeCompoundMember(rest, leading.unit, language);
|
|
829
|
-
if (compound)
|
|
1068
|
+
if (compound) {
|
|
1069
|
+
rest = compound.rest;
|
|
1070
|
+
}
|
|
830
1071
|
const whole = quantity.amount + (compound?.adds ?? 0);
|
|
831
|
-
const
|
|
832
|
-
const
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
);
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
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;
|
|
844
|
-
if (language === "fr" && !leading.unit && statesItemSize(item)) return empty("itemSize");
|
|
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");
|
|
1072
|
+
const equivalents = takeEquivalents(rest, language);
|
|
1073
|
+
const { slashed, trailing, group } = equivalents;
|
|
1074
|
+
rest = equivalents.rest;
|
|
1075
|
+
const item = equivalents.item;
|
|
1076
|
+
const capacity = capacityOfTheContainer(group, leading.unit?.canonical, item);
|
|
1077
|
+
const sizedRatherThanCounted = whatTheFiguresSize(leading, item, language);
|
|
1078
|
+
if (sizedRatherThanCounted !== null) {
|
|
1079
|
+
return empty(sizedRatherThanCounted);
|
|
848
1080
|
}
|
|
849
|
-
const alternates = capacity
|
|
1081
|
+
const alternates = capacity === null ? slashed?.measures ?? group?.measures ?? [] : [];
|
|
850
1082
|
return {
|
|
851
1083
|
original,
|
|
852
1084
|
language,
|
|
@@ -859,8 +1091,8 @@ function parseIngredient(line, choice = "auto") {
|
|
|
859
1091
|
rangeSeparator: range?.separator ?? null,
|
|
860
1092
|
unit: leading.unit,
|
|
861
1093
|
alternates,
|
|
862
|
-
alternateStyle: alternates.length
|
|
863
|
-
alternateIntro: capacity
|
|
1094
|
+
alternateStyle: alternateStyleOf(alternates.length, slashed !== null, trailing !== null),
|
|
1095
|
+
alternateIntro: capacity === null ? group?.intro ?? null : null,
|
|
864
1096
|
capacity,
|
|
865
1097
|
item,
|
|
866
1098
|
articleWord: fromArticle ? article?.word ?? null : null,
|
|
@@ -872,21 +1104,35 @@ function unreadableComma(text, language, evidence) {
|
|
|
872
1104
|
const settled = evidence.french !== evidence.english;
|
|
873
1105
|
return !settled;
|
|
874
1106
|
}
|
|
875
|
-
if (language === "en")
|
|
876
|
-
|
|
1107
|
+
if (language === "en") {
|
|
1108
|
+
return COMMA_DECIMAL.test(text);
|
|
1109
|
+
}
|
|
1110
|
+
return REPEATED_COMMA_GROUPS.test(text);
|
|
877
1111
|
}
|
|
878
1112
|
function readArticle(text, language) {
|
|
879
1113
|
const counts = (rest) => takeLeadingUnit(rest, language, true).unit !== null || readCountMultiplier(rest) !== null;
|
|
880
1114
|
if (language === "fr") {
|
|
881
|
-
const match =
|
|
882
|
-
if (!match)
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1115
|
+
const match = OPENING_ARTICLE_FRENCH.exec(text);
|
|
1116
|
+
if (!match) {
|
|
1117
|
+
return null;
|
|
1118
|
+
}
|
|
1119
|
+
if (!counts(text.slice(match[0].length))) {
|
|
1120
|
+
return null;
|
|
1121
|
+
}
|
|
1122
|
+
const [word = ""] = match.slice(1);
|
|
1123
|
+
const amount = FRENCH_ARTICLES[word.toLowerCase()];
|
|
1124
|
+
if (amount === void 0) {
|
|
1125
|
+
return null;
|
|
1126
|
+
}
|
|
1127
|
+
return { amount, length: match[0].length, word };
|
|
1128
|
+
}
|
|
1129
|
+
const article = LEADING_INDEFINITE_WITH_SPACE.exec(text);
|
|
1130
|
+
if (!article) {
|
|
1131
|
+
return null;
|
|
1132
|
+
}
|
|
1133
|
+
if (!counts(text.slice(article[0].length))) {
|
|
1134
|
+
return null;
|
|
1135
|
+
}
|
|
890
1136
|
return { amount: 1, length: article[0].length, word: article[0].trim() };
|
|
891
1137
|
}
|
|
892
1138
|
var COUNT_MULTIPLIERS = {
|
|
@@ -896,57 +1142,80 @@ var COUNT_MULTIPLIERS = {
|
|
|
896
1142
|
douzaines: 12
|
|
897
1143
|
};
|
|
898
1144
|
function readCountMultiplier(text) {
|
|
899
|
-
const match =
|
|
900
|
-
if (!match)
|
|
901
|
-
|
|
902
|
-
|
|
1145
|
+
const match = LEADING_WORD.exec(text);
|
|
1146
|
+
if (!match) {
|
|
1147
|
+
return null;
|
|
1148
|
+
}
|
|
1149
|
+
const [word = ""] = match.slice(1);
|
|
1150
|
+
const times = COUNT_MULTIPLIERS[normalizeUnitKey(word)];
|
|
1151
|
+
if (times === void 0) {
|
|
1152
|
+
return null;
|
|
1153
|
+
}
|
|
903
1154
|
return { times, rest: text.slice(match[0].length) };
|
|
904
1155
|
}
|
|
905
1156
|
function takeLeadingUnit(text, language, fromArticle) {
|
|
906
1157
|
const taken = takeUnit(text, language);
|
|
907
|
-
if (taken.unit)
|
|
1158
|
+
if (taken.unit) {
|
|
1159
|
+
return { ...taken, partitive: false };
|
|
1160
|
+
}
|
|
908
1161
|
if (language === "fr" && fromArticle) {
|
|
909
1162
|
const measure = readPartitiveMeasure(text);
|
|
910
|
-
if (measure)
|
|
1163
|
+
if (measure) {
|
|
1164
|
+
return { unit: measure.unit, rest: measure.rest, partitive: true };
|
|
1165
|
+
}
|
|
911
1166
|
}
|
|
912
1167
|
return { ...taken, partitive: false };
|
|
913
1168
|
}
|
|
914
1169
|
function stripItemLead(text, language) {
|
|
915
1170
|
if (language === "fr") {
|
|
916
|
-
return text.replace(
|
|
1171
|
+
return text.replace(LEADING_PARTITIVE, "").replace(LEADING_ARTICLE_FRENCH, "").trim();
|
|
917
1172
|
}
|
|
918
|
-
return text.replace(
|
|
1173
|
+
return text.replace(OF_OPENING, "").replace(LEADING_INDEFINITE_WITH_SPACE, "").trim();
|
|
919
1174
|
}
|
|
920
1175
|
function takeAlternates(text, language) {
|
|
921
1176
|
const none = { measures: [], intro: null, published: "", rest: text };
|
|
922
|
-
if (!text.startsWith("("))
|
|
1177
|
+
if (!text.startsWith("(")) {
|
|
1178
|
+
return none;
|
|
1179
|
+
}
|
|
923
1180
|
const close = text.indexOf(")");
|
|
924
|
-
if (close < 0)
|
|
1181
|
+
if (close < 0) {
|
|
1182
|
+
return none;
|
|
1183
|
+
}
|
|
925
1184
|
const read = readBracket(text.slice(0, close + 1), language);
|
|
926
|
-
if (!read)
|
|
1185
|
+
if (!read) {
|
|
1186
|
+
return none;
|
|
1187
|
+
}
|
|
927
1188
|
return { ...read, rest: text.slice(close + 1).trimStart() };
|
|
928
1189
|
}
|
|
929
1190
|
var EQUIVALENT_INTRODUCER = /^(?:soit|environ|about|approx\.?|approximately|or|ou|=)\s*/i;
|
|
930
1191
|
var MEASURE_PARTITIVE = /^(?:de\s+la\s+|de\s+l'|d'|de\s+|du\s+|des\s+|of\s+)/i;
|
|
931
1192
|
function takeMeasureAfterQuantity(text, language) {
|
|
932
1193
|
const direct = takeUnitEitherLanguage(text, language);
|
|
933
|
-
if (direct.unit)
|
|
1194
|
+
if (direct.unit) {
|
|
1195
|
+
return direct;
|
|
1196
|
+
}
|
|
934
1197
|
const partitive = MEASURE_PARTITIVE.exec(text);
|
|
935
|
-
if (!partitive)
|
|
1198
|
+
if (!partitive) {
|
|
1199
|
+
return direct;
|
|
1200
|
+
}
|
|
936
1201
|
return takeUnitEitherLanguage(text.slice(partitive[0].length), language);
|
|
937
1202
|
}
|
|
938
1203
|
function readBracket(bracket, language) {
|
|
939
1204
|
const inside = bracket.slice(1, -1);
|
|
940
1205
|
const intro = EQUIVALENT_INTRODUCER.exec(inside);
|
|
941
1206
|
const body = intro ? inside.slice(intro[0].length) : inside;
|
|
942
|
-
const parts = body.split(
|
|
1207
|
+
const parts = body.split(SLASH_OUTSIDE_A_FRACTION).map((part) => part.trim());
|
|
943
1208
|
const measures = [];
|
|
944
1209
|
for (const part of parts) {
|
|
945
1210
|
const range = parseLeadingRange(part, language);
|
|
946
1211
|
const quantity = range ?? parseLeadingQuantity(part, language);
|
|
947
|
-
if (!quantity)
|
|
1212
|
+
if (!quantity) {
|
|
1213
|
+
return null;
|
|
1214
|
+
}
|
|
948
1215
|
const after = takeMeasureAfterQuantity(part.slice(quantity.length).trimStart(), language);
|
|
949
|
-
if (!after.unit || after.rest.trim() !== "")
|
|
1216
|
+
if (!after.unit || after.rest.trim() !== "") {
|
|
1217
|
+
return null;
|
|
1218
|
+
}
|
|
950
1219
|
measures.push({
|
|
951
1220
|
amount: quantity.amount,
|
|
952
1221
|
amountMax: range?.max ?? null,
|
|
@@ -954,16 +1223,24 @@ function readBracket(bracket, language) {
|
|
|
954
1223
|
unit: after.unit
|
|
955
1224
|
});
|
|
956
1225
|
}
|
|
957
|
-
if (measures.length === 0)
|
|
1226
|
+
if (measures.length === 0) {
|
|
1227
|
+
return null;
|
|
1228
|
+
}
|
|
958
1229
|
return { measures, intro: intro ? intro[0].trim() : null, published: bracket };
|
|
959
1230
|
}
|
|
960
1231
|
function takeTrailingAlternates(item, language) {
|
|
961
|
-
const closing =
|
|
962
|
-
if (!closing)
|
|
963
|
-
|
|
964
|
-
|
|
1232
|
+
const closing = BRACKETED_TAIL.exec(item);
|
|
1233
|
+
if (!closing) {
|
|
1234
|
+
return null;
|
|
1235
|
+
}
|
|
1236
|
+
const read = readBracket(closing[1] ?? "", language);
|
|
1237
|
+
if (!read) {
|
|
1238
|
+
return null;
|
|
1239
|
+
}
|
|
965
1240
|
const head = item.slice(0, closing.index).trim();
|
|
966
|
-
if (head === "")
|
|
1241
|
+
if (head === "") {
|
|
1242
|
+
return null;
|
|
1243
|
+
}
|
|
967
1244
|
return { ...read, item: head };
|
|
968
1245
|
}
|
|
969
1246
|
function takeSlashAlternates(text, language) {
|
|
@@ -973,9 +1250,13 @@ function takeSlashAlternates(text, language) {
|
|
|
973
1250
|
const after = rest.slice(1).trimStart();
|
|
974
1251
|
const range = parseLeadingRange(after, language);
|
|
975
1252
|
const quantity = range ?? parseLeadingQuantity(after, language);
|
|
976
|
-
if (!quantity)
|
|
1253
|
+
if (!quantity) {
|
|
1254
|
+
break;
|
|
1255
|
+
}
|
|
977
1256
|
const taken = takeUnitEitherLanguage(after.slice(quantity.length).trimStart(), language);
|
|
978
|
-
if (!taken.unit)
|
|
1257
|
+
if (!taken.unit) {
|
|
1258
|
+
break;
|
|
1259
|
+
}
|
|
979
1260
|
measures.push({
|
|
980
1261
|
amount: quantity.amount,
|
|
981
1262
|
amountMax: range?.max ?? null,
|
|
@@ -992,9 +1273,15 @@ function formatAmount(amount, language, options = {}) {
|
|
|
992
1273
|
const rendered = String(rounded);
|
|
993
1274
|
return language === "fr" ? rendered.replace(".", ",") : rendered;
|
|
994
1275
|
};
|
|
995
|
-
if (!Number.isFinite(amount))
|
|
996
|
-
|
|
997
|
-
|
|
1276
|
+
if (!Number.isFinite(amount)) {
|
|
1277
|
+
return "";
|
|
1278
|
+
}
|
|
1279
|
+
if (Number.isInteger(amount)) {
|
|
1280
|
+
return String(amount);
|
|
1281
|
+
}
|
|
1282
|
+
if (options.fractions === false) {
|
|
1283
|
+
return decimal(amount);
|
|
1284
|
+
}
|
|
998
1285
|
const whole = Math.floor(amount);
|
|
999
1286
|
const rest = amount - whole;
|
|
1000
1287
|
const known = [
|
|
@@ -1005,20 +1292,52 @@ function formatAmount(amount, language, options = {}) {
|
|
|
1005
1292
|
[0.75, "3/4"]
|
|
1006
1293
|
];
|
|
1007
1294
|
for (const [value, label] of known) {
|
|
1008
|
-
if (Math.abs(rest - value) < 0.02)
|
|
1295
|
+
if (Math.abs(rest - value) < 0.02) {
|
|
1296
|
+
return whole > 0 ? `${whole} ${label}` : label;
|
|
1297
|
+
}
|
|
1009
1298
|
}
|
|
1010
1299
|
return decimal(amount);
|
|
1011
1300
|
}
|
|
1012
1301
|
|
|
1013
1302
|
// src/recipe/scale.ts
|
|
1303
|
+
var DASH_ONLY = /^[-–—]$/;
|
|
1304
|
+
var AL_ENDING2 = /al$/i;
|
|
1305
|
+
var AUX_ENDING2 = /aux$/i;
|
|
1306
|
+
var CLOVE = /\bcloves?\b/i;
|
|
1307
|
+
var CONSONANT_THEN_Y = /[^aeiou]y$/i;
|
|
1308
|
+
var DOUBLE_S_ENDING = /ss$/i;
|
|
1309
|
+
var EAUX_ENDING2 = /eaux$/i;
|
|
1310
|
+
var EAU_ENDING2 = /eau$/i;
|
|
1311
|
+
var F_ENDING = /(?:[^f]f|fe)$/i;
|
|
1312
|
+
var F_OR_FE_ENDING = /fe?$/i;
|
|
1313
|
+
var GARLIC = /\bgarlic\b/i;
|
|
1314
|
+
var IES_ENDING = /ies$/i;
|
|
1315
|
+
var LETTER = new RegExp("\\p{L}", "u");
|
|
1316
|
+
var LETTERS_ONLY = /^[A-Za-z]+$/;
|
|
1317
|
+
var OF_JOINER = / of /i;
|
|
1318
|
+
var PLURAL_ENDING = /s$|eaux$|aux$/i;
|
|
1319
|
+
var SIBILANT_ENDING = /(?:ch|sh|s|x|z)$/i;
|
|
1320
|
+
var SIBILANT_LETTER2 = /[sxz]$/i;
|
|
1321
|
+
var SIBILANT_PLURAL = /(?:ch|sh|s|x|z)es$/i;
|
|
1322
|
+
var TRAILING_S_ANY_CASE2 = /s$/i;
|
|
1323
|
+
var VOWEL_OPENING = /^[aeiouàâäéèêëîïôöûü]/i;
|
|
1324
|
+
var X_ENDING = /x$/i;
|
|
1014
1325
|
function roundTo(value, step) {
|
|
1015
1326
|
return Math.round(Math.round(value / step) * step * 100) / 100;
|
|
1016
1327
|
}
|
|
1017
1328
|
function roundMeasured(value) {
|
|
1018
|
-
if (Number.isInteger(value))
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
if (value >=
|
|
1329
|
+
if (Number.isInteger(value)) {
|
|
1330
|
+
return value;
|
|
1331
|
+
}
|
|
1332
|
+
if (value >= 100) {
|
|
1333
|
+
return roundTo(value, 5);
|
|
1334
|
+
}
|
|
1335
|
+
if (value >= 10) {
|
|
1336
|
+
return roundTo(value, 1);
|
|
1337
|
+
}
|
|
1338
|
+
if (value >= 1) {
|
|
1339
|
+
return roundTo(value, 0.1);
|
|
1340
|
+
}
|
|
1022
1341
|
return Math.round(value * 100) / 100;
|
|
1023
1342
|
}
|
|
1024
1343
|
var SMALLEST_USABLE_FRACTION = 0.25;
|
|
@@ -1034,38 +1353,50 @@ function trim(value) {
|
|
|
1034
1353
|
return Math.round(value * 100) / 100;
|
|
1035
1354
|
}
|
|
1036
1355
|
function roundCountable(value, divisibility, ceiling) {
|
|
1037
|
-
if (value <= 0)
|
|
1356
|
+
if (value <= 0) {
|
|
1357
|
+
return { value: 0, clamped: false };
|
|
1358
|
+
}
|
|
1038
1359
|
const floor = SMALLEST_USABLE[divisibility];
|
|
1039
1360
|
if (divisibility !== "whole" && value >= floor && isHalfStep(value)) {
|
|
1040
1361
|
return { value: trim(value), clamped: false };
|
|
1041
1362
|
}
|
|
1042
1363
|
if (divisibility === "whole") {
|
|
1043
|
-
if (value < 0.5)
|
|
1364
|
+
if (value < 0.5) {
|
|
1365
|
+
return { value: floor, clamped: true };
|
|
1366
|
+
}
|
|
1044
1367
|
return { value: Math.round(value), clamped: false };
|
|
1045
1368
|
}
|
|
1046
|
-
if (value < floor)
|
|
1369
|
+
if (value < floor) {
|
|
1370
|
+
return { value: floor, clamped: true };
|
|
1371
|
+
}
|
|
1047
1372
|
if (value < 1) {
|
|
1048
1373
|
const steps = divisibility === "quarter" ? [0.25, 1 / 3, 0.5, 2 / 3, 0.75, 1] : [0.5, 1];
|
|
1049
1374
|
const candidates = steps.filter(
|
|
1050
1375
|
(candidate) => candidate >= floor && candidate <= Math.max(ceiling, floor)
|
|
1051
1376
|
);
|
|
1052
|
-
let closest = candidates[0];
|
|
1377
|
+
let closest = candidates[0] ?? floor;
|
|
1053
1378
|
for (const candidate of candidates) {
|
|
1054
|
-
if (Math.abs(value - candidate) < Math.abs(value - closest))
|
|
1379
|
+
if (Math.abs(value - candidate) < Math.abs(value - closest)) {
|
|
1380
|
+
closest = candidate;
|
|
1381
|
+
}
|
|
1055
1382
|
}
|
|
1056
1383
|
return { value: trim(closest), clamped: false };
|
|
1057
1384
|
}
|
|
1058
1385
|
return { value: Math.round(value), clamped: false };
|
|
1059
1386
|
}
|
|
1060
1387
|
function roundSpoon(value, ceiling) {
|
|
1061
|
-
if (value <= 0)
|
|
1388
|
+
if (value <= 0) {
|
|
1389
|
+
return { value: 0, clamped: false };
|
|
1390
|
+
}
|
|
1062
1391
|
if (value < 1) {
|
|
1063
1392
|
const candidates = [SMALLEST_USABLE_FRACTION, 1 / 3, 0.5, 2 / 3, 0.75, 1].filter(
|
|
1064
1393
|
(candidate) => candidate <= Math.max(ceiling, SMALLEST_USABLE_FRACTION)
|
|
1065
1394
|
);
|
|
1066
|
-
let closest = candidates[0];
|
|
1395
|
+
let closest = candidates[0] ?? SMALLEST_USABLE_FRACTION;
|
|
1067
1396
|
for (const candidate of candidates) {
|
|
1068
|
-
if (Math.abs(value - candidate) < Math.abs(value - closest))
|
|
1397
|
+
if (Math.abs(value - candidate) < Math.abs(value - closest)) {
|
|
1398
|
+
closest = candidate;
|
|
1399
|
+
}
|
|
1069
1400
|
}
|
|
1070
1401
|
return { value: trim(closest), clamped: value < SMALLEST_USABLE_FRACTION };
|
|
1071
1402
|
}
|
|
@@ -1076,7 +1407,9 @@ function stepDownSpoon(unit, reference) {
|
|
|
1076
1407
|
let ratio = 1;
|
|
1077
1408
|
while (reference * ratio < 1 && !isHalfStep(reference * ratio)) {
|
|
1078
1409
|
const step = demoteUnit(current);
|
|
1079
|
-
if (!step)
|
|
1410
|
+
if (!step) {
|
|
1411
|
+
break;
|
|
1412
|
+
}
|
|
1080
1413
|
ratio *= step.per;
|
|
1081
1414
|
current = step.unit;
|
|
1082
1415
|
}
|
|
@@ -1086,24 +1419,33 @@ var EXACT_WITHIN = 0.01;
|
|
|
1086
1419
|
var EXACT_SHARE = 5e-3;
|
|
1087
1420
|
function landedExactly(exact, amount) {
|
|
1088
1421
|
const gap = Math.abs(exact - amount);
|
|
1089
|
-
if (gap > EXACT_WITHIN)
|
|
1422
|
+
if (gap > EXACT_WITHIN) {
|
|
1423
|
+
return false;
|
|
1424
|
+
}
|
|
1090
1425
|
return exact === 0 || gap / Math.abs(exact) <= EXACT_SHARE;
|
|
1091
1426
|
}
|
|
1092
1427
|
function boundLandedExactly(bound) {
|
|
1093
|
-
const fine =
|
|
1428
|
+
const fine = Math.max(bound.ratio, 1);
|
|
1094
1429
|
return landedExactly(bound.raw * fine, bound.amount * (fine / bound.ratio));
|
|
1095
1430
|
}
|
|
1096
1431
|
function scaleMeasure(low, high, unit, factor, divisibility) {
|
|
1097
|
-
const
|
|
1098
|
-
|
|
1432
|
+
const ends = high === null ? [{ published: low, raw: low * factor }] : [
|
|
1433
|
+
{ published: low, raw: low * factor },
|
|
1434
|
+
{ published: high, raw: high * factor }
|
|
1435
|
+
];
|
|
1436
|
+
const eachEnd = (read) => [
|
|
1437
|
+
read(ends[0]),
|
|
1438
|
+
...ends.slice(1).map(read)
|
|
1439
|
+
];
|
|
1440
|
+
const raws = ends.map((end) => end.raw);
|
|
1099
1441
|
const positive = raws.filter((raw) => raw > 0);
|
|
1100
|
-
const reference = positive.length > 0 ? Math.min(...positive) :
|
|
1442
|
+
const reference = positive.length > 0 ? Math.min(...positive) : low * factor;
|
|
1101
1443
|
const inUnit = (target, ratio) => ({
|
|
1102
|
-
bounds:
|
|
1444
|
+
bounds: eachEnd(({ published, raw }) => {
|
|
1103
1445
|
const exact = raw * ratio;
|
|
1104
1446
|
const rounded = ratio < 1 ? Number((roundMeasured(raw) * ratio).toPrecision(12)) : roundMeasured(exact);
|
|
1105
1447
|
const usable = rounded === 0 && exact > 0 ? Number(exact.toPrecision(2)) : rounded;
|
|
1106
|
-
const ceiling = factor < 1 ?
|
|
1448
|
+
const ceiling = factor < 1 ? published * ratio : Number.POSITIVE_INFINITY;
|
|
1107
1449
|
return {
|
|
1108
1450
|
amount: Math.min(usable, ceiling),
|
|
1109
1451
|
exact,
|
|
@@ -1120,16 +1462,18 @@ function scaleMeasure(low, high, unit, factor, divisibility) {
|
|
|
1120
1462
|
}
|
|
1121
1463
|
if (unit && isSpoonMeasure(unit)) {
|
|
1122
1464
|
const stepped = stepDownSpoon(unit, reference);
|
|
1123
|
-
if (stepped.ratio !== 1)
|
|
1124
|
-
|
|
1125
|
-
|
|
1465
|
+
if (stepped.ratio !== 1) {
|
|
1466
|
+
return inUnit(stepped.unit, stepped.ratio);
|
|
1467
|
+
}
|
|
1468
|
+
const bounds2 = eachEnd(({ published, raw }) => {
|
|
1469
|
+
const ceiling = factor < 1 ? published : Number.POSITIVE_INFINITY;
|
|
1126
1470
|
const rounded = roundSpoon(raw, ceiling);
|
|
1127
1471
|
return { amount: rounded.value, exact: raw, clamped: rounded.clamped, raw, ratio: 1 };
|
|
1128
1472
|
});
|
|
1129
1473
|
return { bounds: bounds2, unit };
|
|
1130
1474
|
}
|
|
1131
|
-
const bounds =
|
|
1132
|
-
const ceiling = factor < 1 ?
|
|
1475
|
+
const bounds = eachEnd(({ published, raw }) => {
|
|
1476
|
+
const ceiling = factor < 1 ? published : Number.POSITIVE_INFINITY;
|
|
1133
1477
|
const rounded = roundCountable(raw, divisibility, ceiling);
|
|
1134
1478
|
return { amount: rounded.value, exact: raw, clamped: rounded.clamped, raw, ratio: 1 };
|
|
1135
1479
|
});
|
|
@@ -1147,14 +1491,18 @@ var HALVED_ITEM = /\b(?:jus|juices?)\b/iu;
|
|
|
1147
1491
|
var WHOLE_ITEM = /\b(?:eggs?|yolks?|egg\s+whites?|oeufs?|œufs?|jaunes?\s+d['e]|clous?|zestes?|zests?)\b/iu;
|
|
1148
1492
|
var HALVED_CUT = /\b(?:breasts?|thighs?|drumsticks?|wings?|cutlets?|cuisses?|ailes?|pilons?|escalopes?|magrets?)\b/iu;
|
|
1149
1493
|
function cloveDivisibility(unit, item) {
|
|
1150
|
-
const counted = unit ? unit.canonical === "clove" :
|
|
1151
|
-
if (!counted)
|
|
1152
|
-
|
|
1494
|
+
const counted = unit ? unit.canonical === "clove" : CLOVE.test(item);
|
|
1495
|
+
if (!counted) {
|
|
1496
|
+
return null;
|
|
1497
|
+
}
|
|
1498
|
+
return GARLIC.test(item) ? "half" : "whole";
|
|
1153
1499
|
}
|
|
1154
1500
|
var BLANC_OF = /\bblancs?\s+d(?:e\s|['’])/iu;
|
|
1155
1501
|
var BLANC_OF_EGG = /\bblancs?\s+d(?:e\s|['’])\s*(?:oeufs?|œufs?)\b/iu;
|
|
1156
1502
|
function blancDivisibility(item) {
|
|
1157
|
-
if (!BLANC_OF.test(item))
|
|
1503
|
+
if (!BLANC_OF.test(item)) {
|
|
1504
|
+
return null;
|
|
1505
|
+
}
|
|
1158
1506
|
return BLANC_OF_EGG.test(item) ? "whole" : "half";
|
|
1159
1507
|
}
|
|
1160
1508
|
function foldItem(item) {
|
|
@@ -1163,15 +1511,31 @@ function foldItem(item) {
|
|
|
1163
1511
|
function divisibilityOf(unit, item) {
|
|
1164
1512
|
const key = foldItem(item);
|
|
1165
1513
|
const clove = cloveDivisibility(unit, key);
|
|
1166
|
-
if (clove)
|
|
1167
|
-
|
|
1514
|
+
if (clove) {
|
|
1515
|
+
return clove;
|
|
1516
|
+
}
|
|
1517
|
+
if (unit && !countsBarePieces(unit)) {
|
|
1518
|
+
return unitDivisibility(unit);
|
|
1519
|
+
}
|
|
1168
1520
|
const blanc = blancDivisibility(key);
|
|
1169
|
-
if (blanc)
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
if (
|
|
1173
|
-
|
|
1174
|
-
|
|
1521
|
+
if (blanc) {
|
|
1522
|
+
return blanc;
|
|
1523
|
+
}
|
|
1524
|
+
if (WHOLE_ITEM.test(key)) {
|
|
1525
|
+
return "whole";
|
|
1526
|
+
}
|
|
1527
|
+
if (PORTION_SIZED_ITEM.test(key)) {
|
|
1528
|
+
return "whole";
|
|
1529
|
+
}
|
|
1530
|
+
if (HALVED_ITEM.test(key)) {
|
|
1531
|
+
return "half";
|
|
1532
|
+
}
|
|
1533
|
+
if (HALVED_CUT.test(key)) {
|
|
1534
|
+
return "half";
|
|
1535
|
+
}
|
|
1536
|
+
if (QUARTERED_MEASURE.test(key)) {
|
|
1537
|
+
return "quarter";
|
|
1538
|
+
}
|
|
1175
1539
|
return QUARTERED_ITEM.test(key) ? "quarter" : "half";
|
|
1176
1540
|
}
|
|
1177
1541
|
var INVARIABLE_ITEM = /* @__PURE__ */ new Set([
|
|
@@ -1260,54 +1624,87 @@ var IRREGULAR_SINGULAR = Object.fromEntries(
|
|
|
1260
1624
|
Object.entries(IRREGULAR_PLURAL).map(([one, many]) => [many, one])
|
|
1261
1625
|
);
|
|
1262
1626
|
function matchCase(source, replacement) {
|
|
1263
|
-
const hasLetter =
|
|
1264
|
-
if (hasLetter && source === source.toUpperCase())
|
|
1265
|
-
|
|
1266
|
-
|
|
1627
|
+
const hasLetter = LETTER.test(source);
|
|
1628
|
+
if (hasLetter && source === source.toUpperCase()) {
|
|
1629
|
+
return replacement.toUpperCase();
|
|
1630
|
+
}
|
|
1631
|
+
const first = source.slice(0, 1);
|
|
1632
|
+
if (first !== "" && first === first.toUpperCase() && source.slice(1) === source.slice(1).toLowerCase()) {
|
|
1633
|
+
return (replacement[0] ?? "").toUpperCase() + replacement.slice(1);
|
|
1267
1634
|
}
|
|
1268
1635
|
return replacement;
|
|
1269
1636
|
}
|
|
1270
1637
|
function toEnglishPlural(word) {
|
|
1271
1638
|
const key = word.toLowerCase();
|
|
1272
|
-
if (INVARIABLE_ITEM.has(key))
|
|
1639
|
+
if (INVARIABLE_ITEM.has(key)) {
|
|
1640
|
+
return word;
|
|
1641
|
+
}
|
|
1273
1642
|
const irregular = IRREGULAR_PLURAL[key];
|
|
1274
|
-
if (irregular)
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
if (
|
|
1643
|
+
if (irregular) {
|
|
1644
|
+
return matchCase(word, irregular);
|
|
1645
|
+
}
|
|
1646
|
+
if (SIBILANT_ENDING.test(word)) {
|
|
1647
|
+
return `${word}es`;
|
|
1648
|
+
}
|
|
1649
|
+
if (CONSONANT_THEN_Y.test(word)) {
|
|
1650
|
+
return `${word.slice(0, -1)}ies`;
|
|
1651
|
+
}
|
|
1652
|
+
if (F_ENDING.test(word)) {
|
|
1653
|
+
return `${word.replace(F_OR_FE_ENDING, "")}ves`;
|
|
1654
|
+
}
|
|
1278
1655
|
return `${word}s`;
|
|
1279
1656
|
}
|
|
1280
1657
|
function toEnglishSingular(word) {
|
|
1281
1658
|
const key = word.toLowerCase();
|
|
1282
|
-
if (INVARIABLE_ITEM.has(key))
|
|
1659
|
+
if (INVARIABLE_ITEM.has(key)) {
|
|
1660
|
+
return word;
|
|
1661
|
+
}
|
|
1283
1662
|
const irregular = IRREGULAR_SINGULAR[key];
|
|
1284
|
-
if (irregular)
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
if (
|
|
1288
|
-
|
|
1663
|
+
if (irregular) {
|
|
1664
|
+
return matchCase(word, irregular);
|
|
1665
|
+
}
|
|
1666
|
+
if (IES_ENDING.test(word) && word.length > 4) {
|
|
1667
|
+
return `${word.slice(0, -3)}y`;
|
|
1668
|
+
}
|
|
1669
|
+
if (SIBILANT_PLURAL.test(word)) {
|
|
1670
|
+
return word.slice(0, -2);
|
|
1671
|
+
}
|
|
1672
|
+
if (DOUBLE_S_ENDING.test(word)) {
|
|
1673
|
+
return word;
|
|
1674
|
+
}
|
|
1675
|
+
if (TRAILING_S_ANY_CASE2.test(word)) {
|
|
1676
|
+
return word.slice(0, -1);
|
|
1677
|
+
}
|
|
1289
1678
|
return word;
|
|
1290
1679
|
}
|
|
1291
1680
|
function agreeInEnglish(item, amount) {
|
|
1292
|
-
if (!item)
|
|
1681
|
+
if (!item) {
|
|
1682
|
+
return item;
|
|
1683
|
+
}
|
|
1293
1684
|
const comma = item.indexOf(",");
|
|
1294
1685
|
const head = comma < 0 ? item : item.slice(0, comma);
|
|
1295
1686
|
const tail = comma < 0 ? "" : item.slice(comma);
|
|
1296
|
-
const preposition =
|
|
1687
|
+
const preposition = OF_JOINER.exec(head);
|
|
1297
1688
|
if (preposition) {
|
|
1298
1689
|
const counted = agreeInEnglish(head.slice(0, preposition.index), amount);
|
|
1299
1690
|
return `${counted}${head.slice(preposition.index)}${tail}`;
|
|
1300
1691
|
}
|
|
1301
1692
|
const words = head.trimEnd().split(" ");
|
|
1302
|
-
const last = words
|
|
1303
|
-
if (
|
|
1693
|
+
const last = words.at(-1) ?? "";
|
|
1694
|
+
if (!LETTERS_ONLY.test(last) || last.length <= 2) {
|
|
1695
|
+
return item;
|
|
1696
|
+
}
|
|
1304
1697
|
const wantsPlural = amount > 1;
|
|
1305
1698
|
const plural = toEnglishPlural(last);
|
|
1306
1699
|
const singular = toEnglishSingular(last);
|
|
1307
1700
|
const isPlural = last.toLowerCase() !== singular.toLowerCase();
|
|
1308
|
-
if (wantsPlural && !isPlural)
|
|
1309
|
-
|
|
1310
|
-
else
|
|
1701
|
+
if (wantsPlural && !isPlural) {
|
|
1702
|
+
words[words.length - 1] = plural;
|
|
1703
|
+
} else if (!wantsPlural && isPlural) {
|
|
1704
|
+
words[words.length - 1] = singular;
|
|
1705
|
+
} else {
|
|
1706
|
+
return item;
|
|
1707
|
+
}
|
|
1311
1708
|
return `${words.join(" ")}${tail}`;
|
|
1312
1709
|
}
|
|
1313
1710
|
var FRENCH_OU_PLURAL_IN_X = /* @__PURE__ */ new Set([
|
|
@@ -1386,62 +1783,109 @@ function agreeTrailingAdjective(word, wantsPlural) {
|
|
|
1386
1783
|
const folded = foldWord(word);
|
|
1387
1784
|
const isPlural = folded.endsWith("s");
|
|
1388
1785
|
const singular = isPlural ? word.slice(0, -1) : word;
|
|
1389
|
-
if (!AGREEABLE_ADJECTIVES.has(foldWord(singular)))
|
|
1390
|
-
|
|
1786
|
+
if (!AGREEABLE_ADJECTIVES.has(foldWord(singular))) {
|
|
1787
|
+
return null;
|
|
1788
|
+
}
|
|
1789
|
+
if (wantsPlural === isPlural) {
|
|
1790
|
+
return null;
|
|
1791
|
+
}
|
|
1391
1792
|
return wantsPlural ? `${word}s` : singular;
|
|
1392
1793
|
}
|
|
1794
|
+
function agreeFrenchHead(head, number) {
|
|
1795
|
+
const { wantsPlural, isPlural, ouPlural } = number;
|
|
1796
|
+
if (wantsPlural && !isPlural) {
|
|
1797
|
+
if (SIBILANT_LETTER2.test(head)) {
|
|
1798
|
+
return head;
|
|
1799
|
+
}
|
|
1800
|
+
if (EAU_ENDING2.test(head)) {
|
|
1801
|
+
return `${head}x`;
|
|
1802
|
+
}
|
|
1803
|
+
if (FRENCH_OU_PLURAL_IN_X.has(foldWord(head))) {
|
|
1804
|
+
return `${head}x`;
|
|
1805
|
+
}
|
|
1806
|
+
if (AL_ENDING2.test(head)) {
|
|
1807
|
+
return `${head.slice(0, -2)}aux`;
|
|
1808
|
+
}
|
|
1809
|
+
return `${head}s`;
|
|
1810
|
+
}
|
|
1811
|
+
if (!wantsPlural && isPlural) {
|
|
1812
|
+
if (EAUX_ENDING2.test(head)) {
|
|
1813
|
+
return head.slice(0, -1);
|
|
1814
|
+
}
|
|
1815
|
+
if (AUX_ENDING2.test(head)) {
|
|
1816
|
+
return `${head.slice(0, -3)}al`;
|
|
1817
|
+
}
|
|
1818
|
+
if (ouPlural) {
|
|
1819
|
+
return head.slice(0, -1);
|
|
1820
|
+
}
|
|
1821
|
+
if (INVARIABLE_FRENCH_NOUN.has(foldWord(head))) {
|
|
1822
|
+
return head;
|
|
1823
|
+
}
|
|
1824
|
+
return head.slice(0, -1);
|
|
1825
|
+
}
|
|
1826
|
+
return head;
|
|
1827
|
+
}
|
|
1393
1828
|
function agreeInFrench(item, amount) {
|
|
1394
|
-
if (!item)
|
|
1829
|
+
if (!item) {
|
|
1830
|
+
return item;
|
|
1831
|
+
}
|
|
1395
1832
|
const words = item.split(" ");
|
|
1396
1833
|
const head = words[0] ?? "";
|
|
1397
|
-
if (head.length <= 3)
|
|
1398
|
-
|
|
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;
|
|
1401
|
-
if (wantsPlural && !isPlural) {
|
|
1402
|
-
if (/[sxz]$/i.test(head)) {
|
|
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`;
|
|
1405
|
-
else if (/al$/i.test(head)) words[0] = `${head.slice(0, -2)}aux`;
|
|
1406
|
-
else words[0] = `${head}s`;
|
|
1407
|
-
} else if (!wantsPlural && isPlural) {
|
|
1408
|
-
if (/eaux$/i.test(head)) words[0] = head.slice(0, -1);
|
|
1409
|
-
else if (/aux$/i.test(head)) words[0] = `${head.slice(0, -3)}al`;
|
|
1410
|
-
else if (ouPlural) words[0] = head.slice(0, -1);
|
|
1411
|
-
else if (INVARIABLE_FRENCH_NOUN.has(foldWord(head))) {
|
|
1412
|
-
} else words[0] = head.slice(0, -1);
|
|
1834
|
+
if (head.length <= 3) {
|
|
1835
|
+
return item;
|
|
1413
1836
|
}
|
|
1837
|
+
const wantsPlural = amount >= 2;
|
|
1838
|
+
const ouPlural = X_ENDING.test(head) && FRENCH_OU_PLURAL_IN_X.has(foldWord(head.slice(0, -1)));
|
|
1839
|
+
const isPlural = PLURAL_ENDING.test(head) || ouPlural;
|
|
1840
|
+
words[0] = agreeFrenchHead(head, { wantsPlural, isPlural, ouPlural });
|
|
1414
1841
|
const last = words.length - 1;
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1842
|
+
const trailing = last > 0 ? words[last] : void 0;
|
|
1843
|
+
if (trailing !== void 0) {
|
|
1844
|
+
const adjective = agreeTrailingAdjective(trailing, wantsPlural);
|
|
1845
|
+
if (adjective) {
|
|
1846
|
+
words[last] = adjective;
|
|
1847
|
+
}
|
|
1418
1848
|
}
|
|
1419
1849
|
return words.join(" ");
|
|
1420
1850
|
}
|
|
1421
1851
|
function agreeLeadingAdjective(word, amount, language) {
|
|
1422
|
-
if (language === "en")
|
|
1852
|
+
if (language === "en") {
|
|
1853
|
+
return word;
|
|
1854
|
+
}
|
|
1423
1855
|
const wantsPlural = amount >= 2;
|
|
1424
1856
|
const folded = foldWord(word);
|
|
1425
1857
|
const isPlural = folded.endsWith("s") && !AGREEABLE_ADJECTIVES.has(folded);
|
|
1426
1858
|
const singular = isPlural ? word.slice(0, -1) : word;
|
|
1427
|
-
if (!AGREEABLE_ADJECTIVES.has(foldWord(singular)))
|
|
1428
|
-
|
|
1859
|
+
if (!AGREEABLE_ADJECTIVES.has(foldWord(singular))) {
|
|
1860
|
+
return word;
|
|
1861
|
+
}
|
|
1862
|
+
if (wantsPlural === isPlural) {
|
|
1863
|
+
return word;
|
|
1864
|
+
}
|
|
1429
1865
|
return wantsPlural ? `${singular}s` : singular;
|
|
1430
1866
|
}
|
|
1431
1867
|
function agreeWithAmount(item, amount, language) {
|
|
1432
1868
|
return language === "fr" ? agreeInFrench(item, amount) : agreeInEnglish(item, amount);
|
|
1433
1869
|
}
|
|
1434
1870
|
function agreeCountedContainer(item, amount, language) {
|
|
1435
|
-
if (language === "fr")
|
|
1871
|
+
if (language === "fr") {
|
|
1872
|
+
return agreeInFrench(item, amount);
|
|
1873
|
+
}
|
|
1436
1874
|
const space = item.indexOf(" ");
|
|
1437
|
-
if (space < 0)
|
|
1875
|
+
if (space < 0) {
|
|
1876
|
+
return agreeInEnglish(item, amount);
|
|
1877
|
+
}
|
|
1438
1878
|
return `${agreeInEnglish(item.slice(0, space), amount)}${item.slice(space)}`;
|
|
1439
1879
|
}
|
|
1440
1880
|
var MUTE_H_WORDS = /^(?:huile|huiles|huitre|huitres|huître|huîtres|herbe|herbes|hysope)\b/i;
|
|
1441
1881
|
function joinItem(item, language) {
|
|
1442
|
-
if (!item)
|
|
1443
|
-
|
|
1444
|
-
|
|
1882
|
+
if (!item) {
|
|
1883
|
+
return "";
|
|
1884
|
+
}
|
|
1885
|
+
if (language === "en") {
|
|
1886
|
+
return ` ${item}`;
|
|
1887
|
+
}
|
|
1888
|
+
const elides = VOWEL_OPENING.test(item) || MUTE_H_WORDS.test(item);
|
|
1445
1889
|
return elides ? ` d'${item}` : ` de ${item}`;
|
|
1446
1890
|
}
|
|
1447
1891
|
var BRANCH_SEPARATORS = {
|
|
@@ -1450,29 +1894,41 @@ var BRANCH_SEPARATORS = {
|
|
|
1450
1894
|
};
|
|
1451
1895
|
function splitBranch(text, parsed) {
|
|
1452
1896
|
const itemStart = parsed.item ? text.indexOf(parsed.item) : text.length;
|
|
1453
|
-
if (itemStart < 0)
|
|
1897
|
+
if (itemStart < 0) {
|
|
1898
|
+
return null;
|
|
1899
|
+
}
|
|
1454
1900
|
const separator = BRANCH_SEPARATORS[parsed.language];
|
|
1455
1901
|
separator.lastIndex = 0;
|
|
1456
1902
|
for (let match = separator.exec(text); match; match = separator.exec(text)) {
|
|
1457
|
-
if (match.index < itemStart)
|
|
1903
|
+
if (match.index < itemStart) {
|
|
1904
|
+
continue;
|
|
1905
|
+
}
|
|
1458
1906
|
const tail = text.slice(match.index + match[0].length);
|
|
1459
|
-
if (parseIngredient(tail, parsed.language).amount === null)
|
|
1907
|
+
if (parseIngredient(tail, parsed.language).amount === null) {
|
|
1908
|
+
continue;
|
|
1909
|
+
}
|
|
1460
1910
|
return { head: text.slice(0, match.index), separator: match[0], tail };
|
|
1461
1911
|
}
|
|
1462
1912
|
return null;
|
|
1463
1913
|
}
|
|
1464
1914
|
function scaleIngredient(line, options) {
|
|
1465
1915
|
const { factor } = options;
|
|
1466
|
-
if (factor === 1)
|
|
1916
|
+
if (factor === 1) {
|
|
1917
|
+
return passthroughIngredient(line, options.language);
|
|
1918
|
+
}
|
|
1467
1919
|
const text = line.trim();
|
|
1468
1920
|
const parsed = parseIngredient(text, options.language ?? "auto");
|
|
1469
1921
|
const branch = splitBranch(text, parsed);
|
|
1470
|
-
if (branch)
|
|
1922
|
+
if (branch) {
|
|
1923
|
+
return scaleBranchedLine(line, branch, { ...options, language: parsed.language });
|
|
1924
|
+
}
|
|
1471
1925
|
return scaleSingleLine(line, options);
|
|
1472
1926
|
}
|
|
1473
1927
|
function scaleBranchedLine(line, branch, options) {
|
|
1474
1928
|
const head = scaleSingleLine(branch.head, options);
|
|
1475
|
-
if (head.scaling === "unscaled")
|
|
1929
|
+
if (head.scaling === "unscaled") {
|
|
1930
|
+
return { ...head, text: line.trim(), original: line };
|
|
1931
|
+
}
|
|
1476
1932
|
const tail = scaleAlternative(branch.tail, options);
|
|
1477
1933
|
const result = {
|
|
1478
1934
|
...head,
|
|
@@ -1487,11 +1943,128 @@ function scaleBranchedLine(line, branch, options) {
|
|
|
1487
1943
|
function scaleAlternative(tail, options) {
|
|
1488
1944
|
const parsed = parseIngredient(tail, options.language ?? "auto");
|
|
1489
1945
|
const published = tail.trim();
|
|
1490
|
-
if (parsed.amount === null)
|
|
1946
|
+
if (parsed.amount === null) {
|
|
1947
|
+
return { text: published, rewritten: false };
|
|
1948
|
+
}
|
|
1491
1949
|
const largest = (parsed.amountMax ?? parsed.amount) * options.factor;
|
|
1492
|
-
if (largest < 1)
|
|
1950
|
+
if (largest < 1) {
|
|
1951
|
+
return { text: published, rewritten: false };
|
|
1952
|
+
}
|
|
1493
1953
|
return { text: scaleIngredient(tail, options).text, rewritten: true };
|
|
1494
1954
|
}
|
|
1955
|
+
function alternateLabel(count, style, texts, bracketed) {
|
|
1956
|
+
if (count === 0) {
|
|
1957
|
+
return "";
|
|
1958
|
+
}
|
|
1959
|
+
if (style === "slash") {
|
|
1960
|
+
return ` / ${texts.join(" / ")}`;
|
|
1961
|
+
}
|
|
1962
|
+
if (style === "trailing") {
|
|
1963
|
+
return "";
|
|
1964
|
+
}
|
|
1965
|
+
return bracketed;
|
|
1966
|
+
}
|
|
1967
|
+
function itemLabelFor(named, item, counted, language) {
|
|
1968
|
+
if (named) {
|
|
1969
|
+
return joinItem(item, language);
|
|
1970
|
+
}
|
|
1971
|
+
if (counted) {
|
|
1972
|
+
return ` ${counted}`;
|
|
1973
|
+
}
|
|
1974
|
+
return "";
|
|
1975
|
+
}
|
|
1976
|
+
function noteForScaledLine(outcome) {
|
|
1977
|
+
const {
|
|
1978
|
+
parsed,
|
|
1979
|
+
bounds: primaryBounds,
|
|
1980
|
+
unit,
|
|
1981
|
+
low,
|
|
1982
|
+
clamped,
|
|
1983
|
+
movedPrimary,
|
|
1984
|
+
movedAlternate,
|
|
1985
|
+
restated,
|
|
1986
|
+
collapsed,
|
|
1987
|
+
language,
|
|
1988
|
+
carriesMore
|
|
1989
|
+
} = outcome;
|
|
1990
|
+
let note;
|
|
1991
|
+
const asPublished = (value, source) => `${formatAmount(value, language, { fractions: false })}${source ? ` ${formatUnit(source, value, language)}` : ""}`;
|
|
1992
|
+
const sentences = [];
|
|
1993
|
+
if (clamped) {
|
|
1994
|
+
sentences.push(
|
|
1995
|
+
`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.`
|
|
1996
|
+
);
|
|
1997
|
+
} else if (movedPrimary) {
|
|
1998
|
+
const moved = primaryBounds.filter((bound) => !boundLandedExactly(bound));
|
|
1999
|
+
sentences.push(
|
|
2000
|
+
moved.map(
|
|
2001
|
+
(bound) => `Rounded ${bound.amount > bound.exact ? "up" : "down"} from ${asPublished(bound.raw, parsed.unit)}.`
|
|
2002
|
+
).join(" ")
|
|
2003
|
+
);
|
|
2004
|
+
} else if (movedAlternate) {
|
|
2005
|
+
sentences.push(
|
|
2006
|
+
`The amount is exact; the equivalent ${restated ? "beside it" : "in brackets"} was rounded to stay readable.`
|
|
2007
|
+
);
|
|
2008
|
+
} else if (restated) {
|
|
2009
|
+
sentences.push(
|
|
2010
|
+
"This line states one quantity twice, and both readings were scaled. They agree as closely as the page wrote them, and no closer."
|
|
2011
|
+
);
|
|
2012
|
+
}
|
|
2013
|
+
if (carriesMore) {
|
|
2014
|
+
sentences.push(
|
|
2015
|
+
"This line carries a further quantity after the first one, and only the first was scaled. Read the rest as published."
|
|
2016
|
+
);
|
|
2017
|
+
}
|
|
2018
|
+
if (parsed.capacity) {
|
|
2019
|
+
sentences.push(
|
|
2020
|
+
`${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.`
|
|
2021
|
+
);
|
|
2022
|
+
}
|
|
2023
|
+
if (collapsed) {
|
|
2024
|
+
sentences.push("The page gave a range, and at this size both ends come to the same amount.");
|
|
2025
|
+
}
|
|
2026
|
+
if (unit?.kind === "measured" && low.amount > 0 && low.amount < 0.05) {
|
|
2027
|
+
sentences.push(
|
|
2028
|
+
"This is smaller than a kitchen scale resolves. Make a larger batch, or measure it by eye."
|
|
2029
|
+
);
|
|
2030
|
+
}
|
|
2031
|
+
if (parsed.approximation) {
|
|
2032
|
+
sentences.push(
|
|
2033
|
+
"The page gave this amount as an approximation, and the scaled figure is no firmer."
|
|
2034
|
+
);
|
|
2035
|
+
}
|
|
2036
|
+
if (sentences.length > 0) {
|
|
2037
|
+
note = sentences.join(" ");
|
|
2038
|
+
}
|
|
2039
|
+
if (parsed.unit && parsed.unit.kind === "approximate") {
|
|
2040
|
+
note = withApproximateNote(parsed.unit, note);
|
|
2041
|
+
}
|
|
2042
|
+
if (parsed.articleWord) {
|
|
2043
|
+
const stood = (parsed.amount ?? 0) / (parsed.countMultiplier ?? 1);
|
|
2044
|
+
const read = `"${parsed.articleWord}" read as ${formatAmount(stood, language)}.`;
|
|
2045
|
+
note = note ? `${read} ${note}` : read;
|
|
2046
|
+
}
|
|
2047
|
+
return note;
|
|
2048
|
+
}
|
|
2049
|
+
function renderScaledLine(parsed, alternates, unit, shown, language) {
|
|
2050
|
+
const named = unit && !countsBarePieces(unit) ? unit : null;
|
|
2051
|
+
const adjective = named && parsed.measureAdjective ? ` ${agreeLeadingAdjective(parsed.measureAdjective, shown, language)}` : "";
|
|
2052
|
+
const unitLabel = named ? `${adjective} ${formatUnit(named, shown, language)}` : "";
|
|
2053
|
+
const alternateTexts = alternates.map((entry) => entry.text);
|
|
2054
|
+
const introduced = parsed.alternateIntro ? `${parsed.alternateIntro} ` : "";
|
|
2055
|
+
const bracketed = alternates.length === 0 ? "" : ` (${introduced}${alternateTexts.join(" / ")})`;
|
|
2056
|
+
const altLabel = alternateLabel(
|
|
2057
|
+
alternates.length,
|
|
2058
|
+
parsed.alternateStyle,
|
|
2059
|
+
alternateTexts,
|
|
2060
|
+
bracketed
|
|
2061
|
+
);
|
|
2062
|
+
const trailingLabel = parsed.alternateStyle === "trailing" ? bracketed : "";
|
|
2063
|
+
const capacityLabel = parsed.capacity ? ` ${parsed.capacity}` : "";
|
|
2064
|
+
const counted = parsed.capacity ? agreeCountedContainer(parsed.item, shown, language) : agreeWithAmount(parsed.item, shown, language);
|
|
2065
|
+
const itemLabel = itemLabelFor(named, parsed.item, counted, language);
|
|
2066
|
+
return { named, unitLabel, capacityLabel, altLabel, trailingLabel, itemLabel };
|
|
2067
|
+
}
|
|
1495
2068
|
var HELD_BACK_NOTE = {
|
|
1496
2069
|
sizeQualifier: "The figures here give the size of one item rather than how many, so the line is left as published.",
|
|
1497
2070
|
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.",
|
|
@@ -1538,17 +2111,13 @@ function scaleSingleLine(line, options) {
|
|
|
1538
2111
|
high === null || collapsed ? null : asText(high.amount),
|
|
1539
2112
|
parsed.rangeSeparator
|
|
1540
2113
|
);
|
|
1541
|
-
const named
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
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);
|
|
1551
|
-
const itemLabel = named ? joinItem(parsed.item, language) : counted ? ` ${counted}` : "";
|
|
2114
|
+
const { named, unitLabel, capacityLabel, altLabel, trailingLabel, itemLabel } = renderScaledLine(
|
|
2115
|
+
parsed,
|
|
2116
|
+
alternates,
|
|
2117
|
+
unit,
|
|
2118
|
+
shown,
|
|
2119
|
+
language
|
|
2120
|
+
);
|
|
1552
2121
|
const result = {
|
|
1553
2122
|
text: `${parsed.decoration ? `${parsed.decoration} ` : ""}${parsed.approximation ?? ""}${amountText}${unitLabel}${capacityLabel}${altLabel}${itemLabel}${trailingLabel}`.trim(),
|
|
1554
2123
|
original: parsed.original,
|
|
@@ -1558,59 +2127,21 @@ function scaleSingleLine(line, options) {
|
|
|
1558
2127
|
unit: named?.canonical ?? null,
|
|
1559
2128
|
language
|
|
1560
2129
|
};
|
|
1561
|
-
const
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
`The amount is exact; the equivalent ${restated ? "beside it" : "in brackets"} was rounded to stay readable.`
|
|
1577
|
-
);
|
|
1578
|
-
} else if (restated) {
|
|
1579
|
-
sentences.push(
|
|
1580
|
-
"This line states one quantity twice, and both readings were scaled. They agree as closely as the page wrote them, and no closer."
|
|
1581
|
-
);
|
|
1582
|
-
}
|
|
1583
|
-
if (carriesMore) {
|
|
1584
|
-
sentences.push(
|
|
1585
|
-
"This line carries a further quantity after the first one, and only the first was scaled. Read the rest as published."
|
|
1586
|
-
);
|
|
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
|
-
}
|
|
1593
|
-
if (collapsed) {
|
|
1594
|
-
sentences.push("The page gave a range, and at this size both ends come to the same amount.");
|
|
1595
|
-
}
|
|
1596
|
-
if (unit?.kind === "measured" && low.amount > 0 && low.amount < 0.05) {
|
|
1597
|
-
sentences.push(
|
|
1598
|
-
"This is smaller than a kitchen scale resolves. Make a larger batch, or measure it by eye."
|
|
1599
|
-
);
|
|
1600
|
-
}
|
|
1601
|
-
if (parsed.approximation) {
|
|
1602
|
-
sentences.push(
|
|
1603
|
-
"The page gave this amount as an approximation, and the scaled figure is no firmer."
|
|
1604
|
-
);
|
|
1605
|
-
}
|
|
1606
|
-
if (sentences.length > 0) result.note = sentences.join(" ");
|
|
1607
|
-
if (parsed.unit && parsed.unit.kind === "approximate") {
|
|
1608
|
-
result.note = withApproximateNote(parsed.unit, result.note);
|
|
1609
|
-
}
|
|
1610
|
-
if (parsed.articleWord) {
|
|
1611
|
-
const stood = (parsed.amount ?? 0) / (parsed.countMultiplier ?? 1);
|
|
1612
|
-
const read = `"${parsed.articleWord}" read as ${formatAmount(stood, language)}.`;
|
|
1613
|
-
result.note = result.note ? `${read} ${result.note}` : read;
|
|
2130
|
+
const note = noteForScaledLine({
|
|
2131
|
+
parsed,
|
|
2132
|
+
bounds: primaryBounds,
|
|
2133
|
+
unit,
|
|
2134
|
+
low,
|
|
2135
|
+
clamped,
|
|
2136
|
+
movedPrimary,
|
|
2137
|
+
movedAlternate,
|
|
2138
|
+
restated,
|
|
2139
|
+
collapsed,
|
|
2140
|
+
language,
|
|
2141
|
+
carriesMore
|
|
2142
|
+
});
|
|
2143
|
+
if (note !== void 0) {
|
|
2144
|
+
result.note = note;
|
|
1614
2145
|
}
|
|
1615
2146
|
return result;
|
|
1616
2147
|
}
|
|
@@ -1642,8 +2173,10 @@ function renderMeasure(measure, factor, language) {
|
|
|
1642
2173
|
};
|
|
1643
2174
|
}
|
|
1644
2175
|
function renderRange(low, high, separator) {
|
|
1645
|
-
if (high === null || separator === null)
|
|
1646
|
-
|
|
2176
|
+
if (high === null || separator === null) {
|
|
2177
|
+
return low;
|
|
2178
|
+
}
|
|
2179
|
+
return DASH_ONLY.test(separator) ? `${low}${separator}${high}` : `${low} ${separator} ${high}`;
|
|
1647
2180
|
}
|
|
1648
2181
|
function scaleIngredients(lines, options) {
|
|
1649
2182
|
return lines.map((line) => scaleIngredient(line, options));
|
|
@@ -1660,9 +2193,11 @@ function passthroughIngredient(line, language = "auto") {
|
|
|
1660
2193
|
unit: held ? null : parsed.unit?.canonical ?? null,
|
|
1661
2194
|
language: parsed.language
|
|
1662
2195
|
};
|
|
1663
|
-
if (parsed.heldBack)
|
|
1664
|
-
|
|
1665
|
-
else if (parsed.
|
|
2196
|
+
if (parsed.heldBack) {
|
|
2197
|
+
result.note = HELD_BACK_NOTE[parsed.heldBack];
|
|
2198
|
+
} else if (parsed.amount === null) {
|
|
2199
|
+
result.note = "No quantity given; adjust to taste.";
|
|
2200
|
+
} else if (parsed.unit?.kind === "approximate") {
|
|
1666
2201
|
result.note = withApproximateNote(parsed.unit, void 0);
|
|
1667
2202
|
}
|
|
1668
2203
|
return result;
|