* write that as yaml file next to the other outputs * extract lyrics * makes chordpro export work in songbooks * more metadata for chordpro export
260 lines
11 KiB
Scheme
260 lines
11 KiB
Scheme
(use-modules (ice-9 rdelim) (ice-9 regex) (ice-9 receive) (srfi srfi-1))
|
|
|
|
;; YAML-Parser: liest eine Teilmenge von YAML (Block-Syntax, keine
|
|
;; Flow-Collections, keine Block-Skalare) in verschachtelte
|
|
;; alist/list-Strukturen. Gegenstück zu yaml_writer.scm.
|
|
;;
|
|
;; Ergebnisformat:
|
|
;; - Mapping -> alist mit String-Keys, Reihenfolge wie in der Datei
|
|
;; - Liste -> Liste (auch als "- key: value"-Inline-Mappings)
|
|
;; - Skalare -> String, Zahl, #t/#f, '() (null / {} / [])
|
|
;;
|
|
;; Skalar-Interpretation:
|
|
;; - unquoted und 'single-quoted' Werte werden interpretiert
|
|
;; (Zahl, true/false/null). Das Interpretieren von single-quoted
|
|
;; Werten ist Kompatibilitätsverhalten zum alten Parser:
|
|
;; authors.yml notiert Zahlen als '1898'.
|
|
;; - "double-quoted" Werte bleiben immer Strings; \\, \" und \n
|
|
;; werden entescaped (so schreibt sie der yaml_writer).
|
|
;; - Kommentare (#) werden nur außerhalb von Quotes entfernt und nur,
|
|
;; wenn ihnen ein Leerzeichen oder der Zeilenanfang vorausgeht.
|
|
|
|
(define (yml-file->scm filename)
|
|
|
|
;; --- Zeilen einlesen ------------------------------------------------
|
|
|
|
;; Anzahl führender Leerzeichen
|
|
(define (line-indent line)
|
|
(let loop ((i 0))
|
|
(if (and (< i (string-length line))
|
|
(char=? (string-ref line i) #\space))
|
|
(loop (+ i 1))
|
|
i)))
|
|
|
|
;; Liefert Items (indent . inhalt); Leerzeilen, reine Kommentarzeilen
|
|
;; und Dokumentmarker (--- / ...) werden übersprungen.
|
|
(define (read-items filename)
|
|
(call-with-input-file filename
|
|
(lambda (port)
|
|
(let loop ((items '()))
|
|
(let ((line (read-line port)))
|
|
(if (eof-object? line)
|
|
(reverse items)
|
|
(let* ((line (if (string-suffix? "\r" line)
|
|
(string-drop-right line 1)
|
|
line))
|
|
(content (string-trim-both line)))
|
|
(if (or (string-null? content)
|
|
(string-prefix? "#" content)
|
|
(string=? content "---")
|
|
(string=? content "..."))
|
|
(loop items)
|
|
(loop (cons (cons (line-indent line) content)
|
|
items))))))))))
|
|
|
|
(define (item-indent item) (car item))
|
|
(define (item-content item) (cdr item))
|
|
|
|
;; --- Skalare ----------------------------------------------------------
|
|
|
|
;; Position des schließenden Double-Quotes ab start (oder #f);
|
|
;; Backslash escaped das Folgezeichen.
|
|
(define (closing-double-quote s start)
|
|
(let loop ((i start))
|
|
(cond ((>= i (string-length s)) #f)
|
|
((char=? (string-ref s i) #\\) (loop (+ i 2)))
|
|
((char=? (string-ref s i) #\") i)
|
|
(else (loop (+ i 1))))))
|
|
|
|
;; Position des schließenden Single-Quotes ab start (oder #f);
|
|
;; '' ist ein escaptes Quote.
|
|
(define (closing-single-quote s start)
|
|
(let loop ((i start))
|
|
(cond ((>= i (string-length s)) #f)
|
|
((char=? (string-ref s i) #\')
|
|
(if (and (< (+ i 1) (string-length s))
|
|
(char=? (string-ref s (+ i 1)) #\'))
|
|
(loop (+ i 2))
|
|
i))
|
|
(else (loop (+ i 1))))))
|
|
|
|
;; \\ -> \, \" -> ", \n -> Zeilenumbruch
|
|
(define (unescape-double s)
|
|
(let loop ((chars (string->list s)) (acc '()))
|
|
(cond ((null? chars) (list->string (reverse acc)))
|
|
((and (char=? (car chars) #\\) (pair? (cdr chars)))
|
|
(loop (cddr chars)
|
|
(cons (if (char=? (cadr chars) #\n)
|
|
#\newline
|
|
(cadr chars))
|
|
acc)))
|
|
(else (loop (cdr chars) (cons (car chars) acc))))))
|
|
|
|
;; '' -> '
|
|
(define (unescape-single s)
|
|
(regexp-substitute/global #f "''" s 'pre "'" 'post))
|
|
|
|
;; Kommentar in einem unquoted Wert entfernen:
|
|
;; # zählt nur am Anfang oder nach Leerzeichen
|
|
(define (strip-plain-comment s)
|
|
(let loop ((i 0))
|
|
(cond ((>= i (string-length s)) s)
|
|
((and (char=? (string-ref s i) #\#)
|
|
(or (zero? i)
|
|
(char-whitespace? (string-ref s (- i 1)))))
|
|
(string-take s i))
|
|
(else (loop (+ i 1))))))
|
|
|
|
;; Unquoted/single-quoted Werte interpretieren
|
|
(define (interpret-plain s)
|
|
(cond
|
|
((member s '("{}" "[]" "null")) '())
|
|
((string=? s "true") #t)
|
|
((string=? s "false") #f)
|
|
((string-match "^-?[0-9]+(\\.[0-9]+)?$" s) (string->number s))
|
|
(else s)))
|
|
|
|
;; Skalar parsen; Rest hinter einem schließenden Quote wird ignoriert
|
|
;; (darf nur ein Kommentar sein)
|
|
(define (parse-scalar str)
|
|
(let ((s (string-trim-both str)))
|
|
(cond
|
|
((string-null? s) "")
|
|
((char=? (string-ref s 0) #\")
|
|
(let ((end (closing-double-quote s 1)))
|
|
(if end
|
|
(unescape-double (substring s 1 end))
|
|
(string-trim-right (strip-plain-comment s)))))
|
|
((char=? (string-ref s 0) #\')
|
|
(let ((end (closing-single-quote s 1)))
|
|
(if end
|
|
(interpret-plain (unescape-single (substring s 1 end)))
|
|
(string-trim-right (strip-plain-comment s)))))
|
|
(else
|
|
(interpret-plain (string-trim-right (strip-plain-comment s)))))))
|
|
|
|
;; --- Struktur ----------------------------------------------------------
|
|
|
|
;; Erste Key-Trennstelle: ":" am Zeilenende oder gefolgt von Leerzeichen.
|
|
;; Beginnt die Zeile mit einem gequoteten Key (oder Skalar), zaehlt ein
|
|
;; ":" innerhalb der Quotes nicht als Trennstelle.
|
|
(define (key-split-index s)
|
|
(let ((start (if (string-null? s)
|
|
0
|
|
(case (string-ref s 0)
|
|
((#\") (let ((end (closing-double-quote s 1)))
|
|
(if end (+ end 1) 0)))
|
|
((#\') (let ((end (closing-single-quote s 1)))
|
|
(if end (+ end 1) 0)))
|
|
(else 0)))))
|
|
(let loop ((i start))
|
|
(cond ((>= i (string-length s)) #f)
|
|
((and (char=? (string-ref s i) #\:)
|
|
(or (= i (- (string-length s) 1))
|
|
(char=? (string-ref s (+ i 1)) #\space)))
|
|
i)
|
|
(else (loop (+ i 1)))))))
|
|
|
|
;; Key interpretieren: gequotete Keys werden entquotet (Strings bleiben
|
|
;; Strings), unquoted Keys bleiben unveraendert
|
|
(define (parse-key s)
|
|
(let ((k (string-trim-both s)))
|
|
(cond
|
|
((string-null? k) k)
|
|
((char=? (string-ref k 0) #\")
|
|
(let ((end (closing-double-quote k 1)))
|
|
(if end (unescape-double (substring k 1 end)) k)))
|
|
((char=? (string-ref k 0) #\')
|
|
(let ((end (closing-single-quote k 1)))
|
|
(if end (unescape-single (substring k 1 end)) k)))
|
|
(else k))))
|
|
|
|
(define (dash-line? content)
|
|
(or (string=? content "-") (string-prefix? "- " content)))
|
|
|
|
;; Wert-String hinter "key:" bzw. "-": reiner Kommentar zählt als leer
|
|
(define (effective-value str)
|
|
(let ((s (string-trim-both str)))
|
|
(if (string-prefix? "#" s) "" s)))
|
|
|
|
;; Einen Block von Items parsen; das erste Item bestimmt die Blockart
|
|
(define (parse-block items)
|
|
(cond
|
|
((null? items) '())
|
|
((dash-line? (item-content (car items))) (parse-list items))
|
|
;; einzelne Zeile ohne Key: Skalar (z.B. eine Datei, die nur {} enthält)
|
|
((and (null? (cdr items))
|
|
(not (key-split-index (item-content (car items)))))
|
|
(parse-scalar (item-content (car items))))
|
|
(else (parse-map items))))
|
|
|
|
;; Mapping: alle Items auf map-indent sind Keys, tiefer eingerückte
|
|
;; Items gehören zum jeweils vorangehenden Key
|
|
(define (parse-map items)
|
|
(let ((map-indent (item-indent (car items))))
|
|
(let loop ((items items) (result '()))
|
|
(if (null? items)
|
|
(reverse result)
|
|
(let* ((content (item-content (car items)))
|
|
(split (key-split-index content)))
|
|
(if (not split)
|
|
(begin
|
|
(format (current-error-port)
|
|
"YAML-Syntaxfehler: Ungültige Zeile: ~a\n" content)
|
|
(loop (cdr items) result))
|
|
(let ((key (parse-key (string-take content split)))
|
|
(value-str (effective-value
|
|
(string-drop content (+ split 1)))))
|
|
(if (string-null? value-str)
|
|
;; Wert steht im eingerückten Block darunter
|
|
(receive (children rest)
|
|
(span (lambda (it) (> (item-indent it) map-indent))
|
|
(cdr items))
|
|
(loop rest
|
|
(cons (cons key (parse-block children))
|
|
result)))
|
|
(loop (cdr items)
|
|
(cons (cons key (parse-scalar value-str))
|
|
result))))))))))
|
|
|
|
;; Liste: "- wert", "- key: value" (Inline-Mapping) oder "-" mit
|
|
;; eingerücktem Block darunter
|
|
(define (parse-list items)
|
|
(let ((list-indent (item-indent (car items))))
|
|
(let loop ((items items) (result '()))
|
|
(if (null? items)
|
|
(reverse result)
|
|
(let ((content (item-content (car items))))
|
|
(if (not (dash-line? content))
|
|
(begin
|
|
(format (current-error-port)
|
|
"YAML-Syntaxfehler: Ungültiges Listenelement: ~a\n"
|
|
content)
|
|
(loop (cdr items) result))
|
|
(receive (children rest)
|
|
(span (lambda (it) (> (item-indent it) list-indent))
|
|
(cdr items))
|
|
(let* ((after-dash (string-drop content 1))
|
|
(inline (effective-value after-dash)))
|
|
(cond
|
|
((and (string-null? inline) (null? children))
|
|
(loop rest (cons '() result)))
|
|
((string-null? inline)
|
|
(loop rest (cons (parse-block children) result)))
|
|
(else
|
|
;; Inline-Inhalt: als synthetisches Item mit der
|
|
;; Spalte des Inhalts vor die Kinder stellen, so
|
|
;; funktioniert auch "- key: value" mit weiteren
|
|
;; Keys auf den Folgezeilen
|
|
(let* ((offset (- (string-length content)
|
|
(string-length
|
|
(string-trim after-dash))))
|
|
(synth (cons (+ list-indent offset) inline)))
|
|
(loop rest
|
|
(cons (parse-block (cons synth children))
|
|
result)))))))))))))
|
|
|
|
(parse-block (read-items filename)))
|
|
|
|
(define (parse-yml-file filename) (resolve-inherits (yml-file->scm filename)))
|