mcp-recipes 3.0.0 → 4.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 +70 -0
- package/PRIVACY.md +8 -6
- package/README.md +192 -103
- package/dist/index.js +1236 -249
- package/dist/index.js.map +1 -1
- package/dist/{language-BgfmjwtF.d.ts → language-B3VNNg7f.d.ts} +2 -2
- package/dist/recipe/scale.d.ts +13 -5
- package/dist/recipe/scale.js +616 -89
- package/dist/recipe/scale.js.map +1 -1
- package/dist/sources/client.d.ts +163 -9
- package/dist/sources/client.js +348 -50
- package/dist/sources/client.js.map +1 -1
- package/package.json +20 -17
- package/server.json +5 -5
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
11
11
|
import process from "process";
|
|
12
12
|
|
|
13
13
|
// src/version.ts
|
|
14
|
-
var PKG_VERSION = "
|
|
14
|
+
var PKG_VERSION = "4.0.0";
|
|
15
15
|
var REPO_URL = "https://github.com/smeet666/mcp-recipes";
|
|
16
16
|
var ISSUES_URL = "https://github.com/smeet666/mcp-recipes/issues";
|
|
17
17
|
|
|
@@ -94,6 +94,7 @@ function loadConfig(env = process.env) {
|
|
|
94
94
|
// src/sources/registry.ts
|
|
95
95
|
import { GoodFoodClient } from "mcp-bbc-goodfood/client";
|
|
96
96
|
import { MarmitonClient } from "mcp-marmiton/client";
|
|
97
|
+
import { PequerecetasClient } from "mcp-pequerecetas/client";
|
|
97
98
|
import { PtitchefClient } from "mcp-ptitchef/client";
|
|
98
99
|
import { SupertoinetteClient } from "mcp-supertoinette/client";
|
|
99
100
|
import { CookbookClient } from "mcp-wikibooks-cookbook/client";
|
|
@@ -254,8 +255,7 @@ var COOKBOOK_PROFILE = {
|
|
|
254
255
|
homeUrl: "https://en.wikibooks.org/wiki/Cookbook:Table_of_Contents",
|
|
255
256
|
language: "en",
|
|
256
257
|
attribution: "Source: Wikibooks Cookbook",
|
|
257
|
-
|
|
258
|
-
mixesReferencePages: true
|
|
258
|
+
rowsThatAreNotRecipes: "a page about an ingredient"
|
|
259
259
|
};
|
|
260
260
|
var SITE_URL = /^https?:\/\/en\.wikibooks\.org\/wiki\//i;
|
|
261
261
|
var NAMESPACE = /^cookbook\s*:/i;
|
|
@@ -362,6 +362,7 @@ function cookbookDetail(payload) {
|
|
|
362
362
|
prepMinutes: null,
|
|
363
363
|
cookMinutes: null,
|
|
364
364
|
totalMinutes: count(recipe.totalMinutes),
|
|
365
|
+
gathers: null,
|
|
365
366
|
category: text(recipe.category),
|
|
366
367
|
// The page is written by everyone who edited it, so it credits nobody in
|
|
367
368
|
// particular and carries no reader rating.
|
|
@@ -388,9 +389,7 @@ var GOODFOOD_PROFILE = {
|
|
|
388
389
|
homeUrl: "https://www.bbcgoodfood.com",
|
|
389
390
|
language: "en",
|
|
390
391
|
attribution: "Source: BBC Good Food",
|
|
391
|
-
|
|
392
|
-
// buying guides is answered with recipes all the same.
|
|
393
|
-
mixesReferencePages: false
|
|
392
|
+
rowsThatAreNotRecipes: null
|
|
394
393
|
};
|
|
395
394
|
var SITE_URL2 = /^https?:\/\/(?:www\.)?bbcgoodfood\.com\//i;
|
|
396
395
|
var REFERENCE = /^recipes\/[^/]+$/;
|
|
@@ -529,6 +528,7 @@ function goodfoodDetail(payload) {
|
|
|
529
528
|
} : null,
|
|
530
529
|
// The site files a recipe under collections rather than under one category
|
|
531
530
|
// on the page itself.
|
|
531
|
+
gathers: null,
|
|
532
532
|
category: null,
|
|
533
533
|
author: text(recipe.author),
|
|
534
534
|
rating: value === null ? null : { value, count: count(recipe.rating_count), max: 5 },
|
|
@@ -548,8 +548,7 @@ var MARMITON_PROFILE = {
|
|
|
548
548
|
homeUrl: "https://www.marmiton.org",
|
|
549
549
|
language: "fr",
|
|
550
550
|
attribution: "Source: Marmiton",
|
|
551
|
-
|
|
552
|
-
mixesReferencePages: false
|
|
551
|
+
rowsThatAreNotRecipes: null
|
|
553
552
|
};
|
|
554
553
|
var SITE_URL3 = /^https?:\/\/(?:www\.)?marmiton\.org\//i;
|
|
555
554
|
var REFERENCE2 = /^\d+$/;
|
|
@@ -649,6 +648,7 @@ function marmitonDetail(payload) {
|
|
|
649
648
|
prepMinutes: count(recipe.prepMinutes),
|
|
650
649
|
cookMinutes: count(recipe.cookMinutes),
|
|
651
650
|
totalMinutes: count(recipe.totalMinutes),
|
|
651
|
+
gathers: null,
|
|
652
652
|
category: text(recipe.category),
|
|
653
653
|
author: text(recipe.author),
|
|
654
654
|
rating: value === null ? null : { value, count: count(rating?.count), max: count(rating?.best) },
|
|
@@ -665,6 +665,181 @@ function marmitonDetail(payload) {
|
|
|
665
665
|
};
|
|
666
666
|
}
|
|
667
667
|
|
|
668
|
+
// src/sources/pequerecetas.ts
|
|
669
|
+
var PEQUERECETAS_PROFILE = {
|
|
670
|
+
id: "pequerecetas",
|
|
671
|
+
name: "Pequerecetas",
|
|
672
|
+
homeUrl: "https://www.pequerecetas.com",
|
|
673
|
+
language: "es",
|
|
674
|
+
attribution: "Source: Pequerecetas",
|
|
675
|
+
rowsThatAreNotRecipes: "an article gathering other recipes"
|
|
676
|
+
};
|
|
677
|
+
var SITE_RECIPE_URL = /^https?:\/\/(?:www\.)?pequerecetas\.com\/receta\/([a-z0-9]+(?:-[a-z0-9]+)*)\/?$/i;
|
|
678
|
+
var REFERENCE3 = /^[a-z0-9]+(?:-[a-z0-9]+)+$/;
|
|
679
|
+
function pequerecetasAdapter(reader) {
|
|
680
|
+
return {
|
|
681
|
+
...PEQUERECETAS_PROFILE,
|
|
682
|
+
claims(raw) {
|
|
683
|
+
const address = SITE_RECIPE_URL.exec(raw);
|
|
684
|
+
if (address?.[1]) {
|
|
685
|
+
return {
|
|
686
|
+
reference: address[1],
|
|
687
|
+
why: "the address is a Pequerecetas recipe page",
|
|
688
|
+
guess: false
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
if (REFERENCE3.test(raw)) {
|
|
692
|
+
return {
|
|
693
|
+
reference: raw,
|
|
694
|
+
why: "hyphenated lowercase words are the shape Pequerecetas mints",
|
|
695
|
+
guess: true
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
return null;
|
|
699
|
+
},
|
|
700
|
+
async search(query) {
|
|
701
|
+
const outcome = await reader.searchRecipes(query);
|
|
702
|
+
const listing = outcome.data ?? {};
|
|
703
|
+
const list = rowsOf(listing.rows, PEQUERECETAS_PROFILE);
|
|
704
|
+
const rows = [];
|
|
705
|
+
let skipped = outcome.skipped?.length ?? 0;
|
|
706
|
+
for (const summary of list) {
|
|
707
|
+
const row = toRow(summary);
|
|
708
|
+
if (row === null) {
|
|
709
|
+
skipped += 1;
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
rows.push(row);
|
|
713
|
+
}
|
|
714
|
+
return {
|
|
715
|
+
rows,
|
|
716
|
+
skipped,
|
|
717
|
+
// The site prints no count of what a search matched, and the rows of
|
|
718
|
+
// one page are not put here in its place.
|
|
719
|
+
reportedTotal: null,
|
|
720
|
+
reportedTotalMeans: null,
|
|
721
|
+
cached: outcome.cached
|
|
722
|
+
};
|
|
723
|
+
},
|
|
724
|
+
async getRecipe(reference) {
|
|
725
|
+
const outcome = await reader.getRecipe(reference);
|
|
726
|
+
return { recipe: pequerecetasDetail(outcome.data), cached: outcome.cached };
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
function toRow(summary) {
|
|
731
|
+
const id = text(summary?.id);
|
|
732
|
+
const title = text(summary?.title);
|
|
733
|
+
const url = text(summary?.url);
|
|
734
|
+
if (!(id && title && url)) {
|
|
735
|
+
return null;
|
|
736
|
+
}
|
|
737
|
+
return {
|
|
738
|
+
id: namespacedId(PEQUERECETAS_PROFILE.id, id),
|
|
739
|
+
source: PEQUERECETAS_PROFILE.id,
|
|
740
|
+
sourceName: PEQUERECETAS_PROFILE.name,
|
|
741
|
+
title,
|
|
742
|
+
url,
|
|
743
|
+
imageUrl: text(summary?.image_url),
|
|
744
|
+
// A listing row carries no gloss on this site.
|
|
745
|
+
excerpt: null
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
function pequerecetasDetail(payload) {
|
|
749
|
+
const page = payload ?? {};
|
|
750
|
+
if (page.kind === "collection") {
|
|
751
|
+
return collectionDetail(page.collection);
|
|
752
|
+
}
|
|
753
|
+
return recipeDetail(page.recipe);
|
|
754
|
+
}
|
|
755
|
+
function recipeDetail(payload) {
|
|
756
|
+
const recipe = payload ?? {};
|
|
757
|
+
const rating = recipe.rating;
|
|
758
|
+
const value = count(rating?.value);
|
|
759
|
+
const published = text(recipe.yield_text);
|
|
760
|
+
const span = readYieldSpan(published);
|
|
761
|
+
const steps = Array.isArray(recipe.steps) ? recipe.steps : [];
|
|
762
|
+
const energy = recipe.nutrition;
|
|
763
|
+
const figure = text(energy?.text);
|
|
764
|
+
return {
|
|
765
|
+
...detailBase(PEQUERECETAS_PROFILE),
|
|
766
|
+
id: namespacedId(PEQUERECETAS_PROFILE.id, required(recipe.id, "id", PEQUERECETAS_PROFILE)),
|
|
767
|
+
title: required(recipe.title, "title", PEQUERECETAS_PROFILE),
|
|
768
|
+
url: required(recipe.url, "url", PEQUERECETAS_PROFILE),
|
|
769
|
+
imageUrl: text(recipe.images?.[0]),
|
|
770
|
+
yieldCount: span.count,
|
|
771
|
+
yieldMax: span.max,
|
|
772
|
+
yieldText: published,
|
|
773
|
+
yieldUnit: span.unit,
|
|
774
|
+
// Carried as published, equipment among them: the site writes what a
|
|
775
|
+
// recipe is cooked with in this list, and telling the two apart is the
|
|
776
|
+
// scaler's work rather than this adapter's.
|
|
777
|
+
ingredients: textList(recipe.ingredients),
|
|
778
|
+
steps: steps.map((step) => text(step?.text)).filter((step) => step !== null),
|
|
779
|
+
// The headings the site groups its steps under belong to the method rather
|
|
780
|
+
// than to the page's layout, so no list of published sections is claimed.
|
|
781
|
+
publishedSections: null,
|
|
782
|
+
prepMinutes: count(recipe.prep_minutes),
|
|
783
|
+
cookMinutes: count(recipe.cook_minutes),
|
|
784
|
+
totalMinutes: count(recipe.total_minutes),
|
|
785
|
+
restMinutes: null,
|
|
786
|
+
// The reader hands over the steps it read and says nothing about whether
|
|
787
|
+
// the page numbered them.
|
|
788
|
+
stepsAsOneBlock: null,
|
|
789
|
+
withheld: null,
|
|
790
|
+
gathers: null,
|
|
791
|
+
category: text(recipe.categories?.[0]),
|
|
792
|
+
author: text(recipe.author),
|
|
793
|
+
// The scale is published, so a value is never read against a guess.
|
|
794
|
+
rating: value === null ? null : { value, count: count(rating?.count), max: count(rating?.scale) },
|
|
795
|
+
// One figure, repeated in the words the page printed it with. The site
|
|
796
|
+
// states no serving size, so nothing is divided: an amount per serving
|
|
797
|
+
// would be arithmetic the page never showed.
|
|
798
|
+
nutrition: figure === null ? null : { calories: figure },
|
|
799
|
+
// The site keeps no list of its own: what a recipe is cooked with is
|
|
800
|
+
// written among the ingredients.
|
|
801
|
+
equipment: [],
|
|
802
|
+
tips: [],
|
|
803
|
+
// The site states no licence on a recipe page, and silence is not a grant.
|
|
804
|
+
license: null
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
function collectionDetail(payload) {
|
|
808
|
+
const collection = payload ?? {};
|
|
809
|
+
const listed = Array.isArray(collection.recipes) ? collection.recipes : [];
|
|
810
|
+
return {
|
|
811
|
+
...detailBase(PEQUERECETAS_PROFILE),
|
|
812
|
+
id: namespacedId(PEQUERECETAS_PROFILE.id, required(collection.id, "id", PEQUERECETAS_PROFILE)),
|
|
813
|
+
title: required(collection.title, "title", PEQUERECETAS_PROFILE),
|
|
814
|
+
url: required(collection.url, "url", PEQUERECETAS_PROFILE),
|
|
815
|
+
imageUrl: text(collection.images?.[0]),
|
|
816
|
+
yieldCount: null,
|
|
817
|
+
yieldMax: null,
|
|
818
|
+
yieldText: null,
|
|
819
|
+
yieldUnit: null,
|
|
820
|
+
ingredients: [],
|
|
821
|
+
steps: [],
|
|
822
|
+
publishedSections: null,
|
|
823
|
+
prepMinutes: null,
|
|
824
|
+
cookMinutes: null,
|
|
825
|
+
totalMinutes: null,
|
|
826
|
+
restMinutes: null,
|
|
827
|
+
stepsAsOneBlock: null,
|
|
828
|
+
withheld: null,
|
|
829
|
+
gathers: {
|
|
830
|
+
headings: textList(collection.headings),
|
|
831
|
+
rows: listed.map(toRow).filter((row) => row !== null)
|
|
832
|
+
},
|
|
833
|
+
category: null,
|
|
834
|
+
author: null,
|
|
835
|
+
rating: null,
|
|
836
|
+
nutrition: null,
|
|
837
|
+
equipment: [],
|
|
838
|
+
tips: [],
|
|
839
|
+
license: null
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
|
|
668
843
|
// src/sources/ptitchef.ts
|
|
669
844
|
var PTITCHEF_PROFILE = {
|
|
670
845
|
id: "ptitchef",
|
|
@@ -672,18 +847,15 @@ var PTITCHEF_PROFILE = {
|
|
|
672
847
|
homeUrl: "https://www.ptitchef.com",
|
|
673
848
|
language: "fr",
|
|
674
849
|
attribution: "Source: Ptitchef",
|
|
675
|
-
|
|
676
|
-
// guide of its own groups recipes under headings rather than mixing in pages
|
|
677
|
-
// about ingredients.
|
|
678
|
-
mixesReferencePages: false
|
|
850
|
+
rowsThatAreNotRecipes: null
|
|
679
851
|
};
|
|
680
852
|
var SITE_URL4 = /^https?:\/\/(?:www\.)?ptitchef\.com\//i;
|
|
681
|
-
var
|
|
853
|
+
var REFERENCE4 = /^recettes\/[^/]+\/[^/]+-fid-\d+$/;
|
|
682
854
|
var DOT_SEGMENT2 = /(?:^|\/)\.\.?(?:\/|$)/;
|
|
683
855
|
var LEADING_SLASH2 = /^\/+/;
|
|
684
856
|
function asReference2(raw) {
|
|
685
857
|
const path = raw.replace(LEADING_SLASH2, "");
|
|
686
|
-
return
|
|
858
|
+
return REFERENCE4.test(path) && !DOT_SEGMENT2.test(`/${path}`) ? path : null;
|
|
687
859
|
}
|
|
688
860
|
function meansFor2(listing) {
|
|
689
861
|
if (count(listing.total_available) === null) {
|
|
@@ -784,6 +956,7 @@ function ptitchefDetail(payload) {
|
|
|
784
956
|
restMinutes: null,
|
|
785
957
|
stepsAsOneBlock: typeof recipe.steps_are_one_block === "boolean" ? recipe.steps_are_one_block : null,
|
|
786
958
|
withheld: null,
|
|
959
|
+
gathers: null,
|
|
787
960
|
category: text(recipe.category),
|
|
788
961
|
author: text(recipe.author),
|
|
789
962
|
// The site prints two counts beside a rating, one of readers who rated and
|
|
@@ -805,17 +978,15 @@ var SUPERTOINETTE_PROFILE = {
|
|
|
805
978
|
homeUrl: "https://www.supertoinette.com",
|
|
806
979
|
language: "fr",
|
|
807
980
|
attribution: "Source: Supertoinette",
|
|
808
|
-
|
|
809
|
-
// recipe, so what arrives here is recipes.
|
|
810
|
-
mixesReferencePages: false
|
|
981
|
+
rowsThatAreNotRecipes: null
|
|
811
982
|
};
|
|
812
|
-
var
|
|
813
|
-
var
|
|
983
|
+
var SITE_RECIPE_URL2 = /^https?:\/\/(?:www\.)?supertoinette\.com\/recette\/([1-9][0-9]{0,9})\//i;
|
|
984
|
+
var REFERENCE5 = /^[1-9][0-9]{0,9}$/;
|
|
814
985
|
function supertoinetteAdapter(reader) {
|
|
815
986
|
return {
|
|
816
987
|
...SUPERTOINETTE_PROFILE,
|
|
817
988
|
claims(raw) {
|
|
818
|
-
const address =
|
|
989
|
+
const address = SITE_RECIPE_URL2.exec(raw);
|
|
819
990
|
if (address?.[1]) {
|
|
820
991
|
return {
|
|
821
992
|
reference: address[1],
|
|
@@ -823,7 +994,7 @@ function supertoinetteAdapter(reader) {
|
|
|
823
994
|
guess: false
|
|
824
995
|
};
|
|
825
996
|
}
|
|
826
|
-
if (
|
|
997
|
+
if (REFERENCE5.test(raw)) {
|
|
827
998
|
return {
|
|
828
999
|
reference: raw,
|
|
829
1000
|
why: "a bare number is the shape Supertoinette mints",
|
|
@@ -908,6 +1079,7 @@ function supertoinetteDetail(payload) {
|
|
|
908
1079
|
// the page numbered them.
|
|
909
1080
|
stepsAsOneBlock: null,
|
|
910
1081
|
withheld: null,
|
|
1082
|
+
gathers: null,
|
|
911
1083
|
category: text(recipe.category),
|
|
912
1084
|
author: text(recipe.author),
|
|
913
1085
|
// The scale is published, so a value is never read against a guess.
|
|
@@ -925,30 +1097,54 @@ function supertoinetteDetail(payload) {
|
|
|
925
1097
|
|
|
926
1098
|
// src/sources/registry.ts
|
|
927
1099
|
var PAGE_READ_BOUNDS = { maxBodyBytes: 8e6, budgetMs: 6e4 };
|
|
928
|
-
|
|
929
|
-
|
|
1100
|
+
var PACING_FLOOR_MS = {
|
|
1101
|
+
marmiton: 500,
|
|
1102
|
+
cookbook: 500,
|
|
1103
|
+
ptitchef: 1e3,
|
|
1104
|
+
goodfood: 1e3,
|
|
1105
|
+
supertoinette: 3e3,
|
|
1106
|
+
pequerecetas: 3e3
|
|
1107
|
+
};
|
|
1108
|
+
var PACED_SOURCES = Object.keys(PACING_FLOOR_MS);
|
|
1109
|
+
function pacingFor(source, configured) {
|
|
1110
|
+
return Math.max(configured, PACING_FLOOR_MS[source] ?? SLOWEST_FLOOR_MS);
|
|
1111
|
+
}
|
|
1112
|
+
var SLOWEST_FLOOR_MS = Math.max(...Object.values(PACING_FLOOR_MS));
|
|
1113
|
+
function pacedConfig(config, source) {
|
|
1114
|
+
return {
|
|
930
1115
|
userAgent: config.userAgent,
|
|
931
|
-
minIntervalMs: config.minIntervalMs,
|
|
1116
|
+
minIntervalMs: pacingFor(source, config.minIntervalMs),
|
|
932
1117
|
timeoutMs: config.timeoutMs,
|
|
933
1118
|
maxRetries: config.maxRetries,
|
|
934
1119
|
cacheTtlMs: config.cacheTtlMs,
|
|
935
1120
|
cacheMaxEntries: config.cacheMaxEntries,
|
|
936
1121
|
logLevel: config.logLevel
|
|
937
1122
|
};
|
|
938
|
-
|
|
939
|
-
|
|
1123
|
+
}
|
|
1124
|
+
function buildSources(config, readers, logger) {
|
|
1125
|
+
const paced = (source) => pacedConfig(config, source);
|
|
1126
|
+
const marmitonOptions = { config: paced(MARMITON_PROFILE.id) };
|
|
1127
|
+
const cookbookOptions = { config: paced(COOKBOOK_PROFILE.id) };
|
|
940
1128
|
const ptitchefOptions = {
|
|
941
|
-
config: { ...
|
|
1129
|
+
config: { ...paced(PTITCHEF_PROFILE.id), ...PAGE_READ_BOUNDS },
|
|
1130
|
+
logger
|
|
1131
|
+
};
|
|
1132
|
+
const goodfoodOptions = { config: paced(GOODFOOD_PROFILE.id), logger };
|
|
1133
|
+
const supertoinetteOptions = {
|
|
1134
|
+
config: paced(SUPERTOINETTE_PROFILE.id),
|
|
1135
|
+
logger
|
|
1136
|
+
};
|
|
1137
|
+
const pequerecetasOptions = {
|
|
1138
|
+
config: paced(PEQUERECETAS_PROFILE.id),
|
|
942
1139
|
logger
|
|
943
1140
|
};
|
|
944
|
-
const goodfoodOptions = { config: shared, logger };
|
|
945
|
-
const supertoinetteOptions = { config: shared, logger };
|
|
946
1141
|
return [
|
|
947
1142
|
marmitonAdapter(readers.marmiton ?? new MarmitonClient(marmitonOptions)),
|
|
948
1143
|
cookbookAdapter(readers.cookbook ?? new CookbookClient(cookbookOptions)),
|
|
949
1144
|
ptitchefAdapter(readers.ptitchef ?? new PtitchefClient(ptitchefOptions)),
|
|
950
1145
|
goodfoodAdapter(readers.goodfood ?? new GoodFoodClient(goodfoodOptions)),
|
|
951
|
-
supertoinetteAdapter(readers.supertoinette ?? new SupertoinetteClient(supertoinetteOptions))
|
|
1146
|
+
supertoinetteAdapter(readers.supertoinette ?? new SupertoinetteClient(supertoinetteOptions)),
|
|
1147
|
+
pequerecetasAdapter(readers.pequerecetas ?? new PequerecetasClient(pequerecetasOptions))
|
|
952
1148
|
];
|
|
953
1149
|
}
|
|
954
1150
|
function selectSources(sources, wanted) {
|
|
@@ -976,7 +1172,8 @@ var PROFILES = [
|
|
|
976
1172
|
COOKBOOK_PROFILE,
|
|
977
1173
|
PTITCHEF_PROFILE,
|
|
978
1174
|
GOODFOOD_PROFILE,
|
|
979
|
-
SUPERTOINETTE_PROFILE
|
|
1175
|
+
SUPERTOINETTE_PROFILE,
|
|
1176
|
+
PEQUERECETAS_PROFILE
|
|
980
1177
|
];
|
|
981
1178
|
|
|
982
1179
|
// src/sources/ids.ts
|
|
@@ -1133,6 +1330,84 @@ var FRENCH_FRAME = /* @__PURE__ */ new Set([
|
|
|
1133
1330
|
"vous",
|
|
1134
1331
|
"\xE0"
|
|
1135
1332
|
]);
|
|
1333
|
+
var SPANISH_FRAME = /* @__PURE__ */ new Set([
|
|
1334
|
+
"algo",
|
|
1335
|
+
"alguna",
|
|
1336
|
+
"algunas",
|
|
1337
|
+
"alguno",
|
|
1338
|
+
"algunos",
|
|
1339
|
+
"buenas",
|
|
1340
|
+
"buenos",
|
|
1341
|
+
"busco",
|
|
1342
|
+
"buscar",
|
|
1343
|
+
"casera",
|
|
1344
|
+
"caseras",
|
|
1345
|
+
"casero",
|
|
1346
|
+
"caseros",
|
|
1347
|
+
"como",
|
|
1348
|
+
"conoces",
|
|
1349
|
+
"cual",
|
|
1350
|
+
"cuales",
|
|
1351
|
+
"dame",
|
|
1352
|
+
"de",
|
|
1353
|
+
"del",
|
|
1354
|
+
"dime",
|
|
1355
|
+
"el",
|
|
1356
|
+
"ella",
|
|
1357
|
+
"ellos",
|
|
1358
|
+
"en",
|
|
1359
|
+
"es",
|
|
1360
|
+
"esta",
|
|
1361
|
+
"estoy",
|
|
1362
|
+
"facil",
|
|
1363
|
+
"faciles",
|
|
1364
|
+
"gracias",
|
|
1365
|
+
"hacer",
|
|
1366
|
+
"hago",
|
|
1367
|
+
"hay",
|
|
1368
|
+
"hola",
|
|
1369
|
+
"las",
|
|
1370
|
+
"los",
|
|
1371
|
+
"mas",
|
|
1372
|
+
"me",
|
|
1373
|
+
"mejor",
|
|
1374
|
+
"mejores",
|
|
1375
|
+
"necesito",
|
|
1376
|
+
"para",
|
|
1377
|
+
"pero",
|
|
1378
|
+
"por",
|
|
1379
|
+
"porfavor",
|
|
1380
|
+
"puedes",
|
|
1381
|
+
"que",
|
|
1382
|
+
"queria",
|
|
1383
|
+
"quiero",
|
|
1384
|
+
"quisiera",
|
|
1385
|
+
"receta",
|
|
1386
|
+
"recetas",
|
|
1387
|
+
"rica",
|
|
1388
|
+
"ricas",
|
|
1389
|
+
"rico",
|
|
1390
|
+
"ricos",
|
|
1391
|
+
"saber",
|
|
1392
|
+
"sabes",
|
|
1393
|
+
"se",
|
|
1394
|
+
"sencilla",
|
|
1395
|
+
"sencillas",
|
|
1396
|
+
"sencillo",
|
|
1397
|
+
"sencillos",
|
|
1398
|
+
"ser",
|
|
1399
|
+
"si",
|
|
1400
|
+
"son",
|
|
1401
|
+
"soy",
|
|
1402
|
+
"tengo",
|
|
1403
|
+
"tienes",
|
|
1404
|
+
"un",
|
|
1405
|
+
"una",
|
|
1406
|
+
"unas",
|
|
1407
|
+
"unos",
|
|
1408
|
+
"y",
|
|
1409
|
+
"yo"
|
|
1410
|
+
]);
|
|
1136
1411
|
var ENGLISH_FRAME = /* @__PURE__ */ new Set([
|
|
1137
1412
|
"a",
|
|
1138
1413
|
"about",
|
|
@@ -1244,6 +1519,8 @@ var NEGATORS = /* @__PURE__ */ new Set([
|
|
|
1244
1519
|
"pas",
|
|
1245
1520
|
"sans",
|
|
1246
1521
|
"sauf",
|
|
1522
|
+
"sin",
|
|
1523
|
+
"tampoco",
|
|
1247
1524
|
"without",
|
|
1248
1525
|
"zero"
|
|
1249
1526
|
]);
|
|
@@ -1265,7 +1542,12 @@ var HEALTH_MARKERS = /* @__PURE__ */ new Set([
|
|
|
1265
1542
|
"intolerant",
|
|
1266
1543
|
"intolerante",
|
|
1267
1544
|
"intolerantes",
|
|
1268
|
-
"
|
|
1545
|
+
"alergia",
|
|
1546
|
+
"alergias",
|
|
1547
|
+
"alergica",
|
|
1548
|
+
"alergico",
|
|
1549
|
+
"celiaca",
|
|
1550
|
+
"celiaco"
|
|
1269
1551
|
]);
|
|
1270
1552
|
var DIETS = /* @__PURE__ */ new Set([
|
|
1271
1553
|
"casher",
|
|
@@ -1287,6 +1569,12 @@ var DIETS = /* @__PURE__ */ new Set([
|
|
|
1287
1569
|
"vegetalienne",
|
|
1288
1570
|
"vegetaliennes",
|
|
1289
1571
|
"vegetaliens",
|
|
1572
|
+
"vegana",
|
|
1573
|
+
"veganas",
|
|
1574
|
+
"veganos",
|
|
1575
|
+
"vegetariana",
|
|
1576
|
+
"vegetarianas",
|
|
1577
|
+
"vegetarianos",
|
|
1290
1578
|
"vegetarian",
|
|
1291
1579
|
"vegetarians",
|
|
1292
1580
|
"vegetarien",
|
|
@@ -1340,10 +1628,10 @@ function foldWord(word) {
|
|
|
1340
1628
|
return word.toLowerCase().normalize("NFD").replace(new RegExp("\\p{M}+", "gu"), "").replace(/œ/gu, "oe");
|
|
1341
1629
|
}
|
|
1342
1630
|
function namesTheDish(word, key) {
|
|
1343
|
-
if (FRENCH_FRAME.has(key) || ENGLISH_FRAME.has(key)) {
|
|
1631
|
+
if (FRENCH_FRAME.has(key) || ENGLISH_FRAME.has(key) || SPANISH_FRAME.has(key)) {
|
|
1344
1632
|
return false;
|
|
1345
1633
|
}
|
|
1346
|
-
if (FRENCH_FRAME.has(word) || ENGLISH_FRAME.has(word)) {
|
|
1634
|
+
if (FRENCH_FRAME.has(word) || ENGLISH_FRAME.has(word) || SPANISH_FRAME.has(word)) {
|
|
1347
1635
|
return false;
|
|
1348
1636
|
}
|
|
1349
1637
|
if (DIGITS_ONLY.test(word)) {
|
|
@@ -1451,7 +1739,7 @@ function takeFood(words, from) {
|
|
|
1451
1739
|
}
|
|
1452
1740
|
function isFiller(word) {
|
|
1453
1741
|
const key = foldWord(word);
|
|
1454
|
-
return CONDITION_JOINERS.has(key) || FRENCH_FRAME.has(key) || ENGLISH_FRAME.has(key);
|
|
1742
|
+
return CONDITION_JOINERS.has(key) || FRENCH_FRAME.has(key) || ENGLISH_FRAME.has(key) || SPANISH_FRAME.has(key);
|
|
1455
1743
|
}
|
|
1456
1744
|
function namesFood(word) {
|
|
1457
1745
|
const key = foldWord(word);
|
|
@@ -1473,7 +1761,7 @@ function namesFood(word) {
|
|
|
1473
1761
|
if (CONDITION_UNITS.has(key)) {
|
|
1474
1762
|
return false;
|
|
1475
1763
|
}
|
|
1476
|
-
return !(FRENCH_FRAME.has(key) || ENGLISH_FRAME.has(key));
|
|
1764
|
+
return !(FRENCH_FRAME.has(key) || ENGLISH_FRAME.has(key) || SPANISH_FRAME.has(key));
|
|
1477
1765
|
}
|
|
1478
1766
|
function readConditions(question) {
|
|
1479
1767
|
const { conditions } = readQuestion(question);
|
|
@@ -1547,6 +1835,7 @@ function tidy(value) {
|
|
|
1547
1835
|
|
|
1548
1836
|
// src/sources/client.ts
|
|
1549
1837
|
var LETTER_OR_DIGIT = /[\p{L}\p{N}]/u;
|
|
1838
|
+
var LONGEST_WAIT_BETWEEN_TRIES_MS = 3e4;
|
|
1550
1839
|
var MAX_LIMIT_PER_SOURCE = 25;
|
|
1551
1840
|
function withGuarantees(config) {
|
|
1552
1841
|
const defaults = loadConfig({});
|
|
@@ -1588,7 +1877,8 @@ function withDeadline(work, ms, source) {
|
|
|
1588
1877
|
});
|
|
1589
1878
|
return Promise.race([work, alarm]).finally(() => clearTimeout(timer));
|
|
1590
1879
|
}
|
|
1591
|
-
function reportOf(attempt,
|
|
1880
|
+
function reportOf(attempt, rows, question) {
|
|
1881
|
+
const count2 = rows.length;
|
|
1592
1882
|
return {
|
|
1593
1883
|
source: attempt.source.id,
|
|
1594
1884
|
name: attempt.source.name,
|
|
@@ -1597,11 +1887,14 @@ function reportOf(attempt, count2) {
|
|
|
1597
1887
|
reportedTotal: attempt.reportedTotal,
|
|
1598
1888
|
reportedTotalMeans: attempt.reportedTotalMeans,
|
|
1599
1889
|
skipped: attempt.skipped,
|
|
1600
|
-
|
|
1890
|
+
rowsThatAreNotRecipes: attempt.source.rowsThatAreNotRecipes,
|
|
1601
1891
|
cached: attempt.cached,
|
|
1602
1892
|
error: attempt.error,
|
|
1603
1893
|
wordings: attempt.wordings,
|
|
1604
|
-
preferredByName: attempt.preferredByName
|
|
1894
|
+
preferredByName: attempt.preferredByName,
|
|
1895
|
+
// Counted over the rows this answer holds, so it is read against `count`
|
|
1896
|
+
// and never against a number of rows nobody was shown.
|
|
1897
|
+
namesTheDish: rows.filter((row) => namesDish(row.title, question)).length
|
|
1605
1898
|
};
|
|
1606
1899
|
}
|
|
1607
1900
|
function heldBack(wording, because) {
|
|
@@ -1677,11 +1970,20 @@ var RecipesClient = class {
|
|
|
1677
1970
|
}
|
|
1678
1971
|
/**
|
|
1679
1972
|
* The backstop deadline over one source, covering every attempt it is
|
|
1680
|
-
* entitled to make and the
|
|
1973
|
+
* entitled to make and the waiting between them.
|
|
1974
|
+
*
|
|
1975
|
+
* Three things a reader spends between two attempts, and all three belong
|
|
1976
|
+
* here or this fires while the reader is still working: the spacing that
|
|
1977
|
+
* source is read at, which is its own and not the shared setting; the backoff
|
|
1978
|
+
* a reader adds after a failure; and the wait a site asks for by name when it
|
|
1979
|
+
* answers "slow down". A deadline shorter than the reader's own abandons a
|
|
1980
|
+
* read whose requests carry on reaching the site, and leaves that source's
|
|
1981
|
+
* queue occupied behind an answer nobody waits for.
|
|
1681
1982
|
*/
|
|
1682
|
-
|
|
1983
|
+
deadlineFor(source) {
|
|
1683
1984
|
const attempts = this.config.maxRetries + 1;
|
|
1684
|
-
|
|
1985
|
+
const between = pacingFor(source, this.config.minIntervalMs) + LONGEST_WAIT_BETWEEN_TRIES_MS;
|
|
1986
|
+
return this.config.timeoutMs * attempts + between * this.config.maxRetries;
|
|
1685
1987
|
}
|
|
1686
1988
|
/**
|
|
1687
1989
|
* Ask every source the same question and merge the rows.
|
|
@@ -1710,7 +2012,7 @@ var RecipesClient = class {
|
|
|
1710
2012
|
const answered = attempts.map((attempt) => ({ attempt, rows: attempt.rows.slice(0, limit) }));
|
|
1711
2013
|
return {
|
|
1712
2014
|
rows: interleave(answered.map((one) => one.rows)),
|
|
1713
|
-
reports: answered.map(({ attempt, rows }) => reportOf(attempt, rows
|
|
2015
|
+
reports: answered.map(({ attempt, rows }) => reportOf(attempt, rows, trimmed))
|
|
1714
2016
|
};
|
|
1715
2017
|
}
|
|
1716
2018
|
/**
|
|
@@ -1772,7 +2074,7 @@ var RecipesClient = class {
|
|
|
1772
2074
|
try {
|
|
1773
2075
|
const read = await withDeadline(
|
|
1774
2076
|
source.search(wording.query, limit),
|
|
1775
|
-
this.
|
|
2077
|
+
this.deadlineFor(source.id),
|
|
1776
2078
|
source
|
|
1777
2079
|
);
|
|
1778
2080
|
answered = true;
|
|
@@ -1821,15 +2123,12 @@ var RecipesClient = class {
|
|
|
1821
2123
|
failure = reason;
|
|
1822
2124
|
}
|
|
1823
2125
|
}
|
|
1824
|
-
const
|
|
1825
|
-
const ordered =
|
|
1826
|
-
...rows.filter((row) => namesDish(row.title, question)),
|
|
1827
|
-
...rows.filter((row) => !namesDish(row.title, question))
|
|
1828
|
-
] : rows;
|
|
2126
|
+
const onTopicRows = rows.filter((row) => namesDish(row.title, question));
|
|
2127
|
+
const ordered = [...onTopicRows, ...rows.filter((row) => !namesDish(row.title, question))];
|
|
1829
2128
|
return {
|
|
1830
2129
|
source,
|
|
1831
2130
|
rows: ordered,
|
|
1832
|
-
preferredByName:
|
|
2131
|
+
preferredByName: true,
|
|
1833
2132
|
cached,
|
|
1834
2133
|
reportedTotal,
|
|
1835
2134
|
reportedTotalMeans,
|
|
@@ -1850,7 +2149,7 @@ var RecipesClient = class {
|
|
|
1850
2149
|
try {
|
|
1851
2150
|
const outcome = await withDeadline(
|
|
1852
2151
|
read.source.getRecipe(read.reference),
|
|
1853
|
-
this.
|
|
2152
|
+
this.deadlineFor(read.source.id),
|
|
1854
2153
|
read.source
|
|
1855
2154
|
);
|
|
1856
2155
|
return { recipe: outcome.recipe, cached: outcome.cached, read };
|
|
@@ -1866,21 +2165,194 @@ import { z as z4 } from "zod";
|
|
|
1866
2165
|
// src/tools/recipeView.ts
|
|
1867
2166
|
import { z as z2 } from "zod";
|
|
1868
2167
|
|
|
2168
|
+
// src/recipe/equipment.ts
|
|
2169
|
+
var LEADING_QUANTITY = /^\s*(?:\d+(?:[.,/]\d+)?|[¼½¾⅓⅔⅛])\s*/u;
|
|
2170
|
+
var LEADING_ARTICLE = /^(?:un|una|unos|unas|une|des|del|de|the|an?|le|la|los|las)\s+/iu;
|
|
2171
|
+
var WORD_SPLIT = /[\s,;:()]+/u;
|
|
2172
|
+
var TOOL_NOUNS = /* @__PURE__ */ new Set([
|
|
2173
|
+
// Spanish
|
|
2174
|
+
"freidora",
|
|
2175
|
+
"airfryer",
|
|
2176
|
+
"batidora",
|
|
2177
|
+
"licuadora",
|
|
2178
|
+
"amasadora",
|
|
2179
|
+
"thermomix",
|
|
2180
|
+
"robot",
|
|
2181
|
+
"olla",
|
|
2182
|
+
"paellera",
|
|
2183
|
+
"cazuela",
|
|
2184
|
+
"sarten",
|
|
2185
|
+
"molde",
|
|
2186
|
+
"rejilla",
|
|
2187
|
+
"brocha",
|
|
2188
|
+
"pincel",
|
|
2189
|
+
"espatula",
|
|
2190
|
+
"varilla",
|
|
2191
|
+
"varillas",
|
|
2192
|
+
"colador",
|
|
2193
|
+
"escurridor",
|
|
2194
|
+
"tamiz",
|
|
2195
|
+
"mortero",
|
|
2196
|
+
"rodillo",
|
|
2197
|
+
"boquilla",
|
|
2198
|
+
"termometro",
|
|
2199
|
+
"temporizador",
|
|
2200
|
+
"microondas",
|
|
2201
|
+
"vaporera",
|
|
2202
|
+
"parrilla",
|
|
2203
|
+
"cuchillo",
|
|
2204
|
+
"pelador",
|
|
2205
|
+
"rallador",
|
|
2206
|
+
"exprimidor",
|
|
2207
|
+
"pinza",
|
|
2208
|
+
"pinzas",
|
|
2209
|
+
"cuenco",
|
|
2210
|
+
"recipiente",
|
|
2211
|
+
"batidor",
|
|
2212
|
+
"prensa",
|
|
2213
|
+
"palillo",
|
|
2214
|
+
"palillos",
|
|
2215
|
+
"brocheta",
|
|
2216
|
+
"brochetas",
|
|
2217
|
+
// French
|
|
2218
|
+
"poele",
|
|
2219
|
+
"cocotte",
|
|
2220
|
+
"sauteuse",
|
|
2221
|
+
"saladier",
|
|
2222
|
+
"fouet",
|
|
2223
|
+
"spatule",
|
|
2224
|
+
"passoire",
|
|
2225
|
+
"mixeur",
|
|
2226
|
+
"mandoline",
|
|
2227
|
+
"pinceau",
|
|
2228
|
+
"casserole",
|
|
2229
|
+
// English
|
|
2230
|
+
"skillet",
|
|
2231
|
+
"saucepan",
|
|
2232
|
+
"whisk",
|
|
2233
|
+
"spatula",
|
|
2234
|
+
"sieve",
|
|
2235
|
+
"colander",
|
|
2236
|
+
"grater",
|
|
2237
|
+
"blender",
|
|
2238
|
+
"processor",
|
|
2239
|
+
"ramekin",
|
|
2240
|
+
"thermometer",
|
|
2241
|
+
"peeler"
|
|
2242
|
+
]);
|
|
2243
|
+
var AMBIGUOUS_TOOL_NOUNS = {
|
|
2244
|
+
papel: /^(?:hornear|horno|aluminio|film|vegetal|absorbente|cocina|parchment|baking)$/,
|
|
2245
|
+
papier: /^(?:cuisson|sulfurise|aluminium|film)$/,
|
|
2246
|
+
vaso: /^(?:medidor|batidora|thermomix)$/,
|
|
2247
|
+
cuchara: /^(?:madera|silicona|helado)$/,
|
|
2248
|
+
manga: /^(?:pastelera)$/,
|
|
2249
|
+
tabla: /^(?:cortar|madera)$/,
|
|
2250
|
+
plancha: /^(?:electrica|hierro)$/,
|
|
2251
|
+
horno: /^(?:precalentado)$/,
|
|
2252
|
+
bol: /^(?:grande|mediano|pequeno|hondo|cristal|acero)$/,
|
|
2253
|
+
plato: /^(?:hondo|llano|grande)$/,
|
|
2254
|
+
bandeja: /^(?:horno|hornear|pastelera)$/,
|
|
2255
|
+
fuente: /^(?:horno|hornear|cristal)$/,
|
|
2256
|
+
film: /^(?:transparente|plastico|alimentario)$/,
|
|
2257
|
+
moule: /^(?:tarte|manque|cake|silicone|charniere)$/,
|
|
2258
|
+
plat: /^(?:four|gratin|cuisson)$/,
|
|
2259
|
+
plaque: /^(?:four|cuisson|patisserie)$/,
|
|
2260
|
+
planche: /^(?:decouper|bois)$/,
|
|
2261
|
+
tin: /^(?:baking|cake|loaf|roasting|flan|sandwich)$/,
|
|
2262
|
+
pan: /^(?:frying|baking|roasting|cake|loaf|sauce|griddle)$/,
|
|
2263
|
+
dish: /^(?:baking|roasting|ovenproof|gratin)$/,
|
|
2264
|
+
tray: /^(?:baking|roasting|oven)$/,
|
|
2265
|
+
sheet: /^(?:baking|greaseproof|parchment)$/,
|
|
2266
|
+
bowl: /^(?:mixing|large|small)$/,
|
|
2267
|
+
mould: /^(?:cake|jelly|silicone)$/,
|
|
2268
|
+
mold: /^(?:cake|jelly|silicone)$/,
|
|
2269
|
+
paper: /^(?:baking|greaseproof|parchment)$/,
|
|
2270
|
+
foil: /^(?:aluminium|aluminum|kitchen)$/,
|
|
2271
|
+
board: /^(?:chopping|cutting|wooden)$/
|
|
2272
|
+
};
|
|
2273
|
+
var QUALIFIES_A_TOOL = /^(?:aire|hornear|horno|cocina|madera|silicona|silicone|barro|cristal|acero|hierro|aluminio|aluminium|plastico|papel|cortar|decouper|bois|varillas|mano|repostera|pasteleria|pastelera|tarte|manque|four|cuisson|gratin|patisserie|baking|frying|roasting|chopping|cutting|mixing|greaseproof|parchment|nonstick|cake|loaf|wooden|\d+|cm|mm|l|ml)$/;
|
|
2274
|
+
var PARTITIVE = /^(?:de|del|du|des|d|a|au|à)$/i;
|
|
2275
|
+
function fold2(word) {
|
|
2276
|
+
return word.normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/œ/g, "oe").toLowerCase();
|
|
2277
|
+
}
|
|
2278
|
+
function holds(vocabulary, word) {
|
|
2279
|
+
if (vocabulary.has(word)) {
|
|
2280
|
+
return true;
|
|
2281
|
+
}
|
|
2282
|
+
if (word.endsWith("es") && vocabulary.has(word.slice(0, -2))) {
|
|
2283
|
+
return true;
|
|
2284
|
+
}
|
|
2285
|
+
return word.endsWith("s") && vocabulary.has(word.slice(0, -1));
|
|
2286
|
+
}
|
|
2287
|
+
function isEquipmentLine(line) {
|
|
2288
|
+
const head = line.replace(LEADING_QUANTITY, "").replace(LEADING_ARTICLE, "").trim();
|
|
2289
|
+
const words = head.split(WORD_SPLIT).filter((word) => word !== "");
|
|
2290
|
+
const first = words[0];
|
|
2291
|
+
if (first === void 0) {
|
|
2292
|
+
return false;
|
|
2293
|
+
}
|
|
2294
|
+
const folded = fold2(first);
|
|
2295
|
+
if (holds(TOOL_NOUNS, folded)) {
|
|
2296
|
+
return notMeasuringSomethingElse(words);
|
|
2297
|
+
}
|
|
2298
|
+
const ambiguous = qualifierFor(folded);
|
|
2299
|
+
if (ambiguous) {
|
|
2300
|
+
return ambiguous.test(wordBeside(words, 0));
|
|
2301
|
+
}
|
|
2302
|
+
const second = words[1];
|
|
2303
|
+
if (second !== void 0 && QUALIFIES_A_TOOL.test(folded)) {
|
|
2304
|
+
const behind = qualifierFor(fold2(second));
|
|
2305
|
+
return behind ? behind.test(folded) : holds(TOOL_NOUNS, fold2(second));
|
|
2306
|
+
}
|
|
2307
|
+
return false;
|
|
2308
|
+
}
|
|
2309
|
+
function qualifierFor(word) {
|
|
2310
|
+
return AMBIGUOUS_TOOL_NOUNS[word] ?? (word.endsWith("es") ? AMBIGUOUS_TOOL_NOUNS[word.slice(0, -2)] : void 0) ?? (word.endsWith("s") ? AMBIGUOUS_TOOL_NOUNS[word.slice(0, -1)] : void 0);
|
|
2311
|
+
}
|
|
2312
|
+
function wordAt(words, index) {
|
|
2313
|
+
const word = words[index];
|
|
2314
|
+
return word === void 0 ? "" : fold2(word);
|
|
2315
|
+
}
|
|
2316
|
+
function wordBeside(words, index) {
|
|
2317
|
+
const next = wordAt(words, index + 1);
|
|
2318
|
+
return PARTITIVE.test(next) ? wordAt(words, index + 2) : next;
|
|
2319
|
+
}
|
|
2320
|
+
function notMeasuringSomethingElse(words) {
|
|
2321
|
+
const after = words[1];
|
|
2322
|
+
if (after === void 0 || !PARTITIVE.test(fold2(after))) {
|
|
2323
|
+
return true;
|
|
2324
|
+
}
|
|
2325
|
+
return QUALIFIES_A_TOOL.test(wordAt(words, 2));
|
|
2326
|
+
}
|
|
2327
|
+
var EQUIPMENT_NOTE = "This line names a tool, so it was left as published: a recipe made for more people uses the same one.";
|
|
2328
|
+
|
|
1869
2329
|
// src/recipe/language.ts
|
|
1870
|
-
var FRENCH_WORDS = /(?:^|[\s(])(?:
|
|
2330
|
+
var FRENCH_WORDS = /(?:^|[\s(])(?:du|des|à|au|aux|le|les|une|quelques|dans|avec|sans|pour|selon|environ)(?=[\s),.]|$)|\bd'|\bl'|\bqu'/iu;
|
|
1871
2331
|
var ENGLISH_WORDS = /(?:^|[\s(])(?:of|an|the|and|with|without|for|into|about|each|per|plus|freshly|finely|roughly|coarsely|optional)(?=[\s),.]|$)/iu;
|
|
1872
|
-
var
|
|
1873
|
-
var
|
|
2332
|
+
var SPANISH_WORDS = /(?:^|[\s(])(?:del|el|los|las|una|unos|unas|con|sin|para|al|y|según|aproximadamente|picad[oa]s?|tro?cead[oa]s?|rallad[oa]s?|molid[oa]s?|cortad[oa]s?|pelad[oa]s?|opcional)(?=[\s),.]|$)/iu;
|
|
2333
|
+
var ROMANCE_WORDS = /(?:^|[\s(])(?:de|la|un|en)(?=[\s),.]|$)/iu;
|
|
2334
|
+
var FRENCH_LETTERS = /[àâçèêëîïôùûœ]/iu;
|
|
2335
|
+
var SPANISH_LETTERS = /[áíóúñ¿¡]/iu;
|
|
2336
|
+
var ROMANCE_LETTERS = /[éü]/iu;
|
|
2337
|
+
var FRENCH_FOODS = /\b(?:oeufs?|œufs?|sel|poivre|sucres?|farine|beurre|lait|crème|creme|huile|ail|oignons?|fromage|pain|poulet|boeuf|bœuf|riz|pâtes|pates|jambon|citrons?|pommes?|vin|eau|persil|thym|jaunes?|blancs?)\b/iu;
|
|
1874
2338
|
var ENGLISH_FOODS = /\b(?:eggs?|salt|pepper|sugar|flour|butter|milk|cream|oil|garlic|onions?|cheese|bread|chicken|beef|rice|pasta|ham|lemons?|apples?|wine|water|parsley|thyme|honey|yolks?|whites?)\b/iu;
|
|
2339
|
+
var SPANISH_FOODS = /\b(?:huevos?|sal|pimienta|azúcar|azucar|harina|mantequilla|leche|nata|aceite|ajos?|cebollas?|queso|pollo|ternera|cerdo|arroz|jamón|jamon|limones|limón|limon|manzanas?|patatas?|vino|agua|perejil|tomillo|yemas?|claras?)\b/iu;
|
|
1875
2340
|
var STRUCTURAL = 3;
|
|
1876
2341
|
var VOCABULARY = 2;
|
|
1877
2342
|
var NAME = 1;
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
englishUnit: false
|
|
1881
|
-
}) {
|
|
2343
|
+
var NO_UNIT = { frenchUnit: false, englishUnit: false, spanishUnit: false };
|
|
2344
|
+
function readLanguage(line, evidence = NO_UNIT) {
|
|
1882
2345
|
let french = 0;
|
|
1883
2346
|
let english = 0;
|
|
2347
|
+
let spanish = 0;
|
|
2348
|
+
if (ROMANCE_WORDS.test(line)) {
|
|
2349
|
+
french += STRUCTURAL;
|
|
2350
|
+
spanish += STRUCTURAL;
|
|
2351
|
+
}
|
|
2352
|
+
if (ROMANCE_LETTERS.test(line)) {
|
|
2353
|
+
french += STRUCTURAL;
|
|
2354
|
+
spanish += STRUCTURAL;
|
|
2355
|
+
}
|
|
1884
2356
|
if (FRENCH_WORDS.test(line)) {
|
|
1885
2357
|
french += STRUCTURAL;
|
|
1886
2358
|
}
|
|
@@ -1890,19 +2362,40 @@ function readLanguage(line, evidence = {
|
|
|
1890
2362
|
if (ENGLISH_WORDS.test(line)) {
|
|
1891
2363
|
english += STRUCTURAL;
|
|
1892
2364
|
}
|
|
2365
|
+
if (SPANISH_WORDS.test(line)) {
|
|
2366
|
+
spanish += STRUCTURAL;
|
|
2367
|
+
}
|
|
2368
|
+
if (SPANISH_LETTERS.test(line)) {
|
|
2369
|
+
spanish += STRUCTURAL;
|
|
2370
|
+
}
|
|
1893
2371
|
if (evidence.frenchUnit) {
|
|
1894
2372
|
french += VOCABULARY;
|
|
1895
2373
|
}
|
|
1896
2374
|
if (evidence.englishUnit) {
|
|
1897
2375
|
english += VOCABULARY;
|
|
1898
2376
|
}
|
|
2377
|
+
if (evidence.spanishUnit) {
|
|
2378
|
+
spanish += VOCABULARY;
|
|
2379
|
+
}
|
|
1899
2380
|
if (FRENCH_FOODS.test(line)) {
|
|
1900
2381
|
french += NAME;
|
|
1901
2382
|
}
|
|
1902
2383
|
if (ENGLISH_FOODS.test(line)) {
|
|
1903
2384
|
english += NAME;
|
|
1904
2385
|
}
|
|
1905
|
-
|
|
2386
|
+
if (SPANISH_FOODS.test(line)) {
|
|
2387
|
+
spanish += NAME;
|
|
2388
|
+
}
|
|
2389
|
+
return { language: winner(french, english, spanish), french, english, spanish };
|
|
2390
|
+
}
|
|
2391
|
+
function winner(french, english, spanish) {
|
|
2392
|
+
if (french >= spanish && french > english) {
|
|
2393
|
+
return "fr";
|
|
2394
|
+
}
|
|
2395
|
+
if (spanish > english) {
|
|
2396
|
+
return "es";
|
|
2397
|
+
}
|
|
2398
|
+
return "en";
|
|
1906
2399
|
}
|
|
1907
2400
|
|
|
1908
2401
|
// src/recipe/units.ts
|
|
@@ -1912,10 +2405,14 @@ var EAUX_ENDING = /eaux$/i;
|
|
|
1912
2405
|
var EAU_ENDING = /eau$/i;
|
|
1913
2406
|
var FUL_MEASURE = /^[a-z]{3,}fuls?$/;
|
|
1914
2407
|
var HEAD_BEFORE_DE = new RegExp("^\\s*(\\p{L}+)\\s+(?=(?:de|du|des)\\s|d')", "u");
|
|
1915
|
-
var HOUSEHOLD_MEASURE = /^(cup|tablespoon|Tbsp|teaspoon|tsp|cuillère à soupe|cuillère à café|tasse)$/;
|
|
2408
|
+
var HOUSEHOLD_MEASURE = /^(cup|tablespoon|Tbsp|teaspoon|tsp|cuillère à soupe|cuillère à café|tasse|taza|cucharada|cucharadita|cda|cdta)$/;
|
|
1916
2409
|
var SIBILANT_LETTER = /[sxz]$/i;
|
|
1917
2410
|
var TRAILING_S_ANY_CASE = /s$/i;
|
|
1918
2411
|
var VOWEL_THEN_S = /[aiou]s$/i;
|
|
2412
|
+
var INVARIABLE_SPANISH = /(?:is|us)$/i;
|
|
2413
|
+
var ES_ENDING = /(?:[^aeiouáéíóú]es)$/i;
|
|
2414
|
+
var SPANISH_VOWEL_ENDING = /[aeiouáéíóú]$/i;
|
|
2415
|
+
var Z_ENDING = /z$/i;
|
|
1919
2416
|
var SHARED_UNITS = {
|
|
1920
2417
|
mg: { canonical: "mg", kind: "measured", system: "metric", symbol: true },
|
|
1921
2418
|
g: { canonical: "g", kind: "measured", system: "metric", symbol: true },
|
|
@@ -2197,9 +2694,102 @@ var ENGLISH_UNITS = {
|
|
|
2197
2694
|
knob: { canonical: "knob", kind: "approximate", system: "none" },
|
|
2198
2695
|
knobs: { canonical: "knob", kind: "approximate", system: "none" }
|
|
2199
2696
|
};
|
|
2697
|
+
var SPANISH_UNITS = {
|
|
2698
|
+
...SHARED_UNITS,
|
|
2699
|
+
gramo: { canonical: "g", kind: "measured", system: "metric", symbol: true },
|
|
2700
|
+
gramos: { canonical: "g", kind: "measured", system: "metric", symbol: true },
|
|
2701
|
+
kilogramo: { canonical: "kg", kind: "measured", system: "metric", symbol: true },
|
|
2702
|
+
kilogramos: { canonical: "kg", kind: "measured", system: "metric", symbol: true },
|
|
2703
|
+
litro: { canonical: "l", kind: "measured", system: "metric", symbol: true },
|
|
2704
|
+
litros: { canonical: "l", kind: "measured", system: "metric", symbol: true },
|
|
2705
|
+
mililitro: { canonical: "ml", kind: "measured", system: "metric", symbol: true },
|
|
2706
|
+
mililitros: { canonical: "ml", kind: "measured", system: "metric", symbol: true },
|
|
2707
|
+
decilitro: { canonical: "dl", kind: "measured", system: "metric", symbol: true },
|
|
2708
|
+
decilitros: { canonical: "dl", kind: "measured", system: "metric", symbol: true },
|
|
2709
|
+
centilitro: { canonical: "cl", kind: "measured", system: "metric", symbol: true },
|
|
2710
|
+
centilitros: { canonical: "cl", kind: "measured", system: "metric", symbol: true },
|
|
2711
|
+
// A page glossing a metric weight names the pound and the ounce, which
|
|
2712
|
+
// Spanish writes in full.
|
|
2713
|
+
libra: { canonical: "libra", kind: "measured", system: "imperial", plural: "libras" },
|
|
2714
|
+
libras: { canonical: "libra", kind: "measured", system: "imperial", plural: "libras" },
|
|
2715
|
+
onza: { canonical: "onza", kind: "measured", system: "imperial", plural: "onzas" },
|
|
2716
|
+
onzas: { canonical: "onza", kind: "measured", system: "imperial", plural: "onzas" },
|
|
2717
|
+
// Spoons and cups: real measures, but only in sensible fractions.
|
|
2718
|
+
cucharada: { canonical: "cucharada", kind: "portioned", system: "none", plural: "cucharadas" },
|
|
2719
|
+
cucharadas: { canonical: "cucharada", kind: "portioned", system: "none", plural: "cucharadas" },
|
|
2720
|
+
cda: { canonical: "cda", kind: "portioned", system: "none", symbol: true },
|
|
2721
|
+
cdas: { canonical: "cda", kind: "portioned", system: "none", symbol: true },
|
|
2722
|
+
"c s": { canonical: "cucharada", kind: "portioned", system: "none", plural: "cucharadas" },
|
|
2723
|
+
cucharadita: {
|
|
2724
|
+
canonical: "cucharadita",
|
|
2725
|
+
kind: "portioned",
|
|
2726
|
+
system: "none",
|
|
2727
|
+
plural: "cucharaditas"
|
|
2728
|
+
},
|
|
2729
|
+
cucharaditas: {
|
|
2730
|
+
canonical: "cucharadita",
|
|
2731
|
+
kind: "portioned",
|
|
2732
|
+
system: "none",
|
|
2733
|
+
plural: "cucharaditas"
|
|
2734
|
+
},
|
|
2735
|
+
cdta: { canonical: "cdta", kind: "portioned", system: "none", symbol: true },
|
|
2736
|
+
cdtas: { canonical: "cdta", kind: "portioned", system: "none", symbol: true },
|
|
2737
|
+
"c p": { canonical: "cucharadita", kind: "portioned", system: "none", plural: "cucharaditas" },
|
|
2738
|
+
taza: { canonical: "taza", kind: "portioned", system: "none", plural: "tazas" },
|
|
2739
|
+
tazas: { canonical: "taza", kind: "portioned", system: "none", plural: "tazas" },
|
|
2740
|
+
vaso: { canonical: "vaso", kind: "portioned", system: "none" },
|
|
2741
|
+
vasos: { canonical: "vaso", kind: "portioned", system: "none" },
|
|
2742
|
+
// Packaging and natural units: countable, so they round to whole things.
|
|
2743
|
+
sobre: { canonical: "sobre", kind: "portioned", system: "none" },
|
|
2744
|
+
sobres: { canonical: "sobre", kind: "portioned", system: "none" },
|
|
2745
|
+
diente: { canonical: "diente", kind: "portioned", system: "none" },
|
|
2746
|
+
dientes: { canonical: "diente", kind: "portioned", system: "none" },
|
|
2747
|
+
lata: { canonical: "lata", kind: "portioned", system: "none", plural: "latas" },
|
|
2748
|
+
latas: { canonical: "lata", kind: "portioned", system: "none", plural: "latas" },
|
|
2749
|
+
bote: { canonical: "bote", kind: "portioned", system: "none" },
|
|
2750
|
+
botes: { canonical: "bote", kind: "portioned", system: "none" },
|
|
2751
|
+
tarro: { canonical: "tarro", kind: "portioned", system: "none" },
|
|
2752
|
+
tarros: { canonical: "tarro", kind: "portioned", system: "none" },
|
|
2753
|
+
paquete: { canonical: "paquete", kind: "portioned", system: "none" },
|
|
2754
|
+
paquetes: { canonical: "paquete", kind: "portioned", system: "none" },
|
|
2755
|
+
loncha: { canonical: "loncha", kind: "portioned", system: "none", plural: "lonchas" },
|
|
2756
|
+
lonchas: { canonical: "loncha", kind: "portioned", system: "none", plural: "lonchas" },
|
|
2757
|
+
rebanada: { canonical: "rebanada", kind: "portioned", system: "none", plural: "rebanadas" },
|
|
2758
|
+
rebanadas: { canonical: "rebanada", kind: "portioned", system: "none", plural: "rebanadas" },
|
|
2759
|
+
rodaja: { canonical: "rodaja", kind: "portioned", system: "none", plural: "rodajas" },
|
|
2760
|
+
rodajas: { canonical: "rodaja", kind: "portioned", system: "none", plural: "rodajas" },
|
|
2761
|
+
hoja: { canonical: "hoja", kind: "portioned", system: "none", plural: "hojas" },
|
|
2762
|
+
hojas: { canonical: "hoja", kind: "portioned", system: "none", plural: "hojas" },
|
|
2763
|
+
rama: { canonical: "rama", kind: "portioned", system: "none", plural: "ramas" },
|
|
2764
|
+
ramas: { canonical: "rama", kind: "portioned", system: "none", plural: "ramas" },
|
|
2765
|
+
ramita: { canonical: "ramita", kind: "portioned", system: "none", plural: "ramitas" },
|
|
2766
|
+
ramitas: { canonical: "ramita", kind: "portioned", system: "none", plural: "ramitas" },
|
|
2767
|
+
manojo: { canonical: "manojo", kind: "portioned", system: "none" },
|
|
2768
|
+
manojos: { canonical: "manojo", kind: "portioned", system: "none" },
|
|
2769
|
+
unidad: { canonical: "unidad", kind: "portioned", system: "none", plural: "unidades" },
|
|
2770
|
+
unidades: { canonical: "unidad", kind: "portioned", system: "none", plural: "unidades" },
|
|
2771
|
+
// Held to no better than a hand: the count scales, the size of one does not.
|
|
2772
|
+
pizca: { canonical: "pizca", kind: "approximate", system: "none", plural: "pizcas" },
|
|
2773
|
+
pizcas: { canonical: "pizca", kind: "approximate", system: "none", plural: "pizcas" },
|
|
2774
|
+
punado: { canonical: "pu\xF1ado", kind: "approximate", system: "none", plural: "pu\xF1ados" },
|
|
2775
|
+
punados: { canonical: "pu\xF1ado", kind: "approximate", system: "none", plural: "pu\xF1ados" },
|
|
2776
|
+
chorro: { canonical: "chorro", kind: "approximate", system: "none" },
|
|
2777
|
+
chorros: { canonical: "chorro", kind: "approximate", system: "none" },
|
|
2778
|
+
chorrito: { canonical: "chorrito", kind: "approximate", system: "none" },
|
|
2779
|
+
chorritos: { canonical: "chorrito", kind: "approximate", system: "none" },
|
|
2780
|
+
gota: { canonical: "gota", kind: "approximate", system: "none", plural: "gotas" },
|
|
2781
|
+
gotas: { canonical: "gota", kind: "approximate", system: "none", plural: "gotas" },
|
|
2782
|
+
cazo: { canonical: "cazo", kind: "approximate", system: "none" },
|
|
2783
|
+
cazos: { canonical: "cazo", kind: "approximate", system: "none" },
|
|
2784
|
+
cucharon: { canonical: "cuchar\xF3n", kind: "approximate", system: "none", plural: "cucharones" },
|
|
2785
|
+
cucharones: { canonical: "cuchar\xF3n", kind: "approximate", system: "none", plural: "cucharones" },
|
|
2786
|
+
toque: { canonical: "toque", kind: "approximate", system: "none" },
|
|
2787
|
+
toques: { canonical: "toque", kind: "approximate", system: "none" }
|
|
2788
|
+
};
|
|
2200
2789
|
var VOCABULARY2 = {
|
|
2201
2790
|
fr: FRENCH_UNITS,
|
|
2202
|
-
en: ENGLISH_UNITS
|
|
2791
|
+
en: ENGLISH_UNITS,
|
|
2792
|
+
es: SPANISH_UNITS
|
|
2203
2793
|
};
|
|
2204
2794
|
function normalizeUnitKey(text2) {
|
|
2205
2795
|
return text2.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/\./g, " ").replace(/\((?:s|x|es)\)/g, "").replace(/\s+/g, " ").trim();
|
|
@@ -2211,7 +2801,8 @@ function orderedKeys(vocabulary) {
|
|
|
2211
2801
|
}
|
|
2212
2802
|
var UNIT_KEYS = {
|
|
2213
2803
|
fr: orderedKeys(FRENCH_UNITS),
|
|
2214
|
-
en: orderedKeys(ENGLISH_UNITS)
|
|
2804
|
+
en: orderedKeys(ENGLISH_UNITS),
|
|
2805
|
+
es: orderedKeys(SPANISH_UNITS)
|
|
2215
2806
|
};
|
|
2216
2807
|
function unitKeys(language) {
|
|
2217
2808
|
return UNIT_KEYS[language];
|
|
@@ -2221,7 +2812,8 @@ function lookupUnit(text2, language) {
|
|
|
2221
2812
|
}
|
|
2222
2813
|
var EMBEDDED = {
|
|
2223
2814
|
fr: buildEmbedded("fr"),
|
|
2224
|
-
en: buildEmbedded("en")
|
|
2815
|
+
en: buildEmbedded("en"),
|
|
2816
|
+
es: buildEmbedded("es")
|
|
2225
2817
|
};
|
|
2226
2818
|
function buildEmbedded(language) {
|
|
2227
2819
|
const keys = UNIT_KEYS[language].map((key) => key.replace(/ /g, "\\s+"));
|
|
@@ -2248,7 +2840,7 @@ var NOT_A_MEASURE = /* @__PURE__ */ new Set([
|
|
|
2248
2840
|
"melange",
|
|
2249
2841
|
"ensemble"
|
|
2250
2842
|
]);
|
|
2251
|
-
function readPartitiveMeasure(text2) {
|
|
2843
|
+
function readPartitiveMeasure(text2, language) {
|
|
2252
2844
|
const match = HEAD_BEFORE_DE.exec(text2);
|
|
2253
2845
|
if (!match) {
|
|
2254
2846
|
return null;
|
|
@@ -2260,16 +2852,16 @@ function readPartitiveMeasure(text2) {
|
|
|
2260
2852
|
if (NOT_A_MEASURE.has(normalizeUnitKey(word))) {
|
|
2261
2853
|
return null;
|
|
2262
2854
|
}
|
|
2263
|
-
if (lookupUnit(word,
|
|
2855
|
+
if (lookupUnit(word, language)) {
|
|
2264
2856
|
return null;
|
|
2265
2857
|
}
|
|
2266
|
-
const canonical = frenchSingular(word);
|
|
2858
|
+
const canonical = language === "es" ? spanishSingular(word) : frenchSingular(word);
|
|
2267
2859
|
return {
|
|
2268
2860
|
unit: {
|
|
2269
2861
|
canonical,
|
|
2270
2862
|
kind: "approximate",
|
|
2271
2863
|
system: "none",
|
|
2272
|
-
plural: frenchPlural(canonical)
|
|
2864
|
+
plural: language === "es" ? spanishPlural(canonical) : frenchPlural(canonical)
|
|
2273
2865
|
},
|
|
2274
2866
|
rest: text2.slice(match[0].length)
|
|
2275
2867
|
};
|
|
@@ -2297,6 +2889,30 @@ function frenchSingular(word) {
|
|
|
2297
2889
|
}
|
|
2298
2890
|
return word;
|
|
2299
2891
|
}
|
|
2892
|
+
function spanishSingular(word) {
|
|
2893
|
+
if (INVARIABLE_SPANISH.test(word)) {
|
|
2894
|
+
return word;
|
|
2895
|
+
}
|
|
2896
|
+
if (ES_ENDING.test(word) && word.length > 4) {
|
|
2897
|
+
return word.slice(0, -2);
|
|
2898
|
+
}
|
|
2899
|
+
if (TRAILING_S_ANY_CASE.test(word) && word.length > 3) {
|
|
2900
|
+
return word.slice(0, -1);
|
|
2901
|
+
}
|
|
2902
|
+
return word;
|
|
2903
|
+
}
|
|
2904
|
+
function spanishPlural(word) {
|
|
2905
|
+
if (INVARIABLE_SPANISH.test(word)) {
|
|
2906
|
+
return word;
|
|
2907
|
+
}
|
|
2908
|
+
if (SPANISH_VOWEL_ENDING.test(word)) {
|
|
2909
|
+
return `${word}s`;
|
|
2910
|
+
}
|
|
2911
|
+
if (Z_ENDING.test(word)) {
|
|
2912
|
+
return `${word.slice(0, -1)}ces`;
|
|
2913
|
+
}
|
|
2914
|
+
return `${word}es`;
|
|
2915
|
+
}
|
|
2300
2916
|
function frenchPlural(word) {
|
|
2301
2917
|
if (SIBILANT_LETTER.test(word)) {
|
|
2302
2918
|
return word;
|
|
@@ -2326,7 +2942,13 @@ var APPROXIMATE_EQUIVALENT = {
|
|
|
2326
2942
|
filet: "commonly taken as about a teaspoon poured in a thin line",
|
|
2327
2943
|
noix: "commonly taken as about a tablespoon of butter",
|
|
2328
2944
|
louche: "commonly taken as about half a cup",
|
|
2329
|
-
soupcon: "commonly taken as the smallest amount a spoon tip carries"
|
|
2945
|
+
soupcon: "commonly taken as the smallest amount a spoon tip carries",
|
|
2946
|
+
pizca: "commonly taken as about half a teaspoon",
|
|
2947
|
+
punado: "commonly taken as about a quarter of a cup",
|
|
2948
|
+
chorrito: "commonly taken as about a teaspoon poured in a thin line",
|
|
2949
|
+
chorro: "commonly taken as about a tablespoon poured free-hand",
|
|
2950
|
+
gota: "commonly taken as a single drop",
|
|
2951
|
+
cucharon: "commonly taken as about half a cup"
|
|
2330
2952
|
};
|
|
2331
2953
|
function approximateEquivalent(unit) {
|
|
2332
2954
|
return APPROXIMATE_EQUIVALENT[normalizeUnitKey(unit.canonical)] ?? null;
|
|
@@ -2354,6 +2976,9 @@ var DEMOTIONS = {
|
|
|
2354
2976
|
tbsp: { to: "tsp", per: 3, language: "en" },
|
|
2355
2977
|
"cuillere a soupe": { to: "cuillere a cafe", per: 3, language: "fr" },
|
|
2356
2978
|
tasse: { to: "cuillere a soupe", per: 16, language: "fr" },
|
|
2979
|
+
cucharada: { to: "cucharadita", per: 3, language: "es" },
|
|
2980
|
+
cda: { to: "cdta", per: 3, language: "es" },
|
|
2981
|
+
taza: { to: "cucharada", per: 16, language: "es" },
|
|
2357
2982
|
kg: { to: "g", per: 1e3, language: "en" },
|
|
2358
2983
|
g: { to: "mg", per: 1e3, language: "en" },
|
|
2359
2984
|
l: { to: "cl", per: 100, language: "en" },
|
|
@@ -2382,7 +3007,7 @@ function unitDivisibility(unit) {
|
|
|
2382
3007
|
}
|
|
2383
3008
|
return QUARTERED_MEASURE.test(unit.canonical) ? "quarter" : "half";
|
|
2384
3009
|
}
|
|
2385
|
-
var QUARTERED_MEASURE = /\b(pots?|bouteilles?|bottles?|jars?|blocs?|blocks?|tranches?|slices?)\b/i;
|
|
3010
|
+
var QUARTERED_MEASURE = /\b(pots?|bouteilles?|bottles?|jars?|blocs?|blocks?|tranches?|slices?|botes?|tarros?|botellas?|bloques?|lonchas?|rebanadas?|rodajas?)\b/i;
|
|
2386
3011
|
function countsBarePieces(unit) {
|
|
2387
3012
|
return unit.canonical === "ea";
|
|
2388
3013
|
}
|
|
@@ -2429,17 +3054,22 @@ function formatUnit(unit, amount, language) {
|
|
|
2429
3054
|
if (singular) {
|
|
2430
3055
|
return unit.canonical;
|
|
2431
3056
|
}
|
|
2432
|
-
|
|
3057
|
+
if (unit.plural) {
|
|
3058
|
+
return unit.plural;
|
|
3059
|
+
}
|
|
3060
|
+
return language === "es" ? spanishPlural(unit.canonical) : `${unit.canonical}s`;
|
|
2433
3061
|
}
|
|
2434
3062
|
|
|
2435
3063
|
// src/recipe/quantity.ts
|
|
2436
3064
|
var BRACKETED_TAIL = /\s*(\([^()]*\))\s*$/;
|
|
2437
|
-
var
|
|
3065
|
+
var LEADING_ARTICLE2 = /^(?:of\s+)?an?\s+/i;
|
|
2438
3066
|
var LEADING_ARTICLE_FRENCH = /^(?:une|un)\s+/i;
|
|
2439
3067
|
var LEADING_INDEFINITE_WITH_SPACE = /^an?\s+/i;
|
|
2440
3068
|
var LEADING_PARTITIVE = /^(?:de\s+la\s+|de\s+l'|d'|de\s+|du\s+|des\s+)/i;
|
|
2441
3069
|
var OF_OPENING = /^of\s+/i;
|
|
2442
3070
|
var OPENING_ARTICLE_FRENCH = /^\s*(un|une)\b\s*/i;
|
|
3071
|
+
var OPENING_ARTICLE_SPANISH = /^\s*(un|una)\b\s*/i;
|
|
3072
|
+
var LEADING_ARTICLE_SPANISH = /^(?:una|un)\s+/i;
|
|
2443
3073
|
var REPEATED_COMMA_GROUPS = /^\s*\d+,\d+,\d/;
|
|
2444
3074
|
var SLASH_OUTSIDE_A_FRACTION = /(?<!\d)\/|\/(?!\d)/;
|
|
2445
3075
|
var ATTACHED_DE = /\s+de\s+(?=\d)/i;
|
|
@@ -2448,19 +3078,47 @@ var DIGIT = /\d/;
|
|
|
2448
3078
|
var ENGLISH_RANGE_JOINER = /^\s+(to|or)\s+/i;
|
|
2449
3079
|
var FRACTION_WORD = /^(?:(a|an|one|two|three)[\s-]+)?(halves|half|thirds|third|quarters|quarter|fourths|fourth)\b/i;
|
|
2450
3080
|
var FRENCH_RANGE_JOINER = /^\s+(à|a|ou)\s+/i;
|
|
3081
|
+
var SPANISH_RANGE_JOINER = /^\s+(a|o)\s+/i;
|
|
3082
|
+
var SPANISH_FRACTION_WORD = /^(?:medi[oa]s?\b|(?:(un|una|dos|tres)\s+)?(cuartos|cuarto|tercios|tercio|mitad(?:es)?)\b)/i;
|
|
2451
3083
|
var GROUPED_DECIMAL = /^(\d{1,3}(?:,\d{3})+(?:\.\d+)?|\d+(?:\.\d+)?)/;
|
|
3084
|
+
function writesDecimalComma(language) {
|
|
3085
|
+
return language !== "en";
|
|
3086
|
+
}
|
|
3087
|
+
function usesPartitiveDe(language) {
|
|
3088
|
+
return language !== "en";
|
|
3089
|
+
}
|
|
3090
|
+
var OPENING_ARTICLE_ROMANCE = {
|
|
3091
|
+
en: OPENING_ARTICLE_FRENCH,
|
|
3092
|
+
fr: OPENING_ARTICLE_FRENCH,
|
|
3093
|
+
es: OPENING_ARTICLE_SPANISH
|
|
3094
|
+
};
|
|
3095
|
+
var ROMANCE_ARTICLE = {
|
|
3096
|
+
en: LEADING_ARTICLE_FRENCH,
|
|
3097
|
+
fr: LEADING_ARTICLE_FRENCH,
|
|
3098
|
+
es: LEADING_ARTICLE_SPANISH
|
|
3099
|
+
};
|
|
3100
|
+
var RANGE_JOINER = {
|
|
3101
|
+
en: ENGLISH_RANGE_JOINER,
|
|
3102
|
+
fr: FRENCH_RANGE_JOINER,
|
|
3103
|
+
es: SPANISH_RANGE_JOINER
|
|
3104
|
+
};
|
|
3105
|
+
var OTHER_LANGUAGES = {
|
|
3106
|
+
en: ["fr", "es"],
|
|
3107
|
+
fr: ["es", "en"],
|
|
3108
|
+
es: ["fr", "en"]
|
|
3109
|
+
};
|
|
2452
3110
|
var HYPHENATED_TAIL = new RegExp("^-\\p{L}", "u");
|
|
2453
3111
|
var LEADING_DASH = /^\s*(–|—|-)\s*/;
|
|
2454
3112
|
var LEADING_INDEFINITE = /^an?\s/i;
|
|
2455
3113
|
var LEADING_OF = /^\s*of\s+/i;
|
|
2456
|
-
var
|
|
3114
|
+
var LEADING_QUANTITY2 = /^[\s\d.,/½⅓⅔¼¾⅕⅖⅗⅘⅙⅚⅛⅜⅝⅞\-–—]+/u;
|
|
2457
3115
|
var LEADING_WORD = new RegExp("^\\s*(\\p{L}+)\\s+", "u");
|
|
2458
3116
|
var MIXED_FRACTION = /^(\d+)\s+(\d+)\s*\/\s*(\d+)/;
|
|
2459
3117
|
var PARTITIVE_BEFORE_BRACKET = /^(?:de\s+|du\s+|des\s+|d'|of\s+)(?=\()/i;
|
|
2460
3118
|
var PARTITIVE_OPENING = /^(?:de\s|d'|du\s|des\s|,)/i;
|
|
2461
3119
|
var SIMPLE_FRACTION = /^(\d+)\s*\/\s*(\d+)/;
|
|
2462
3120
|
var TRAILING_S = /s$/;
|
|
2463
|
-
var VAGUE_ARTICLE = /^(?:un|
|
|
3121
|
+
var VAGUE_ARTICLE = /^(?:une|unas|unos|una|un|quelques|of\s+an?|an?)\s+/iu;
|
|
2464
3122
|
var WHITESPACE = /\s+/;
|
|
2465
3123
|
function leadingMeasure(direct, behind) {
|
|
2466
3124
|
if (direct.unit) {
|
|
@@ -2543,10 +3201,11 @@ function readable(text2) {
|
|
|
2543
3201
|
}
|
|
2544
3202
|
var APPROXIMATION_PREFIX = {
|
|
2545
3203
|
en: /^(?:~|\u2248|about|approx\.?|approximately|around|roughly)\s*/i,
|
|
2546
|
-
fr: /^(?:~|\u2248|environ|approximativement|\u00e0 peu pr\u00e8s|a peu pres)\s*/i
|
|
3204
|
+
fr: /^(?:~|\u2248|environ|approximativement|\u00e0 peu pr\u00e8s|a peu pres)\s*/i,
|
|
3205
|
+
es: /^(?:~|≈|unos|unas|aproximadamente|alrededor de|más o menos|mas o menos|como)\s*/i
|
|
2547
3206
|
};
|
|
2548
3207
|
var CONTAINER_CONTENTS = /^\s*(?:de\s|d'|du\s|des\s)/i;
|
|
2549
|
-
function statesItemSize(item) {
|
|
3208
|
+
function statesItemSize(item, language) {
|
|
2550
3209
|
const attached = ATTACHED_DE.exec(item);
|
|
2551
3210
|
if (!attached) {
|
|
2552
3211
|
return false;
|
|
@@ -2555,14 +3214,14 @@ function statesItemSize(item) {
|
|
|
2555
3214
|
if (!named || DIGIT.test(named)) {
|
|
2556
3215
|
return false;
|
|
2557
3216
|
}
|
|
2558
|
-
return isStatedSize(item.slice(attached.index + attached[0].length));
|
|
3217
|
+
return isStatedSize(item.slice(attached.index + attached[0].length), language);
|
|
2559
3218
|
}
|
|
2560
|
-
function isStatedSize(text2) {
|
|
2561
|
-
const size = parseLeadingQuantity(text2,
|
|
3219
|
+
function isStatedSize(text2, language) {
|
|
3220
|
+
const size = parseLeadingQuantity(text2, language);
|
|
2562
3221
|
if (!size) {
|
|
2563
3222
|
return false;
|
|
2564
3223
|
}
|
|
2565
|
-
const measure = takeUnit(text2.slice(size.length).trimStart(),
|
|
3224
|
+
const measure = takeUnit(text2.slice(size.length).trimStart(), language);
|
|
2566
3225
|
if (measure.unit?.kind !== "measured") {
|
|
2567
3226
|
return false;
|
|
2568
3227
|
}
|
|
@@ -2613,12 +3272,20 @@ function namesContainer(word) {
|
|
|
2613
3272
|
}
|
|
2614
3273
|
var TIME_UNIT = {
|
|
2615
3274
|
en: /^(?:h|hr|hrs|hours?|mins?|minutes?|secs?|seconds?|days?|nights?|weeks?)\b/i,
|
|
2616
|
-
fr: /^(?:h|mn|heures?|mins?|minutes?|secs?|secondes?|jours?|nuits?|semaines?)\b/i
|
|
3275
|
+
fr: /^(?:h|mn|heures?|mins?|minutes?|secs?|secondes?|jours?|nuits?|semaines?)\b/i,
|
|
3276
|
+
es: /^(?:h|hs|horas?|mins?|minutos?|segs?|segundos?|días?|dias?|noches?|semanas?)\b/i
|
|
2617
3277
|
};
|
|
3278
|
+
function statesAnAmountPerEater(text2) {
|
|
3279
|
+
return Object.values(PER_PERSON).some((pattern) => pattern.test(text2));
|
|
3280
|
+
}
|
|
3281
|
+
function namesALengthOfTime(text2) {
|
|
3282
|
+
return Object.values(TIME_UNIT).some((pattern) => pattern.test(text2));
|
|
3283
|
+
}
|
|
2618
3284
|
var ORDINAL_SUFFIX = /^(?:ers?|[eè]res?|[eè]mes?|es?|st|nd|rd|th)\b/i;
|
|
2619
3285
|
var PER_PERSON = {
|
|
2620
3286
|
en: /\bper\s+(?:person|head|serving|guest|diner)\b/i,
|
|
2621
|
-
fr: /\bpar\s+(?:personne|convive|part|t\u00eate)\b/i
|
|
3287
|
+
fr: /\bpar\s+(?:personne|convive|part|t\u00eate)\b/i,
|
|
3288
|
+
es: /\bpor\s+(?:persona|comensal|ración|racion)\b/i
|
|
2622
3289
|
};
|
|
2623
3290
|
var MEASURE_ADJECTIVES = {
|
|
2624
3291
|
en: /* @__PURE__ */ new Set([
|
|
@@ -2644,6 +3311,24 @@ var MEASURE_ADJECTIVES = {
|
|
|
2644
3311
|
"grosse",
|
|
2645
3312
|
"petit",
|
|
2646
3313
|
"petite"
|
|
3314
|
+
]),
|
|
3315
|
+
es: /* @__PURE__ */ new Set([
|
|
3316
|
+
"buen",
|
|
3317
|
+
"buena",
|
|
3318
|
+
"colmada",
|
|
3319
|
+
"colmado",
|
|
3320
|
+
"escasa",
|
|
3321
|
+
"escaso",
|
|
3322
|
+
"generosa",
|
|
3323
|
+
"generoso",
|
|
3324
|
+
"gran",
|
|
3325
|
+
"grande",
|
|
3326
|
+
"mediana",
|
|
3327
|
+
"mediano",
|
|
3328
|
+
"peque\xF1a",
|
|
3329
|
+
"peque\xF1o",
|
|
3330
|
+
"rasa",
|
|
3331
|
+
"raso"
|
|
2647
3332
|
])
|
|
2648
3333
|
};
|
|
2649
3334
|
function takeMeasureAdjective(text2, language) {
|
|
@@ -2692,25 +3377,33 @@ function parseLeadingQuantity(text2, language) {
|
|
|
2692
3377
|
if (glyph && glyph in VULGAR_FRACTIONS) {
|
|
2693
3378
|
return { amount: VULGAR_FRACTIONS[glyph] ?? Number.NaN, length: offset + 1 };
|
|
2694
3379
|
}
|
|
2695
|
-
const decimal = (language
|
|
3380
|
+
const decimal = (writesDecimalComma(language) ? DECIMAL : GROUPED_DECIMAL).exec(trimmed);
|
|
2696
3381
|
if (decimal) {
|
|
2697
|
-
const [
|
|
3382
|
+
const [written2 = ""] = decimal.slice(1);
|
|
2698
3383
|
const amount = Number(
|
|
2699
|
-
language
|
|
3384
|
+
writesDecimalComma(language) ? written2.replace(",", ".") : written2.replace(/,/g, "")
|
|
2700
3385
|
);
|
|
2701
3386
|
if (Number.isFinite(amount)) {
|
|
2702
3387
|
return { amount, length: offset + decimal[0].length };
|
|
2703
3388
|
}
|
|
2704
3389
|
}
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
return { amount: written.amount, length: offset + written.length };
|
|
2709
|
-
}
|
|
3390
|
+
const written = language === "es" ? parseSpanishFraction(trimmed) : parseWrittenFraction(trimmed, language);
|
|
3391
|
+
if (written) {
|
|
3392
|
+
return { amount: written.amount, length: offset + written.length };
|
|
2710
3393
|
}
|
|
2711
3394
|
return null;
|
|
2712
3395
|
}
|
|
2713
|
-
var WRITTEN_NUMERATORS = {
|
|
3396
|
+
var WRITTEN_NUMERATORS = {
|
|
3397
|
+
a: 1,
|
|
3398
|
+
an: 1,
|
|
3399
|
+
one: 1,
|
|
3400
|
+
two: 2,
|
|
3401
|
+
three: 3,
|
|
3402
|
+
un: 1,
|
|
3403
|
+
una: 1,
|
|
3404
|
+
dos: 2,
|
|
3405
|
+
tres: 3
|
|
3406
|
+
};
|
|
2714
3407
|
var WRITTEN_DENOMINATORS = {
|
|
2715
3408
|
half: 2,
|
|
2716
3409
|
halves: 2,
|
|
@@ -2719,9 +3412,18 @@ var WRITTEN_DENOMINATORS = {
|
|
|
2719
3412
|
quarter: 4,
|
|
2720
3413
|
quarters: 4,
|
|
2721
3414
|
fourth: 4,
|
|
2722
|
-
fourths: 4
|
|
3415
|
+
fourths: 4,
|
|
3416
|
+
cuarto: 4,
|
|
3417
|
+
cuartos: 4,
|
|
3418
|
+
tercio: 3,
|
|
3419
|
+
tercios: 3,
|
|
3420
|
+
mitad: 2,
|
|
3421
|
+
mitades: 2
|
|
2723
3422
|
};
|
|
2724
|
-
function parseWrittenFraction(text2) {
|
|
3423
|
+
function parseWrittenFraction(text2, language) {
|
|
3424
|
+
if (language !== "en") {
|
|
3425
|
+
return null;
|
|
3426
|
+
}
|
|
2725
3427
|
const match = FRACTION_WORD.exec(text2);
|
|
2726
3428
|
if (!match) {
|
|
2727
3429
|
return null;
|
|
@@ -2737,13 +3439,29 @@ function parseWrittenFraction(text2) {
|
|
|
2737
3439
|
}
|
|
2738
3440
|
return { amount: numerator / denominator, length: match[0].length };
|
|
2739
3441
|
}
|
|
3442
|
+
function parseSpanishFraction(text2) {
|
|
3443
|
+
const match = SPANISH_FRACTION_WORD.exec(text2);
|
|
3444
|
+
if (!match) {
|
|
3445
|
+
return null;
|
|
3446
|
+
}
|
|
3447
|
+
const numerator = match[1] ? WRITTEN_NUMERATORS[match[1].toLowerCase()] : 1;
|
|
3448
|
+
const denominator = match[2] ? WRITTEN_DENOMINATORS[match[2].toLowerCase()] : 2;
|
|
3449
|
+
if (!(numerator && denominator)) {
|
|
3450
|
+
return null;
|
|
3451
|
+
}
|
|
3452
|
+
const rest = text2.slice(match[0].length).replace(LEADING_PARTITIVE, "").trimStart();
|
|
3453
|
+
if (rest === "") {
|
|
3454
|
+
return null;
|
|
3455
|
+
}
|
|
3456
|
+
return { amount: numerator / denominator, length: match[0].length };
|
|
3457
|
+
}
|
|
2740
3458
|
function parseLeadingRange(text2, language) {
|
|
2741
3459
|
const low = parseLeadingQuantity(text2, language);
|
|
2742
3460
|
if (!low) {
|
|
2743
3461
|
return null;
|
|
2744
3462
|
}
|
|
2745
3463
|
const after = text2.slice(low.length);
|
|
2746
|
-
const written = language
|
|
3464
|
+
const written = RANGE_JOINER[language];
|
|
2747
3465
|
const separator = written.exec(after) ?? LEADING_DASH.exec(after);
|
|
2748
3466
|
if (!separator) {
|
|
2749
3467
|
return null;
|
|
@@ -2759,7 +3477,7 @@ function parseLeadingRange(text2, language) {
|
|
|
2759
3477
|
length: low.length + separator[0].length + high.length
|
|
2760
3478
|
};
|
|
2761
3479
|
}
|
|
2762
|
-
var
|
|
3480
|
+
var ROMANCE_ARTICLES = { un: 1, une: 1, una: 1 };
|
|
2763
3481
|
function takeUnit(text2, language) {
|
|
2764
3482
|
const normalized = normalizeUnitKey(text2);
|
|
2765
3483
|
for (const key of unitKeys(language)) {
|
|
@@ -2795,35 +3513,57 @@ function afterKey(text2, key) {
|
|
|
2795
3513
|
}
|
|
2796
3514
|
return null;
|
|
2797
3515
|
}
|
|
2798
|
-
function
|
|
3516
|
+
function takeUnitAnyLanguage(text2, language) {
|
|
2799
3517
|
const first = takeUnit(text2, language);
|
|
2800
3518
|
if (first.unit) {
|
|
2801
3519
|
return first;
|
|
2802
3520
|
}
|
|
2803
|
-
|
|
3521
|
+
for (const other of OTHER_LANGUAGES[language]) {
|
|
3522
|
+
const taken = takeUnit(text2, other);
|
|
3523
|
+
if (taken.unit) {
|
|
3524
|
+
return taken;
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
3527
|
+
return first;
|
|
3528
|
+
}
|
|
3529
|
+
function detectLanguage(line) {
|
|
3530
|
+
return readEvidence(line).language;
|
|
2804
3531
|
}
|
|
2805
3532
|
function readEvidence(line) {
|
|
2806
3533
|
const text2 = line.trim();
|
|
2807
|
-
const afterFigures = text2.replace(
|
|
3534
|
+
const afterFigures = text2.replace(LEADING_QUANTITY2, "").trimStart();
|
|
2808
3535
|
const afterArticle = afterFigures.replace(VAGUE_ARTICLE, "");
|
|
2809
3536
|
const frenchUnit = takeUnit(afterArticle, "fr").unit !== null;
|
|
2810
3537
|
const englishUnit = takeUnit(afterArticle, "en").unit !== null;
|
|
2811
|
-
|
|
3538
|
+
const spanishUnit = takeUnit(afterArticle, "es").unit !== null;
|
|
3539
|
+
return readLanguage(text2, { frenchUnit, englishUnit, spanishUnit });
|
|
2812
3540
|
}
|
|
2813
3541
|
function readMeasure(rest, language, fromArticle) {
|
|
2814
3542
|
const direct = takeLeadingUnit(rest, language, fromArticle);
|
|
2815
|
-
|
|
3543
|
+
if (direct.unit) {
|
|
3544
|
+
const following = language === "es" ? takeMeasureAdjective(direct.rest, language) : null;
|
|
3545
|
+
if (following?.adjective) {
|
|
3546
|
+
return {
|
|
3547
|
+
leading: { ...direct, rest: following.rest },
|
|
3548
|
+
described: { adjective: following.adjective, rest: direct.rest },
|
|
3549
|
+
behind: direct,
|
|
3550
|
+
follows: true
|
|
3551
|
+
};
|
|
3552
|
+
}
|
|
3553
|
+
return { leading: direct, described: { adjective: null, rest }, behind: null, follows: false };
|
|
3554
|
+
}
|
|
3555
|
+
const described = takeMeasureAdjective(rest, language);
|
|
2816
3556
|
const behind = described.adjective ? takeLeadingUnit(described.rest, language, fromArticle) : null;
|
|
2817
|
-
return { leading: leadingMeasure(direct, behind), described, behind };
|
|
3557
|
+
return { leading: leadingMeasure(direct, behind), described, behind, follows: false };
|
|
2818
3558
|
}
|
|
2819
|
-
function whatTheFigureIsNot(behindFigure, couldBeARank
|
|
3559
|
+
function whatTheFigureIsNot(behindFigure, couldBeARank) {
|
|
2820
3560
|
if (HYPHENATED_TAIL.test(behindFigure)) {
|
|
2821
3561
|
return "sizeQualifier";
|
|
2822
3562
|
}
|
|
2823
3563
|
if (couldBeARank && ORDINAL_SUFFIX.test(behindFigure)) {
|
|
2824
3564
|
return null;
|
|
2825
3565
|
}
|
|
2826
|
-
if (
|
|
3566
|
+
if (namesALengthOfTime(behindFigure.trimStart())) {
|
|
2827
3567
|
return "duration";
|
|
2828
3568
|
}
|
|
2829
3569
|
return false;
|
|
@@ -2839,19 +3579,20 @@ function takeEquivalents(text2, language) {
|
|
|
2839
3579
|
if (slashed) {
|
|
2840
3580
|
rest = slashed.rest;
|
|
2841
3581
|
}
|
|
2842
|
-
|
|
3582
|
+
const lead = stripItemLead(rest, language);
|
|
3583
|
+
let item = lead.item;
|
|
2843
3584
|
const trailing = bracketed.measures.length === 0 && !slashed ? takeTrailingAlternates(item, language) : null;
|
|
2844
3585
|
if (trailing) {
|
|
2845
3586
|
item = trailing.item;
|
|
2846
3587
|
}
|
|
2847
3588
|
const group = trailing ?? (bracketed.measures.length > 0 ? bracketed : null);
|
|
2848
|
-
return { rest, item, bracketed, slashed, trailing, group };
|
|
3589
|
+
return { rest, item, partitive: lead.partitive, bracketed, slashed, trailing, group };
|
|
2849
3590
|
}
|
|
2850
3591
|
function whatTheFiguresSize(leading, item, language) {
|
|
2851
|
-
if (language
|
|
3592
|
+
if (usesPartitiveDe(language) && !leading.unit && statesItemSize(item, language)) {
|
|
2852
3593
|
return "itemSize";
|
|
2853
3594
|
}
|
|
2854
|
-
if (language
|
|
3595
|
+
if (usesPartitiveDe(language) && leading.partitive && isStatedSize(item, language)) {
|
|
2855
3596
|
return "itemSize";
|
|
2856
3597
|
}
|
|
2857
3598
|
if (leading.unit?.kind === "measured" && CONTAINER_NOUN.test(normalizeUnitKey(item))) {
|
|
@@ -2862,7 +3603,7 @@ function whatTheFiguresSize(leading, item, language) {
|
|
|
2862
3603
|
function whatTheCountIsOf(behindFigure, language) {
|
|
2863
3604
|
let rest = behindFigure.trimStart();
|
|
2864
3605
|
if (language === "en") {
|
|
2865
|
-
rest = rest.replace(
|
|
3606
|
+
rest = rest.replace(LEADING_ARTICLE2, "");
|
|
2866
3607
|
}
|
|
2867
3608
|
const multiplier = readCountMultiplier(rest);
|
|
2868
3609
|
if (multiplier) {
|
|
@@ -2893,6 +3634,8 @@ function parseIngredient(line, choice = "auto") {
|
|
|
2893
3634
|
decoration,
|
|
2894
3635
|
approximation: null,
|
|
2895
3636
|
measureAdjective: null,
|
|
3637
|
+
measureAdjectiveFollows: false,
|
|
3638
|
+
itemPartitive: false,
|
|
2896
3639
|
amount: null,
|
|
2897
3640
|
amountMax: null,
|
|
2898
3641
|
rangeSeparator: null,
|
|
@@ -2916,11 +3659,7 @@ function parseIngredient(line, choice = "auto") {
|
|
|
2916
3659
|
if (!quantity) {
|
|
2917
3660
|
return empty(null);
|
|
2918
3661
|
}
|
|
2919
|
-
const notAnAmount = whatTheFigureIsNot(
|
|
2920
|
-
stated.slice(quantity.length),
|
|
2921
|
-
quantity !== article,
|
|
2922
|
-
language
|
|
2923
|
-
);
|
|
3662
|
+
const notAnAmount = whatTheFigureIsNot(stated.slice(quantity.length), quantity !== article);
|
|
2924
3663
|
if (notAnAmount !== false) {
|
|
2925
3664
|
return empty(notAnAmount);
|
|
2926
3665
|
}
|
|
@@ -2950,10 +3689,12 @@ function parseIngredient(line, choice = "auto") {
|
|
|
2950
3689
|
return {
|
|
2951
3690
|
original,
|
|
2952
3691
|
language,
|
|
2953
|
-
heldBack:
|
|
3692
|
+
heldBack: statesAnAmountPerEater(text2) ? "perPerson" : null,
|
|
2954
3693
|
decoration,
|
|
2955
3694
|
approximation: loose ? loose[0] : null,
|
|
2956
3695
|
measureAdjective: behind?.unit ? described.adjective : null,
|
|
3696
|
+
measureAdjectiveFollows: measure.follows,
|
|
3697
|
+
itemPartitive: equivalents.partitive,
|
|
2957
3698
|
amount: whole * times,
|
|
2958
3699
|
amountMax: range === null ? null : range.max * times,
|
|
2959
3700
|
rangeSeparator: range?.separator ?? null,
|
|
@@ -2969,7 +3710,9 @@ function parseIngredient(line, choice = "auto") {
|
|
|
2969
3710
|
}
|
|
2970
3711
|
function unreadableComma(text2, language, evidence) {
|
|
2971
3712
|
if (COMMA_GROUPED.test(text2)) {
|
|
2972
|
-
const
|
|
3713
|
+
const scores = [evidence.french, evidence.english, evidence.spanish];
|
|
3714
|
+
const top = Math.max(...scores);
|
|
3715
|
+
const settled = scores.filter((score) => score === top).length === 1;
|
|
2973
3716
|
return !settled;
|
|
2974
3717
|
}
|
|
2975
3718
|
if (language === "en") {
|
|
@@ -2979,8 +3722,8 @@ function unreadableComma(text2, language, evidence) {
|
|
|
2979
3722
|
}
|
|
2980
3723
|
function readArticle(text2, language) {
|
|
2981
3724
|
const counts = (rest) => takeLeadingUnit(rest, language, true).unit !== null || readCountMultiplier(rest) !== null;
|
|
2982
|
-
if (language
|
|
2983
|
-
const match =
|
|
3725
|
+
if (usesPartitiveDe(language)) {
|
|
3726
|
+
const match = OPENING_ARTICLE_ROMANCE[language].exec(text2);
|
|
2984
3727
|
if (!match) {
|
|
2985
3728
|
return null;
|
|
2986
3729
|
}
|
|
@@ -2988,7 +3731,7 @@ function readArticle(text2, language) {
|
|
|
2988
3731
|
return null;
|
|
2989
3732
|
}
|
|
2990
3733
|
const [word = ""] = match.slice(1);
|
|
2991
|
-
const amount =
|
|
3734
|
+
const amount = ROMANCE_ARTICLES[word.toLowerCase()];
|
|
2992
3735
|
if (amount === void 0) {
|
|
2993
3736
|
return null;
|
|
2994
3737
|
}
|
|
@@ -3026,8 +3769,8 @@ function takeLeadingUnit(text2, language, fromArticle) {
|
|
|
3026
3769
|
if (taken.unit) {
|
|
3027
3770
|
return { ...taken, partitive: false };
|
|
3028
3771
|
}
|
|
3029
|
-
if (language
|
|
3030
|
-
const measure = readPartitiveMeasure(text2);
|
|
3772
|
+
if (usesPartitiveDe(language) && fromArticle) {
|
|
3773
|
+
const measure = readPartitiveMeasure(text2, language);
|
|
3031
3774
|
if (measure) {
|
|
3032
3775
|
return { unit: measure.unit, rest: measure.rest, partitive: true };
|
|
3033
3776
|
}
|
|
@@ -3035,10 +3778,17 @@ function takeLeadingUnit(text2, language, fromArticle) {
|
|
|
3035
3778
|
return { ...taken, partitive: false };
|
|
3036
3779
|
}
|
|
3037
3780
|
function stripItemLead(text2, language) {
|
|
3038
|
-
if (language
|
|
3039
|
-
|
|
3781
|
+
if (usesPartitiveDe(language)) {
|
|
3782
|
+
const partitive = LEADING_PARTITIVE.test(text2);
|
|
3783
|
+
return {
|
|
3784
|
+
item: text2.replace(LEADING_PARTITIVE, "").replace(ROMANCE_ARTICLE[language], "").trim(),
|
|
3785
|
+
partitive
|
|
3786
|
+
};
|
|
3040
3787
|
}
|
|
3041
|
-
return
|
|
3788
|
+
return {
|
|
3789
|
+
item: text2.replace(OF_OPENING, "").replace(LEADING_INDEFINITE_WITH_SPACE, "").trim(),
|
|
3790
|
+
partitive: false
|
|
3791
|
+
};
|
|
3042
3792
|
}
|
|
3043
3793
|
function takeAlternates(text2, language) {
|
|
3044
3794
|
const none = { measures: [], intro: null, published: "", rest: text2 };
|
|
@@ -3058,7 +3808,7 @@ function takeAlternates(text2, language) {
|
|
|
3058
3808
|
var EQUIVALENT_INTRODUCER = /^(?:soit|environ|about|approx\.?|approximately|or|ou|=)\s*/i;
|
|
3059
3809
|
var MEASURE_PARTITIVE = /^(?:de\s+la\s+|de\s+l'|d'|de\s+|du\s+|des\s+|of\s+)/i;
|
|
3060
3810
|
function takeMeasureAfterQuantity(text2, language) {
|
|
3061
|
-
const direct =
|
|
3811
|
+
const direct = takeUnitAnyLanguage(text2, language);
|
|
3062
3812
|
if (direct.unit) {
|
|
3063
3813
|
return direct;
|
|
3064
3814
|
}
|
|
@@ -3066,7 +3816,7 @@ function takeMeasureAfterQuantity(text2, language) {
|
|
|
3066
3816
|
if (!partitive) {
|
|
3067
3817
|
return direct;
|
|
3068
3818
|
}
|
|
3069
|
-
return
|
|
3819
|
+
return takeUnitAnyLanguage(text2.slice(partitive[0].length), language);
|
|
3070
3820
|
}
|
|
3071
3821
|
function readBracket(bracket, language) {
|
|
3072
3822
|
const inside = bracket.slice(1, -1);
|
|
@@ -3121,7 +3871,7 @@ function takeSlashAlternates(text2, language) {
|
|
|
3121
3871
|
if (!quantity) {
|
|
3122
3872
|
break;
|
|
3123
3873
|
}
|
|
3124
|
-
const taken =
|
|
3874
|
+
const taken = takeUnitAnyLanguage(after.slice(quantity.length).trimStart(), language);
|
|
3125
3875
|
if (!taken.unit) {
|
|
3126
3876
|
break;
|
|
3127
3877
|
}
|
|
@@ -3139,7 +3889,7 @@ function formatAmount(amount, language, options = {}) {
|
|
|
3139
3889
|
const decimal = (value) => {
|
|
3140
3890
|
const rounded = value !== 0 && Math.abs(value) < 0.01 ? Number(value.toPrecision(2)) : Math.round(value * 100) / 100;
|
|
3141
3891
|
const rendered = String(rounded);
|
|
3142
|
-
return language
|
|
3892
|
+
return writesDecimalComma(language) ? rendered.replace(".", ",") : rendered;
|
|
3143
3893
|
};
|
|
3144
3894
|
if (!Number.isFinite(amount)) {
|
|
3145
3895
|
return "";
|
|
@@ -3182,6 +3932,7 @@ var GARLIC = /\bgarlic\b/i;
|
|
|
3182
3932
|
var IES_ENDING = /ies$/i;
|
|
3183
3933
|
var LETTER = new RegExp("\\p{L}", "u");
|
|
3184
3934
|
var LETTERS_ONLY = /^[A-Za-z]+$/;
|
|
3935
|
+
var WORD_LETTERS = new RegExp("^\\p{L}+$", "u");
|
|
3185
3936
|
var OF_JOINER = / of /i;
|
|
3186
3937
|
var PLURAL_ENDING = /s$|eaux$|aux$/i;
|
|
3187
3938
|
var SIBILANT_ENDING = /(?:ch|sh|s|x|z)$/i;
|
|
@@ -3190,6 +3941,19 @@ var SIBILANT_PLURAL = /(?:ch|sh|s|x|z)es$/i;
|
|
|
3190
3941
|
var TRAILING_S_ANY_CASE2 = /s$/i;
|
|
3191
3942
|
var VOWEL_OPENING = /^[aeiouàâäéèêëîïôöûü]/i;
|
|
3192
3943
|
var X_ENDING = /x$/i;
|
|
3944
|
+
function equipmentIngredient(line, language = "auto") {
|
|
3945
|
+
return {
|
|
3946
|
+
text: line.trim(),
|
|
3947
|
+
original: line,
|
|
3948
|
+
scaling: "unscaled",
|
|
3949
|
+
amount: null,
|
|
3950
|
+
amountMax: null,
|
|
3951
|
+
unit: null,
|
|
3952
|
+
language: language === "auto" ? detectLanguage(line) : language,
|
|
3953
|
+
isEquipment: true,
|
|
3954
|
+
note: EQUIPMENT_NOTE
|
|
3955
|
+
};
|
|
3956
|
+
}
|
|
3193
3957
|
function roundTo(value, step) {
|
|
3194
3958
|
return Math.round(Math.round(value / step) * step * 100) / 100;
|
|
3195
3959
|
}
|
|
@@ -3352,11 +4116,11 @@ var PORTION_SIZED_ITEM = new RegExp(
|
|
|
3352
4116
|
"iu"
|
|
3353
4117
|
);
|
|
3354
4118
|
var QUARTERED_ITEM = new RegExp(
|
|
3355
|
-
"\\b(?:onions?|shallots?|potatoes|potato|carrots?|apples?|pears?|lemons?|limes?|oranges?|tomato(?:es)?|cucumbers?|courgettes?|zucchinis?|aubergines?|eggplants?|squash(?:es)?|pumpkins?|cabbages?|melons?|watermelons?|peppers?|beets?|turnips?|parsnips?|leeks?|bananas?|mango(?:e?s)?|avocados?|legs? of lamb|lamb legs?|baguettes?|camemberts?|cheeses?|chorizos?|pineapples?|peach(?:es)?|apricots?|milk|chickens?|guinea fowls?|roasts?|oignons?|\xE9chalotes?|echalotes?|pommes? de terre|carottes?|pommes?|poires?|citrons?|tomates?|concombres?|courgettes?|aubergines?|potirons?|choux?|chou|melons?|poivrons?|betteraves?|navets?|panais|poireaux?|bananes?|mangues?|avocats?|past\xE8ques?|pasteques?|gigots?|fromages?|ch\xE8vres?|chevres?|ananas|p\xEAches?|peches?|abricots?|laits?|poulets?|pintades?|reblochons?|b\xFBches?|buches?|r\xF4tis?|rotis?)\\b",
|
|
4119
|
+
"\\b(?:onions?|shallots?|potatoes|potato|carrots?|apples?|pears?|lemons?|limes?|oranges?|tomato(?:es)?|cucumbers?|courgettes?|zucchinis?|aubergines?|eggplants?|squash(?:es)?|pumpkins?|cabbages?|melons?|watermelons?|peppers?|beets?|turnips?|parsnips?|leeks?|bananas?|mango(?:e?s)?|avocados?|legs? of lamb|lamb legs?|baguettes?|camemberts?|cheeses?|chorizos?|pineapples?|peach(?:es)?|apricots?|milk|chickens?|guinea fowls?|roasts?|oignons?|\xE9chalotes?|echalotes?|pommes? de terre|carottes?|pommes?|poires?|citrons?|tomates?|concombres?|courgettes?|aubergines?|potirons?|choux?|chou|melons?|poivrons?|betteraves?|navets?|panais|poireaux?|bananes?|mangues?|avocats?|past\xE8ques?|pasteques?|gigots?|fromages?|ch\xE8vres?|chevres?|ananas|p\xEAches?|peches?|abricots?|laits?|poulets?|pintades?|reblochons?|b\xFBches?|buches?|r\xF4tis?|rotis?|cebollas?|cebolletas?|chalotas?|patatas?|papas?|zanahorias?|manzanas?|peras?|limones|lim\xF3n|limon|limas?|naranjas?|tomates?|pepinos?|calabacines|calabac\xEDn|calabacin|berenjenas?|calabazas?|coles|col|melones|mel\xF3n|melon|sand\xEDas?|sandias?|pimientos?|remolachas?|nabos?|chiriv\xEDas?|chirivias?|puerros?|pl\xE1tanos?|platanos?|mangos?|aguacates?|pi\xF1as?|pinas?|melocotones|melocot\xF3n|melocoton|albaricoques?|duraznos?|quesos?|pollos?|asados?|barras? de pan)\\b",
|
|
3356
4120
|
"iu"
|
|
3357
4121
|
);
|
|
3358
4122
|
var HALVED_ITEM = /\b(?:jus|juices?)\b/iu;
|
|
3359
|
-
var WHOLE_ITEM = /\b(?:eggs?|yolks?|egg\s+whites?|oeufs?|œufs?|jaunes?\s+d['e]|clous?|zestes?|zests
|
|
4123
|
+
var WHOLE_ITEM = /\b(?:eggs?|yolks?|egg\s+whites?|oeufs?|œufs?|jaunes?\s+d['e]|clous?|zestes?|zests?|huevos?|yemas?\s+de|claras?\s+de|clavos?\s+de)\b/iu;
|
|
3360
4124
|
var HALVED_CUT = /\b(?:breasts?|thighs?|drumsticks?|wings?|cutlets?|cuisses?|ailes?|pilons?|escalopes?|magrets?)\b/iu;
|
|
3361
4125
|
function cloveDivisibility(unit, item) {
|
|
3362
4126
|
const counted = unit ? unit.canonical === "clove" : CLOVE.test(item);
|
|
@@ -3720,6 +4484,9 @@ function agreeLeadingAdjective(word, amount, language) {
|
|
|
3720
4484
|
if (language === "en") {
|
|
3721
4485
|
return word;
|
|
3722
4486
|
}
|
|
4487
|
+
if (language === "es") {
|
|
4488
|
+
return agreeSpanishAdjective(word, amount);
|
|
4489
|
+
}
|
|
3723
4490
|
const wantsPlural = amount >= 2;
|
|
3724
4491
|
const folded = foldWord2(word);
|
|
3725
4492
|
const isPlural = folded.endsWith("s") && !AGREEABLE_ADJECTIVES.has(folded);
|
|
@@ -3733,12 +4500,57 @@ function agreeLeadingAdjective(word, amount, language) {
|
|
|
3733
4500
|
return wantsPlural ? `${singular}s` : singular;
|
|
3734
4501
|
}
|
|
3735
4502
|
function agreeWithAmount(item, amount, language) {
|
|
3736
|
-
return language === "fr" ? agreeInFrench(item, amount) : agreeInEnglish(item, amount);
|
|
3737
|
-
}
|
|
3738
|
-
function agreeCountedContainer(item, amount, language) {
|
|
3739
4503
|
if (language === "fr") {
|
|
3740
4504
|
return agreeInFrench(item, amount);
|
|
3741
4505
|
}
|
|
4506
|
+
return language === "es" ? agreeInSpanish(item, amount) : agreeInEnglish(item, amount);
|
|
4507
|
+
}
|
|
4508
|
+
function agreeInSpanish(item, amount) {
|
|
4509
|
+
if (!item) {
|
|
4510
|
+
return item;
|
|
4511
|
+
}
|
|
4512
|
+
const words = item.split(" ");
|
|
4513
|
+
const head = words[0] ?? "";
|
|
4514
|
+
if (head.length <= 2 || !WORD_LETTERS.test(head)) {
|
|
4515
|
+
return item;
|
|
4516
|
+
}
|
|
4517
|
+
const wantsPlural = amount > 1;
|
|
4518
|
+
const singular = spanishSingular(head);
|
|
4519
|
+
const isPlural = head.toLowerCase() !== singular.toLowerCase();
|
|
4520
|
+
if (wantsPlural === isPlural) {
|
|
4521
|
+
return item;
|
|
4522
|
+
}
|
|
4523
|
+
words[0] = wantsPlural ? spanishPlural(head) : singular;
|
|
4524
|
+
for (let index = 1; index < words.length; index += 1) {
|
|
4525
|
+
const word = words[index] ?? "";
|
|
4526
|
+
if (!WORD_LETTERS.test(word) || SPANISH_PARTITIVE.test(word)) {
|
|
4527
|
+
break;
|
|
4528
|
+
}
|
|
4529
|
+
const adjectiveSingular = spanishSingular(word);
|
|
4530
|
+
const adjectiveIsPlural = word.toLowerCase() !== adjectiveSingular.toLowerCase();
|
|
4531
|
+
if (wantsPlural !== adjectiveIsPlural) {
|
|
4532
|
+
words[index] = wantsPlural ? spanishPlural(word) : adjectiveSingular;
|
|
4533
|
+
}
|
|
4534
|
+
}
|
|
4535
|
+
return words.join(" ");
|
|
4536
|
+
}
|
|
4537
|
+
var SPANISH_PARTITIVE = /^(?:de|del|con|sin|para|al|y|en|o)$/i;
|
|
4538
|
+
function agreeSpanishAdjective(word, amount) {
|
|
4539
|
+
if (!WORD_LETTERS.test(word)) {
|
|
4540
|
+
return word;
|
|
4541
|
+
}
|
|
4542
|
+
const wantsPlural = amount > 1;
|
|
4543
|
+
const singular = spanishSingular(word);
|
|
4544
|
+
const isPlural = word.toLowerCase() !== singular.toLowerCase();
|
|
4545
|
+
if (wantsPlural === isPlural) {
|
|
4546
|
+
return word;
|
|
4547
|
+
}
|
|
4548
|
+
return wantsPlural ? spanishPlural(word) : singular;
|
|
4549
|
+
}
|
|
4550
|
+
function agreeCountedContainer(item, amount, language) {
|
|
4551
|
+
if (language !== "en") {
|
|
4552
|
+
return agreeWithAmount(item, amount, language);
|
|
4553
|
+
}
|
|
3742
4554
|
const space = item.indexOf(" ");
|
|
3743
4555
|
if (space < 0) {
|
|
3744
4556
|
return agreeInEnglish(item, amount);
|
|
@@ -3746,19 +4558,23 @@ function agreeCountedContainer(item, amount, language) {
|
|
|
3746
4558
|
return `${agreeInEnglish(item.slice(0, space), amount)}${item.slice(space)}`;
|
|
3747
4559
|
}
|
|
3748
4560
|
var MUTE_H_WORDS = /^(?:huile|huiles|huitre|huitres|huître|huîtres|herbe|herbes|hysope)\b/i;
|
|
3749
|
-
function joinItem(item, language) {
|
|
4561
|
+
function joinItem(item, language, partitive) {
|
|
3750
4562
|
if (!item) {
|
|
3751
4563
|
return "";
|
|
3752
4564
|
}
|
|
3753
|
-
if (language === "en") {
|
|
4565
|
+
if (language === "en" || !partitive) {
|
|
3754
4566
|
return ` ${item}`;
|
|
3755
4567
|
}
|
|
4568
|
+
if (language === "es") {
|
|
4569
|
+
return ` de ${item}`;
|
|
4570
|
+
}
|
|
3756
4571
|
const elides = VOWEL_OPENING.test(item) || MUTE_H_WORDS.test(item);
|
|
3757
4572
|
return elides ? ` d'${item}` : ` de ${item}`;
|
|
3758
4573
|
}
|
|
3759
4574
|
var BRANCH_SEPARATORS = {
|
|
3760
4575
|
en: /\s+or\s+/gi,
|
|
3761
|
-
fr: /\s+ou\s+/gi
|
|
4576
|
+
fr: /\s+ou\s+/gi,
|
|
4577
|
+
es: /\s+o\s+/gi
|
|
3762
4578
|
};
|
|
3763
4579
|
function splitBranch(text2, parsed) {
|
|
3764
4580
|
const itemStart = parsed.item ? text2.indexOf(parsed.item) : text2.length;
|
|
@@ -3781,6 +4597,9 @@ function splitBranch(text2, parsed) {
|
|
|
3781
4597
|
}
|
|
3782
4598
|
function scaleIngredient(line, options) {
|
|
3783
4599
|
const { factor } = options;
|
|
4600
|
+
if (isEquipmentLine(line)) {
|
|
4601
|
+
return equipmentIngredient(line, options.language);
|
|
4602
|
+
}
|
|
3784
4603
|
if (factor === 1) {
|
|
3785
4604
|
return passthroughIngredient(line, options.language);
|
|
3786
4605
|
}
|
|
@@ -3832,9 +4651,9 @@ function alternateLabel(count2, style, texts, bracketed) {
|
|
|
3832
4651
|
}
|
|
3833
4652
|
return bracketed;
|
|
3834
4653
|
}
|
|
3835
|
-
function itemLabelFor(named, item, counted, language) {
|
|
4654
|
+
function itemLabelFor(named, item, counted, language, partitive) {
|
|
3836
4655
|
if (named) {
|
|
3837
|
-
return joinItem(item, language);
|
|
4656
|
+
return joinItem(item, language, partitive);
|
|
3838
4657
|
}
|
|
3839
4658
|
if (counted) {
|
|
3840
4659
|
return ` ${counted}`;
|
|
@@ -3917,7 +4736,8 @@ function noteForScaledLine(outcome) {
|
|
|
3917
4736
|
function renderScaledLine(parsed, alternates, unit, shown, language) {
|
|
3918
4737
|
const named = unit && !countsBarePieces(unit) ? unit : null;
|
|
3919
4738
|
const adjective = named && parsed.measureAdjective ? ` ${agreeLeadingAdjective(parsed.measureAdjective, shown, language)}` : "";
|
|
3920
|
-
const
|
|
4739
|
+
const measure = named ? ` ${formatUnit(named, shown, language)}` : "";
|
|
4740
|
+
const unitLabel = parsed.measureAdjectiveFollows ? `${measure}${adjective}` : `${adjective}${measure}`;
|
|
3921
4741
|
const alternateTexts = alternates.map((entry) => entry.text);
|
|
3922
4742
|
const introduced = parsed.alternateIntro ? `${parsed.alternateIntro} ` : "";
|
|
3923
4743
|
const bracketed = alternates.length === 0 ? "" : ` (${introduced}${alternateTexts.join(" / ")})`;
|
|
@@ -3930,7 +4750,7 @@ function renderScaledLine(parsed, alternates, unit, shown, language) {
|
|
|
3930
4750
|
const trailingLabel = parsed.alternateStyle === "trailing" ? bracketed : "";
|
|
3931
4751
|
const capacityLabel = parsed.capacity ? ` ${parsed.capacity}` : "";
|
|
3932
4752
|
const counted = parsed.capacity ? agreeCountedContainer(parsed.item, shown, language) : agreeWithAmount(parsed.item, shown, language);
|
|
3933
|
-
const itemLabel = itemLabelFor(named, parsed.item, counted, language);
|
|
4753
|
+
const itemLabel = itemLabelFor(named, parsed.item, counted, language, parsed.itemPartitive);
|
|
3934
4754
|
return { named, unitLabel, capacityLabel, altLabel, trailingLabel, itemLabel };
|
|
3935
4755
|
}
|
|
3936
4756
|
var HELD_BACK_NOTE = {
|
|
@@ -3954,6 +4774,7 @@ function scaleSingleLine(line, options) {
|
|
|
3954
4774
|
amountMax: null,
|
|
3955
4775
|
unit: null,
|
|
3956
4776
|
language,
|
|
4777
|
+
isEquipment: false,
|
|
3957
4778
|
note: parsed.heldBack ? HELD_BACK_NOTE[parsed.heldBack] : "No quantity given; adjust to taste."
|
|
3958
4779
|
};
|
|
3959
4780
|
}
|
|
@@ -3993,7 +4814,8 @@ function scaleSingleLine(line, options) {
|
|
|
3993
4814
|
amount: low.amount,
|
|
3994
4815
|
amountMax: collapsed ? null : high?.amount ?? null,
|
|
3995
4816
|
unit: named?.canonical ?? null,
|
|
3996
|
-
language
|
|
4817
|
+
language,
|
|
4818
|
+
isEquipment: false
|
|
3997
4819
|
};
|
|
3998
4820
|
const note = noteForScaledLine({
|
|
3999
4821
|
parsed,
|
|
@@ -4050,6 +4872,9 @@ function scaleIngredients(lines, options) {
|
|
|
4050
4872
|
return lines.map((line) => scaleIngredient(line, options));
|
|
4051
4873
|
}
|
|
4052
4874
|
function passthroughIngredient(line, language = "auto") {
|
|
4875
|
+
if (isEquipmentLine(line)) {
|
|
4876
|
+
return equipmentIngredient(line, language);
|
|
4877
|
+
}
|
|
4053
4878
|
const parsed = parseIngredient(line, language);
|
|
4054
4879
|
const held = parsed.amount === null || parsed.heldBack !== null;
|
|
4055
4880
|
const result = {
|
|
@@ -4059,7 +4884,8 @@ function passthroughIngredient(line, language = "auto") {
|
|
|
4059
4884
|
amount: held ? null : parsed.amount,
|
|
4060
4885
|
amountMax: held ? null : parsed.amountMax,
|
|
4061
4886
|
unit: held ? null : parsed.unit?.canonical ?? null,
|
|
4062
|
-
language: parsed.language
|
|
4887
|
+
language: parsed.language,
|
|
4888
|
+
isEquipment: false
|
|
4063
4889
|
};
|
|
4064
4890
|
if (parsed.heldBack) {
|
|
4065
4891
|
result.note = HELD_BACK_NOTE[parsed.heldBack];
|
|
@@ -4112,6 +4938,9 @@ var reportSchema = z.object({
|
|
|
4112
4938
|
preferred_by_name: z.boolean().describe(
|
|
4113
4939
|
"Whether this source's rows were arranged so the ones whose title names the dish come first. An order over one source's own rows, never a score against another source."
|
|
4114
4940
|
),
|
|
4941
|
+
names_the_dish: z.number().int().describe(
|
|
4942
|
+
"How many of this source's rows in this answer carry the dish in their title, out of 'count'. A row is what the source returned for the words it was handed, and one of these indexes answers any wording with something, so zero means the source offered rows and none of them is the dish."
|
|
4943
|
+
),
|
|
4115
4944
|
wordings: z.array(
|
|
4116
4945
|
z.object({
|
|
4117
4946
|
query: z.string().describe("What was sent to this source, or would have been."),
|
|
@@ -4141,12 +4970,15 @@ var ingredientSchema = z.object({
|
|
|
4141
4970
|
text: z.string().describe("The line as it now reads."),
|
|
4142
4971
|
original: z.string().describe("The line as published."),
|
|
4143
4972
|
scaling: z.enum(["scaled", "rounded", "unscaled"]).describe(
|
|
4144
|
-
"'scaled' means the arithmetic landed on the exact product, 'rounded' that something had to move for the line to stay usable or that the line carries a second quantity whose match is the page's claim, 'unscaled' that
|
|
4973
|
+
"'scaled' means the arithmetic landed on the exact product, 'rounded' that something had to move for the line to stay usable or that the line carries a second quantity whose match is the page's claim, 'unscaled' that the line was left as given. Read 'is_equipment' beside it: a tool line is 'unscaled' whether or not it carries a figure."
|
|
4145
4974
|
),
|
|
4146
4975
|
amount: z.number().nullable().describe("The scaled quantity, in 'unit'. Read the two together, never the number alone."),
|
|
4147
4976
|
amount_max: z.number().nullable().describe("Upper bound when the line gives a range."),
|
|
4148
4977
|
unit: z.string().nullable().describe("The unit 'amount' is in, which may differ from the page's."),
|
|
4149
|
-
language: z.enum(["fr", "en"]).describe("The language the line was read and rewritten in."),
|
|
4978
|
+
language: z.enum(["fr", "en", "es"]).describe("The language the line was read and rewritten in."),
|
|
4979
|
+
is_equipment: z.boolean().describe(
|
|
4980
|
+
"True for a line naming a tool, which some sites write among the ingredients. Such a line is never multiplied: a recipe made for more people uses the same pan."
|
|
4981
|
+
),
|
|
4150
4982
|
note: z.string().optional().describe("What happened to this line, when anything did.")
|
|
4151
4983
|
});
|
|
4152
4984
|
function toIngredientPayload(entry) {
|
|
@@ -4158,6 +4990,7 @@ function toIngredientPayload(entry) {
|
|
|
4158
4990
|
amount_max: entry.amountMax,
|
|
4159
4991
|
unit: entry.unit,
|
|
4160
4992
|
language: entry.language,
|
|
4993
|
+
is_equipment: entry.isEquipment,
|
|
4161
4994
|
...entry.note ? { note: entry.note } : {}
|
|
4162
4995
|
};
|
|
4163
4996
|
}
|
|
@@ -4186,6 +5019,7 @@ function toReportPayload(report) {
|
|
|
4186
5019
|
skipped: report.skipped,
|
|
4187
5020
|
cached: report.cached,
|
|
4188
5021
|
preferred_by_name: report.preferredByName,
|
|
5022
|
+
names_the_dish: report.namesTheDish,
|
|
4189
5023
|
wordings: report.wordings.map((attempt) => ({
|
|
4190
5024
|
query: attempt.query,
|
|
4191
5025
|
derivation: attempt.derivation,
|
|
@@ -4221,6 +5055,47 @@ function labelNote(label, note) {
|
|
|
4221
5055
|
const said = note.text;
|
|
4222
5056
|
return mustKeep(`${label}: ${said}`);
|
|
4223
5057
|
}
|
|
5058
|
+
function answeredNotes(report) {
|
|
5059
|
+
const notes = [];
|
|
5060
|
+
const sent = report.wordings.filter((attempt) => attempt.ran && attempt.error === null);
|
|
5061
|
+
if (report.count === 0) {
|
|
5062
|
+
notes.push(nothingOfferedNote(report, sent));
|
|
5063
|
+
}
|
|
5064
|
+
if (sent.length > 1) {
|
|
5065
|
+
notes.push(
|
|
5066
|
+
`${report.name} was asked ${sent.length} wordings and these rows are their union: ${sent.map((attempt) => `"${quoteForeign(attempt.query)}" (${attempt.count ?? 0})`).join(", ")}. Those counts are per wording and are never added.`
|
|
5067
|
+
);
|
|
5068
|
+
}
|
|
5069
|
+
if (report.skipped > 0) {
|
|
5070
|
+
notes.push(
|
|
5071
|
+
mustKeep(
|
|
5072
|
+
`${report.name} sent ${report.skipped} row(s) in a shape this server could not read, and they were left out.`
|
|
5073
|
+
)
|
|
5074
|
+
);
|
|
5075
|
+
}
|
|
5076
|
+
if (report.reportedTotal !== null && report.reportedTotalMeans !== null) {
|
|
5077
|
+
const forWording = sent.length > 1 ? ` That number belongs to "${quoteForeign(sent[0]?.query ?? "")}" alone.` : "";
|
|
5078
|
+
notes.push(
|
|
5079
|
+
`${report.name} reported ${report.reportedTotal}: ${report.reportedTotalMeans}.${forWording}`
|
|
5080
|
+
);
|
|
5081
|
+
} else {
|
|
5082
|
+
notes.push(
|
|
5083
|
+
`${report.name} publishes no count of what a search matched, so a short list here is not evidence that little exists.`
|
|
5084
|
+
);
|
|
5085
|
+
}
|
|
5086
|
+
return notes;
|
|
5087
|
+
}
|
|
5088
|
+
function listNames(names2) {
|
|
5089
|
+
if (names2.length <= 1) {
|
|
5090
|
+
return names2.join("");
|
|
5091
|
+
}
|
|
5092
|
+
return `${names2.slice(0, -1).join(", ")} and ${names2.at(-1)}`;
|
|
5093
|
+
}
|
|
5094
|
+
function nothingOfferedNote(report, sent) {
|
|
5095
|
+
return mustKeep(
|
|
5096
|
+
sent.length > 1 ? `${report.name} answered and offered no row for any of the ${sent.length} wordings it was sent (${sent.map((attempt) => `"${quoteForeign(attempt.query)}"`).join(", ")}). Each of those is a statement about a wording; together they are the closest this server comes to saying the corpus holds nothing. Try a main ingredient, or the dish's name in the language that source publishes in.` : `${report.name} answered and offered no row for this wording. That is a statement about the wording as much as about the corpus: try the dish's name in another language, or a main ingredient.`
|
|
5097
|
+
);
|
|
5098
|
+
}
|
|
4224
5099
|
function reportNotes(reports) {
|
|
4225
5100
|
const notes = [];
|
|
4226
5101
|
const answered = reports.filter((report) => report.status === "answered");
|
|
@@ -4234,51 +5109,34 @@ function reportNotes(reports) {
|
|
|
4234
5109
|
);
|
|
4235
5110
|
}
|
|
4236
5111
|
for (const report of answered) {
|
|
4237
|
-
|
|
4238
|
-
if (report.count === 0) {
|
|
4239
|
-
notes.push(
|
|
4240
|
-
mustKeep(
|
|
4241
|
-
sent.length > 1 ? `${report.name} answered and offered no row for any of the ${sent.length} wordings it was sent (${sent.map((attempt) => `"${attempt.query}"`).join(", ")}). Each of those is a statement about a wording; together they are the closest this server comes to saying the corpus holds nothing. Try a main ingredient, or the dish's name in the language that source publishes in.` : `${report.name} answered and offered no row for this wording. That is a statement about the wording as much as about the corpus: try the dish's name in another language, or a main ingredient.`
|
|
4242
|
-
)
|
|
4243
|
-
);
|
|
4244
|
-
}
|
|
4245
|
-
if (sent.length > 1) {
|
|
4246
|
-
notes.push(
|
|
4247
|
-
`${report.name} was asked ${sent.length} wordings and these rows are their union: ${sent.map((attempt) => `"${attempt.query}" (${attempt.count ?? 0})`).join(", ")}. Those counts are per wording and are never added.`
|
|
4248
|
-
);
|
|
4249
|
-
}
|
|
4250
|
-
if (report.preferredByName) {
|
|
4251
|
-
notes.push(
|
|
4252
|
-
`${report.name}'s rows are arranged with the ones naming the dish first, so a wording that returned near-misses cannot crowd out what another wording found. That is an order over one source's own rows and not a score against any other source.`
|
|
4253
|
-
);
|
|
4254
|
-
}
|
|
4255
|
-
if (report.skipped > 0) {
|
|
4256
|
-
notes.push(
|
|
4257
|
-
mustKeep(
|
|
4258
|
-
`${report.name} sent ${report.skipped} row(s) in a shape this server could not read, and they were left out.`
|
|
4259
|
-
)
|
|
4260
|
-
);
|
|
4261
|
-
}
|
|
4262
|
-
if (report.reportedTotal !== null && report.reportedTotalMeans !== null) {
|
|
4263
|
-
const forWording = sent.length > 1 ? ` That number belongs to "${sent[0]?.query}" alone.` : "";
|
|
4264
|
-
notes.push(
|
|
4265
|
-
`${report.name} reported ${report.reportedTotal}: ${report.reportedTotalMeans}.${forWording}`
|
|
4266
|
-
);
|
|
4267
|
-
} else {
|
|
4268
|
-
notes.push(
|
|
4269
|
-
`${report.name} states no total and offers no second page, so a short list here is not evidence that little exists.`
|
|
4270
|
-
);
|
|
4271
|
-
}
|
|
5112
|
+
notes.push(...answeredNotes(report));
|
|
4272
5113
|
}
|
|
4273
|
-
|
|
4274
|
-
|
|
5114
|
+
const offTopic = answered.filter((report) => report.count > 0 && report.namesTheDish === 0);
|
|
5115
|
+
const onTopic = answered.some((report) => report.namesTheDish > 0);
|
|
5116
|
+
if (offTopic.length > 0 && onTopic) {
|
|
4275
5117
|
notes.push(
|
|
4276
|
-
|
|
5118
|
+
mustKeep(
|
|
5119
|
+
`No row from ${listNames(offTopic.map((report) => report.name))} carries this dish in its title. Those indexes answer the words they are handed, so rows came back for a dish they may not hold. Ask in the language that source publishes in, or open a row with get_recipe before calling it the dish.`
|
|
5120
|
+
)
|
|
4277
5121
|
);
|
|
4278
5122
|
}
|
|
5123
|
+
if (answered.some((report) => report.count > 0)) {
|
|
5124
|
+
notes.push(
|
|
5125
|
+
"Each source's rows are arranged with the ones naming the dish first. That is an order over one source's own rows and not a score against any other source."
|
|
5126
|
+
);
|
|
5127
|
+
}
|
|
5128
|
+
for (const report of answered) {
|
|
5129
|
+
if (report.rowsThatAreNotRecipes !== null && report.count > 0) {
|
|
5130
|
+
notes.push(
|
|
5131
|
+
`A row from ${report.name} can be ${report.rowsThatAreNotRecipes}. get_recipe says what it read off the page.`
|
|
5132
|
+
);
|
|
5133
|
+
}
|
|
5134
|
+
}
|
|
4279
5135
|
if (answered.length > 1) {
|
|
4280
5136
|
notes.push(
|
|
4281
|
-
|
|
5137
|
+
mustKeep(
|
|
5138
|
+
"Each count above measures something different, and they are never added together into one total."
|
|
5139
|
+
)
|
|
4282
5140
|
);
|
|
4283
5141
|
}
|
|
4284
5142
|
if (reports.some((report) => report.cached)) {
|
|
@@ -4383,7 +5241,7 @@ function creditLine(contributors) {
|
|
|
4383
5241
|
}
|
|
4384
5242
|
|
|
4385
5243
|
// src/tools/recipeView.ts
|
|
4386
|
-
var SERVING_WORD = /personne|people|serving|portion|part\b/i;
|
|
5244
|
+
var SERVING_WORD = /personne|people|serving|portion|part\b|racion|raciones|rac\b|persona|personas|comensal|comensales/i;
|
|
4387
5245
|
function whatTheOtherPartHolds(part, recipe) {
|
|
4388
5246
|
if (part === "ingredients") {
|
|
4389
5247
|
return recipe.steps.length > 0 ? `${recipe.steps.length} step(s) of method were read from it` : null;
|
|
@@ -4409,7 +5267,7 @@ var recipeSchema = z2.object({
|
|
|
4409
5267
|
id: z2.string(),
|
|
4410
5268
|
source: z2.string(),
|
|
4411
5269
|
source_name: z2.string(),
|
|
4412
|
-
language: z2.enum(["fr", "en"]).describe("The language this recipe is written in."),
|
|
5270
|
+
language: z2.enum(["fr", "en", "es"]).describe("The language this recipe is written in."),
|
|
4413
5271
|
title: z2.string(),
|
|
4414
5272
|
url: z2.string(),
|
|
4415
5273
|
image_url: z2.string().nullable(),
|
|
@@ -4446,9 +5304,41 @@ var recipeSchema = z2.object({
|
|
|
4446
5304
|
scaling_summary: z2.object({
|
|
4447
5305
|
scaled_count: z2.number().int(),
|
|
4448
5306
|
rounded_count: z2.number().int(),
|
|
4449
|
-
unscaled_count: z2.number().int()
|
|
4450
|
-
|
|
5307
|
+
unscaled_count: z2.number().int(),
|
|
5308
|
+
equipment_count: z2.number().int().describe("Lines naming a tool, which some sites write among the ingredients.")
|
|
5309
|
+
}).describe("The four counts add up to the number of ingredient lines.")
|
|
5310
|
+
});
|
|
5311
|
+
var collectionSchema = z2.object({
|
|
5312
|
+
id: z2.string(),
|
|
5313
|
+
source: z2.string(),
|
|
5314
|
+
source_name: z2.string(),
|
|
5315
|
+
title: z2.string(),
|
|
5316
|
+
url: z2.string(),
|
|
5317
|
+
image_url: z2.string().nullable(),
|
|
5318
|
+
headings: z2.array(z2.string()).describe(
|
|
5319
|
+
"The headings the article is built from, in the order it prints them, up to 'max_gathered'."
|
|
5320
|
+
),
|
|
5321
|
+
recipes: z2.array(rowSchema).describe(
|
|
5322
|
+
"The recipes it points at, each readable with get_recipe, up to 'max_gathered'. 'gathered_count' says how many the article holds."
|
|
5323
|
+
),
|
|
5324
|
+
gathered_count: z2.number().int().describe("Recipes the article points at, whether or not they are all listed here."),
|
|
5325
|
+
attribution: z2.string().describe("Show this, with the url, when repeating anything from here.")
|
|
4451
5326
|
});
|
|
5327
|
+
function buildCollectionView(recipe, limit) {
|
|
5328
|
+
const gathered = recipe.gathers?.rows ?? [];
|
|
5329
|
+
return {
|
|
5330
|
+
id: recipe.id,
|
|
5331
|
+
source: recipe.source,
|
|
5332
|
+
source_name: recipe.sourceName,
|
|
5333
|
+
title: recipe.title,
|
|
5334
|
+
url: recipe.url,
|
|
5335
|
+
image_url: recipe.imageUrl,
|
|
5336
|
+
headings: (recipe.gathers?.headings ?? []).slice(0, limit),
|
|
5337
|
+
recipes: gathered.slice(0, limit).map(toRowPayload),
|
|
5338
|
+
gathered_count: gathered.length,
|
|
5339
|
+
attribution: recipe.attribution
|
|
5340
|
+
};
|
|
5341
|
+
}
|
|
4452
5342
|
var SECTIONS = [
|
|
4453
5343
|
"ingredients",
|
|
4454
5344
|
"steps",
|
|
@@ -4498,7 +5388,7 @@ function resolveFactor(recipe, servings) {
|
|
|
4498
5388
|
const unit = recipe.yieldUnit;
|
|
4499
5389
|
if (unit && !SERVING_WORD.test(unit)) {
|
|
4500
5390
|
notes.push(
|
|
4501
|
-
`This page
|
|
5391
|
+
`This page counts its yield in ${quoteForeign(unit)}, which is not a number of eaters, so asking for ${servings} multiplied the quantities by ${round(factor)} to give ${servings} ${quoteForeign(unit)}.`
|
|
4502
5392
|
);
|
|
4503
5393
|
}
|
|
4504
5394
|
return { factor, notes };
|
|
@@ -4555,6 +5445,11 @@ function partNotes(recipe, wants) {
|
|
|
4555
5445
|
if (wants("steps") && recipe.steps.length === 0) {
|
|
4556
5446
|
notes.push(emptyPartNote(recipe, METHOD_PART));
|
|
4557
5447
|
}
|
|
5448
|
+
if (wants("equipment") && recipe.equipment.length === 0 && recipe.ingredients.length > 0) {
|
|
5449
|
+
notes.push(
|
|
5450
|
+
`${recipe.sourceName} keeps no equipment list of its own. What a recipe is cooked with is written among the ingredients, where each such line carries 'is_equipment'.`
|
|
5451
|
+
);
|
|
5452
|
+
}
|
|
4558
5453
|
if (wants("steps") && recipe.stepsAsOneBlock === true && recipe.steps.length > 0) {
|
|
4559
5454
|
notes.push(
|
|
4560
5455
|
mustKeep(
|
|
@@ -4569,8 +5464,11 @@ function buildRecipeView(recipe, options) {
|
|
|
4569
5464
|
const wants = (section) => options.sections.includes(section);
|
|
4570
5465
|
const ingredients = factor === null || factor === 1 ? passthroughIngredients(recipe.ingredients, recipe.language) : scaleIngredients(recipe.ingredients, { factor, language: recipe.language });
|
|
4571
5466
|
const notes = [...yieldNotes, ...partNotes(recipe, wants)];
|
|
4572
|
-
const rounded = ingredients.filter((entry) => entry.scaling === "rounded");
|
|
4573
|
-
const
|
|
5467
|
+
const rounded = ingredients.filter((entry) => entry.scaling === "rounded" && !entry.isEquipment);
|
|
5468
|
+
const equipment = ingredients.filter((entry) => entry.isEquipment);
|
|
5469
|
+
const unscaled = ingredients.filter(
|
|
5470
|
+
(entry) => entry.scaling === "unscaled" && !entry.isEquipment
|
|
5471
|
+
);
|
|
4574
5472
|
if (rounded.length > 0) {
|
|
4575
5473
|
notes.push(
|
|
4576
5474
|
`${rounded.length} line(s) did not land on the exact product; each says in its own note what happened to it.`
|
|
@@ -4581,6 +5479,11 @@ function buildRecipeView(recipe, options) {
|
|
|
4581
5479
|
`${unscaled.length} line(s) carry no quantity to multiply and are repeated as published.`
|
|
4582
5480
|
);
|
|
4583
5481
|
}
|
|
5482
|
+
if (equipment.length > 0) {
|
|
5483
|
+
notes.push(
|
|
5484
|
+
`${equipment.length} line(s) name a tool, which this site writes among them. They were left as published: a recipe made for more people uses the same one.`
|
|
5485
|
+
);
|
|
5486
|
+
}
|
|
4584
5487
|
if (recipe.license) {
|
|
4585
5488
|
notes.push(
|
|
4586
5489
|
mustKeep(
|
|
@@ -4636,9 +5539,14 @@ function buildRecipeView(recipe, options) {
|
|
|
4636
5539
|
sections_returned: [...options.sections],
|
|
4637
5540
|
sections_omitted: omitted,
|
|
4638
5541
|
scaling_summary: {
|
|
4639
|
-
|
|
5542
|
+
// A tool line is counted apart, so the four counts add up to the lines in
|
|
5543
|
+
// the list. It carries "unscaled" like a line with no figure on it, and
|
|
5544
|
+
// merging the two would say this server found no quantity where it found
|
|
5545
|
+
// one and declined it.
|
|
5546
|
+
scaled_count: ingredients.filter((entry) => entry.scaling === "scaled" && !entry.isEquipment).length,
|
|
4640
5547
|
rounded_count: rounded.length,
|
|
4641
|
-
unscaled_count: unscaled.length
|
|
5548
|
+
unscaled_count: unscaled.length,
|
|
5549
|
+
equipment_count: equipment.length
|
|
4642
5550
|
}
|
|
4643
5551
|
};
|
|
4644
5552
|
if (options.label) {
|
|
@@ -4769,12 +5677,6 @@ var compareRecipesOutput = z4.object({
|
|
|
4769
5677
|
});
|
|
4770
5678
|
var TEXT_INGREDIENT_LINES = 8;
|
|
4771
5679
|
var TEXT_DIFFERENCE_LINES = 4;
|
|
4772
|
-
function listNames(names2) {
|
|
4773
|
-
if (names2.length <= 1) {
|
|
4774
|
-
return names2.join("");
|
|
4775
|
-
}
|
|
4776
|
-
return `${names2.slice(0, -1).join(", ")} and ${names2.at(-1)}`;
|
|
4777
|
-
}
|
|
4778
5680
|
function agrees(names2, singular, plural) {
|
|
4779
5681
|
return names2.length === 1 ? singular : plural;
|
|
4780
5682
|
}
|
|
@@ -4823,12 +5725,11 @@ function describeDifferences(recipes, payloads, sections) {
|
|
|
4823
5725
|
`${stated.map((one) => `${one.name} yields ${quoteForeign(one.text)}`).join("; ")}.`
|
|
4824
5726
|
);
|
|
4825
5727
|
}
|
|
4826
|
-
const
|
|
5728
|
+
const foodLines = (payload) => payload.ingredients.filter((entry) => !entry.is_equipment).length;
|
|
5729
|
+
const counts = payloads.map(foodLines);
|
|
4827
5730
|
if (new Set(counts).size > 1 && counts.every((value) => value > 0)) {
|
|
4828
5731
|
differences.push(
|
|
4829
|
-
`${payloads.map(
|
|
4830
|
-
(payload) => `${payload.source_name} lists ${payload.ingredients.length} ingredient lines`
|
|
4831
|
-
).join("; ")}.`
|
|
5732
|
+
`${payloads.map((payload) => `${payload.source_name} lists ${foodLines(payload)} ingredient lines`).join("; ")}.`
|
|
4832
5733
|
);
|
|
4833
5734
|
}
|
|
4834
5735
|
if (sections.includes("times")) {
|
|
@@ -4874,6 +5775,30 @@ function describeDifferences(recipes, payloads, sections) {
|
|
|
4874
5775
|
}
|
|
4875
5776
|
return differences;
|
|
4876
5777
|
}
|
|
5778
|
+
function whyItIsMissing(report, dish, outcome) {
|
|
5779
|
+
if (outcome.article) {
|
|
5780
|
+
return mustKeep(
|
|
5781
|
+
`${report.name} is missing from this comparison because the page its search offered gathers other recipes: ${quoteForeign(outcome.article.title)}, ${quoteForeign(outcome.article.url)}. Read one of the recipes it lists with get_recipe.`
|
|
5782
|
+
);
|
|
5783
|
+
}
|
|
5784
|
+
if (report.status === "failed") {
|
|
5785
|
+
return mustKeep(
|
|
5786
|
+
`${report.name} is missing from this comparison because its search did not answer (${quoteForeign(
|
|
5787
|
+
`${report.error?.code ?? "unknown"}: ${report.error?.message ?? "unknown"}`
|
|
5788
|
+
)}). Nothing here is evidence about what it holds.`
|
|
5789
|
+
);
|
|
5790
|
+
}
|
|
5791
|
+
if (outcome.failedRead) {
|
|
5792
|
+
return mustKeep(
|
|
5793
|
+
`${report.name}'s search answered and offered a version, and that version could not be read (${quoteForeign(
|
|
5794
|
+
`${outcome.failedRead.code}: ${outcome.failedRead.message}`
|
|
5795
|
+
)}). The failure is in the reading, so nothing here says whether ${report.name} holds this dish.`
|
|
5796
|
+
);
|
|
5797
|
+
}
|
|
5798
|
+
return mustKeep(
|
|
5799
|
+
`${report.name} answered and offered nothing close enough to "${quoteForeign(dish)}" to compare.`
|
|
5800
|
+
);
|
|
5801
|
+
}
|
|
4877
5802
|
async function runCompareRecipes(client, args) {
|
|
4878
5803
|
try {
|
|
4879
5804
|
const merged = await client.searchRecipes(
|
|
@@ -4891,6 +5816,7 @@ async function runCompareRecipes(client, args) {
|
|
|
4891
5816
|
const reads = await Promise.allSettled(offered.map(([, id]) => client.getRecipe(id)));
|
|
4892
5817
|
const recipes = [];
|
|
4893
5818
|
const unread = /* @__PURE__ */ new Map();
|
|
5819
|
+
const gathered = /* @__PURE__ */ new Map();
|
|
4894
5820
|
const opened = /* @__PURE__ */ new Set();
|
|
4895
5821
|
reads.forEach((read, index) => {
|
|
4896
5822
|
const source = offered[index]?.[0];
|
|
@@ -4899,6 +5825,10 @@ async function runCompareRecipes(client, args) {
|
|
|
4899
5825
|
}
|
|
4900
5826
|
opened.add(source);
|
|
4901
5827
|
if (read.status === "fulfilled") {
|
|
5828
|
+
if (read.value.recipe.gathers) {
|
|
5829
|
+
gathered.set(source, read.value.recipe);
|
|
5830
|
+
return;
|
|
5831
|
+
}
|
|
4902
5832
|
recipes.push(read.value.recipe);
|
|
4903
5833
|
return;
|
|
4904
5834
|
}
|
|
@@ -4935,30 +5865,12 @@ async function runCompareRecipes(client, args) {
|
|
|
4935
5865
|
if (recipes.some((recipe) => recipe.source === report.source)) {
|
|
4936
5866
|
continue;
|
|
4937
5867
|
}
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
)}). Nothing here is evidence about what it holds.`
|
|
4945
|
-
)
|
|
4946
|
-
);
|
|
4947
|
-
} else if (failedRead) {
|
|
4948
|
-
notes.push(
|
|
4949
|
-
mustKeep(
|
|
4950
|
-
`${report.name}'s search answered and offered a version, and that version could not be read (${quoteForeign(
|
|
4951
|
-
`${failedRead.code}: ${failedRead.message}`
|
|
4952
|
-
)}). The failure is in the reading, so nothing here says whether ${report.name} holds this dish.`
|
|
4953
|
-
)
|
|
4954
|
-
);
|
|
4955
|
-
} else {
|
|
4956
|
-
notes.push(
|
|
4957
|
-
mustKeep(
|
|
4958
|
-
`${report.name} answered and offered nothing close enough to "${quoteForeign(args.dish)}" to compare.`
|
|
4959
|
-
)
|
|
4960
|
-
);
|
|
4961
|
-
}
|
|
5868
|
+
notes.push(
|
|
5869
|
+
whyItIsMissing(report, args.dish, {
|
|
5870
|
+
article: gathered.get(report.source),
|
|
5871
|
+
failedRead: unread.get(report.source)
|
|
5872
|
+
})
|
|
5873
|
+
);
|
|
4962
5874
|
}
|
|
4963
5875
|
const weighed = weighTitlesAgainstTheDish(payloads, args.dish, notes);
|
|
4964
5876
|
const comparable = recipes.filter(
|
|
@@ -5047,42 +5959,60 @@ var getRecipeDescription = [
|
|
|
5047
5959
|
"'id' must come from search_recipes. It names the source, so this reads the right one without guessing; an identifier no source would have minted is refused, because sending it anywhere would answer about the wrong dish.",
|
|
5048
5960
|
"Pass 'servings' to rescale. Quantities land where a kitchen can follow them: an egg stays whole, anything that pours or cuts can halve, a small measurement moves to a smaller unit before it is rounded, and anything unmultipliable is flagged rather than scaled.",
|
|
5049
5961
|
"A page that states no number of servings comes back as published and says so, because dividing by a yield nobody wrote would answer for a number of people the page never claimed.",
|
|
5962
|
+
"Read 'kind' first. Some sources publish articles gathering other recipes at the same kind of address as a recipe, and such an answer carries 'collection' with the recipes it points at and no 'recipe' at all: there is nothing to cook from that page, and the recipes it lists are read with get_recipe.",
|
|
5050
5963
|
"A part this answer holds nothing for says which of two things happened: the page showed no sign of it, or this server failed to read what the page carries. An empty ingredient list is never evidence that an ingredient is absent from the dish.",
|
|
5051
5964
|
"'sections' decides what comes back, and 'sections_omitted' names what was left out: a field belonging to an omitted section is empty because nobody asked for it, never because the page states nothing.",
|
|
5052
5965
|
"A field a source does not publish is null, never zero. Credit the source and link the url when you repeat any of it."
|
|
5053
5966
|
].join(" ");
|
|
5967
|
+
var LISTING_SHARE = 2 / 3;
|
|
5054
5968
|
var getRecipeInput = strictInput({
|
|
5055
5969
|
id: z5.string().min(1).max(500).describe(
|
|
5056
|
-
"From search_recipes, such as 'marmiton:44078', '
|
|
5970
|
+
"From search_recipes, such as 'marmiton:44078', 'pequerecetas:paella-de-marisco' or 'cookbook:Cookbook:Carbonara'. Two sources address a recipe by a bare number, so spell an id with its source."
|
|
5057
5971
|
),
|
|
5058
5972
|
servings: z5.number().int().min(1).max(500).optional().describe("Rescale to this many. Left out, the quantities come back as published."),
|
|
5059
5973
|
sections: z5.array(z5.enum(SECTIONS)).default(["ingredients", "steps"]).describe("Which parts to return. A full recipe is a lot of text, so this defaults to two."),
|
|
5060
5974
|
max_steps: z5.number().int().min(1).max(100).default(20).describe("Steps to return. The answer says how many more there are."),
|
|
5975
|
+
max_gathered: z5.number().int().min(1).max(500).default(30).describe(
|
|
5976
|
+
"Recipes and headings to return from an address that gathers recipes. The answer says how many the article holds."
|
|
5977
|
+
),
|
|
5061
5978
|
max_step_chars: z5.number().int().min(80).max(4e3).default(600).describe("Characters kept per step. Raise it only if a step was cut mid-sentence.")
|
|
5062
5979
|
});
|
|
5063
5980
|
var getRecipeOutput = z5.object({
|
|
5064
|
-
|
|
5981
|
+
kind: z5.enum(["recipe", "collection"]).describe(
|
|
5982
|
+
"What the address held. 'recipe' comes with 'recipe' and no 'collection'; 'collection' comes with 'collection' and no 'recipe', and is an article gathering other recipes, with no ingredients and no method of its own."
|
|
5983
|
+
),
|
|
5984
|
+
recipe: recipeSchema.optional().describe("Present when 'kind' is 'recipe'."),
|
|
5985
|
+
collection: collectionSchema.optional().describe("Present when 'kind' is 'collection'."),
|
|
5065
5986
|
id_read_as: z5.string().nullable().describe("How a raw identifier was routed, when it was not spelled with its source."),
|
|
5066
5987
|
notes: z5.array(z5.string())
|
|
5067
5988
|
});
|
|
5989
|
+
var ARGUMENTS_FOR_A_RECIPE = ["servings"];
|
|
5068
5990
|
async function runGetRecipe(client, args) {
|
|
5069
5991
|
try {
|
|
5070
5992
|
const { recipe, cached, read } = await client.getRecipe(args.id);
|
|
5993
|
+
const routed = read.inferred ? [
|
|
5994
|
+
`"${quoteForeign(args.id)}" was read as ${read.source.name}'s, because ${read.inferred}. Spell an id with its source to leave nothing to infer.`
|
|
5995
|
+
] : [];
|
|
5996
|
+
const served = cached ? ["Served from this server's short-lived in-memory cache."] : [];
|
|
5997
|
+
if (recipe.gathers) {
|
|
5998
|
+
const ignored = ARGUMENTS_FOR_A_RECIPE.filter((name) => args[name] !== void 0);
|
|
5999
|
+
const said = ignored.length > 0 ? [
|
|
6000
|
+
`This address held an article, so ${ignored.map((name) => `'${name}'`).join(" and ")} had nothing to act on. Call get_recipe on one of the recipes it lists.`
|
|
6001
|
+
] : [];
|
|
6002
|
+
return gatheredAnswer(
|
|
6003
|
+
recipe,
|
|
6004
|
+
read.inferred,
|
|
6005
|
+
[...routed, ...served, ...said],
|
|
6006
|
+
args.max_gathered
|
|
6007
|
+
);
|
|
6008
|
+
}
|
|
5071
6009
|
const view = buildRecipeView(recipe, {
|
|
5072
6010
|
servings: args.servings ?? null,
|
|
5073
6011
|
sections: args.sections,
|
|
5074
6012
|
maxSteps: args.max_steps,
|
|
5075
6013
|
maxStepChars: args.max_step_chars
|
|
5076
6014
|
});
|
|
5077
|
-
const notes = [...view.notes];
|
|
5078
|
-
if (read.inferred) {
|
|
5079
|
-
notes.unshift(
|
|
5080
|
-
`"${quoteForeign(args.id)}" was read as ${read.source.name}'s, because ${read.inferred}. Spell an id with its source to leave nothing to infer.`
|
|
5081
|
-
);
|
|
5082
|
-
}
|
|
5083
|
-
if (cached) {
|
|
5084
|
-
notes.push("Served from this server's short-lived in-memory cache.");
|
|
5085
|
-
}
|
|
6015
|
+
const notes = [...routed, ...view.notes, ...served];
|
|
5086
6016
|
const payload = view.payload;
|
|
5087
6017
|
const credit = creditLine([{ attribution: payload.attribution, url: payload.url }]);
|
|
5088
6018
|
const head = [
|
|
@@ -5090,9 +6020,12 @@ async function runGetRecipe(client, args) {
|
|
|
5090
6020
|
quoteForeign(payload.url),
|
|
5091
6021
|
renderYield(payload)
|
|
5092
6022
|
];
|
|
5093
|
-
const ingredientLines = payload.ingredients.map(
|
|
5094
|
-
|
|
5095
|
-
|
|
6023
|
+
const ingredientLines = payload.ingredients.map((entry) => {
|
|
6024
|
+
if (entry.is_equipment) {
|
|
6025
|
+
return `- ${quoteForeign(entry.text)} (a tool)`;
|
|
6026
|
+
}
|
|
6027
|
+
return entry.scaling === "unscaled" ? `- ${quoteForeign(entry.text)} (no quantity)` : `- ${quoteForeign(entry.text)}`;
|
|
6028
|
+
});
|
|
5096
6029
|
const stepLines = payload.steps.map((step, index) => `${index + 1}. ${quoteForeign(step)}`);
|
|
5097
6030
|
const room = roomForBody({ notes, credit }) - head.join("\n").length;
|
|
5098
6031
|
const parts = [ingredientLines, stepLines].filter((part) => part.length > 0);
|
|
@@ -5115,7 +6048,7 @@ async function runGetRecipe(client, args) {
|
|
|
5115
6048
|
}
|
|
5116
6049
|
}
|
|
5117
6050
|
return ok(
|
|
5118
|
-
{ recipe: payload, id_read_as: read.inferred, notes: noteTexts(notes) },
|
|
6051
|
+
{ kind: "recipe", recipe: payload, id_read_as: read.inferred, notes: noteTexts(notes) },
|
|
5119
6052
|
lines.join("\n"),
|
|
5120
6053
|
{ notes, credit }
|
|
5121
6054
|
);
|
|
@@ -5123,11 +6056,54 @@ async function runGetRecipe(client, args) {
|
|
|
5123
6056
|
return toToolError(error);
|
|
5124
6057
|
}
|
|
5125
6058
|
}
|
|
6059
|
+
function gatheredAnswer(recipe, inferred, earlier, limit) {
|
|
6060
|
+
const payload = buildCollectionView(recipe, limit);
|
|
6061
|
+
const notes = [
|
|
6062
|
+
...earlier,
|
|
6063
|
+
...payload.gathered_count > payload.recipes.length ? [
|
|
6064
|
+
`This article points at ${payload.gathered_count} recipes and ${payload.recipes.length} are here. Raise 'max_gathered' for the rest.`
|
|
6065
|
+
] : [],
|
|
6066
|
+
mustKeep(
|
|
6067
|
+
`This address gathers other recipes, so there are no ingredients and no method to read. ${payload.source_name} publishes both at this kind of address. Call get_recipe on one of the recipes listed here.`
|
|
6068
|
+
)
|
|
6069
|
+
];
|
|
6070
|
+
const credit = creditLine([{ attribution: payload.attribution, url: payload.url }]);
|
|
6071
|
+
const head = [
|
|
6072
|
+
`${quoteForeign(payload.title)} \xB7 ${quoteForeign(payload.source_name)}`,
|
|
6073
|
+
quoteForeign(payload.url),
|
|
6074
|
+
`An article that gathers ${payload.gathered_count} recipe(s).`
|
|
6075
|
+
];
|
|
6076
|
+
const rows = payload.recipes.map(
|
|
6077
|
+
(row) => `- ${quoteForeign(row.title)} (${quoteForeign(row.id)})`
|
|
6078
|
+
);
|
|
6079
|
+
const headings = payload.headings.map(quoteForeign);
|
|
6080
|
+
const room = roomForBody({ notes, credit }) - head.join("\n").length;
|
|
6081
|
+
const fitted = fitLines(rows, room * LISTING_SHARE);
|
|
6082
|
+
const shownHeadings = fitLines(headings, room - fitted.lines.join("\n").length);
|
|
6083
|
+
const lines = [...head];
|
|
6084
|
+
if (rows.length > 0) {
|
|
6085
|
+
lines.push("", "Recipes it points at:", ...fitted.lines);
|
|
6086
|
+
if (fitted.hidden > 0) {
|
|
6087
|
+
lines.push(omittedLinesLine(fitted.lines.length, rows.length, "recipes"));
|
|
6088
|
+
}
|
|
6089
|
+
}
|
|
6090
|
+
if (headings.length > 0 && shownHeadings.lines.length > 0) {
|
|
6091
|
+
lines.push("", `Built from: ${shownHeadings.lines.join(", ")}`);
|
|
6092
|
+
if (shownHeadings.hidden > 0) {
|
|
6093
|
+
lines.push(omittedLinesLine(shownHeadings.lines.length, headings.length, "headings"));
|
|
6094
|
+
}
|
|
6095
|
+
}
|
|
6096
|
+
return ok(
|
|
6097
|
+
{ kind: "collection", collection: payload, id_read_as: inferred, notes: noteTexts(notes) },
|
|
6098
|
+
lines.join("\n"),
|
|
6099
|
+
{ notes, credit }
|
|
6100
|
+
);
|
|
6101
|
+
}
|
|
5126
6102
|
|
|
5127
6103
|
// src/tools/scaleIngredients.ts
|
|
5128
6104
|
import { z as z6 } from "zod";
|
|
5129
6105
|
var scaleIngredientsDescription = [
|
|
5130
|
-
"Multiply a list of ingredient lines, in French, in English, or in a list holding
|
|
6106
|
+
"Multiply a list of ingredient lines, in French, in English, in Spanish, or in a list holding more than one of them.",
|
|
5131
6107
|
"Give either 'factor', or 'from_servings' and 'to_servings' and the factor is worked out from them.",
|
|
5132
6108
|
"Quantities land where a kitchen can follow them: an egg stays whole because half of one is not something a cook takes out of the shell, while anything that pours, weighs or cuts can halve, a spoonful shrinks into the smaller spoon before it is rounded, and a pinch keeps whatever size a hand gives it while its count is multiplied.",
|
|
5133
6109
|
"Every line comes back with 'scaling': 'scaled' when the arithmetic landed on the exact product, 'rounded' when something had to move for the line to stay usable, 'unscaled' when the line carries nothing to multiply. A rounded line says what it was rounded from and in which direction.",
|
|
@@ -5142,8 +6118,8 @@ var scaleIngredientsInput = strictInput({
|
|
|
5142
6118
|
),
|
|
5143
6119
|
from_servings: z6.number().int().min(1).max(500).optional().describe("What the list serves now. Give it with 'to_servings', and without 'factor'."),
|
|
5144
6120
|
to_servings: z6.number().int().min(1).max(500).optional().describe("What it should serve. Give it with 'from_servings', and without 'factor'."),
|
|
5145
|
-
language: z6.enum(["auto", "fr", "en"]).default("auto").describe(
|
|
5146
|
-
"'auto' reads each line on its own, which is what a list holding
|
|
6121
|
+
language: z6.enum(["auto", "fr", "en", "es"]).default("auto").describe(
|
|
6122
|
+
"'auto' reads each line on its own, which is what a list holding more than one language needs. Name a language to read every line that way."
|
|
5147
6123
|
)
|
|
5148
6124
|
});
|
|
5149
6125
|
var scaleIngredientsOutput = z6.object({
|
|
@@ -5152,7 +6128,10 @@ var scaleIngredientsOutput = z6.object({
|
|
|
5152
6128
|
ingredients: z6.array(ingredientSchema),
|
|
5153
6129
|
scaled_count: z6.number().int().describe("Lines whose arithmetic came out exact."),
|
|
5154
6130
|
rounded_count: z6.number().int().describe("Lines whose value moved to stay usable."),
|
|
5155
|
-
unscaled_count: z6.number().int().describe("Lines carrying nothing that can be multiplied."),
|
|
6131
|
+
unscaled_count: z6.number().int().describe("Lines carrying nothing that can be multiplied, tools excepted."),
|
|
6132
|
+
equipment_count: z6.number().int().describe(
|
|
6133
|
+
"Lines naming a tool, which are never multiplied: a recipe made for more people uses the same pan."
|
|
6134
|
+
),
|
|
5156
6135
|
notes: z6.array(z6.string())
|
|
5157
6136
|
});
|
|
5158
6137
|
function resolveFactor2(args) {
|
|
@@ -5195,7 +6174,8 @@ function runScaleIngredients(args) {
|
|
|
5195
6174
|
const counts = {
|
|
5196
6175
|
scaled_count: scaled.filter((entry) => entry.scaling === "scaled").length,
|
|
5197
6176
|
rounded_count: scaled.filter((entry) => entry.scaling === "rounded").length,
|
|
5198
|
-
unscaled_count: scaled.filter((entry) => entry.scaling === "unscaled").length
|
|
6177
|
+
unscaled_count: scaled.filter((entry) => entry.scaling === "unscaled" && !entry.isEquipment).length,
|
|
6178
|
+
equipment_count: scaled.filter((entry) => entry.isEquipment).length
|
|
5199
6179
|
};
|
|
5200
6180
|
const notes = [];
|
|
5201
6181
|
if (counts.rounded_count > 0) {
|
|
@@ -5208,11 +6188,16 @@ function runScaleIngredients(args) {
|
|
|
5208
6188
|
`${counts.unscaled_count} line(s) carry no quantity to multiply and are repeated as given.`
|
|
5209
6189
|
);
|
|
5210
6190
|
}
|
|
6191
|
+
if (counts.equipment_count > 0) {
|
|
6192
|
+
notes.push(
|
|
6193
|
+
`${counts.equipment_count} line(s) name a tool and were left as given: a recipe made for more people uses the same one.`
|
|
6194
|
+
);
|
|
6195
|
+
}
|
|
5211
6196
|
if (args.language === "auto") {
|
|
5212
6197
|
const languages = new Set(scaled.map((entry) => entry.language));
|
|
5213
6198
|
if (languages.size > 1) {
|
|
5214
6199
|
notes.push(
|
|
5215
|
-
"This list holds lines in
|
|
6200
|
+
"This list holds lines in more than one language, and each was read and rewritten in its own."
|
|
5216
6201
|
);
|
|
5217
6202
|
}
|
|
5218
6203
|
}
|
|
@@ -5261,7 +6246,7 @@ var searchRecipesDescription = [
|
|
|
5261
6246
|
"Search every recipe source this server reads, at the same time, for a dish or an ingredient, and get one merged list.",
|
|
5262
6247
|
"Each row carries the id get_recipe takes, and that id names the source it came from, so nothing has to be guessed afterwards.",
|
|
5263
6248
|
"The sources are written in different languages and count their own results differently, so 'per_source' says what each one answered, what its own number means, and names any that failed. A short list is never evidence of what exists.",
|
|
5264
|
-
"Some sources
|
|
6249
|
+
"Not every row opens onto a recipe. Some sources file pages about an ingredient beside the recipes using it, and some publish articles that gather recipes; 'per_source' says in each source's own words what one of its rows can be. Only get_recipe tells them apart, and it says what it read off the page.",
|
|
5265
6250
|
"The query goes to each source's own search as free text. There is no filtering: a word naming a diet, a time or a calorie count matches only where that source's index happens to carry it.",
|
|
5266
6251
|
"Ask in a whole sentence if that is the question. These indexes answer the words they are handed, so a sentence is also sent as the words naming the dish and as the dish word alone, and the rows are the union; 'per_source' lists every wording and what it returned.",
|
|
5267
6252
|
"What the question says the recipe must not hold is set aside from those shorter wordings rather than searched for, and named back in the notes: a negation, an allergy stated as one, a diet named in one word, and the number of people at the table. No source filters on any of it, so open a row with get_recipe and read the ingredient list before calling it suitable.",
|
|
@@ -5378,7 +6363,8 @@ async function runSearchRecipes(client, args) {
|
|
|
5378
6363
|
];
|
|
5379
6364
|
const alsoSent = derived.length > 0 ? `
|
|
5380
6365
|
Also searched for: ${derived.map((wording) => `"${quoteForeign(wording)}"`).join(", ")}.` : "";
|
|
5381
|
-
const
|
|
6366
|
+
const anyNamesTheDish = merged.reports.some((report) => report.namesTheDish > 0);
|
|
6367
|
+
const body = results.length > 0 ? `${results.length} ${anyNamesTheDish ? "recipes" : "rows"} for "${args.query}":
|
|
5382
6368
|
${renderRows(results)}${alsoSent}` : nothingCameBack(answered.length, args.query, alsoSent);
|
|
5383
6369
|
return ok(
|
|
5384
6370
|
{
|
|
@@ -5415,8 +6401,9 @@ function buildInstructions(sources) {
|
|
|
5415
6401
|
`Tools for recipes, reading ${sources.length} sources: ${named}. No API key and no account are needed.`,
|
|
5416
6402
|
"search_recipes asks every source at once and returns one list; every id it hands back names the source it came from, and get_recipe routes on that.",
|
|
5417
6403
|
"Use compare_recipes when the question is how a dish differs between traditions: it reads each source's closest version and can rescale them all to the same number of servings so the lists stand comparison.",
|
|
6404
|
+
"get_recipe answers in two shapes and says which under 'kind': a recipe, or an article gathering other recipes, which some sources publish at the same kind of address and which carries no ingredients and no method of its own.",
|
|
5418
6405
|
"Do not rescale quantities yourself. get_recipe and scale_ingredients keep an egg whole, halve what pours or cuts, move a small measurement to a smaller unit before rounding it, and flag what cannot be scaled, which is what stops answers like '2.4 eggs'.",
|
|
5419
|
-
"scale_ingredients works offline on a list from anywhere and reads French and
|
|
6406
|
+
"scale_ingredients works offline on a list from anywhere and reads French, English and Spanish lines in one call, each in its own language.",
|
|
5420
6407
|
"A source that fails is named as a source that failed. An answer holding rows from some sources is never evidence about what the others hold, and 'per_source' says which was which.",
|
|
5421
6408
|
"The sources share no scale. Their result counts count different things and are never added; some carry reader ratings and some have no author and no rating by nature, so rows are interleaved rather than ranked.",
|
|
5422
6409
|
"Nothing is converted between measuring systems, and a yield is repeated in the source's own words, because '4 \xE0 6 personnes' and '4 personnes' are different claims. A page stating no yield cannot be rescaled and says so.",
|
|
@@ -5457,7 +6444,7 @@ function createServer(options = {}) {
|
|
|
5457
6444
|
server.registerTool(
|
|
5458
6445
|
"scale_ingredients",
|
|
5459
6446
|
{
|
|
5460
|
-
title: "Scale an ingredient list, in
|
|
6447
|
+
title: "Scale an ingredient list, in any language it reads",
|
|
5461
6448
|
description: scaleIngredientsDescription,
|
|
5462
6449
|
inputSchema: scaleIngredientsInput,
|
|
5463
6450
|
outputSchema: scaleIngredientsOutput,
|