feat: added density conversion
This commit is contained in:
parent
bac7a14f95
commit
df26f0243b
5 changed files with 415 additions and 12 deletions
|
|
@ -159,15 +159,31 @@
|
||||||
spans.forEach(function (span) {
|
spans.forEach(function (span) {
|
||||||
var type = span.getAttribute('data-measurement-type');
|
var type = span.getAttribute('data-measurement-type');
|
||||||
var isHybrid = span.getAttribute('data-hybrid') === 'true';
|
var isHybrid = span.getAttribute('data-hybrid') === 'true';
|
||||||
var useVolumeMode = isHybrid && measureByVolume;
|
|
||||||
|
|
||||||
var effectiveType = useVolumeMode ? 'volume' : type;
|
var scalableAttr, defaultAttr, altAttr, effectiveType;
|
||||||
|
|
||||||
|
if (isHybrid && measureByVolume && type === 'weight') {
|
||||||
|
// weight measurement in volume mode -> use volume data
|
||||||
|
scalableAttr = 'data-volume-scalable';
|
||||||
|
defaultAttr = 'data-volume-default';
|
||||||
|
altAttr = 'data-volume-alt';
|
||||||
|
effectiveType = 'volume';
|
||||||
|
} else if (isHybrid && !measureByVolume && type === 'volume') {
|
||||||
|
// volume measurement in weight mode -> use weight data
|
||||||
|
scalableAttr = 'data-weight-scalable';
|
||||||
|
defaultAttr = 'data-weight-default';
|
||||||
|
altAttr = 'data-weight-alt';
|
||||||
|
effectiveType = 'weight';
|
||||||
|
} else {
|
||||||
|
// default: use primary data
|
||||||
|
scalableAttr = 'data-scalable';
|
||||||
|
defaultAttr = 'data-default';
|
||||||
|
altAttr = 'data-alt';
|
||||||
|
effectiveType = type;
|
||||||
|
}
|
||||||
|
|
||||||
var imperial = !!showingAlt[effectiveType];
|
var imperial = !!showingAlt[effectiveType];
|
||||||
|
|
||||||
var scalableAttr = useVolumeMode ? 'data-volume-scalable' : 'data-scalable';
|
|
||||||
var defaultAttr = useVolumeMode ? 'data-volume-default' : 'data-default';
|
|
||||||
var altAttr = useVolumeMode ? 'data-volume-alt' : 'data-alt';
|
|
||||||
|
|
||||||
var scalableRaw = span.getAttribute(scalableAttr);
|
var scalableRaw = span.getAttribute(scalableAttr);
|
||||||
|
|
||||||
if (scalableRaw && scale !== 1) {
|
if (scalableRaw && scale !== 1) {
|
||||||
|
|
@ -258,9 +274,10 @@
|
||||||
if (span.getAttribute('data-hybrid') === 'true') hasHybrid = true;
|
if (span.getAttribute('data-hybrid') === 'true') hasHybrid = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Hybrid measurements in volume mode need the volume metric/imperial toggle
|
// Hybrid measurements need both weight and volume metric/imperial toggles
|
||||||
if (hasHybrid) {
|
if (hasHybrid) {
|
||||||
typeHasToggle['volume'] = true;
|
typeHasToggle['volume'] = true;
|
||||||
|
typeHasToggle['weight'] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasAny = false;
|
var hasAny = false;
|
||||||
|
|
|
||||||
|
|
@ -272,9 +272,12 @@ describe('hybrid weight/volume measurements', () => {
|
||||||
expect(html).toContain('data-volume-scalable=');
|
expect(html).toContain('data-volume-scalable=');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does NOT add hybrid attributes for weight-to-weight alt', () => {
|
it('does NOT add manual hybrid attributes for weight-to-weight alt', () => {
|
||||||
const md = '- butter 227g (8 oz)';
|
// weight-to-weight alt doesn't produce manual hybrid volume data,
|
||||||
|
// but density-based auto hybrid will still kick in for known ingredients
|
||||||
|
const md = '- cheddar 227g (8 oz)';
|
||||||
const html = render(md);
|
const html = render(md);
|
||||||
|
// "cheddar" is not in the density table, so no hybrid at all
|
||||||
expect(html).not.toContain('data-hybrid');
|
expect(html).not.toContain('data-hybrid');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -324,3 +327,195 @@ describe('hybrid weight/volume measurements', () => {
|
||||||
expect(html).toContain('data-measurement-type="weight"');
|
expect(html).toContain('data-measurement-type="weight"');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ─── Density Table ──────────────────────────────────────────
|
||||||
|
|
||||||
|
const { matchDensity } = require('../densities');
|
||||||
|
|
||||||
|
describe('matchDensity', () => {
|
||||||
|
it('returns density for known ingredient', () => {
|
||||||
|
expect(matchDensity('butter')).toBe(0.96);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is case-insensitive', () => {
|
||||||
|
expect(matchDensity('Butter')).toBe(0.96);
|
||||||
|
expect(matchDensity('FLOUR')).toBe(0.53);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null for unknown ingredient', () => {
|
||||||
|
expect(matchDensity('dragon fruit')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches longest keyword first (brown sugar vs sugar)', () => {
|
||||||
|
expect(matchDensity('brown sugar')).toBe(0.93);
|
||||||
|
expect(matchDensity('sugar')).toBe(0.85);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches longest keyword first (olive oil vs oil)', () => {
|
||||||
|
expect(matchDensity('olive oil')).toBe(0.92);
|
||||||
|
expect(matchDensity('vegetable oil')).toBe(0.92);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches ingredient within longer text', () => {
|
||||||
|
expect(matchDensity('unsalted butter')).toBe(0.96);
|
||||||
|
expect(matchDensity('all-purpose flour')).toBe(0.53);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null for empty/null input', () => {
|
||||||
|
expect(matchDensity('')).toBeNull();
|
||||||
|
expect(matchDensity(null)).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Density-Based Auto Hybrid Generation ───────────────────
|
||||||
|
|
||||||
|
function getWeightAttrs(html) {
|
||||||
|
const matches = [];
|
||||||
|
const re = /data-hybrid="true"[^>]*data-weight-default="([^"]*)"[^>]*data-weight-alt="([^"]*)"(?:[^>]*data-weight-scalable="([^"]*)")?/g;
|
||||||
|
let m;
|
||||||
|
while ((m = re.exec(html)) !== null) {
|
||||||
|
matches.push({
|
||||||
|
weightDefault: decodeHtmlEntities(m[1]),
|
||||||
|
weightAlt: decodeHtmlEntities(m[2]),
|
||||||
|
weightScalable: m[3] ? JSON.parse(decodeHtmlEntities(m[3])) : null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('density-based auto hybrid: weight -> volume', () => {
|
||||||
|
it('auto-generates volume data for weight measurement with known ingredient', () => {
|
||||||
|
const md = '- butter 227g';
|
||||||
|
const html = render(md);
|
||||||
|
expect(html).toContain('data-hybrid="true"');
|
||||||
|
expect(html).toContain('data-volume-default=');
|
||||||
|
expect(html).toContain('data-volume-alt=');
|
||||||
|
expect(html).toContain('data-volume-scalable=');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('generates correct volume from density (butter 227g)', () => {
|
||||||
|
const md = '- butter 227g';
|
||||||
|
const html = render(md);
|
||||||
|
const hybrids = getHybridAttrs(html);
|
||||||
|
expect(hybrids.length).toBe(1);
|
||||||
|
// 227g / 0.96 density = 236.46ml -> "236.5ml" metric
|
||||||
|
expect(hybrids[0].volumeDefault).toBe('236.5ml');
|
||||||
|
expect(hybrids[0].volumeScalable).toBeDefined();
|
||||||
|
expect(hybrids[0].volumeScalable.type).toBe('volume');
|
||||||
|
expect(hybrids[0].volumeScalable.base).toBeCloseTo(236.46, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('generates correct volume from density (flour 500g)', () => {
|
||||||
|
const md = '- all-purpose flour 500g';
|
||||||
|
const html = render(md);
|
||||||
|
const hybrids = getHybridAttrs(html);
|
||||||
|
expect(hybrids.length).toBe(1);
|
||||||
|
// 500g / 0.53 density = 943.4ml -> "943.4ml"
|
||||||
|
expect(hybrids[0].volumeDefault).toBe('943.4ml');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not generate hybrid for unknown ingredient weight', () => {
|
||||||
|
const md = '- mystery powder 100g';
|
||||||
|
const html = render(md);
|
||||||
|
expect(html).not.toContain('data-hybrid');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles range weight measurements with density', () => {
|
||||||
|
const md = '- butter 200-250g';
|
||||||
|
const html = render(md);
|
||||||
|
const hybrids = getHybridAttrs(html);
|
||||||
|
expect(hybrids.length).toBe(1);
|
||||||
|
// 200/0.96 = 208.3ml, 250/0.96 = 260.4ml
|
||||||
|
expect(hybrids[0].volumeDefault).toMatch(/208.*-.*260/);
|
||||||
|
expect(hybrids[0].volumeScalable.base).toHaveLength(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('density-based auto hybrid: volume -> weight', () => {
|
||||||
|
it('auto-generates weight data for volume measurement with known ingredient', () => {
|
||||||
|
const md = '- flour 2 cups';
|
||||||
|
const html = render(md);
|
||||||
|
expect(html).toContain('data-hybrid="true"');
|
||||||
|
expect(html).toContain('data-weight-default=');
|
||||||
|
expect(html).toContain('data-weight-alt=');
|
||||||
|
expect(html).toContain('data-weight-scalable=');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('generates correct weight from density (flour 2 cups)', () => {
|
||||||
|
const md = '- flour 2 cups';
|
||||||
|
const html = render(md);
|
||||||
|
const weightAttrs = getWeightAttrs(html);
|
||||||
|
expect(weightAttrs.length).toBe(1);
|
||||||
|
// 2 cups = 2 * 236.588ml = 473.176ml, * 0.53 density = 250.8g
|
||||||
|
expect(weightAttrs[0].weightDefault).toBe('250.8g');
|
||||||
|
expect(weightAttrs[0].weightScalable).toBeDefined();
|
||||||
|
expect(weightAttrs[0].weightScalable.type).toBe('weight');
|
||||||
|
expect(weightAttrs[0].weightScalable.base).toBeCloseTo(250.78, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('generates correct weight from density (milk 1 cup)', () => {
|
||||||
|
const md = '- milk 1 cup';
|
||||||
|
const html = render(md);
|
||||||
|
const weightAttrs = getWeightAttrs(html);
|
||||||
|
expect(weightAttrs.length).toBe(1);
|
||||||
|
// 1 cup = 236.588ml, * 1.03 density = 243.69g
|
||||||
|
expect(weightAttrs[0].weightDefault).toBe('243.7g');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not generate hybrid for unknown ingredient volume', () => {
|
||||||
|
const md = '- unicorn tears 1 cup';
|
||||||
|
const html = render(md);
|
||||||
|
expect(html).not.toContain('data-hybrid');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles range volume measurements with density', () => {
|
||||||
|
const md = '- sugar 1-2 cups';
|
||||||
|
const html = render(md);
|
||||||
|
const weightAttrs = getWeightAttrs(html);
|
||||||
|
expect(weightAttrs.length).toBe(1);
|
||||||
|
// 1 cup = 236.588ml * 0.85 = 201.1g, 2 cups = 473.176ml * 0.85 = 402.2g
|
||||||
|
expect(weightAttrs[0].weightDefault).toMatch(/201.*-.*402/);
|
||||||
|
expect(weightAttrs[0].weightScalable.base).toHaveLength(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('density vs manual hybrid precedence', () => {
|
||||||
|
it('manual hybrid annotation takes precedence over density table', () => {
|
||||||
|
const md = '- unsalted butter 80g (6 tablespoons)';
|
||||||
|
const html = render(md);
|
||||||
|
const hybrids = getHybridAttrs(html);
|
||||||
|
expect(hybrids.length).toBe(1);
|
||||||
|
// Should use the manual 6 tablespoons, not density-computed value
|
||||||
|
expect(hybrids[0].volumeDefault).toBe('88.7ml');
|
||||||
|
expect(hybrids[0].volumeAlt).toBe('6 tbsp');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('weight-to-weight alt does not trigger density hybrid (butter 227g (8 oz))', () => {
|
||||||
|
// "227g (8 oz)" is weight-to-weight alt, no manual hybrid volume data,
|
||||||
|
// but density should still auto-generate volume data
|
||||||
|
const md = '- butter 227g (8 oz)';
|
||||||
|
const html = render(md);
|
||||||
|
// getHybridVolumeData returns null for weight-to-weight, so density kicks in
|
||||||
|
expect(html).toContain('data-hybrid="true"');
|
||||||
|
const hybrids = getHybridAttrs(html);
|
||||||
|
expect(hybrids.length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('density with approximate values', () => {
|
||||||
|
it('preserves approximate prefix in density-generated volume', () => {
|
||||||
|
const md = '- butter ~200g';
|
||||||
|
const html = render(md);
|
||||||
|
const hybrids = getHybridAttrs(html);
|
||||||
|
expect(hybrids.length).toBe(1);
|
||||||
|
expect(hybrids[0].volumeDefault).toMatch(/^~/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves approximate prefix in density-generated weight', () => {
|
||||||
|
const md = '- flour ~2 cups';
|
||||||
|
const html = render(md);
|
||||||
|
const weightAttrs = getWeightAttrs(html);
|
||||||
|
expect(weightAttrs.length).toBe(1);
|
||||||
|
expect(weightAttrs[0].weightDefault).toMatch(/^~/);
|
||||||
|
});
|
||||||
|
});
|
||||||
58
lib/measurements/densities.js
Normal file
58
lib/measurements/densities.js
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
/**
|
||||||
|
* Ingredient density table for automatic weight/volume conversion.
|
||||||
|
*
|
||||||
|
* Each entry has keywords (ingredient names) and a density in g/ml.
|
||||||
|
* Conversion:
|
||||||
|
* Weight -> Volume: ml = grams / density
|
||||||
|
* Volume -> Weight: grams = ml * density
|
||||||
|
*
|
||||||
|
* Entries are sorted longest-keyword-first so that "brown sugar" matches
|
||||||
|
* before "sugar", "olive oil" before "oil", etc.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const DENSITIES = [
|
||||||
|
{ keywords: ['all-purpose flour', 'all purpose flour', 'ap flour'], density: 0.53 },
|
||||||
|
{ keywords: ['powdered sugar', 'confectioners sugar', 'icing sugar'], density: 0.50 },
|
||||||
|
{ keywords: ['vanilla extract'], density: 1.03 },
|
||||||
|
{ keywords: ['baking powder'], density: 0.90 },
|
||||||
|
{ keywords: ['cocoa powder'], density: 0.43 },
|
||||||
|
{ keywords: ['brown sugar'], density: 0.93 },
|
||||||
|
{ keywords: ['maple syrup'], density: 1.32 },
|
||||||
|
{ keywords: ['baking soda', 'bicarbonate'], density: 1.10 },
|
||||||
|
{ keywords: ['bread flour'], density: 0.55 },
|
||||||
|
{ keywords: ['sour cream'], density: 1.05 },
|
||||||
|
{ keywords: ['olive oil'], density: 0.92 },
|
||||||
|
{ keywords: ['cornstarch', 'corn starch'], density: 0.54 },
|
||||||
|
{ keywords: ['butter'], density: 0.96 },
|
||||||
|
{ keywords: ['cream'], density: 1.01 },
|
||||||
|
{ keywords: ['flour'], density: 0.53 },
|
||||||
|
{ keywords: ['sugar'], density: 0.85 },
|
||||||
|
{ keywords: ['honey'], density: 1.42 },
|
||||||
|
{ keywords: ['milk'], density: 1.03 },
|
||||||
|
{ keywords: ['water'], density: 1.0 },
|
||||||
|
{ keywords: ['salt'], density: 1.22 },
|
||||||
|
{ keywords: ['rice'], density: 0.80 },
|
||||||
|
{ keywords: ['oil'], density: 0.92 },
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if text contains a known ingredient and return its density.
|
||||||
|
* Lowercases the text and checks keywords longest-first.
|
||||||
|
*
|
||||||
|
* @param {string} text - text preceding the measurement (ingredient context)
|
||||||
|
* @returns {number|null} density in g/ml, or null if no match
|
||||||
|
*/
|
||||||
|
function matchDensity(text) {
|
||||||
|
if (!text) return null;
|
||||||
|
const lower = text.toLowerCase().trim();
|
||||||
|
for (const entry of DENSITIES) {
|
||||||
|
for (const keyword of entry.keywords) {
|
||||||
|
if (lower.includes(keyword)) {
|
||||||
|
return entry.density;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { DENSITIES, matchDensity };
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { findAllMeasurements, unitType } = require('./matcher');
|
const { findAllMeasurements, unitType } = require('./matcher');
|
||||||
|
const { matchDensity } = require('./densities');
|
||||||
|
|
||||||
// ─── Number Formatting ──────────────────────────────────
|
// ─── Number Formatting ──────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -463,6 +464,116 @@ function formatMeasurementText(amount, unit, approximate) {
|
||||||
return prefix + formatValueUnit(amount.value, unit);
|
return prefix + formatValueUnit(amount.value, unit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Density-Based Auto Hybrid Generation ───────────────
|
||||||
|
|
||||||
|
function generateDensityVolumeData(measurement, density) {
|
||||||
|
const { amount, unit, approximate } = measurement;
|
||||||
|
const prefix = approximate ? '~' : '';
|
||||||
|
|
||||||
|
if (amount.min !== undefined) {
|
||||||
|
const mlMin = toBaseGrams(amount.min.value, unit) / density;
|
||||||
|
const mlMax = toBaseGrams(amount.max.value, unit) / density;
|
||||||
|
const mMin = smartMetricVolume(mlMin);
|
||||||
|
const mMax = smartMetricVolume(mlMax);
|
||||||
|
const iMin = smartImperialVolume(mlMin);
|
||||||
|
const iMax = smartImperialVolume(mlMax);
|
||||||
|
const rPrefix = (approximate || amount.min.approximate) ? '~' : '';
|
||||||
|
|
||||||
|
let volumeDefault, volumeAlt;
|
||||||
|
if (mMin.unit === mMax.unit) {
|
||||||
|
const space = NO_SPACE_UNITS.has(mMin.unit) ? '' : ' ';
|
||||||
|
volumeDefault = rPrefix + formatNumber(mMin.value) + '-' + formatNumber(mMax.value) + space + unitLabel(mMin.unit, mMax.value !== 1);
|
||||||
|
} else {
|
||||||
|
volumeDefault = rPrefix + formatValueUnit(mMin.value, mMin.unit) + '-' + formatValueUnit(mMax.value, mMax.unit);
|
||||||
|
}
|
||||||
|
if (iMin.unit === iMax.unit) {
|
||||||
|
const space = NO_SPACE_UNITS.has(iMin.unit) ? '' : ' ';
|
||||||
|
volumeAlt = rPrefix + formatNumber(iMin.value) + '-' + formatNumber(iMax.value) + space + unitLabel(iMin.unit, iMax.value !== 1);
|
||||||
|
} else {
|
||||||
|
volumeAlt = rPrefix + formatValueUnit(iMin.value, iMin.unit) + '-' + formatValueUnit(iMax.value, iMax.unit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
default: volumeDefault,
|
||||||
|
alt: volumeAlt,
|
||||||
|
scalable: {
|
||||||
|
base: [mlMin, mlMax],
|
||||||
|
type: 'volume',
|
||||||
|
approx: approximate || amount.min.approximate || false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const grams = toBaseGrams(amount.value, unit);
|
||||||
|
const ml = grams / density;
|
||||||
|
const metric = smartMetricVolume(ml);
|
||||||
|
const imperial = smartImperialVolume(ml);
|
||||||
|
|
||||||
|
return {
|
||||||
|
default: prefix + formatValueUnit(metric.value, metric.unit),
|
||||||
|
alt: prefix + formatValueUnit(imperial.value, imperial.unit),
|
||||||
|
scalable: {
|
||||||
|
base: ml,
|
||||||
|
type: 'volume',
|
||||||
|
approx: approximate || false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateDensityWeightData(measurement, density) {
|
||||||
|
const { amount, unit, approximate } = measurement;
|
||||||
|
const prefix = approximate ? '~' : '';
|
||||||
|
|
||||||
|
if (amount.min !== undefined) {
|
||||||
|
const gMin = toBaseMl(amount.min.value, unit) * density;
|
||||||
|
const gMax = toBaseMl(amount.max.value, unit) * density;
|
||||||
|
const mMin = smartMetricWeight(gMin);
|
||||||
|
const mMax = smartMetricWeight(gMax);
|
||||||
|
const iMin = smartImperialWeight(gMin / 28.3495);
|
||||||
|
const iMax = smartImperialWeight(gMax / 28.3495);
|
||||||
|
const rPrefix = (approximate || amount.min.approximate) ? '~' : '';
|
||||||
|
|
||||||
|
let weightDefault, weightAlt;
|
||||||
|
if (mMin.unit === mMax.unit) {
|
||||||
|
const space = NO_SPACE_UNITS.has(mMin.unit) ? '' : ' ';
|
||||||
|
weightDefault = rPrefix + formatNumber(mMin.value) + '-' + formatNumber(mMax.value) + space + unitLabel(mMin.unit, mMax.value !== 1);
|
||||||
|
} else {
|
||||||
|
weightDefault = rPrefix + formatValueUnit(mMin.value, mMin.unit) + '-' + formatValueUnit(mMax.value, mMax.unit);
|
||||||
|
}
|
||||||
|
if (iMin.unit === iMax.unit) {
|
||||||
|
const space = NO_SPACE_UNITS.has(iMin.unit) ? '' : ' ';
|
||||||
|
weightAlt = rPrefix + formatNumber(iMin.value) + '-' + formatNumber(iMax.value) + space + unitLabel(iMin.unit, iMax.value !== 1);
|
||||||
|
} else {
|
||||||
|
weightAlt = rPrefix + formatValueUnit(iMin.value, iMin.unit) + '-' + formatValueUnit(iMax.value, iMax.unit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
default: weightDefault,
|
||||||
|
alt: weightAlt,
|
||||||
|
scalable: {
|
||||||
|
base: [gMin, gMax],
|
||||||
|
type: 'weight',
|
||||||
|
approx: approximate || amount.min.approximate || false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const ml = toBaseMl(amount.value, unit);
|
||||||
|
const grams = ml * density;
|
||||||
|
const metric = smartMetricWeight(grams);
|
||||||
|
const imperial = smartImperialWeight(grams / 28.3495);
|
||||||
|
|
||||||
|
return {
|
||||||
|
default: prefix + formatValueUnit(metric.value, metric.unit),
|
||||||
|
alt: prefix + formatValueUnit(imperial.value, imperial.unit),
|
||||||
|
scalable: {
|
||||||
|
base: grams,
|
||||||
|
type: 'weight',
|
||||||
|
approx: approximate || false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Serving Vessel Detection ───────────────────────────
|
// ─── Serving Vessel Detection ───────────────────────────
|
||||||
|
|
||||||
// Serving vessel keywords — items in the Tools section containing these words
|
// Serving vessel keywords — items in the Tools section containing these words
|
||||||
|
|
@ -534,6 +645,26 @@ function replaceMeasurementsInToken(token, Token, inToolsSection) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Density-based auto hybrid: generate the OTHER type's data from ingredient density
|
||||||
|
if (!hybridData && (m.type === 'weight' || m.type === 'volume')) {
|
||||||
|
const textBefore = text.slice(0, m.index);
|
||||||
|
const density = matchDensity(textBefore);
|
||||||
|
if (density) {
|
||||||
|
open.attrSet('data-hybrid', 'true');
|
||||||
|
if (m.type === 'weight') {
|
||||||
|
const volData = generateDensityVolumeData(m, density);
|
||||||
|
open.attrSet('data-volume-default', volData.default);
|
||||||
|
open.attrSet('data-volume-alt', volData.alt);
|
||||||
|
open.attrSet('data-volume-scalable', JSON.stringify(volData.scalable));
|
||||||
|
} else {
|
||||||
|
const wtData = generateDensityWeightData(m, density);
|
||||||
|
open.attrSet('data-weight-default', wtData.default);
|
||||||
|
open.attrSet('data-weight-alt', wtData.alt);
|
||||||
|
open.attrSet('data-weight-scalable', JSON.stringify(wtData.scalable));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
newTokens.push(open);
|
newTokens.push(open);
|
||||||
|
|
||||||
// Display metric-normalized text by default
|
// Display metric-normalized text by default
|
||||||
|
|
@ -621,3 +752,5 @@ module.exports.collapsedTime = collapsedTime;
|
||||||
module.exports.expandedTime = expandedTime;
|
module.exports.expandedTime = expandedTime;
|
||||||
module.exports.getHybridVolumeData = getHybridVolumeData;
|
module.exports.getHybridVolumeData = getHybridVolumeData;
|
||||||
module.exports.generateHybridVolumeTexts = generateHybridVolumeTexts;
|
module.exports.generateHybridVolumeTexts = generateHybridVolumeTexts;
|
||||||
|
module.exports.generateDensityVolumeData = generateDensityVolumeData;
|
||||||
|
module.exports.generateDensityWeightData = generateDensityWeightData;
|
||||||
|
|
@ -10,9 +10,9 @@ title: Japanese Curry Soup
|
||||||
- [ ] carrots 4x
|
- [ ] carrots 4x
|
||||||
- [ ] lion's mane
|
- [ ] lion's mane
|
||||||
- [ ] vegetable stock 1 jar
|
- [ ] vegetable stock 1 jar
|
||||||
- [ ] 5 cups water
|
- [ ] water 5 cups
|
||||||
- [ ] heavy cream [^1]
|
- [ ] heavy cream [^1]
|
||||||
- [ ] 4 japanese curry bricks
|
- [ ] japanese curry bricks 4
|
||||||
- [ ] bay leaf
|
- [ ] bay leaf
|
||||||
- [ ] red pepper
|
- [ ] red pepper
|
||||||
## Tools
|
## Tools
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue