Skip to content

1.6

Choose a tag to compare

@sbeitzel sbeitzel released this 18 Sep 19:26
· 70 commits to main since this release
e5d9931

A directive belongs to the tune it was written in.

Six fixes, five of them the same bug wearing different clothes. A stylesheet directive
written in a tune header governs that tune (ABC v2.2 §4.23) — CeolKit had been letting
some of them run on to the end of the document, dropping others on the floor, and folding
%%footer into a single document-wide value that the last tune won. All of them are
scoped properly now. The sixth fix hands back the layout the renderer used to throw away,
so a consumer can finally say which page a tune landed on.

Highlights

Which page did this tune land on? (#152). A multi-tune document shares a page between
tunes whenever they fit, so the page a tune starts on is decided by the layout walk and is
written nowhere in the emitted SVG. render(_:) returned [String] and discarded the
layout that knew. renderDocument(_:) returns it instead:

let doc = try SVGRenderer(config: config).renderDocument(score)
doc.pages                      // exactly what render(_:) returns
doc.layout                     // the positioned layout, footers included
for p in doc.placements {
    let title = score.tunes[p.tuneIndex].titles.first?.value ?? ""
    print("\(title) … page \(p.printedPageNumber)")
}

printedPageNumber is what the page prints, after %%newpage N and
%%ceolkit:pagenumber have renumbered it — not pageIndex + 1. topY is where the tune's
title block sits, so a tune packed in below another is distinguishable from one that opens
a page. Enough to print a table of contents, or answer "turn to page N". render(_:) is
unchanged and still there.

A tune header's layout directives no longer leak forward (#153). %%ceolkit:scale,
%%ceolkit:gracenotespacing, %%pipeformat and %%ceolkit:justifylast lived in vars
outside the renderer's tune loop and were never reset, so a scale written in tune 3's
header quietly re-engraved tunes 4 through 20 as well. Each tune now starts from what the
file preamble said, and applies its own header on top. scale, stemDirection,
straightFlags and graceSlurs travel with the TuneBlock for the same reason: one page
can hold systems from several tunes, and they can disagree.

%%footer is scoped, not document-wide (#155). The semantic pass carried one
var currentFooter across the whole file, last-wins, and stamped the survivor onto
Score. A tunebook where each tune names its own footer printed the last tune's footer
on every page. Tune.footer now holds what governs that tune, Score.footer holds what
the file header said, and a renderer resolves tune.footer ?? score.footer. A page belongs
to the tune whose music opens it — the new ResolvedPage.openingTuneIndex — because that
is the tune a reader turning to the page is reading. $T and %%dateformat follow the
same tune, so a footer reading "Tune title — date" is the right title with the right
format. %%footer "" in a tune header is a statement, not an absence: it suppresses a
file-header footer for that tune rather than inheriting it.

%%straightflags and %%graceslurs honour scope (#156). applyingScoreDirectives
read the first tune's directive list and ignored scope entirely, so these two were
broken in both directions at once: tune 1's header applied to the whole document, and every
later tune's was dropped. Only fileGlobal directives set the document value now; a tune
header's reaches that tune's blocks.

A file-level %%score / %%staves governs every tune it covers (#157). The same
asymmetry inverted: a plan written in the file preamble was promoted to the first tune
only, so tunes 2 onwards engraved with no plan at all. It now governs every tune it covers.
A plan is voice-specific in a way the other directives are not, so a tune that does not have
the voices the plan names is laid out as though the plan had not been written — reported as
staffPlanNotApplicableToTune — rather than half-applying it and dropping voices from a
tune that engraves perfectly well without it.

A page size is a property of a page (#158). %%landscape was folded into the preamble
and resolved last-wins for the document, which meant a %%landscape 0 written between
two tunes retroactively unrotated pages that had already been engraved landscape — and a
tunebook that opens landscape and turns portrait halfway through could not be expressed at
all. A page cannot change size part-way down, so a change of orientation is paired with the
%%newpage written at the same point:

%%landscape 1
X:1
T:First
…
%%newpage
%%landscape 0
X:2
T:Second

Page 0 comes out 792 × 612, page 1 612 × 792. The width follows, so systems on a landscape
page are broken and justified to the landscape line. A %%landscape with no %%newpage at
its own point is dropped with a landscapeWithoutPageBreak warning rather than reaching
forward to a break the author did not write. One in the file header is not a change at all —
it is the orientation the document opens in.

New in the model

Type / member Purpose
Tune.footer the %%footer template governing this tune, or nil for Score.footer
PageBreak.landscape the orientation the new page takes, from a %%landscape at this break
StaffPlan.namedVoices every voice a plan names, in written order — the plan read as a question about a tune
DiagnosticCode.landscapeWithoutPageBreak a %%landscape with no page boundary to start at
DiagnosticCode.staffPlanNotApplicableToTune a file-level plan naming a voice some tune it governs does not have

New in the renderer

Type / member Purpose
SVGRenderer.renderDocument(_:) pages, plus the layout and where every tune landed
SVGRenderer.renderDocument(_:diagnostics:) the same, collecting render diagnostics
RenderedDocument pages, layout, placements
TunePlacement tuneIndex, pageIndex, printedPageNumber, topY
VerticalLayoutEngine.layoutDocument(_:firstPageNumber:) the layout together with the placements it worked out
ResolvedPage.pageSize the size this page is packed to and written out at
ResolvedPage.openingTuneIndex the tune whose music opens the page, and so owns its footer
TuneBlock.stemDirection / .straightFlags / .graceSlurs per-tune values, nil to leave the document's standing
ResolvedSystem.tuneStemDirection / .straightFlags / .graceSlurs the same, carried through to emission

Smaller changes

  • Size is Hashable rather than only Equatable.
  • ckprobe reports a placements: block — which page each tune landed on and what that
    page prints as — in both its text and --json output.

Documentation

  • CONFORMANCE.md — a new §9.1 on %%landscape: where a change of orientation takes
    effect, why the file header is read separately, and what is dropped.
  • README.md — renderDocument(_:) in the module table, placements in the ckprobe
    sample output.

What's Changed

  • Fix #152 — report which page each tune landed on by @sbeitzel in #154
  • Fix #153 — layout directives in a tune header no longer leak into following tunes by @sbeitzel in #159
  • Fix #155 — %%footer and %%dateformat are scoped, not document-wide by @sbeitzel in #160
  • Fix #156 — %%straightflags and %%graceslurs honour directive scope by @sbeitzel in #161
  • Fix #157 — a file-level %%score / %%staves plan governs every tune it covers by @sbeitzel in #162
  • Fix #158 — a page size is a property of a page, and %%landscape turns one at a %%newpage by @sbeitzel in #163
  • Release 1.6.0 by @sbeitzel in #164

Full Changelog: v1.5.0...v1.6.0

Upgrading

Source-compatible with 1.5.0. Three signatures gain defaulted parameters and existing
calls compile unchanged: PageBreak.init, ResolvedPage.init and TuneBlock.init.

The behaviour changes are the point of the release, so if you worked around any of them
downstream, remove the workaround:

  • Score.footer no longer means "the document's footer, last occurrence wins". It is the
    file header's footer — the one every tune starts from. Resolve a page's footer as
    tune.footer ?? score.footer, or let renderDocument(_:) / render(_:) do it.
  • A tune header's %%ceolkit:scale, %%ceolkit:gracenotespacing, %%pipeformat,
    %%ceolkit:justifylast, %%straightflags and %%graceslurs stop at the end of that
    tune. A document that relied on one running on needs it written in the file header.
  • %%landscape outside the file header now needs a %%newpage at the same point, and
    turns only the pages from there on.
  • A %%score / %%staves in the file preamble now applies to every tune, not just the
    first.

Engraved output for a single-tune document is byte-identical to 1.5.0.