diff --git a/Extensions.md b/Extensions.md new file mode 100644 index 0000000..055612f --- /dev/null +++ b/Extensions.md @@ -0,0 +1,185 @@ +# Extension API + +With extensions to Sibmei, text objects and symbols can be exported in a customized way. This allows addressing custom symbols and text styles and project specific needs. + +Extensions are regular Sibelius plugins written in ManuScript. When running Sibmei, it scans for extension plugins. Users can choose which extensions to activate when running Sibmei. Multiple extensions can be activated simultaneously. + +![choosing Sibmei extensions](assets/extension-choice.png) + +## Example + +```js +{ + // The `SibmeiExtensionAPIVersion` field must be present so Sibmei can + // recognize compatible extensions + SibmeiExtensionAPIVersion "1.0.0" + + Initialize "() { + // The extension choice dialog will list this extension as + // 'Example extension' (the first argument to to AddToPluginsMenu()). + // Second argument can be `null` because an extension plugin does not need a + // `Run()` method. + AddToPluginsMenu('Example extension', null); + }" + + // InitSibmeiExtension() is the entry point for Sibmei and must be present + // for Sibmei to recognize an extension plugin. + InitSibmeiExtension "(api) { + // It is recommended to register the api and libmei objects as global + // variables: + Self._property:api = api; + Self._property:libmei = api.libmei; + + // Declare which text styles this extension handles + api.RegisterTextHandlers(CreateDictionary( + // Text objects can be matched either by their StyleId or StyleAsText + // property. Here, we match by StyleAsText. + 'StyleAsText', CreateDictionary( + // We want the HandleMyText() method to handle Text objects matching + // textObj.StyleAsText = 'My text' + 'My text', 'HandleMyText' + ) + ), Self); + }" + + HandleMyText "(_, textObj) { + // Create and return an MEI element that Sibmei will append as a child to + // the measure element. + textElement = api.GenerateControlEvent(textObj, 'AnchoredText'); + api.AddFormattedText(textElement, textObj); + return textElement; + }" +} +``` + +See [another example](./lib/sibmei4_extension_test.plg) for code handling symbols. + +## Required Data + +### `ExtensionAPIVersion` + +A [semantic version string](https://en.wikipedia.org/wiki/Software_versioning#Degree_of_compatibility) specifying for which version of the Sibmei extension +API the extension was written. The current API version of Sibmei can be found in +[`GLOBALS.mss`](./src/GLOBALS.mss). + +The API is guaranteed to remain backwards compatible with newer releases that retain the same major version number for `ExtensionAPIVersion`. With minor version numbers, new functionality is added while existing functionality remains backwards compatible. + +## Required Methods + +### Symbol or Text Handlers + +The core purpose of an extension is to define symbol and text handlers to export Sibelius objects in custom ways. (See `HandleMyText()` in the above [example](#example)) These handlers take two arguments: + +* `this`: a Dictionary that is passed for technical reasons and *must be ignored by the extension* +* a Sibelius object (`SymbolItem` or `SystemSymbolitem` for symbol handlers, `Text` or `SystemTextItem` for text handlers) + +A text handler should return an MEI element (created using libmei) that +Sibmei will append to the `` element. If `null` is returned instead, +the object will not be exported. + +A symbol handler should either call the `HandleModifier()` or `HandleControlEvent()` methods. If neither is called, the object will not be exported. Symbol handlers needn't return anything. + +### `InitSibmeiExtension()` + +Sibmei calls this method and passes an API Dictionary as argument (see below). +Register your symbol and text handlers in this function using `RegisterSymbolHandlers()` and `RegisterTextHandlers()` (see below). + +## API Dictionary + +### Interaction with Sibmei + +Extensions must only interact with Sibmei through the API dictionary passed to `InitSibmeiExtension()` because Sibmei's core methods may change at any point. If an extension requires access to functionality that is not exposed by the API dictionary, [create an issue](https://github.com/music-encoding/sibmei/issues/new) or a pull request on GitHub. + +### API data and methods + +The API dictionary exposes the following object: + +* **`libmei`**: A reference to libmei that can be used to construct and + manipulate MEI elements. *This dictionary must not be modified.* + +It exposes the following methods that must only be called in the initialization phase: + +* **`RegisterSymbolHandlers()`**: Call this function to make a symbol handler + known to Sibmei. To tell Sibmei which symbols the extension handles, the symbols must be + registered by their `Index` or `Name` property. For built-in + symbols, always use the `Index` property, for custom symbols, always use the + `Name` property. + + The Dictionary that needs to be passed to `RegisterSymbolHandlers()` has the + following structure: + + ``` + CreateDictionary( + 'Name', CreateDictionary( + 'My custom symbol', 'MyCustomSymbolHandler', + 'My other custom symbol', 'MyAlternativeCustomSymbolHandler' + ), + 'Index', CreateDictionary( + myIndex, 'MyCustomSymbolHandler', + myOtherIndex, 'MyCustomSymbolHandler' + ) + ) + ``` + + If Sibmei finds a symbol with a `Name` or `Index` property matching a key in + the respective sub-Dictionaries, it will call the symbol handler registered + under that key. A method of that name must be present in the extension + plugin. + + If no symbols are registered by either `Name` or `Index` property, the + respective sub-dictionaries can be omitted. + + Second argument of `RegisterSymbolHandler()` must be `Self`. + +* **`RegisterTextHandlers()`**: Works the same way as + `RegisterSymbolHandlers()`, with the difference that sub-Dictionary keys are + `StyleId` and `StyleAsText` instead of `Index` and `Name`. Always use + `StyleId` for built-in text styles and `StyleAsText` for custom text styles. + +The following methods must only be used by handler methods: + +* **`MeiFactory()`**: A convenience method that takes a template SparseArray as + argument and generates an MEI element from it. For detailed information, see + the documentation comments in [`Utilities.mss`](./src/Utilities.mss). + + It is recommended to define template dictionaries as global variables in the + `InitSibmeiExtension()` method instead of defining them locally in the symbol + handler methods. + +* **`HandleControlEvent()`**: Pass this function two arguments: + + * The to be exported `SymbolItem` or `SystemSymbolItem` + * A template suitable for passing to `MeiFactory()` + + `HandleControlEvent()` creates an MEI element and attaches it to the `` element. It returns the element for further manipulation by the extension plugin. + +* **`HandleModifier()`**: Works similarly to `HandleControlEvent()`, but attaches the generated MEI element to an event element (``, `` etc.) instead of the `` element. + +* **`AddFormattedText()`**: Takes arguments: + + * `parentElement`: MEI element that the formatted text nodes should be appended to + * `textObj`: A `Text` or `SystemTextItem` object. Its `TextWithFormatting` property is converted to MEI markup. + +* **`GenerateControlEvent()`**: Takes two arguments: + + * `bobj`: A `BarObject` + * `elementName`: Capitalized MEI element name, e.g. `'Line'`. + + Uses the `elementName` to generate an MEI element and adds applicable control event attributes (see `AddControlEventAttributes`) + +* **`AddControlEventAttributes()`**: Takes two arguments: + + * `bobj`: A `BarObject` + * `element`: An MEI element + + Adds the following control event attributes: + + * `@startid` (if a start object could be identified) and `@tstamp` + * If applicable (e.g. for lines), `@endid` (if an end object could be identified) and `@tstamp2` + * `@staff` (if object is staff-attached) + * `@layer` + * For lines: + * `@dur.ppq` (unless `Duration` is 0) + * `@startho`, `@startvo`, `@endho`, `@endvo` + * For elements other than lines: + * `@ho`, `@vo` diff --git a/README.md b/README.md index f511166..ca9166a 100644 --- a/README.md +++ b/README.md @@ -49,4 +49,20 @@ These unit tests are primarily used to test specific Sibmei functions. They use ### mocha -[Mocha](https://mochajs.org/) is used to test Sibmei's output from a set of test files. After exporting the test file set with sibmei (Testsibmei will automatically do that), run `npm test` from the root directory of this git repository. +[Mocha](https://mochajs.org/) is used to test Sibmei's output from a set of test files. After exporting the test file set with sibmei (Testsibmei will automatically do that), either run `npm test` from the root directory of this git repository or have Testsibmei automatically trigger the tests. The latter requires a `test.bat` or `test.sh` file in the same directory as the Sibmei `*.plg` files, depending on the operating system. Create a file that looks like this: + +#### Windows: test.bat + +``` +x: +cd x:\path\to\sibmei +cmd /k npm test +``` + +#### Mac: test.sh + +Help for testing and documenting for Mac welcome! + +## Writing Extensions + +For project specific handling of text and symbols, [extension plugins](Extensions.md) can be written. \ No newline at end of file diff --git a/assets/extension-choice.png b/assets/extension-choice.png new file mode 100644 index 0000000..e14d1c8 Binary files /dev/null and b/assets/extension-choice.png differ diff --git a/lib/libmei4.plg b/lib/libmei4.plg index df1ce3b..9dbbe13 100644 --- a/lib/libmei4.plg +++ b/lib/libmei4.plg @@ -1,6 +1,6 @@ { - + _License "() { return 'Copyright (c) 2011-2015 Andrew Hankinson, Alastair Porter, and Others @@ -24,15 +24,15 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.'; }" - + Arpeg "() { CreateElement('arpeg', null); }" -Attacca "() { - CreateElement('attacca', null); -}" - +Attacca "() { + CreateElement('attacca', null); +}" + BTrem "() { CreateElement('bTrem', null); }" @@ -53,10 +53,10 @@ Bend "() { CreateElement('bend', null); }" -BracketSpan "() { - CreateElement('bracketSpan', null); -}" - +BracketSpan "() { + CreateElement('bracketSpan', null); +}" + Breath "() { CreateElement('breath', null); }" @@ -73,10 +73,10 @@ Gliss "() { CreateElement('gliss', null); }" -GraceGrp "() { - CreateElement('graceGrp', null); -}" - +GraceGrp "() { + CreateElement('graceGrp', null); +}" + Hairpin "() { CreateElement('hairpin', null); }" @@ -89,14 +89,14 @@ HarpPedal "() { CreateElement('harpPedal', null); }" -Lv "() { - CreateElement('lv', null); -}" - -MNum "() { - CreateElement('mNum', null); -}" - +Lv "() { + CreateElement('lv', null); +}" + +MNum "() { + CreateElement('mNum', null); +}" + MRest "() { CreateElement('mRest', null); }" @@ -133,14 +133,14 @@ MultiRpt "() { CreateElement('multiRpt', null); }" -OLayer "() { - CreateElement('oLayer', null); -}" - -OStaff "() { - CreateElement('oStaff', null); -}" - +OLayer "() { + CreateElement('oLayer', null); +}" + +OStaff "() { + CreateElement('oStaff', null); +}" + Octave "() { CreateElement('octave', null); }" @@ -202,13 +202,13 @@ Rdg "() { }" Sp "() { - CreateElement('sp', null); -}" - -StageDir "() { - CreateElement('stageDir', null); -}" - + CreateElement('sp', null); +}" + +StageDir "() { + CreateElement('stageDir', null); +}" + Abbr "() { CreateElement('abbr', null); }" @@ -225,10 +225,10 @@ Corr "() { CreateElement('corr', null); }" -CpMark "() { - CreateElement('cpMark', null); -}" - +CpMark "() { + CreateElement('cpMark', null); +}" + Damage "() { CreateElement('damage', null); }" @@ -249,10 +249,10 @@ HandShift "() { CreateElement('handShift', null); }" -MetaMark "() { - CreateElement('metaMark', null); -}" - +MetaMark "() { + CreateElement('metaMark', null); +}" + Orig "() { CreateElement('orig', null); }" @@ -321,14 +321,14 @@ Tr "() { CreateElement('tr', null); }" -Fing "() { - CreateElement('fing', null); -}" - -FingGrp "() { - CreateElement('fingGrp', null); -}" - +Fing "() { + CreateElement('fing', null); +}" + +FingGrp "() { + CreateElement('fingGrp', null); +}" + Expression "() { CreateElement('expression', null); }" @@ -345,22 +345,22 @@ ItemList "() { CreateElement('itemList', null); }" -Manifestation "() { - CreateElement('manifestation', null); -}" - -ManifestationList "() { - CreateElement('manifestationList', null); -}" - -GenDesc "() { - CreateElement('genDesc', null); -}" - -GenState "() { - CreateElement('genState', null); -}" - +Manifestation "() { + CreateElement('manifestation', null); +}" + +ManifestationList "() { + CreateElement('manifestationList', null); +}" + +GenDesc "() { + CreateElement('genDesc', null); +}" + +GenState "() { + CreateElement('genState', null); +}" + ChordDef "() { CreateElement('chordDef', null); }" @@ -389,9 +389,9 @@ AccessRestrict "() { CreateElement('accessRestrict', null); }" -Acquisition "() { - CreateElement('acquisition', null); -}" +Acquisition "() { + CreateElement('acquisition', null); +}" AltId "() { CreateElement('altId', null); @@ -405,10 +405,10 @@ Application "() { CreateElement('application', null); }" -AttUsage "() { - CreateElement('attUsage', null); -}" - +AttUsage "() { + CreateElement('attUsage', null); +}" + Audience "() { CreateElement('audience', null); }" @@ -417,14 +417,14 @@ Availability "() { CreateElement('availability', null); }" -Bifolium "() { - CreateElement('bifolium', null); -}" - -Byline "() { - CreateElement('byline', null); -}" - +Bifolium "() { + CreateElement('bifolium', null); +}" + +Byline "() { + CreateElement('byline', null); +}" + CaptureMode "() { CreateElement('captureMode', null); }" @@ -433,14 +433,14 @@ CarrierForm "() { CreateElement('carrierForm', null); }" -CatRel "() { - CreateElement('catRel', null); -}" - -Category "() { - CreateElement('category', null); -}" - +CatRel "() { + CreateElement('catRel', null); +}" + +Category "() { + CreateElement('category', null); +}" + Change "() { CreateElement('change', null); }" @@ -449,18 +449,18 @@ ChangeDesc "() { CreateElement('changeDesc', null); }" -ClassDecls "() { - CreateElement('classDecls', null); +ClassDecls "() { + CreateElement('classDecls', null); }" Classification "() { CreateElement('classification', null); }" -ComponentList "() { - CreateElement('componentList', null); -}" - +ComponentList "() { + CreateElement('componentList', null); +}" + Condition "() { CreateElement('condition', null); }" @@ -481,18 +481,18 @@ Correction "() { CreateElement('correction', null); }" -Cutout "() { - CreateElement('cutout', null); -}" - -Dedication "() { - CreateElement('dedication', null); -}" - -DomainsDecl "() { - CreateElement('domainsDecl', null); -}" - +Cutout "() { + CreateElement('cutout', null); +}" + +Dedication "() { + CreateElement('dedication', null); +}" + +DomainsDecl "() { + CreateElement('domainsDecl', null); +}" + EditionStmt "() { CreateElement('editionStmt', null); }" @@ -505,17 +505,17 @@ EncodingDesc "() { CreateElement('encodingDesc', null); }" -ExhibHist "() { - CreateElement('exhibHist', null); -}" - -ExtMeta "() { - CreateElement('extMeta', null); -}" - -FileChar "() { - CreateElement('fileChar', null); -}" +ExhibHist "() { + CreateElement('exhibHist', null); +}" + +ExtMeta "() { + CreateElement('extMeta', null); +}" + +FileChar "() { + CreateElement('fileChar', null); +}" FileDesc "() { CreateElement('fileDesc', null); @@ -525,14 +525,14 @@ Fingerprint "() { CreateElement('fingerprint', null); }" -FoliaDesc "() { - CreateElement('foliaDesc', null); -}" - -Folium "() { - CreateElement('folium', null); -}" - +FoliaDesc "() { + CreateElement('foliaDesc', null); +}" + +Folium "() { + CreateElement('folium', null); +}" + Hand "() { CreateElement('hand', null); }" @@ -585,10 +585,10 @@ Meter "() { CreateElement('meter', null); }" -Namespace "() { - CreateElement('namespace', null); -}" - +Namespace "() { + CreateElement('namespace', null); +}" + Normalization "() { CreateElement('normalization', null); }" @@ -601,26 +601,26 @@ OtherChar "() { CreateElement('otherChar', null); }" -Patch "() { - CreateElement('patch', null); -}" - -PerfDuration "() { - CreateElement('perfDuration', null); -}" - +Patch "() { + CreateElement('patch', null); +}" + +PerfDuration "() { + CreateElement('perfDuration', null); +}" + PerfMedium "() { CreateElement('perfMedium', null); }" -PerfRes "() { - CreateElement('perfRes', null); -}" - -PerfResList "() { - CreateElement('perfResList', null); -}" - +PerfRes "() { + CreateElement('perfRes', null); +}" + +PerfResList "() { + CreateElement('perfResList', null); +}" + PhysDesc "() { CreateElement('physDesc', null); }" @@ -697,18 +697,18 @@ SysReq "() { CreateElement('sysReq', null); }" -TagUsage "() { - CreateElement('tagUsage', null); +TagUsage "() { + CreateElement('tagUsage', null); +}" + +TagsDecl "() { + CreateElement('tagsDecl', null); }" -TagsDecl "() { - CreateElement('tagsDecl', null); +Taxonomy "() { + CreateElement('taxonomy', null); }" -Taxonomy "() { - CreateElement('taxonomy', null); -}" - TermList "() { CreateElement('termList', null); }" @@ -717,10 +717,10 @@ TitleStmt "() { CreateElement('titleStmt', null); }" -TrackConfig "() { - CreateElement('trackConfig', null); -}" - +TrackConfig "() { + CreateElement('trackConfig', null); +}" + TreatHist "() { CreateElement('treatHist', null); }" @@ -745,22 +745,22 @@ Work "() { CreateElement('work', null); }" -WorkList "() { - CreateElement('workList', null); +WorkList "() { + CreateElement('workList', null); +}" + +Refrain "() { + CreateElement('refrain', null); }" -Refrain "() { - CreateElement('refrain', null); -}" - Verse "() { CreateElement('verse', null); }" -Volta "() { - CreateElement('volta', null); -}" - +Volta "() { + CreateElement('volta', null); +}" + Ligature "() { CreateElement('ligature', null); }" @@ -773,6 +773,10 @@ Proport "() { CreateElement('proport', null); }" +Stem "() { + CreateElement('stem', null); +}" + Cc "() { CreateElement('cc', null); }" @@ -841,162 +845,162 @@ Vel "() { CreateElement('vel', null); }" -AccMat "() { - CreateElement('accMat', null); -}" - -AddDesc "() { - CreateElement('addDesc', null); -}" - -Binding "() { - CreateElement('binding', null); -}" - -BindingDesc "() { - CreateElement('bindingDesc', null); -}" - -Catchwords "() { - CreateElement('catchwords', null); -}" - -Collation "() { - CreateElement('collation', null); -}" - -Colophon "() { - CreateElement('colophon', null); -}" - -DecoDesc "() { - CreateElement('decoDesc', null); -}" - -DecoNote "() { - CreateElement('decoNote', null); -}" - -Explicit "() { - CreateElement('explicit', null); -}" - -Foliation "() { - CreateElement('foliation', null); -}" - -Heraldry "() { - CreateElement('heraldry', null); -}" - -Layout "() { - CreateElement('layout', null); -}" - -LayoutDesc "() { - CreateElement('layoutDesc', null); -}" - -Locus "() { - CreateElement('locus', null); -}" - -LocusGrp "() { - CreateElement('locusGrp', null); -}" - -Rubric "() { - CreateElement('rubric', null); -}" - -ScriptDesc "() { - CreateElement('scriptDesc', null); -}" - -ScriptNote "() { - CreateElement('scriptNote', null); -}" - -Seal "() { - CreateElement('seal', null); -}" - -SealDesc "() { - CreateElement('sealDesc', null); -}" - -SecFolio "() { - CreateElement('secFolio', null); -}" - -Signatures "() { - CreateElement('signatures', null); -}" - -Stamp "() { - CreateElement('stamp', null); -}" - -Support "() { - CreateElement('support', null); -}" - -SupportDesc "() { - CreateElement('supportDesc', null); -}" - -TypeDesc "() { - CreateElement('typeDesc', null); -}" - -TypeNote "() { - CreateElement('typeNote', null); -}" - -AddName "() { - CreateElement('addName', null); -}" - -Bloc "() { - CreateElement('bloc', null); -}" - +AccMat "() { + CreateElement('accMat', null); +}" + +AddDesc "() { + CreateElement('addDesc', null); +}" + +Binding "() { + CreateElement('binding', null); +}" + +BindingDesc "() { + CreateElement('bindingDesc', null); +}" + +Catchwords "() { + CreateElement('catchwords', null); +}" + +Collation "() { + CreateElement('collation', null); +}" + +Colophon "() { + CreateElement('colophon', null); +}" + +DecoDesc "() { + CreateElement('decoDesc', null); +}" + +DecoNote "() { + CreateElement('decoNote', null); +}" + +Explicit "() { + CreateElement('explicit', null); +}" + +Foliation "() { + CreateElement('foliation', null); +}" + +Heraldry "() { + CreateElement('heraldry', null); +}" + +Layout "() { + CreateElement('layout', null); +}" + +LayoutDesc "() { + CreateElement('layoutDesc', null); +}" + +Locus "() { + CreateElement('locus', null); +}" + +LocusGrp "() { + CreateElement('locusGrp', null); +}" + +Rubric "() { + CreateElement('rubric', null); +}" + +ScriptDesc "() { + CreateElement('scriptDesc', null); +}" + +ScriptNote "() { + CreateElement('scriptNote', null); +}" + +Seal "() { + CreateElement('seal', null); +}" + +SealDesc "() { + CreateElement('sealDesc', null); +}" + +SecFolio "() { + CreateElement('secFolio', null); +}" + +Signatures "() { + CreateElement('signatures', null); +}" + +Stamp "() { + CreateElement('stamp', null); +}" + +Support "() { + CreateElement('support', null); +}" + +SupportDesc "() { + CreateElement('supportDesc', null); +}" + +TypeDesc "() { + CreateElement('typeDesc', null); +}" + +TypeNote "() { + CreateElement('typeNote', null); +}" + +AddName "() { + CreateElement('addName', null); +}" + +Bloc "() { + CreateElement('bloc', null); +}" + CorpName "() { CreateElement('corpName', null); }" -Country "() { - CreateElement('country', null); -}" - -District "() { - CreateElement('district', null); -}" - -FamName "() { - CreateElement('famName', null); -}" - -ForeName "() { - CreateElement('foreName', null); -}" - -GenName "() { - CreateElement('genName', null); -}" - -GeogFeat "() { - CreateElement('geogFeat', null); -}" - +Country "() { + CreateElement('country', null); +}" + +District "() { + CreateElement('district', null); +}" + +FamName "() { + CreateElement('famName', null); +}" + +ForeName "() { + CreateElement('foreName', null); +}" + +GenName "() { + CreateElement('genName', null); +}" + +GeogFeat "() { + CreateElement('geogFeat', null); +}" + GeogName "() { CreateElement('geogName', null); }" -NameLink "() { - CreateElement('nameLink', null); -}" - +NameLink "() { + CreateElement('nameLink', null); +}" + PeriodName "() { CreateElement('periodName', null); }" @@ -1005,78 +1009,78 @@ PersName "() { CreateElement('persName', null); }" -PostBox "() { - CreateElement('postBox', null); -}" - -PostCode "() { - CreateElement('postCode', null); -}" - -Region "() { - CreateElement('region', null); -}" - -RoleName "() { - CreateElement('roleName', null); -}" - -Settlement "() { - CreateElement('settlement', null); -}" - -Street "() { - CreateElement('street', null); -}" - +PostBox "() { + CreateElement('postBox', null); +}" + +PostCode "() { + CreateElement('postCode', null); +}" + +Region "() { + CreateElement('region', null); +}" + +RoleName "() { + CreateElement('roleName', null); +}" + +Settlement "() { + CreateElement('settlement', null); +}" + +Street "() { + CreateElement('street', null); +}" + StyleName "() { CreateElement('styleName', null); }" -Episema "() { - CreateElement('episema', null); -}" - -HispanTick "() { - CreateElement('hispanTick', null); -}" - -Liquescent "() { - CreateElement('liquescent', null); -}" - -Nc "() { - CreateElement('nc', null); -}" - -NcGrp "() { - CreateElement('ncGrp', null); -}" - -Neume "() { - CreateElement('neume', null); -}" - -Oriscus "() { - CreateElement('oriscus', null); -}" - -Quilisma "() { - CreateElement('quilisma', null); -}" - -SignifLet "() { - CreateElement('signifLet', null); -}" - -Strophicus "() { - CreateElement('strophicus', null); -}" - -Syllable "() { - CreateElement('syllable', null); -}" - +Episema "() { + CreateElement('episema', null); +}" + +HispanTick "() { + CreateElement('hispanTick', null); +}" + +Liquescent "() { + CreateElement('liquescent', null); +}" + +Nc "() { + CreateElement('nc', null); +}" + +NcGrp "() { + CreateElement('ncGrp', null); +}" + +Neume "() { + CreateElement('neume', null); +}" + +Oriscus "() { + CreateElement('oriscus', null); +}" + +Quilisma "() { + CreateElement('quilisma', null); +}" + +SignifLet "() { + CreateElement('signifLet', null); +}" + +Strophicus "() { + CreateElement('strophicus', null); +}" + +Syllable "() { + CreateElement('syllable', null); +}" + AvFile "() { CreateElement('avFile', null); }" @@ -1093,10 +1097,10 @@ Recording "() { CreateElement('recording', null); }" -When "() { - CreateElement('when', null); -}" - +When "() { + CreateElement('when', null); +}" + Ptr "() { CreateElement('ptr', null); }" @@ -1121,18 +1125,18 @@ Address "() { CreateElement('address', null); }" -AmbNote "() { - CreateElement('ambNote', null); -}" - -Ambitus "() { - CreateElement('ambitus', null); -}" - -Analytic "() { - CreateElement('analytic', null); -}" - +AmbNote "() { + CreateElement('ambNote', null); +}" + +Ambitus "() { + CreateElement('ambitus', null); +}" + +Analytic "() { + CreateElement('analytic', null); +}" + Annot "() { CreateElement('annot', null); }" @@ -1165,18 +1169,18 @@ BiblScope "() { CreateElement('biblScope', null); }" -BiblStruct "() { - CreateElement('biblStruct', null); -}" - +BiblStruct "() { + CreateElement('biblStruct', null); +}" + Body "() { CreateElement('body', null); }" -Caesura "() { - CreateElement('caesura', null); -}" - +Caesura "() { + CreateElement('caesura', null); +}" + Caption "() { CreateElement('caption', null); }" @@ -1193,10 +1197,10 @@ CastList "() { CreateElement('castList', null); }" -Cb "() { - CreateElement('cb', null); -}" - +Cb "() { + CreateElement('cb', null); +}" + Chord "() { CreateElement('chord', null); }" @@ -1209,18 +1213,18 @@ ClefGrp "() { CreateElement('clefGrp', null); }" -ColLayout "() { - CreateElement('colLayout', null); -}" - +ColLayout "() { + CreateElement('colLayout', null); +}" + Composer "() { CreateElement('composer', null); }" -Contributor "() { - CreateElement('contributor', null); -}" - +Contributor "() { + CreateElement('contributor', null); +}" + Creation "() { CreateElement('creation', null); }" @@ -1233,26 +1237,26 @@ Date "() { CreateElement('date', null); }" -Dedicatee "() { - CreateElement('dedicatee', null); -}" - -Depth "() { - CreateElement('depth', null); -}" - -Desc "() { - CreateElement('desc', null); -}" - -Dim "() { - CreateElement('dim', null); -}" - -Dimensions "() { - CreateElement('dimensions', null); -}" - +Dedicatee "() { + CreateElement('dedicatee', null); +}" + +Depth "() { + CreateElement('depth', null); +}" + +Desc "() { + CreateElement('desc', null); +}" + +Dim "() { + CreateElement('dim', null); +}" + +Dimensions "() { + CreateElement('dimensions', null); +}" + Dir "() { CreateElement('dir', null); }" @@ -1261,10 +1265,10 @@ Distributor "() { CreateElement('distributor', null); }" -Div "() { - CreateElement('div', null); -}" - +Div "() { + CreateElement('div', null); +}" + Dot "() { CreateElement('dot', null); }" @@ -1285,14 +1289,14 @@ Ending "() { CreateElement('ending', null); }" -Event "() { - CreateElement('event', null); -}" - -EventList "() { - CreateElement('eventList', null); -}" - +Event "() { + CreateElement('event', null); +}" + +EventList "() { + CreateElement('eventList', null); +}" + Expansion "() { CreateElement('expansion', null); }" @@ -1317,14 +1321,14 @@ GrpSym "() { CreateElement('grpSym', null); }" -Head "() { - CreateElement('head', null); -}" - -Height "() { - CreateElement('height', null); -}" - +Head "() { + CreateElement('head', null); +}" + +Height "() { + CreateElement('height', null); +}" + Identifier "() { CreateElement('identifier', null); }" @@ -1349,10 +1353,10 @@ Label "() { CreateElement('label', null); }" -LabelAbbr "() { - CreateElement('labelAbbr', null); -}" - +LabelAbbr "() { + CreateElement('labelAbbr', null); +}" + Layer "() { CreateElement('layer', null); }" @@ -1365,10 +1369,10 @@ Lb "() { CreateElement('lb', null); }" -Lg "() { - CreateElement('lg', null); -}" - +Lg "() { + CreateElement('lg', null); +}" + Librettist "() { CreateElement('librettist', null); }" @@ -1385,10 +1389,10 @@ Mei "() { CreateElement('mei', null); }" -Monogr "() { - CreateElement('monogr', null); -}" - +Monogr "() { + CreateElement('monogr', null); +}" + Music "() { CreateElement('music', null); }" @@ -1405,10 +1409,10 @@ Num "() { CreateElement('num', null); }" -Ornam "() { - CreateElement('ornam', null); -}" - +Ornam "() { + CreateElement('ornam', null); +}" + P "() { CreateElement('p', null); }" @@ -1473,14 +1477,14 @@ RelatedItem "() { CreateElement('relatedItem', null); }" -Relation "() { - CreateElement('relation', null); -}" - -RelationList "() { - CreateElement('relationList', null); -}" - +Relation "() { + CreateElement('relation', null); +}" + +RelationList "() { + CreateElement('relationList', null); +}" + Rend "() { CreateElement('rend', null); }" @@ -1533,10 +1537,10 @@ Space "() { CreateElement('space', null); }" -Speaker "() { - CreateElement('speaker', null); -}" - +Speaker "() { + CreateElement('speaker', null); +}" + Sponsor "() { CreateElement('sponsor', null); }" @@ -1561,18 +1565,18 @@ Syl "() { CreateElement('syl', null); }" -Symbol "() { - CreateElement('symbol', null); -}" - +Symbol "() { + CreateElement('symbol', null); +}" + Tempo "() { CreateElement('tempo', null); }" -Term "() { - CreateElement('term', null); -}" - +Term "() { + CreateElement('term', null); +}" + TextLang "() { CreateElement('textLang', null); }" @@ -1585,36 +1589,36 @@ TitlePage "() { CreateElement('titlePage', null); }" -TitlePart "() { - CreateElement('titlePart', null); -}" - -Width "() { - CreateElement('width', null); -}" - -Barre "() { - CreateElement('barre', null); -}" - -Argument "() { - CreateElement('argument', null); -}" - +TitlePart "() { + CreateElement('titlePart', null); +}" + +Width "() { + CreateElement('width', null); +}" + +Barre "() { + CreateElement('barre', null); +}" + +Argument "() { + CreateElement('argument', null); +}" + Back "() { CreateElement('back', null); }" -Epigraph "() { - CreateElement('epigraph', null); +Epigraph "() { + CreateElement('epigraph', null); }" Front "() { CreateElement('front', null); }" -Imprimatur "() { - CreateElement('imprimatur', null); +Imprimatur "() { + CreateElement('imprimatur', null); }" L "() { @@ -1629,18 +1633,18 @@ List "() { CreateElement('list', null); }" -Q "() { - CreateElement('q', null); -}" - +Q "() { + CreateElement('q', null); +}" + Quote "() { CreateElement('quote', null); }" -Seg "() { - CreateElement('seg', null); -}" - +Seg "() { + CreateElement('seg', null); +}" + AnchoredText "() { CreateElement('anchoredText', null); }" @@ -1653,41 +1657,40 @@ Line "() { CreateElement('line', null); }" -Mapping "() { - CreateElement('mapping', null); -}" - -PropName "() { - CreateElement('propName', null); -}" - -PropValue "() { - CreateElement('propValue', null); -}" - -SymName "() { - CreateElement('symName', null); -}" - -SymProp "() { - CreateElement('symProp', null); -}" - -SymbolDef "() { - CreateElement('symbolDef', null); -}" +Mapping "() { + CreateElement('mapping', null); +}" + +PropName "() { + CreateElement('propName', null); +}" + +PropValue "() { + CreateElement('propValue', null); +}" + +SymName "() { + CreateElement('symName', null); +}" + +SymProp "() { + CreateElement('symProp', null); +}" + +SymbolDef "() { + CreateElement('symbolDef', null); +}" SymbolTable "() { CreateElement('symbolTable', null); }" - + Initialize "() { tree_doc = CreateSparseArray(); flat_doc = CreateDictionary(); id_incr = 0; - Self._property:XMLIdToObjectMap = CreateDictionary(); Self._property:MEIDocument = tree_doc; Self._property:MEIFlattened = flat_doc; Self._property:MEIID = id_incr; @@ -1698,7 +1701,6 @@ Initialize "() { commentObj.text = comment; return commentObj; }" - CreateElement "(tagname, orig_id) { element = CreateDictionary( 'name', tagname, @@ -1723,7 +1725,6 @@ CreateElement "(tagname, orig_id) { return element; }" - GetChildren "(element) { c = CreateSparseArray(); for each child_id in element.children { @@ -1732,17 +1733,23 @@ GetChildren "(element) { } return c; }" - SetChildren "(element, childarr) { element.children = childarr; }" - +AddChildAtPosition "(element, child, position) { + AddChild(element, child); + c = element.children; + // shift all children that are at a higher index than `position` + for i = c.Length - 1 to position step -1 { + c[i] = c[i - 1]; + } + element.children[position] = child._id; +}" AddChild "(element, child) { cid = child._id; child._parent = element._id; element.children.Push(cid); }" - RemoveChild "(element, child) { newarr = CreateSparseArray(); @@ -1756,32 +1763,15 @@ RemoveChild "(element, child) { element.children = newarr; }" - -GetChildById "(element, childid) { - d = Self._property:MEIFlattened; - - for each elid in element.children - { - if (elid = childid) - { - return d[elid]; - } - } - - return False; -}" - GetAttributes "(element) { return element.attrs; }" - AddAttribute "(element, attrname, attrval) { a = element.attrs; // check and replace any newlines val = _encodeEntities(attrval); a[attrname] = val; }" - AddAttributeValue "(element, attrname, attrval) { // appends a value to an existing attribute. Used, for example, // in appending multiple articulations to @artic on note. @@ -1800,7 +1790,6 @@ AddAttributeValue "(element, attrname, attrval) { element.attrs[attrname] = val; }" - GetAttribute "(element, attrname) { attrs = element.attrs; if (attrs.PropertyExists(attrname)) @@ -1812,7 +1801,6 @@ GetAttribute "(element, attrname) { return False; } }" - SetAttributes "(element, new_attrs) { //sets all attributes, wiping out any previous ones element.attrs = CreateDictionary(); @@ -1823,11 +1811,9 @@ SetAttributes "(element, new_attrs) { AddAttribute(element, a.Name, a.Value); } }" - GetId "(element) { return element._id; }" - SetId "(element, value) { olddict = Self._property:MEIFlattened; oldid = element._id; @@ -1837,7 +1823,6 @@ SetId "(element, value) { newdict[value] = element; Self._property:MEIFlattened = newdict; }" - RemoveAttribute "(element, attrname) { // since there are no delete functions // for dictionaries, we set the attribute @@ -1848,19 +1833,15 @@ RemoveAttribute "(element, attrname) { GetName "(element) { return element.name; }" - SetText "(element, val) { element.text = _encodeEntities(val); }" - GetText "(element) { return element.text; }" - SetTail "(element, val) { element.tail = _encodeEntities(val); }" - GetTail "(element) { return element.tail; }" @@ -1869,7 +1850,6 @@ GetTail "(element) { // cleans up Self._property:MEIFlattened = CreateDictionary(); Self._property:MEIDocument = CreateSparseArray(); - Self._property:XMLIdToObjectMap = CreateDictionary(); Self._property:MEIID = 0; }" @@ -2073,7 +2053,7 @@ GetTail "(element) { meiDocumentToFile "(meidoc, filename) { meiout = _exportMeiDocument(meidoc); if (Sibelius.CreateTextFile(filename)) { - return Sibelius.AppendTextFile(filename, meiout, 1); + return Sibelius.AppendTextFile(filename, meiout, true); } else { return false; } @@ -2087,7 +2067,6 @@ GetTail "(element) { return res; }" - popMode "(arr) { if (arr.Length > 0) { return arr.Pop(); @@ -2096,7 +2075,6 @@ GetTail "(element) { return 15; } }" - _encodeEntities "(string) { /* @@ -2124,11 +2102,10 @@ GetTail "(element) { return string; }" - _xmlImport "(filename) { /* Based on the Quick-n-Dirty XML parser at - http://www.javaworld.com/javatips/jw-javatip128.html + https://www.infoworld.com/article/2077493/java-tip-128--create-a-quick-and-dirty-xml-parser.html */ xmlinput = Sibelius.ReadTextFile(filename, true); meidoc = CreateSparseArray(); diff --git a/lib/sibmei4_batch_mxml.plg b/lib/sibmei4_batch_mxml.plg index 2c9b632..7c7662a 100644 --- a/lib/sibmei4_batch_mxml.plg +++ b/lib/sibmei4_batch_mxml.plg @@ -11,6 +11,8 @@ if (IsObject(folder)) { + sibmei4.InitGlobals(null); + // count files for progress dialog numFiles = folder.FileCount('XML'); index = 0; diff --git a/lib/sibmei4_batch_sib.plg b/lib/sibmei4_batch_sib.plg index cfd3166..2113101 100644 --- a/lib/sibmei4_batch_sib.plg +++ b/lib/sibmei4_batch_sib.plg @@ -10,19 +10,21 @@ folder = Sibelius.SelectFolder(); if (null != folder) { - ConvertFolder(folder); + ConvertFolder(folder, null); } }" - ConvertFolder "(folder) + ConvertFolder "(folder, extensions) { if (not IsObject(folder)) { - Sibelius.MessageBox('Not a folder object: ' & folder); + Sibelius.MessageBox('Not a folder object: ' & folder); } else { + sibmei4.InitGlobals(extensions); + // count files for progress dialog numFiles = folder.FileCount('SIB'); index = 0; diff --git a/lib/sibmei4_extension_test.plg b/lib/sibmei4_extension_test.plg new file mode 100644 index 0000000..6fa02c7 --- /dev/null +++ b/lib/sibmei4_extension_test.plg @@ -0,0 +1,51 @@ +{ + SibmeiExtensionAPIVersion "1.0.0" + + Initialize "() { + AddToPluginsMenu('Sibmei extension test', 'Run'); + }" + + Run "() { + // The plugin will be listed in the menu, but it is not runnable. Give some + // instructions instead of showing no response when users try to run it. + Sibelius.MessageBox( + 'This plug-in is an extension of the sibmei MEI export plug-in. To use it, run MEI export.' + ); + }" + + InitSibmeiExtension "(api) { + Self._property:api = api; + Self._property:libmei = api.libmei; + + Self._property:MySymbolTemplate = CreateSparseArray('Symbol', CreateDictionary( + 'fontfam', 'myCustomFont', + 'glyph.name', 'mySymbolGlyph' + )); + + api.RegisterSymbolHandlers(CreateDictionary( + 'Name', CreateDictionary( + 'My symbol', 'HandleMySymbol' + ) + ), Self); + + api.RegisterTextHandlers(CreateDictionary( + 'StyleAsText', CreateDictionary( + 'My text', 'HandleMyText' + ) + ), Self); + + }" + + HandleMySymbol "(this, obj) { + symbolElement = api.HandleControlEvent(obj, MySymbolTemplate); + if (obj.ColorRed = 255) { + libmei.AddAttribute(symbolElement, 'type', 'myRedType'); + } + }" + + HandleMyText "(this, textObj) { + textElement = api.GenerateControlEvent(textObj, 'AnchoredText'); + api.AddFormattedText(textElement, textObj); + return textElement; + }" +} diff --git a/plgconfig.js b/plgconfig.js index c80e43a..f2189b4 100644 --- a/plgconfig.js +++ b/plgconfig.js @@ -7,7 +7,7 @@ var config = { plgCategory: 'MEI Export', pluginFilename: 'sibmei4.plg', linkLibraries: [ - 'libmei4.plg', 'sibmei4_batch_mxml.plg', 'sibmei4_batch_sib.plg', 'sibmei4_test_runner.plg' + 'libmei4.plg', 'sibmei4_batch_mxml.plg', 'sibmei4_batch_sib.plg', 'sibmei4_test_runner.plg', 'sibmei4_extension_test.plg' ], importDir: './import', buildDir: './build', diff --git a/src/ExportConverters.mss b/src/ExportConverters.mss index c75dd7d..3bb934f 100644 --- a/src/ExportConverters.mss +++ b/src/ExportConverters.mss @@ -592,15 +592,11 @@ function ConvertSibeliusStructure (score) { { for each Bar b in s { - if (bar_to_staff.PropertyExists(b.BarNumber)) - { - bar_to_staff[b.BarNumber].Push(s.StaffNum); - } - else + if (not bar_to_staff.PropertyExists(b.BarNumber)) { bar_to_staff[b.BarNumber] = CreateSparseArray(); - bar_to_staff[b.BarNumber].Push(s.StaffNum); } + bar_to_staff[b.BarNumber].Push(s.StaffNum); } } return bar_to_staff; @@ -611,8 +607,7 @@ function ConvertColor (nrest) { r = nrest.ColorRed; g = nrest.ColorGreen; b = nrest.ColorBlue; - a_dec = nrest.ColorAlpha & '.0'; - a = a_dec / 255.0; + a = nrest.ColorAlpha / 255.0; return 'rgba(' & r & ',' & g & ',' & b & ',' & a & ')'; } //$end @@ -880,89 +875,6 @@ function ConvertBarline (linetype) { } } //$end -function ConvertText (textobj) { - //$module(ExportConverters.mss) - styleid = textobj.StyleId; - switch (styleid) - { - case ('text.staff.expression') - { - dynam = libmei.Dynam(); - libmei.SetText(dynam, lstrip(textobj.Text)); - libmei.AddAttribute(dynam, 'staff', textobj.ParentBar.ParentStaff.StaffNum); - libmei.AddAttribute(dynam, 'tstamp', ConvertPositionToTimestamp(textobj.Position, textobj.ParentBar)); - - if (textobj.Dx != 0) - { - libmei.AddAttribute(dynam, 'ho', ConvertOffsetsToMEI(textobj.Dx)); - } - - if (textobj.Dy != 0) - { - libmei.AddAttribute(dynam, 'vo', ConvertOffsetsToMEI(textobj.Dy)); - } - return dynam; - } - case ('text.system.page_aligned.title') - { - text = ConvertSubstitution(textobj.Text); - atext = libmei.AnchoredText(); - title = libmei.Title(); - - libmei.AddChild(atext, title); - libmei.SetText(title, text); - - return atext; - } - case ('text.system.page_aligned.composer') - { - return ConvertTextElement(textobj); - } - case ('text.system.tempo') - { - tempo = libmei.Tempo(); - atext = ConvertTextElement(textobj); - libmei.AddAttribute(tempo, 'tstamp', ConvertPositionToTimestamp(textobj.Position, textobj.ParentBar)); - libmei.AddChild(tempo, atext); - return tempo; - } - case ('text.staff.space.figuredbass') - { - harm = libmei.Harm(); - harm = AddBarObjectInfoToElement(textobj, harm); - fb = libmei.Fb(); - libmei.AddChild(harm, fb); - ConvertFbFigures(fb, textobj); - return harm; - } - default - { - return null; - } - } -} //$end - -function ConvertTextElement (textobj) { - //$module(ExportConverters.mss) - obj = libmei.AnchoredText(); - - text = ConvertSubstitution(textobj.Text); - - libmei.SetText(obj, text); - - if (textobj.Dx != 0) - { - libmei.AddAttribute(obj, 'ho', ConvertOffsetsToMEI(textobj.Dx)); - } - - if (textobj.Dy != 0) - { - libmei.AddAttribute(obj, 'vo', ConvertOffsetsToMEI(textobj.Dy)); - } - - return obj; -} //$end - function ConvertFbFigures (fb, bobj) { //$module(ExportConverters) if (Self._property:FigbassCharMap = null) @@ -1148,59 +1060,29 @@ function ConvertTimeStamp (time) { return isodate; } //$end - -function ConvertSubstitution (string) { +function ConvertFermataForm (bobj) { //$module(ExportConverters.mss) - // if the string does not start with a substitution, send back the original string. - if (Substring(string, 0, 2) != '\\$') + + // Tries to find out @shape for 'keypad fermatas' of NoteRests and BarRests. + // At this point we expect that the calling function has already determined + // that the noteRest has a 'keypad fermata'. + + if (bobj.Type = 'BarRest') { - return string; + stemweight = 0; + } + else + { + stemweight = bobj.Stemweight; } - score = Self._property:ActiveScore; - // it's 3 because of the two chars at the beginning, and then the last backslash. - fieldname = Substring(string, 2, Length(string) - 3); - - switch (fieldname) + if ((stemweight < 0) or (bobj.VoiceNumber % 2 = 1) or HasSingleVoice(bobj.ParentBar)) { - case ('Title') - { - return score.Title; - } - case ('Composer') - { - return score.Composer; - } - case ('Arranger') - { - return score.Arranger; - } - case ('Lyricist') - { - return score.Lyricist; - } - case ('MoreInfo') - { - return score.MoreInfo; - } - case ('Artist') - { - return score.Artist; - } - case ('Copyright') - { - return score.Copyright; - } - case ('Publisher') - { - return score.Publisher; - } - case ('PartName') - { - return score.PartName; - } + return 'norm'; + } + else + { + return 'inv'; } - // if it doesn't match anything, return the original string. - return string; } //$end diff --git a/src/ExportGenerators.mss b/src/ExportGenerators.mss index 261a581..1f7ad51 100644 --- a/src/ExportGenerators.mss +++ b/src/ExportGenerators.mss @@ -121,6 +121,20 @@ function GenerateApplicationInfo () { libmei.AddChild(plgapp, plgname); libmei.AddChild(appI, plgapp); + if (Self._property:ChosenExtensions) + { + for each Pair ext in Self._property:ChosenExtensions + { + extapp = libmei.Application(); + libmei.SetId(extapp, ext.Name); + libmei.AddAttribute(extapp, 'type', 'extension'); + extName = libmei.Name(); + libmei.SetText(extName, ext.Value); + libmei.AddChild(extapp, extName); + libmei.AddChild(appI,extapp); + } + } + return appI; } //$end @@ -134,7 +148,6 @@ function GenerateMEIMusic () { Self._property:LyricWords = CreateDictionary(); Self._property:SpecialBarlines = CreateDictionary(); Self._property:SystemText = CreateDictionary(); - Self._property:LayerObjectPositions = null; Self._property:ObjectPositions = CreateDictionary(); Self._property:VoltaBars = CreateDictionary(); @@ -354,9 +367,6 @@ function GenerateMeasure (num) { libmei.AddAttribute(m, 'metcon', 'false'); } - systf = score.SystemStaff; - sysBar = systf[num]; - if (sysBar.NthBarInSystem = 0) { Self._property:SystemBreak = libmei.Sb(); @@ -428,7 +438,7 @@ function GenerateMeasure (num) { textobjs = systemtext[num]; for each textobj in textobjs { - text = ConvertText(textobj); + text = HandleText(textobj); if (text != null) { @@ -701,17 +711,7 @@ function GenerateLayers (staffnum, measurenum) { } case('Text') { - mobj = ConvertText(bobj); - if (mobj != null) - { - //Try to get note at position of bracket and put id - obj = GetNoteObjectAtPosition(bobj); - - if (obj != null) - { - libmei.AddAttribute(mobj, 'startid', '#' & obj._id); - } - } + mobj = HandleText(bobj); } } @@ -741,7 +741,7 @@ function GenerateLayers (staffnum, measurenum) { for each SymbolItem sobj in bar { - ProcessSymbol(sobj); + HandleSymbol(sobj); } ProcessEndingSlurs(bar); @@ -844,15 +844,19 @@ function GenerateNoteRest (bobj, layer) { libmei.AddAttribute(nr, 'stem.mod', '1slash'); } - if (bobj.GetArticulation(PauseArtic) or bobj.GetArticulation(TriPauseArtic) or bobj.GetArticulation(SquarePauseArtic)) + if (bobj.GetArticulation(PauseArtic)) { - fermata = GenerateFermata(bobj); - if (fermata != null) - { - libmei.AddAttribute(fermata, 'startid', '#' & nr._id); - measureObjs = Self._property:MeasureObjects; - measureObjs.Push(fermata._id); - } + GenerateFermata(bobj, 'curved', ConvertFermataForm(bobj)); + } + + if (bobj.GetArticulation(SquarePauseArtic)) + { + GenerateFermata(bobj, 'square', ConvertFermataForm(bobj)); + } + + if (bobj.GetArticulation(TriPauseArtic)) + { + GenerateFermata(bobj, 'angular', ConvertFermataForm(bobj)); } if (bobj.GetArticulation(StaccatoArtic)) @@ -979,11 +983,6 @@ function GenerateRest (bobj) { libmei.AddAttribute(r, 'color', nrest_color); } - if (bobj.GetArticulation(PauseArtic)) - { - libmei.AddAttribute(r, 'fermata', 'above'); - } - return r; } //$end @@ -999,7 +998,7 @@ function GenerateNote (nobj) { ptuplet = nobj.ParentNoteRest.ParentTupletIfAny; pnum = ptuplet.Left; pden = ptuplet.Right; - floatgesdur = (pden & '.0' / pnum & '.0') * dur; + floatgesdur = (pden * 1.0 / pnum) * dur; gesdur = Round(floatgesdur); } else @@ -1224,12 +1223,19 @@ function GenerateBarRest (bobj) { } } - fermata = GenerateFermata(bobj); - if (fermata != null) - { - libmei.AddAttribute(fermata, 'startid', '#' & obj._id); - measureObjs = Self._property:MeasureObjects; - measureObjs.Push(fermata._id); + switch (bobj.PauseType) { + case(PauseTypeRound) + { + GenerateFermata(bobj, 'curved', ConvertFermataForm(bobj)); + } + case(PauseTypeTriangular) + { + GenerateFermata(bobj, 'angular', ConvertFermataForm(bobj)); + } + case(PauseTypeSquare) + { + GenerateFermata(bobj, 'square', ConvertFermataForm(bobj)); + } } if (bobj.Hidden = true) @@ -1320,7 +1326,6 @@ function GenerateStaffGroups (score, barnum) { for each Staff s in score { std = libmei.StaffDef(); - libmei.XMLIdToObjectMap[std._id] = s; libmei.AddAttribute(std, 'n', s.StaffNum); libmei.AddAttribute(std, 'lines', s.InitialInstrumentType.NumStaveLines); @@ -1430,6 +1435,12 @@ function GenerateStaffGroups (score, barnum) { return parentstgrp; } //$end +function GenerateControlEvent (bobj, elementName) { + //$module(ExportGenerators.mss) + + return AddControlEventAttributes(bobj, libmei.@elementName()); +} //$end + function GenerateTuplet(tupletObj) { //$module(ExportGenerators.mss) tuplet = libmei.Tuplet(); @@ -1479,36 +1490,34 @@ function GenerateLine (bobj) { { case ('Slur') { - line = libmei.Slur(); + line = GenerateControlEvent(bobj, 'Slur'); slurrend = ConvertSlurStyle(bobj.StyleId); libmei.AddAttribute(line, 'lform', slurrend[1]); } case ('CrescendoLine') { - line = libmei.Hairpin(); + line = GenerateControlEvent(bobj, 'Hairpin'); libmei.AddAttribute(line, 'form', 'cres'); } case ('DiminuendoLine') { - line = libmei.Hairpin(); + line = GenerateControlEvent(bobj, 'Hairpin'); libmei.AddAttribute(line, 'form', 'dim'); } case ('OctavaLine') { - line = libmei.Octave(); + line = GenerateControlEvent(bobj, 'Octave'); octrend = ConvertOctava(bobj.StyleId); libmei.AddAttribute(line, 'dis', octrend[0]); libmei.AddAttribute(line, 'dis.place', octrend[1]); } case ('GlissandoLine') { - line = libmei.Gliss(); + line = GenerateControlEvent(bobj, 'Gliss'); } case ('Trill') { line = GenerateTrill(bobj); - // NB: Return here since the trill already has its properties set. - return line; } case ('Line') { @@ -1519,7 +1528,7 @@ function GenerateLine (bobj) { //brackets case ('bracket') { - line = libmei.Line(); + line = GenerateControlEvent(bobj, 'Line'); bracketType = 'bracket'; //horizontal brackets @@ -1606,7 +1615,7 @@ function GenerateLine (bobj) { //solid vertical line case ('vertical') { - line = libmei.Line(); + line = GenerateControlEvent(bobj, 'Line'); libmei.AddAttribute(line,'form','solid'); libmei.AddAttribute(line,'type','vertical'); } @@ -1618,7 +1627,7 @@ function GenerateLine (bobj) { { if (linecomps[3] = 'vertical') { - line = libmei.Line(); + line = GenerateControlEvent(bobj, 'Line'); libmei.AddAttribute(line,'form','dashed'); libmei.AddAttribute(line,'type','vertical'); } @@ -1626,25 +1635,25 @@ function GenerateLine (bobj) { //dashed horizontal line else { - line = libmei.Line(); + line = GenerateControlEvent(bobj, 'Line'); libmei.AddAttribute(line,'form','dashed'); } } //dotted horizontal line case('dotted') { - line = libmei.Line(); + line = GenerateControlEvent(bobj, 'Line'); libmei.AddAttribute(line,'form','dotted'); } //solid horizontal line case('plain') { - line = libmei.Line(); + line = GenerateControlEvent(bobj, 'Line'); libmei.AddAttribute(line,'form','solid'); } case ('vibrato') { - line = libmei.Line(); + line = GenerateControlEvent(bobj, 'Line'); libmei.AddAttribute(line, 'type', 'vibrato'); libmei.AddAttribute(line, 'form', 'wavy'); libmei.AddAttribute(line, 'place', 'above'); @@ -1654,26 +1663,18 @@ function GenerateLine (bobj) { //To catch diverse line types, set a default default { - line = libmei.Line(); + line = GenerateControlEvent(bobj, 'Line'); } } } } - if (line = null) - { - return null; - } - - line = AddBarObjectInfoToElement(bobj, line); - return line; } //$end function GenerateArpeggio (bobj) { //$module(ExportGenerators.mss) - arpeg = libmei.Arpeg(); orientation = null; switch (bobj.Type) @@ -1711,6 +1712,8 @@ function GenerateArpeggio (bobj) { } } + arpeg = GenerateControlEvent(bobj, 'Arpeg'); + if (orientation = null) { libmei.AddAttribute(arpeg, 'arrow', 'false'); @@ -1729,8 +1732,6 @@ function GenerateArpeggio (bobj) { } } - arpeg = AddBarObjectInfoToElement(bobj, arpeg); - return arpeg; } //$end @@ -1740,7 +1741,7 @@ function GenerateTrill (bobj) { /* There are two types of trills in Sibelius: A line object and a symbol object. This method normalizes both of these. */ - trill = libmei.Trill(); + trill = GenerateControlEvent(bobj, 'Trill'); bar = bobj.ParentBar; obj = GetNoteObjectAtPosition(bobj); @@ -1749,68 +1750,19 @@ function GenerateTrill (bobj) { libmei.AddAttribute(trill, 'startid', '#' & obj._id); } - trill = AddBarObjectInfoToElement(bobj, trill); - return trill; } //$end -function GenerateFermata (bobj) { +function GenerateFermata (bobj, shape, form) { //$module(ExportGenerators.mss) - /* Note rests can have multiple fermatas in Sibelius, - but this is currently not supported. - Also, fermatas added as symbols are not yet handled. - */ - shape = null; - - switch (bobj.Type) - { - case('NoteRest') - { - if (bobj.GetArticulation(PauseArtic)) - { - shape = 'curved'; - } - if (bobj.GetArticulation(TriPauseArtic)) - { - shape = 'angular'; - } - if (bobj.GetArticulation(SquarePauseArtic)) - { - shape = 'square'; - } - } - case('BarRest') - { - switch (bobj.PauseType) - { - case(PauseTypeRound) - { - shape = 'curved'; - } - case(PauseTypeTriangular) - { - shape = 'angular'; - } - case(PauseTypeSquare) - { - shape = 'square'; - } - } - } - } - - if (shape = null) - { - return null; - } - - fermata = libmei.Fermata(); + fermata = GenerateControlEvent(bobj, 'Fermata'); - libmei.AddAttribute(fermata, 'form', 'norm'); + libmei.AddAttribute(fermata, 'form', form); libmei.AddAttribute(fermata, 'shape', shape); - fermata = AddBarObjectInfoToElement(bobj, fermata); + measureObjs = Self._property:MeasureObjects; + measureObjs.Push(fermata._id); return fermata; } //$end @@ -1829,247 +1781,6 @@ function GenerateChordSymbol (bobj) { return harm; } //$end -function GenerateFormattedString (bobj) { - //$module(ExportGenerators.mss) - /* - Returns an array containing at least one paragraph - tag, formatted with the element. - - Multiple paragraph tags may be returned if the formatting string contains - a '\n\' (new paragraph) - */ - - FORMATOPEN = 1; - FORMATCLOSE = 2; - FORMATTAG = 3; - FORMATINFO = 4; - TEXTSTR = 5; - - // initialize context as a text string, since we may not always open with a formatting tag. - ctx = TEXTSTR; - tag = null; - activeinfo = ''; - activetext = ''; - - ret = CreateSparseArray(); - activeDiv = libmei.Div(); - activePara = libmei.P(); - libmei.AddChild(activeDiv, activePara); - ret.Push(activeDiv); - - text = bobj.TextWithFormattingAsString; - - if (text = '') - { - return ret; - } - - for i = 0 to Length(text) - { - c = CharAt(text, i); - - if (c = '\\') - { - if (ctx = FORMATINFO or ctx = FORMATTAG) - { - /* - If we have an open format context or - we are looking at format info and see - a slash, we are closing the format context - */ - ctx = FORMATCLOSE; - } - else - { - if (ctx = TEXTSTR or ctx = FORMATCLOSE) - { - /* If we have a slash we are either switching - into a new formatting tag context - or we are opening a new formatting tag - immediately after closing one. - */ - ctx = FORMATOPEN; - } - } - } - else - { - switch (ctx) - { - case (FORMATOPEN) - { - // the previous iteration gave us an opening - // formatting string, so the next character is - // the formatting tag - ctx = FORMATTAG; - } - - case (FORMATTAG) - { - /* - After seeing a tag we will expect to find some - info. If there is no info, the next character will - be a \ and it will be caught above. - */ - ctx = FORMATINFO; - activeinfo = activeinfo & c; - } - - case (FORMATINFO) - { - // keep appending the active info until - // we reach the end. - activeinfo = activeinfo & c; - } - - case (FORMATCLOSE) - { - // the previous context was a closing format tag, - // so the next character, if it is not another opening - // tag, is a text string. Assume it is a text string - // which will be corrected on the next go-round. - ctx = TEXTSTR; - activetext = activetext & c; - } - - case (TEXTSTR) - { - activetext = activetext & c; - } - } - } - - // now that we have figured out what context we are in, we - // can do something about it. - switch (ctx) - { - case (FORMATTAG) - { - tag = c; - - if (tag = 'n') - { - if (activetext != '') - { - libmei.SetText(activePara, activetext); - activetext = ''; - } - - activePara = libmei.P(); - libmei.AddChild(activeDiv, activePara); - } - - if (tag = 'N') - { - if (activetext != '') - { - children = activePara.children; - - if (children.Length > 0) - { - lastLbId = children[-1]; - lastLb = libmei.getElementById(lastLbId); - libmei.SetTail(lastLb, activetext); - } - else - { - libmei.SetText(activePara, activetext); - } - } - activetext = ''; - - lb = libmei.Lb(); - libmei.AddChild(activePara, lb); - } - } - - case (TEXTSTR) - { - ; - } - - case (FORMATOPEN) - { - // if we have hit a new format opening tag and we have some previous text. - // if (activetext != '') - // { - // // we have some pending text that needs to be dealt with - // libmei.SetText(activePara, activetext); - // activetext = ''; - // } - ; - } - - case (FORMATCLOSE) - { - if (activeinfo != '') - { - // tags that have info. - switch (tag) - { - case ('s') - { - // our info block should contain units. - // Log('Units: ' & activeinfo); - ; - } - - case ('c') - { - // our info block should contain a style - // Log('Style: ' & activeinfo); - ; - } - - case ('f') - { - // our info block should either contain - // a font name or an underscore to switch - // back to the default font. - // Log('Font: ' & activeinfo); - ; - } - - case ('$') - { - // our info block should contain a substitution - // Log('Substitution: ' & activeinfo); - ; - } - } - - activeinfo = ''; - tag = ''; - } - } - default - { - // Log('default: ' & ctx); - ; - } - } - } - - // - if (ctx = TEXTSTR and activetext != '') - { - // if we end the text on a text string, append it to the active paragraph element. - children = activePara.children; - if (children.Length > 0) - { - lastLbId = children[-1]; - lastLb = libmei.getElementById(lastLbId); - libmei.SetTail(lastLb, activetext); - } - else - { - libmei.SetText(activePara, activetext); - } - } - - return ret; -} //$end - function GenerateSmuflAltsym (glyphnum, glyphname) { //$module(ExportGenerators.mss) if (Self._property:SmuflSymbolIds = null) @@ -2083,7 +1794,8 @@ function GenerateSmuflAltsym (glyphnum, glyphname) { if (Self._property:SymbolTable = null) { symbolTable = libmei.SymbolTable(); - libmei.AddChild(Self._property:MainScoreDef, symbolTable); + scoreDef = Self._property:MainScoreDef; + libmei.AddChildAtPosition(scoreDef, symbolTable, 0); Self._property:SymbolTable = symbolTable; } symbolTable = Self._property:SymbolTable; @@ -2097,6 +1809,10 @@ function GenerateSmuflAltsym (glyphnum, glyphname) { libmei.AddAttribute(symbol, 'glyph.auth', 'smufl'); libmei.AddAttribute(symbol, 'glyph.num', glyphnum); libmei.AddAttribute(symbol, 'glyph.name', glyphname); + // Add x/y attributes to satisfy some Schematron rules + libmei.AddAttribute(symbol, 'x', '0'); + libmei.AddAttribute(symbol, 'y', '0'); + symbolIds[glyphnum] = symbolDef._id; } diff --git a/src/ExportProcessors.mss b/src/ExportProcessors.mss index cf5899d..937d02f 100644 --- a/src/ExportProcessors.mss +++ b/src/ExportProcessors.mss @@ -406,10 +406,9 @@ function ProcessFrontMatter (bobj) { libmei.AddAttribute(pb, 'n', pnum); frontmatter[pnum] = CreateSparseArray(pb); } - pagematter = frontmatter[pnum]; - text = GenerateFormattedString(bobj); - frontmatter[pnum] = pagematter.Concat(text); + text = AddFormattedText(libmei.Div(), bobj); + frontmatter[pnum].Push(text); } //$end @@ -474,366 +473,6 @@ function ProcessVolta (mnum) { return null; } //$end -function ProcessTremolo (bobj) { - //$module(ExportProcessors.mss) - if (bobj.DoubleTremolos = 0) - { - return null; - } - - Log('Fingered tremolo: ' & bobj.DoubleTremolos); - tremEl = libmei.FTrem(); - libmei.AddAttribute(tremEl, 'beams', bobj.DoubleTremolos); - libmei.AddAttribute(tremEl, 'unitdur'); - -} //$end - -function ProcessSymbol (sobj) { - //$module(ExportProcessors.mss) - Log('symbol index: ' & sobj.Index & ' name: ' & sobj.Name); - Log(sobj.VoiceNumber); - voicenum = sobj.VoiceNumber; - bar = sobj.ParentBar; - - if (voicenum = 0) - { - // assign it to the first voice, since we don't have any notes in voice/layer 0. - sobj.VoiceNumber = 1; - warnings = Self._property:warnings; - warnings.Push(utils.Format(_ObjectAssignedToAllVoicesWarning, bar.BarNumber, voicenum, 'Symbol')); - } - - switch (sobj.Index) - { - case ('32') - { - // trill - trill = GenerateTrill(sobj); - mlines = Self._property:MeasureObjects; - mlines.Push(trill._id); - } - - case ('36') - { - // inverted mordent - mordent = libmei.Mordent(); - libmei.AddAttribute(mordent, 'form', 'lower'); - mordent = AddBarObjectInfoToElement(sobj, mordent); - mlines = Self._property:MeasureObjects; - mlines.Push(mordent._id); - } - - case ('37') - { - // mordent - mordent = libmei.Mordent(); - libmei.AddAttribute(mordent, 'form', 'upper'); - mordent = AddBarObjectInfoToElement(sobj, mordent); - mlines = Self._property:MeasureObjects; - mlines.Push(mordent._id); - } - - case ('38') - { - // turn - turn = libmei.Turn(); - libmei.AddAttribute(turn, 'form', 'upper'); - turn = AddBarObjectInfoToElement(sobj, turn); - mlines = Self._property:MeasureObjects; - mlines.Push(turn._id); - } - - case ('39') - { - // inverted turn - turn = libmei.Turn(); - libmei.AddAttribute(turn, 'form', 'lower'); - turn = AddBarObjectInfoToElement(sobj, turn); - mlines = Self._property:MeasureObjects; - mlines.Push(turn._id); - } - case ('52') - { - nobj = GetNoteObjectAtPosition(sobj); - - if (nobj != null) - { - artic = libmei.Artic(); - libmei.AddAttribute(artic, 'artic', 'heel'); - libmei.AddChild(nobj, artic); - } - } - case ('53') - { - nobj = GetNoteObjectAtPosition(sobj); - if (nobj != null) - { - artic = libmei.Artic(); - libmei.AddAttribute(artic, 'artic', 'heel'); - libmei.AddChild(nobj, artic); - } - - } - case ('54') - { - nobj = GetNoteObjectAtPosition(sobj); - if (nobj != null) - { - artic = libmei.Artic(); - libmei.AddAttribute(artic, 'artic', 'toe'); - libmei.AddChild(nobj, artic); - } - - } - case ('55') - { - nobj = GetNoteObjectAtPosition(sobj); - if (nobj != null) - { - artic = libmei.Artic(); - libmei.AddAttribute(artic, 'artic', 'toe'); - libmei.AddChild(nobj, artic); - } - - } - case ('160') - { - nobj = GetNoteObjectAtPosition(sobj); - if (nobj != null) - { - artic = libmei.Artic(); - libmei.AddAttribute(artic, 'artic', 'stop'); - libmei.AddChild(nobj, artic); - } - } - case ('162') - { - nobj = GetNoteObjectAtPosition(sobj); - if (nobj != null) - { - artic = libmei.Artic(); - libmei.AddAttribute(artic, 'artic', 'open'); - libmei.AddChild(nobj, artic); - } - } - case ('163') - { - nobj = GetNoteObjectAtPosition(sobj); - if (nobj != null) - { - artic = libmei.Artic(); - libmei.AddAttribute(artic, 'artic', 'damp'); - libmei.AddChild(nobj, artic); - } - } - case ('164') - { - nobj = GetNoteObjectAtPosition(sobj); - if (nobj != null) - { - artic = libmei.Artic(); - libmei.AddAttribute(artic, 'artic', 'damp'); - libmei.AddChild(nobj, artic); - } - - } - case ('165') - { - nobj = GetNoteObjectAtPosition(sobj); - if (nobj != null) - { - artic = libmei.Artic(); - libmei.AddAttribute(artic, 'artic', 'damp'); - libmei.AddChild(nobj, artic); - } - } - case ('166') - { - nobj = GetNoteObjectAtPosition(sobj); - if (nobj != null) - { - artic = libmei.Artic(); - libmei.AddAttribute(artic, 'artic', 'damp'); - libmei.AddChild(nobj, artic); - } - - } - case ('212') - { - nobj = GetNoteObjectAtPosition(sobj); - if (nobj != null) - { - artic = libmei.Artic(); - libmei.AddAttribute(artic, 'artic', 'ten'); - libmei.AddAttribute(artic, 'place', 'above'); - libmei.AddChild(nobj, artic); - } - } - case ('214') - { - nobj = GetNoteObjectAtPosition(sobj); - - if (nobj != null) - { - artic = libmei.Artic(); - libmei.AddAttribute(artic, 'artic', 'marc'); - libmei.AddAttribute(artic, 'place', 'above'); - libmei.AddChild(nobj, artic); - } - } - case ('217') - { - // up-bow above - nobj = GetNoteObjectAtPosition(sobj); - - if (nobj != null) - { - artic = libmei.Artic(); - libmei.AddAttribute(artic, 'artic', 'upbow'); - libmei.AddAttribute(artic, 'place', 'above'); - libmei.AddChild(nobj, artic); - } - } - case ('218') - { - // down-bow above - nobj = GetNoteObjectAtPosition(sobj); - - if (nobj != null) - { - artic = libmei.Artic(); - libmei.AddAttribute(artic, 'artic', 'dnbow'); - libmei.AddAttribute(artic, 'place', 'above'); - libmei.AddChild(nobj, artic); - } - - } - case ('233') - { - // up-bow below - nobj = GetNoteObjectAtPosition(sobj); - - if (nobj != null) - { - artic = libmei.Artic(); - libmei.AddAttribute(artic, 'artic', 'upbow'); - libmei.AddAttribute(artic, 'place', 'below'); - libmei.AddChild(nobj, artic); - } - } - case ('234') - { - // down-bow below - nobj = GetNoteObjectAtPosition(sobj); - - if (nobj != null) - { - artic = libmei.Artic(); - libmei.AddAttribute(artic, 'artic', 'dnbow'); - libmei.AddAttribute(artic, 'place', 'below'); - libmei.AddChild(nobj, artic); - } - else - { - warnings = Self._property:warnings; - warnings.Push(utils.Format(_ObjectCouldNotFindAttachment, bar.BarNumber, voicenum, sobj.Name)); - } - } - case ('240') - { - // double staccato - return null; - } - case ('241') - { - // triple staccato - return null; - } - case ('242') - { - // quadruple staccato - return null; - } - case ('243') - { - // snap - nobj = GetNoteObjectAtPosition(sobj); - - if (nobj != null) - { - artic = libmei.Artic(); - libmei.AddAttributeValue(artic, 'artic', 'snap'); - libmei.AddChild(nobj, artic); - } - else - { - warnings = Self._property:warnings; - warnings.Push(utils.Format(_ObjectCouldNotFindAttachment, bar.BarNumber, voicenum, sobj.Name)); - } - } - case ('480') - { - //scoop - nobj = GetNoteObjectAtPosition(sobj); - if (nobj != null) - { - artic = libmei.Artic(); - libmei.AddAttributeValue(artic, 'artic', 'scoop'); - libmei.AddChild(nobj, artic); - } - } - case ('481') - { - //fall - nobj = GetNoteObjectAtPosition(sobj); - if (nobj != null) - { - artic = libmei.Artic(); - libmei.AddAttributeValue(artic, 'artic', 'fall'); - libmei.AddChild(nobj, artic); - } - - } - case ('490') - { - nobj = GetNoteObjectAtPosition(sobj); - - if (nobj != null) - { - artic = libmei.Artic(); - libmei.AddAttributeValue(artic, 'artic', 'fingernail'); - libmei.AddChild(nobj, artic); - } - } - case ('494') - { - //doit - nobj = GetNoteObjectAtPosition(sobj); - if (nobj != null) - { - artic = libmei.Artic(); - libmei.AddAttributeValue(artic, 'artic', 'doit'); - libmei.AddChild(nobj, artic); - } - - } - case ('495') - { - //plop - nobj = GetNoteObjectAtPosition(sobj); - - if (nobj != null) - { - artic = libmei.Artic(); - libmei.AddAttributeValue(artic, 'artic', 'plop'); - libmei.AddChild(nobj, artic); - } - - } - - } -} //$end - function ProcessEndingSlurs (bar) { //$module(ExportProcessors.mss) slur_resolver = Self._property:SlurResolver; diff --git a/src/Extensions.mss b/src/Extensions.mss new file mode 100644 index 0000000..b4c33b0 --- /dev/null +++ b/src/Extensions.mss @@ -0,0 +1,184 @@ +function RegisterAvailableExtensions (availableExtensions) { + //$module(Initialize.mss) + // Expects and empty TreeNode Hash map object as argument. + // Looks for existing extensions and registers them in this Hash map. + // Keys in the Hash map are the names by which the extension plugins can be + // referenced in ManuScript, e.g. like `@name.SibmeiExtensionAPIVersion`. + // Values are the full names that are displayed to the user. + + apiSemver = SplitString(ExtensionAPIVersion, '.'); + errors = ''; + + for each pluginObject in Sibelius.Plugins + { + if (pluginObject.DataExists('SibmeiExtensionAPIVersion')) + { + + plgName = pluginObject.File.NameNoPath; + extensionSemver = SplitString(@plgName.SibmeiExtensionAPIVersion, '.'); + + switch (true) + { + case (extensionSemver.NumChildren != 3) + { + error = 'Extension %s must have a valid semantic versioning string in field `ExtensionAPIVersion`'; + } + case ((apiSemver[0] = extensionSemver[0]) and (apiSemver[1] >= extensionSemver[1])) + { + error = null; + } + case ((apiSemver[0] < extensionSemver[0]) or (apiSemver[1] < extensionSemver[1])) + { + error = 'Extension %s requires Sibmei to be updated to a newer version'; + } + default + { + error = 'Extension %s needs to be updated to be compatible with the current Sibmei version'; + } + } + + if (null = error) + { + // Storing key/value pairs in old-style Hash TreeNodes needs @-indirection + availableExtensions.@plgName = pluginObject.Name; + } + else + { + errors = errors & utils.Format(error, plgName) & '\n'; + } + } + } + + return errors; +} //$end + + +function ChooseExtensions (availableExtensions, chosenExtensions) { + // Expects an empty Dictionary as second argument. + // Runs the ExtensionDialog and stores all extensions the user chose in the + // Dictionary. Adds key/value pairs in the same way as + // RegisterAvailableExtensions() + // Returns true on success, false if the user canceled the dialog. + + if (not Sibelius.ShowDialog(ExtensionDialog, Self)) + { + return false; + } + // Unfortunately, SelectedExtensions only has the selected values from the + // AvailableExtensions object, i.e. the extension plugin's full user facing + // name, not the PLG name that we need to reference it in ManuScript. + extensionIsSelected = CreateDictionary(); + for each fullExtensionName in SelectedExtensions + { + extensionIsSelected[fullExtensionName] = true; + } + for each extension in AvailableExtensions + { + // Cast TreeNode Hash object to its string value + fullExtensionName = extension & ''; + if (extensionIsSelected[extension]) + { + plgName = extension.Label; + chosenExtensions[plgName] = fullExtensionName; + } + } + + return true; +} //$end + + +function SelectAllExtensions () { + SelectedExtensions = AvailableExtensions; + Sibelius.RefreshDialog(); +} //$end + + +function DeselectAllExtensions () { + SelectedExtensions = CreateHash(); + Sibelius.RefreshDialog(); +} //$end + + +function InitExtensions (extensions) { + // To let the user choose extensions via dialog, pass `null` as argument. + // If extensions should be activated without showing the dialog, pass a + // SparseArray with the 'PLG names' of the extensions, i.e. the names that + // `RegisterAvailableExtensions()` will use as keys. This is useful e.g. + // for running tests without requiring user interaction. + // + // Returns false if the user aborted the selection of extensions or if there + // are any errors, otherwise returns true. + + AvailableExtensions = CreateHash(); + errors = RegisterAvailableExtensions(AvailableExtensions); + if (null != errors) + { + Sibelius.MessageBox(errors); + return false; + } + + chosenExtensions = CreateDictionary(); + if (null = extensions) + { + if (not ChooseExtensions(AvailableExtensions, chosenExtensions)) + { + return false; + } + } else { + for each plgName in extensions + { + // Attention, choose AvailableExtensions with .@ + chosenExtensions[plgName] = AvailableExtensions.@plgName; + } + } + + apiObject = CreateApiObject(); + + for each Name plgName in chosenExtensions + { + @plgName.InitSibmeiExtension(apiObject); + } + + // store chosenExtensions as global to add application info + Self._property:ChosenExtensions = chosenExtensions; + + return true; +} //$end + + +function CreateApiObject () { + apiObject = CreateDictionary('libmei', libmei); + apiObject.SetMethod('RegisterSymbolHandlers', Self, 'ExtensionAPI_RegisterSymbolHandlers'); + apiObject.SetMethod('RegisterTextHandlers', Self, 'ExtensionAPI_RegisterTextHandlers'); + apiObject.SetMethod('MeiFactory', Self, 'ExtensionAPI_MeiFactory'); + apiObject.SetMethod('HandleControlEvent', Self, 'HandleControlEvent'); + apiObject.SetMethod('HandleModifier', Self, 'HandleModifier'); + apiObject.SetMethod('AddFormattedText', Self, 'ExtensionAPI_AddFormattedText'); + apiObject.SetMethod('GenerateControlEvent', Self, 'ExtensionAPI_GenerateControlEvent'); + apiObject.SetMethod('AddControlEventAttributes', Self, 'ExtensionAPI_AddControlEventAttributes'); + return apiObject; +} //$end + +function ExtensionAPI_RegisterSymbolHandlers (this, symbolHandlerDict, plugin) { + RegisterHandlers(Self._property:SymbolHandlers, symbolHandlerDict, plugin); +} //$end + +function ExtensionAPI_RegisterTextHandlers (this, textHandlerDict, plugin) { + RegisterHandlers(Self._property:TextHandlers, textHandlerDict, plugin); +} //$end + +function ExtensionAPI_MeiFactory (this, templateObject) { + MeiFactory(templateObject); +} //$end + +function ExtensionAPI_AddFormattedText (this, parentElement, textObj) { + AddFormattedText (parentElement, textObj); +} //$end + +function ExtensionAPI_GenerateControlEvent (this, bobj, elementName) { + GenerateControlEvent(bobj, elementName); +} //$end + +function ExtensionAPI_AddControlEventAttributes (this, bobj) { + AddControlEventAttributes(bobj); +} //$end diff --git a/src/GLOBALS.mss b/src/GLOBALS.mss index 98bb255..0336788 100644 --- a/src/GLOBALS.mss +++ b/src/GLOBALS.mss @@ -1,6 +1,7 @@ Version "4.0.0" PluginName "Sibelius to MEI 4 Exporter" Author "Andrew Hankinson" +ExtensionAPIVersion "1.1.0" _InitialProgressTitle "Exporting %s to MEI" _ExportFileIsNull "You must specify a file to save." @@ -16,3 +17,6 @@ _ObjectIsOnAnIllogicalObject "Bar %s, voice %s. %s is added to a %s object. This _ObjectCouldNotFindAttachment "Bar %s, voice %s. %s could not be attached to a Note object, so it will not appear in the output." LOGFILE "sibelius.log" + +AvailableExtensions +SelectedExtensions diff --git a/src/Initialize.mss b/src/Initialize.mss index 77bd623..07e971e 100644 --- a/src/Initialize.mss +++ b/src/Initialize.mss @@ -1,10 +1,76 @@ function Initialize() { + //$module(Initialize.mss) Self._property:Logfile = GetTempDir() & LOGFILE; + AddToPluginsMenu(PluginName,'Run'); +} //$end + + +function InitGlobals (extensions) { + //$module(Initialize.mss) + + // `extensions` can be null or a SparseArray. See `InitExtensions()` for + // more detailed information. + + // initialize libmei as soon as possible + Self._property:libmei = libmei4; + if (Sibelius.FileExists(Self._property:Logfile) = False) { Sibelius.CreateTextFile(Self._property:Logfile); } - AddToPluginsMenu(PluginName,'Run'); + Self._property:TypeHasEndBarNumberProperty = CreateDictionary( + // We omit 'ArpeggioLine'. It technically has an EndBarNumber property, + // but Sibelius does not allow creating an Arpeggio with a Duration + // other than 0, which means the EndBarNumber is always the same as the + // start bar number. + 'BeamLine', true, + 'Bend', true, + 'Box', true, + 'CrescendoLine', true, + 'DiminuendoLine', true, + 'GlissandoLine', true, + 'Line', true, + 'OctavaLine', true, + 'PedalLine', true, + 'RepeatTimeLine', true, + 'RitardLine', true, + 'Slur', true, + 'Trill', true + ); + + // Initialize symbol styles + Self._property:SymbolHandlers = InitSymbolHandlers(); + Self._property:SymbolMap = InitSymbolMap(); + Self._property:TextHandlers = InitTextHandlers(); + Self._property:TextSubstituteMap = InitTextSubstituteMap(); + + if (not InitExtensions(extensions)) + { + return false; + } + + Self._property:_Initialized = true; + + return true; } //$end + +function RegisterHandlers(handlers, handlerDefinitions, plugin) { + //$module(Initialize.mss) + // Text handlers can be registered by 'idType' StyleId or StyleAsText + // Symbol handlers can be registered by 'idType' Index or Name + + for each Name idType in handlers + { + if (null != handlerDefinitions[idType]) + { + handle = handlers[idType]; + for each Name id in handlerDefinitions[idType] + { + handle.SetMethod(id, plugin, handlerDefinitions[idType].@id); + } + } + } + +} //$end diff --git a/src/Run.mss b/src/Run.mss index 24e9643..6395215 100644 --- a/src/Run.mss +++ b/src/Run.mss @@ -17,13 +17,19 @@ function Run() { // get the active score object activeScore = Sibelius.ActiveScore; - // it does not seem possible to get the current folder for the file - // so we will default to the user's documents folder. - // NB: it seems that if we don't specify a folder name, the filename - // is not properly set. - activeFileNameFull = activeScore.FileName; - activeFileName = utils.ExtractFileName(activeFileNameFull); - activePath = Sibelius.GetDocumentsFolder(); + if (Sibelius.FileExists(activeScore.FileName)) { + scoreFile = Sibelius.GetFile(activeScore.FileName); + activeFileName = scoreFile.NameNoPath & '.mei'; + activePath = scoreFile.Path; + } else { + activeFileName = 'untitled.mei'; + activePath = Sibelius.GetDocumentsFolder(); + } + + if (not InitGlobals(null)) + { + return false; + } // Ask to the file to be saved somewhere filename = Sibelius.SelectFileToSave('Save as...', activeFileName, activePath, 'mei', 'TEXT', 'Music Encoding Initiative'); @@ -41,8 +47,14 @@ function Run() { function DoExport (filename) { //$module(Run.mss) + if (not Self._property:_Initialized) + { + Trace('InitGlobals() must be called before running DoExport()'); + return null; + } + // first, ensure we're running with a clean slate. - Self._property:libmei = libmei4; + // (initialization of libmei has moved to InitGlobals()) libmei.destroy(); // set the active score here so we can refer to it throughout the plugin diff --git a/src/SymbolHandler.mss b/src/SymbolHandler.mss new file mode 100644 index 0000000..57ce0d0 --- /dev/null +++ b/src/SymbolHandler.mss @@ -0,0 +1,178 @@ +// TODO: Missing symbols +// '240' double staccato +// '241' tripe staccato +// '242' quadruple staccato + +function InitSymbolHandlers () { + //$module(SymbolHandler.mss) + + symbolHandlers = CreateDictionary( + 'Index', CreateDictionary(), + 'Name', CreateDictionary() + ); + + RegisterHandlers(symbolHandlers, CreateDictionary( + 'Index', CreateDictionary( + '36', 'HandleControlEvent', //inverted mordent + '37', 'HandleControlEvent', //mordent + '38', 'HandleControlEvent', //turn + '39', 'HandleControlEvent', //inverted turn + '52', 'HandleModifier', //heel + '53', 'HandleModifier', //heel (2) (was toe in previous version, but this seems to be wrong) + '54', 'HandleModifier', //toe + '160', 'HandleModifier', //stop + '162', 'HandleModifier', //open + '163', 'HandleModifier', //damp + '164', 'HandleModifier', //damp (2) + '165', 'HandleModifier', //damp (3) + '166', 'HandleModifier', //damp (4) + '212', 'HandleModifier', //ten above + '214', 'HandleModifier', //marc above + '217', 'HandleModifier', //upbow above + '218', 'HandleModifier', //dnbow above + '220', 'HandleControlEvent', // square fermata above + '221', 'HandleControlEvent', // round fermata above + '222', 'HandleControlEvent', // triangular fermata above + '236', 'HandleControlEvent', // square fermata below + '237', 'HandleControlEvent', // round fermata below + '238', 'HandleControlEvent', // triangular fermata below + '233', 'HandleModifier', //upbow below + '234', 'HandleModifier', //dnbow below + '243', 'HandleModifier', //snap + '480', 'HandleModifier', //scoop + '481', 'HandleModifier', //fall + '490', 'HandleModifier', //fingernail + '494', 'HandleModifier', //doit + '495', 'HandleModifier' //plop + ) + // 'Name', CreateDictionary( + // 'Pedal', 'HandleControlEvent' + // ) + ), Self); + + return symbolHandlers; + +}//$end + +function InitSymbolMap () { + //$module(SymbolHandler.mss) + // Create a dictionary with symbol index number as key (sobj.Index) and a value that determines the element that has to be created + // 0th element in SparseArray is the element name as function call + // see for further instructions Utilities/MeiFactory() + + return CreateDictionary( + '36', CreateSparseArray('Mordent', CreateDictionary('form', 'lower')), //inverted mordent + '37', CreateSparseArray('Mordent', CreateDictionary('form','upper')), //mordent + '38', CreateSparseArray('Turn', CreateDictionary('form', 'upper')), //turn + '39', CreateSparseArray('Turn', CreateDictionary('form', 'lower')), //inverted turn + '52', CreateSparseArray('Artic', CreateDictionary('artic','heel')), //heel + '53', CreateSparseArray('Artic', CreateDictionary('artic','heel')), //heel (2) (was toe in previous version, but this seems to be wrong) + '54', CreateSparseArray('Artic', CreateDictionary('artic','toe')), //toe + '160', CreateSparseArray('Artic', CreateDictionary('artic','stop')), //stop + '162', CreateSparseArray('Artic', CreateDictionary('artic','open')), //open + '163', CreateSparseArray('Artic', CreateDictionary('artic','damp')), //damp + '164', CreateSparseArray('Artic', CreateDictionary('artic','damp')), //damp (2) + '165', CreateSparseArray('Artic', CreateDictionary('artic','damp')), //damp (3) + '166', CreateSparseArray('Artic', CreateDictionary('artic','damp')), //damp (4) + '212', CreateSparseArray('Artic', CreateDictionary('artic','ten', 'place','above')), //ten above + '214', CreateSparseArray('Artic', CreateDictionary('artic','marc', 'place','above')), //marc above + '217', CreateSparseArray('Artic', CreateDictionary('artic','upbow', 'place','above')), //upbow above + '218', CreateSparseArray('Artic', CreateDictionary('artic','dnbow', 'place','above')), //dnbow above + '220', CreateSparseArray('Fermata', CreateDictionary('shape', 'square', 'form', 'norm')), // square fermata above + '221', CreateSparseArray('Fermata', CreateDictionary('shape', 'curved', 'form', 'norm')), // round fermata above + '222', CreateSparseArray('Fermata', CreateDictionary('shape', 'angular', 'form', 'norm')), // triangular fermata above + '236', CreateSparseArray('Fermata', CreateDictionary('shape', 'square', 'form', 'inv')), // square fermata below + '237', CreateSparseArray('Fermata', CreateDictionary('shape', 'curved', 'form', 'inv')), // round fermata below + '238', CreateSparseArray('Fermata', CreateDictionary('shape', 'angular', 'form', 'inv')), // triangular fermata below + '233', CreateSparseArray('Artic', CreateDictionary('artic','upbow', 'place','below')), //upbow below + '234', CreateSparseArray('Artic', CreateDictionary('artic','dnbow', 'place','below')), //dnbow below + '243', CreateSparseArray('Artic', CreateDictionary('artic','snap')), //snap + '480', CreateSparseArray('Artic', CreateDictionary('artic','scoop')), //scoop + '481', CreateSparseArray('Artic', CreateDictionary('artic','fall')), //fall + '490', CreateSparseArray('Artic', CreateDictionary('artic','fingernail')), //fingernail + '494', CreateSparseArray('Artic', CreateDictionary('artic','doit')), //doit + '495', CreateSparseArray('Artic', CreateDictionary('artic','plop')) //plop + // 'Pedal', CreateSparseArray('Pedal', CreateDictionary('dir', 'down', 'func', 'sustain')) //Pedal + ); + +} //$end + + +function HandleSymbol (sobj) { + //$module(SymbolHandler.mss) + Log('symbol index: ' & sobj.Index & ' name: ' & sobj.Name); + Log(sobj.VoiceNumber); + voicenum = sobj.VoiceNumber; + bar = sobj.ParentBar; + + if (voicenum = 0) + { + // assign it to the first voice, since we don't have any notes in voice/layer 0. + sobj.VoiceNumber = 1; + warnings = Self._property:warnings; + warnings.Push(utils.Format(_ObjectAssignedToAllVoicesWarning, bar.BarNumber, voicenum, 'Symbol')); + } + + // trills are special + if (sobj.Index = '32') + { + // trill + trill = GenerateTrill(sobj); + mlines = Self._property:MeasureObjects; + mlines.Push(trill._id); + } + + // get SymbolIndexHandlers and SymbolIndexMap + symbolHandlers = Self._property:SymbolHandlers; + symbolMap = Self._property:SymbolMap; + + // look for symbol index in symbolHandlers.Index + if(symbolHandlers.Index.MethodExists(sobj.Index)) + { + symbId = sobj.Index; + symbolHandlers.Index.@symbId(sobj, symbolMap[symbId]); + } + else + { + // look for symbol name in symbolHandlers.Name + if(symbolHandlers.Name.MethodExists(sobj.Name)) + { + symbName = sobj.Name; + symbolHandlers.Name.@symbName(sobj, symbolMap[symbName]); + } + } + +} //$end + +function HandleModifier(this, sobj, template){ + //$module(SymbolHandler.mss) + + nobj = GetNoteObjectAtPosition(sobj); + + if (nobj != null) + { + modifier = MeiFactory(template); + libmei.AddChild(nobj, modifier); + return modifier; + } + else + { + warnings = Self._property:warnings; + barNum = sobj.ParentBar.BarNumber; + voiceNum = sobj.VoiceNumber; + warnings.Push(utils.Format(_ObjectCouldNotFindAttachment, barNum, voiceNum, sobj.Name)); + } +} //$end + +function HandleControlEvent(this, sobj, template){ + //$module(SymbolHandler.mss) + + symbol = MeiFactory(template); + + symbol = AddControlEventAttributes(sobj, symbol); + mlines = Self._property:MeasureObjects; + mlines.Push(symbol._id); + + return symbol; + +} //$end diff --git a/src/TextHandler.mss b/src/TextHandler.mss new file mode 100644 index 0000000..2cf055b --- /dev/null +++ b/src/TextHandler.mss @@ -0,0 +1,489 @@ +function InitTextHandlers() { + // QUESTION: We could also take an argument and throw all text handlers from + // extensions into the same dictionary + + textHandlers = CreateDictionary( + 'StyleId', CreateDictionary(), + 'StyleAsText', CreateDictionary() + ); + + RegisterHandlers(textHandlers, CreateDictionary( + 'StyleId', CreateDictionary( + 'text.staff.expression', 'ExpressionTextHandler', + 'text.system.page_aligned.title', 'PageTitleHandler', + 'text.system.page_aligned.subtitle', 'PageTitleHandler', + 'text.system.page_aligned.composer', 'PageComposerTextHandler', + 'text.system.tempo', 'TempoTextHandler', + 'text.staff.space.figuredbass', 'FiguredBassTextHandler', + 'text.staff.plain', 'CreateAnchoredText' + ) + ), Self); + + return textHandlers; +} //$end + +function InitTextSubstituteMap() { + return CreateDictionary( + 'Title', CreateSparseArray('Title'), + 'Subtitle', CreateSparseArray('Title', CreateDictionary('type', 'subordinate')), + // is only allowed on and , so use + // generic element + 'Dedication', CreateSparseArray('Seg', CreateDictionary('type', 'Dedication')), + // , , , and + // are only allowed in a few places, e.g. metadata or title pages. + // We therfore use more generic elements + 'Composer', CreateSparseArray('PersName', CreateDictionary('role', 'Composer')), + 'Arranger', CreateSparseArray('PersName', CreateDictionary('role', 'Arranger')), + 'Lyricist', CreateSparseArray('PersName', CreateDictionary('role', 'Lyricist')), + 'Artist', CreateSparseArray('PersName', CreateDictionary('role', 'Artist')), + // is only allowed on , so use generic element + 'Copyright', CreateSparseArray('Seg', CreateDictionary('type', 'Copyright')), + // is only allowed in a few places, so use generic element + // We don't even know if it's a person or an institution + 'Publisher', CreateSparseArray('Seg', CreateDictionary('type', 'Publisher')), + 'MoreInfo', CreateSparseArray('Seg', CreateDictionary('type', 'MoreInfo')), + 'PartName', CreateSparseArray('Seg', CreateDictionary('type', 'PartName')) + ); +} //$end + + +function HandleText (textObject) { + // Step through the different ID types ('StyleId' and 'StyleAsText') and + // check for text handlers for this type + textHandlers = Self._property:TextHandlers; + for each Name idType in textHandlers { + handlersForIdType = textHandlers.@idType; + idValue = textObject.@idType; + if(handlersForIdType.MethodExists(idValue)) + { + return handlersForIdType.@idValue(textObject); + } + } +} //$end + + +function ExpressionTextHandler (this, textObject) { + dynam = GenerateControlEvent(textObject, 'Dynam'); + AddFormattedText(dynam, textObject); + return dynam; +} //$end + + +function PageTitleHandler (this, textObject) { + anchoredText = libmei.AnchoredText(); + title = libmei.Title(); + if (textObject.StyleId = 'text.system.page_aligned.subtitle') + { + libmei.AddAttribute(title, 'type', 'subordinate'); + } + + libmei.AddChild(anchoredText, title); + AddFormattedText(title, textObject); + + return anchoredText; +} //$end + + +function PageComposerTextHandler (this, textObject) { + // 'text.system.page_aligned.composer' + anchoredText = libmei.AnchoredText(); + AddFormattedText(anchoredText, textObject); + return anchoredText; +} //$end + + +function TempoTextHandler (this, textObject) { + // 'text.system.tempo' + tempo = GenerateControlEvent(textObject, 'Tempo'); + AddFormattedText(tempo, textObject); + return tempo; +} //$end + + +function FiguredBassTextHandler (this, textObject) { + // 'text.staff.space.figuredbass' + harm = GenerateControlEvent(textObject, 'Harm'); + fb = libmei.Fb(); + libmei.AddChild(harm, fb); + ConvertFbFigures(fb, textObject); + return harm; +} //$end + + +function CreateAnchoredText (this, textObj) { + anchoredText = libmei.AnchoredText(); + AddFormattedText(anchoredText, textObj); + return anchoredText; +} //$end + + +function AddFormattedText (parentElement, textObj) { + textWithFormatting = textObj.TextWithFormatting; + if (textWithFormatting.NumChildren < 2 and CharAt(textWithFormatting[0], 0) != '\\') + { + if (parentElement.name = 'div') + { + p = libmei.P(); + libmei.SetText(p, textObj.Text); + libmei.AddChild(parentElement, p); + } + else + { + libmei.SetText(parentElement, textObj.Text); + } + return parentElement; + } + + nodes = CreateSparseArray(); + + state = CreateDictionary( + 'currentText', null, + 'rendAttributes', CreateDictionary(), + 'rendFlags', CreateDictionary(), + // TODO: Also track the active character style (mainly + // `\ctext.character.musictext\`, and custom styles) + 'nodes', nodes, + 'paragraphs', null + ); + + for each component in textObj.TextWithFormatting + { + switch (Substring(component, 0, 2)) + { + case ('\\n') + { + PushStyledText(state); + nodes.Push(libmei.Lb()); + } + case ('\\N') + { + PushStyledText(state); + // TODO: Add

