nutrition: extract shared ref resolution, fix HTML in ingredient names
All checks were successful
CI / update (push) Successful in 4m45s
All checks were successful
CI / update (push) Successful in 4m45s
- Move parseAnchorRecipeRef and resolveReferencedNutrition from the items endpoint into nutritionMatcher.ts for reuse - JSON-LD endpoint now includes nutrition from referenced recipes (base recipe refs and anchor-tag ingredient refs) - Strip HTML tags in normalizeIngredientName/De before matching to prevent regex crash on ingredients containing anchor tags - Escape regex special chars in substringMatchScore word-boundary check
This commit is contained in:
@@ -4,7 +4,7 @@ import { dbConnect } from '$utils/db';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import type { RecipeModelType, IngredientItem, InstructionItem } from '$types/types';
|
||||
import { isEnglish } from '$lib/server/recipeHelpers';
|
||||
import { getNutritionEntryByFdcId, getBlsEntryByCode, computeRecipeNutritionTotals } from '$lib/server/nutritionMatcher';
|
||||
import { getNutritionEntryByFdcId, getBlsEntryByCode, resolveReferencedNutrition } from '$lib/server/nutritionMatcher';
|
||||
|
||||
/** Recursively map populated baseRecipeRef to resolvedRecipe field */
|
||||
function mapBaseRecipeRefs(items: any[]): any[] {
|
||||
@@ -42,72 +42,6 @@ function resolveNutritionData(mappings: any[]): any[] {
|
||||
});
|
||||
}
|
||||
|
||||
/** Parse anchor href from ingredient name, return short_name or null */
|
||||
function parseAnchorRecipeRef(ingredientName: string): string | null {
|
||||
const match = ingredientName.match(/<a\s+href=["']?([^"' >]+)["']?[^>]*>/i);
|
||||
if (!match) return null;
|
||||
let href = match[1].trim();
|
||||
// Strip query params (e.g., ?multiplier={{multiplier}})
|
||||
href = href.split('?')[0];
|
||||
// Skip external links
|
||||
if (href.startsWith('http') || href.includes('://')) return null;
|
||||
// Strip leading path components like /rezepte/ or ./
|
||||
href = href.replace(/^(\.?\/?rezepte\/|\.\/|\/)/, '');
|
||||
// Skip if contains a dot (file extensions, external domains)
|
||||
if (href.includes('.')) return null;
|
||||
return href || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build nutrition totals for referenced recipes:
|
||||
* 1. Base recipe references (type='reference' with populated baseRecipeRef)
|
||||
* 2. Anchor-tag references in ingredient names (<a href=...>)
|
||||
*/
|
||||
async function resolveReferencedNutrition(
|
||||
ingredients: any[],
|
||||
): Promise<{ shortName: string; name: string; nutrition: Record<string, number>; baseMultiplier: number }[]> {
|
||||
const results: { shortName: string; name: string; nutrition: Record<string, number>; baseMultiplier: number }[] = [];
|
||||
const processedSlugs = new Set<string>();
|
||||
|
||||
for (const section of ingredients) {
|
||||
// Type 1: Base recipe references
|
||||
if (section.type === 'reference' && section.baseRecipeRef) {
|
||||
const ref = section.baseRecipeRef;
|
||||
const slug = ref.short_name;
|
||||
if (processedSlugs.has(slug)) continue;
|
||||
processedSlugs.add(slug);
|
||||
|
||||
if (ref.nutritionMappings?.length > 0) {
|
||||
const mult = section.baseMultiplier || 1;
|
||||
const nutrition = computeRecipeNutritionTotals(ref.ingredients || [], ref.nutritionMappings, 1);
|
||||
results.push({ shortName: slug, name: ref.name, nutrition, baseMultiplier: mult });
|
||||
}
|
||||
}
|
||||
|
||||
// Type 2: Anchor-tag references in ingredient names
|
||||
if (section.list) {
|
||||
for (const item of section.list) {
|
||||
const refSlug = parseAnchorRecipeRef(item.name || '');
|
||||
if (!refSlug || processedSlugs.has(refSlug)) continue;
|
||||
processedSlugs.add(refSlug);
|
||||
|
||||
// Look up the referenced recipe
|
||||
const refRecipe = await Recipe.findOne({ short_name: refSlug })
|
||||
.select('short_name name ingredients nutritionMappings portions')
|
||||
.lean();
|
||||
if (!refRecipe?.nutritionMappings?.length) continue;
|
||||
|
||||
const nutrition = computeRecipeNutritionTotals(
|
||||
refRecipe.ingredients || [], refRecipe.nutritionMappings, 1
|
||||
);
|
||||
results.push({ shortName: refSlug, name: refRecipe.name, nutrition, baseMultiplier: 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
export const GET: RequestHandler = async ({ params }) => {
|
||||
await dbConnect();
|
||||
const en = isEnglish(params.recipeLang!);
|
||||
|
||||
@@ -2,19 +2,23 @@ import { json, type RequestHandler } from '@sveltejs/kit';
|
||||
import { Recipe } from '$models/Recipe';
|
||||
import { dbConnect } from '$utils/db';
|
||||
import { generateRecipeJsonLd } from '$lib/js/recipeJsonLd';
|
||||
import { resolveReferencedNutrition } from '$lib/server/nutritionMatcher';
|
||||
import type { RecipeModelType } from '$types/types';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export const GET: RequestHandler = async ({ params, setHeaders }) => {
|
||||
await dbConnect();
|
||||
let recipe = (await Recipe.findOne({ short_name: params.name }).lean()) as unknown as RecipeModelType;
|
||||
let recipe = (await Recipe.findOne({ short_name: params.name })
|
||||
.populate({ path: 'ingredients.baseRecipeRef', select: 'short_name name ingredients nutritionMappings' })
|
||||
.lean()) as unknown as RecipeModelType;
|
||||
|
||||
recipe = JSON.parse(JSON.stringify(recipe));
|
||||
if (recipe == null) {
|
||||
throw error(404, "Recipe not found");
|
||||
}
|
||||
|
||||
const jsonLd = generateRecipeJsonLd(recipe);
|
||||
const referencedNutrition = await resolveReferencedNutrition(recipe.ingredients || []);
|
||||
const jsonLd = generateRecipeJsonLd(recipe, referencedNutrition);
|
||||
|
||||
// Set appropriate headers for JSON-LD
|
||||
setHeaders({
|
||||
|
||||
Reference in New Issue
Block a user