#| https://github.com/cooklang/spec/blob/main/EBNF.md |# (define-record-type (make-amount quantity unit) amount? (quantity amount-quantity) (unit amount-unit)) (define-record-type (make-component name amount) component? (name component-name) (amount component-amount)) (define-record-type (make-ingredient name amount) ingredient? (name ingredient-name) (amount ingredient-amount)) (define (component->ingredient comp) (make-ingredient (component-name comp) (component-amount comp))) (define-record-type (make-cookware name amount) cookware? (name cookware-name) (amount cookware-amount)) (define (component->cookware comp) (make-cookware (component-name comp) (component-amount comp))) (define-record-type (make-timer name amount) timer? (name timer-name) (amount timer-amount)) (define (component->timer comp) (make-timer (component-name comp) (component-amount comp))) (define-record-type (make-metadata-line key value) metadata-line? (key metadata-line-key) (value metadata-line-value)) (define-record-type (make-step elements) step? (elements step-elements)) (define-record-type (make-comment text) comment? (text comment-text)) (define-record-type (make-recipe metadata body) recipe? (metadata recipe-metadata) (body recipe-body)) (define (metadata-line-list->hash-table meta-lines) (let ((metadata (make-hash-table))) (for-each (lambda (line) (hash-table-set! metadata (metadata-line-key line) (metadata-line-value line))) meta-lines) metadata)) (define (merge-step-strings lis) (reduce-right (lambda (el acc) (cond ((char? el) (if (and (< 0 (length acc)) (string? (car acc))) (cons (string-append (string el) (car acc)) (cdr acc)) (append (list (string el)) acc))) (else (append (list el) acc)))) '() lis)) (define whitespace-chars (char-set #\space)) (define newline-chars (char-set #\x000A #\x000D #\x0085 #\x2028 #\x2029)) (define punctuation-chars (char-set #\. #\{ #\})) ;; TODO: do it right (define word-chars (char-set-difference char-set:full punctuation-chars newline-chars whitespace-chars)) (define any-text-chars (char-set-difference char-set:full newline-chars)) (define text-chars (char-set-difference any-text-chars (char-set #\@ #\# #\~))) (define unit-chars (char-set-difference text-chars (char-set #\}))) (define component-chars (char-set-difference text-chars (char-set #\{ #\}))) (define component-word-chars (char-set-difference component-chars newline-chars whitespace-chars)) (define quantity-chars (char-set-difference text-chars (char-set #\} #\%))) (define metadata-chars (char-set-difference text-chars (char-set #\:))) (define-grammar cook (nl ((: ,newline-chars))) (whitespace ((=> x (+ ,whitespace-chars)) (list->string x))) (any-text-item ((: (=> c (+ ,any-text-chars))) (list->string c))) (comment ((: "--" (=> c (+ ,any-text-chars)) (or eos ,nl)) (make-comment (list->string c))) ((: "[-" (=> c (* any)) "-]") (make-comment (list->string c)))) (word ((=> w (+ ,word-chars)) (list->string w))) (unit ((=> u (+ ,unit-chars)) (list->string u))) (quantity ((=> q (+ ,quantity-chars)) (string->number (list->string q)))) (meta-key ((=> k (+ ,metadata-chars)) (list->string k))) (amount ((: (=> q ,quantity) "%" (=> u ,unit)) (make-amount q u)) ((=> q ,quantity) (make-amount q #f))) (amount-block ((: "{" (? (=> a ,amount)) "}") a)) (no-word-component ((: (=> a ,amount-block)) (make-component #f a))) (component-word ((=> w (+ ,component-word-chars)) (list->string w))) (component ((: (=> cw ,component-word) (? (: (=> cc (* ,component-chars)) (=> a ,amount-block)))) (make-component (string-append cw (if cc (list->string cc) "")) a))) (timer ((: "~" (=> c (or ,component ,no-word-component))) (component->timer c))) (cookware ((: "#" (=> c ,component)) (component->cookware c))) (ingredient ((: "@" (=> c ,component)) (component->ingredient c))) (text-item ((=> t (+ (or ,comment ,ingredient ,cookware ,timer ,any-text-chars))) t)) (step-line ((: (? ,whitespace) (=> s (+ ,text-item)) (=> n (? ,nl))) (append (concatenate s) (if n '(#\space) '())))) (step ((: (=> s ,step-line) (=> ns (* ,step-line))) (make-step (merge-step-strings (concatenate (append (list s) ns)))))) (metadata ((: bol ">>" (* ,whitespace) (=> k ,meta-key) (* ,whitespace) ":" (* ,whitespace) (=> v ,any-text-item) (* ,whitespace) eol) (make-metadata-line k v))) (element ((or ,metadata ,comment ,step))) (recipe ((=> els (* (: ,element ,(parse-ignore (parse-seq (parse-repeat nl) (parse-optional parse-end)))))) (concatenate els)))) (define (parse-cook str) (let ((lis (parse-fully recipe str))) (let-values (((meta-lines body) (partition metadata-line? lis))) (make-recipe (metadata-line-list->hash-table meta-lines) body))))