Repository navigation
1.6
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
SizeisHashablerather than onlyEquatable.ckprobereports aplacements:block — which page each tune landed on and what that
page prints as — in both its text and--jsonoutput.
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,placementsin theckprobe
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.footerno 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 letrenderDocument(_:)/render(_:)do it.- A tune header's
%%ceolkit:scale,%%ceolkit:gracenotespacing,%%pipeformat,
%%ceolkit:justifylast,%%straightflagsand%%graceslursstop at the end of that
tune. A document that relied on one running on needs it written in the file header. %%landscapeoutside the file header now needs a%%newpageat the same point, and
turns only the pages from there on.- A
%%score/%%stavesin 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.