Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ It is the fastest way to answer a layout question without a GUI:
swift run ckprobe tune.abc # full report
swift run ckprobe tune.abc --scale 0.6 # override %%scale (default 0.75)
swift run ckprobe tune.abc --sweep 1.0,0.75,0.6 # systems/pages per %%scale
swift run ckprobe tune.abc --natural # unstretched system widths
swift run ckprobe tune.abc --natural # unstretched system widths (%%stretchstaff 0)
swift run ckprobe tune.abc --out /tmp/out --json
swift run ckprobe --fonts [--system-fonts] # faces text could be set in
swift run ckprobe tune.abc --out /tmp/out --font-face # <text> + @font-face
Expand Down Expand Up @@ -112,7 +112,8 @@ Eight `%%ceolkit:*` directives are first-class model members:
- `%%ceolkit:pipeformat true|false`
- `%%ceolkit:pagenumber N`
- `%%ceolkit:stemalignment N`
- `%%ceolkit:justifylast true|false`
- `%%ceolkit:justifylast true|false` (**deprecated**: parsed as `%%stretchlast 1|0` with a
`deprecatedDirective` warning; the last line is abcm2ps's `%%stretchlast`/`%%stretchstaff`)
- `%%ceolkit:scale F` (**deprecated**: parsed as `%%pagescale F` with a `deprecatedDirective` warning;
the page scale is `%%scale`/`%%pagescale`, as in abcm2ps — see `EXTENSIONS.md`)
- `%%ceolkit:gracenotespacing F` (F >= 1, in grace notehead widths; tune-wide, never per-voice)
Expand Down
58 changes: 47 additions & 11 deletions EXTENSIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,21 +161,18 @@ K:A

---

## `%%ceolkit:justifylast`
## `%%ceolkit:justifylast` (deprecated)

**Syntax:** `%%ceolkit:justifylast <true|false>`

**Type:** boolean (`true` or `false`; case-insensitive)

**Default:** `false`

**Scope:** global (file preamble or tune header)

### Description
**Status:** deprecated since issue #198; use `%%stretchlast` (see
[The last line](#the-last-line-stretchlast-and-stretchstaff) below). It will be removed
in a later release.

The default way to render a tune is not to justify the last line, leaving it ragged.
This setting, when `true`, forces the last line to be justified so that it spans the
whole width.
`%%ceolkit:justifylast true` now means `%%stretchlast 1`, and `false` means
`%%stretchlast 0`. Either produces a `deprecatedDirective` warning naming the
replacement. Note that `false` no longer draws the last line at its natural width:
like abcm2ps, a short last line takes the spacing of the line above it.

---

Expand Down Expand Up @@ -565,6 +562,45 @@ be removed in a later release.
the title block and the `W:` words along with the music, which it did not do
before.

## The last line: `%%stretchlast` and `%%stretchstaff`

These are abcm2ps's directives (ABC v2.2 §11.4.3 lists both), implemented the way
abcm2ps 8.14.0 behaves (issue #198). Every system but the last is stretched to the
full line. The last system of a tune is handled like this:

- **`%%stretchlast F`**, with `F` from 0 to 1. The default is **0.25**. The last
system is stretched to the full line when its unstretched staff (header and music)
reaches `1 − F` of the line. At the default, that means three quarters of the line.
`1` stretches every last line, and `0` stretches none. The spec's logical form is
accepted too, with `true` meaning 1 and `false` meaning 0. abcm2ps accepts only the
number.
- A last system that is **not** stretched is spaced like the line before it, so its
notes do not suddenly close up at the end of the tune. It is never squeezed below
its natural width and never runs past the line. A tune of one line has no line
before it, so its last line is drawn at natural width.
- **`%%stretchstaff <true|false>`** (default `true`). `false` stretches no system at
all, the last one included. A system that overruns the line is still compressed to
fit.

A value that is not a number from 0 to 1, or a logical, produces an
`invalidStretchLast` warning, and the directive is ignored.

Both are scoped like `%%scale`: written in the file header, the value governs every
tune; written in a tune, it governs that tune only. `SVGRenderConfig.stretchLast` and
`SVGRenderConfig.stretchStaff` set the values the host uses where the document says
nothing.

```abc
X:1
T:Last line always stretched
%%stretchlast 1
M:6/8
L:1/8
K:G
GAB cde | dcB A3 | B2c d2d | cdc B3 |
B2c d2d |]
```

## The page scale: `%%scale` and `%%pagescale`

These are abcm2ps's directives (ABC v2.2 §11.4.3 lists `%%scale`), implemented
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ pages: 2
| `--scale <factor>` | Override `%%scale` (abcm2ps's `-s`; default 0.75) before rendering. |
| `--grace-spacing <f>` | Override `%%ceolkit:gracenotespacing` (≥ 1) before rendering. |
| `--sweep <f,f,…>` | Render at each `%%scale`; print a systems/pages table. |
| `--natural` | Force `%%ceolkit:justifylast false`, so widths are unstretched. |
| `--natural` | Force `%%stretchstaff 0`, so every system's width is unstretched. |
| `--out <dir>` | Write `page0.svg`, `page1.svg`, … into `<dir>`. |
| `--json` | Emit JSON instead of the text report. |

Expand Down
11 changes: 10 additions & 1 deletion Sources/CeolKitModel/CeolKitDirective.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,16 @@ public enum CeolKitDirective: Hashable, Sendable {
case graceNoteSpacing(Double) // %%ceolkit:gracenotespacing F (F >= 1)
case landscape(Bool) // %%landscape 0|1 (ABC v2.2 §9.1)
case flatBeams(Bool) // %%flatbeams true|false (abcm2ps; implicit in pipeFormat)
case justifyLast(Bool) // %%ceolkit:justifylast true|false
/// How short the last system of a tune may be and still be stretched to the full line,
/// as abcm2ps's `%%stretchlast` value (issue #198): the system is stretched when its
/// natural width reaches `1 − F` of the line. `0.25` is abcm2ps's — and CeolKit's —
/// default; `0` never stretches it, `1` always does. The spec's logical form and the
/// deprecated `%%ceolkit:justifylast` are `1` for true and `0` for false.
case stretchLast(Double) // %%stretchlast F (0…1) | true|false | %%ceolkit:justifylast
/// Whether systems are stretched to the line at all, from `%%stretchstaff` (ABC v2.2
/// §11.4.3 lists it; abcm2ps defaults it to true). `false` draws every system at its
/// natural width, the last one included.
case stretchStaff(Bool) // %%stretchstaff true|false
case label(String) // %%ceolkit:label "<text>" (value of a `${label}` footer mark)
case writeFields(String, Bool) // %%writefields <fieldList> [true|false] (ABC v2.2 §11.4.6)
case dateFormat(String) // %%dateformat <strftime-string> (abcm2ps/abc2svg)
Expand Down
6 changes: 5 additions & 1 deletion Sources/CeolKitModel/Diagnostic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,18 @@ public enum DiagnosticCode: String, Codable, Sendable {
/// would change scale from the next music line; that is not implemented.)
case scaleAppliesToWholeTune
case invalidGraceNoteSpacing
/// `%%stretchlast` whose value is neither a number from 0 to 1 nor a logical, or a
/// `%%ceolkit:justifylast` that is not `true` or `false`.
case invalidStretchLast
/// A font directive (§11.4.2) whose payload is not `<font name> [<size>]`: no name, a
/// size that is not a positive number, or words after the size.
case invalidFontDirective
// Directives
case unknownDirective
case redundantDirective
/// A directive CeolKit still honours but is going to remove, with its replacement named
/// in the message — `%%ceolkit:scale`, which is `%%pagescale` (issue #203).
/// in the message — `%%ceolkit:scale`, which is `%%pagescale` (issue #203), and
/// `%%ceolkit:justifylast`, which is `%%stretchlast` (issue #198).
case deprecatedDirective
/// A `%%footer` template contains a `$`-token CeolKit does not substitute — an
/// abcm2ps placeholder that is not implemented here, or a typo such as `$p`. The
Expand Down
40 changes: 35 additions & 5 deletions Sources/CeolKitParser/Semantic/SemanticPass.swift
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,8 @@ struct SemanticPass {
|| name == "dateformat" || name == "footer"
|| name == "straightflags" || name == "graceslurs"
|| name == "score" || name == "staves" || name == "newpage"
|| name == "scale" || name == "pagescale"
|| name == "scale" || name == "pagescale" || name == "stretchlast"
|| name == "stretchstaff"
|| TextFontRole(rawValue: name) != nil
}

Expand Down Expand Up @@ -1071,7 +1072,8 @@ struct SemanticPass {
beforeStave: ctx.currentStaveIndex,
restartingAt: parseNewPage(payload, source: source, diagnostics: &diagnostics),
source: source))
case "landscape", "flatbeams", "ceolkit:justifylast", "ceolkit:scale",
case "landscape", "flatbeams", "ceolkit:justifylast", "stretchlast", "stretchstaff",
"ceolkit:scale",
"scale", "pagescale", "ceolkit:gracenotespacing", "ceolkit:fontlist", "writefields",
"dateformat", "footer", "straightflags", "graceslurs",
_ where TextFontRole(rawValue: name) != nil:
Expand Down Expand Up @@ -1549,11 +1551,39 @@ struct SemanticPass {
diagnostics.append(Diagnostic(severity: .warning, code: .unknownDirective,
message: "%%flatbeams expects '0'/'false' or '1'/'true'", source: source))
return nil
case "ceolkit:justifylast":
if let value = parseLogical(trimmed) { return .justifyLast(value) }
case "stretchlast":
// abcm2ps takes a fraction of the line and rejects anything outside 0…1; the
// ABC v2.2 spec's list of directives gives it a logical, which is the two ends of
// that range. A number is tried first, so `0` and `1` mean the same either way.
if let f = Double(trimmed), f.isFinite {
guard (0...1).contains(f) else {
diagnostics.append(Diagnostic(severity: .warning, code: .invalidStretchLast,
message: "%%stretchlast must be between 0 and 1 (got \(trimmed))",
source: source))
return nil
}
return .stretchLast(f)
}
if let value = parseLogical(trimmed) { return .stretchLast(value ? 1 : 0) }
diagnostics.append(Diagnostic(severity: .warning, code: .invalidStretchLast,
message: "%%stretchlast expects a number from 0 to 1, or 'true'/'false' "
+ "(got '\(trimmed)')", source: source))
return nil
case "stretchstaff":
if let value = parseLogical(trimmed) { return .stretchStaff(value) }
diagnostics.append(Diagnostic(severity: .warning, code: .unknownDirective,
message: "%%ceolkit:justifylast expects 'true' or 'false'", source: source))
message: "%%stretchstaff expects '0'/'false' or '1'/'true'", source: source))
return nil
case "ceolkit:justifylast":
guard let value = parseLogical(trimmed) else {
diagnostics.append(Diagnostic(severity: .warning, code: .invalidStretchLast,
message: "%%ceolkit:justifylast expects 'true' or 'false'", source: source))
return nil
}
diagnostics.append(Diagnostic(severity: .warning, code: .deprecatedDirective,
message: "%%ceolkit:justifylast is deprecated; use %%stretchlast \(value ? 1 : 0), "
+ "which it now means", source: source))
return .stretchLast(value ? 1 : 0)
case "ceolkit:fontlist":
if trimmed.isEmpty { return .fontList(.resolved) }
if let mode = FontListMode(rawValue: trimmed.lowercased()) { return .fontList(mode) }
Expand Down
2 changes: 1 addition & 1 deletion Sources/CeolKitSVGRenderer/CeolKitSVGRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ public struct SVGRenderer: CeolKitRenderer {
}
let tuneGroups = groups.isEmpty ? [] : justifier.justifyGroups(
groups, usableWidth: usableWidth(on: pageSizes.opening),
justifyLastSystem: layout.justifyLastSystem,
stretchLast: layout.stretchLast, stretchStaff: layout.stretchStaff,
systemHeaderWidths: headerWidths, systemUsableWidths: usableWidthOfGroup)

// Build the title block for this tune per §6.1.3.
Expand Down
16 changes: 13 additions & 3 deletions Sources/CeolKitSVGRenderer/Config/SVGRenderConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,15 @@ public struct SVGRenderConfig: Sendable {
public var spanStaffGap: Double
/// Vertical gap added after the last system of a tune, before the next tune's title block.
public var tuneGap: Double
public var justifyLastSystem: Bool
/// abcm2ps's `%%stretchlast`, from 0 to 1: the last system of a tune is stretched to the
/// full line when its natural width reaches `1 − stretchLast` of it (issue #198). The
/// default, `0.25`, is abcm2ps's. `0` leaves every last system at its natural width; `1`
/// stretches every one. A `%%stretchlast` in the document overrides it.
public var stretchLast: Double
/// abcm2ps's `%%stretchstaff`: whether systems are stretched to the line at all. `false`
/// draws every system at its natural width, the last one included; a system that
/// overruns is still compressed to fit. A `%%stretchstaff` in the document overrides it.
public var stretchStaff: Bool
public var straightFlags: Bool
public var graceSlurs: Bool
/// Step between adjacent grace noteheads within one grace group, as a multiple of the
Expand Down Expand Up @@ -86,7 +94,8 @@ public struct SVGRenderConfig: Sendable {
staffGap: Double? = nil,
spanStaffGap: Double? = nil,
tuneGap: Double? = nil,
justifyLastSystem: Bool = false,
stretchLast: Double = 0.25,
stretchStaff: Bool = true,
straightFlags: Bool = false,
graceSlurs: Bool = true,
graceNoteSpacing: Double = 1.05,
Expand All @@ -105,7 +114,8 @@ public struct SVGRenderConfig: Sendable {
self.staffGap = staffGap ?? staffSize * 3
self.spanStaffGap = spanStaffGap ?? staffSize * 2
self.tuneGap = tuneGap ?? staffSize * 16
self.justifyLastSystem = justifyLastSystem
self.stretchLast = min(max(stretchLast, 0), 1)
self.stretchStaff = stretchStaff
self.straightFlags = straightFlags
self.graceSlurs = graceSlurs
self.graceNoteSpacing = graceNoteSpacing
Expand Down
Loading
Loading