Use chapters for digital toc

* introduce \songsChapter and \markuppage
* have nested chapters in PDF toc
This commit is contained in:
tux
2026-07-05 14:09:44 +02:00
parent 50e08220c4
commit 7076b16b63
3 changed files with 145 additions and 56 deletions
@@ -26,25 +26,49 @@
#(define pdf-encode (@@ (lily framework-ps) pdf-encode)) #(define pdf-encode (@@ (lily framework-ps) pdf-encode))
% PDF tags % PDF tags
#(define-markup-command (title-to-pdf-toc layout props title) (string?) % Erzeugt ein PDF-Lesezeichen (Outline-Eintrag) auf der aktuellen Seite.
% count = Anzahl der direkten Unterpunkte, die (in Seitenreihenfolge) direkt
% nachfolgen. 0 = Blatt-Eintrag (kein /Count), >0 = aufgeklappt,
% <0 = zugeklappt (Betrag = Anzahl direkter Kinder).
#(define (make-pdf-toc-stencil title count)
(if (string-null? title) (if (string-null? title)
empty-stencil empty-stencil
(ly:make-stencil (ly:make-stencil
(list 'embedded-ps (list 'embedded-ps
(ly:format (ly:format
"[/Action /GoTo /View [/XYZ -4 currentpagedevice /PageSize get 1 get 4 add null] /Title (~a) /OUT pdfmark" (pdf-encode title))) "[~a/Action /GoTo /View [/XYZ -4 currentpagedevice /PageSize get 1 get 4 add null] /Title (~a) /OUT pdfmark"
(if (= count 0) "" (ly:format "/Count ~a " count))
(pdf-encode title)))
empty-interval empty-interval empty-interval empty-interval
;'(0 . 0) '(0 . 0) ;'(0 . 0) '(0 . 0)
))) )))
% Einzelner (Blatt-)Eintrag im PDF-Inhaltsverzeichnis.
#(define-markup-command (title-to-pdf-toc layout props title) (string?)
(make-pdf-toc-stencil title 0))
% Kapitel-Eintrag mit verschachtelten Unterpunkten.
% count = Anzahl der direkt nachfolgenden Einträge, die diesem Kapitel
% untergeordnet werden sollen (positiv = aufgeklappt, negativ = zugeklappt).
#(define-markup-command (chapter-to-pdf-toc layout props count title) (integer? string?)
(make-pdf-toc-stencil title count))
#(define-markup-command (build-full-title layout props right)(boolean?) #(define-markup-command (build-full-title layout props right)(boolean?)
(interpret-markup layout (prepend-alist-chain 'songfilename (chain-assoc-get 'header:songfilename props "") props) (interpret-markup layout (prepend-alist-chain 'songfilename (chain-assoc-get 'header:songfilename props "") props)
(let* ((title (chain-assoc-get 'header:title props "")) (let* ((title (chain-assoc-get 'header:title props ""))
(starttext (chain-assoc-get 'header:starttext props #f)) (starttext (chain-assoc-get 'header:starttext props #f))
(pdfbookmark (if starttext (string-append starttext " | " title) title)) (pdfbookmark (if starttext (string-append starttext " | " title) title))
(chaptertitle (chain-assoc-get 'header:chaptertitle props #f))
(chaptercount (chain-assoc-get 'header:chaptercount props 0))
(title-markup (ly:output-def-lookup layout (if right 'oddTitleLineMarkup 'evenTitleLineMarkup)))) (title-markup (ly:output-def-lookup layout (if right 'oddTitleLineMarkup 'evenTitleLineMarkup))))
(if title (if title
(markup #:title-to-pdf-toc pdfbookmark title-markup) ;; Ist dieses Lied das erste eines Kapitels, wird der Kapitel-Eintrag
;; unmittelbar vor dem Lied-Eintrag ausgegeben, damit die pdfmark-/OUT-
;; Reihenfolge (Elternteil vor Kindern) im PDF-Stream garantiert stimmt.
(if (and (string? chaptertitle) (not (string-null? chaptertitle)))
(markup #:chapter-to-pdf-toc chaptercount chaptertitle
#:title-to-pdf-toc pdfbookmark title-markup)
(markup #:title-to-pdf-toc pdfbookmark title-markup))
make-null-markup) make-null-markup)
))) )))
+115 -50
View File
@@ -119,7 +119,7 @@ width may require additional tweaking.)"
includeSong = includeSong =
#(define-void-function (parser location filename) (string?) #(define-void-function (filename) (string?)
#{ #{
\bookOutputName #filename \bookOutputName #filename
#} #}
@@ -130,32 +130,47 @@ includeSong =
(acons label additional-page-numbers additional-page-switch-label-list)) (acons label additional-page-numbers additional-page-switch-label-list))
(set! song-list (set! song-list
(acons (string->symbol filename) (acons (string->symbol filename)
(acons 'label label `(
(acons 'header HEADER (label . ,label)
(acons 'music MUSIC (header . ,HEADER)
(acons 'layout LAYOUT (music . ,MUSIC)
(acons 'text-pages (layout . ,LAYOUT)
(map (lambda (text) (text-pages . ,(map (lambda (text)
#{ \markuplist \setsongfilename $filename $text #}) #{ \markuplist \setsongfilename $filename $text #})
TEXT_PAGES) TEXT_PAGES))
'()))))) )
song-list)) song-list))
)) ))
blankpage = blankpage =
#(define-void-function (parser location) () #(define-void-function () ()
(set! song-list (set! song-list
(acons 'emptyPage (acons 'emptyPage
'() '()
song-list))) song-list)))
imagepage = imagepage =
#(define-void-function (parser location xsize filename) (number? string?) #(define-void-function (xsize filename) (number? string?)
(set! song-list (set! song-list
(acons 'imagePage (acons 'imagePage
(acons 'xsize xsize (acons 'filename filename '())) `((xsize . ,xsize)
(filename . ,filename))
song-list))) song-list)))
markuppage =
#(define-void-function (mkup) (markup?)
(set! song-list
(acons 'markupPage
`((markup . ,mkup))
song-list)))
#(define chapters '())
songsChapter =
#(define-void-function (title) (string?)
(set! chapters
(cons (cons title (length song-list)) chapters)))
#(define-markup-command (pagecenter layout props stuff)(markup?) #(define-markup-command (pagecenter layout props stuff)(markup?)
(interpret-markup layout props (interpret-markup layout props
(let ((halfpaperheight (/ (ly:output-def-lookup layout 'paper-height) 2)) (let ((halfpaperheight (/ (ly:output-def-lookup layout 'paper-height) 2))
@@ -164,42 +179,92 @@ imagepage =
))) )))
songs = songs =
#(define-void-function (parser location) () #(define-void-function () ()
(let* ((ordered (reverse song-list))
(total (length ordered))
;; Ein Lied erzeugt nur dann einen PDF-Outline-Eintrag (/OUT), wenn
;; title oder starttext gesetzt sind (siehe build-full-title). Nur solche
;; Lieder duerfen als Kapitel-Kinder gezaehlt werden -- sonst wird die
;; /Count zu hoch und der erste Folgeeintrag (z.B. ein spaeteres Kapitel)
;; rutscht faelschlich als Kind mit hinein.
(emits-bookmark?
(lambda (e)
(and (not (eq? (car e) 'emptyPage))
(not (eq? (car e) 'imagePage))
(not (eq? (car e) 'markupPage))
(let ((vars (extract-and-check-vars-from-header
(assq-ref (cdr e) 'header) '(title starttext))))
(if (or (assq-ref vars 'title) (assq-ref vars 'starttext)) #t #f)))))
(count-bookmark-songs
(lambda (start end)
(let loop ((i 0) (l ordered) (c 0))
(cond ((or (null? l) (>= i end)) c)
((< i start) (loop (+ i 1) (cdr l) c))
(else (loop (+ i 1) (cdr l) (if (emits-bookmark? (car l)) (+ c 1) c)))))))
;; chapters: (title . start-index). start-index = Anzahl der song-list-
;; Eintraege beim Aufruf von songsChapter = Index in `ordered`, ab dem
;; das Kapitel beginnt.
(chapters-asc (sort chapters (lambda (a b) (< (cdr a) (cdr b)))))
;; pending: aufsteigende Liste von (start title count); count = Anzahl
;; der Lesezeichen-Lieder von diesem Kapitel bis zum Beginn des naechsten.
(pending
(let loop ((chs chapters-asc) (acc '()))
(if (null? chs)
(reverse acc)
(let* ((title (caar chs))
(start (cdar chs))
(end (if (null? (cdr chs)) total (cdadr chs)))
(cnt (count-bookmark-songs start end)))
(loop (cdr chs) (cons (list start title cnt) acc))))))
(index 0))
(for-each (lambda (songitems) (for-each (lambda (songitems)
(ly:book-add-bookpart! (ly:parser-lookup '$current-book) (let ((this-index index))
(let ((filename (car songitems)) (set! index (+ index 1))
(songvars (cdr songitems))) (ly:book-add-bookpart! (ly:parser-lookup '$current-book)
(if (eq? filename 'emptyPage) (let ((filename (car songitems))
#{ \bookpart { \markup { \null } } #} (songvars (cdr songitems)))
(if (eq? filename 'imagePage) (if (eq? filename 'emptyPage)
(let ((xsize (assq-ref songvars 'xsize)) #{ \bookpart { \markup { \null } } #}
(filename (ly:format "~a/~a" imagePagePath (assq-ref songvars 'filename)))) (if (eq? filename 'markupPage)
#{ \bookpart { #{ \bookpart { \markup { $(assq-ref songvars 'markup) } } #}
\paper { (if (eq? filename 'imagePage)
%{ (let ((xsize (assq-ref songvars 'xsize))
inner-margin = 0 (filename (ly:format "~a/~a" imagePagePath (assq-ref songvars 'filename))))
outer-margin = 0 #{ \bookpart {
binding-offset = 0 \paper {
top-margin = 0 %{
bottom-margin = 0 inner-margin = 0
%} outer-margin = 0
print-page-number = ##f binding-offset = 0
last-bottom-spacing = #'((basic-distance . 0) (minimum-distance . 0) (padding . 0)) top-margin = 0
page-count = 1 bottom-margin = 0
} %}
\markup { \pagecenter { \epsfile #X #xsize #filename } } print-page-number = ##f
} #} last-bottom-spacing = #'((basic-distance . 0) (minimum-distance . 0) (padding . 0))
) page-count = 1
(let* ((newnumber (+ 1 song-number)) }
(header #{ \bookpart { $(assq-ref songvars 'header) \header { \markup { \pagecenter { \epsfile #X #xsize #filename } }
songfilename = $(symbol->string filename) } #}
myindexlabel = #(assq-ref songvars 'label) )
songnumber = #(number->string newnumber) ;; Ist ein Kapitel faellig (Start <= aktuelle Position) und dies ein
} } #}) ;; echtes Lied, wird die Kapitelinfo an dieses Lied gehaengt und aus
(music (assq-ref songvars 'music)) ;; `pending` entfernt.
(layout (assq-ref songvars 'layout)) (let* ((newnumber (+ 1 song-number))
(text-pages (assq-ref songvars 'text-pages)) (chaptertitle (if (and (pair? pending) (<= (car (car pending)) this-index))
(label (assq-ref songvars 'label))) (cadr (car pending)) #f))
(chaptercount (if chaptertitle (caddr (car pending)) 0))
(header #{ \bookpart { $(assq-ref songvars 'header) \header {
songfilename = $(symbol->string filename)
myindexlabel = #(assq-ref songvars 'label)
songnumber = #(number->string newnumber)
chaptertitle = #chaptertitle
chaptercount = #chaptercount
} } #})
(music (assq-ref songvars 'music))
(layout (assq-ref songvars 'layout))
(text-pages (assq-ref songvars 'text-pages))
(label (assq-ref songvars 'label)))
(if chaptertitle (set! pending (cdr pending)))
(set! song-number newnumber) (set! song-number newnumber)
#{ #{
\bookpart { \bookpart {
@@ -208,9 +273,9 @@ songs =
\score { $music \layout { $layout songfilename = $(symbol->string filename) } } \score { $music \layout { $layout songfilename = $(symbol->string filename) } }
$(add-text-pages text-pages) $(add-text-pages text-pages)
} }
#})))))) #}))))))))
(reverse song-list) ordered
)) )))
#(define (boernel-stats) #(define (boernel-stats)
(let ( (let (
+3 -3
View File
@@ -366,7 +366,7 @@ headerToTOC = #(define-music-function (parser location header label) (ly:book? s
(let ((song-lines (map (lambda (song) (let ((song-lines (map (lambda (song)
(let* ((filename (symbol->string (car song))) (let* ((filename (symbol->string (car song)))
(songvars (cdr song)) (songvars (cdr song))
(page-number (number->string (assoc-get (assq-ref songvars 'label) labelPageTable))) (page-number (assoc-get (assq-ref songvars 'label) labelPageTable))
(extractedheadervars (extract-and-check-vars-from-header (assq-ref songvars 'header) (extractedheadervars (extract-and-check-vars-from-header (assq-ref songvars 'header)
'(title starttext alttitle categorytitle categories authors year_text year_melody year_translation year_composition year_adaption infotext translation pronunciation copyright source))) '(title starttext alttitle categorytitle categories authors year_text year_melody year_translation year_composition year_adaption infotext translation pronunciation copyright source)))
(headervar-or-empty (lambda (varsym) (headervar-or-empty (lambda (varsym)
@@ -413,7 +413,7 @@ headerToTOC = #(define-music-function (parser location header label) (ly:book? s
(format-info-paragraphs (headervar-or-empty 'translation)) (format-info-paragraphs (headervar-or-empty 'translation))
(format-info-paragraphs (headervar-or-empty 'pronunciation)) (format-info-paragraphs (headervar-or-empty 'pronunciation))
)))) ))))
(alist-delete 'imagePage (alist-delete 'emptyPage song-list))))) (alist-delete 'markupPage (alist-delete 'imagePage (alist-delete 'emptyPage song-list))))))
(call-with-output-file "toc.csv" (call-with-output-file "toc.csv"
(lambda (port) (lambda (port)
(csv-write (cons '( (csv-write (cons '(
@@ -443,6 +443,6 @@ headerToTOC = #(define-music-function (parser location header label) (ly:book? s
; we use a delayed stencil to have all the page references available ; we use a delayed stencil to have all the page references available
(ly:make-stencil (ly:make-stencil
`(delay-stencil-evaluation `(delay-stencil-evaluation
,(delay (let* ((table (ly:output-def-lookup layout 'label-absolute-page-table))) ,(delay (let* ((table (ly:output-def-lookup layout 'label-page-string-table)))
(generate-toc-csv (if (list? table) table '())) (generate-toc-csv (if (list? table) table '()))
empty-stencil))))) empty-stencil)))))