feat: fixed more tool edge cases

This commit is contained in:
Leyla Becker 2026-02-22 15:19:46 -06:00
parent edd4e01c8d
commit 4c7d0fe262
10 changed files with 307 additions and 12 deletions

View file

@ -356,14 +356,31 @@ function formatMeasurementText(amount, unit, approximate) {
return prefix + formatValueUnit(amount.value, unit);
}
// ─── Serving Vessel Detection ───────────────────────────
// Serving vessel keywords — items in the Tools section containing these words
// represent containers that hold the finished product, so their counts should
// scale with the recipe (e.g., doubling a recipe needs twice as many glasses).
// Cooking tools like pots, pans, bowls, whisks, etc. are NOT in this list
// because their quantity doesn't change when scaling a recipe.
const SERVING_VESSEL_RE = /\b(glass(?:es)?|mugs?|dish(?:es)?|jars?|containers?|highballs?|lowballs?|tumblers?|goblets?|snifters?|flutes?|coupes?|ramekins?|pyrex|plates?)\b/i;
// ─── Token Replacement ──────────────────────────────────
function replaceMeasurementsInToken(token, Token) {
function replaceMeasurementsInToken(token, Token, inToolsSection) {
const text = token.content;
const measurements = findAllMeasurements(text);
if (measurements.length === 0) return [token];
// In the tools section, counts are only scalable if:
// 1. The line has non-count measurements (dimensions, weights, volumes) alongside
// the count — e.g. "9x13 inch baking dish 1" or "64 oz mason jar 4"
// 2. OR the line contains a serving vessel keyword — e.g. "mug 1", "highball 1"
// Cooking tools like "pots 2" or "mixing bowls 2" have neither, so they stay fixed.
const hasNonCountMeasurements = measurements.some(m => m.type !== 'count');
const hasServingVessel = SERVING_VESSEL_RE.test(text);
const newTokens = [];
let lastEnd = 0;
@ -385,10 +402,15 @@ function replaceMeasurementsInToken(token, Token) {
open.attrSet('data-alt', altText);
open.attrSet('title', m.match);
// Add scaling data for weight/volume
// Add scaling data for weight/volume/count
const scaleData = computeScaleData(m);
if (scaleData) {
open.attrSet('data-scalable', JSON.stringify(scaleData));
// In tools section, skip scaling for bare counts on cooking tools ("pots 2")
// but allow scaling for serving vessels (keyword match or has measurements)
const skipScale = inToolsSection && m.type === 'count' && !hasNonCountMeasurements && !hasServingVessel;
if (!skipScale) {
open.attrSet('data-scalable', JSON.stringify(scaleData));
}
}
newTokens.push(open);
@ -421,7 +443,33 @@ function measurementPlugin(md) {
md.core.ruler.push('measurements', function measurementRule(state) {
const tokens = state.tokens;
let inToolsSection = false;
let toolsSectionLevel = null;
for (let i = 0; i < tokens.length; i++) {
// Detect heading boundaries to track the "Tools" section.
// When we see a heading_open, peek at the next inline token for the text.
if (tokens[i].type === 'heading_open') {
const level = tokens[i].tag; // 'h1', 'h2', 'h3', etc.
// If we're in a tools section and hit a heading at the same or higher level,
// we've left the tools section.
if (inToolsSection && level <= toolsSectionLevel) {
inToolsSection = false;
toolsSectionLevel = null;
}
// Check if this heading is "Tools" (case-insensitive)
if (i + 1 < tokens.length && tokens[i + 1].type === 'inline') {
const headingText = tokens[i + 1].content.trim().toLowerCase();
if (headingText === 'tools') {
inToolsSection = true;
toolsSectionLevel = level;
}
}
continue;
}
if (tokens[i].type !== 'inline' || !tokens[i].children) continue;
const children = tokens[i].children;
@ -429,7 +477,7 @@ function measurementPlugin(md) {
for (const child of children) {
if (child.type === 'text') {
const replaced = replaceMeasurementsInToken(child, state.Token);
const replaced = replaceMeasurementsInToken(child, state.Token, inToolsSection);
newChildren.push(...replaced);
} else {
newChildren.push(child);