1- import type { MetadataExtract , Metadata } from "./types" ;
2- import { metadataRegex } from "./regex" ;
1+ import type {
2+ MetadataExtract ,
3+ Metadata ,
4+ FixedValue ,
5+ Range ,
6+ TextValue ,
7+ DecimalValue ,
8+ FractionValue ,
9+ } from "./types" ;
10+ import { metadataRegex , rangeRegex , numberLikeRegex } from "./regex" ;
311import { Section as SectionObject } from "./classes/section" ;
412import type { Ingredient , Note , Step , Cookware } from "./types" ;
5- import { addQuantities } from "./units" ;
13+ import {
14+ addQuantities ,
15+ CannotAddTextValueError ,
16+ IncompatibleUnitsError ,
17+ Quantity ,
18+ } from "./units" ;
619
720/**
821 * Finds an item in a list or adds it if not present, then returns its index.
@@ -88,15 +101,28 @@ export function findAndUpsertIngredient(
88101 // Ingredient already exists, update it
89102 const existingIngredient = ingredients [ index ] ! ;
90103 if ( quantity !== undefined ) {
91- const currentQuantity = {
92- value : existingIngredient . quantity ?? 0 ,
104+ const currentQuantity : Quantity = {
105+ value : existingIngredient . quantity ?? {
106+ type : "fixed" ,
107+ value : { type : "decimal" , value : 0 } ,
108+ } ,
93109 unit : existingIngredient . unit ?? "" ,
94110 } ;
95111 const newQuantity = { value : quantity , unit : unit ?? "" } ;
96112
97- const total = addQuantities ( currentQuantity , newQuantity ) ;
98- existingIngredient . quantity = total . value ;
99- existingIngredient . unit = total . unit || undefined ;
113+ try {
114+ const total = addQuantities ( currentQuantity , newQuantity ) ;
115+ existingIngredient . quantity = total . value ;
116+ existingIngredient . unit = total . unit || undefined ;
117+ } catch ( e ) {
118+ if (
119+ e instanceof IncompatibleUnitsError ||
120+ e instanceof CannotAddTextValueError
121+ ) {
122+ // Addition not possible, so add as a new ingredient.
123+ return ingredients . push ( newIngredient ) - 1 ;
124+ }
125+ }
100126 }
101127 return index ;
102128 }
@@ -129,18 +155,52 @@ export function findAndUpsertCookware(
129155 return cookware . push ( newCookware ) - 1 ;
130156}
131157
132- export function parseNumber ( input_str : string ) : number {
133- const clean_str = String ( input_str ) . replace ( "," , "." ) ;
134- if ( ! clean_str . startsWith ( "/" ) && clean_str . includes ( "/" ) ) {
135- const [ num , den ] = clean_str . split ( "/" ) . map ( Number ) ;
136- return num ! / den ! ;
158+ // Parser when we know the input is either a number-like value
159+ const parseFixedValue = (
160+ input_str : string ,
161+ ) : TextValue | DecimalValue | FractionValue => {
162+ if ( ! numberLikeRegex . test ( input_str ) ) {
163+ return { type : "text" , value : input_str } ;
137164 }
138- return Number ( clean_str ) ;
165+
166+ // After this we know that s is either a fraction or a decimal value
167+ const s = input_str . trim ( ) . replace ( "," , "." ) ;
168+
169+ // fraction
170+ if ( s . includes ( "/" ) ) {
171+ const parts = s . split ( "/" ) ;
172+
173+ const num = Number ( parts [ 0 ] ) ;
174+ const den = Number ( parts [ 1 ] ) ;
175+
176+ return { type : "fraction" , num, den } ;
177+ }
178+
179+ // decimal
180+ return { type : "decimal" , value : Number ( s ) } ;
181+ } ;
182+
183+ export function parseQuantityInput ( input_str : string ) : FixedValue | Range {
184+ const clean_str = String ( input_str ) . trim ( ) ;
185+
186+ if ( rangeRegex . test ( clean_str ) ) {
187+ const range_parts = clean_str . split ( "-" ) ;
188+ // As we've tested for it, we know that we have Number-like Quantities to parse
189+ const min = parseFixedValue ( range_parts [ 0 ] ! . trim ( ) ) as
190+ | DecimalValue
191+ | FractionValue ;
192+ const max = parseFixedValue ( range_parts [ 1 ] ! . trim ( ) ) as
193+ | DecimalValue
194+ | FractionValue ;
195+ return { type : "range" , min, max } ;
196+ }
197+
198+ return { type : "fixed" , value : parseFixedValue ( clean_str ) } ;
139199}
140200
141201export function parseSimpleMetaVar ( content : string , varName : string ) {
142202 const varMatch = content . match (
143- new RegExp ( `^${ varName } :\\ s*(.*(?:\\ r?\\n\ \s+.*)*)+` , "m" ) ,
203+ new RegExp ( `^${ varName } :\s*(.*(?:\r?\n \s+.*)*)+` , "m" ) ,
144204 ) ;
145205 return varMatch
146206 ? varMatch [ 1 ] ?. trim ( ) . replace ( / \s * \r ? \n \s + / g, " " )
@@ -149,7 +209,7 @@ export function parseSimpleMetaVar(content: string, varName: string) {
149209
150210export function parseScalingMetaVar ( content : string , varName : string ) {
151211 const varMatch = content . match (
152- new RegExp ( `^${ varName } :[\\ t ]*(([^,\ \n]*),? ?(?:.*)?)` , "m" ) ,
212+ new RegExp ( `^${ varName } :[\t ]*(([^,\n]*),? ?(?:.*)?)` , "m" ) ,
153213 ) ;
154214 if ( ! varMatch ) return undefined ;
155215 if ( isNaN ( Number ( varMatch [ 2 ] ?. trim ( ) ) ) ) {
@@ -161,10 +221,7 @@ export function parseScalingMetaVar(content: string, varName: string) {
161221export function parseListMetaVar ( content : string , varName : string ) {
162222 // Handle both inline and YAML-style tags
163223 const listMatch = content . match (
164- new RegExp (
165- `^${ varName } :\\s*(?:\\[([^\\]]*)\\]|((?:\\r?\\n\\s*-\\s*.+)+))` ,
166- "m" ,
167- ) ,
224+ new RegExp ( `^${ varName } :\s*(?:[([^]]*)]|((?:\r?\n\s*-\s*.+)+))` , "m" ) ,
168225 ) ;
169226 if ( ! listMatch ) return undefined ;
170227
0 commit comments