if it is allowed within parentElement (use libmei.GetName()) + nodes.Push(libmei.Lb()); + } + case ('\\B') + { + SwitchTextStyle(state, 'fontweight', 'bold'); + } + case ('\\b') + { + SwitchTextStyle(state, 'fontweight', 'normal'); + } + case ('\\I') + { + SwitchTextStyle(state, 'fontstyle', 'italic'); + } + case ('\\i') + { + SwitchTextStyle(state, 'fontstyle', 'normal'); + } + case ('\\U') + { + SwitchTextStyle(state, 'rend', 'underline', true); + } + case ('\\u') + { + SwitchTextStyle(state, 'rend', 'underline', false); + } + case ('\\f') + { + SwitchFont(state, GetTextCommandArg(component)); + } + case ('\\c') + { + // TODO: Can we sensibly handle a character style change? The + // only built-in one seem to be `text.character.musictext`. We + // might want to allow Extensions to handle custom character + // styles. + ; + } + case ('\\s') + { + fontsize = ConvertOffsetsToMEI(GetTextCommandArg(component)); + SwitchTextStyle(state, 'fontsize', fontsize); + } + case ('\\v') + { + // Vertical scale change (vertical stretching). Probably not + // possible to handle + ; + } + case ('\\h') + { + // Horizontal scale change (horizontal stretching). Probably not + // possible to handle + ; + } + case ('\\t') + { + // Tracking. Probably not possible to handle. + ; + } + case ('\\p') + { + SwitchBaselineAdjust(state, GetTextCommandArg(component)); + } + case ('\\$') { + AppendTextSubstitute(state, GetTextCommandArg(component)); + } + case ('\\\\') + { + // According to the documentation, 'backslashes themselves are + // represented by \\ , to avoid conflicting with the above + // commands'. Though that does not seem to work, let's just + // assume it does in case Avid fixes this. + + // We strip one leading backspace. + state.currentText = state.currentText & Substring(component, 1); + } + default + { + // This is regular text + state.currentText = state.currentText & component; + } + } + } + + PushStyledText(state); + + nodeCount = nodes.Length; + precedingElement = null; + + nodeIndex = 0; + while (nodeIndex < nodeCount) + { + node = nodes[nodeIndex]; + if (IsObject(node)) + { + // We have an element + libmei.AddChild(parentElement, node); + precedingElement = node; + } + else + { + // We have a text node + text = node; + // If there are multiple adjacent text nodes, we need to join them + while (nodeIndex < nodeCount and not IsObject(nodes[nodeIndex + 1])) { + nodeIndex = nodeIndex + 1; + text = text & nodes[nodeIndex]; + } + + if (precedingElement = null) + { + libmei.SetText(parentElement, text); + } + else + { + libmei.SetTail(precedingElement, text); + } + } + nodeIndex = nodeIndex + 1; + } +} //$end + + +function NewTextParagraph (state) { + // TODO! + ; +} //$end + + +function GetTextCommandArg (command) { + // Remove leading part, e.g. the '\$' or '\s' and trailing '\' + return Substring(command, 2, Length(command) - 3); +} //$end + + +function SwitchBaselineAdjust (state, param) { + sup = (param = 'superscript'); + sub = (param = 'subscript'); + if (sup != state.rendFlags['sup'] or sub != state.rendFlags['sub']) { + // Style changed, push the previous text before changing the style + PushStyledText(state); + } + state.rendFlags['sup'] = sup; + state.rendFlags['sub'] = sub; +} //$end + + +function ResetTextStyles (state, infoOnly) { + // If `infoOnly` is `true`, does not make any changes, only tells us if + // there are re-settable styles + if (null != state.rendAttributes) + { + for each Name attName in state.rendAttributes + { + if (infoOnly and state.rendAttributes[attName] != null) + { + return true; + } + state.rendAttributes[attName] = null; + } + } + + if (null != state.rendFlags) + { + for each Name flagName in state.rendFlags + { + if (infoOnly and state.rendFlags[flagName]) + { + return true; + } + state.rendFlags[flagName] = false; + } + } + return false; +} //$end + + +function SwitchFont (state, fontName) { + if (fontName = '_') + { + // Before resetting the style, we have to add text preceding the '\f_\' + // style reset – but only if the style reset actually changes something. + if (ResetTextStyles(state, true)) + { + PushStyledText(state); + ResetTextStyles(state, false); + } + } + else + { + SwitchTextStyle(state, 'fontfam', fontName); + } +} //$end + + +function SwitchTextStyle (state, attName, value) { + if (state.rendAttributes[attName] != value) + { + // Style changes, so append current text before modifying style state + PushStyledText(state); + } + state.rendAttributes[attName] = value; +} //$end + + +function SwitchRendFlags (state, flagName, value) { + if (state.rendFlags[flagName] != value) + { + PushStyledText(state); + } + state.rendFlags[flagName] = value; +} //$end + + +function PushStyledText (state) { + if (state.currentText = '') + { + return null; + } + + styleAttributes = GetStyleAttributes(state); + if (null = styleAttributes) + { + // We attach unstyled text without wrapping it in + state.nodes.Push(state.currentText); + } + else + { + rend = libmei.Rend(); + for each Name attName in styleAttributes { + libmei.AddAttribute(rend, attName, styleAttributes[attName]); + } + libmei.SetText(rend, state.currentText); + state.nodes.Push(rend); + } + + state.currentText = ''; +} //$end + + +function GetStyleAttributes (state) { + rendAttributes = null; + + if (null != state.rendAttributes) + { + for each Name attName in state.rendAttributes + { + value = state.rendAttributes[attName]; + if (null != value) + { + if (null = rendAttributes) { + rendAttributes = CreateDictionary(); + } + rendAttributes[attName] = value; + } + } + } + + if (null != state.rendFlags) + { + rendAttValue = ''; + firstRendFlag = true; + for each Name flagName in state.rendFlags + { + flagActive = state.rendFlags[flagName]; + if (flagActive) + { + if (firstRendFlag = true) + { + rendAttValue = rendAttValue & flagName; + firstRendFlag = false; + } + else + { + rendAttValue = rendAttValue & ' ' & flagName; + } + } + } + if (rendAttValue != '') + { + if (null = rendAttributes) { + rendAttributes = CreateDictionary(); + } + rendAttributes['rend'] = rendAttValue; + } + } + + return rendAttributes; +} //$end + + +function AppendTextSubstitute (state, substituteName) { + score = Self._property:ActiveScore; + + textSubstituteInfo = TextSubstituteMap[substituteName]; + if (null = textSubstituteInfo) + { + // No known substitution. Sibelius renders those literally. + state.currentText = state.currentText & '\\$' & substituteName & '\\'; + return null; + } + + substitutedText = score.@substituteName; + if (substitutedText = '') { + return null; + } + + element = MeiFactory(textSubstituteInfo); + state.nodes.Push(element); + + styleAttributes = GetStyleAttributes(state); + rendElement = null; + if (null = styleAttributes) + { + libmei.SetText(element, substitutedText); + } + else + { + rendElement = libmei.Rend(); + libmei.AddChild(element, rendElement); + for each Name attName in styleAttributes + { + libmei.AddAttribute(rendElement, attName, styleAttributes[attName]); + } + libmei.SetText(rendElement, substitutedText); + } +} //$end diff --git a/src/Utilities.mss b/src/Utilities.mss index 80d44ff..5a1a6de 100644 --- a/src/Utilities.mss +++ b/src/Utilities.mss @@ -219,10 +219,16 @@ function GetNoteObjectAtPosition (bobj) { // If one isn't found exactly at the end position, it will first look back (previous) // and then look forward, for candidate objects. + voice_num = bobj.VoiceNumber; + if (voice_num = 0) + { + // Things like titles or composer text needn't/shouldn't be attached to + // voices or notes. + return null; + } objectPositions = Self._property:ObjectPositions; staff_num = bobj.ParentBar.ParentStaff.StaffNum; bar_num = bobj.ParentBar.BarNumber; - voice_num = bobj.VoiceNumber; staffObjectPositions = objectPositions[staff_num]; barObjectPositions = staffObjectPositions[bar_num]; @@ -271,7 +277,7 @@ function GetNoteObjectAtPosition (bobj) { return null; } //$end -function AddBarObjectInfoToElement (bobj, element) { +function AddControlEventAttributes (bobj, element) { //$module(Utilities.mss) /* adds timing and position info (tstamps, etc.) to an element. @@ -290,51 +296,26 @@ function AddBarObjectInfoToElement (bobj, element) { libmei.AddAttribute(element, 'tstamp', ConvertPositionToTimestamp(bobj.Position, bar)); - switch (bobj.Type) + start_obj = GetNoteObjectAtPosition(bobj); + if (start_obj != null) { - case('SymbolItem') - { - start_obj = GetNoteObjectAtPosition(bobj); - if (start_obj != null) - { - libmei.AddAttribute(element, 'startid', '#' & start_obj._id); - } - } - case('NoteRest') - { - start_obj = GetNoteObjectAtPosition(bobj); - if (start_obj != null) - { - libmei.AddAttribute(element, 'startid', '#' & start_obj._id); - } - } - case('ArpeggioLine') - { - start_obj = GetNoteObjectAtPosition(bobj); - if (start_obj != null) - { - libmei.AddAttribute(element, 'startid', '#' & start_obj._id); - } - } - // at default add tstamp2 and try to find startid and endid - default - { - libmei.AddAttribute(element, 'tstamp2', ConvertPositionWithDurationToTimestamp(bobj)); - start_obj = GetNoteObjectAtPosition(bobj); - end_obj = GetNoteObjectAtEndPosition(bobj); - if (start_obj != null) - { - libmei.AddAttribute(element, 'startid', '#' & start_obj._id); - } + libmei.AddAttribute(element, 'startid', '#' & start_obj._id); + } - if (end_obj != null) - { - libmei.AddAttribute(element, 'endid', '#' & end_obj._id); - } + if (TypeHasEndBarNumberProperty[bobj.Type]) { + libmei.AddAttribute(element, 'tstamp2', ConvertPositionWithDurationToTimestamp(bobj)); + end_obj = GetNoteObjectAtEndPosition(bobj); + if (end_obj != null) + { + libmei.AddAttribute(element, 'endid', '#' & end_obj._id); } } - libmei.AddAttribute(element, 'staff', bar.ParentStaff.StaffNum); + if (bar.ParentStaff.StaffNum > 0) + { + // Only add @staff if this is not attached to the SystemStaff + libmei.AddAttribute(element, 'staff', bar.ParentStaff.StaffNum); + } libmei.AddAttribute(element, 'layer', voicenum); if (bobj.Type = 'Line') @@ -621,6 +602,27 @@ function PrevNormalOrGrace (noteRest, grace) { return prev_obj; } //$end +function HasSingleVoice (bar) { + //$module(Utilities.mss) + + // Returns true if the bar has at most one single voice + + voiceNum = -1; + for each bobj in bar + { + if (voiceNum != bobj.VoiceNumber and (bobj.Type = 'NoteRest' or bobj.Type = 'BarRest')) + { + if (voiceNum > 0) + { + return false; + } + voiceNum = bobj.VoiceNumber; + } + } + + return true; +} //$end + function GetNongraceParentBeam (noteRest, layer) { //$module(Utilities.mss) /* @@ -845,3 +847,79 @@ function AppendToLayer (meielement, l, beam, tuplet) { } } } //$end + + +function MeiFactory (data) { + /* + Allows creating MEI from data structures, e.g. for templating purposes. + Takes an array with the following content: + + 0. The capitalized tag name + 1. A dictionary with attribute names and values (unlike tag names, + attribute names are not capitalized). Can be null if no + attributes are declared. + 2. A child node (optional), represented by either a string for text + or a SparseArray of the same form for a child element. + 3. Any number of additional child nodes. + ... + + Note that all element names are capitalized, but attribute names remain + lower case. + + Example: + + MeiFactory(CreateSparseArray( + 'P', null, + 'This is ', + CreateSparseArray('Rend', CreateDictionary('rend', 'italic'), + 'declarative' + ), + ' MEI generation.' + )); + + Output: + +

This is declarative MEI generation.

+ */ + tagName = data[0]; + element = libmei.@tagName(); + + attributes = data[1]; + if (null != attributes) + { + for each Name attName in attributes + { + libmei.AddAttribute(element, attName, attributes[attName]); + } + } + + if (data.Length > 2) + { + // Add children + currentChild = null; + for i = 2 to data.Length + { + childData = data[i]; + if (IsObject(childData)) + { + // We have a child element + currentChild = MeiFactory(childData); + libmei.AddChild(element, currentChild); + } + else + { + // We have a text child + if (currentChild = null) + { + libmei.SetText(element, libmei.GetText(element) & childData); + } + else + { + libmei.SetTail(currentChild, libmei.GetTail(currentChild) & childData); + } + } + } + } + + return element; +} //$end diff --git a/src/dialog/ExtensionDialog.msd b/src/dialog/ExtensionDialog.msd new file mode 100644 index 0000000..9d4783d --- /dev/null +++ b/src/dialog/ExtensionDialog.msd @@ -0,0 +1,83 @@ +ExtensionDialog "Dialog" +{ + Title "Choose Sibmei extensions to activate" + X "138" + Y "282" + Width "205" + Height "170" + Controls + { + Text + { + Title "Available Sibmei extensions" + X "1" + Y "2" + Width "100" + Height "14" + } + ListBox + { + Title + X "1" + Y "15" + Width "201" + Height "60" + ListVar "AvailableExtensions" + AllowMultipleSelections "1" + Value "SelectedExtensions" + } + Button + { + Title "Activate All Extensions" + X "2" + Y "77" + Width "68" + Height "14" + Method "SelectAllExtensions" + } + Button + { + Title "Deactivate All Extensions" + X "72" + Y "77" + Width "68" + Height "14" + Method "DeselectAllExtensions" + } + Text + { + Title "Highlighted extensions are active." + X "1" + Y "99" + Width "204" + Height "10" + } + Text + { + Title "To activate multiple extensions, use Ctrl+click on Windows/Cmd+click on Mac" + X "1" + Y "109" + Width "205" + Height "14" + } + Button + { + Title "Export" + X "151" + Y "129" + Width "50" + Height "14" + DefaultButton "1" + EndDialog "1" + } + Button + { + Title "Cancel" + X "100" + Y "129" + Width "50" + Height "14" + EndDialog "0" + } + } +} diff --git a/test/mocha/test-extensions.js b/test/mocha/test-extensions.js new file mode 100644 index 0000000..ccda4fd --- /dev/null +++ b/test/mocha/test-extensions.js @@ -0,0 +1,30 @@ +"use strict"; + +const assert = require('assert'); +const xpath = require('fontoxpath'); +const utils = require('./utils'); + +describe("Extensions", function() { + const mei = utils.getTestMeiDom('extensions.mei'); + const symbols = xpath.evaluateXPath('//*:symbol', mei); + const text = xpath.evaluateXPath('//*:anchoredText', mei); + + it("exports custom symbols", function() { + utils.assertAttrValueFormat(symbols, 'fontfam', 'myCustomFont'); + utils.assertAttrValueFormat(symbols, 'glyph.name', 'mySymbolGlyph'); + assert.strictEqual(symbols.length, 2, '2 symbols expected'); + utils.assertAttrOnElements(symbols, [1], 'type', 'myRedType'); + }); + + it("attaches control events to measures", function(){ + for (let i = 0; i < 2; i++) { + const measure = symbols[i].parentElement; + assert.strictEqual(measure.tagName, "measure", 'must be attached to measures'); + assert.strictEqual(String(i + 1), measure.getAttribute("n"), 'test file has 1 symbol per measure'); + } + }); + + it("exports custom text by name", function(){ + assert.notStrictEqual(text.length, 0 ,"custom is missing"); + }); +}); diff --git a/test/mocha/test-fermatas.js b/test/mocha/test-fermatas.js new file mode 100644 index 0000000..ac83f22 --- /dev/null +++ b/test/mocha/test-fermatas.js @@ -0,0 +1,68 @@ +"use strict"; + +const assert = require('assert'); +const xpath = require('fontoxpath'); +const utils = require('./utils'); + +describe("Fermatas", function() { + const mei = utils.getTestMeiDom('fermatas.mei'); + const measures = xpath.evaluateXPath('//*:measure', mei); + // Fermatas may be written in arbitrary order, so we sort them. + const fermatasByMeasure = measures.map(measure => xpath + .evaluateXPathToNodes('.//*:fermata', measure) + .sort((a, b) => { + const tstampA = a.getAttribute('tstamp'); + const tstampB = b.getAttribute('tstamp'); + if (tstampA < tstampB) { + return -1; + } else if (tstampA > tstampB) { + return 1; + } + // For multiple fermatas at the same tstamp, sort alphabeticall by @shape + if (a.getAttribute('shape') < b.getAttribute('shape')) { + return -1; + } else { + return 1; + } + }) + ); + + + it("rests don't have @fermata attributes" , function() { + const rests = xpath.evaluateXPath('//*:rest',mei); + utils.assertHasAttrNot(rests,"fermata"); + }); + + it("expected fermata shapes", function() { + const expectedShapes = [ + ["curved"], // b. 1, 7, 13 + ["angular"], // b. 2, 8, 14 + ["square"], // b. 3, 9, 15 + ["curved", "angular", "square", "angular", "curved"], // b. 4, 10, 16 + ["angular", "square"], // b. 5, 11, 17 + ["curved", "curved"] // b. 6, 12, 18 + ]; + for (let barIndex = 0; barIndex < measures.length; barIndex += 1) { + const foundShapes = (fermatasByMeasure[barIndex] || []).map(fermata => fermata.getAttribute("shape")); + const expectedShapesInBar = expectedShapes[barIndex % 6]; + assert.deepEqual(foundShapes, expectedShapesInBar, `Expected fermata shapes ${expectedShapesInBar} in bar ${barIndex + 1}, but found ${foundShapes}`); + } + }); + + it("expected fermata forms", function() { + const expectedForms = [ + [1], // b. 1, 7, 13 + [1], // b. 2, 8, 14 + [1], // b. 3, 9, 15 + [1, 1, 1, 1, 1], // b. 4, 10, 16 + [1, 1], // b. 5, 11, 17 + [1, -1] // b. 6, 12, 18 + ]; + for (let barIndex = 0; barIndex < measures.length; barIndex += 1) { + const foundForms = (fermatasByMeasure[barIndex] || []).map(fermata => fermata.getAttribute("form")); + const flip = barIndex >= 12 ? -1 : 1; // Starting from bar 13, all fermatas are flipped + const expectedFormsInBar = expectedForms[barIndex % 6].map(factor => factor * flip == 1 ? 'norm' : 'inv'); + assert.deepEqual(foundForms, expectedFormsInBar, `Expected fermata forms ${expectedFormsInBar} in bar ${barIndex + 1}, but found ${foundForms}`); + } + }); +}); diff --git a/test/mocha/test-mei40.js b/test/mocha/test-mei40.js index 307df92..deeb21c 100644 --- a/test/mocha/test-mei40.js +++ b/test/mocha/test-mei40.js @@ -9,7 +9,6 @@ const meiHead = utils.getTestMeiDom('header.mei'); const meiMdivs = utils.getTestMeiDom('mdivs.mei'); const meiNRsmall = utils.getTestMeiDom('nrsmall.mei'); const meiBarRests = utils.getTestMeiDom('barrests.mei'); -const meiSymbols = utils.getTestMeiDom('symbols.mei'); describe("Head 4.0", () => { it("correct meiversion is set", () => { @@ -105,20 +104,3 @@ describe("Measure rests and repeats", () => { assert.strictEqual(xpath.evaluateXPath('//*:measure[@n="8"]//*:multiRpt', meiBarRests).getAttribute('num'), '4'); }); }); - -describe("Updated attributes for symbols (mordents and turns)", () => { - const mordents = xpath.evaluateXPath('//*:mordent', meiSymbols); - const turns = xpath.evaluateXPath('//*:turn', meiSymbols); - it("Mordent has @form='upper'", () => { - utils.assertAttrOnElements(mordents, [0], 'form', 'upper'); - }); - it("Inverted mordent has @form='lower'", () => { - utils.assertAttrOnElements(mordents, [1], 'form', 'lower'); - }); - it("Turn has @form='upper'", () => { - utils.assertAttrOnElements(turns, [0], 'form', 'upper'); - }); - it("Inverted turn has @form='lower'", () => { - utils.assertAttrOnElements(turns, [1], 'form', 'lower'); - }); -}); diff --git a/test/mocha/test-symbols.js b/test/mocha/test-symbols.js new file mode 100644 index 0000000..ebc6da1 --- /dev/null +++ b/test/mocha/test-symbols.js @@ -0,0 +1,44 @@ +"use strict"; + +const assert = require('assert'); +const xpath = require('fontoxpath'); +const utils = require('./utils'); + +const meiSymbols = utils.getTestMeiDom('symbols.mei'); + +describe("Symbols", function() { + describe("Control events: Expected attributes (mordents and turns)", function() { + const mordents = xpath.evaluateXPath('//*:mordent', meiSymbols); + const turns = xpath.evaluateXPath('//*:turn', meiSymbols); + it("Mordent has @form='upper'", function() { + utils.assertAttrOnElements(mordents, [1], 'form', 'upper'); + }); + it("Inverted mordent has @form='lower'", function() { + utils.assertAttrOnElements(mordents, [0], 'form', 'lower'); + }); + it("Turn has @form='upper'", function() { + utils.assertAttrOnElements(turns, [0], 'form', 'upper'); + }); + it("Inverted turn has @form='lower'", function() { + utils.assertAttrOnElements(turns, [1], 'form', 'lower'); + }); + }); + describe("Modifiers (children of note): Articulations", function() { + var artics = xpath.evaluateXPath('//*:artic', meiSymbols); + it("21 articulations were created", function () { + assert.strictEqual(artics.length, 21, "Not all 21 articulations were created"); + }); + it(" is child of ", function() { + for (let count = 0; count < artics.length; count++) { + assert.strictEqual(artics[count].parentNode.localName, "note", ' ${i} is not a child of '); + } + }); + it("every has @artic", function() { + utils.assertHasAttr(artics, "artic"); + }); + it(" with @place", function() { + utils.assertElsHasAttr(artics, [9, 10, 11, 12, 13, 14], 'place'); + }); + }); +}); + \ No newline at end of file diff --git a/test/mocha/test-text.js b/test/mocha/test-text.js new file mode 100644 index 0000000..d4398f3 --- /dev/null +++ b/test/mocha/test-text.js @@ -0,0 +1,83 @@ +"use strict"; + +const assert = require('assert'); +const xpath = require('fontoxpath'); +const utils = require('./utils'); + +const meiText = utils.getTestMeiDom('text.mei'); + +describe("Text elements", function() { + it("figured bass elements in measure 4", function() { + const m4harms = xpath.evaluateXPath("//*:measure[@n='4']/*:harm", meiText); + assert.strictEqual(m4harms.length, 3, "There should be 3 elements in measure 4"); + }); + it("check for in measure 6", function() { + const tempo = xpath.evaluateXPath("//*:measure[@n='6']/*:tempo", meiText); + assert.notStrictEqual(tempo.length, 0 ," in measure 6 is missing"); + }); + // test for dynam + it("two elements", function() { + const dynams = xpath.evaluateXPath("//*:dynam", meiText); + assert.strictEqual(dynams.length, 2,"there should be 2 elements"); + }); + // test for title, subtitle & composer + it("check for composer label in measure 1", function() { + const composerEl = xpath.evaluateXPath("//*:measure[@n='1']//*:persName[@role='Composer']", meiText); + assert.notStrictEqual(composerEl.length, 0,"The composer label is missing"); + }); + it("check for subordinate title in measure 1", function() { + const subTitle = xpath.evaluateXPath("//*:measure[@n='1']//*:title[@type='subordinate']", meiText); + assert.notStrictEqual(subTitle.length, 0, "The subtitle is missing"); + }); + // test for plain text (not implemented yet) + it("check for plain text in measure 2", function() { + const plain = xpath.evaluateXPath("//*:measure[@n='2']/*:anchoredText", meiText); + assert.notStrictEqual(plain.length, 0 ,"plain text in measure 2 is missing"); + }); + // test formatting: subscript, superscript + it("check for superscript", function() { + const superscript = xpath.evaluateXPath("//*:measure[@n='1']//*:title[@type='subordinate']/*:rend[@rend='sup']", meiText); + assert.notStrictEqual(superscript.length, 0, "Superscript in subtitle is missing"); + }); + it("check for subscript", function() { + const subscript = xpath.evaluateXPath("//*:measure[@n='1']//*:title[@type='subordinate']/*:rend[@rend='sub']", meiText); + assert.notStrictEqual(subscript.length, 0, "Subscript in subtitle is missing"); + }); + // check for front matter + it("check for front matter", function() { + const firstMusicChild = xpath.evaluateXPath("//*:music/element()[1]", meiText); + assert.strictEqual(firstMusicChild.localName, "front"); + }); + // test formatting: these tests all depend on the test file looking like this, working with content querying + // make sure that no false positives occur by checking for the length of the arrays first... + // bold: "Title text" @fontweight="bold" + it("check for bold text", function() { + const bold = xpath.evaluateXPath("//*:rend[./text()='Title Text' or ./text()='poco']", meiText); + assert.strictEqual(bold.length, 2, "There are only " + bold.length + " elements queried!"); + utils.assertAttrOnElements(bold, [0, 1], "fontweight", "bold"); + }); + // italic: "Subtitle Text" @fontstyle="italic" + it("check for italic text", function() { + const subtitleText = xpath.evaluateXPath("//*:rend[./text()='Subtitle Text']", meiText); + assert.notStrictEqual(subtitleText.length, 0, "No matching elements to assert were found!"); + assert.strictEqual(subtitleText.getAttribute("fontstyle"), "italic", "Italic rendering is missing"); + }); + // underline: "underlined for me" @rend="underline" + it("check for underlined text", function() { + const underline = xpath.evaluateXPath("//*:rend[./text()='underlined ' or ./text()='for me']", meiText); + assert.strictEqual(underline.length, 2, "There are only " + underline.length + " elements queried!"); + utils.assertAttrOnElements(underline, [0, 1], "rend", "underline"); + }); + // font change: @fontfam "change " & "the font" + it("check for font change", function() { + const changeTheFont = xpath.evaluateXPath("//*:rend[./text()='change ' or ./text()='the font']", meiText); + assert.strictEqual(changeTheFont.length, 2, "There are only " + changeTheFont.length + " elements queried!"); + utils.assertHasAttr(changeTheFont, "fontfam"); + }); + // font size: @fontsize "larger" + it("check for font size", function() { + const larger = xpath.evaluateXPath("//*:rend[./text()='larger']", meiText); + assert.notStrictEqual(larger.length, 0, "No matching elements to assert were found!"); + assert.notEqual(larger.getAttribute("fontsize"), null, "@fontsize is missing"); + }); +}); \ No newline at end of file diff --git a/test/mocha/utils.js b/test/mocha/utils.js index 97660d3..62d68e5 100644 --- a/test/mocha/utils.js +++ b/test/mocha/utils.js @@ -31,6 +31,12 @@ module.exports = { } }, + assertHasAttrNot: function(elements, attName) { + for (let i = 0; i < elements.length; i += 1) { + assert.strictEqual(elements[i].getAttribute(attName), null, `element ${i} should not have attribute @${attName}`); + } + }, + assertElsHasAttr: function(elements, indices, attName) { for (let i = 0; i < elements.length; i += 1) { const elementDescription = 'element index ' + i + ' (' + elements[i].localName + ')'; @@ -43,15 +49,28 @@ module.exports = { } }, + /** + * @param {Element|Element[]} elements + * @param {string} attName + * @param {string|RegExp} expectedFormat If a string is supplied, the + * attributes will be tested for strict equality, otherwise if they match + * the RegExp. + */ assertAttrValueFormat: function (elements, attName, expectedFormat) { + if (!elements instanceof Array) { + elements = [elements]; + } for (let i = 0; i < elements.length; i += 1) { const actualValue = elements[i].getAttribute(attName); if (actualValue == undefined) { assert.ok(false, 'element ' + i + ' has no @' + attName); } - else { + else if (expectedFormat instanceof RegExp) { assert.ok(expectedFormat.test(actualValue), 'value on element ' + i + ' does not match'); } + else { + assert.strictEqual(actualValue, expectedFormat); + } } } } diff --git a/test/sib-test/Run.mss b/test/sib-test/Run.mss index bab85fe..d0fd3e0 100644 --- a/test/sib-test/Run.mss +++ b/test/sib-test/Run.mss @@ -1,69 +1,86 @@ function Run() { - Self._property:libmei = libmei4; - Self._property:sibmei = sibmei4; - sibmei4._property:libmei = libmei; + Self._property:libmei = libmei4; + Self._property:sibmei = sibmei4; + sibmei4._property:libmei = libmei; + sibmei.InitGlobals(CreateSparseArray('sibmei4_extension_test')); - plugins = Sibelius.Plugins; + plugins = Sibelius.Plugins; - if (not (plugins.Contains('Test'))) { - Sibelius.MessageBox('Please install the Test plugin!'); - ExitPlugin(); - } + if (not (plugins.Contains('Test'))) + { + Sibelius.MessageBox('Please install the Test plugin!'); + ExitPlugin(); + } - // In an attempt to minimize the chance of Sibelius crashing randomly, close - // all scores before running the tests. - if (not Sibelius.YesNoMessageBox( - 'All open scores will be closed without saving before running the tests. Continue?' - )) { - ExitPlugin(); - } + // In an attempt to minimize the chance of Sibelius crashing randomly, close + // all scores before running the tests. + if (not Sibelius.YesNoMessageBox( + 'All open scores will be closed without saving before running the tests. Continue?' + )) + { + ExitPlugin(); + } - Sibelius.CloseAllWindows(false); - Sibelius.New(); + Sibelius.CloseAllWindows(false); + Sibelius.New(); - Self._property:pluginDir = GetPluginFolder('sibmei4.plg'); - Self._property:tempDir = CreateNewTempDir(); - Self._property:_SibTestFileDirectory = pluginDir & 'sibmeiTestSibs' - & Sibelius.PathSeparator; + Self._property:pluginDir = GetPluginFolder('sibmei4.plg'); + Self._property:tempDir = CreateNewTempDir(); + Self._property:_SibTestFileDirectory = pluginDir & 'sibmeiTestSibs' & Sibelius.PathSeparator; - suite = Test.Suite('Sibelius MEI Exporter', Self, sibmei); + suite = Test.Suite('Sibelius MEI Exporter', Self, sibmei); - suite - .AddModule('TestExportConverters') - .AddModule('TestLibmei') - .AddModule('TestExportGenerators') - .AddModule('TestUtilities') + suite + .AddModule('TestExportConverters') + .AddModule('TestLibmei') + .AddModule('TestExportGenerators') + .AddModule('TestUtilities') ; - suite.Run(); + suite.Run(); - sibmei4_batch_sib.ConvertFolder(Sibelius.GetFolder(_SibTestFileDirectory)); + Sibelius.CloseAllWindows(false); - // We do not 'clean up' with Sibelius.CloseAllWindows() here because it - // sometimes causes Sibelius crashes. - Trace('Run `npm test` to test output written to ' & _SibTestFileDirectory); + sibmei4_batch_sib.ConvertFolder( + Sibelius.GetFolder(_SibTestFileDirectory), + CreateSparseArray('sibmei4_extension_test') + ); + + // Make sure we have an open window so Sibelius will neither crash nor + // decide to open a new window later that will force the mocha test results + // into the background. + Sibelius.New(); + + if (Sibelius.PathSeparator = '/') { + mochaScript = pluginDir & 'test.sh'; + } else { + mochaScript = pluginDir & 'test.bat'; + } + if (not (Sibelius.FileExists(mochaScript) and Sibelius.LaunchApplication(mochaScript))) { + Sibelius.MessageBox('Run `npm test` to test output written to ' & _SibTestFileDirectory); + } } //$end function GetPluginFolder(plgName) { - //$module(Run.mss) - plgNameLength = Length(plgName); - - for each plugin in Sibelius.Plugins - { - path = plugin.File; - i = Length(path) - 2; - while (i >= 0 and CharAt(path, i) != Sibelius.PathSeparator) { - i = i - 1; - } + //$module(Run.mss) + plgNameLength = Length(plgName); - if (Substring(path, i + 1) = plgName) { - return Substring(path, 0, i + 1); + for each plugin in Sibelius.Plugins + { + path = plugin.File; + i = Length(path) - 2; + while (i >= 0 and CharAt(path, i) != Sibelius.PathSeparator) { + i = i - 1; + } + + if (Substring(path, i + 1) = plgName) { + return Substring(path, 0, i + 1); + } } - } - Sibelius.MessageBox(plgName & ' was not found'); - ExitPlugin(); + Sibelius.MessageBox(plgName & ' was not found'); + ExitPlugin(); } //$end diff --git a/test/sibmeiTestSibs/extensions.sib b/test/sibmeiTestSibs/extensions.sib new file mode 100644 index 0000000..74aaba9 Binary files /dev/null and b/test/sibmeiTestSibs/extensions.sib differ diff --git a/test/sibmeiTestSibs/fermatas.sib b/test/sibmeiTestSibs/fermatas.sib new file mode 100644 index 0000000..9835c97 Binary files /dev/null and b/test/sibmeiTestSibs/fermatas.sib differ diff --git a/test/sibmeiTestSibs/symbols.sib b/test/sibmeiTestSibs/symbols.sib index 082a1f2..617f733 100644 Binary files a/test/sibmeiTestSibs/symbols.sib and b/test/sibmeiTestSibs/symbols.sib differ diff --git a/test/sibmeiTestSibs/text.sib b/test/sibmeiTestSibs/text.sib new file mode 100644 index 0000000..76de7ac Binary files /dev/null and b/test/sibmeiTestSibs/text.sib differ