From 0d9104e91236b87ca18c68c07732581584057d79 Mon Sep 17 00:00:00 2001 From: danielku15 Date: Fri, 9 Oct 2026 17:59:13 +0200 Subject: [PATCH 1/8] feat(alphatex): define staff display tags for staff configs and bar overrides Add \defaultStaffDisplay (score), \staffDisplay (staff) and \barDisplay (bar) sharing one definition: the arguments select the staff types (score, tabs, slash, numbered), the properties set one model field each (Visibility/Placement/Systems for clef, ks, ts and rests, plus barNumber and rhythm). Adds the AlphaTexStaffType enum, enum mappings for StaffPlacement, SystemDisplay and TabRhythmMode and the AT307 warning code. Refs #2912 --- .../alphaTex/AlphaTex1EnumMappings.ts | 36 ++- .../alphaTex/AlphaTex1LanguageDefinitions.ts | 97 +++++- .../src/importer/alphaTex/AlphaTexShared.ts | 29 ++ .../alphatab/src/importer/alphaTex/_barrel.ts | 1 + packages/alphatex/src/common.ts | 65 +++- packages/alphatex/src/definitions.ts | 12 +- packages/alphatex/src/enum.ts | 40 ++- .../alphatex/src/metadata/bar/barDisplay.ts | 24 ++ .../src/metadata/score/defaultStaffDisplay.ts | 24 ++ .../src/metadata/staff/staffDisplay.ts | 23 ++ packages/alphatex/src/staffDisplay.ts | 297 ++++++++++++++++++ 11 files changed, 638 insertions(+), 10 deletions(-) create mode 100644 packages/alphatex/src/metadata/bar/barDisplay.ts create mode 100644 packages/alphatex/src/metadata/score/defaultStaffDisplay.ts create mode 100644 packages/alphatex/src/metadata/staff/staffDisplay.ts create mode 100644 packages/alphatex/src/staffDisplay.ts diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1EnumMappings.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1EnumMappings.ts index a3fe7fab71..401d24a217 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTex1EnumMappings.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTex1EnumMappings.ts @@ -1,4 +1,9 @@ -import type { AlphaTexAccidentalMode, AlphaTexVoiceMode } from '@coderline/alphatab/importer/alphaTex/AlphaTexShared'; +import type { TabRhythmMode } from '@coderline/alphatab/NotationSettings'; +import type { + AlphaTexAccidentalMode, + AlphaTexStaffType, + AlphaTexVoiceMode +} from '@coderline/alphatab/importer/alphaTex/AlphaTexShared'; import type { BarLineStyle } from '@coderline/alphatab/model/Bar'; import type { BarreShape } from '@coderline/alphatab/model/BarreShape'; import type { BendStyle } from '@coderline/alphatab/model/BendStyle'; @@ -6,6 +11,7 @@ import type { BendType } from '@coderline/alphatab/model/BendType'; import type { Clef } from '@coderline/alphatab/model/Clef'; import type { Direction } from '@coderline/alphatab/model/Direction'; import type { DynamicValue } from '@coderline/alphatab/model/DynamicValue'; +import type { StaffPlacement, SystemDisplay } from '@coderline/alphatab/model/ElementDisplay'; import type { FermataType } from '@coderline/alphatab/model/Fermata'; import type { GraceType } from '@coderline/alphatab/model/GraceType'; import type { KeySignature } from '@coderline/alphatab/model/KeySignature'; @@ -84,6 +90,15 @@ export class AlphaTex1EnumMappings { public static readonly alphaTexVoiceModeReversed = AlphaTex1EnumMappings._reverse( AlphaTex1EnumMappings.alphaTexVoiceMode ); + public static readonly alphaTexStaffType = new Map([ + ['score', 0], + ['tabs', 1], + ['slash', 2], + ['numbered', 3] + ]); + public static readonly alphaTexStaffTypeReversed = AlphaTex1EnumMappings._reverse( + AlphaTex1EnumMappings.alphaTexStaffType + ); public static readonly noteAccidentalMode = new Map([ ['default', 0], ['forcenone', 1], @@ -422,6 +437,25 @@ export class AlphaTex1EnumMappings { public static readonly barNumberDisplayReversed = AlphaTex1EnumMappings._reverse( AlphaTex1EnumMappings.barNumberDisplay ); + public static readonly staffPlacement = new Map([ + ['allstaves', 0], + ['primary', 1] + ]); + public static readonly staffPlacementReversed = AlphaTex1EnumMappings._reverse( + AlphaTex1EnumMappings.staffPlacement + ); + public static readonly systemDisplay = new Map([ + ['allsystems', 0], + ['firstsystemonly', 1] + ]); + public static readonly systemDisplayReversed = AlphaTex1EnumMappings._reverse(AlphaTex1EnumMappings.systemDisplay); + public static readonly tabRhythmMode = new Map([ + ['hidden', 0], + ['showwithbeams', 1], + ['showwithbars', 2], + ['automatic', 3] + ]); + public static readonly tabRhythmModeReversed = AlphaTex1EnumMappings._reverse(AlphaTex1EnumMappings.tabRhythmMode); public static readonly keySignaturesMinorReversed = new Map([ [-7, 'abminor'], [-6, 'ebminor'], diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts index 175ef83784..c2c23b33d8 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts @@ -195,7 +195,8 @@ export class AlphaTex1LanguageDefinitions { ['hideemptystaves', null], ['hideemptystavesinfirstsystem', null], ['showsinglestaffbrackets', null], - ['defaultbarnumberdisplay', [[[[100, 107], 0, ['allbars', 'firstofsystem', 'hide']]]]] + ['defaultbarnumberdisplay', [[[[100, 107], 0, ['allbars', 'firstofsystem', 'hide']]]]], + ['defaultstaffdisplay', [[[[100, 107], 5, ['score', 'tabs', 'slash', 'numbered']]]]] ]); public static readonly staffMetaDataSignatures = AlphaTex1LanguageDefinitions._signatures([ ['tuning', [[[[100, 107], 0, ['piano', 'none', 'voice']]], [[[100, 107], 5]]]], @@ -231,7 +232,8 @@ export class AlphaTex1LanguageDefinitions { ], ['displaytranspose', [[[[106], 0]]]], ['transpose', [[[[106], 0]]]], - ['instrument', [[[[106], 0]], [[[107, 100], 0]], [[[100], 0, ['percussion']]]]] + ['instrument', [[[[106], 0]], [[[107, 100], 0]], [[[100], 0, ['percussion']]]]], + ['staffdisplay', [[[[100, 107], 5, ['score', 'tabs', 'slash', 'numbered']]]]] ]); public static readonly structuralMetaDataSignatures = AlphaTex1LanguageDefinitions._signatures([ [ @@ -491,7 +493,8 @@ export class AlphaTex1LanguageDefinitions { ] ] ], - ['barnumber', [[[[107, 100], 0]], [[[106], 0]]]] + ['barnumber', [[[[107, 100], 0]], [[[106], 0]]]], + ['bardisplay', [[[[100, 107], 5, ['score', 'tabs', 'slash', 'numbered']]]]] ]); public static readonly metaDataProperties = AlphaTex1LanguageDefinitions._metaProps([ [ @@ -551,6 +554,37 @@ export class AlphaTex1LanguageDefinitions { ['hideemptystavesinfirstsystem', null], ['showsinglestaffbrackets', null], ['defaultbarnumberdisplay', null], + [ + 'defaultstaffdisplay', + [ + [ + 'clefvisibility', + [[], [[[107], 0, ['true', 'false']]], [[[100], 0, ['true', 'false']]], [[[106], 0, ['1', '0']]]] + ], + ['clefplacement', [[[[100, 107], 0, ['allstaves', 'primary']]]]], + ['clefsystems', [[[[100, 107], 0, ['allsystems', 'firstsystemonly']]]]], + [ + 'ksvisibility', + [[], [[[107], 0, ['true', 'false']]], [[[100], 0, ['true', 'false']]], [[[106], 0, ['1', '0']]]] + ], + ['ksplacement', [[[[100, 107], 0, ['allstaves', 'primary']]]]], + ['kssystems', [[[[100, 107], 0, ['allsystems', 'firstsystemonly']]]]], + [ + 'tsvisibility', + [[], [[[107], 0, ['true', 'false']]], [[[100], 0, ['true', 'false']]], [[[106], 0, ['1', '0']]]] + ], + ['tsplacement', [[[[100, 107], 0, ['allstaves', 'primary']]]]], + ['tssystems', [[[[100, 107], 0, ['allsystems', 'firstsystemonly']]]]], + [ + 'restsvisibility', + [[], [[[107], 0, ['true', 'false']]], [[[100], 0, ['true', 'false']]], [[[106], 0, ['1', '0']]]] + ], + ['restsplacement', [[[[100, 107], 0, ['allstaves', 'primary']]]]], + ['restssystems', [[[[100, 107], 0, ['allsystems', 'firstsystemonly']]]]], + ['barnumber', [[[[100, 107], 0, ['allbars', 'firstofsystem', 'hide']]]]], + ['rhythm', [[[[100, 107], 0, ['hidden', 'showwithbeams', 'showwithbars', 'automatic']]]]] + ] + ], [ 'tuning', [ @@ -583,6 +617,37 @@ export class AlphaTex1LanguageDefinitions { ['displaytranspose', null], ['transpose', null], ['instrument', null], + [ + 'staffdisplay', + [ + [ + 'clefvisibility', + [[], [[[107], 0, ['true', 'false']]], [[[100], 0, ['true', 'false']]], [[[106], 0, ['1', '0']]]] + ], + ['clefplacement', [[[[100, 107], 0, ['allstaves', 'primary']]]]], + ['clefsystems', [[[[100, 107], 0, ['allsystems', 'firstsystemonly']]]]], + [ + 'ksvisibility', + [[], [[[107], 0, ['true', 'false']]], [[[100], 0, ['true', 'false']]], [[[106], 0, ['1', '0']]]] + ], + ['ksplacement', [[[[100, 107], 0, ['allstaves', 'primary']]]]], + ['kssystems', [[[[100, 107], 0, ['allsystems', 'firstsystemonly']]]]], + [ + 'tsvisibility', + [[], [[[107], 0, ['true', 'false']]], [[[100], 0, ['true', 'false']]], [[[106], 0, ['1', '0']]]] + ], + ['tsplacement', [[[[100, 107], 0, ['allstaves', 'primary']]]]], + ['tssystems', [[[[100, 107], 0, ['allsystems', 'firstsystemonly']]]]], + [ + 'restsvisibility', + [[], [[[107], 0, ['true', 'false']]], [[[100], 0, ['true', 'false']]], [[[106], 0, ['1', '0']]]] + ], + ['restsplacement', [[[[100, 107], 0, ['allstaves', 'primary']]]]], + ['restssystems', [[[[100, 107], 0, ['allsystems', 'firstsystemonly']]]]], + ['barnumber', [[[[100, 107], 0, ['allbars', 'firstofsystem', 'hide']]]]], + ['rhythm', [[[[100, 107], 0, ['hidden', 'showwithbeams', 'showwithbars', 'automatic']]]]] + ] + ], ['ts', null], ['ro', null], ['rc', null], @@ -610,7 +675,31 @@ export class AlphaTex1LanguageDefinitions { ['voicemode', null], ['barnumberdisplay', null], ['beaming', null], - ['barnumber', null] + ['barnumber', null], + [ + 'bardisplay', + [ + [ + 'clefvisibility', + [[], [[[107], 0, ['true', 'false']]], [[[100], 0, ['true', 'false']]], [[[106], 0, ['1', '0']]]] + ], + ['clefplacement', [[[[100, 107], 0, ['allstaves', 'primary']]]]], + ['clefsystems', [[[[100, 107], 0, ['allsystems', 'firstsystemonly']]]]], + [ + 'ksvisibility', + [[], [[[107], 0, ['true', 'false']]], [[[100], 0, ['true', 'false']]], [[[106], 0, ['1', '0']]]] + ], + ['ksplacement', [[[[100, 107], 0, ['allstaves', 'primary']]]]], + ['kssystems', [[[[100, 107], 0, ['allsystems', 'firstsystemonly']]]]], + [ + 'tsvisibility', + [[], [[[107], 0, ['true', 'false']]], [[[100], 0, ['true', 'false']]], [[[106], 0, ['1', '0']]]] + ], + ['tsplacement', [[[[100, 107], 0, ['allstaves', 'primary']]]]], + ['tssystems', [[[[100, 107], 0, ['allsystems', 'firstsystemonly']]]]], + ['barnumber', [[[[100, 107], 0, ['allbars', 'firstofsystem', 'hide']]]]] + ] + ] ]); public static readonly metaDataSignatures = [ AlphaTex1LanguageDefinitions.scoreMetaDataSignatures, diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTexShared.ts b/packages/alphatab/src/importer/alphaTex/AlphaTexShared.ts index 51c50b8803..9477906c2d 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTexShared.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTexShared.ts @@ -237,6 +237,11 @@ export enum AlphaTexDiagnosticCode { */ AT306 = 306, + /** + * The staff display property is not supported by the staff type, the value has no effect. + */ + AT307 = 307, + /** * The dots separating score metadata, score contents and the sync points can be removed. */ @@ -286,6 +291,30 @@ export enum AlphaTexVoiceMode { BarWise = 1 } +/** + * The staff types to which the alphaTex staff display tags + * (`\defaultStaffDisplay`, `\staffDisplay`, `\barDisplay`) apply. + * @public + */ +export enum AlphaTexStaffType { + /** + * The standard notation staff. + */ + Score = 0, + /** + * The guitar tablature staff. + */ + Tabs = 1, + /** + * The slash notation staff. + */ + Slash = 2, + /** + * The numbered notation (Jianpu) staff. + */ + Numbered = 3 +} + /** * @public */ diff --git a/packages/alphatab/src/importer/alphaTex/_barrel.ts b/packages/alphatab/src/importer/alphaTex/_barrel.ts index b814cfddf7..fe034e20b0 100644 --- a/packages/alphatab/src/importer/alphaTex/_barrel.ts +++ b/packages/alphatab/src/importer/alphaTex/_barrel.ts @@ -44,6 +44,7 @@ export { AlphaTexDiagnosticCode, AlphaTexDiagnosticsSeverity, AlphaTexStaffNoteKind, + AlphaTexStaffType, AlphaTexVoiceMode, ArgumentListParseTypesMode, type IAlphaTexImporter, diff --git a/packages/alphatex/src/common.ts b/packages/alphatex/src/common.ts index c57640b534..75de4dcda4 100644 --- a/packages/alphatex/src/common.ts +++ b/packages/alphatex/src/common.ts @@ -1,4 +1,9 @@ -import type { MetadataTagDefinition, PropertyDefinition } from '@coderline/alphatab-alphatex/types'; +import * as alphaTab from '@coderline/alphatab'; +import type { + MetadataTagDefinition, + PropertyDefinition, + SignatureDefinition +} from '@coderline/alphatab-alphatex/types'; export const generalMidiInstruments = [ 'Acoustic Grand Piano', @@ -138,3 +143,61 @@ export function properties(...props: PropertyDefinition[]): Map(metadata.map(t => [t.tag.toLowerCase(), t])); } + +/** + * Creates the signatures for a boolean-like value: no value (= true), `true|false`, `"true"|"false"` or `1|0`. + */ +export function booleanSignatures( + name: string, + shortDescription: string, + trueDescription: string, + falseDescription: string +): SignatureDefinition[] { + return [ + { + parameters: [] + }, + { + parameters: [ + { + name, + shortDescription, + parseMode: alphaTab.importer.alphaTex.ArgumentListParseTypesMode.Required, + type: alphaTab.importer.alphaTex.AlphaTexNodeType.String, + values: [ + { name: 'true', snippet: '"true"', shortDescription: trueDescription }, + { name: 'false', snippet: '"false"', shortDescription: falseDescription } + ] + } + ] + }, + { + parameters: [ + { + name, + shortDescription, + parseMode: alphaTab.importer.alphaTex.ArgumentListParseTypesMode.Required, + type: alphaTab.importer.alphaTex.AlphaTexNodeType.Ident, + values: [ + { name: 'true', snippet: 'true', shortDescription: trueDescription }, + { name: 'false', snippet: 'false', shortDescription: falseDescription } + ] + } + ] + }, + { + parameters: [ + { + name, + shortDescription, + parseMode: alphaTab.importer.alphaTex.ArgumentListParseTypesMode.Required, + type: alphaTab.importer.alphaTex.AlphaTexNodeType.Number, + values: [ + { name: '1', snippet: '1', shortDescription: trueDescription }, + { name: '0', snippet: '0', shortDescription: falseDescription } + ] + } + ] + } + ]; +} diff --git a/packages/alphatex/src/definitions.ts b/packages/alphatex/src/definitions.ts index e6227fe9ee..0770afb988 100644 --- a/packages/alphatex/src/definitions.ts +++ b/packages/alphatex/src/definitions.ts @@ -161,6 +161,9 @@ import { barNumberDisplay } from '@coderline/alphatab-alphatex/metadata/bar/barn import { beaming } from '@coderline/alphatab-alphatex/metadata/bar/beamingRule'; import { restDisplayPitch } from '@coderline/alphatab-alphatex/properties/beat/restDisplayPitch'; import { barNumber } from '@coderline/alphatab-alphatex/metadata/bar/barNumber'; +import { defaultStaffDisplay } from '@coderline/alphatab-alphatex/metadata/score/defaultStaffDisplay'; +import { staffDisplay } from '@coderline/alphatab-alphatex/metadata/staff/staffDisplay'; +import { barDisplay } from '@coderline/alphatab-alphatex/metadata/bar/barDisplay'; export const structuralMetaData = metadata(track, staff, voice); export const scoreMetaData = metadata( @@ -195,7 +198,8 @@ export const scoreMetaData = metadata( hideEmptyStaves, hideEmptyStavesInFirstSystem, showSingleStaffBrackets, - defaultBarNumberDisplay + defaultBarNumberDisplay, + defaultStaffDisplay ); export const staffMetaData = metadata( @@ -206,7 +210,8 @@ export const staffMetaData = metadata( articulation, displayTranspose, transpose, - instrumentMeta + instrumentMeta, + staffDisplay ); export const barMetaData = metadata( @@ -237,7 +242,8 @@ export const barMetaData = metadata( voiceMode, barNumberDisplay, beaming, - barNumber + barNumber, + barDisplay ); export const allMetadata = new Map([ diff --git a/packages/alphatex/src/enum.ts b/packages/alphatex/src/enum.ts index 65331f264b..57abe42171 100644 --- a/packages/alphatex/src/enum.ts +++ b/packages/alphatex/src/enum.ts @@ -10,6 +10,7 @@ export const alphaTexMappedEnumLookup = { FermataType: alphaTab.model.FermataType, AlphaTexAccidentalMode: alphaTab.importer.alphaTex.AlphaTexAccidentalMode, AlphaTexVoiceMode: alphaTab.importer.alphaTex.AlphaTexVoiceMode, + AlphaTexStaffType: alphaTab.importer.alphaTex.AlphaTexStaffType, NoteAccidentalMode: alphaTab.model.NoteAccidentalMode, BarreShape: alphaTab.model.BarreShape, Ottavia: alphaTab.model.Ottavia, @@ -30,7 +31,10 @@ export const alphaTexMappedEnumLookup = { SimileMark: alphaTab.model.SimileMark, Direction: alphaTab.model.Direction, TremoloPickingStyle: alphaTab.model.TremoloPickingStyle, - BarNumberDisplay: alphaTab.model.BarNumberDisplay + BarNumberDisplay: alphaTab.model.BarNumberDisplay, + StaffPlacement: alphaTab.model.StaffPlacement, + SystemDisplay: alphaTab.model.SystemDisplay, + TabRhythmMode: alphaTab.TabRhythmMode }; export type AlphaTexMappedEnumName = keyof typeof alphaTexMappedEnumLookup; @@ -122,6 +126,12 @@ export const alphaTexMappedEnumMapping: { longDescription: 'A new voice adds a new voice to the current bar only.' } }, + AlphaTexStaffType: { + Score: { snippet: 'score', shortDescription: 'The standard notation staff' }, + Tabs: { snippet: 'tabs', shortDescription: 'The guitar tablature staff' }, + Slash: { snippet: 'slash', shortDescription: 'The slash notation staff' }, + Numbered: { snippet: 'numbered', shortDescription: 'The numbered notation (Jianpu) staff' } + }, NoteAccidentalMode: { Default: { snippet: 'default', shortDescription: 'Auto-detect the accidentals', aliases: ['d'] }, ForceNone: { snippet: 'forceNone', shortDescription: 'Force no accidentals', aliases: ['-'] }, @@ -464,6 +474,34 @@ export const alphaTexMappedEnumMapping: { AllBars: { snippet: 'allBars', shortDescription: 'All bars' }, FirstOfSystem: { snippet: 'firstOfSystem', shortDescription: 'First bar of every system' }, Hide: { snippet: 'hide', shortDescription: 'Hide' } + }, + StaffPlacement: { + AllStaves: { snippet: 'allStaves', shortDescription: 'Show the element on all staves' }, + Primary: { + snippet: 'primary', + shortDescription: 'Show the element only on the primary staff', + longDescription: + 'Show the element only on the topmost displayed notation of the staff (priority: score, tabs, slash, numbered).' + } + }, + SystemDisplay: { + AllSystems: { snippet: 'allSystems', shortDescription: 'Show the element on all systems' }, + FirstSystemOnly: { snippet: 'firstSystemOnly', shortDescription: 'Show the element only on the first system' } + }, + TabRhythmMode: { + Hidden: { snippet: 'hidden', shortDescription: 'Hide the rhythm notation' }, + ShowWithBeams: { + snippet: 'showWithBeams', + shortDescription: 'Show rhythm notation with individual beams per beat' + }, + ShowWithBars: { + snippet: 'showWithBars', + shortDescription: 'Show rhythm notation with connected bars like in standard notation' + }, + Automatic: { + snippet: 'automatic', + shortDescription: 'Show rhythm notation automatically if no standard notation is shown' + } } }; diff --git a/packages/alphatex/src/metadata/bar/barDisplay.ts b/packages/alphatex/src/metadata/bar/barDisplay.ts new file mode 100644 index 0000000000..f1fc98e130 --- /dev/null +++ b/packages/alphatex/src/metadata/bar/barDisplay.ts @@ -0,0 +1,24 @@ +import type { MetadataTagDefinition } from '@coderline/alphatab-alphatex/types'; +import { staffDisplayProperties, staffDisplayTargetSignatures } from '@coderline/alphatab-alphatex/staffDisplay'; + +export const barDisplay: MetadataTagDefinition = { + tag: '\\barDisplay', + snippet: '\\barDisplay $1 {$2}$0', + shortDescription: 'Sets how elements are displayed on the current bar.', + longDescription: ` + Defines the display of elements like clefs, key signatures, time signatures and bar numbers for the current bar. + + The arguments select the staff types to which the properties apply. If no staff types are specified, every property applies to all staff types supporting the related element. + + All properties are optional. Properties which are not specified are inherited from [\`\\staffDisplay\`](https://alphatab.net/docs/alphatex/staff-metadata#staffdisplay) and [\`\\defaultStaffDisplay\`](https://alphatab.net/docs/alphatex/score-metadata#defaultstaffdisplay). The values only apply to the bar they are specified on. + `, + signatures: staffDisplayTargetSignatures(), + examples: ` + \\track + \\staff {score tabs} + \\ks D 3.3.4 3.3 3.3 3.3 | + \\barDisplay score { ksVisibility false } \\ks A 3.3 3.3 3.3 3.3 | + 3.3 3.3 3.3 3.3 + `, + properties: staffDisplayProperties('\\barDisplay', 'bar') +}; diff --git a/packages/alphatex/src/metadata/score/defaultStaffDisplay.ts b/packages/alphatex/src/metadata/score/defaultStaffDisplay.ts new file mode 100644 index 0000000000..e6f66a9f2a --- /dev/null +++ b/packages/alphatex/src/metadata/score/defaultStaffDisplay.ts @@ -0,0 +1,24 @@ +import type { MetadataTagDefinition } from '@coderline/alphatab-alphatex/types'; +import { staffDisplayProperties, staffDisplayTargetSignatures } from '@coderline/alphatab-alphatex/staffDisplay'; + +export const defaultStaffDisplay: MetadataTagDefinition = { + tag: '\\defaultStaffDisplay', + snippet: '\\defaultStaffDisplay $1 {$2}$0', + shortDescription: 'Sets how elements are displayed on the staves of the whole song.', + longDescription: ` + Defines the song-wide display of elements like clefs, key signatures, time signatures, rests and bar numbers. + + The arguments select the staff types to which the properties apply. If no staff types are specified, every property applies to all staff types supporting the related element. + + All properties are optional. Properties which are not specified keep their default. The values can be overridden per staff with [\`\\staffDisplay\`](https://alphatab.net/docs/alphatex/staff-metadata#staffdisplay) and per bar with [\`\\barDisplay\`](https://alphatab.net/docs/alphatex/bar-metadata#bardisplay). + `, + signatures: staffDisplayTargetSignatures(), + examples: ` + \\defaultStaffDisplay { clefPlacement primary } + \\defaultStaffDisplay tabs { tsVisibility true tsPlacement allStaves } + \\track + \\staff {score tabs} + 3.3.4 3.3 3.3 3.3 | 3.3 3.3 3.3 3.3 + `, + properties: staffDisplayProperties('\\defaultStaffDisplay', 'score') +}; diff --git a/packages/alphatex/src/metadata/staff/staffDisplay.ts b/packages/alphatex/src/metadata/staff/staffDisplay.ts new file mode 100644 index 0000000000..5c781b34a0 --- /dev/null +++ b/packages/alphatex/src/metadata/staff/staffDisplay.ts @@ -0,0 +1,23 @@ +import type { MetadataTagDefinition } from '@coderline/alphatab-alphatex/types'; +import { staffDisplayProperties, staffDisplayTargetSignatures } from '@coderline/alphatab-alphatex/staffDisplay'; + +export const staffDisplay: MetadataTagDefinition = { + tag: '\\staffDisplay', + snippet: '\\staffDisplay $1 {$2}$0', + shortDescription: 'Sets how elements are displayed on the current staff.', + longDescription: ` + Defines the display of elements like clefs, key signatures, time signatures, rests and bar numbers for the current staff. + + The arguments select the staff types to which the properties apply. If no staff types are specified, every property applies to all staff types supporting the related element. + + All properties are optional. Properties which are not specified are inherited from [\`\\defaultStaffDisplay\`](https://alphatab.net/docs/alphatex/score-metadata#defaultstaffdisplay). The values can be overridden per bar with [\`\\barDisplay\`](https://alphatab.net/docs/alphatex/bar-metadata#bardisplay). + `, + signatures: staffDisplayTargetSignatures(), + examples: ` + \\track + \\staff {score tabs} + \\staffDisplay tabs { tsPlacement allStaves rhythm showWithBeams } + 3.3.4 3.3 3.3 3.3 | 3.3 3.3 3.3 3.3 + `, + properties: staffDisplayProperties('\\staffDisplay', 'staff') +}; diff --git a/packages/alphatex/src/staffDisplay.ts b/packages/alphatex/src/staffDisplay.ts new file mode 100644 index 0000000000..9510d09f77 --- /dev/null +++ b/packages/alphatex/src/staffDisplay.ts @@ -0,0 +1,297 @@ +import * as alphaTab from '@coderline/alphatab'; +import { booleanSignatures, properties } from '@coderline/alphatab-alphatex/common'; +import { enumParameter } from '@coderline/alphatab-alphatex/enum'; +import type { AlphaTexExample, PropertyDefinition, SignatureDefinition } from '@coderline/alphatab-alphatex/types'; + +/** + * The level on which a staff display tag applies its properties: + * score-wide (`\defaultStaffDisplay`), per staff (`\staffDisplay`) or per bar (`\barDisplay`). + */ +export type StaffDisplayLevel = 'score' | 'staff' | 'bar'; + +/** + * Creates the signatures selecting the staff types to which the staff display properties apply. + */ +export function staffDisplayTargetSignatures(): SignatureDefinition[] { + return [ + { + parameters: [ + { + name: 'staffTypes', + shortDescription: 'The staff types to which the properties apply', + longDescription: `The staff types to which the properties apply. If omitted, every property applies to all staff types supporting the related element.`, + parseMode: alphaTab.importer.alphaTex.ArgumentListParseTypesMode.ValueListWithoutParenthesis, + ...enumParameter('AlphaTexStaffType') + } + ] + } + ]; +} + +/** + * The sample content used to build the examples of a staff display property. + */ +interface StaffDisplaySample { + /** + * The staff types to apply the properties to (empty for all). + */ + target: string; + /** + * The properties to apply. + */ + props: string; + /** + * The notations to show on the staff (`\staff` properties). + */ + staff: string; + /** + * The bars of the example, the staff display tag is placed on the second bar for bar level examples. + */ + bars: string[]; + /** + * Whether the example needs multiple systems to show the effect. + */ + systems?: true; +} + +function staffDisplayExample(tag: string, level: StaffDisplayLevel, sample: StaffDisplaySample): AlphaTexExample { + const meta = `${tag}${sample.target ? ` ${sample.target}` : ''} { ${sample.props} }`; + const lines: string[] = []; + if (level === 'score') { + lines.push(meta); + } + lines.push(sample.systems ? '\\track { defaultSystemsLayout 2 }' : '\\track'); + lines.push(` \\staff {${sample.staff}}`); + if (level === 'staff') { + lines.push(` ${meta}`); + } + if (level === 'bar') { + lines.push(` ${sample.bars[0]} |`); + lines.push(` ${meta} ${sample.bars.slice(1).join(' | ')}`); + } else { + lines.push(` ${sample.bars.join(' | ')}`); + } + + const tex = lines.join('\n'); + return sample.systems ? { options: { display: { layoutMode: 'Parchment' } }, tex } : tex; +} + +/** + * Describes an element which is configured via the `Visibility`, `Placement` and + * `Systems` properties. + */ +interface StaffDisplayElement { + prefix: string; + name: string; + staffTypes: string; + barLevel: boolean; + visibility: StaffDisplaySample; + placement: StaffDisplaySample; + systems: StaffDisplaySample; +} + +const tabBars = ['3.3.4 3.3 3.3 3.3', '3.3 3.3 3.3 3.3', '3.3 3.3 3.3 3.3', '3.3 3.3 3.3 3.3']; +const tabRestBars = ['3.3.4 r 3.3 r', 'r 3.3 r 3.3', '3.3 r 3.3 r', 'r 3.3 r 3.3']; + +const staffDisplayElements: StaffDisplayElement[] = [ + { + prefix: 'clef', + name: 'clef', + staffTypes: '`score`, `tabs`', + barLevel: true, + visibility: { target: 'tabs', props: 'clefVisibility false', staff: 'score tabs', bars: tabBars.slice(0, 2) }, + placement: { target: '', props: 'clefPlacement primary', staff: 'score tabs', bars: tabBars.slice(0, 2) }, + systems: { + target: '', + props: 'clefSystems firstSystemOnly', + staff: 'score tabs', + bars: tabBars, + systems: true + } + }, + { + prefix: 'ks', + name: 'key signature', + staffTypes: '`score`, `slash`', + barLevel: true, + visibility: { + target: 'slash', + props: 'ksVisibility true', + staff: 'score slash', + bars: ['\\ks D 3.3.4 3.3 3.3 3.3', '\\ks A 3.3 3.3 3.3 3.3'] + }, + placement: { + target: '', + props: 'ksVisibility true ksPlacement primary', + staff: 'score slash', + bars: ['\\ks D 3.3.4 3.3 3.3 3.3', '\\ks A 3.3 3.3 3.3 3.3'] + }, + systems: { + target: '', + props: 'ksSystems firstSystemOnly', + staff: 'score', + bars: ['\\ks D 3.3.4 3.3 3.3 3.3', '3.3 3.3 3.3 3.3', '3.3 3.3 3.3 3.3', '3.3 3.3 3.3 3.3'], + systems: true + } + }, + { + prefix: 'ts', + name: 'time signature', + staffTypes: '`score`, `tabs`, `slash`, `numbered`', + barLevel: true, + visibility: { target: 'score', props: 'tsVisibility false', staff: 'score tabs', bars: tabBars.slice(0, 2) }, + placement: { target: 'tabs', props: 'tsPlacement allStaves', staff: 'score tabs', bars: tabBars.slice(0, 2) }, + systems: { + target: '', + props: 'tsSystems firstSystemOnly', + staff: 'score tabs', + bars: ['3.3.4 3.3 3.3 3.3', '3.3 3.3 3.3 3.3', '\\ts 3 4 3.3.4 3.3 3.3', '3.3 3.3 3.3'], + systems: true + } + }, + { + prefix: 'rests', + name: 'rests', + staffTypes: '`tabs`', + barLevel: false, + visibility: { target: '', props: 'restsVisibility false', staff: 'tabs', bars: tabRestBars.slice(0, 2) }, + placement: { + target: '', + props: 'restsPlacement allStaves', + staff: 'score tabs', + bars: tabRestBars.slice(0, 2) + }, + systems: { target: '', props: 'restsSystems firstSystemOnly', staff: 'tabs', bars: tabRestBars, systems: true } + } +]; + +function staffDisplayElementProperties( + tag: string, + level: StaffDisplayLevel, + element: StaffDisplayElement +): PropertyDefinition[] { + const supported = `Supported staff types: ${element.staffTypes}.`; + return [ + { + property: `${element.prefix}Visibility`, + snippet: `${element.prefix}Visibility $0`, + shortDescription: `Show or hide the ${element.name}.`, + longDescription: `Defines whether the ${element.name} is shown. ${supported}`, + signatures: booleanSignatures( + 'visibility', + `Whether the ${element.name} is shown`, + `Show the ${element.name}`, + `Hide the ${element.name}` + ), + examples: staffDisplayExample(tag, level, element.visibility) + }, + { + property: `${element.prefix}Placement`, + snippet: `${element.prefix}Placement $0`, + shortDescription: `Set on which staves the ${element.name} is shown.`, + longDescription: `Defines whether the ${element.name} is shown on all staves or only on the primary staff (the topmost displayed notation of the staff). ${supported}`, + signatures: [ + { + parameters: [ + { + name: 'placement', + shortDescription: `On which staves the ${element.name} is shown`, + parseMode: alphaTab.importer.alphaTex.ArgumentListParseTypesMode.Required, + ...enumParameter('StaffPlacement') + } + ] + } + ], + examples: staffDisplayExample(tag, level, element.placement) + }, + { + property: `${element.prefix}Systems`, + snippet: `${element.prefix}Systems $0`, + shortDescription: `Set on which systems the ${element.name} is shown.`, + longDescription: `Defines whether the ${element.name} is shown on all systems or only on the first system. ${supported}`, + signatures: [ + { + parameters: [ + { + name: 'systems', + shortDescription: `On which systems the ${element.name} is shown`, + parseMode: alphaTab.importer.alphaTex.ArgumentListParseTypesMode.Required, + ...enumParameter('SystemDisplay') + } + ] + } + ], + examples: staffDisplayExample(tag, level, element.systems) + } + ]; +} + +/** + * Creates the properties of a staff display tag. A new set of definitions is created for every tag, + * as the definitions are prepared in place and the examples use the respective tag. + * Bar level tags only support the elements which can be overridden on bar level. + */ +export function staffDisplayProperties(tag: string, level: StaffDisplayLevel): Map { + const props: PropertyDefinition[] = []; + for (const element of staffDisplayElements) { + if (level !== 'bar' || element.barLevel) { + props.push(...staffDisplayElementProperties(tag, level, element)); + } + } + + props.push({ + property: 'barNumber', + snippet: 'barNumber $0', + shortDescription: 'Set how bar numbers are shown.', + longDescription: + 'Defines how bar numbers are shown. Supported staff types: `score`, `tabs`, `slash`, `numbered`.', + signatures: [ + { + parameters: [ + { + name: 'mode', + shortDescription: 'The mode to use', + parseMode: alphaTab.importer.alphaTex.ArgumentListParseTypesMode.Required, + ...enumParameter('BarNumberDisplay') + } + ] + } + ], + examples: staffDisplayExample(tag, level, { + target: '', + props: 'barNumber allBars', + staff: 'score tabs', + bars: tabBars, + systems: true + }) + }); + + if (level !== 'bar') { + props.push({ + property: 'rhythm', + snippet: 'rhythm $0', + shortDescription: 'Set how rhythm notation is shown on tabs.', + longDescription: 'Defines how the rhythm notation is shown on tabs. Supported staff types: `tabs`.', + signatures: [ + { + parameters: [ + { + name: 'mode', + shortDescription: 'The mode to use', + parseMode: alphaTab.importer.alphaTex.ArgumentListParseTypesMode.Required, + ...enumParameter('TabRhythmMode') + } + ] + } + ], + examples: staffDisplayExample(tag, level, { + target: '', + props: 'rhythm showWithBeams', + staff: 'tabs', + bars: tabBars.slice(0, 2) + }) + }); + } + + return properties(...props); +} From f1ed7dc1545a91f5d2efb5b5bb5cbaf4394a4779 Mon Sep 17 00:00:00 2001 From: danielku15 Date: Fri, 9 Oct 2026 17:59:27 +0200 Subject: [PATCH 2/8] feat(alphatex): import and export staff display configs and bar overrides One shared applier writes the staff display properties per field into the stylesheet, staff or bar carriers of the targeted staff types, and one shared builder exports all three levels. Equal values are grouped across staff types, the staff types are omitted when all supporting types are affected, and uniform bar number displays keep using the existing \defaultBarNumberDisplay/\barNumberDisplay shorthands. Replaces the export via the deprecated barNumberDisplay getters, which only covered the standard notation values. Refs #2912 --- .../alphaTex/AlphaTex1LanguageHandler.ts | 685 +++++++++++++++++- 1 file changed, 672 insertions(+), 13 deletions(-) diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts index c09255fb39..4947933953 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts @@ -31,6 +31,7 @@ import { AlphaTexDiagnosticCode, AlphaTexDiagnosticsSeverity, AlphaTexStaffNoteKind, + AlphaTexStaffType, ArgumentListParseTypesMode, type IAlphaTexImporter } from '@coderline/alphatab/importer/alphaTex/AlphaTexShared'; @@ -70,6 +71,7 @@ import { NoteOrnament } from '@coderline/alphatab/model/NoteOrnament'; import { Ottavia } from '@coderline/alphatab/model/Ottavia'; import { PercussionMapper } from '@coderline/alphatab/model/PercussionMapper'; import { PickStroke } from '@coderline/alphatab/model/PickStroke'; +import type { ElementDisplay } from '@coderline/alphatab/model/ElementDisplay'; import { BarNumberDisplay, type RenderStylesheet, TuningDisplayMode } from '@coderline/alphatab/model/RenderStylesheet'; import { HeaderFooterStyle, Score, ScoreStyle, ScoreSubElement } from '@coderline/alphatab/model/Score'; import { Section } from '@coderline/alphatab/model/Section'; @@ -77,6 +79,12 @@ import { SimileMark } from '@coderline/alphatab/model/SimileMark'; import { SlideInType } from '@coderline/alphatab/model/SlideInType'; import { SlideOutType } from '@coderline/alphatab/model/SlideOutType'; import { Staff } from '@coderline/alphatab/model/Staff'; +import type { + NumberedStaffConfig, + ScoreStaffConfig, + SlashStaffConfig, + TabStaffConfig +} from '@coderline/alphatab/model/StaffConfigs'; import { Track } from '@coderline/alphatab/model/Track'; import { TremoloPickingEffect, TremoloPickingStyle } from '@coderline/alphatab/model/TremoloPickingEffect'; import { TripletFeel } from '@coderline/alphatab/model/TripletFeel'; @@ -84,8 +92,24 @@ import { Tuning } from '@coderline/alphatab/model/Tuning'; import { VibratoType } from '@coderline/alphatab/model/VibratoType'; import { WahPedal } from '@coderline/alphatab/model/WahPedal'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; +import type { TabRhythmMode } from '@coderline/alphatab/NotationSettings'; import { SynthConstants } from '@coderline/alphatab/synth/SynthConstants'; +/** + * The values read from the properties of a staff display tag + * (`\defaultStaffDisplay`, `\staffDisplay`, `\barDisplay`). + * @internal + * @record + */ +interface AlphaTexStaffDisplayValues { + clef?: ElementDisplay; + keySignature?: ElementDisplay; + timeSignature?: ElementDisplay; + rests?: ElementDisplay; + barNumber?: BarNumberDisplay; + rhythm?: TabRhythmMode; +} + /** * @internal */ @@ -300,6 +324,9 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler } score.stylesheet.barNumberDisplay = barNumberDisplay!; return ApplyNodeResult.Applied; + case 'defaultstaffdisplay': + this._applyStaffDisplay(importer, metaData, score.stylesheet, undefined, undefined); + return ApplyNodeResult.Applied; default: return ApplyNodeResult.NotAppliedUnrecognizedMarker; @@ -431,6 +458,9 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler importer.state.staffTuningApplied.delete(staff); this._readTrackInstrument(importer, staff.track, metaData.arguments!); + return ApplyNodeResult.Applied; + case 'staffdisplay': + this._applyStaffDisplay(importer, metaData, undefined, staff, undefined); return ApplyNodeResult.Applied; case 'bank': staff.track.playbackInfo.bank = (metaData.arguments!.arguments[0] as AlphaTexNumberLiteral).value; @@ -922,6 +952,9 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler } bar.barNumberDisplay = barNumberDisplay!; return ApplyNodeResult.Applied; + case 'bardisplay': + this._applyStaffDisplay(importer, metaData, undefined, undefined, bar); + return ApplyNodeResult.Applied; case 'barnumber': switch (metaData.arguments!.arguments[0].nodeType) { case AlphaTexNodeType.Number: @@ -1290,6 +1323,360 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler } } + // + // staff display (\defaultStaffDisplay, \staffDisplay, \barDisplay) + + private static readonly _staffDisplayTypes = [ + AlphaTexStaffType.Score, + AlphaTexStaffType.Tabs, + AlphaTexStaffType.Slash, + AlphaTexStaffType.Numbered + ]; + + /** + * The element configured by each staff display property. + */ + private static readonly _staffDisplayPropertyElements = new Map([ + ['clefvisibility', 'clef'], + ['clefplacement', 'clef'], + ['clefsystems', 'clef'], + ['ksvisibility', 'ks'], + ['ksplacement', 'ks'], + ['kssystems', 'ks'], + ['tsvisibility', 'ts'], + ['tsplacement', 'ts'], + ['tssystems', 'ts'], + ['restsvisibility', 'rests'], + ['restsplacement', 'rests'], + ['restssystems', 'rests'], + ['barnumber', 'barnumber'], + ['rhythm', 'rhythm'] + ]); + + /** + * The staff types supporting the elements configured via the staff display properties. + */ + private static readonly _staffDisplayElementTypes = new Map([ + ['clef', [AlphaTexStaffType.Score, AlphaTexStaffType.Tabs]], + ['ks', [AlphaTexStaffType.Score, AlphaTexStaffType.Slash]], + ['ts', AlphaTex1LanguageHandler._staffDisplayTypes], + ['rests', [AlphaTexStaffType.Tabs]], + ['barnumber', AlphaTex1LanguageHandler._staffDisplayTypes], + ['rhythm', [AlphaTexStaffType.Tabs]] + ]); + + private static _staffTypeNames(staffTypes: AlphaTexStaffType[]): string { + return staffTypes.map(t => AlphaTex1EnumMappings.alphaTexStaffTypeReversed.get(t)!).join(','); + } + + /** + * Applies a staff display tag to the stylesheet (score level), the staff (staff level) or the bar (bar level). + * The arguments select the staff types, without arguments all staff types supporting a property are affected. + */ + private _applyStaffDisplay( + importer: IAlphaTexImporter, + metaData: AlphaTexMetaDataNode, + stylesheet: RenderStylesheet | undefined, + staff: Staff | undefined, + bar: Bar | undefined + ) { + const explicitTypes = metaData.arguments !== undefined && metaData.arguments!.arguments.length > 0; + let staffTypes = AlphaTex1LanguageHandler._staffDisplayTypes; + if (explicitTypes) { + staffTypes = []; + for (let i = 0; i < metaData.arguments!.arguments.length; i++) { + const staffType = AlphaTex1LanguageHandler._parseEnumValue( + importer, + metaData.arguments!, + 'staff type', + AlphaTex1EnumMappings.alphaTexStaffType, + i + ); + if (staffType !== undefined) { + staffTypes.push(staffType); + } + } + } + + const values: AlphaTexStaffDisplayValues = {}; + if (metaData.properties) { + const lookup = AlphaTex1LanguageDefinitions.metaDataProperties.get(metaData.tag.tag.text.toLowerCase())!; + for (const p of metaData.properties!.properties) { + if (!this._checkProperty(importer, [lookup], p)) { + continue; + } + + const name = p.property.text.toLowerCase(); + if (!AlphaTex1LanguageHandler._readStaffDisplayProperty(importer, p, name, values)) { + continue; + } + + if (explicitTypes) { + const element = AlphaTex1LanguageHandler._staffDisplayPropertyElements.get(name)!; + const supported = AlphaTex1LanguageHandler._staffDisplayElementTypes.get(element)!; + const unsupported = staffTypes.filter(t => supported.indexOf(t) === -1); + if (unsupported.length > 0) { + importer.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT307, + message: `The property '${p.property.text}' is not supported by the staff types ${AlphaTex1LanguageHandler._staffTypeNames(unsupported)}, the value has no effect there. Supported staff types: ${AlphaTex1LanguageHandler._staffTypeNames(supported)}`, + severity: AlphaTexDiagnosticsSeverity.Warning, + start: p.start, + end: p.end + }); + } + } + } + } + + for (const staffType of staffTypes) { + AlphaTex1LanguageHandler._applyStaffDisplayValues(values, staffType, stylesheet, staff, bar); + } + } + + private static _readStaffDisplayProperty( + importer: IAlphaTexImporter, + p: AlphaTexPropertyNode, + name: string, + values: AlphaTexStaffDisplayValues + ): boolean { + switch (name) { + case 'barnumber': + const barNumber = AlphaTex1LanguageHandler._parseEnumValue( + importer, + p.arguments!, + 'bar number display', + AlphaTex1EnumMappings.barNumberDisplay + ); + if (barNumber === undefined) { + return false; + } + values.barNumber = barNumber; + return true; + case 'rhythm': + const rhythm = AlphaTex1LanguageHandler._parseEnumValue( + importer, + p.arguments!, + 'rhythm mode', + AlphaTex1EnumMappings.tabRhythmMode + ); + if (rhythm === undefined) { + return false; + } + values.rhythm = rhythm; + return true; + } + + const element = AlphaTex1LanguageHandler._staffDisplayPropertyElements.get(name)!; + let display: ElementDisplay; + switch (element) { + case 'clef': + values.clef ??= {}; + display = values.clef!; + break; + case 'ks': + values.keySignature ??= {}; + display = values.keySignature!; + break; + case 'ts': + values.timeSignature ??= {}; + display = values.timeSignature!; + break; + default: + values.rests ??= {}; + display = values.rests!; + break; + } + + if (name === `${element}visibility`) { + display.isVisible = AlphaTex1LanguageHandler._booleanLikeValue( + p.arguments ? p.arguments!.arguments : [], + 0 + ); + } else if (name === `${element}placement`) { + const placement = AlphaTex1LanguageHandler._parseEnumValue( + importer, + p.arguments!, + 'staff placement', + AlphaTex1EnumMappings.staffPlacement + ); + if (placement === undefined) { + return false; + } + display.staffPlacement = placement; + } else { + const systems = AlphaTex1LanguageHandler._parseEnumValue( + importer, + p.arguments!, + 'system display', + AlphaTex1EnumMappings.systemDisplay + ); + if (systems === undefined) { + return false; + } + display.systemDisplay = systems; + } + return true; + } + + /** + * Merges the specified axes of the values into the current display (creating it if needed). + */ + private static _mergeElementDisplay( + current: ElementDisplay | undefined, + values: ElementDisplay | undefined + ): ElementDisplay | undefined { + if (!values) { + return current; + } + const merged: ElementDisplay = current ?? {}; + merged.isVisible = values.isVisible ?? merged.isVisible; + merged.staffPlacement = values.staffPlacement ?? merged.staffPlacement; + merged.systemDisplay = values.systemDisplay ?? merged.systemDisplay; + return merged; + } + + /** + * Writes the values supported by the staff type into the carrier of the given level. + * Carriers on staff and bar level are only created if there are values for them. + */ + private static _applyStaffDisplayValues( + values: AlphaTexStaffDisplayValues, + staffType: AlphaTexStaffType, + stylesheet: RenderStylesheet | undefined, + staff: Staff | undefined, + bar: Bar | undefined + ) { + const hasAllTypeValues = values.timeSignature !== undefined || values.barNumber !== undefined; + switch (staffType) { + case AlphaTexStaffType.Score: + if (!hasAllTypeValues && values.clef === undefined && values.keySignature === undefined) { + return; + } + if (bar) { + bar.scoreDisplay ??= {}; + const c = bar.scoreDisplay!; + c.clef = AlphaTex1LanguageHandler._mergeElementDisplay(c.clef, values.clef); + c.keySignature = AlphaTex1LanguageHandler._mergeElementDisplay(c.keySignature, values.keySignature); + c.timeSignature = AlphaTex1LanguageHandler._mergeElementDisplay( + c.timeSignature, + values.timeSignature + ); + c.barNumber = values.barNumber ?? c.barNumber; + } else { + let c: ScoreStaffConfig; + if (staff) { + staff.scoreConfig ??= {}; + c = staff.scoreConfig!; + } else { + c = stylesheet!.scoreConfig; + } + c.clef = AlphaTex1LanguageHandler._mergeElementDisplay(c.clef, values.clef); + c.keySignature = AlphaTex1LanguageHandler._mergeElementDisplay(c.keySignature, values.keySignature); + c.timeSignature = AlphaTex1LanguageHandler._mergeElementDisplay( + c.timeSignature, + values.timeSignature + ); + c.barNumber = values.barNumber ?? c.barNumber; + } + break; + case AlphaTexStaffType.Tabs: + if (bar) { + if (!hasAllTypeValues && values.clef === undefined) { + return; + } + bar.tabDisplay ??= {}; + const c = bar.tabDisplay!; + c.clef = AlphaTex1LanguageHandler._mergeElementDisplay(c.clef, values.clef); + c.timeSignature = AlphaTex1LanguageHandler._mergeElementDisplay( + c.timeSignature, + values.timeSignature + ); + c.barNumber = values.barNumber ?? c.barNumber; + } else { + if ( + !hasAllTypeValues && + values.clef === undefined && + values.rests === undefined && + values.rhythm === undefined + ) { + return; + } + let c: TabStaffConfig; + if (staff) { + staff.tabConfig ??= {}; + c = staff.tabConfig!; + } else { + c = stylesheet!.tabConfig; + } + c.clef = AlphaTex1LanguageHandler._mergeElementDisplay(c.clef, values.clef); + c.timeSignature = AlphaTex1LanguageHandler._mergeElementDisplay( + c.timeSignature, + values.timeSignature + ); + c.rests = AlphaTex1LanguageHandler._mergeElementDisplay(c.rests, values.rests); + c.barNumber = values.barNumber ?? c.barNumber; + c.rhythm = values.rhythm ?? c.rhythm; + } + break; + case AlphaTexStaffType.Slash: + if (!hasAllTypeValues && values.keySignature === undefined) { + return; + } + if (bar) { + bar.slashDisplay ??= {}; + const c = bar.slashDisplay!; + c.keySignature = AlphaTex1LanguageHandler._mergeElementDisplay(c.keySignature, values.keySignature); + c.timeSignature = AlphaTex1LanguageHandler._mergeElementDisplay( + c.timeSignature, + values.timeSignature + ); + c.barNumber = values.barNumber ?? c.barNumber; + } else { + let c: SlashStaffConfig; + if (staff) { + staff.slashConfig ??= {}; + c = staff.slashConfig!; + } else { + c = stylesheet!.slashConfig; + } + c.keySignature = AlphaTex1LanguageHandler._mergeElementDisplay(c.keySignature, values.keySignature); + c.timeSignature = AlphaTex1LanguageHandler._mergeElementDisplay( + c.timeSignature, + values.timeSignature + ); + c.barNumber = values.barNumber ?? c.barNumber; + } + break; + case AlphaTexStaffType.Numbered: + if (!hasAllTypeValues) { + return; + } + if (bar) { + bar.numberedDisplay ??= {}; + const c = bar.numberedDisplay!; + c.timeSignature = AlphaTex1LanguageHandler._mergeElementDisplay( + c.timeSignature, + values.timeSignature + ); + c.barNumber = values.barNumber ?? c.barNumber; + } else { + let c: NumberedStaffConfig; + if (staff) { + staff.numberedConfig ??= {}; + c = staff.numberedConfig!; + } else { + c = stylesheet!.numberedConfig; + } + c.timeSignature = AlphaTex1LanguageHandler._mergeElementDisplay( + c.timeSignature, + values.timeSignature + ); + c.barNumber = values.barNumber ?? c.barNumber; + } + break; + } + } + public applyStructuralMetaData( importer: IAlphaTexImporter, metaData: AlphaTexMetaDataNode @@ -2738,9 +3125,19 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler nodes.push(Atnf.meta('showSingleStaffBrackets')); } - if (stylesheet.barNumberDisplay !== BarNumberDisplay.FirstOfSystem) { - nodes.push(Atnf.identMeta('defaultBarNumberDisplay', BarNumberDisplay[stylesheet.barNumberDisplay])); - } + const defaultStylesheet = AlphaTex1LanguageHandler._defaultScore.stylesheet; + AlphaTex1LanguageHandler._buildStaffDisplayNodes( + nodes, + 'defaultStaffDisplay', + 'defaultBarNumberDisplay', + AlphaTex1LanguageHandler._staffDisplayTypes.map(t => + AlphaTex1LanguageHandler._diffStaffDisplayValues( + AlphaTex1LanguageHandler._staffDisplayValues(t, stylesheet, undefined, undefined), + AlphaTex1LanguageHandler._staffDisplayValues(t, defaultStylesheet, undefined, undefined) + ) + ), + false + ); // Unsupported: // 'globaldisplaychorddiagramsontop', @@ -2921,12 +3318,15 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler ]; } - if ( - bar.barNumberDisplay !== undefined && - !AlphaTex1LanguageHandler._isImpliedBarNumberDisplay(staff, bar, voice) - ) { - nodes.push(Atnf.identMeta('barNumberDisplay', BarNumberDisplay[bar.barNumberDisplay])); - } + AlphaTex1LanguageHandler._buildStaffDisplayNodes( + nodes, + 'barDisplay', + 'barNumberDisplay', + AlphaTex1LanguageHandler._staffDisplayTypes.map(t => + AlphaTex1LanguageHandler._staffDisplayValues(t, undefined, undefined, bar) + ), + AlphaTex1LanguageHandler._isImpliedBarNumberDisplay(staff, bar, voice) + ); return nodes; } @@ -2979,6 +3379,16 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler } } + AlphaTex1LanguageHandler._buildStaffDisplayNodes( + nodes, + 'staffDisplay', + '', + AlphaTex1LanguageHandler._staffDisplayTypes.map(t => + AlphaTex1LanguageHandler._staffDisplayValues(t, undefined, staff, undefined) + ), + false + ); + if (firstStaffMetaIndex < nodes.length) { nodes[firstStaffMetaIndex].leadingComments = [ { @@ -3016,8 +3426,8 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler } /** - * A custom bar number (`\barNumber`) implies a forced bar number display on the bar it is written on - * (the first staff of the first track). In this case the display does not need to be exported. + * A custom bar number (`\barNumber`) implies a forced bar number display (`allBars` on all staff types) + * on the bar it is written on (the first staff of the first track). In this case these values do not need to be exported. */ private static _isImpliedBarNumberDisplay(staff: Staff, bar: Bar, voice: number): boolean { const masterBar = bar.masterBar; @@ -3025,11 +3435,260 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler voice === 0 && staff.index === 0 && staff.track.index === 0 && - (masterBar.customBarNumber !== undefined || masterBar.customBarNumberText !== undefined) && - bar.barNumberDisplay === BarNumberDisplay.AllBars + (masterBar.customBarNumber !== undefined || masterBar.customBarNumberText !== undefined) ); } + /** + * Collects the display values of a staff type from the carrier of the given level. + */ + private static _staffDisplayValues( + staffType: AlphaTexStaffType, + stylesheet: RenderStylesheet | undefined, + staff: Staff | undefined, + bar: Bar | undefined + ): AlphaTexStaffDisplayValues { + const v: AlphaTexStaffDisplayValues = {}; + switch (staffType) { + case AlphaTexStaffType.Score: + if (bar) { + const c = bar.scoreDisplay; + if (c) { + v.clef = c.clef; + v.keySignature = c.keySignature; + v.timeSignature = c.timeSignature; + v.barNumber = c.barNumber; + } + } else { + const c = staff ? staff.scoreConfig : stylesheet!.scoreConfig; + if (c) { + v.clef = c.clef; + v.keySignature = c.keySignature; + v.timeSignature = c.timeSignature; + v.barNumber = c.barNumber; + } + } + break; + case AlphaTexStaffType.Tabs: + if (bar) { + const c = bar.tabDisplay; + if (c) { + v.clef = c.clef; + v.timeSignature = c.timeSignature; + v.barNumber = c.barNumber; + } + } else { + const c = staff ? staff.tabConfig : stylesheet!.tabConfig; + if (c) { + v.clef = c.clef; + v.timeSignature = c.timeSignature; + v.rests = c.rests; + v.barNumber = c.barNumber; + v.rhythm = c.rhythm; + } + } + break; + case AlphaTexStaffType.Slash: + if (bar) { + const c = bar.slashDisplay; + if (c) { + v.keySignature = c.keySignature; + v.timeSignature = c.timeSignature; + v.barNumber = c.barNumber; + } + } else { + const c = staff ? staff.slashConfig : stylesheet!.slashConfig; + if (c) { + v.keySignature = c.keySignature; + v.timeSignature = c.timeSignature; + v.barNumber = c.barNumber; + } + } + break; + case AlphaTexStaffType.Numbered: + if (bar) { + const c = bar.numberedDisplay; + if (c) { + v.timeSignature = c.timeSignature; + v.barNumber = c.barNumber; + } + } else { + const c = staff ? staff.numberedConfig : stylesheet!.numberedConfig; + if (c) { + v.timeSignature = c.timeSignature; + v.barNumber = c.barNumber; + } + } + break; + } + return v; + } + + /** + * Keeps only the axes of the values which differ from the defaults. + */ + private static _diffStaffDisplayValues( + values: AlphaTexStaffDisplayValues, + defaults: AlphaTexStaffDisplayValues + ): AlphaTexStaffDisplayValues { + const diff: AlphaTexStaffDisplayValues = {}; + diff.clef = AlphaTex1LanguageHandler._diffElementDisplay(values.clef, defaults.clef); + diff.keySignature = AlphaTex1LanguageHandler._diffElementDisplay(values.keySignature, defaults.keySignature); + diff.timeSignature = AlphaTex1LanguageHandler._diffElementDisplay(values.timeSignature, defaults.timeSignature); + diff.rests = AlphaTex1LanguageHandler._diffElementDisplay(values.rests, defaults.rests); + if (values.barNumber !== defaults.barNumber) { + diff.barNumber = values.barNumber; + } + if (values.rhythm !== defaults.rhythm) { + diff.rhythm = values.rhythm; + } + return diff; + } + + private static _diffElementDisplay( + value: ElementDisplay | undefined, + defaults: ElementDisplay | undefined + ): ElementDisplay | undefined { + if (!value) { + return undefined; + } + const diff: ElementDisplay = {}; + let hasDiff = false; + if (value.isVisible !== defaults?.isVisible) { + diff.isVisible = value.isVisible; + hasDiff = true; + } + if (value.staffPlacement !== defaults?.staffPlacement) { + diff.staffPlacement = value.staffPlacement; + hasDiff = true; + } + if (value.systemDisplay !== defaults?.systemDisplay) { + diff.systemDisplay = value.systemDisplay; + hasDiff = true; + } + return hasDiff ? diff : undefined; + } + + /** + * Lists the staff display properties (name and value) for the defined values. + */ + private static _staffDisplayPropertyValues(values: AlphaTexStaffDisplayValues): [string, string][] { + const props: [string, string][] = []; + AlphaTex1LanguageHandler._elementDisplayPropertyValues(props, 'clef', values.clef); + AlphaTex1LanguageHandler._elementDisplayPropertyValues(props, 'ks', values.keySignature); + AlphaTex1LanguageHandler._elementDisplayPropertyValues(props, 'ts', values.timeSignature); + AlphaTex1LanguageHandler._elementDisplayPropertyValues(props, 'rests', values.rests); + if (values.barNumber !== undefined) { + props.push(['barNumber', AlphaTex1EnumMappings.barNumberDisplayReversed.get(values.barNumber!)!]); + } + if (values.rhythm !== undefined) { + props.push(['rhythm', AlphaTex1EnumMappings.tabRhythmModeReversed.get(values.rhythm!)!]); + } + return props; + } + + private static _elementDisplayPropertyValues( + props: [string, string][], + prefix: string, + display: ElementDisplay | undefined + ) { + if (!display) { + return; + } + if (display.isVisible !== undefined) { + props.push([`${prefix}Visibility`, display.isVisible! ? 'true' : 'false']); + } + if (display.staffPlacement !== undefined) { + props.push([ + `${prefix}Placement`, + AlphaTex1EnumMappings.staffPlacementReversed.get(display.staffPlacement!)! + ]); + } + if (display.systemDisplay !== undefined) { + props.push([`${prefix}Systems`, AlphaTex1EnumMappings.systemDisplayReversed.get(display.systemDisplay!)!]); + } + } + + /** + * Builds the staff display tags for one level. Equal property values across staff types are grouped + * into one tag, the staff types are omitted if the value applies to all staff types supporting the property. + * @param valuesPerType the values for each of {@link _staffDisplayTypes} + * @param barNumberShorthandTag the tag to use if all staff types have the same bar number display (empty if none) + * @param impliedAllBars whether bar number display `allBars` is implied (custom bar numbers) and must not be exported + */ + private static _buildStaffDisplayNodes( + nodes: AlphaTexMetaDataNode[], + tag: string, + barNumberShorthandTag: string, + valuesPerType: AlphaTexStaffDisplayValues[], + impliedAllBars: boolean + ) { + const staffTypes = AlphaTex1LanguageHandler._staffDisplayTypes; + + // property value -> staff types using it + const propertyTypes = new Map(); + const propertyValues = new Map(); + for (let i = 0; i < staffTypes.length; i++) { + const values = valuesPerType[i]; + if (impliedAllBars && values.barNumber === BarNumberDisplay.AllBars) { + values.barNumber = undefined; + } + for (const p of AlphaTex1LanguageHandler._staffDisplayPropertyValues(values)) { + const key = `${p[0]} ${p[1]}`; + if (!propertyTypes.has(key)) { + propertyTypes.set(key, []); + propertyValues.set(key, p); + } + propertyTypes.get(key)!.push(staffTypes[i]); + } + } + + // staff types -> properties + const groupTypes = new Map(); + const groupProperties = new Map(); + groupTypes.set('', []); + groupProperties.set('', []); + for (const [key, types] of propertyTypes) { + const p = propertyValues.get(key)!; + const element = AlphaTex1LanguageHandler._staffDisplayPropertyElements.get(p[0].toLowerCase())!; + const supported = AlphaTex1LanguageHandler._staffDisplayElementTypes.get(element)!; + const group = types.length === supported.length ? '' : AlphaTex1LanguageHandler._staffTypeNames(types); + + if (group === '' && element === 'barnumber' && barNumberShorthandTag.length > 0) { + const barNumber = AlphaTex1EnumMappings.barNumberDisplay.get(p[1])!; + nodes.push(Atnf.identMeta(barNumberShorthandTag, BarNumberDisplay[barNumber])); + continue; + } + + if (!groupTypes.has(group)) { + groupTypes.set(group, types); + groupProperties.set(group, []); + } + groupProperties.get(group)!.push(p); + } + + for (const [group, props] of groupProperties) { + if (props.length === 0) { + continue; + } + const types = groupTypes.get(group)!; + nodes.push( + Atnf.meta( + tag, + Atnf.args( + types.map( + t => + Atnf.ident( + AlphaTex1EnumMappings.alphaTexStaffTypeReversed.get(t)! + ) as IAlphaTexArgumentValue + ) + ), + Atnf.props(props.map(p => [p[0], Atnf.identValue(p[1])] as [string, AlphaTexArgumentList])) + ) + ); + } + } + private static _buildMasterBarMetaDataNodes(nodes: AlphaTexMetaDataNode[], masterBar: MasterBar) { const firstMetaIndex = nodes.length; From ff17bff3b32dbdc78477f0e4ca76555c882d343c Mon Sep 17 00:00:00 2001 From: danielku15 Date: Fri, 9 Oct 2026 17:59:27 +0200 Subject: [PATCH 3/8] test(alphatex): cover staff display tags Importer and exporter tests for all levels, and the staff display visual tests now use the alphaTex syntax instead of programmatic model setup (unchanged references). Refs #2912 --- .../test/exporter/AlphaTexExporter.test.ts | 40 +++++ .../test/importer/AlphaTexImporter.test.ts | 139 ++++++++++++++++++ .../features/StaffDisplayConfig.test.ts | 32 +--- 3 files changed, 187 insertions(+), 24 deletions(-) diff --git a/packages/alphatab/test/exporter/AlphaTexExporter.test.ts b/packages/alphatab/test/exporter/AlphaTexExporter.test.ts index f262ba4da9..54528bf2f3 100644 --- a/packages/alphatab/test/exporter/AlphaTexExporter.test.ts +++ b/packages/alphatab/test/exporter/AlphaTexExporter.test.ts @@ -2,7 +2,10 @@ import { describe, expect, it } from 'vitest'; import { AlphaTexExporter } from '@coderline/alphatab/exporter/AlphaTexExporter'; import { AlphaTexErrorWithDiagnostics } from '@coderline/alphatab/importer/AlphaTexImporter'; import { ScoreLoader } from '@coderline/alphatab/importer/ScoreLoader'; +import { StaffPlacement, SystemDisplay } from '@coderline/alphatab/model/ElementDisplay'; +import { BarNumberDisplay } from '@coderline/alphatab/model/RenderStylesheet'; import type { Score } from '@coderline/alphatab/model/Score'; +import { TabRhythmMode } from '@coderline/alphatab/NotationSettings'; import { Settings } from '@coderline/alphatab/Settings'; import { ComparisonHelpers } from 'test/model/ComparisonHelpers'; import { TestPlatform } from 'test/TestPlatform'; @@ -206,4 +209,41 @@ describe('AlphaTexExporterTest', () => { const reimported = ScoreLoader.loadAlphaTex(exported); ComparisonHelpers.alphaTexExportRoundtripEqual('accidental-mode-only-when-needed', reimported, score); }); + + it('staff-display', () => { + const score = ScoreLoader.loadAlphaTex( + '\\track \\staff {score tabs} \\tuning (E4 B3 G3 D3 A2 E2) 3.3.4 3.3 3.3 3.3 | 3.3 3.3 3.3 3.3 | 3.3 3.3 3.3 3.3' + ); + const stylesheet = score.stylesheet; + stylesheet.scoreConfig.clef!.systemDisplay = SystemDisplay.FirstSystemOnly; + stylesheet.tabConfig.clef!.systemDisplay = SystemDisplay.FirstSystemOnly; + stylesheet.tabConfig.timeSignature!.staffPlacement = StaffPlacement.AllStaves; + stylesheet.tabConfig.rhythm = TabRhythmMode.ShowWithBars; + + const staff = score.tracks[0].staves[0]; + staff.scoreConfig = { barNumber: BarNumberDisplay.AllBars }; + staff.slashConfig = { keySignature: { isVisible: true } }; + + staff.bars[1].scoreDisplay = { timeSignature: { isVisible: false } }; + staff.bars[1].tabDisplay = { timeSignature: { isVisible: false }, barNumber: BarNumberDisplay.Hide }; + staff.bars[2].barNumberDisplay = BarNumberDisplay.Hide; + + const exported = exportAlphaTex(score) + .split('\n') + .map(l => l.trim()) + .join(' '); + + // equal values are grouped, staff types are omitted if all supported types are affected + expect(exported).toContain('\\defaultStaffDisplay { clefSystems firstsystemonly rhythm showwithbars }'); + expect(exported).toContain('\\defaultStaffDisplay tabs { tsPlacement allstaves }'); + expect(exported).toContain('\\staffDisplay score { barNumber allbars }'); + expect(exported).toContain('\\staffDisplay slash { ksVisibility true }'); + expect(exported).toContain('\\barDisplay (score tabs) { tsVisibility false }'); + expect(exported).toContain('\\barDisplay tabs { barNumber hide }'); + // equal bar number display on all staff types uses the shorthand + expect(exported).toContain('\\barNumberDisplay Hide'); + + const reimported = ScoreLoader.loadAlphaTex(exportAlphaTex(score)); + ComparisonHelpers.alphaTexExportRoundtripEqual('staff-display', reimported, score); + }); }); diff --git a/packages/alphatab/test/importer/AlphaTexImporter.test.ts b/packages/alphatab/test/importer/AlphaTexImporter.test.ts index cd240cdd66..29ff20dabc 100644 --- a/packages/alphatab/test/importer/AlphaTexImporter.test.ts +++ b/packages/alphatab/test/importer/AlphaTexImporter.test.ts @@ -15,6 +15,7 @@ import { CrescendoType } from '@coderline/alphatab/model/CrescendoType'; import { Direction } from '@coderline/alphatab/model/Direction'; import { Duration } from '@coderline/alphatab/model/Duration'; import { DynamicValue } from '@coderline/alphatab/model/DynamicValue'; +import { StaffPlacement, SystemDisplay } from '@coderline/alphatab/model/ElementDisplay'; import { FadeType } from '@coderline/alphatab/model/FadeType'; import { FermataType } from '@coderline/alphatab/model/Fermata'; import { Fingers } from '@coderline/alphatab/model/Fingers'; @@ -50,6 +51,7 @@ import { TextAlign } from '@coderline/alphatab/platform/ICanvas'; import { harmonicToString } from '@coderline/alphatab/rendering/effects/HarmonicsEffectInfo'; import { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; +import { TabRhythmMode } from '@coderline/alphatab/NotationSettings'; import { Settings } from '@coderline/alphatab/Settings'; import { StaveProfile } from '@coderline/alphatab/StaveProfile'; import { ComparisonHelpers } from 'test/model/ComparisonHelpers'; @@ -2690,6 +2692,143 @@ describe('AlphaTexImporterTest', () => { test('\\defaultBarNumberDisplay allBars C4 | \\barNumberDisplay hide C4 ', BarNumberDisplay.Hide)); }); + describe('staffDisplay', () => { + function semanticDiagnosticCodes(tex: string): AlphaTexDiagnosticCode[] { + const importer: AlphaTexImporter = new AlphaTexImporter(); + importer.initFromString(tex, new Settings()); + try { + importer.readScore(); + } catch { + // checked by caller + } + return importer.semanticDiagnostics.items.map(d => d.code!); + } + + it('score-level', () => { + const score = parseTex( + '\\defaultStaffDisplay tabs { tsVisibility false tsPlacement allStaves restsSystems firstSystemOnly rhythm showWithBeams } C4' + ); + const tabs = score.stylesheet.tabConfig; + expect(tabs.timeSignature!.isVisible).toBe(false); + expect(tabs.timeSignature!.staffPlacement).toBe(StaffPlacement.AllStaves); + expect(tabs.timeSignature!.systemDisplay).toBe(SystemDisplay.AllSystems); + expect(tabs.rests!.isVisible).toBe(true); + expect(tabs.rests!.systemDisplay).toBe(SystemDisplay.FirstSystemOnly); + expect(tabs.rhythm).toBe(TabRhythmMode.ShowWithBeams); + expect(score.stylesheet.scoreConfig.timeSignature!.isVisible).toBe(true); + + testExportRoundtrip(score); + }); + + it('staff-level-all-types', () => { + const score = parseTex('\\staff {score tabs} \\staffDisplay { tsPlacement primary barNumber hide } C4'); + const staff = score.tracks[0].staves[0]; + for (const config of [staff.scoreConfig, staff.tabConfig, staff.slashConfig, staff.numberedConfig]) { + expect(config!.timeSignature!.staffPlacement).toBe(StaffPlacement.Primary); + expect(config!.timeSignature!.isVisible).toBeUndefined(); + expect(config!.timeSignature!.systemDisplay).toBeUndefined(); + expect(config!.barNumber).toBe(BarNumberDisplay.Hide); + } + expect(staff.scoreConfig!.clef).toBeUndefined(); + + testExportRoundtrip(score); + }); + + it('staff-level-supported-types-only', () => { + const score = parseTex('\\staffDisplay { ksVisibility false } C4'); + const staff = score.tracks[0].staves[0]; + expect(staff.scoreConfig!.keySignature!.isVisible).toBe(false); + expect(staff.slashConfig!.keySignature!.isVisible).toBe(false); + expect(staff.tabConfig).toBeUndefined(); + expect(staff.numberedConfig).toBeUndefined(); + + testExportRoundtrip(score); + }); + + it('bar-level', () => { + const score = parseTex( + 'C4 | \\barDisplay (score tabs) { clefVisibility false tsSystems firstSystemOnly } C4 | C4' + ); + const bars = score.tracks[0].staves[0].bars; + expect(bars[0].scoreDisplay).toBeUndefined(); + expect(bars[1].scoreDisplay!.clef!.isVisible).toBe(false); + expect(bars[1].scoreDisplay!.timeSignature!.systemDisplay).toBe(SystemDisplay.FirstSystemOnly); + expect(bars[1].tabDisplay!.clef!.isVisible).toBe(false); + expect(bars[1].slashDisplay).toBeUndefined(); + expect(bars[1].numberedDisplay).toBeUndefined(); + expect(bars[2].scoreDisplay).toBeUndefined(); + + testExportRoundtrip(score); + }); + + it('later-tags-win', () => { + const score = parseTex( + '\\defaultStaffDisplay { tsVisibility false } \\defaultStaffDisplay tabs { tsVisibility true tsPlacement allStaves } C4' + ); + expect(score.stylesheet.scoreConfig.timeSignature!.isVisible).toBe(false); + expect(score.stylesheet.slashConfig.timeSignature!.isVisible).toBe(false); + expect(score.stylesheet.tabConfig.timeSignature!.isVisible).toBe(true); + expect(score.stylesheet.tabConfig.timeSignature!.staffPlacement).toBe(StaffPlacement.AllStaves); + + testExportRoundtrip(score); + }); + + it('visibility-values', () => { + const visibility = (value: string) => + parseTex(`\\staffDisplay score { tsVisibility ${value} } C4`).tracks[0].staves[0].scoreConfig! + .timeSignature!.isVisible; + expect(visibility('')).toBe(true); + expect(visibility('true')).toBe(true); + expect(visibility('false')).toBe(false); + expect(visibility('"false"')).toBe(false); + expect(visibility('1')).toBe(true); + expect(visibility('0')).toBe(false); + }); + + it('unsupported-staff-type', () => { + expect(semanticDiagnosticCodes('\\staffDisplay tabs { ksVisibility false } C4')).toContain( + AlphaTexDiagnosticCode.AT307 + ); + const score = parseTex('\\staffDisplay tabs { ksVisibility false } C4'); + expect(score.tracks[0].staves[0].tabConfig).toBeUndefined(); + }); + + it('bar-level-unsupported-properties', () => { + for (const tex of [ + 'C4 | \\barDisplay { restsVisibility false } C4', + 'C4 | \\barDisplay { rhythm hidden } C4' + ]) { + const importer: AlphaTexImporter = new AlphaTexImporter(); + importer.initFromString(tex, new Settings()); + try { + importer.readScore(); + } catch { + // checked below + } + expect(importer.parserDiagnostics.items.map(d => d.code)).toContain(AlphaTexDiagnosticCode.AT205); + } + }); + + it('legacy-shorthands', () => { + const score = parseTex('\\defaultBarNumberDisplay hide C4 | \\barNumberDisplay allBars C4'); + const stylesheet = score.stylesheet; + for (const config of [ + stylesheet.scoreConfig, + stylesheet.tabConfig, + stylesheet.slashConfig, + stylesheet.numberedConfig + ]) { + expect(config.barNumber).toBe(BarNumberDisplay.Hide); + } + const bar = score.tracks[0].staves[0].bars[1]; + for (const display of [bar.scoreDisplay, bar.tabDisplay, bar.slashDisplay, bar.numberedDisplay]) { + expect(display!.barNumber).toBe(BarNumberDisplay.AllBars); + } + + testExportRoundtrip(score); + }); + }); + it('custom-beaming', () => { const score = parseTex(` \\ts (4 4) diff --git a/packages/alphatab/test/visualTests/features/StaffDisplayConfig.test.ts b/packages/alphatab/test/visualTests/features/StaffDisplayConfig.test.ts index 1912618505..1fefcac655 100644 --- a/packages/alphatab/test/visualTests/features/StaffDisplayConfig.test.ts +++ b/packages/alphatab/test/visualTests/features/StaffDisplayConfig.test.ts @@ -87,15 +87,11 @@ describe('StaffDisplayConfig', () => { ` \\track \\staff {tabs} + \\staffDisplay tabs { tsVisibility false } 3.3.4 3.3 3.3 3.3 | 3.3 3.3 3.3 3.3 | `, - 'test-data/visual-tests/staff-display-config/override-l2-tab-ts-hide.png', - undefined, - o => { - const staff = o.score.tracks[0].staves[0]; - staff.tabConfig = { timeSignature: { isVisible: false } }; - } + 'test-data/visual-tests/staff-display-config/override-l2-tab-ts-hide.png' ); }); @@ -104,16 +100,12 @@ describe('StaffDisplayConfig', () => { ` \\track \\staff {score} + \\staffDisplay score { clefVisibility false } \\ks D 3.3.4 3.3 3.3 3.3 | 3.3 3.3 3.3 3.3 | `, - 'test-data/visual-tests/staff-display-config/override-l2-score-clef-hide.png', - undefined, - o => { - const staff = o.score.tracks[0].staves[0]; - staff.scoreConfig = { clef: { isVisible: false } }; - } + 'test-data/visual-tests/staff-display-config/override-l2-score-clef-hide.png' ); }); }); @@ -127,15 +119,11 @@ describe('StaffDisplayConfig', () => { \\ks D 3.3.4 3.3 3.3 3.3 | \\ks A + \\barDisplay score { ksVisibility false } 3.3.4 3.3 3.3 3.3 | 3.3.4 3.3 3.3 3.3 | `, - 'test-data/visual-tests/staff-display-config/override-l1-ks-change-hide.png', - undefined, - o => { - const bars = o.score.tracks[0].staves[0].bars; - bars[1].scoreDisplay = { keySignature: { isVisible: false } }; - } + 'test-data/visual-tests/staff-display-config/override-l1-ks-change-hide.png' ); }); @@ -144,15 +132,11 @@ describe('StaffDisplayConfig', () => { ` \\track \\staff {score tabs} + \\barDisplay score { tsVisibility false } 3.3.4 3.3 3.3 3.3 | 3.3 3.3 3.3 3.3 | `, - 'test-data/visual-tests/staff-display-config/override-l1-ts-hide-firstbar.png', - undefined, - o => { - const bars = o.score.tracks[0].staves[0].bars; - bars[0].scoreDisplay = { timeSignature: { isVisible: false } }; - } + 'test-data/visual-tests/staff-display-config/override-l1-ts-hide-firstbar.png' ); }); }); From 477ffad9837d9c3144f22d58921847536712a4c5 Mon Sep 17 00:00:00 2001 From: danielku15 Date: Fri, 9 Oct 2026 18:07:58 +0200 Subject: [PATCH 4/8] fix(alphatex): keep staff display export and tests compatible with C# and Kotlin Build the exported property nodes via Atnf.prop (Kotlin list invariance) and avoid arrays mixing the per-staff-type config types in the tests. Refs #2912 --- .../alphaTex/AlphaTex1LanguageHandler.ts | 24 ++++----- .../test/importer/AlphaTexImporter.test.ts | 51 ++++++++++++------- 2 files changed, 43 insertions(+), 32 deletions(-) diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts index 4947933953..77c759dd64 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts @@ -3671,21 +3671,15 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler if (props.length === 0) { continue; } - const types = groupTypes.get(group)!; - nodes.push( - Atnf.meta( - tag, - Atnf.args( - types.map( - t => - Atnf.ident( - AlphaTex1EnumMappings.alphaTexStaffTypeReversed.get(t)! - ) as IAlphaTexArgumentValue - ) - ), - Atnf.props(props.map(p => [p[0], Atnf.identValue(p[1])] as [string, AlphaTexArgumentList])) - ) - ); + const args: IAlphaTexArgumentValue[] = []; + for (const t of groupTypes.get(group)!) { + args.push(Atnf.ident(AlphaTex1EnumMappings.alphaTexStaffTypeReversed.get(t)!)); + } + const propNodes = Atnf.props([]); + for (const p of props) { + Atnf.prop(propNodes.properties, p[0], Atnf.identValue(p[1])); + } + nodes.push(Atnf.meta(tag, Atnf.args(args), propNodes)); } } diff --git a/packages/alphatab/test/importer/AlphaTexImporter.test.ts b/packages/alphatab/test/importer/AlphaTexImporter.test.ts index 29ff20dabc..d971e8489c 100644 --- a/packages/alphatab/test/importer/AlphaTexImporter.test.ts +++ b/packages/alphatab/test/importer/AlphaTexImporter.test.ts @@ -2701,7 +2701,7 @@ describe('AlphaTexImporterTest', () => { } catch { // checked by caller } - return importer.semanticDiagnostics.items.map(d => d.code!); + return importer.semanticDiagnostics.items.map(d => d.code); } it('score-level', () => { @@ -2723,12 +2723,23 @@ describe('AlphaTexImporterTest', () => { it('staff-level-all-types', () => { const score = parseTex('\\staff {score tabs} \\staffDisplay { tsPlacement primary barNumber hide } C4'); const staff = score.tracks[0].staves[0]; - for (const config of [staff.scoreConfig, staff.tabConfig, staff.slashConfig, staff.numberedConfig]) { - expect(config!.timeSignature!.staffPlacement).toBe(StaffPlacement.Primary); - expect(config!.timeSignature!.isVisible).toBeUndefined(); - expect(config!.timeSignature!.systemDisplay).toBeUndefined(); - expect(config!.barNumber).toBe(BarNumberDisplay.Hide); + const timeSignatures = [ + staff.scoreConfig!.timeSignature!, + staff.tabConfig!.timeSignature!, + staff.slashConfig!.timeSignature!, + staff.numberedConfig!.timeSignature! + ]; + for (const ts of timeSignatures) { + expect(ts.staffPlacement).toBe(StaffPlacement.Primary); + expect(ts.isVisible).toBeUndefined(); + expect(ts.systemDisplay).toBeUndefined(); } + expect([ + staff.scoreConfig!.barNumber, + staff.tabConfig!.barNumber, + staff.slashConfig!.barNumber, + staff.numberedConfig!.barNumber + ]).toEqual([BarNumberDisplay.Hide, BarNumberDisplay.Hide, BarNumberDisplay.Hide, BarNumberDisplay.Hide]); expect(staff.scoreConfig!.clef).toBeUndefined(); testExportRoundtrip(score); @@ -2812,18 +2823,24 @@ describe('AlphaTexImporterTest', () => { it('legacy-shorthands', () => { const score = parseTex('\\defaultBarNumberDisplay hide C4 | \\barNumberDisplay allBars C4'); const stylesheet = score.stylesheet; - for (const config of [ - stylesheet.scoreConfig, - stylesheet.tabConfig, - stylesheet.slashConfig, - stylesheet.numberedConfig - ]) { - expect(config.barNumber).toBe(BarNumberDisplay.Hide); - } + expect([ + stylesheet.scoreConfig.barNumber, + stylesheet.tabConfig.barNumber, + stylesheet.slashConfig.barNumber, + stylesheet.numberedConfig.barNumber + ]).toEqual([BarNumberDisplay.Hide, BarNumberDisplay.Hide, BarNumberDisplay.Hide, BarNumberDisplay.Hide]); const bar = score.tracks[0].staves[0].bars[1]; - for (const display of [bar.scoreDisplay, bar.tabDisplay, bar.slashDisplay, bar.numberedDisplay]) { - expect(display!.barNumber).toBe(BarNumberDisplay.AllBars); - } + expect([ + bar.scoreDisplay!.barNumber, + bar.tabDisplay!.barNumber, + bar.slashDisplay!.barNumber, + bar.numberedDisplay!.barNumber + ]).toEqual([ + BarNumberDisplay.AllBars, + BarNumberDisplay.AllBars, + BarNumberDisplay.AllBars, + BarNumberDisplay.AllBars + ]); testExportRoundtrip(score); }); From f3571c215bea2b2ad898d68aa106d3a07f6c9151 Mon Sep 17 00:00:00 2001 From: danielku15 Date: Fri, 9 Oct 2026 18:09:01 +0200 Subject: [PATCH 5/8] docs(alphatex): wrap time signature arguments in staff display example Refs #2912 --- packages/alphatex/src/staffDisplay.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/alphatex/src/staffDisplay.ts b/packages/alphatex/src/staffDisplay.ts index 9510d09f77..160a82a27c 100644 --- a/packages/alphatex/src/staffDisplay.ts +++ b/packages/alphatex/src/staffDisplay.ts @@ -145,7 +145,7 @@ const staffDisplayElements: StaffDisplayElement[] = [ target: '', props: 'tsSystems firstSystemOnly', staff: 'score tabs', - bars: ['3.3.4 3.3 3.3 3.3', '3.3 3.3 3.3 3.3', '\\ts 3 4 3.3.4 3.3 3.3', '3.3 3.3 3.3'], + bars: ['3.3.4 3.3 3.3 3.3', '3.3 3.3 3.3 3.3', '\\ts (3 4) 3.3.4 3.3 3.3', '3.3 3.3 3.3'], systems: true } }, From 991811815fc222f3687f6d65900fabb5c31eac7d Mon Sep 17 00:00:00 2001 From: danielku15 Date: Fri, 9 Oct 2026 18:29:11 +0200 Subject: [PATCH 6/8] refactor(alphatex): condense staff display import, export and definitions - import: values are taken from the already validated arguments and properties, carriers are only written for affected staff types - export: one reader per staff type, values compared as property text (also for the score level defaults), tags grouped while building - definitions: one shared example score and a compact property factory Refs #2912 --- .../alphaTex/AlphaTex1LanguageHandler.ts | 694 ++++++------------ .../src/importer/alphaTex/AlphaTexShared.ts | 19 +- .../test/importer/AlphaTexImporter.test.ts | 145 +--- packages/alphatex/src/common.ts | 64 +- packages/alphatex/src/enum.ts | 38 +- packages/alphatex/src/staffDisplay.ts | 341 +++------ 6 files changed, 388 insertions(+), 913 deletions(-) diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts index 77c759dd64..e428367c64 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts @@ -79,12 +79,6 @@ import { SimileMark } from '@coderline/alphatab/model/SimileMark'; import { SlideInType } from '@coderline/alphatab/model/SlideInType'; import { SlideOutType } from '@coderline/alphatab/model/SlideOutType'; import { Staff } from '@coderline/alphatab/model/Staff'; -import type { - NumberedStaffConfig, - ScoreStaffConfig, - SlashStaffConfig, - TabStaffConfig -} from '@coderline/alphatab/model/StaffConfigs'; import { Track } from '@coderline/alphatab/model/Track'; import { TremoloPickingEffect, TremoloPickingStyle } from '@coderline/alphatab/model/TremoloPickingEffect'; import { TripletFeel } from '@coderline/alphatab/model/TripletFeel'; @@ -96,15 +90,14 @@ import type { TabRhythmMode } from '@coderline/alphatab/NotationSettings'; import { SynthConstants } from '@coderline/alphatab/synth/SynthConstants'; /** - * The values read from the properties of a staff display tag - * (`\defaultStaffDisplay`, `\staffDisplay`, `\barDisplay`). + * The staff display values of one staff type, named like the alphaTex properties. * @internal * @record */ interface AlphaTexStaffDisplayValues { clef?: ElementDisplay; - keySignature?: ElementDisplay; - timeSignature?: ElementDisplay; + ks?: ElementDisplay; + ts?: ElementDisplay; rests?: ElementDisplay; barNumber?: BarNumberDisplay; rhythm?: TabRhythmMode; @@ -1326,7 +1319,7 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler // // staff display (\defaultStaffDisplay, \staffDisplay, \barDisplay) - private static readonly _staffDisplayTypes = [ + private static readonly _staffTypes = [ AlphaTexStaffType.Score, AlphaTexStaffType.Tabs, AlphaTexStaffType.Slash, @@ -1334,44 +1327,29 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler ]; /** - * The element configured by each staff display property. + * The elements configured by the staff display properties and the staff types supporting them. */ - private static readonly _staffDisplayPropertyElements = new Map([ - ['clefvisibility', 'clef'], - ['clefplacement', 'clef'], - ['clefsystems', 'clef'], - ['ksvisibility', 'ks'], - ['ksplacement', 'ks'], - ['kssystems', 'ks'], - ['tsvisibility', 'ts'], - ['tsplacement', 'ts'], - ['tssystems', 'ts'], - ['restsvisibility', 'rests'], - ['restsplacement', 'rests'], - ['restssystems', 'rests'], - ['barnumber', 'barnumber'], - ['rhythm', 'rhythm'] - ]); - - /** - * The staff types supporting the elements configured via the staff display properties. - */ - private static readonly _staffDisplayElementTypes = new Map([ + private static readonly _staffDisplayElements = new Map([ ['clef', [AlphaTexStaffType.Score, AlphaTexStaffType.Tabs]], ['ks', [AlphaTexStaffType.Score, AlphaTexStaffType.Slash]], - ['ts', AlphaTex1LanguageHandler._staffDisplayTypes], + ['ts', AlphaTex1LanguageHandler._staffTypes], ['rests', [AlphaTexStaffType.Tabs]], - ['barnumber', AlphaTex1LanguageHandler._staffDisplayTypes], + ['barNumber', AlphaTex1LanguageHandler._staffTypes], ['rhythm', [AlphaTexStaffType.Tabs]] ]); - private static _staffTypeNames(staffTypes: AlphaTexStaffType[]): string { - return staffTypes.map(t => AlphaTex1EnumMappings.alphaTexStaffTypeReversed.get(t)!).join(','); + private static _staffDisplayElement(property: string): string { + for (const e of AlphaTex1LanguageHandler._staffDisplayElements.keys()) { + if (property.toLowerCase().startsWith(e.toLowerCase())) { + return e; + } + } + return ''; } /** - * Applies a staff display tag to the stylesheet (score level), the staff (staff level) or the bar (bar level). - * The arguments select the staff types, without arguments all staff types supporting a property are affected. + * Applies a staff display tag to the stylesheet, staff or bar. The arguments select the staff types, + * without arguments the properties apply to all staff types supporting them. */ private _applyStaffDisplay( importer: IAlphaTexImporter, @@ -1380,148 +1358,93 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler staff: Staff | undefined, bar: Bar | undefined ) { - const explicitTypes = metaData.arguments !== undefined && metaData.arguments!.arguments.length > 0; - let staffTypes = AlphaTex1LanguageHandler._staffDisplayTypes; - if (explicitTypes) { - staffTypes = []; - for (let i = 0; i < metaData.arguments!.arguments.length; i++) { - const staffType = AlphaTex1LanguageHandler._parseEnumValue( - importer, - metaData.arguments!, - 'staff type', - AlphaTex1EnumMappings.alphaTexStaffType, - i - ); - if (staffType !== undefined) { - staffTypes.push(staffType); - } + if (!metaData.properties) { + return; + } + const explicit: AlphaTexStaffType[] = []; + if (metaData.arguments) { + for (const a of metaData.arguments!.arguments) { + explicit.push(AlphaTex1EnumMappings.alphaTexStaffType.get((a as AlphaTexTextNode).text.toLowerCase())!); } } - const values: AlphaTexStaffDisplayValues = {}; - if (metaData.properties) { - const lookup = AlphaTex1LanguageDefinitions.metaDataProperties.get(metaData.tag.tag.text.toLowerCase())!; - for (const p of metaData.properties!.properties) { - if (!this._checkProperty(importer, [lookup], p)) { - continue; - } - - const name = p.property.text.toLowerCase(); - if (!AlphaTex1LanguageHandler._readStaffDisplayProperty(importer, p, name, values)) { - continue; - } - - if (explicitTypes) { - const element = AlphaTex1LanguageHandler._staffDisplayPropertyElements.get(name)!; - const supported = AlphaTex1LanguageHandler._staffDisplayElementTypes.get(element)!; - const unsupported = staffTypes.filter(t => supported.indexOf(t) === -1); - if (unsupported.length > 0) { - importer.addSemanticDiagnostic({ - code: AlphaTexDiagnosticCode.AT307, - message: `The property '${p.property.text}' is not supported by the staff types ${AlphaTex1LanguageHandler._staffTypeNames(unsupported)}, the value has no effect there. Supported staff types: ${AlphaTex1LanguageHandler._staffTypeNames(supported)}`, - severity: AlphaTexDiagnosticsSeverity.Warning, - start: p.start, - end: p.end - }); - } + const affected = new Set(); + const lookup = AlphaTex1LanguageDefinitions.metaDataProperties.get(metaData.tag.tag.text.toLowerCase())!; + for (const p of metaData.properties!.properties) { + if (!this._checkProperty(importer, [lookup], p)) { + continue; + } + const supported = AlphaTex1LanguageHandler._staffDisplayElements.get( + AlphaTex1LanguageHandler._readStaffDisplayProperty(p, values) + )!; + for (const t of explicit.length > 0 ? explicit : supported) { + if (supported.indexOf(t) !== -1) { + affected.add(t); + } else { + importer.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT307, + message: `The property '${p.property.text}' has no effect on the staff type '${AlphaTex1EnumMappings.alphaTexStaffTypeReversed.get(t)}'`, + severity: AlphaTexDiagnosticsSeverity.Warning, + start: p.start, + end: p.end + }); } } } - for (const staffType of staffTypes) { - AlphaTex1LanguageHandler._applyStaffDisplayValues(values, staffType, stylesheet, staff, bar); + for (const t of affected) { + AlphaTex1LanguageHandler._writeStaffDisplay(t, stylesheet, staff, bar, values); } } - private static _readStaffDisplayProperty( - importer: IAlphaTexImporter, - p: AlphaTexPropertyNode, - name: string, - values: AlphaTexStaffDisplayValues - ): boolean { - switch (name) { - case 'barnumber': - const barNumber = AlphaTex1LanguageHandler._parseEnumValue( - importer, - p.arguments!, - 'bar number display', - AlphaTex1EnumMappings.barNumberDisplay - ); - if (barNumber === undefined) { - return false; - } - values.barNumber = barNumber; - return true; - case 'rhythm': - const rhythm = AlphaTex1LanguageHandler._parseEnumValue( - importer, - p.arguments!, - 'rhythm mode', - AlphaTex1EnumMappings.tabRhythmMode - ); - if (rhythm === undefined) { - return false; + /** + * Reads the (already validated) property into the values and returns the configured element. + */ + private static _readStaffDisplayProperty(p: AlphaTexPropertyNode, values: AlphaTexStaffDisplayValues): string { + const element = AlphaTex1LanguageHandler._staffDisplayElement(p.property.text); + const value = () => (p.arguments!.arguments[0] as AlphaTexTextNode).text.toLowerCase(); + const d: ElementDisplay = {}; + switch (p.property.text.toLowerCase().substring(element.length)) { + case '': + if (element === 'barNumber') { + values.barNumber = AlphaTex1EnumMappings.barNumberDisplay.get(value()); + } else { + values.rhythm = AlphaTex1EnumMappings.tabRhythmMode.get(value()); } - values.rhythm = rhythm; - return true; + return element; + case 'visibility': + d.isVisible = p.arguments + ? AlphaTex1LanguageHandler._booleanLikeValue(p.arguments!.arguments, 0) + : true; + break; + case 'placement': + d.staffPlacement = AlphaTex1EnumMappings.staffPlacement.get(value()); + break; + default: + d.systemDisplay = AlphaTex1EnumMappings.systemDisplay.get(value()); + break; } - - const element = AlphaTex1LanguageHandler._staffDisplayPropertyElements.get(name)!; - let display: ElementDisplay; switch (element) { case 'clef': - values.clef ??= {}; - display = values.clef!; + values.clef = AlphaTex1LanguageHandler._mergeDisplay(values.clef, d); break; case 'ks': - values.keySignature ??= {}; - display = values.keySignature!; + values.ks = AlphaTex1LanguageHandler._mergeDisplay(values.ks, d); break; case 'ts': - values.timeSignature ??= {}; - display = values.timeSignature!; + values.ts = AlphaTex1LanguageHandler._mergeDisplay(values.ts, d); break; default: - values.rests ??= {}; - display = values.rests!; + values.rests = AlphaTex1LanguageHandler._mergeDisplay(values.rests, d); break; } - - if (name === `${element}visibility`) { - display.isVisible = AlphaTex1LanguageHandler._booleanLikeValue( - p.arguments ? p.arguments!.arguments : [], - 0 - ); - } else if (name === `${element}placement`) { - const placement = AlphaTex1LanguageHandler._parseEnumValue( - importer, - p.arguments!, - 'staff placement', - AlphaTex1EnumMappings.staffPlacement - ); - if (placement === undefined) { - return false; - } - display.staffPlacement = placement; - } else { - const systems = AlphaTex1LanguageHandler._parseEnumValue( - importer, - p.arguments!, - 'system display', - AlphaTex1EnumMappings.systemDisplay - ); - if (systems === undefined) { - return false; - } - display.systemDisplay = systems; - } - return true; + return element; } /** - * Merges the specified axes of the values into the current display (creating it if needed). + * Merges the defined axes of the values into the current display. */ - private static _mergeElementDisplay( + private static _mergeDisplay( current: ElementDisplay | undefined, values: ElementDisplay | undefined ): ElementDisplay | undefined { @@ -1536,142 +1459,84 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler } /** - * Writes the values supported by the staff type into the carrier of the given level. - * Carriers on staff and bar level are only created if there are values for them. + * Merges the values supported by the staff type into its carrier (created on demand on staff and bar level). */ - private static _applyStaffDisplayValues( - values: AlphaTexStaffDisplayValues, - staffType: AlphaTexStaffType, + private static _writeStaffDisplay( + t: AlphaTexStaffType, stylesheet: RenderStylesheet | undefined, staff: Staff | undefined, - bar: Bar | undefined + bar: Bar | undefined, + v: AlphaTexStaffDisplayValues ) { - const hasAllTypeValues = values.timeSignature !== undefined || values.barNumber !== undefined; - switch (staffType) { + switch (t) { case AlphaTexStaffType.Score: - if (!hasAllTypeValues && values.clef === undefined && values.keySignature === undefined) { - return; - } if (bar) { bar.scoreDisplay ??= {}; const c = bar.scoreDisplay!; - c.clef = AlphaTex1LanguageHandler._mergeElementDisplay(c.clef, values.clef); - c.keySignature = AlphaTex1LanguageHandler._mergeElementDisplay(c.keySignature, values.keySignature); - c.timeSignature = AlphaTex1LanguageHandler._mergeElementDisplay( - c.timeSignature, - values.timeSignature - ); - c.barNumber = values.barNumber ?? c.barNumber; + c.clef = AlphaTex1LanguageHandler._mergeDisplay(c.clef, v.clef); + c.keySignature = AlphaTex1LanguageHandler._mergeDisplay(c.keySignature, v.ks); + c.timeSignature = AlphaTex1LanguageHandler._mergeDisplay(c.timeSignature, v.ts); + c.barNumber = v.barNumber ?? c.barNumber; } else { - let c: ScoreStaffConfig; if (staff) { staff.scoreConfig ??= {}; - c = staff.scoreConfig!; - } else { - c = stylesheet!.scoreConfig; } - c.clef = AlphaTex1LanguageHandler._mergeElementDisplay(c.clef, values.clef); - c.keySignature = AlphaTex1LanguageHandler._mergeElementDisplay(c.keySignature, values.keySignature); - c.timeSignature = AlphaTex1LanguageHandler._mergeElementDisplay( - c.timeSignature, - values.timeSignature - ); - c.barNumber = values.barNumber ?? c.barNumber; + const c = staff ? staff.scoreConfig! : stylesheet!.scoreConfig; + c.clef = AlphaTex1LanguageHandler._mergeDisplay(c.clef, v.clef); + c.keySignature = AlphaTex1LanguageHandler._mergeDisplay(c.keySignature, v.ks); + c.timeSignature = AlphaTex1LanguageHandler._mergeDisplay(c.timeSignature, v.ts); + c.barNumber = v.barNumber ?? c.barNumber; } break; case AlphaTexStaffType.Tabs: if (bar) { - if (!hasAllTypeValues && values.clef === undefined) { - return; - } bar.tabDisplay ??= {}; const c = bar.tabDisplay!; - c.clef = AlphaTex1LanguageHandler._mergeElementDisplay(c.clef, values.clef); - c.timeSignature = AlphaTex1LanguageHandler._mergeElementDisplay( - c.timeSignature, - values.timeSignature - ); - c.barNumber = values.barNumber ?? c.barNumber; + c.clef = AlphaTex1LanguageHandler._mergeDisplay(c.clef, v.clef); + c.timeSignature = AlphaTex1LanguageHandler._mergeDisplay(c.timeSignature, v.ts); + c.barNumber = v.barNumber ?? c.barNumber; } else { - if ( - !hasAllTypeValues && - values.clef === undefined && - values.rests === undefined && - values.rhythm === undefined - ) { - return; - } - let c: TabStaffConfig; if (staff) { staff.tabConfig ??= {}; - c = staff.tabConfig!; - } else { - c = stylesheet!.tabConfig; } - c.clef = AlphaTex1LanguageHandler._mergeElementDisplay(c.clef, values.clef); - c.timeSignature = AlphaTex1LanguageHandler._mergeElementDisplay( - c.timeSignature, - values.timeSignature - ); - c.rests = AlphaTex1LanguageHandler._mergeElementDisplay(c.rests, values.rests); - c.barNumber = values.barNumber ?? c.barNumber; - c.rhythm = values.rhythm ?? c.rhythm; + const c = staff ? staff.tabConfig! : stylesheet!.tabConfig; + c.clef = AlphaTex1LanguageHandler._mergeDisplay(c.clef, v.clef); + c.timeSignature = AlphaTex1LanguageHandler._mergeDisplay(c.timeSignature, v.ts); + c.rests = AlphaTex1LanguageHandler._mergeDisplay(c.rests, v.rests); + c.barNumber = v.barNumber ?? c.barNumber; + c.rhythm = v.rhythm ?? c.rhythm; } break; case AlphaTexStaffType.Slash: - if (!hasAllTypeValues && values.keySignature === undefined) { - return; - } if (bar) { bar.slashDisplay ??= {}; const c = bar.slashDisplay!; - c.keySignature = AlphaTex1LanguageHandler._mergeElementDisplay(c.keySignature, values.keySignature); - c.timeSignature = AlphaTex1LanguageHandler._mergeElementDisplay( - c.timeSignature, - values.timeSignature - ); - c.barNumber = values.barNumber ?? c.barNumber; + c.keySignature = AlphaTex1LanguageHandler._mergeDisplay(c.keySignature, v.ks); + c.timeSignature = AlphaTex1LanguageHandler._mergeDisplay(c.timeSignature, v.ts); + c.barNumber = v.barNumber ?? c.barNumber; } else { - let c: SlashStaffConfig; if (staff) { staff.slashConfig ??= {}; - c = staff.slashConfig!; - } else { - c = stylesheet!.slashConfig; } - c.keySignature = AlphaTex1LanguageHandler._mergeElementDisplay(c.keySignature, values.keySignature); - c.timeSignature = AlphaTex1LanguageHandler._mergeElementDisplay( - c.timeSignature, - values.timeSignature - ); - c.barNumber = values.barNumber ?? c.barNumber; + const c = staff ? staff.slashConfig! : stylesheet!.slashConfig; + c.keySignature = AlphaTex1LanguageHandler._mergeDisplay(c.keySignature, v.ks); + c.timeSignature = AlphaTex1LanguageHandler._mergeDisplay(c.timeSignature, v.ts); + c.barNumber = v.barNumber ?? c.barNumber; } break; case AlphaTexStaffType.Numbered: - if (!hasAllTypeValues) { - return; - } if (bar) { bar.numberedDisplay ??= {}; const c = bar.numberedDisplay!; - c.timeSignature = AlphaTex1LanguageHandler._mergeElementDisplay( - c.timeSignature, - values.timeSignature - ); - c.barNumber = values.barNumber ?? c.barNumber; + c.timeSignature = AlphaTex1LanguageHandler._mergeDisplay(c.timeSignature, v.ts); + c.barNumber = v.barNumber ?? c.barNumber; } else { - let c: NumberedStaffConfig; if (staff) { staff.numberedConfig ??= {}; - c = staff.numberedConfig!; - } else { - c = stylesheet!.numberedConfig; } - c.timeSignature = AlphaTex1LanguageHandler._mergeElementDisplay( - c.timeSignature, - values.timeSignature - ); - c.barNumber = values.barNumber ?? c.barNumber; + const c = staff ? staff.numberedConfig! : stylesheet!.numberedConfig; + c.timeSignature = AlphaTex1LanguageHandler._mergeDisplay(c.timeSignature, v.ts); + c.barNumber = v.barNumber ?? c.barNumber; } break; } @@ -3125,17 +2990,13 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler nodes.push(Atnf.meta('showSingleStaffBrackets')); } - const defaultStylesheet = AlphaTex1LanguageHandler._defaultScore.stylesheet; AlphaTex1LanguageHandler._buildStaffDisplayNodes( nodes, 'defaultStaffDisplay', 'defaultBarNumberDisplay', - AlphaTex1LanguageHandler._staffDisplayTypes.map(t => - AlphaTex1LanguageHandler._diffStaffDisplayValues( - AlphaTex1LanguageHandler._staffDisplayValues(t, stylesheet, undefined, undefined), - AlphaTex1LanguageHandler._staffDisplayValues(t, defaultStylesheet, undefined, undefined) - ) - ), + stylesheet, + undefined, + undefined, false ); @@ -3322,9 +3183,9 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler nodes, 'barDisplay', 'barNumberDisplay', - AlphaTex1LanguageHandler._staffDisplayTypes.map(t => - AlphaTex1LanguageHandler._staffDisplayValues(t, undefined, undefined, bar) - ), + undefined, + undefined, + bar, AlphaTex1LanguageHandler._isImpliedBarNumberDisplay(staff, bar, voice) ); @@ -3379,15 +3240,7 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler } } - AlphaTex1LanguageHandler._buildStaffDisplayNodes( - nodes, - 'staffDisplay', - '', - AlphaTex1LanguageHandler._staffDisplayTypes.map(t => - AlphaTex1LanguageHandler._staffDisplayValues(t, undefined, staff, undefined) - ), - false - ); + AlphaTex1LanguageHandler._buildStaffDisplayNodes(nodes, 'staffDisplay', '', undefined, staff, undefined, false); if (firstStaffMetaIndex < nodes.length) { nodes[firstStaffMetaIndex].leadingComments = [ @@ -3440,246 +3293,147 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler } /** - * Collects the display values of a staff type from the carrier of the given level. + * Reads the staff display values of the staff type from its carrier on stylesheet, staff or bar level. */ - private static _staffDisplayValues( - staffType: AlphaTexStaffType, + private static _readStaffDisplay( + t: AlphaTexStaffType, stylesheet: RenderStylesheet | undefined, staff: Staff | undefined, bar: Bar | undefined ): AlphaTexStaffDisplayValues { - const v: AlphaTexStaffDisplayValues = {}; - switch (staffType) { - case AlphaTexStaffType.Score: - if (bar) { - const c = bar.scoreDisplay; - if (c) { - v.clef = c.clef; - v.keySignature = c.keySignature; - v.timeSignature = c.timeSignature; - v.barNumber = c.barNumber; - } - } else { - const c = staff ? staff.scoreConfig : stylesheet!.scoreConfig; - if (c) { - v.clef = c.clef; - v.keySignature = c.keySignature; - v.timeSignature = c.timeSignature; - v.barNumber = c.barNumber; - } - } - break; - case AlphaTexStaffType.Tabs: - if (bar) { - const c = bar.tabDisplay; - if (c) { - v.clef = c.clef; - v.timeSignature = c.timeSignature; - v.barNumber = c.barNumber; - } - } else { - const c = staff ? staff.tabConfig : stylesheet!.tabConfig; - if (c) { - v.clef = c.clef; - v.timeSignature = c.timeSignature; - v.rests = c.rests; - v.barNumber = c.barNumber; - v.rhythm = c.rhythm; - } - } - break; - case AlphaTexStaffType.Slash: - if (bar) { - const c = bar.slashDisplay; - if (c) { - v.keySignature = c.keySignature; - v.timeSignature = c.timeSignature; - v.barNumber = c.barNumber; - } - } else { - const c = staff ? staff.slashConfig : stylesheet!.slashConfig; - if (c) { - v.keySignature = c.keySignature; - v.timeSignature = c.timeSignature; - v.barNumber = c.barNumber; - } - } - break; - case AlphaTexStaffType.Numbered: - if (bar) { - const c = bar.numberedDisplay; - if (c) { - v.timeSignature = c.timeSignature; - v.barNumber = c.barNumber; - } - } else { - const c = staff ? staff.numberedConfig : stylesheet!.numberedConfig; - if (c) { - v.timeSignature = c.timeSignature; - v.barNumber = c.barNumber; - } - } - break; - } - return v; - } - - /** - * Keeps only the axes of the values which differ from the defaults. - */ - private static _diffStaffDisplayValues( - values: AlphaTexStaffDisplayValues, - defaults: AlphaTexStaffDisplayValues - ): AlphaTexStaffDisplayValues { - const diff: AlphaTexStaffDisplayValues = {}; - diff.clef = AlphaTex1LanguageHandler._diffElementDisplay(values.clef, defaults.clef); - diff.keySignature = AlphaTex1LanguageHandler._diffElementDisplay(values.keySignature, defaults.keySignature); - diff.timeSignature = AlphaTex1LanguageHandler._diffElementDisplay(values.timeSignature, defaults.timeSignature); - diff.rests = AlphaTex1LanguageHandler._diffElementDisplay(values.rests, defaults.rests); - if (values.barNumber !== defaults.barNumber) { - diff.barNumber = values.barNumber; - } - if (values.rhythm !== defaults.rhythm) { - diff.rhythm = values.rhythm; - } - return diff; - } - - private static _diffElementDisplay( - value: ElementDisplay | undefined, - defaults: ElementDisplay | undefined - ): ElementDisplay | undefined { - if (!value) { - return undefined; - } - const diff: ElementDisplay = {}; - let hasDiff = false; - if (value.isVisible !== defaults?.isVisible) { - diff.isVisible = value.isVisible; - hasDiff = true; - } - if (value.staffPlacement !== defaults?.staffPlacement) { - diff.staffPlacement = value.staffPlacement; - hasDiff = true; - } - if (value.systemDisplay !== defaults?.systemDisplay) { - diff.systemDisplay = value.systemDisplay; - hasDiff = true; + switch (t) { + case AlphaTexStaffType.Score: { + const o = bar?.scoreDisplay; + const c = bar ? undefined : staff ? staff.scoreConfig : stylesheet!.scoreConfig; + return { + clef: o?.clef ?? c?.clef, + ks: o?.keySignature ?? c?.keySignature, + ts: o?.timeSignature ?? c?.timeSignature, + barNumber: o?.barNumber ?? c?.barNumber + }; + } + case AlphaTexStaffType.Tabs: { + const o = bar?.tabDisplay; + const c = bar ? undefined : staff ? staff.tabConfig : stylesheet!.tabConfig; + return { + clef: o?.clef ?? c?.clef, + ts: o?.timeSignature ?? c?.timeSignature, + rests: c?.rests, + barNumber: o?.barNumber ?? c?.barNumber, + rhythm: c?.rhythm + }; + } + case AlphaTexStaffType.Slash: { + const o = bar?.slashDisplay; + const c = bar ? undefined : staff ? staff.slashConfig : stylesheet!.slashConfig; + return { + ks: o?.keySignature ?? c?.keySignature, + ts: o?.timeSignature ?? c?.timeSignature, + barNumber: o?.barNumber ?? c?.barNumber + }; + } + default: { + const o = bar?.numberedDisplay; + const c = bar ? undefined : staff ? staff.numberedConfig : stylesheet!.numberedConfig; + return { ts: o?.timeSignature ?? c?.timeSignature, barNumber: o?.barNumber ?? c?.barNumber }; + } } - return hasDiff ? diff : undefined; } /** - * Lists the staff display properties (name and value) for the defined values. + * Lists the defined values as ` `. */ - private static _staffDisplayPropertyValues(values: AlphaTexStaffDisplayValues): [string, string][] { - const props: [string, string][] = []; - AlphaTex1LanguageHandler._elementDisplayPropertyValues(props, 'clef', values.clef); - AlphaTex1LanguageHandler._elementDisplayPropertyValues(props, 'ks', values.keySignature); - AlphaTex1LanguageHandler._elementDisplayPropertyValues(props, 'ts', values.timeSignature); - AlphaTex1LanguageHandler._elementDisplayPropertyValues(props, 'rests', values.rests); - if (values.barNumber !== undefined) { - props.push(['barNumber', AlphaTex1EnumMappings.barNumberDisplayReversed.get(values.barNumber!)!]); - } - if (values.rhythm !== undefined) { - props.push(['rhythm', AlphaTex1EnumMappings.tabRhythmModeReversed.get(values.rhythm!)!]); + private static _staffDisplayProperties(v: AlphaTexStaffDisplayValues): string[] { + const props: string[] = []; + AlphaTex1LanguageHandler._elementDisplayProperties(props, 'clef', v.clef); + AlphaTex1LanguageHandler._elementDisplayProperties(props, 'ks', v.ks); + AlphaTex1LanguageHandler._elementDisplayProperties(props, 'ts', v.ts); + AlphaTex1LanguageHandler._elementDisplayProperties(props, 'rests', v.rests); + if (v.barNumber !== undefined) { + props.push(`barNumber ${AlphaTex1EnumMappings.barNumberDisplayReversed.get(v.barNumber!)}`); + } + if (v.rhythm !== undefined) { + props.push(`rhythm ${AlphaTex1EnumMappings.tabRhythmModeReversed.get(v.rhythm!)}`); } return props; } - private static _elementDisplayPropertyValues( - props: [string, string][], - prefix: string, - display: ElementDisplay | undefined - ) { - if (!display) { + private static _elementDisplayProperties(props: string[], element: string, d: ElementDisplay | undefined) { + if (!d) { return; } - if (display.isVisible !== undefined) { - props.push([`${prefix}Visibility`, display.isVisible! ? 'true' : 'false']); + if (d.isVisible !== undefined) { + props.push(`${element}Visibility ${d.isVisible! ? 'true' : 'false'}`); } - if (display.staffPlacement !== undefined) { - props.push([ - `${prefix}Placement`, - AlphaTex1EnumMappings.staffPlacementReversed.get(display.staffPlacement!)! - ]); + if (d.staffPlacement !== undefined) { + props.push(`${element}Placement ${AlphaTex1EnumMappings.staffPlacementReversed.get(d.staffPlacement!)}`); } - if (display.systemDisplay !== undefined) { - props.push([`${prefix}Systems`, AlphaTex1EnumMappings.systemDisplayReversed.get(display.systemDisplay!)!]); + if (d.systemDisplay !== undefined) { + props.push(`${element}Systems ${AlphaTex1EnumMappings.systemDisplayReversed.get(d.systemDisplay!)}`); } } /** - * Builds the staff display tags for one level. Equal property values across staff types are grouped - * into one tag, the staff types are omitted if the value applies to all staff types supporting the property. - * @param valuesPerType the values for each of {@link _staffDisplayTypes} - * @param barNumberShorthandTag the tag to use if all staff types have the same bar number display (empty if none) - * @param impliedAllBars whether bar number display `allBars` is implied (custom bar numbers) and must not be exported + * Builds the staff display tags of one level: equal values are grouped across staff types, the staff types + * are omitted if all supporting staff types share a value, a bar number shared by all staff types uses the shorthand tag. + * On score level only values differing from the defaults are written. */ private static _buildStaffDisplayNodes( nodes: AlphaTexMetaDataNode[], tag: string, - barNumberShorthandTag: string, - valuesPerType: AlphaTexStaffDisplayValues[], + shorthandTag: string, + stylesheet: RenderStylesheet | undefined, + staff: Staff | undefined, + bar: Bar | undefined, impliedAllBars: boolean ) { - const staffTypes = AlphaTex1LanguageHandler._staffDisplayTypes; - - // property value -> staff types using it + const defaults = stylesheet ? AlphaTex1LanguageHandler._defaultScore.stylesheet : undefined; const propertyTypes = new Map(); - const propertyValues = new Map(); - for (let i = 0; i < staffTypes.length; i++) { - const values = valuesPerType[i]; - if (impliedAllBars && values.barNumber === BarNumberDisplay.AllBars) { - values.barNumber = undefined; + for (const t of AlphaTex1LanguageHandler._staffTypes) { + let skip: string[] = []; + if (defaults) { + skip = AlphaTex1LanguageHandler._staffDisplayProperties( + AlphaTex1LanguageHandler._readStaffDisplay(t, defaults, undefined, undefined) + ); + } + if (impliedAllBars) { + skip.push('barNumber allbars'); } - for (const p of AlphaTex1LanguageHandler._staffDisplayPropertyValues(values)) { - const key = `${p[0]} ${p[1]}`; - if (!propertyTypes.has(key)) { - propertyTypes.set(key, []); - propertyValues.set(key, p); + const values = AlphaTex1LanguageHandler._readStaffDisplay(t, stylesheet, staff, bar); + for (const p of AlphaTex1LanguageHandler._staffDisplayProperties(values)) { + if (skip.indexOf(p) === -1) { + if (!propertyTypes.has(p)) { + propertyTypes.set(p, []); + } + propertyTypes.get(p)!.push(t); } - propertyTypes.get(key)!.push(staffTypes[i]); } } - // staff types -> properties - const groupTypes = new Map(); - const groupProperties = new Map(); - groupTypes.set('', []); - groupProperties.set('', []); - for (const [key, types] of propertyTypes) { - const p = propertyValues.get(key)!; - const element = AlphaTex1LanguageHandler._staffDisplayPropertyElements.get(p[0].toLowerCase())!; - const supported = AlphaTex1LanguageHandler._staffDisplayElementTypes.get(element)!; - const group = types.length === supported.length ? '' : AlphaTex1LanguageHandler._staffTypeNames(types); - - if (group === '' && element === 'barnumber' && barNumberShorthandTag.length > 0) { - const barNumber = AlphaTex1EnumMappings.barNumberDisplay.get(p[1])!; - nodes.push(Atnf.identMeta(barNumberShorthandTag, BarNumberDisplay[barNumber])); + const tags = new Map(); + for (const [p, types] of propertyTypes) { + const name = p.substring(0, p.indexOf(' ')); + const value = p.substring(p.indexOf(' ') + 1); + const element = AlphaTex1LanguageHandler._staffDisplayElement(name); + const allTypes = types.length === AlphaTex1LanguageHandler._staffDisplayElements.get(element)!.length; + if (allTypes && element === 'barNumber' && shorthandTag.length > 0) { + const barNumber = AlphaTex1EnumMappings.barNumberDisplay.get(value)!; + nodes.push(Atnf.identMeta(shorthandTag, BarNumberDisplay[barNumber])); continue; } - if (!groupTypes.has(group)) { - groupTypes.set(group, types); - groupProperties.set(group, []); - } - groupProperties.get(group)!.push(p); - } - - for (const [group, props] of groupProperties) { - if (props.length === 0) { - continue; - } - const args: IAlphaTexArgumentValue[] = []; - for (const t of groupTypes.get(group)!) { - args.push(Atnf.ident(AlphaTex1EnumMappings.alphaTexStaffTypeReversed.get(t)!)); - } - const propNodes = Atnf.props([]); - for (const p of props) { - Atnf.prop(propNodes.properties, p[0], Atnf.identValue(p[1])); + const key = allTypes ? '' : types.join(','); + if (!tags.has(key)) { + const args: IAlphaTexArgumentValue[] = []; + if (!allTypes) { + for (const t of types) { + args.push(Atnf.ident(AlphaTex1EnumMappings.alphaTexStaffTypeReversed.get(t)!)); + } + } + tags.set(key, Atnf.meta(tag, Atnf.args(args), Atnf.props([]))); + nodes.push(tags.get(key)!); } - nodes.push(Atnf.meta(tag, Atnf.args(args), propNodes)); + Atnf.prop(tags.get(key)!.properties!.properties, name, Atnf.identValue(value)); } } diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTexShared.ts b/packages/alphatab/src/importer/alphaTex/AlphaTexShared.ts index 9477906c2d..b609fafe59 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTexShared.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTexShared.ts @@ -292,26 +292,17 @@ export enum AlphaTexVoiceMode { } /** - * The staff types to which the alphaTex staff display tags - * (`\defaultStaffDisplay`, `\staffDisplay`, `\barDisplay`) apply. + * The staff types selected by the alphaTex staff display tags (`\defaultStaffDisplay`, `\staffDisplay`, `\barDisplay`). * @public */ export enum AlphaTexStaffType { - /** - * The standard notation staff. - */ + /** Standard notation */ Score = 0, - /** - * The guitar tablature staff. - */ + /** Guitar tablature */ Tabs = 1, - /** - * The slash notation staff. - */ + /** Slash notation */ Slash = 2, - /** - * The numbered notation (Jianpu) staff. - */ + /** Numbered notation (Jianpu) */ Numbered = 3 } diff --git a/packages/alphatab/test/importer/AlphaTexImporter.test.ts b/packages/alphatab/test/importer/AlphaTexImporter.test.ts index d971e8489c..27eea8bb0e 100644 --- a/packages/alphatab/test/importer/AlphaTexImporter.test.ts +++ b/packages/alphatab/test/importer/AlphaTexImporter.test.ts @@ -2693,80 +2693,33 @@ describe('AlphaTexImporterTest', () => { }); describe('staffDisplay', () => { - function semanticDiagnosticCodes(tex: string): AlphaTexDiagnosticCode[] { - const importer: AlphaTexImporter = new AlphaTexImporter(); - importer.initFromString(tex, new Settings()); - try { - importer.readScore(); - } catch { - // checked by caller - } - return importer.semanticDiagnostics.items.map(d => d.code); - } - - it('score-level', () => { - const score = parseTex( - '\\defaultStaffDisplay tabs { tsVisibility false tsPlacement allStaves restsSystems firstSystemOnly rhythm showWithBeams } C4' - ); + it('levels', () => { + const score = parseTex(` + \\defaultStaffDisplay tabs { tsVisibility false tsPlacement allStaves rhythm showWithBeams } + \\staff {score tabs} + \\staffDisplay { ksVisibility false barNumber hide } + C4 | \\barDisplay (score tabs) { clefVisibility false tsSystems firstSystemOnly } C4 | C4 + `); const tabs = score.stylesheet.tabConfig; expect(tabs.timeSignature!.isVisible).toBe(false); expect(tabs.timeSignature!.staffPlacement).toBe(StaffPlacement.AllStaves); expect(tabs.timeSignature!.systemDisplay).toBe(SystemDisplay.AllSystems); - expect(tabs.rests!.isVisible).toBe(true); - expect(tabs.rests!.systemDisplay).toBe(SystemDisplay.FirstSystemOnly); expect(tabs.rhythm).toBe(TabRhythmMode.ShowWithBeams); expect(score.stylesheet.scoreConfig.timeSignature!.isVisible).toBe(true); - testExportRoundtrip(score); - }); - - it('staff-level-all-types', () => { - const score = parseTex('\\staff {score tabs} \\staffDisplay { tsPlacement primary barNumber hide } C4'); - const staff = score.tracks[0].staves[0]; - const timeSignatures = [ - staff.scoreConfig!.timeSignature!, - staff.tabConfig!.timeSignature!, - staff.slashConfig!.timeSignature!, - staff.numberedConfig!.timeSignature! - ]; - for (const ts of timeSignatures) { - expect(ts.staffPlacement).toBe(StaffPlacement.Primary); - expect(ts.isVisible).toBeUndefined(); - expect(ts.systemDisplay).toBeUndefined(); - } - expect([ - staff.scoreConfig!.barNumber, - staff.tabConfig!.barNumber, - staff.slashConfig!.barNumber, - staff.numberedConfig!.barNumber - ]).toEqual([BarNumberDisplay.Hide, BarNumberDisplay.Hide, BarNumberDisplay.Hide, BarNumberDisplay.Hide]); - expect(staff.scoreConfig!.clef).toBeUndefined(); - - testExportRoundtrip(score); - }); - - it('staff-level-supported-types-only', () => { - const score = parseTex('\\staffDisplay { ksVisibility false } C4'); const staff = score.tracks[0].staves[0]; expect(staff.scoreConfig!.keySignature!.isVisible).toBe(false); expect(staff.slashConfig!.keySignature!.isVisible).toBe(false); - expect(staff.tabConfig).toBeUndefined(); - expect(staff.numberedConfig).toBeUndefined(); - - testExportRoundtrip(score); - }); + expect(staff.scoreConfig!.clef).toBeUndefined(); + expect(staff.tabConfig!.barNumber).toBe(BarNumberDisplay.Hide); + expect(staff.numberedConfig!.barNumber).toBe(BarNumberDisplay.Hide); - it('bar-level', () => { - const score = parseTex( - 'C4 | \\barDisplay (score tabs) { clefVisibility false tsSystems firstSystemOnly } C4 | C4' - ); - const bars = score.tracks[0].staves[0].bars; + const bars = staff.bars; expect(bars[0].scoreDisplay).toBeUndefined(); expect(bars[1].scoreDisplay!.clef!.isVisible).toBe(false); - expect(bars[1].scoreDisplay!.timeSignature!.systemDisplay).toBe(SystemDisplay.FirstSystemOnly); expect(bars[1].tabDisplay!.clef!.isVisible).toBe(false); + expect(bars[1].tabDisplay!.timeSignature!.systemDisplay).toBe(SystemDisplay.FirstSystemOnly); expect(bars[1].slashDisplay).toBeUndefined(); - expect(bars[1].numberedDisplay).toBeUndefined(); expect(bars[2].scoreDisplay).toBeUndefined(); testExportRoundtrip(score); @@ -2774,73 +2727,49 @@ describe('AlphaTexImporterTest', () => { it('later-tags-win', () => { const score = parseTex( - '\\defaultStaffDisplay { tsVisibility false } \\defaultStaffDisplay tabs { tsVisibility true tsPlacement allStaves } C4' + '\\defaultStaffDisplay { tsVisibility false } \\defaultStaffDisplay tabs { tsVisibility true tsSystems firstSystemOnly } C4' ); expect(score.stylesheet.scoreConfig.timeSignature!.isVisible).toBe(false); - expect(score.stylesheet.slashConfig.timeSignature!.isVisible).toBe(false); expect(score.stylesheet.tabConfig.timeSignature!.isVisible).toBe(true); - expect(score.stylesheet.tabConfig.timeSignature!.staffPlacement).toBe(StaffPlacement.AllStaves); + expect(score.stylesheet.tabConfig.timeSignature!.systemDisplay).toBe(SystemDisplay.FirstSystemOnly); + expect(score.stylesheet.tabConfig.timeSignature!.staffPlacement).toBe(StaffPlacement.Primary); testExportRoundtrip(score); }); it('visibility-values', () => { - const visibility = (value: string) => - parseTex(`\\staffDisplay score { tsVisibility ${value} } C4`).tracks[0].staves[0].scoreConfig! - .timeSignature!.isVisible; - expect(visibility('')).toBe(true); - expect(visibility('true')).toBe(true); - expect(visibility('false')).toBe(false); - expect(visibility('"false"')).toBe(false); - expect(visibility('1')).toBe(true); - expect(visibility('0')).toBe(false); - }); - - it('unsupported-staff-type', () => { - expect(semanticDiagnosticCodes('\\staffDisplay tabs { ksVisibility false } C4')).toContain( - AlphaTexDiagnosticCode.AT307 - ); - const score = parseTex('\\staffDisplay tabs { ksVisibility false } C4'); - expect(score.tracks[0].staves[0].tabConfig).toBeUndefined(); + const values = ['', 'true', 'false', '"false"', '1', '0']; + const expected = [true, true, false, false, true, false]; + for (let i = 0; i < values.length; i++) { + const score = parseTex(`\\staffDisplay score { tsVisibility ${values[i]} } C4`); + expect(score.tracks[0].staves[0].scoreConfig!.timeSignature!.isVisible).toBe(expected[i]); + } }); - it('bar-level-unsupported-properties', () => { - for (const tex of [ - 'C4 | \\barDisplay { restsVisibility false } C4', - 'C4 | \\barDisplay { rhythm hidden } C4' - ]) { - const importer: AlphaTexImporter = new AlphaTexImporter(); - importer.initFromString(tex, new Settings()); + it('diagnostics', () => { + const importer = (tex: string) => { + const i = new AlphaTexImporter(); + i.initFromString(tex, new Settings()); try { - importer.readScore(); + i.readScore(); } catch { // checked below } - expect(importer.parserDiagnostics.items.map(d => d.code)).toContain(AlphaTexDiagnosticCode.AT205); - } + return i; + }; + const unsupported = importer('\\staffDisplay tabs { ksVisibility false } C4'); + expect(unsupported.semanticDiagnostics.items.map(d => d.code)).toContain(AlphaTexDiagnosticCode.AT307); + const barLevel = importer('C4 | \\barDisplay { restsVisibility false } C4'); + expect(barLevel.parserDiagnostics.items.map(d => d.code)).toContain(AlphaTexDiagnosticCode.AT205); + expect(parseTex('\\staffDisplay tabs { ksVisibility false } C4').tracks[0].staves[0].tabConfig).toBe( + undefined + ); }); it('legacy-shorthands', () => { const score = parseTex('\\defaultBarNumberDisplay hide C4 | \\barNumberDisplay allBars C4'); - const stylesheet = score.stylesheet; - expect([ - stylesheet.scoreConfig.barNumber, - stylesheet.tabConfig.barNumber, - stylesheet.slashConfig.barNumber, - stylesheet.numberedConfig.barNumber - ]).toEqual([BarNumberDisplay.Hide, BarNumberDisplay.Hide, BarNumberDisplay.Hide, BarNumberDisplay.Hide]); - const bar = score.tracks[0].staves[0].bars[1]; - expect([ - bar.scoreDisplay!.barNumber, - bar.tabDisplay!.barNumber, - bar.slashDisplay!.barNumber, - bar.numberedDisplay!.barNumber - ]).toEqual([ - BarNumberDisplay.AllBars, - BarNumberDisplay.AllBars, - BarNumberDisplay.AllBars, - BarNumberDisplay.AllBars - ]); + expect(score.stylesheet.tabConfig.barNumber).toBe(BarNumberDisplay.Hide); + expect(score.tracks[0].staves[0].bars[1].tabDisplay!.barNumber).toBe(BarNumberDisplay.AllBars); testExportRoundtrip(score); }); diff --git a/packages/alphatex/src/common.ts b/packages/alphatex/src/common.ts index 75de4dcda4..eb32780039 100644 --- a/packages/alphatex/src/common.ts +++ b/packages/alphatex/src/common.ts @@ -153,51 +153,25 @@ export function booleanSignatures( trueDescription: string, falseDescription: string ): SignatureDefinition[] { + const overload = (type: alphaTab.importer.alphaTex.AlphaTexNodeType, t: string, f: string, quote: string) => ({ + parameters: [ + { + name, + shortDescription, + parseMode: alphaTab.importer.alphaTex.ArgumentListParseTypesMode.Required, + type, + values: [ + { name: t, snippet: `${quote}${t}${quote}`, shortDescription: trueDescription }, + { name: f, snippet: `${quote}${f}${quote}`, shortDescription: falseDescription } + ] + } + ] + }); + const types = alphaTab.importer.alphaTex.AlphaTexNodeType; return [ - { - parameters: [] - }, - { - parameters: [ - { - name, - shortDescription, - parseMode: alphaTab.importer.alphaTex.ArgumentListParseTypesMode.Required, - type: alphaTab.importer.alphaTex.AlphaTexNodeType.String, - values: [ - { name: 'true', snippet: '"true"', shortDescription: trueDescription }, - { name: 'false', snippet: '"false"', shortDescription: falseDescription } - ] - } - ] - }, - { - parameters: [ - { - name, - shortDescription, - parseMode: alphaTab.importer.alphaTex.ArgumentListParseTypesMode.Required, - type: alphaTab.importer.alphaTex.AlphaTexNodeType.Ident, - values: [ - { name: 'true', snippet: 'true', shortDescription: trueDescription }, - { name: 'false', snippet: 'false', shortDescription: falseDescription } - ] - } - ] - }, - { - parameters: [ - { - name, - shortDescription, - parseMode: alphaTab.importer.alphaTex.ArgumentListParseTypesMode.Required, - type: alphaTab.importer.alphaTex.AlphaTexNodeType.Number, - values: [ - { name: '1', snippet: '1', shortDescription: trueDescription }, - { name: '0', snippet: '0', shortDescription: falseDescription } - ] - } - ] - } + { parameters: [] }, + overload(types.String, 'true', 'false', '"'), + overload(types.Ident, 'true', 'false', ''), + overload(types.Number, '1', '0', '') ]; } diff --git a/packages/alphatex/src/enum.ts b/packages/alphatex/src/enum.ts index 57abe42171..3937c06f60 100644 --- a/packages/alphatex/src/enum.ts +++ b/packages/alphatex/src/enum.ts @@ -127,10 +127,10 @@ export const alphaTexMappedEnumMapping: { } }, AlphaTexStaffType: { - Score: { snippet: 'score', shortDescription: 'The standard notation staff' }, - Tabs: { snippet: 'tabs', shortDescription: 'The guitar tablature staff' }, - Slash: { snippet: 'slash', shortDescription: 'The slash notation staff' }, - Numbered: { snippet: 'numbered', shortDescription: 'The numbered notation (Jianpu) staff' } + Score: { snippet: 'score', shortDescription: 'Standard notation' }, + Tabs: { snippet: 'tabs', shortDescription: 'Guitar tablature' }, + Slash: { snippet: 'slash', shortDescription: 'Slash notation' }, + Numbered: { snippet: 'numbered', shortDescription: 'Numbered notation (Jianpu)' } }, NoteAccidentalMode: { Default: { snippet: 'default', shortDescription: 'Auto-detect the accidentals', aliases: ['d'] }, @@ -476,32 +476,18 @@ export const alphaTexMappedEnumMapping: { Hide: { snippet: 'hide', shortDescription: 'Hide' } }, StaffPlacement: { - AllStaves: { snippet: 'allStaves', shortDescription: 'Show the element on all staves' }, - Primary: { - snippet: 'primary', - shortDescription: 'Show the element only on the primary staff', - longDescription: - 'Show the element only on the topmost displayed notation of the staff (priority: score, tabs, slash, numbered).' - } + AllStaves: { snippet: 'allStaves', shortDescription: 'All staves' }, + Primary: { snippet: 'primary', shortDescription: 'Only the primary (topmost) staff' } }, SystemDisplay: { - AllSystems: { snippet: 'allSystems', shortDescription: 'Show the element on all systems' }, - FirstSystemOnly: { snippet: 'firstSystemOnly', shortDescription: 'Show the element only on the first system' } + AllSystems: { snippet: 'allSystems', shortDescription: 'All systems' }, + FirstSystemOnly: { snippet: 'firstSystemOnly', shortDescription: 'Only the first system' } }, TabRhythmMode: { - Hidden: { snippet: 'hidden', shortDescription: 'Hide the rhythm notation' }, - ShowWithBeams: { - snippet: 'showWithBeams', - shortDescription: 'Show rhythm notation with individual beams per beat' - }, - ShowWithBars: { - snippet: 'showWithBars', - shortDescription: 'Show rhythm notation with connected bars like in standard notation' - }, - Automatic: { - snippet: 'automatic', - shortDescription: 'Show rhythm notation automatically if no standard notation is shown' - } + Hidden: { snippet: 'hidden', shortDescription: 'Hidden' }, + ShowWithBeams: { snippet: 'showWithBeams', shortDescription: 'Individual beams per beat' }, + ShowWithBars: { snippet: 'showWithBars', shortDescription: 'Connected bars like standard notation' }, + Automatic: { snippet: 'automatic', shortDescription: 'Shown if no standard notation is shown' } } }; diff --git a/packages/alphatex/src/staffDisplay.ts b/packages/alphatex/src/staffDisplay.ts index 160a82a27c..ff8dd10a03 100644 --- a/packages/alphatex/src/staffDisplay.ts +++ b/packages/alphatex/src/staffDisplay.ts @@ -1,11 +1,10 @@ import * as alphaTab from '@coderline/alphatab'; import { booleanSignatures, properties } from '@coderline/alphatab-alphatex/common'; -import { enumParameter } from '@coderline/alphatab-alphatex/enum'; +import { type AlphaTexMappedEnumName, enumParameter } from '@coderline/alphatab-alphatex/enum'; import type { AlphaTexExample, PropertyDefinition, SignatureDefinition } from '@coderline/alphatab-alphatex/types'; /** - * The level on which a staff display tag applies its properties: - * score-wide (`\defaultStaffDisplay`), per staff (`\staffDisplay`) or per bar (`\barDisplay`). + * The level of a staff display tag: score (`\defaultStaffDisplay`), staff (`\staffDisplay`) or bar (`\barDisplay`). */ export type StaffDisplayLevel = 'score' | 'staff' | 'bar'; @@ -18,8 +17,8 @@ export function staffDisplayTargetSignatures(): SignatureDefinition[] { parameters: [ { name: 'staffTypes', - shortDescription: 'The staff types to which the properties apply', - longDescription: `The staff types to which the properties apply. If omitted, every property applies to all staff types supporting the related element.`, + shortDescription: + 'The staff types to which the properties apply. If omitted, every property applies to all staff types supporting it.', parseMode: alphaTab.importer.alphaTex.ArgumentListParseTypesMode.ValueListWithoutParenthesis, ...enumParameter('AlphaTexStaffType') } @@ -28,269 +27,111 @@ export function staffDisplayTargetSignatures(): SignatureDefinition[] { ]; } -/** - * The sample content used to build the examples of a staff display property. - */ -interface StaffDisplaySample { - /** - * The staff types to apply the properties to (empty for all). - */ - target: string; - /** - * The properties to apply. - */ - props: string; - /** - * The notations to show on the staff (`\staff` properties). - */ - staff: string; - /** - * The bars of the example, the staff display tag is placed on the second bar for bar level examples. - */ - bars: string[]; - /** - * Whether the example needs multiple systems to show the effect. - */ - systems?: true; +function enumSignatures(name: string, shortDescription: string, type: AlphaTexMappedEnumName): SignatureDefinition[] { + return [ + { + parameters: [ + { + name, + shortDescription, + parseMode: alphaTab.importer.alphaTex.ArgumentListParseTypesMode.Required, + ...enumParameter(type) + } + ] + } + ]; } -function staffDisplayExample(tag: string, level: StaffDisplayLevel, sample: StaffDisplaySample): AlphaTexExample { - const meta = `${tag}${sample.target ? ` ${sample.target}` : ''} { ${sample.props} }`; - const lines: string[] = []; - if (level === 'score') { - lines.push(meta); - } - lines.push(sample.systems ? '\\track { defaultSystemsLayout 2 }' : '\\track'); - lines.push(` \\staff {${sample.staff}}`); - if (level === 'staff') { - lines.push(` ${meta}`); - } +function staffDisplayExample(tag: string, level: StaffDisplayLevel, props: string): AlphaTexExample { + const meta = `${tag} { ${props} }`; + const bars = ['\\ks D 3.3.4 r 3.3 3.3', '3.3.4 3.3 r 3.3', '\\ts (3 4) \\ks A 3.3.4 3.3 r', '3.3.4 r 3.3']; if (level === 'bar') { - lines.push(` ${sample.bars[0]} |`); - lines.push(` ${meta} ${sample.bars.slice(1).join(' | ')}`); - } else { - lines.push(` ${sample.bars.join(' | ')}`); + bars[2] = `${meta} ${bars[2]}`; } - - const tex = lines.join('\n'); - return sample.systems ? { options: { display: { layoutMode: 'Parchment' } }, tex } : tex; + const lines = [ + level === 'score' ? meta : '', + '\\track { defaultSystemsLayout 2 }', + ' \\staff {score tabs}', + level === 'staff' ? ` ${meta}` : '', + ` ${bars.join(' | ')}` + ]; + return { options: { display: { layoutMode: 'Parchment' } }, tex: lines.filter(l => l.length > 0).join('\n') }; } /** - * Describes an element which is configured via the `Visibility`, `Placement` and - * `Systems` properties. + * The elements configured via `Visibility`, `Placement` and `Systems`: + * prefix, name, supported staff types, available on bar level. */ -interface StaffDisplayElement { - prefix: string; - name: string; - staffTypes: string; - barLevel: boolean; - visibility: StaffDisplaySample; - placement: StaffDisplaySample; - systems: StaffDisplaySample; -} - -const tabBars = ['3.3.4 3.3 3.3 3.3', '3.3 3.3 3.3 3.3', '3.3 3.3 3.3 3.3', '3.3 3.3 3.3 3.3']; -const tabRestBars = ['3.3.4 r 3.3 r', 'r 3.3 r 3.3', '3.3 r 3.3 r', 'r 3.3 r 3.3']; - -const staffDisplayElements: StaffDisplayElement[] = [ - { - prefix: 'clef', - name: 'clef', - staffTypes: '`score`, `tabs`', - barLevel: true, - visibility: { target: 'tabs', props: 'clefVisibility false', staff: 'score tabs', bars: tabBars.slice(0, 2) }, - placement: { target: '', props: 'clefPlacement primary', staff: 'score tabs', bars: tabBars.slice(0, 2) }, - systems: { - target: '', - props: 'clefSystems firstSystemOnly', - staff: 'score tabs', - bars: tabBars, - systems: true - } - }, - { - prefix: 'ks', - name: 'key signature', - staffTypes: '`score`, `slash`', - barLevel: true, - visibility: { - target: 'slash', - props: 'ksVisibility true', - staff: 'score slash', - bars: ['\\ks D 3.3.4 3.3 3.3 3.3', '\\ks A 3.3 3.3 3.3 3.3'] - }, - placement: { - target: '', - props: 'ksVisibility true ksPlacement primary', - staff: 'score slash', - bars: ['\\ks D 3.3.4 3.3 3.3 3.3', '\\ks A 3.3 3.3 3.3 3.3'] - }, - systems: { - target: '', - props: 'ksSystems firstSystemOnly', - staff: 'score', - bars: ['\\ks D 3.3.4 3.3 3.3 3.3', '3.3 3.3 3.3 3.3', '3.3 3.3 3.3 3.3', '3.3 3.3 3.3 3.3'], - systems: true - } - }, - { - prefix: 'ts', - name: 'time signature', - staffTypes: '`score`, `tabs`, `slash`, `numbered`', - barLevel: true, - visibility: { target: 'score', props: 'tsVisibility false', staff: 'score tabs', bars: tabBars.slice(0, 2) }, - placement: { target: 'tabs', props: 'tsPlacement allStaves', staff: 'score tabs', bars: tabBars.slice(0, 2) }, - systems: { - target: '', - props: 'tsSystems firstSystemOnly', - staff: 'score tabs', - bars: ['3.3.4 3.3 3.3 3.3', '3.3 3.3 3.3 3.3', '\\ts (3 4) 3.3.4 3.3 3.3', '3.3 3.3 3.3'], - systems: true - } - }, - { - prefix: 'rests', - name: 'rests', - staffTypes: '`tabs`', - barLevel: false, - visibility: { target: '', props: 'restsVisibility false', staff: 'tabs', bars: tabRestBars.slice(0, 2) }, - placement: { - target: '', - props: 'restsPlacement allStaves', - staff: 'score tabs', - bars: tabRestBars.slice(0, 2) - }, - systems: { target: '', props: 'restsSystems firstSystemOnly', staff: 'tabs', bars: tabRestBars, systems: true } - } +const staffDisplayElements: [string, string, string, boolean][] = [ + ['clef', 'clef', '`score`, `tabs`', true], + ['ks', 'key signature', '`score`, `slash`', true], + ['ts', 'time signature', '`score`, `tabs`, `slash`, `numbered`', true], + ['rests', 'rests', '`tabs`', false] ]; -function staffDisplayElementProperties( - tag: string, - level: StaffDisplayLevel, - element: StaffDisplayElement -): PropertyDefinition[] { - const supported = `Supported staff types: ${element.staffTypes}.`; - return [ - { - property: `${element.prefix}Visibility`, - snippet: `${element.prefix}Visibility $0`, - shortDescription: `Show or hide the ${element.name}.`, - longDescription: `Defines whether the ${element.name} is shown. ${supported}`, - signatures: booleanSignatures( - 'visibility', - `Whether the ${element.name} is shown`, - `Show the ${element.name}`, - `Hide the ${element.name}` - ), - examples: staffDisplayExample(tag, level, element.visibility) - }, - { - property: `${element.prefix}Placement`, - snippet: `${element.prefix}Placement $0`, - shortDescription: `Set on which staves the ${element.name} is shown.`, - longDescription: `Defines whether the ${element.name} is shown on all staves or only on the primary staff (the topmost displayed notation of the staff). ${supported}`, - signatures: [ - { - parameters: [ - { - name: 'placement', - shortDescription: `On which staves the ${element.name} is shown`, - parseMode: alphaTab.importer.alphaTex.ArgumentListParseTypesMode.Required, - ...enumParameter('StaffPlacement') - } - ] - } - ], - examples: staffDisplayExample(tag, level, element.placement) - }, - { - property: `${element.prefix}Systems`, - snippet: `${element.prefix}Systems $0`, - shortDescription: `Set on which systems the ${element.name} is shown.`, - longDescription: `Defines whether the ${element.name} is shown on all systems or only on the first system. ${supported}`, - signatures: [ - { - parameters: [ - { - name: 'systems', - shortDescription: `On which systems the ${element.name} is shown`, - parseMode: alphaTab.importer.alphaTex.ArgumentListParseTypesMode.Required, - ...enumParameter('SystemDisplay') - } - ] - } - ], - examples: staffDisplayExample(tag, level, element.systems) - } - ]; -} - /** - * Creates the properties of a staff display tag. A new set of definitions is created for every tag, - * as the definitions are prepared in place and the examples use the respective tag. - * Bar level tags only support the elements which can be overridden on bar level. + * Creates the properties of a staff display tag. Every tag gets own definitions as the definitions + * are prepared in place and the examples use the tag. */ export function staffDisplayProperties(tag: string, level: StaffDisplayLevel): Map { + const prop = ( + property: string, + shortDescription: string, + signatures: SignatureDefinition[], + value: string + ): PropertyDefinition => ({ + property, + snippet: `${property} $0`, + shortDescription, + signatures, + examples: staffDisplayExample(tag, level, `${property} ${value}`) + }); + const props: PropertyDefinition[] = []; - for (const element of staffDisplayElements) { - if (level !== 'bar' || element.barLevel) { - props.push(...staffDisplayElementProperties(tag, level, element)); + for (const [prefix, name, staffTypes, barLevel] of staffDisplayElements) { + if (level === 'bar' && !barLevel) { + continue; } + const supported = `Supported staff types: ${staffTypes}.`; + props.push( + prop( + `${prefix}Visibility`, + `Show or hide the ${name}. ${supported}`, + booleanSignatures('visibility', `Whether the ${name} is shown`, `Show the ${name}`, `Hide the ${name}`), + 'false' + ), + prop( + `${prefix}Placement`, + `Show the ${name} on all staves or only on the primary (topmost) staff. ${supported}`, + enumSignatures('placement', `On which staves the ${name} is shown`, 'StaffPlacement'), + 'primary' + ), + prop( + `${prefix}Systems`, + `Show the ${name} on all systems or only on the first system. ${supported}`, + enumSignatures('systems', `On which systems the ${name} is shown`, 'SystemDisplay'), + 'firstSystemOnly' + ) + ); } - props.push({ - property: 'barNumber', - snippet: 'barNumber $0', - shortDescription: 'Set how bar numbers are shown.', - longDescription: - 'Defines how bar numbers are shown. Supported staff types: `score`, `tabs`, `slash`, `numbered`.', - signatures: [ - { - parameters: [ - { - name: 'mode', - shortDescription: 'The mode to use', - parseMode: alphaTab.importer.alphaTex.ArgumentListParseTypesMode.Required, - ...enumParameter('BarNumberDisplay') - } - ] - } - ], - examples: staffDisplayExample(tag, level, { - target: '', - props: 'barNumber allBars', - staff: 'score tabs', - bars: tabBars, - systems: true - }) - }); - + props.push( + prop( + 'barNumber', + 'Set how bar numbers are shown. Supported staff types: all.', + enumSignatures('mode', 'The mode to use', 'BarNumberDisplay'), + 'allBars' + ) + ); if (level !== 'bar') { - props.push({ - property: 'rhythm', - snippet: 'rhythm $0', - shortDescription: 'Set how rhythm notation is shown on tabs.', - longDescription: 'Defines how the rhythm notation is shown on tabs. Supported staff types: `tabs`.', - signatures: [ - { - parameters: [ - { - name: 'mode', - shortDescription: 'The mode to use', - parseMode: alphaTab.importer.alphaTex.ArgumentListParseTypesMode.Required, - ...enumParameter('TabRhythmMode') - } - ] - } - ], - examples: staffDisplayExample(tag, level, { - target: '', - props: 'rhythm showWithBeams', - staff: 'tabs', - bars: tabBars.slice(0, 2) - }) - }); + props.push( + prop( + 'rhythm', + 'Set how the rhythm notation is shown. Supported staff types: `tabs`.', + enumSignatures('mode', 'The mode to use', 'TabRhythmMode'), + 'showWithBeams' + ) + ); } return properties(...props); From c721302e4b921cd67bd9c5952fe8ae4c9ecc1087 Mon Sep 17 00:00:00 2001 From: danielku15 Date: Fri, 9 Oct 2026 18:51:54 +0200 Subject: [PATCH 7/8] refactor(alphatex): move staff display handling into AlphaTex1StaffDisplay The per staff type carriers share their structure but not their type. Accessing them via their generated JSON serializers replaces the per type read/write code with one property table. The export writes one tag per staff type (shorthand tags for bar numbers shared by all staff types are kept). The language handler only dispatches. Refs #2912 --- .../alphaTex/AlphaTex1LanguageHandler.ts | 400 +----------------- .../alphaTex/AlphaTex1StaffDisplay.ts | 288 +++++++++++++ .../test/exporter/AlphaTexExporter.test.ts | 13 +- 3 files changed, 301 insertions(+), 400 deletions(-) create mode 100644 packages/alphatab/src/importer/alphaTex/AlphaTex1StaffDisplay.ts diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts index e428367c64..bbd1c7ec62 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts @@ -5,6 +5,7 @@ // but we emit "Hint" diagnostics for now. import { AlphaTex1EnumMappings } from '@coderline/alphatab/importer/alphaTex/AlphaTex1EnumMappings'; +import { AlphaTex1StaffDisplay } from '@coderline/alphatab/importer/alphaTex/AlphaTex1StaffDisplay'; import { AlphaTex1LanguageDefinitions, type AlphaTexSignatureDefinition @@ -31,7 +32,6 @@ import { AlphaTexDiagnosticCode, AlphaTexDiagnosticsSeverity, AlphaTexStaffNoteKind, - AlphaTexStaffType, ArgumentListParseTypesMode, type IAlphaTexImporter } from '@coderline/alphatab/importer/alphaTex/AlphaTexShared'; @@ -71,7 +71,6 @@ import { NoteOrnament } from '@coderline/alphatab/model/NoteOrnament'; import { Ottavia } from '@coderline/alphatab/model/Ottavia'; import { PercussionMapper } from '@coderline/alphatab/model/PercussionMapper'; import { PickStroke } from '@coderline/alphatab/model/PickStroke'; -import type { ElementDisplay } from '@coderline/alphatab/model/ElementDisplay'; import { BarNumberDisplay, type RenderStylesheet, TuningDisplayMode } from '@coderline/alphatab/model/RenderStylesheet'; import { HeaderFooterStyle, Score, ScoreStyle, ScoreSubElement } from '@coderline/alphatab/model/Score'; import { Section } from '@coderline/alphatab/model/Section'; @@ -86,23 +85,8 @@ import { Tuning } from '@coderline/alphatab/model/Tuning'; import { VibratoType } from '@coderline/alphatab/model/VibratoType'; import { WahPedal } from '@coderline/alphatab/model/WahPedal'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; -import type { TabRhythmMode } from '@coderline/alphatab/NotationSettings'; import { SynthConstants } from '@coderline/alphatab/synth/SynthConstants'; -/** - * The staff display values of one staff type, named like the alphaTex properties. - * @internal - * @record - */ -interface AlphaTexStaffDisplayValues { - clef?: ElementDisplay; - ks?: ElementDisplay; - ts?: ElementDisplay; - rests?: ElementDisplay; - barNumber?: BarNumberDisplay; - rhythm?: TabRhythmMode; -} - /** * @internal */ @@ -1316,41 +1300,6 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler } } - // - // staff display (\defaultStaffDisplay, \staffDisplay, \barDisplay) - - private static readonly _staffTypes = [ - AlphaTexStaffType.Score, - AlphaTexStaffType.Tabs, - AlphaTexStaffType.Slash, - AlphaTexStaffType.Numbered - ]; - - /** - * The elements configured by the staff display properties and the staff types supporting them. - */ - private static readonly _staffDisplayElements = new Map([ - ['clef', [AlphaTexStaffType.Score, AlphaTexStaffType.Tabs]], - ['ks', [AlphaTexStaffType.Score, AlphaTexStaffType.Slash]], - ['ts', AlphaTex1LanguageHandler._staffTypes], - ['rests', [AlphaTexStaffType.Tabs]], - ['barNumber', AlphaTex1LanguageHandler._staffTypes], - ['rhythm', [AlphaTexStaffType.Tabs]] - ]); - - private static _staffDisplayElement(property: string): string { - for (const e of AlphaTex1LanguageHandler._staffDisplayElements.keys()) { - if (property.toLowerCase().startsWith(e.toLowerCase())) { - return e; - } - } - return ''; - } - - /** - * Applies a staff display tag to the stylesheet, staff or bar. The arguments select the staff types, - * without arguments the properties apply to all staff types supporting them. - */ private _applyStaffDisplay( importer: IAlphaTexImporter, metaData: AlphaTexMetaDataNode, @@ -1358,188 +1307,9 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler staff: Staff | undefined, bar: Bar | undefined ) { - if (!metaData.properties) { - return; - } - const explicit: AlphaTexStaffType[] = []; - if (metaData.arguments) { - for (const a of metaData.arguments!.arguments) { - explicit.push(AlphaTex1EnumMappings.alphaTexStaffType.get((a as AlphaTexTextNode).text.toLowerCase())!); - } - } - const values: AlphaTexStaffDisplayValues = {}; - const affected = new Set(); - const lookup = AlphaTex1LanguageDefinitions.metaDataProperties.get(metaData.tag.tag.text.toLowerCase())!; - for (const p of metaData.properties!.properties) { - if (!this._checkProperty(importer, [lookup], p)) { - continue; - } - const supported = AlphaTex1LanguageHandler._staffDisplayElements.get( - AlphaTex1LanguageHandler._readStaffDisplayProperty(p, values) - )!; - for (const t of explicit.length > 0 ? explicit : supported) { - if (supported.indexOf(t) !== -1) { - affected.add(t); - } else { - importer.addSemanticDiagnostic({ - code: AlphaTexDiagnosticCode.AT307, - message: `The property '${p.property.text}' has no effect on the staff type '${AlphaTex1EnumMappings.alphaTexStaffTypeReversed.get(t)}'`, - severity: AlphaTexDiagnosticsSeverity.Warning, - start: p.start, - end: p.end - }); - } - } - } - - for (const t of affected) { - AlphaTex1LanguageHandler._writeStaffDisplay(t, stylesheet, staff, bar, values); - } - } - - /** - * Reads the (already validated) property into the values and returns the configured element. - */ - private static _readStaffDisplayProperty(p: AlphaTexPropertyNode, values: AlphaTexStaffDisplayValues): string { - const element = AlphaTex1LanguageHandler._staffDisplayElement(p.property.text); - const value = () => (p.arguments!.arguments[0] as AlphaTexTextNode).text.toLowerCase(); - const d: ElementDisplay = {}; - switch (p.property.text.toLowerCase().substring(element.length)) { - case '': - if (element === 'barNumber') { - values.barNumber = AlphaTex1EnumMappings.barNumberDisplay.get(value()); - } else { - values.rhythm = AlphaTex1EnumMappings.tabRhythmMode.get(value()); - } - return element; - case 'visibility': - d.isVisible = p.arguments - ? AlphaTex1LanguageHandler._booleanLikeValue(p.arguments!.arguments, 0) - : true; - break; - case 'placement': - d.staffPlacement = AlphaTex1EnumMappings.staffPlacement.get(value()); - break; - default: - d.systemDisplay = AlphaTex1EnumMappings.systemDisplay.get(value()); - break; - } - switch (element) { - case 'clef': - values.clef = AlphaTex1LanguageHandler._mergeDisplay(values.clef, d); - break; - case 'ks': - values.ks = AlphaTex1LanguageHandler._mergeDisplay(values.ks, d); - break; - case 'ts': - values.ts = AlphaTex1LanguageHandler._mergeDisplay(values.ts, d); - break; - default: - values.rests = AlphaTex1LanguageHandler._mergeDisplay(values.rests, d); - break; - } - return element; - } - - /** - * Merges the defined axes of the values into the current display. - */ - private static _mergeDisplay( - current: ElementDisplay | undefined, - values: ElementDisplay | undefined - ): ElementDisplay | undefined { - if (!values) { - return current; - } - const merged: ElementDisplay = current ?? {}; - merged.isVisible = values.isVisible ?? merged.isVisible; - merged.staffPlacement = values.staffPlacement ?? merged.staffPlacement; - merged.systemDisplay = values.systemDisplay ?? merged.systemDisplay; - return merged; - } - - /** - * Merges the values supported by the staff type into its carrier (created on demand on staff and bar level). - */ - private static _writeStaffDisplay( - t: AlphaTexStaffType, - stylesheet: RenderStylesheet | undefined, - staff: Staff | undefined, - bar: Bar | undefined, - v: AlphaTexStaffDisplayValues - ) { - switch (t) { - case AlphaTexStaffType.Score: - if (bar) { - bar.scoreDisplay ??= {}; - const c = bar.scoreDisplay!; - c.clef = AlphaTex1LanguageHandler._mergeDisplay(c.clef, v.clef); - c.keySignature = AlphaTex1LanguageHandler._mergeDisplay(c.keySignature, v.ks); - c.timeSignature = AlphaTex1LanguageHandler._mergeDisplay(c.timeSignature, v.ts); - c.barNumber = v.barNumber ?? c.barNumber; - } else { - if (staff) { - staff.scoreConfig ??= {}; - } - const c = staff ? staff.scoreConfig! : stylesheet!.scoreConfig; - c.clef = AlphaTex1LanguageHandler._mergeDisplay(c.clef, v.clef); - c.keySignature = AlphaTex1LanguageHandler._mergeDisplay(c.keySignature, v.ks); - c.timeSignature = AlphaTex1LanguageHandler._mergeDisplay(c.timeSignature, v.ts); - c.barNumber = v.barNumber ?? c.barNumber; - } - break; - case AlphaTexStaffType.Tabs: - if (bar) { - bar.tabDisplay ??= {}; - const c = bar.tabDisplay!; - c.clef = AlphaTex1LanguageHandler._mergeDisplay(c.clef, v.clef); - c.timeSignature = AlphaTex1LanguageHandler._mergeDisplay(c.timeSignature, v.ts); - c.barNumber = v.barNumber ?? c.barNumber; - } else { - if (staff) { - staff.tabConfig ??= {}; - } - const c = staff ? staff.tabConfig! : stylesheet!.tabConfig; - c.clef = AlphaTex1LanguageHandler._mergeDisplay(c.clef, v.clef); - c.timeSignature = AlphaTex1LanguageHandler._mergeDisplay(c.timeSignature, v.ts); - c.rests = AlphaTex1LanguageHandler._mergeDisplay(c.rests, v.rests); - c.barNumber = v.barNumber ?? c.barNumber; - c.rhythm = v.rhythm ?? c.rhythm; - } - break; - case AlphaTexStaffType.Slash: - if (bar) { - bar.slashDisplay ??= {}; - const c = bar.slashDisplay!; - c.keySignature = AlphaTex1LanguageHandler._mergeDisplay(c.keySignature, v.ks); - c.timeSignature = AlphaTex1LanguageHandler._mergeDisplay(c.timeSignature, v.ts); - c.barNumber = v.barNumber ?? c.barNumber; - } else { - if (staff) { - staff.slashConfig ??= {}; - } - const c = staff ? staff.slashConfig! : stylesheet!.slashConfig; - c.keySignature = AlphaTex1LanguageHandler._mergeDisplay(c.keySignature, v.ks); - c.timeSignature = AlphaTex1LanguageHandler._mergeDisplay(c.timeSignature, v.ts); - c.barNumber = v.barNumber ?? c.barNumber; - } - break; - case AlphaTexStaffType.Numbered: - if (bar) { - bar.numberedDisplay ??= {}; - const c = bar.numberedDisplay!; - c.timeSignature = AlphaTex1LanguageHandler._mergeDisplay(c.timeSignature, v.ts); - c.barNumber = v.barNumber ?? c.barNumber; - } else { - if (staff) { - staff.numberedConfig ??= {}; - } - const c = staff ? staff.numberedConfig! : stylesheet!.numberedConfig; - c.timeSignature = AlphaTex1LanguageHandler._mergeDisplay(c.timeSignature, v.ts); - c.barNumber = v.barNumber ?? c.barNumber; - } - break; - } + const lookup = [AlphaTex1LanguageDefinitions.metaDataProperties.get(metaData.tag.tag.text.toLowerCase())!]; + const isValid = (p: AlphaTexPropertyNode) => this._checkProperty(importer, lookup, p); + AlphaTex1StaffDisplay.apply(importer, metaData, isValid, stylesheet, staff, bar); } public applyStructuralMetaData( @@ -2990,15 +2760,7 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler nodes.push(Atnf.meta('showSingleStaffBrackets')); } - AlphaTex1LanguageHandler._buildStaffDisplayNodes( - nodes, - 'defaultStaffDisplay', - 'defaultBarNumberDisplay', - stylesheet, - undefined, - undefined, - false - ); + AlphaTex1StaffDisplay.buildNodes(nodes, stylesheet, undefined, undefined, false); // Unsupported: // 'globaldisplaychorddiagramsontop', @@ -3179,10 +2941,8 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler ]; } - AlphaTex1LanguageHandler._buildStaffDisplayNodes( + AlphaTex1StaffDisplay.buildNodes( nodes, - 'barDisplay', - 'barNumberDisplay', undefined, undefined, bar, @@ -3240,8 +3000,7 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler } } - AlphaTex1LanguageHandler._buildStaffDisplayNodes(nodes, 'staffDisplay', '', undefined, staff, undefined, false); - + AlphaTex1StaffDisplay.buildNodes(nodes, undefined, staff, undefined, false); if (firstStaffMetaIndex < nodes.length) { nodes[firstStaffMetaIndex].leadingComments = [ { @@ -3292,151 +3051,6 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler ); } - /** - * Reads the staff display values of the staff type from its carrier on stylesheet, staff or bar level. - */ - private static _readStaffDisplay( - t: AlphaTexStaffType, - stylesheet: RenderStylesheet | undefined, - staff: Staff | undefined, - bar: Bar | undefined - ): AlphaTexStaffDisplayValues { - switch (t) { - case AlphaTexStaffType.Score: { - const o = bar?.scoreDisplay; - const c = bar ? undefined : staff ? staff.scoreConfig : stylesheet!.scoreConfig; - return { - clef: o?.clef ?? c?.clef, - ks: o?.keySignature ?? c?.keySignature, - ts: o?.timeSignature ?? c?.timeSignature, - barNumber: o?.barNumber ?? c?.barNumber - }; - } - case AlphaTexStaffType.Tabs: { - const o = bar?.tabDisplay; - const c = bar ? undefined : staff ? staff.tabConfig : stylesheet!.tabConfig; - return { - clef: o?.clef ?? c?.clef, - ts: o?.timeSignature ?? c?.timeSignature, - rests: c?.rests, - barNumber: o?.barNumber ?? c?.barNumber, - rhythm: c?.rhythm - }; - } - case AlphaTexStaffType.Slash: { - const o = bar?.slashDisplay; - const c = bar ? undefined : staff ? staff.slashConfig : stylesheet!.slashConfig; - return { - ks: o?.keySignature ?? c?.keySignature, - ts: o?.timeSignature ?? c?.timeSignature, - barNumber: o?.barNumber ?? c?.barNumber - }; - } - default: { - const o = bar?.numberedDisplay; - const c = bar ? undefined : staff ? staff.numberedConfig : stylesheet!.numberedConfig; - return { ts: o?.timeSignature ?? c?.timeSignature, barNumber: o?.barNumber ?? c?.barNumber }; - } - } - } - - /** - * Lists the defined values as ` `. - */ - private static _staffDisplayProperties(v: AlphaTexStaffDisplayValues): string[] { - const props: string[] = []; - AlphaTex1LanguageHandler._elementDisplayProperties(props, 'clef', v.clef); - AlphaTex1LanguageHandler._elementDisplayProperties(props, 'ks', v.ks); - AlphaTex1LanguageHandler._elementDisplayProperties(props, 'ts', v.ts); - AlphaTex1LanguageHandler._elementDisplayProperties(props, 'rests', v.rests); - if (v.barNumber !== undefined) { - props.push(`barNumber ${AlphaTex1EnumMappings.barNumberDisplayReversed.get(v.barNumber!)}`); - } - if (v.rhythm !== undefined) { - props.push(`rhythm ${AlphaTex1EnumMappings.tabRhythmModeReversed.get(v.rhythm!)}`); - } - return props; - } - - private static _elementDisplayProperties(props: string[], element: string, d: ElementDisplay | undefined) { - if (!d) { - return; - } - if (d.isVisible !== undefined) { - props.push(`${element}Visibility ${d.isVisible! ? 'true' : 'false'}`); - } - if (d.staffPlacement !== undefined) { - props.push(`${element}Placement ${AlphaTex1EnumMappings.staffPlacementReversed.get(d.staffPlacement!)}`); - } - if (d.systemDisplay !== undefined) { - props.push(`${element}Systems ${AlphaTex1EnumMappings.systemDisplayReversed.get(d.systemDisplay!)}`); - } - } - - /** - * Builds the staff display tags of one level: equal values are grouped across staff types, the staff types - * are omitted if all supporting staff types share a value, a bar number shared by all staff types uses the shorthand tag. - * On score level only values differing from the defaults are written. - */ - private static _buildStaffDisplayNodes( - nodes: AlphaTexMetaDataNode[], - tag: string, - shorthandTag: string, - stylesheet: RenderStylesheet | undefined, - staff: Staff | undefined, - bar: Bar | undefined, - impliedAllBars: boolean - ) { - const defaults = stylesheet ? AlphaTex1LanguageHandler._defaultScore.stylesheet : undefined; - const propertyTypes = new Map(); - for (const t of AlphaTex1LanguageHandler._staffTypes) { - let skip: string[] = []; - if (defaults) { - skip = AlphaTex1LanguageHandler._staffDisplayProperties( - AlphaTex1LanguageHandler._readStaffDisplay(t, defaults, undefined, undefined) - ); - } - if (impliedAllBars) { - skip.push('barNumber allbars'); - } - const values = AlphaTex1LanguageHandler._readStaffDisplay(t, stylesheet, staff, bar); - for (const p of AlphaTex1LanguageHandler._staffDisplayProperties(values)) { - if (skip.indexOf(p) === -1) { - if (!propertyTypes.has(p)) { - propertyTypes.set(p, []); - } - propertyTypes.get(p)!.push(t); - } - } - } - - const tags = new Map(); - for (const [p, types] of propertyTypes) { - const name = p.substring(0, p.indexOf(' ')); - const value = p.substring(p.indexOf(' ') + 1); - const element = AlphaTex1LanguageHandler._staffDisplayElement(name); - const allTypes = types.length === AlphaTex1LanguageHandler._staffDisplayElements.get(element)!.length; - if (allTypes && element === 'barNumber' && shorthandTag.length > 0) { - const barNumber = AlphaTex1EnumMappings.barNumberDisplay.get(value)!; - nodes.push(Atnf.identMeta(shorthandTag, BarNumberDisplay[barNumber])); - continue; - } - - const key = allTypes ? '' : types.join(','); - if (!tags.has(key)) { - const args: IAlphaTexArgumentValue[] = []; - if (!allTypes) { - for (const t of types) { - args.push(Atnf.ident(AlphaTex1EnumMappings.alphaTexStaffTypeReversed.get(t)!)); - } - } - tags.set(key, Atnf.meta(tag, Atnf.args(args), Atnf.props([]))); - nodes.push(tags.get(key)!); - } - Atnf.prop(tags.get(key)!.properties!.properties, name, Atnf.identValue(value)); - } - } - private static _buildMasterBarMetaDataNodes(nodes: AlphaTexMetaDataNode[], masterBar: MasterBar) { const firstMetaIndex = nodes.length; diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1StaffDisplay.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1StaffDisplay.ts new file mode 100644 index 0000000000..15331f942e --- /dev/null +++ b/packages/alphatab/src/importer/alphaTex/AlphaTex1StaffDisplay.ts @@ -0,0 +1,288 @@ +import { BarSerializer } from '@coderline/alphatab/generated/model/BarSerializer'; +import { NumberedBarOverrideSerializer } from '@coderline/alphatab/generated/model/NumberedBarOverrideSerializer'; +import { NumberedStaffConfigSerializer } from '@coderline/alphatab/generated/model/NumberedStaffConfigSerializer'; +import { RenderStylesheetSerializer } from '@coderline/alphatab/generated/model/RenderStylesheetSerializer'; +import { ScoreBarOverrideSerializer } from '@coderline/alphatab/generated/model/ScoreBarOverrideSerializer'; +import { ScoreStaffConfigSerializer } from '@coderline/alphatab/generated/model/ScoreStaffConfigSerializer'; +import { SlashBarOverrideSerializer } from '@coderline/alphatab/generated/model/SlashBarOverrideSerializer'; +import { SlashStaffConfigSerializer } from '@coderline/alphatab/generated/model/SlashStaffConfigSerializer'; +import { StaffSerializer } from '@coderline/alphatab/generated/model/StaffSerializer'; +import { TabBarOverrideSerializer } from '@coderline/alphatab/generated/model/TabBarOverrideSerializer'; +import { TabStaffConfigSerializer } from '@coderline/alphatab/generated/model/TabStaffConfigSerializer'; +import { AlphaTex1EnumMappings } from '@coderline/alphatab/importer/alphaTex/AlphaTex1EnumMappings'; +import { + type AlphaTexMetaDataNode, + AlphaTexNodeType, + type AlphaTexNumberLiteral, + type AlphaTexPropertyNode, + type AlphaTexTextNode +} from '@coderline/alphatab/importer/alphaTex/AlphaTexAst'; +import { + AlphaTexDiagnosticCode, + AlphaTexDiagnosticsSeverity, + AlphaTexStaffType, + type IAlphaTexImporter +} from '@coderline/alphatab/importer/alphaTex/AlphaTexShared'; +import { Atnf } from '@coderline/alphatab/importer/alphaTex/ATNF'; +import { JsonHelper } from '@coderline/alphatab/io/JsonHelper'; +import type { Bar } from '@coderline/alphatab/model/Bar'; +import { StaffPlacement, SystemDisplay } from '@coderline/alphatab/model/ElementDisplay'; +import { BarNumberDisplay, RenderStylesheet } from '@coderline/alphatab/model/RenderStylesheet'; +import type { Staff } from '@coderline/alphatab/model/Staff'; +import { TabRhythmMode } from '@coderline/alphatab/NotationSettings'; + +/** + * A staff display property, its location in the carrier JSON (setting is empty for direct values) + * and the staff types supporting it. + * @record + * @internal + */ +interface AlphaTexStaffDisplayProperty { + name: string; + key: string; + setting: string; + types: AlphaTexStaffType[]; +} + +/** + * Imports and exports the staff display tags (`\defaultStaffDisplay`, `\staffDisplay`, `\barDisplay`). + * The per staff type carriers (e.g. `stylesheet.scoreConfig`, `staff.tabConfig`, `bar.slashDisplay`) share + * their structure but not their type, hence they are accessed via their JSON serializers. + * @internal + */ +export class AlphaTex1StaffDisplay { + private static readonly _defaults = new RenderStylesheet(); + private static readonly _carriers = new Map([ + [AlphaTexStaffType.Score, 'score'], + [AlphaTexStaffType.Tabs, 'tab'], + [AlphaTexStaffType.Slash, 'slash'], + [AlphaTexStaffType.Numbered, 'numbered'] + ]); + + /** + * The properties by their lower case name. + */ + private static readonly _properties = AlphaTex1StaffDisplay._createProperties(); + + private static _createProperties() { + const props = new Map(); + const add = (name: string, key: string, setting: string, types: AlphaTexStaffType[]) => { + props.set(name.toLowerCase(), { name: name, key: key, setting: setting, types: types }); + }; + const all = Array.from(AlphaTex1StaffDisplay._carriers.keys()); + const elements = new Map([ + ['clef', [AlphaTexStaffType.Score, AlphaTexStaffType.Tabs]], + ['keysignature', [AlphaTexStaffType.Score, AlphaTexStaffType.Slash]], + ['timesignature', all], + ['rests', [AlphaTexStaffType.Tabs]] + ]); + for (const [key, types] of elements) { + const prefix = key === 'keysignature' ? 'ks' : key === 'timesignature' ? 'ts' : key; + add(`${prefix}Visibility`, key, 'isvisible', types); + add(`${prefix}Placement`, key, 'staffplacement', types); + add(`${prefix}Systems`, key, 'systemdisplay', types); + } + add('barNumber', 'barnumber', '', all); + add('rhythm', 'rhythm', '', [AlphaTexStaffType.Tabs]); + return props; + } + + /** + * Applies the (validated) tag to the stylesheet, staff or bar. The arguments select the staff types, + * without arguments every property applies to all staff types supporting it. + */ + public static apply( + importer: IAlphaTexImporter, + metaData: AlphaTexMetaDataNode, + isValid: (p: AlphaTexPropertyNode) => boolean, + stylesheet: RenderStylesheet | undefined, + staff: Staff | undefined, + bar: Bar | undefined + ) { + const explicit: AlphaTexStaffType[] = []; + if (metaData.arguments) { + for (const a of metaData.arguments!.arguments) { + explicit.push(AlphaTex1EnumMappings.alphaTexStaffType.get((a as AlphaTexTextNode).text.toLowerCase())!); + } + } + + if (!metaData.properties) { + return; + } + const changes = new Map>(); + for (const p of metaData.properties!.properties) { + if (!isValid(p)) { + continue; + } + const prop = AlphaTex1StaffDisplay._properties.get(p.property.text.toLowerCase())!; + const supported = prop.types; + // enum values are matched by name (case insensitive) when deserializing + const value: unknown = + prop.setting === 'isvisible' + ? AlphaTex1StaffDisplay._isTrue(p) + : (p.arguments!.arguments[0] as AlphaTexTextNode).text; + for (const t of explicit.length > 0 ? explicit : supported) { + if (supported.indexOf(t) === -1) { + importer.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT307, + message: `The property '${prop.name}' has no effect on the staff type '${AlphaTex1EnumMappings.alphaTexStaffTypeReversed.get(t)}'`, + severity: AlphaTexDiagnosticsSeverity.Warning, + start: p.start, + end: p.end + }); + continue; + } + if (!changes.has(t)) { + changes.set( + t, + AlphaTex1StaffDisplay._read(t, stylesheet, staff, bar) ?? new Map() + ); + } + const json = changes.get(t)!; + if (prop.setting.length > 0) { + const element = + (json.get(prop.key) as Map | undefined) ?? new Map(); + element.set(prop.setting, value); + json.set(prop.key, element); + } else { + json.set(prop.key, value); + } + } + } + + for (const [t, json] of changes) { + const carrier = `${AlphaTex1StaffDisplay._carriers.get(t)}${bar ? 'display' : 'config'}`; + if (bar) { + BarSerializer.setProperty(bar, carrier, json); + } else if (staff) { + StaffSerializer.setProperty(staff, carrier, json); + } else { + RenderStylesheetSerializer.setProperty(stylesheet!, carrier, json); + } + } + } + + private static _isTrue(p: AlphaTexPropertyNode): boolean { + if (!p.arguments || p.arguments!.arguments.length === 0) { + return true; + } + const v = p.arguments!.arguments[0]; + return v.nodeType === AlphaTexNodeType.Number + ? (v as AlphaTexNumberLiteral).value !== 0 + : (v as AlphaTexTextNode).text !== 'false'; + } + + /** + * Builds the tags of one level (one per staff type). On score level only values differing from the defaults + * are written, a bar number shared by all staff types is written via the existing shorthand tag. + */ + public static buildNodes( + nodes: AlphaTexMetaDataNode[], + stylesheet: RenderStylesheet | undefined, + staff: Staff | undefined, + bar: Bar | undefined, + impliedAllBars: boolean + ) { + const perType = new Map(); + for (const t of AlphaTex1StaffDisplay._carriers.keys()) { + let skip: string[] = []; + if (stylesheet) { + skip = AlphaTex1StaffDisplay._texts( + AlphaTex1StaffDisplay._read(t, AlphaTex1StaffDisplay._defaults, undefined, undefined) + ); + } + if (impliedAllBars) { + skip.push('barNumber AllBars'); + } + const texts = AlphaTex1StaffDisplay._texts(AlphaTex1StaffDisplay._read(t, stylesheet, staff, bar)); + perType.set(t, texts.filter(p => skip.indexOf(p) === -1)); + } + + const barNumber = perType.get(AlphaTexStaffType.Score)!.find(p => p.startsWith('barNumber ')); + let shared = !staff && barNumber !== undefined; + for (const v of perType.values()) { + shared = shared && v.indexOf(barNumber!) !== -1; + } + if (shared) { + nodes.push(Atnf.identMeta(bar ? 'barNumberDisplay' : 'defaultBarNumberDisplay', barNumber!.substring(10))); + for (const v of perType.values()) { + v.splice(v.indexOf(barNumber!), 1); + } + } + + for (const [t, texts] of perType) { + if (texts.length > 0) { + const props = Atnf.props([]); + for (const p of texts) { + const i = p.indexOf(' '); + Atnf.prop(props.properties, p.substring(0, i), Atnf.identValue(p.substring(i + 1))); + } + const type = Atnf.identValue(AlphaTex1EnumMappings.alphaTexStaffTypeReversed.get(t)!); + nodes.push(Atnf.meta(bar ? 'barDisplay' : staff ? 'staffDisplay' : 'defaultStaffDisplay', type, props)); + } + } + } + + private static _read( + t: AlphaTexStaffType, + stylesheet: RenderStylesheet | undefined, + staff: Staff | undefined, + bar: Bar | undefined + ): Map | null { + switch (t) { + case AlphaTexStaffType.Score: + return bar + ? ScoreBarOverrideSerializer.toJson(bar.scoreDisplay) + : ScoreStaffConfigSerializer.toJson(staff ? staff.scoreConfig : stylesheet!.scoreConfig); + case AlphaTexStaffType.Tabs: + return bar + ? TabBarOverrideSerializer.toJson(bar.tabDisplay) + : TabStaffConfigSerializer.toJson(staff ? staff.tabConfig : stylesheet!.tabConfig); + case AlphaTexStaffType.Slash: + return bar + ? SlashBarOverrideSerializer.toJson(bar.slashDisplay) + : SlashStaffConfigSerializer.toJson(staff ? staff.slashConfig : stylesheet!.slashConfig); + default: + return bar + ? NumberedBarOverrideSerializer.toJson(bar.numberedDisplay) + : NumberedStaffConfigSerializer.toJson(staff ? staff.numberedConfig : stylesheet!.numberedConfig); + } + } + + /** + * Lists the defined values of the JSON as ` `. + */ + private static _texts(json: Map | null): string[] { + const texts: string[] = []; + if (!json) { + return texts; + } + for (const prop of AlphaTex1StaffDisplay._properties.values()) { + let v = json.get(prop.key); + if (prop.setting.length > 0) { + v = (v as Map | undefined)?.get(prop.setting); + } + if (v !== undefined && v !== null) { + texts.push( + `${prop.name} ${AlphaTex1StaffDisplay._text(prop.setting.length > 0 ? prop.setting : prop.key, v)}` + ); + } + } + return texts; + } + + private static _text(key: string, v: unknown): string { + switch (key) { + case 'isvisible': + return (v as boolean) ? 'true' : 'false'; + case 'staffplacement': + return StaffPlacement[JsonHelper.parseEnum(v, StaffPlacement)!]; + case 'systemdisplay': + return SystemDisplay[JsonHelper.parseEnum(v, SystemDisplay)!]; + case 'barnumber': + return BarNumberDisplay[JsonHelper.parseEnum(v, BarNumberDisplay)!]; + default: + return TabRhythmMode[JsonHelper.parseEnum(v, TabRhythmMode)!]; + } + } +} diff --git a/packages/alphatab/test/exporter/AlphaTexExporter.test.ts b/packages/alphatab/test/exporter/AlphaTexExporter.test.ts index 54528bf2f3..999491827c 100644 --- a/packages/alphatab/test/exporter/AlphaTexExporter.test.ts +++ b/packages/alphatab/test/exporter/AlphaTexExporter.test.ts @@ -233,14 +233,13 @@ describe('AlphaTexExporterTest', () => { .map(l => l.trim()) .join(' '); - // equal values are grouped, staff types are omitted if all supported types are affected - expect(exported).toContain('\\defaultStaffDisplay { clefSystems firstsystemonly rhythm showwithbars }'); - expect(exported).toContain('\\defaultStaffDisplay tabs { tsPlacement allstaves }'); - expect(exported).toContain('\\staffDisplay score { barNumber allbars }'); + expect(exported).toContain('\\defaultStaffDisplay score { clefSystems FirstSystemOnly }'); + expect(exported).toContain( + '\\defaultStaffDisplay tabs { clefSystems FirstSystemOnly tsPlacement AllStaves rhythm ShowWithBars }' + ); expect(exported).toContain('\\staffDisplay slash { ksVisibility true }'); - expect(exported).toContain('\\barDisplay (score tabs) { tsVisibility false }'); - expect(exported).toContain('\\barDisplay tabs { barNumber hide }'); - // equal bar number display on all staff types uses the shorthand + expect(exported).toContain('\\barDisplay tabs { tsVisibility false barNumber Hide }'); + // a bar number display shared by all staff types uses the shorthand expect(exported).toContain('\\barNumberDisplay Hide'); const reimported = ScoreLoader.loadAlphaTex(exportAlphaTex(score)); From f2be29e383cf5ed31c637a79ec7bff7548f144b5 Mon Sep 17 00:00:00 2001 From: danielku15 Date: Fri, 9 Oct 2026 19:42:06 +0200 Subject: [PATCH 8/8] refactor(alphatex): handle staff display tags in the language handler Replace AlphaTex1StaffDisplay with plain handler code following the existing tag handling: properties are applied like the chord/track properties with one line per model field of each carrier, the export reads the model fields directly like the other metadata builders. Refs #2912 --- .../alphaTex/AlphaTex1LanguageHandler.ts | 272 ++++++++++++++++- .../alphaTex/AlphaTex1StaffDisplay.ts | 288 ------------------ 2 files changed, 262 insertions(+), 298 deletions(-) delete mode 100644 packages/alphatab/src/importer/alphaTex/AlphaTex1StaffDisplay.ts diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts index bbd1c7ec62..fbc11f7e54 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts @@ -5,7 +5,6 @@ // but we emit "Hint" diagnostics for now. import { AlphaTex1EnumMappings } from '@coderline/alphatab/importer/alphaTex/AlphaTex1EnumMappings'; -import { AlphaTex1StaffDisplay } from '@coderline/alphatab/importer/alphaTex/AlphaTex1StaffDisplay'; import { AlphaTex1LanguageDefinitions, type AlphaTexSignatureDefinition @@ -21,6 +20,7 @@ import { type AlphaTexMetaDataNode, AlphaTexNodeType, type AlphaTexNumberLiteral, + type AlphaTexPropertiesNode, type AlphaTexPropertyNode, type AlphaTexStringLiteral, type AlphaTexTextNode, @@ -32,6 +32,7 @@ import { AlphaTexDiagnosticCode, AlphaTexDiagnosticsSeverity, AlphaTexStaffNoteKind, + AlphaTexStaffType, ArgumentListParseTypesMode, type IAlphaTexImporter } from '@coderline/alphatab/importer/alphaTex/AlphaTexShared'; @@ -45,6 +46,12 @@ import { GeneralMidi } from '@coderline/alphatab/midi/GeneralMidi'; import { AccentuationType } from '@coderline/alphatab/model/AccentuationType'; import { Automation, AutomationType, type FlatSyncPoint } from '@coderline/alphatab/model/Automation'; import { type Bar, BarLineStyle, SustainPedalMarker, SustainPedalMarkerType } from '@coderline/alphatab/model/Bar'; +import type { + NumberedBarOverride, + ScoreBarOverride, + SlashBarOverride, + TabBarOverride +} from '@coderline/alphatab/model/BarOverrides'; import { BarreShape } from '@coderline/alphatab/model/BarreShape'; import { type Beat, BeatBeamingMode } from '@coderline/alphatab/model/Beat'; import { BendPoint } from '@coderline/alphatab/model/BendPoint'; @@ -55,6 +62,7 @@ import { Clef } from '@coderline/alphatab/model/Clef'; import { Color } from '@coderline/alphatab/model/Color'; import { CrescendoType } from '@coderline/alphatab/model/CrescendoType'; import { Duration } from '@coderline/alphatab/model/Duration'; +import { type ElementDisplay, StaffPlacement, SystemDisplay } from '@coderline/alphatab/model/ElementDisplay'; import { FadeType } from '@coderline/alphatab/model/FadeType'; import { Fermata } from '@coderline/alphatab/model/Fermata'; import { Fingers } from '@coderline/alphatab/model/Fingers'; @@ -78,12 +86,19 @@ import { SimileMark } from '@coderline/alphatab/model/SimileMark'; import { SlideInType } from '@coderline/alphatab/model/SlideInType'; import { SlideOutType } from '@coderline/alphatab/model/SlideOutType'; import { Staff } from '@coderline/alphatab/model/Staff'; +import type { + NumberedStaffConfig, + ScoreStaffConfig, + SlashStaffConfig, + TabStaffConfig +} from '@coderline/alphatab/model/StaffConfigs'; import { Track } from '@coderline/alphatab/model/Track'; import { TremoloPickingEffect, TremoloPickingStyle } from '@coderline/alphatab/model/TremoloPickingEffect'; import { TripletFeel } from '@coderline/alphatab/model/TripletFeel'; import { Tuning } from '@coderline/alphatab/model/Tuning'; import { VibratoType } from '@coderline/alphatab/model/VibratoType'; import { WahPedal } from '@coderline/alphatab/model/WahPedal'; +import { TabRhythmMode } from '@coderline/alphatab/NotationSettings'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; import { SynthConstants } from '@coderline/alphatab/synth/SynthConstants'; @@ -302,7 +317,7 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler score.stylesheet.barNumberDisplay = barNumberDisplay!; return ApplyNodeResult.Applied; case 'defaultstaffdisplay': - this._applyStaffDisplay(importer, metaData, score.stylesheet, undefined, undefined); + this._staffDisplayProperties(importer, metaData, score.stylesheet, undefined, undefined); return ApplyNodeResult.Applied; default: @@ -437,7 +452,7 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler return ApplyNodeResult.Applied; case 'staffdisplay': - this._applyStaffDisplay(importer, metaData, undefined, staff, undefined); + this._staffDisplayProperties(importer, metaData, undefined, staff, undefined); return ApplyNodeResult.Applied; case 'bank': staff.track.playbackInfo.bank = (metaData.arguments!.arguments[0] as AlphaTexNumberLiteral).value; @@ -930,7 +945,7 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler bar.barNumberDisplay = barNumberDisplay!; return ApplyNodeResult.Applied; case 'bardisplay': - this._applyStaffDisplay(importer, metaData, undefined, undefined, bar); + this._staffDisplayProperties(importer, metaData, undefined, undefined, bar); return ApplyNodeResult.Applied; case 'barnumber': switch (metaData.arguments!.arguments[0].nodeType) { @@ -1300,16 +1315,152 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler } } - private _applyStaffDisplay( + private _staffDisplayProperties( importer: IAlphaTexImporter, metaData: AlphaTexMetaDataNode, stylesheet: RenderStylesheet | undefined, staff: Staff | undefined, bar: Bar | undefined ) { + if (!metaData.properties) { + return; + } + + // without arguments the properties apply to all staff types supporting them + const staffTypes: AlphaTexStaffType[] = []; + if (metaData.arguments) { + for (const a of metaData.arguments!.arguments) { + staffTypes.push( + AlphaTex1EnumMappings.alphaTexStaffType.get((a as AlphaTexTextNode).text.toLowerCase())! + ); + } + } + const isExplicit = staffTypes.length > 0; + if (!isExplicit) { + for (const t of AlphaTex1EnumMappings.alphaTexStaffType.values()) { + staffTypes.push(t); + } + } + const lookup = [AlphaTex1LanguageDefinitions.metaDataProperties.get(metaData.tag.tag.text.toLowerCase())!]; - const isValid = (p: AlphaTexPropertyNode) => this._checkProperty(importer, lookup, p); - AlphaTex1StaffDisplay.apply(importer, metaData, isValid, stylesheet, staff, bar); + for (const p of metaData.properties.properties) { + if (!this._checkProperty(importer, lookup, p)) { + continue; + } + + for (const t of staffTypes) { + if (!AlphaTex1LanguageHandler._applyStaffDisplayProperty(p, t, stylesheet, staff, bar) && isExplicit) { + importer.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT307, + message: `The property '${p.property.text}' has no effect on the staff type '${AlphaTex1EnumMappings.alphaTexStaffTypeReversed.get(t)}'`, + severity: AlphaTexDiagnosticsSeverity.Warning, + start: p.start, + end: p.end + }); + } + } + } + } + + /** + * Applies the property to the display settings of the staff type on the stylesheet, staff or bar + * (created on demand). Returns false if the staff type has no such setting. + */ + private static _applyStaffDisplayProperty( + p: AlphaTexPropertyNode, + t: AlphaTexStaffType, + stylesheet: RenderStylesheet | undefined, + staff: Staff | undefined, + bar: Bar | undefined + ): boolean { + const name = p.property.text.toLowerCase(); + const value = () => (p.arguments!.arguments[0] as AlphaTexTextNode).text.toLowerCase(); + // all staff types have a bar number display + let applied = name === 'barnumber'; + const barNumber = (v: BarNumberDisplay | undefined): BarNumberDisplay | undefined => + name === 'barnumber' ? AlphaTex1EnumMappings.barNumberDisplay.get(value())! : v; + // Visibility, Placement and Systems + const element = (prefix: string, display: ElementDisplay | undefined): ElementDisplay | undefined => { + if (!name.startsWith(prefix)) { + return display; + } + applied = true; + const d: ElementDisplay = display ?? {}; + if (name.endsWith('visibility')) { + d.isVisible = p.arguments + ? AlphaTex1LanguageHandler._booleanLikeValue(p.arguments!.arguments, 0) + : true; + } else if (name.endsWith('placement')) { + d.staffPlacement = AlphaTex1EnumMappings.staffPlacement.get(value())!; + } else { + d.systemDisplay = AlphaTex1EnumMappings.systemDisplay.get(value())!; + } + return d; + }; + + if (bar && t === AlphaTexStaffType.Score) { + const c: ScoreBarOverride = bar.scoreDisplay ?? {}; + c.clef = element('clef', c.clef); + c.keySignature = element('ks', c.keySignature); + c.timeSignature = element('ts', c.timeSignature); + c.barNumber = barNumber(c.barNumber); + bar.scoreDisplay = applied ? c : bar.scoreDisplay; + } else if (bar && t === AlphaTexStaffType.Tabs) { + const c: TabBarOverride = bar.tabDisplay ?? {}; + c.clef = element('clef', c.clef); + c.timeSignature = element('ts', c.timeSignature); + c.barNumber = barNumber(c.barNumber); + bar.tabDisplay = applied ? c : bar.tabDisplay; + } else if (bar && t === AlphaTexStaffType.Slash) { + const c: SlashBarOverride = bar.slashDisplay ?? {}; + c.keySignature = element('ks', c.keySignature); + c.timeSignature = element('ts', c.timeSignature); + c.barNumber = barNumber(c.barNumber); + bar.slashDisplay = applied ? c : bar.slashDisplay; + } else if (bar) { + const c: NumberedBarOverride = bar.numberedDisplay ?? {}; + c.timeSignature = element('ts', c.timeSignature); + c.barNumber = barNumber(c.barNumber); + bar.numberedDisplay = applied ? c : bar.numberedDisplay; + } else if (t === AlphaTexStaffType.Score) { + const c: ScoreStaffConfig = staff?.scoreConfig ?? stylesheet?.scoreConfig ?? {}; + c.clef = element('clef', c.clef); + c.keySignature = element('ks', c.keySignature); + c.timeSignature = element('ts', c.timeSignature); + c.barNumber = barNumber(c.barNumber); + if (staff && applied) { + staff.scoreConfig = c; + } + } else if (t === AlphaTexStaffType.Tabs) { + const c: TabStaffConfig = staff?.tabConfig ?? stylesheet?.tabConfig ?? {}; + c.clef = element('clef', c.clef); + c.timeSignature = element('ts', c.timeSignature); + c.rests = element('rests', c.rests); + c.barNumber = barNumber(c.barNumber); + if (name === 'rhythm') { + c.rhythm = AlphaTex1EnumMappings.tabRhythmMode.get(value())!; + applied = true; + } + if (staff && applied) { + staff.tabConfig = c; + } + } else if (t === AlphaTexStaffType.Slash) { + const c: SlashStaffConfig = staff?.slashConfig ?? stylesheet?.slashConfig ?? {}; + c.keySignature = element('ks', c.keySignature); + c.timeSignature = element('ts', c.timeSignature); + c.barNumber = barNumber(c.barNumber); + if (staff && applied) { + staff.slashConfig = c; + } + } else { + const c: NumberedStaffConfig = staff?.numberedConfig ?? stylesheet?.numberedConfig ?? {}; + c.timeSignature = element('ts', c.timeSignature); + c.barNumber = barNumber(c.barNumber); + if (staff && applied) { + staff.numberedConfig = c; + } + } + return applied; } public applyStructuralMetaData( @@ -2760,7 +2911,7 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler nodes.push(Atnf.meta('showSingleStaffBrackets')); } - AlphaTex1StaffDisplay.buildNodes(nodes, stylesheet, undefined, undefined, false); + AlphaTex1LanguageHandler._buildStaffDisplayNodes(nodes, stylesheet, undefined, undefined, false); // Unsupported: // 'globaldisplaychorddiagramsontop', @@ -2941,7 +3092,7 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler ]; } - AlphaTex1StaffDisplay.buildNodes( + AlphaTex1LanguageHandler._buildStaffDisplayNodes( nodes, undefined, undefined, @@ -2951,6 +3102,107 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler return nodes; } + /** + * Builds the staff display tags (one per staff type) of the stylesheet, a staff or a bar. Values equal to the + * defaults (stylesheet) or implied by a custom bar number (bar) are skipped, a bar number display shared by all + * staff types is written via the shorthand tag. + */ + private static _buildStaffDisplayNodes( + nodes: AlphaTexMetaDataNode[], + stylesheet: RenderStylesheet | undefined, + staff: Staff | undefined, + bar: Bar | undefined, + isImpliedBarNumber: boolean + ) { + const element = ( + props: AlphaTexPropertiesNode, + prefix: string, + v: ElementDisplay | undefined, + d: ElementDisplay | undefined + ) => { + if (v?.isVisible !== undefined && v!.isVisible !== d?.isVisible) { + Atnf.prop(props.properties, `${prefix}Visibility`, Atnf.identValue(v!.isVisible! ? 'true' : 'false')); + } + if (v?.staffPlacement !== undefined && v!.staffPlacement !== d?.staffPlacement) { + Atnf.prop(props.properties, `${prefix}Placement`, Atnf.identValue(StaffPlacement[v!.staffPlacement!])); + } + if (v?.systemDisplay !== undefined && v!.systemDisplay !== d?.systemDisplay) { + Atnf.prop(props.properties, `${prefix}Systems`, Atnf.identValue(SystemDisplay[v!.systemDisplay!])); + } + }; + + const score = Atnf.props([]); + const tabs = Atnf.props([]); + const slash = Atnf.props([]); + const numbered = Atnf.props([]); + const barNumbers: (BarNumberDisplay | undefined)[] = []; + if (bar) { + element(score, 'clef', bar.scoreDisplay?.clef, undefined); + element(score, 'ks', bar.scoreDisplay?.keySignature, undefined); + element(score, 'ts', bar.scoreDisplay?.timeSignature, undefined); + element(tabs, 'clef', bar.tabDisplay?.clef, undefined); + element(tabs, 'ts', bar.tabDisplay?.timeSignature, undefined); + element(slash, 'ks', bar.slashDisplay?.keySignature, undefined); + element(slash, 'ts', bar.slashDisplay?.timeSignature, undefined); + element(numbered, 'ts', bar.numberedDisplay?.timeSignature, undefined); + barNumbers.push(bar.scoreDisplay?.barNumber); + barNumbers.push(bar.tabDisplay?.barNumber); + barNumbers.push(bar.slashDisplay?.barNumber); + barNumbers.push(bar.numberedDisplay?.barNumber); + } else { + // staves have no defaults, every defined value overrides the stylesheet + const defaults = staff ? undefined : AlphaTex1LanguageHandler._defaultScore.stylesheet; + const sc = staff ? staff.scoreConfig : stylesheet!.scoreConfig; + const tc = staff ? staff.tabConfig : stylesheet!.tabConfig; + const slc = staff ? staff.slashConfig : stylesheet!.slashConfig; + const nc = staff ? staff.numberedConfig : stylesheet!.numberedConfig; + element(score, 'clef', sc?.clef, defaults?.scoreConfig?.clef); + element(score, 'ks', sc?.keySignature, defaults?.scoreConfig?.keySignature); + element(score, 'ts', sc?.timeSignature, defaults?.scoreConfig?.timeSignature); + element(tabs, 'clef', tc?.clef, defaults?.tabConfig?.clef); + element(tabs, 'ts', tc?.timeSignature, defaults?.tabConfig?.timeSignature); + element(tabs, 'rests', tc?.rests, defaults?.tabConfig?.rests); + if (tc?.rhythm !== undefined && tc!.rhythm !== defaults?.tabConfig?.rhythm) { + Atnf.prop(tabs.properties, 'rhythm', Atnf.identValue(TabRhythmMode[tc!.rhythm!])); + } + element(slash, 'ks', slc?.keySignature, defaults?.slashConfig?.keySignature); + element(slash, 'ts', slc?.timeSignature, defaults?.slashConfig?.timeSignature); + element(numbered, 'ts', nc?.timeSignature, defaults?.numberedConfig?.timeSignature); + barNumbers.push(sc?.barNumber); + barNumbers.push(tc?.barNumber); + barNumbers.push(slc?.barNumber); + barNumbers.push(nc?.barNumber); + } + + // bar numbers equal to the default (stylesheet) or implied by a custom bar number (bar) are not written, + // one shared by all staff types is written via the shorthand tag (there is none for staves) + let defaultBarNumber: BarNumberDisplay | undefined = undefined; + if (stylesheet) { + defaultBarNumber = BarNumberDisplay.FirstOfSystem; + } else if (isImpliedBarNumber) { + defaultBarNumber = BarNumberDisplay.AllBars; + } + const isShared = !staff && barNumbers.filter(b => b === barNumbers[0]).length === barNumbers.length; + if (isShared && barNumbers[0] !== undefined && barNumbers[0] !== defaultBarNumber) { + nodes.push( + Atnf.identMeta(bar ? 'barNumberDisplay' : 'defaultBarNumberDisplay', BarNumberDisplay[barNumbers[0]!]) + ); + } + + const tag = bar ? 'barDisplay' : staff ? 'staffDisplay' : 'defaultStaffDisplay'; + const staffTypes = ['score', 'tabs', 'slash', 'numbered']; + const all = [score, tabs, slash, numbered]; + for (let i = 0; i < all.length; i++) { + const barNumber = barNumbers[i]; + if (!isShared && barNumber !== undefined && barNumber !== defaultBarNumber) { + Atnf.prop(all[i].properties, 'barNumber', Atnf.identValue(BarNumberDisplay[barNumber!])); + } + if (all[i].properties.length > 0) { + nodes.push(Atnf.meta(tag, Atnf.identValue(staffTypes[i]), all[i])); + } + } + } + private static _buildStaffMetaDataNodes(nodes: AlphaTexMetaDataNode[], staff: Staff) { const firstStaffMetaIndex = nodes.length; @@ -3000,7 +3252,7 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler } } - AlphaTex1StaffDisplay.buildNodes(nodes, undefined, staff, undefined, false); + AlphaTex1LanguageHandler._buildStaffDisplayNodes(nodes, undefined, staff, undefined, false); if (firstStaffMetaIndex < nodes.length) { nodes[firstStaffMetaIndex].leadingComments = [ { diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1StaffDisplay.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1StaffDisplay.ts deleted file mode 100644 index 15331f942e..0000000000 --- a/packages/alphatab/src/importer/alphaTex/AlphaTex1StaffDisplay.ts +++ /dev/null @@ -1,288 +0,0 @@ -import { BarSerializer } from '@coderline/alphatab/generated/model/BarSerializer'; -import { NumberedBarOverrideSerializer } from '@coderline/alphatab/generated/model/NumberedBarOverrideSerializer'; -import { NumberedStaffConfigSerializer } from '@coderline/alphatab/generated/model/NumberedStaffConfigSerializer'; -import { RenderStylesheetSerializer } from '@coderline/alphatab/generated/model/RenderStylesheetSerializer'; -import { ScoreBarOverrideSerializer } from '@coderline/alphatab/generated/model/ScoreBarOverrideSerializer'; -import { ScoreStaffConfigSerializer } from '@coderline/alphatab/generated/model/ScoreStaffConfigSerializer'; -import { SlashBarOverrideSerializer } from '@coderline/alphatab/generated/model/SlashBarOverrideSerializer'; -import { SlashStaffConfigSerializer } from '@coderline/alphatab/generated/model/SlashStaffConfigSerializer'; -import { StaffSerializer } from '@coderline/alphatab/generated/model/StaffSerializer'; -import { TabBarOverrideSerializer } from '@coderline/alphatab/generated/model/TabBarOverrideSerializer'; -import { TabStaffConfigSerializer } from '@coderline/alphatab/generated/model/TabStaffConfigSerializer'; -import { AlphaTex1EnumMappings } from '@coderline/alphatab/importer/alphaTex/AlphaTex1EnumMappings'; -import { - type AlphaTexMetaDataNode, - AlphaTexNodeType, - type AlphaTexNumberLiteral, - type AlphaTexPropertyNode, - type AlphaTexTextNode -} from '@coderline/alphatab/importer/alphaTex/AlphaTexAst'; -import { - AlphaTexDiagnosticCode, - AlphaTexDiagnosticsSeverity, - AlphaTexStaffType, - type IAlphaTexImporter -} from '@coderline/alphatab/importer/alphaTex/AlphaTexShared'; -import { Atnf } from '@coderline/alphatab/importer/alphaTex/ATNF'; -import { JsonHelper } from '@coderline/alphatab/io/JsonHelper'; -import type { Bar } from '@coderline/alphatab/model/Bar'; -import { StaffPlacement, SystemDisplay } from '@coderline/alphatab/model/ElementDisplay'; -import { BarNumberDisplay, RenderStylesheet } from '@coderline/alphatab/model/RenderStylesheet'; -import type { Staff } from '@coderline/alphatab/model/Staff'; -import { TabRhythmMode } from '@coderline/alphatab/NotationSettings'; - -/** - * A staff display property, its location in the carrier JSON (setting is empty for direct values) - * and the staff types supporting it. - * @record - * @internal - */ -interface AlphaTexStaffDisplayProperty { - name: string; - key: string; - setting: string; - types: AlphaTexStaffType[]; -} - -/** - * Imports and exports the staff display tags (`\defaultStaffDisplay`, `\staffDisplay`, `\barDisplay`). - * The per staff type carriers (e.g. `stylesheet.scoreConfig`, `staff.tabConfig`, `bar.slashDisplay`) share - * their structure but not their type, hence they are accessed via their JSON serializers. - * @internal - */ -export class AlphaTex1StaffDisplay { - private static readonly _defaults = new RenderStylesheet(); - private static readonly _carriers = new Map([ - [AlphaTexStaffType.Score, 'score'], - [AlphaTexStaffType.Tabs, 'tab'], - [AlphaTexStaffType.Slash, 'slash'], - [AlphaTexStaffType.Numbered, 'numbered'] - ]); - - /** - * The properties by their lower case name. - */ - private static readonly _properties = AlphaTex1StaffDisplay._createProperties(); - - private static _createProperties() { - const props = new Map(); - const add = (name: string, key: string, setting: string, types: AlphaTexStaffType[]) => { - props.set(name.toLowerCase(), { name: name, key: key, setting: setting, types: types }); - }; - const all = Array.from(AlphaTex1StaffDisplay._carriers.keys()); - const elements = new Map([ - ['clef', [AlphaTexStaffType.Score, AlphaTexStaffType.Tabs]], - ['keysignature', [AlphaTexStaffType.Score, AlphaTexStaffType.Slash]], - ['timesignature', all], - ['rests', [AlphaTexStaffType.Tabs]] - ]); - for (const [key, types] of elements) { - const prefix = key === 'keysignature' ? 'ks' : key === 'timesignature' ? 'ts' : key; - add(`${prefix}Visibility`, key, 'isvisible', types); - add(`${prefix}Placement`, key, 'staffplacement', types); - add(`${prefix}Systems`, key, 'systemdisplay', types); - } - add('barNumber', 'barnumber', '', all); - add('rhythm', 'rhythm', '', [AlphaTexStaffType.Tabs]); - return props; - } - - /** - * Applies the (validated) tag to the stylesheet, staff or bar. The arguments select the staff types, - * without arguments every property applies to all staff types supporting it. - */ - public static apply( - importer: IAlphaTexImporter, - metaData: AlphaTexMetaDataNode, - isValid: (p: AlphaTexPropertyNode) => boolean, - stylesheet: RenderStylesheet | undefined, - staff: Staff | undefined, - bar: Bar | undefined - ) { - const explicit: AlphaTexStaffType[] = []; - if (metaData.arguments) { - for (const a of metaData.arguments!.arguments) { - explicit.push(AlphaTex1EnumMappings.alphaTexStaffType.get((a as AlphaTexTextNode).text.toLowerCase())!); - } - } - - if (!metaData.properties) { - return; - } - const changes = new Map>(); - for (const p of metaData.properties!.properties) { - if (!isValid(p)) { - continue; - } - const prop = AlphaTex1StaffDisplay._properties.get(p.property.text.toLowerCase())!; - const supported = prop.types; - // enum values are matched by name (case insensitive) when deserializing - const value: unknown = - prop.setting === 'isvisible' - ? AlphaTex1StaffDisplay._isTrue(p) - : (p.arguments!.arguments[0] as AlphaTexTextNode).text; - for (const t of explicit.length > 0 ? explicit : supported) { - if (supported.indexOf(t) === -1) { - importer.addSemanticDiagnostic({ - code: AlphaTexDiagnosticCode.AT307, - message: `The property '${prop.name}' has no effect on the staff type '${AlphaTex1EnumMappings.alphaTexStaffTypeReversed.get(t)}'`, - severity: AlphaTexDiagnosticsSeverity.Warning, - start: p.start, - end: p.end - }); - continue; - } - if (!changes.has(t)) { - changes.set( - t, - AlphaTex1StaffDisplay._read(t, stylesheet, staff, bar) ?? new Map() - ); - } - const json = changes.get(t)!; - if (prop.setting.length > 0) { - const element = - (json.get(prop.key) as Map | undefined) ?? new Map(); - element.set(prop.setting, value); - json.set(prop.key, element); - } else { - json.set(prop.key, value); - } - } - } - - for (const [t, json] of changes) { - const carrier = `${AlphaTex1StaffDisplay._carriers.get(t)}${bar ? 'display' : 'config'}`; - if (bar) { - BarSerializer.setProperty(bar, carrier, json); - } else if (staff) { - StaffSerializer.setProperty(staff, carrier, json); - } else { - RenderStylesheetSerializer.setProperty(stylesheet!, carrier, json); - } - } - } - - private static _isTrue(p: AlphaTexPropertyNode): boolean { - if (!p.arguments || p.arguments!.arguments.length === 0) { - return true; - } - const v = p.arguments!.arguments[0]; - return v.nodeType === AlphaTexNodeType.Number - ? (v as AlphaTexNumberLiteral).value !== 0 - : (v as AlphaTexTextNode).text !== 'false'; - } - - /** - * Builds the tags of one level (one per staff type). On score level only values differing from the defaults - * are written, a bar number shared by all staff types is written via the existing shorthand tag. - */ - public static buildNodes( - nodes: AlphaTexMetaDataNode[], - stylesheet: RenderStylesheet | undefined, - staff: Staff | undefined, - bar: Bar | undefined, - impliedAllBars: boolean - ) { - const perType = new Map(); - for (const t of AlphaTex1StaffDisplay._carriers.keys()) { - let skip: string[] = []; - if (stylesheet) { - skip = AlphaTex1StaffDisplay._texts( - AlphaTex1StaffDisplay._read(t, AlphaTex1StaffDisplay._defaults, undefined, undefined) - ); - } - if (impliedAllBars) { - skip.push('barNumber AllBars'); - } - const texts = AlphaTex1StaffDisplay._texts(AlphaTex1StaffDisplay._read(t, stylesheet, staff, bar)); - perType.set(t, texts.filter(p => skip.indexOf(p) === -1)); - } - - const barNumber = perType.get(AlphaTexStaffType.Score)!.find(p => p.startsWith('barNumber ')); - let shared = !staff && barNumber !== undefined; - for (const v of perType.values()) { - shared = shared && v.indexOf(barNumber!) !== -1; - } - if (shared) { - nodes.push(Atnf.identMeta(bar ? 'barNumberDisplay' : 'defaultBarNumberDisplay', barNumber!.substring(10))); - for (const v of perType.values()) { - v.splice(v.indexOf(barNumber!), 1); - } - } - - for (const [t, texts] of perType) { - if (texts.length > 0) { - const props = Atnf.props([]); - for (const p of texts) { - const i = p.indexOf(' '); - Atnf.prop(props.properties, p.substring(0, i), Atnf.identValue(p.substring(i + 1))); - } - const type = Atnf.identValue(AlphaTex1EnumMappings.alphaTexStaffTypeReversed.get(t)!); - nodes.push(Atnf.meta(bar ? 'barDisplay' : staff ? 'staffDisplay' : 'defaultStaffDisplay', type, props)); - } - } - } - - private static _read( - t: AlphaTexStaffType, - stylesheet: RenderStylesheet | undefined, - staff: Staff | undefined, - bar: Bar | undefined - ): Map | null { - switch (t) { - case AlphaTexStaffType.Score: - return bar - ? ScoreBarOverrideSerializer.toJson(bar.scoreDisplay) - : ScoreStaffConfigSerializer.toJson(staff ? staff.scoreConfig : stylesheet!.scoreConfig); - case AlphaTexStaffType.Tabs: - return bar - ? TabBarOverrideSerializer.toJson(bar.tabDisplay) - : TabStaffConfigSerializer.toJson(staff ? staff.tabConfig : stylesheet!.tabConfig); - case AlphaTexStaffType.Slash: - return bar - ? SlashBarOverrideSerializer.toJson(bar.slashDisplay) - : SlashStaffConfigSerializer.toJson(staff ? staff.slashConfig : stylesheet!.slashConfig); - default: - return bar - ? NumberedBarOverrideSerializer.toJson(bar.numberedDisplay) - : NumberedStaffConfigSerializer.toJson(staff ? staff.numberedConfig : stylesheet!.numberedConfig); - } - } - - /** - * Lists the defined values of the JSON as ` `. - */ - private static _texts(json: Map | null): string[] { - const texts: string[] = []; - if (!json) { - return texts; - } - for (const prop of AlphaTex1StaffDisplay._properties.values()) { - let v = json.get(prop.key); - if (prop.setting.length > 0) { - v = (v as Map | undefined)?.get(prop.setting); - } - if (v !== undefined && v !== null) { - texts.push( - `${prop.name} ${AlphaTex1StaffDisplay._text(prop.setting.length > 0 ? prop.setting : prop.key, v)}` - ); - } - } - return texts; - } - - private static _text(key: string, v: unknown): string { - switch (key) { - case 'isvisible': - return (v as boolean) ? 'true' : 'false'; - case 'staffplacement': - return StaffPlacement[JsonHelper.parseEnum(v, StaffPlacement)!]; - case 'systemdisplay': - return SystemDisplay[JsonHelper.parseEnum(v, SystemDisplay)!]; - case 'barnumber': - return BarNumberDisplay[JsonHelper.parseEnum(v, BarNumberDisplay)!]; - default: - return TabRhythmMode[JsonHelper.parseEnum(v, TabRhythmMode)!]; - } - } -}