VexFoundation is a Swift port of VexFlow, focused on music notation rendering with Swift-native APIs and stronger type safety than the original TypeScript surface.
The project keeps VexFlow concepts (Factory, EasyScore, System, notes, modifiers, beams, tuplets, tablature, etc.) while intentionally preferring typed models over stringly-typed inputs.
- Swift Package (
swift-tools-version: 6.0) - Platforms: iOS 16+, macOS 13+
- Active port: API parity with VexFlow is in progress and APIs may evolve
- Current test suite:
845passing tests
Add the package from GitHub in Package.swift:
dependencies: [
.package(url: "https://github.com/migueldeicaza/VexFoundation.git", branch: "main")
]Then add "VexFoundation" to your target dependencies.
If you are working from a local checkout, you can use:
dependencies: [
.package(path: "../VexFoundation")
]import SwiftUI
import VexFoundation
struct ScoreView: View {
var body: some View {
VexCanvas(width: 500, height: 200) { ctx in
ctx.clear()
FontLoader.loadDefaultFonts()
let f = Factory(options: FactoryOptions(width: 500, height: 200))
_ = f.setContext(ctx)
let score = f.EasyScore()
let system = f.System(options: SystemOptions(factory: f, x: 10, width: 480, y: 10))
let upper = score.notes("C#5/q, B4, A4, G#4", options: ["stem": "up"])
let lower = score.notes("C#4/h, C#4", options: ["stem": "down"])
_ = system.addStave(SystemStave(
voices: [
score.voice(upper.map { $0 as Note }),
score.voice(lower.map { $0 as Note })
]
))
.addClef(.treble)
.addTimeSignature(.meter(4, 4))
system.format()
try? f.draw()
}
}
}import SwiftUI
import VexFoundation
struct TypedScoreView: View {
var body: some View {
VexCanvas(width: 500, height: 200) { ctx in
ctx.clear()
FontLoader.loadDefaultFonts()
let f = Factory(options: FactoryOptions(width: 500, height: 200))
_ = f.setContext(ctx)
let c4 = StaffKeySpec(letter: .c, octave: 4)
let e4 = StaffKeySpec(letter: .e, octave: 4)
let g4 = StaffKeySpec(letter: .g, octave: 4)
let n1 = f.StaveNote(StaveNoteStruct(
keys: NonEmptyArray(c4),
duration: .quarter
))
let n2 = f.StaveNote(StaveNoteStruct(
keys: NonEmptyArray(e4),
duration: .quarter
))
let n3 = f.StaveNote(StaveNoteStruct(
keys: NonEmptyArray(g4),
duration: .half
))
let voice = f.Voice(timeSignature: .meter(4, 4))
_ = voice.addTickables([n1, n2, n3])
let system = f.System(options: SystemOptions(factory: f, x: 10, width: 480, y: 10))
_ = system.addStave(SystemStave(voices: [voice]))
.addClef(.treble)
.addTimeSignature(.meter(4, 4))
system.format()
try? f.draw()
}
}
}VexFoundation favors typed specs over free-form strings:
NoteDurationSpecinstead of raw duration stringsStaffKeySpecinstead of raw key tokensTimeSignatureSpecinstead of raw time signature stringsNonEmptyArraywhere empty collections are invalid (for example, stave/grace note keys)
import VexFoundation
let cSharp4 = StaffKeySpec(letter: .c, accidental: .sharp, octave: 4)
let e4 = StaffKeySpec(letter: .e, octave: 4)
let sn = StaveNote(StaveNoteStruct(
keys: NonEmptyArray(cSharp4, e4),
duration: .quarter
))String constructors / factory paths are still available, but explicit:
- Throwing parse APIs for recoverable errors
- Failable parse APIs when you want
nilon failure - Typed APIs remain the recommended default
import VexFoundation
// Throwing parse API
let parsed = try StaveNoteStruct(
parsingKeys: ["c#/4", "e/4"],
duration: "8dr"
)
// Failable parse API
let maybeParsed = StaveNoteStruct(
parsingKeysOrNil: ["c#/4", "e/4"],
duration: .eighth
)
// Duration-only parse examples
let ghost = try GhostNote("8r")
let maybeGhost = GhostNote(parsingDuration: "8r")
// Factory string convenience (throwing / failable)
let f = Factory()
let sharp = try f.Accidental(parsing: "#")
let maybeAccidental = f.Accidental(parsingOrNil: "invalid")VexFoundation ships lightweight compatibility symbols for migration scenarios:
Flow: selected constants/utilities and music-font helpers.Vex: selected helper utilities.Version/VexVersion: build metadata.
String convenience APIs remain explicit and safe:
import VexFoundation
_ = try Flow.setMusicFont(parsing: ["Bravura", "Custom"])
let maybeFonts = Flow.setMusicFont(parsingOrNil: ["Petaluma", "Custom"])Mutable runtime state is now scoped through VexRuntimeContext:
- Default registry (
Registry.enableDefaultRegistry) - Music font stack and glyph cache
- Auto-generated element IDs
- Runtime flags such as
Tables.UNISON
Use isolated contexts to avoid cross-test or cross-session leakage:
import VexFoundation
let context = Flow.makeRuntimeContext()
Flow.withRuntimeContext(context) {
FontLoader.loadDefaultFonts()
Tables.UNISON = false
let reg = Registry()
Registry.enableDefaultRegistry(reg)
let e = VexElement()
print(e.getAttribute("id") ?? "")
}Constructor-level runtime threading is also available:
import VexFoundation
let runtime = Flow.makeRuntimeContext()
let factory = Factory(runtimeContext: runtime)
let score = factory.EasyScore(options: EasyScoreOptions(runtimeContext: runtime))
let system = factory.System(options: SystemOptions(runtimeContext: runtime, x: 10, width: 480, y: 20))VexFoundation now includes a deterministic SVG backend for parity validation:
SVGRenderContextSVGRenderOptions- Golden snapshot test fixtures under
Tests/VexFoundationTests/Fixtures/svg
Use the helper script:
tools/svg_snapshot.sh --check
tools/svg_snapshot.sh --regenCompare Swift-rendered drawing output against upstream pptr-*.svg files:
tools/upstream_svg_parity.sh
tools/upstream_svg_parity.sh --font Bravura
tools/upstream_svg_parity.sh --reference-dir ../vexmotion/build/images/reference
tools/upstream_svg_parity.sh --strict
tools/upstream_svg_parity.sh --tolerant
tools/upstream_svg_parity.sh --signature-epsilon 0.005This workflow also writes a manifest to:
.build/upstream-svg-parity/upstream_svg_manifest.json- mismatch artifacts under
.build/upstream-svg-parity/artifacts
Track how many upstream SVG reference cases are covered by implemented parity tests:
tools/generate_upstream_svg_coverage.sh
tools/generate_upstream_svg_coverage.sh --parity-mode strict
tools/generate_upstream_svg_coverage.sh --parity-mode tolerant --signature-epsilon 0.005
tools/generate_upstream_svg_coverage.sh --checkThis writes:
docs/upstream-svg-coverage.md.build/upstream-svg-parity/upstream_svg_coverage.json
- API redesign favors compile-time validation.
- Invalid states are reduced via typed enums/specs and non-empty collections.
- Parsing is explicit at boundaries, not implicit throughout the API.
- Lightweight
Flow/Vexcompatibility facades are available for incremental migration. - SwiftUI rendering backend (
VexCanvas/SwiftUICanvasContext) is provided for app integration. - Core scope is Swift-native rendering abstractions (
RenderContext) rather than 1:1 browser modules (renderer,canvascontext,svgcontext,web).
swift build
swift test
tools/generate_parity_matrix.shCI verifies parity matrix freshness with:
tools/generate_parity_matrix.sh --checkGenerate local documentation:
swift package \
--allow-writing-to-directory docs-site \
generate-documentation \
--target VexFoundation \
--disable-indexing \
--transform-for-static-hosting \
--hosting-base-path VexFoundation \
--output-path docs-siteThe package includes swift-docc-plugin support in Package.swift.
Docs are published by .github/workflows/docs.yml on pushes to main (and manually via workflow_dispatch).
Published docs:
VexFoundation is derived from VexFlow and preserves original attribution:
- VexFlow: https://github.com/vexflow/vexflow
- Original author: Mohit Muthanna Cheppudira
MIT (see LICENSE).