Mehrere book in einer Quelldatei führen zu Fehler in Folgeinhaltsverzeichnissen [Bug]

Begonnen von martinmagtenor, Samstag, 21. Februar 2026, 15:01

Vorheriges Thema - Nächstes Thema

martinmagtenor

Hallo zusammen,

es hat sich ergeben, in einer Master-Quell-Datei mehrere book-Abschnitte zu haben. Diese erzeugen wie erwartet unabhängige Ausgabedateien.

Dabei ist mir aufgefallen, dass meine Lilypond-Version 2.24.4 einen eigenartigen Fehler erzeugt. Das Inhaltsverzeichnis des ersten Buchs ist ganz normal. Im Zweiten werden aber die Inhaltsverzeichniseinträge des ersten wiederholt, dann aber ohne Seitenzahl (aber mit Fragezeichen).

Hier ist mein Minimalbeispiel:

\version "2.24.4"

\header { tagline = ##f }
\paper {
  tocItemMarkup = \tocItemWithDotsMarkup
}

\book {
  \header {
    title = "Buch 1"
    section = "Abschnitt 1"
  }
  \markuplist \table-of-contents
  \tocItem \markup { Allegro }
  \tocItem \markup { Largo }
  \markup \null
}

\book {
  \header {
    title = "Buch 2"
    section = "Abschnitt 1"
  }
  \markuplist \table-of-contents
  \tocItem \markup { Menuett }
  \tocItem \markup { Allemande }
  \markup \null
}

Die Seitennummerierung beginnt wieder bei 1, das ist wie erwartet bzw. gewünscht.

Jetzt stellt sich mir die Frage: Wie bekomme ich die Einträge des ersten Buchs im Inhaltsverzeichnis des Zweiten entfernt?

Eigentlich haben die da ja gar nichts verloren. Habe ich etwas übersehen oder ist das ein Bug?

Vielen Dank
  Martin

harm6


harm6

Oder folgendes, hat aber auch Nachteile (siehe Kommentare) und \tocItem braucht eine zusätzliches Argument:

\version "2.24.4"

#(let (;; Maps TOC item IDs (symbols) to alists
       (toc-hashtab (make-hash-table))
       ;; Same, in alist form.  This is what we eventually want to return, but a
       ;; hash table avoids quadratic algorithms while constructing the TOC tree.
       (toc-alist '())
       ;; Map names, i.e. terminal symbols of the paths
       ;; (\tocItem foo.bar.baz ... has the name 'baz) to
       ;; TOC IDs.
       (toc-name-id-hashtab (make-hash-table)))
;; NB Commenting next lines may cause bleed-over into next session, while
;; doing: lilypond file-1.ly file-2.ly
;; Though otherwise we cannot use this coding
;;
;;   (call-after-session (lambda ()
;;                         (hash-clear! toc-hashtab)
;;                         (set! toc-alist '())
;;                         (hash-clear! toc-name-id-hashtab)))
   (set! add-toc-item!
         (lambda* (markup-symbol text #:optional raw-path)
           (let* ((id (gensym "toc"))
                  (path (cond
                         ((symbol? raw-path) (list raw-path))
                         ;; Without a raw-path, we add an entry at the toplevel, which
                         ;; is the same as a one-element raw-path.
                         ((or (not raw-path) (null? raw-path)) (list id))
                         ((list? raw-path) raw-path)
                         (else (begin
                                (ly:warning (_i "Invalid toc label: ~a")
                                            raw-path))
                               (list id))))
                  (level
                   ;; Find which existing TOC entry, if any, to attach this entry to.
                   ;; The principle is that the first element of path is interpreted specially:
                   ;; it can refer to a previously defined nested node, as with
                   ;; \tocItem foo.bar "x"
                   ;; \tocItem bar.baz "y"
                   ;; This attaches bar as a subtree of foo, which can be handy in
                   ;; large nested TOCs. If there are several possibilities (foo.bar
                   ;; and baz.bar), we choose the one that added last.  This is
                   ;; achieved by simply overwriting any existing entry in
                   ;; toc-name-id-hashtab when doing the hashq-set!.
                   (match path
                     ((single)
                      (hashq-set! toc-name-id-hashtab single id)
                      0)
                     ((head . tail)
                      (let* ((node-id (hashq-ref toc-name-id-hashtab head))
                             (entry (and node-id (hashq-ref toc-hashtab node-id))))
                        (let loop ((path path)
                                   ;; entry corresponds to the entry for the first element
                                   ;; in the path.  path still contains its name so a warning
                                   ;; can be emitted if entry is #f.
                                   (entry entry)
                                   (level (and entry (1+ (assq-ref entry 'level)))))
                          (if entry
                              (let ((children (assq-ref entry 'children)))
                                (match path
                                  ((head name)
                                   ;; The last component is a newly created node.
                                   (hashq-set! children name id)
                                   (hashq-set! toc-name-id-hashtab name id)
                                   level)
                                  ((head . (and remaining (child . rest)))
                                   (loop remaining
                                         (let ((child-id (hashq-ref children child)))
                                           (and child-id (hashq-ref toc-hashtab child-id)))
                                         (1+ level)))))
                              (begin
                               (ly:warning (G_ "TOC node ~a not defined")
                                           (car path))
                               ;; Insert the node on the toplevel.
                               (let ((final-name (last path)))
                                 (hashq-set! toc-name-id-hashtab final-name id))
                               0)))))))
                  (alist
                   `((text . ,text)
                     (name . ,(car path))
                     (toc-markup . ,markup-symbol)
                     (children . ,(make-hash-table))
                     (level . ,level))))
             ;; Register the new entry.
             (hashq-set! toc-hashtab id alist)
             (set! toc-alist (acons id alist toc-alist))
             (label id))))
   (set! toc-items (lambda ()
                     (reverse toc-alist))))

%% Due to issue 4227
%% https://gitlab.com/lilypond/lilypond/-/issues/4227
%% we change `table-of-contents`.
%% For now we abuse the 'name entry of every toc-item, if it equals
%% the newly provided `toc-name` property we proceed. The default setting of
%% `toc-name` results in the same behaviour as before.
%% Otherwise return #f, i.e. this toc-item will be filtered out.
%% TODO ofcourse this disturbs the usual usage of the 'name entry of toc-item
#(define-markup-list-command (table-of-contents layout props) ()
  #:properties ((baseline-skip)
                (toc-name 'all))
  ( _i "Outputs the table of contents, using the paper variable
@code{tocTitleMarkup} for its title, then the list of lines
built using the @code{tocItem} music function.
Usage: @code{\\markuplist \\table-of-contents}" )
  (let ((titleMarkup (ly:output-def-lookup layout 'tocTitleMarkup))
        (indentMarkup (ly:output-def-lookup layout 'tocIndentMarkup))
        (toplevelFormatter (ly:output-def-lookup layout 'tocFormatMarkup))
        (toc-alist (toc-items)))

    (ly:output-def-set-variable!
      layout
      'label-alist-table
      (append (ly:output-def-lookup layout 'label-alist-table) toc-alist))

    (cons (interpret-markup layout props titleMarkup)
          (space-lines
            baseline-skip
            (filter-map
              (lambda (toc-item)
                (let ((alist (cdr toc-item)))
                   (if (or (eq? (assoc-get 'name alist) toc-name)
                           (eq? toc-name 'all))
                       (let* ((label (car toc-item))
                              (toc-markup (assoc-get 'toc-markup alist))
                              (text (assoc-get 'text alist))
                              (level (assoc-get 'level alist)))
                         (interpret-markup
                          layout
                          (cons (list
                                  (cons 'toc:page
                                        (markup #:with-link label
                                                #:page-ref label "XXX" "?"))
                                  (cons 'toc:text
                                        (markup #:with-link label text))
                                  (cons 'toc:label label)
                                  (cons 'toc:level level)
                                  (cons 'toc:toplevel-formatter
                                        toplevelFormatter)
                                  (cons 'toc:indent
                                        (make-line-markup
                                         (make-list level indentMarkup))))
                                props)
                          (ly:output-def-lookup layout toc-markup)))
                        #f)))
                 toc-alist)))))

\book {
  \markuplist
    \override #`(toc-name . I)
    \table-of-contents
  \tocItem I \markup "test1.1"
  \score { b1 }
  \tocItem I \markup "test1.2"
  \score { b1 }
}

\book {
  \markuplist
    \override #`(toc-name . II)
    \table-of-contents
  \tocItem II \markup "test2.1"
  \score { b1 }
  \tocItem II \markup "test2.2"
  \score { b1 }
}

Gruß,
  Harm


martinmagtenor

Hallo Harm,

Danke, krass, das Problem hat also kürzlich sein Elfjähriges begangen ...

Den Workaround werde ich ausprobieren. Da noch ein Merkmal mitgeben zu müssen ist für mich erstmal kein Problem.

Nach meinem Verständnis ist die Inhaltsverzeichnisfunktionalität im Kontext mit mehreren book-Abschnitten in einer Quelldatei kaputt. Denn die Möglichkeit ein Inhaltsverzeichnis über mehrere Bücher zu erstellen funktioniert nicht und ab dem zweiten Buch funktioniert das Einzel-Inhaltsverzeichnis auch nicht. Und das gilt auch dann, wenn die Books in getrennten Quelldateien sind, die über include zusammengefasst werden.

Grüße
  Martin

martinmagtenor

Hallo Harm,

Dein Code funktioniert, nochmal vielen Dank.

Was Dein Code aus meiner Sicht macht, ist ja, dass das Inhaltsverzeichnis des Buches einen Namen (toc-name), quasi eine ID, bekommt und dieser Name in tocItem angegeben werden muss, um die richtige Zuordnung zu bekommen. Und beim Aufrufe markuplist muss man dann angeben, welches Inhaltsverzeichnis gewünscht wird.

Und wie ich gerade auf das Minimalbeispiel schaue, kommt mir ein Gedanke, vielleicht verwegen, aber Ideen dürfen das ja sein:

Könnte man diesen Namen nicht einfach im ersten Header eines Buches unter einem reservierten Label, z.B. toc-name, für das ganze Buch festlegen? Dann könnte man auf das Angeben dieses Namens bei tocItem und markuplist verzichten!

Und für den einfachen und häufigen Fall eines einzelnen book wird ein Standardwert vorgesehen, womit eine Abwärtskompatibilität gegeben wäre.

Grüße
  Martin