diff --git a/src.compiler/csharp/CSharpAstTransformer.ts b/src.compiler/csharp/CSharpAstTransformer.ts index 920897a9e..1c52af2e2 100644 --- a/src.compiler/csharp/CSharpAstTransformer.ts +++ b/src.compiler/csharp/CSharpAstTransformer.ts @@ -3055,6 +3055,8 @@ export default class CSharpAstTransformer { break; case 'String': switch (symbol.name) { + case 'includes': + return 'Contains'; case 'trimRight': return 'TrimEnd'; case 'trimLeft': diff --git a/src.compiler/kotlin/KotlinAstTransformer.ts b/src.compiler/kotlin/KotlinAstTransformer.ts index dd833aa16..a39723652 100644 --- a/src.compiler/kotlin/KotlinAstTransformer.ts +++ b/src.compiler/kotlin/KotlinAstTransformer.ts @@ -297,16 +297,9 @@ export default class KotlinAstTransformer extends CSharpAstTransformer { case 'String': switch (symbol.name) { case 'length': - if ( - expression.parent && - (cs.isReturnStatement(expression.parent) || - cs.isVariableDeclaration(expression.parent) || - (cs.isBinaryExpression(expression.parent) && expression.parent.operator === '=')) - ) { - return 'length.toDouble()'; - } - return 'length.toDouble()'; + case 'includes': + return 'contains'; case 'indexOf': return 'indexOfInDouble'; case 'lastIndexOf': diff --git a/src/Logger.ts b/src/Logger.ts index e80def20f..d3541a9c1 100644 --- a/src/Logger.ts +++ b/src/Logger.ts @@ -33,7 +33,7 @@ export class ConsoleLogger implements ILogger { export class Logger { public static logLevel: LogLevel = LogLevel.Info; - public static log:ILogger = new ConsoleLogger(); + public static log: ILogger = new ConsoleLogger(); private static shouldLog(level: LogLevel): boolean { return Logger.logLevel !== LogLevel.None && level >= Logger.logLevel; diff --git a/src/importer/AlphaTexImporter.ts b/src/importer/AlphaTexImporter.ts index 5625682a0..804cde7c5 100644 --- a/src/importer/AlphaTexImporter.ts +++ b/src/importer/AlphaTexImporter.ts @@ -57,48 +57,62 @@ export enum AlphaTexSymbols { MetaCommand, Multiply, LowerThan, - Property } export class AlphaTexError extends AlphaTabError { - public position: number = 0; - public nonTerm: string = ''; - public expected: AlphaTexSymbols = AlphaTexSymbols.No; - public symbol: AlphaTexSymbols = AlphaTexSymbols.No; - public symbolData: unknown = null; - - public constructor(message: string | null) { + public position: number; + public line: number; + public col: number; + public nonTerm: string; + public expected: AlphaTexSymbols; + public symbol: AlphaTexSymbols; + public symbolData: unknown; + + public constructor( + message: string | null, + position: number, + line: number, + col: number, + nonTerm: string | null, + expected: AlphaTexSymbols | null, + symbol: AlphaTexSymbols | null, + symbolData: unknown = null, + ) { super(AlphaTabErrorType.AlphaTex, message); + this.position = position; + this.line = line; + this.col = col; + this.nonTerm = nonTerm ?? ''; + this.expected = expected ?? AlphaTexSymbols.No; + this.symbol = symbol ?? AlphaTexSymbols.No; + this.symbolData = symbolData; Object.setPrototypeOf(this, AlphaTexError.prototype); } public static symbolError( position: number, + line: number, + col: number, nonTerm: string, expected: AlphaTexSymbols, symbol: AlphaTexSymbols, - symbolData: unknown = null + symbolData: unknown = null, ): AlphaTexError { - let message: string; + let message = `MalFormed AlphaTex: @${position} (line ${line}, col ${col}): Error on block ${nonTerm}`; if (expected !== symbol) { - message = `MalFormed AlphaTex: @${position}: Error on block ${nonTerm}, expected a ${AlphaTexSymbols[expected]} found a ${AlphaTexSymbols[symbol]}: '${symbolData}'`; + message += `, expected a ${AlphaTexSymbols[expected]} found a ${AlphaTexSymbols[symbol]}`; + if (symbolData !== null) { + message += `: '${symbolData}'`; + } } else { - message = `MalFormed AlphaTex: @${position}: Error on block ${nonTerm}, invalid value: '${symbolData}'`; + message += `, invalid value: '${symbolData}'`; } - let exception: AlphaTexError = new AlphaTexError(message); - exception.position = position; - exception.nonTerm = nonTerm; - exception.expected = expected; - exception.symbol = symbol; - exception.symbolData = symbolData; - return exception; + return new AlphaTexError(message, position, line, col, nonTerm, expected, symbol, symbolData); } - public static errorMessage(position: number, message: string): AlphaTexError { - message = `MalFormed AlphaTex: @${position}: ${message}`; - let exception: AlphaTexError = new AlphaTexError(message); - exception.position = position; - return exception; + public static errorMessage(message: string, position: number, line: number, col: number): AlphaTexError { + message = `MalFormed AlphaTex: @${position} (line ${line}, col ${col}): ${message}`; + return new AlphaTexError(message, position, line, col, null, null, null, null); } } @@ -112,8 +126,13 @@ export class AlphaTexImporter extends ScoreImporter { private _currentTrack!: Track; private _currentStaff!: Staff; private _input: string = ""; - private _ch: number = 0; + private _ch: number = AlphaTexImporter.Eof; + // Keeps track of where in input string we are private _curChPos: number = 0; + private _line: number = 1; + private _col: number = 0; + // Last known position that had valid syntax/symbols + private _lastValidSpot: number[] = [0, 1, 0]; private _sy: AlphaTexSymbols = AlphaTexSymbols.No; private _syData: unknown = ""; private _allowNegatives: boolean = false; @@ -142,7 +161,6 @@ export class AlphaTexImporter extends ScoreImporter { this.settings = settings; } - public readScore(): Score { try { if (this.data.length > 0) { @@ -152,6 +170,9 @@ export class AlphaTexImporter extends ScoreImporter { this._lyrics = new Map(); this.createDefaultScore(); this._curChPos = 0; + this._line = 1; + this._col = 0; + this.saveValidSpot(); this._currentDuration = Duration.Quarter; this._currentDynamics = DynamicValue.F; this._currentTuplet = 1; @@ -159,9 +180,15 @@ export class AlphaTexImporter extends ScoreImporter { this._sy = this.newSy(); if (this._sy === AlphaTexSymbols.LowerThan) { // potential XML, stop parsing (alphaTex never starts with <) - throw new UnsupportedFormatError('Unknown start sign <'); + throw new UnsupportedFormatError("Unknown start sign '<' (meant to import as XML?)"); + } else if (this._sy === AlphaTexSymbols.Eof) { + throw new UnsupportedFormatError('Unexpected end of file'); + } + const anyMetaRead = this.metaData(); + const anyBarsRead = this.bars(); + if (!anyMetaRead && !anyBarsRead) { + throw new UnsupportedFormatError('No alphaTex data found'); } - this.score(); this.consolidate(); this._score.finish(this.settings); this._score.rebuildRepeatGroups(); @@ -171,16 +198,18 @@ export class AlphaTexImporter extends ScoreImporter { return this._score; } catch (e) { if (e instanceof AlphaTexError) { - throw new UnsupportedFormatError(e.message); + throw new UnsupportedFormatError(e.message, e); } else { throw e; } } } + /** + * Ensures all staffs of all tracks have the correct number of bars + * (the number of bars per staff and track could be inconsistent) + */ private consolidate(): void { - // the number of bars per staff and track could be inconsistent, - // we need to ensure all staffs of all tracks have the correct number of bars for (let track of this._score.tracks) { for (let staff of track.staves) { while (staff.bars.length < this._score.masterBars.length) { @@ -193,13 +222,33 @@ export class AlphaTexImporter extends ScoreImporter { } } - private error(nonterm: string, expected: AlphaTexSymbols, symbolError: boolean = true): void { - let e: AlphaTexError; - if (symbolError) { - e = AlphaTexError.symbolError(this._curChPos, nonterm, expected, this._sy, this._syData); + private error(nonterm: string, expected: AlphaTexSymbols, wrongSymbol: boolean = true): void { + let receivedSymbol: AlphaTexSymbols; + let showSyData = false; + if (wrongSymbol) { + receivedSymbol = this._sy; + if ( + // These are the only symbols that can have associated _syData set + receivedSymbol === AlphaTexSymbols.String || + receivedSymbol === AlphaTexSymbols.Number || + receivedSymbol === AlphaTexSymbols.MetaCommand // || + // Tuning does not have a toString() yet, therefore excluded. + // receivedSymbol === AlphaTexSymbols.Tuning + ) { + showSyData = true; + } } else { - e = AlphaTexError.symbolError(this._curChPos, nonterm, expected, expected, this._syData); + receivedSymbol = expected; } + let e = AlphaTexError.symbolError( + this._lastValidSpot[0], + this._lastValidSpot[1], + this._lastValidSpot[2], + nonterm, + expected, + receivedSymbol, + showSyData ? this._syData : null + ); if (this.logErrors) { Logger.error(this.name, e.message!); } @@ -207,7 +256,12 @@ export class AlphaTexImporter extends ScoreImporter { } private errorMessage(message: string): void { - let e: AlphaTexError = AlphaTexError.errorMessage(this._curChPos, message); + let e: AlphaTexError = AlphaTexError.errorMessage( + message, + this._lastValidSpot[0], + this._lastValidSpot[1], + this._lastValidSpot[2] + ); if (this.logErrors) { Logger.error(this.name, e.message!); } @@ -381,7 +435,7 @@ export class AlphaTexImporter extends ScoreImporter { case 'd': case 'dmajor': case 'bminor': - return KeySignature.D;; + return KeySignature.D; case 'a': case 'amajor': case 'f#minor': @@ -408,28 +462,47 @@ export class AlphaTexImporter extends ScoreImporter { } /** - * Reads the next character of the source stream. + * Reads, saves, and returns the next character of the source stream. */ private nextChar(): number { if (this._curChPos < this._input.length) { - this._ch = this._input.charCodeAt(this._curChPos++) + this._ch = this._input.charCodeAt(this._curChPos++); + // line/col counting + if (this._ch === 0x0a /* \n */) { + this._line++; + this._col = 0; + } else { + this._col++; + } } else { - this._ch = 0; + this._ch = AlphaTexImporter.Eof; } return this._ch; } /** - * Reads the next terminal symbol. + * Saves the current position, line, and column. + * All parsed data until this point is assumed to be valid. + */ + private saveValidSpot(): void { + this._lastValidSpot = [this._curChPos, this._line, this._col]; + } + + /** + * Reads, saves, and returns the next terminal symbol. */ private newSy(): AlphaTexSymbols { + // When a new symbol is read, the previous one is assumed to be valid. + // The valid spot is also moved forward when reading past whitespace or comments. + this.saveValidSpot(); this._sy = AlphaTexSymbols.No; - do { + while (this._sy === AlphaTexSymbols.No) { if (this._ch === AlphaTexImporter.Eof) { this._sy = AlphaTexSymbols.Eof; - } else if (this._ch === 0x20 || this._ch === 0x0b || this._ch === 0x0d || this._ch === 0x0a || this._ch === 0x09) { + } else if (AlphaTexImporter.isWhiteSpace(this._ch)) { // skip whitespaces this._ch = this.nextChar(); + this.saveValidSpot(); } else if (this._ch === 0x2f /* / */) { this._ch = this.nextChar(); if (this._ch === 0x2f /* / */) { @@ -455,8 +528,9 @@ export class AlphaTexImporter extends ScoreImporter { } } } else { - this.error('symbol', AlphaTexSymbols.String, false); + this.error('comment', AlphaTexSymbols.String, false); } + this.saveValidSpot(); } else if (this._ch === 0x22 /* " */ || this._ch === 0x27 /* ' */) { let startChar: number = this._ch; this._ch = this.nextChar(); @@ -466,6 +540,9 @@ export class AlphaTexImporter extends ScoreImporter { s += String.fromCharCode(this._ch); this._ch = this.nextChar(); } + if (this._ch === AlphaTexImporter.Eof) { + this.errorMessage('String opened but never closed'); + } this._syData = s; this._ch = this.nextChar(); } else if (this._ch === 0x2d /* - */) { @@ -512,7 +589,7 @@ export class AlphaTexImporter extends ScoreImporter { } else if (this.isDigit(this._ch)) { this._sy = AlphaTexSymbols.Number; this._syData = this.readNumber(); - } else if (AlphaTexImporter.isLetter(this._ch)) { + } else if (AlphaTexImporter.isNameLetter(this._ch)) { let name: string = this.readName(); let tuning: TuningParseResult | null = this._allowTuning ? ModelUtils.parseTuning(name) : null; if (tuning) { @@ -525,29 +602,24 @@ export class AlphaTexImporter extends ScoreImporter { } else { this.error('symbol', AlphaTexSymbols.String, false); } - } while (this._sy === AlphaTexSymbols.No); + } return this._sy; } /** - * Checks if the given character is a letter. + * Checks if the given character is a valid letter for a name. * (no control characters, whitespaces, numbers or dots) - * @param code the character - * @returns true if the given character is a letter, otherwise false. */ - private static isLetter(code: number): boolean { - // no control characters, whitespaces, numbers or dots + private static isNameLetter(ch: number): boolean { return ( - !AlphaTexImporter.isTerminal(code) && - ((code >= 0x21 && code <= 0x2f) || (code >= 0x3a && code <= 0x7e) || code > 0x80) - ); /* Unicode Symbols */ + !AlphaTexImporter.isTerminal(ch) && ( // no control characters, whitespaces, numbers or dots + (0x21 <= ch && ch <= 0x2f) || + (0x3a <= ch && ch <= 0x7e) || + 0x80 <= ch // Unicode Symbols + ) + ); } - /** - * Checks if the given charater is a non terminal. - * @param ch the character - * @returns true if the given character is a terminal, otherwise false. - */ private static isTerminal(ch: number): boolean { return ( ch === 0x2e /* . */ || @@ -564,13 +636,21 @@ export class AlphaTexImporter extends ScoreImporter { ); } - /** - * Checks if the given character is a digit. - * @param code the character - * @returns true if the given character is a digit, otherwise false. - */ - private isDigit(code: number): boolean { - return (code >= 0x30 && code <= 0x39) /* 0-9 */ || (code === 0x2d /* - */ && this._allowNegatives); // allow - if negatives + private static isWhiteSpace(ch: number): boolean { + return ( + ch === 0x09 /* \t */ || + ch === 0x0a /* \n */ || + ch === 0x0b /* \v */ || + ch === 0x0d /* \r */ || + ch === 0x20 /* space */ + ); + } + + private isDigit(ch: number): boolean { + return ( + (ch >= 0x30 && ch <= 0x39) /* 0-9 */ || + (this._allowNegatives && ch === 0x2d /* - */) // allow minus sign if negatives + ); } /** @@ -582,7 +662,7 @@ export class AlphaTexImporter extends ScoreImporter { do { str += String.fromCharCode(this._ch); this._ch = this.nextChar(); - } while (AlphaTexImporter.isLetter(this._ch) || this.isDigit(this._ch) || this._ch === 0x23 /* # */); + } while (AlphaTexImporter.isNameLetter(this._ch) || this.isDigit(this._ch)); return str; } @@ -599,17 +679,6 @@ export class AlphaTexImporter extends ScoreImporter { return parseInt(str); } - private score(): void { - if (this._sy === AlphaTexSymbols.Eof) { - throw new UnsupportedFormatError('Unexpected end of file'); - } - const anyMetaRead = this.metaData(); - const anyBarsRead = this.bars(); - if (!anyMetaRead && !anyBarsRead) { - throw new UnsupportedFormatError('No alphaTex data found'); - } - } - private metaData(): boolean { let anyMeta: boolean = false; let continueReading: boolean = true; @@ -629,7 +698,7 @@ export class AlphaTexImporter extends ScoreImporter { // Need to use quotes in that case, or rewrite parsing logic. this.error(metadataTag, AlphaTexSymbols.String, true); } - let metadataValue: string = (this._syData as string); + let metadataValue: string = this._syData as string; switch (metadataTag) { case 'title': this._score.title = metadataValue; @@ -895,47 +964,42 @@ export class AlphaTexImporter extends ScoreImporter { } private trackStaffMeta(): boolean { - let anyMeta = false; - if (this._sy === AlphaTexSymbols.MetaCommand) { - anyMeta = true; - let syData: string = (this._syData as string).toLowerCase(); - if (syData === 'track') { - this._staffHasExplicitTuning = false; - this._staffTuningApplied = false; + if (this._sy !== AlphaTexSymbols.MetaCommand) { + return false; + } + if ((this._syData as string).toLowerCase() === 'track') { + this._staffHasExplicitTuning = false; + this._staffTuningApplied = false; + this._sy = this.newSy(); + // new track starting? - if no masterbars it's the \track of the initial track. + if (this._score.masterBars.length > 0) { + this.newTrack(); + } + // name + if (this._sy === AlphaTexSymbols.String) { + this._currentTrack.name = this._syData as string; this._sy = this.newSy(); - // new track starting? - if no masterbars it's the \track of the initial track. - if (this._score.masterBars.length > 0) { - this.newTrack(); - } - // name - if (this._sy === AlphaTexSymbols.String) { - this._currentTrack.name = (this._syData as string); - this._sy = this.newSy(); - } - // short name - if (this._sy === AlphaTexSymbols.String) { - this._currentTrack.shortName = (this._syData as string); - this._sy = this.newSy(); - } } - if (this._sy === AlphaTexSymbols.MetaCommand) { - syData = (this._syData as string).toLowerCase(); - if (syData === 'staff') { - this._staffHasExplicitTuning = false; - this._staffTuningApplied = false; + // short name + if (this._sy === AlphaTexSymbols.String) { + this._currentTrack.shortName = this._syData as string; + this._sy = this.newSy(); + } + } + if (this._sy === AlphaTexSymbols.MetaCommand && (this._syData as string).toLowerCase() === 'staff') { + this._staffHasExplicitTuning = false; + this._staffTuningApplied = false; - this._sy = this.newSy(); - if (this._currentTrack.staves[0].bars.length > 0) { - this._currentTrack.ensureStaveCount(this._currentTrack.staves.length + 1); - this._currentStaff = this._currentTrack.staves[this._currentTrack.staves.length - 1]; - this._currentDynamics = DynamicValue.F; - } - this.staffProperties(); - } + this._sy = this.newSy(); + if (this._currentTrack.staves[0].bars.length > 0) { + this._currentTrack.ensureStaveCount(this._currentTrack.staves.length + 1); + this._currentStaff = this._currentTrack.staves[this._currentTrack.staves.length - 1]; + this._currentDynamics = DynamicValue.F; } + this.staffProperties(); } - return anyMeta; + return true; } private staffProperties(): void { @@ -994,8 +1058,7 @@ export class AlphaTexImporter extends ScoreImporter { this._currentStaff.displayTranspositionPitch = 0; this._currentStaff.stringTuning.tunings = []; - - if (program == 15 || program >= 24 && program <= 31) { + if (program == 15 || (program >= 24 && program <= 31)) { // dulcimer+guitar E4 B3 G3 D3 A2 E2 this._currentStaff.displayTranspositionPitch = -12; this._currentStaff.stringTuning.tunings = Tuning.getDefaultTuningFor(6)!.tunings; @@ -1107,7 +1170,7 @@ export class AlphaTexImporter extends ScoreImporter { beat.duration = this._currentDuration; beat.dynamics = this._currentDynamics; if (this._currentTuplet !== 1 && !beat.hasTuplet) { - this.applyTuplet(beat, this._currentTuplet); + AlphaTexImporter.applyTuplet(beat, this._currentTuplet); } // beat multiplier (repeat beat n times) let beatRepeat: number = 1; @@ -1173,7 +1236,6 @@ export class AlphaTexImporter extends ScoreImporter { } this._sy = this.newSy(); while (this._sy === AlphaTexSymbols.String) { - this._syData = (this._syData as string).toLowerCase(); if (!this.applyBeatEffect(beat)) { this.error('beat-effects', AlphaTexSymbols.String, false); } @@ -1214,7 +1276,7 @@ export class AlphaTexImporter extends ScoreImporter { this.error('tuplet', AlphaTexSymbols.Number, true); return false; } - this.applyTuplet(beat, this._syData as number); + AlphaTexImporter.applyTuplet(beat, this._syData as number); } else if (syData === 'tb' || syData === 'tbe') { let exact: boolean = syData === 'tbe'; // read points @@ -1286,7 +1348,7 @@ export class AlphaTexImporter extends ScoreImporter { this._sy = this.newSy(); if (this._sy === AlphaTexSymbols.Number) { // explicit duration - beat.brushDuration = (this._syData as number); + beat.brushDuration = this._syData as number; this._sy = this.newSy(); return true; } @@ -1300,7 +1362,7 @@ export class AlphaTexImporter extends ScoreImporter { return true; } else if (syData === 'ch') { this._sy = this.newSy(); - let chordName: string = (this._syData as string); + let chordName: string = this._syData as string; let chordId: string = this.getChordId(this._currentStaff, chordName); if (!this._currentStaff.hasChord(chordId)) { let chord: Chord = new Chord(); @@ -1386,10 +1448,10 @@ export class AlphaTexImporter extends ScoreImporter { } private getChordId(currentStaff: Staff, chordName: string): string { - return chordName.toLowerCase() + currentStaff.index + currentStaff.track.index + return chordName.toLowerCase() + currentStaff.index + currentStaff.track.index; } - private applyTuplet(beat: Beat, tuplet: number): void { + private static applyTuplet(beat: Beat, tuplet: number): void { switch (tuplet) { case 3: beat.tupletNumerator = 3; @@ -1508,10 +1570,9 @@ export class AlphaTexImporter extends ScoreImporter { } this._sy = this.newSy(); while (this._sy === AlphaTexSymbols.String) { - let syData: string = (this._syData as string).toLowerCase(); - this._syData = syData; + let syData = (this._syData as string).toLowerCase(); if (syData === 'b' || syData === 'be') { - let exact: boolean = (this._syData as string) === 'be'; + let exact: boolean = syData === 'be'; // read points this._sy = this.newSy(); if (this._sy !== AlphaTexSymbols.LParensis) { @@ -1765,7 +1826,7 @@ export class AlphaTexImporter extends ScoreImporter { if (this._sy !== AlphaTexSymbols.Number) { this.error('repeatclose', AlphaTexSymbols.Number, true); } - if (this._syData as number > 2048) { + if ((this._syData as number) > 2048) { this.error('repeatclose', AlphaTexSymbols.Number, false); } master.repeatCount = this._syData as number; @@ -1775,7 +1836,7 @@ export class AlphaTexImporter extends ScoreImporter { if (this._sy === AlphaTexSymbols.LParensis) { this._sy = this.newSy(); if (this._sy !== AlphaTexSymbols.Number) { - this.error('alternateending', AlphaTexSymbols.Number, true) + this.error('alternateending', AlphaTexSymbols.Number, true); } this.applyAlternateEnding(master); while (this._sy === AlphaTexSymbols.Number) { @@ -1787,7 +1848,7 @@ export class AlphaTexImporter extends ScoreImporter { this._sy = this.newSy(); } else { if (this._sy !== AlphaTexSymbols.Number) { - this.error('alternateending', AlphaTexSymbols.Number, true) + this.error('alternateending', AlphaTexSymbols.Number, true); } this.applyAlternateEnding(master); } @@ -1888,7 +1949,7 @@ export class AlphaTexImporter extends ScoreImporter { let num = this._syData as number; if (num < 1) { // Repeat numberings start from 1 - this.error('alternateending', AlphaTexSymbols.Number, true) + this.error('alternateending', AlphaTexSymbols.Number, true); } // Alternate endings bitflag starts from 0 master.alternateEndings |= 1 << (num - 1); diff --git a/test/importer/AlphaTexImporter.test.ts b/test/importer/AlphaTexImporter.test.ts index b29275254..e85e22ce2 100644 --- a/test/importer/AlphaTexImporter.test.ts +++ b/test/importer/AlphaTexImporter.test.ts @@ -1,5 +1,5 @@ import { StaveProfile } from '@src/StaveProfile'; -import { AlphaTexImporter } from '@src/importer/AlphaTexImporter'; +import { AlphaTexError, AlphaTexImporter, AlphaTexSymbols } from '@src/importer/AlphaTexImporter'; import { UnsupportedFormatError } from '@src/importer/UnsupportedFormatError'; import { Beat } from '@src/model/Beat'; import { BrushType } from '@src/model/BrushType'; @@ -1017,8 +1017,12 @@ describe('AlphaTexImporterTest', () => { }); it('does-not-hang-on-backslash', () => { - expect(() => parseTex('\\title Test . 3.3 \\')).toThrowError(UnsupportedFormatError) - }) + expect(() => parseTex('\\title Test . 3.3 \\')).toThrowError(UnsupportedFormatError); + }); + + it('disallows-unclosed-string', () => { + expect(() => parseTex('\\title "Test . 3.3')).toThrowError(UnsupportedFormatError); + }); function runSectionNoteSymbolTest(noteSymbol: string) { const score = parseTex(`1.3.4 * 4 | \\section Verse ${noteSymbol}.1 | 2.3.4*4`); @@ -1034,5 +1038,94 @@ describe('AlphaTexImporterTest', () => { runSectionNoteSymbolTest('r'); runSectionNoteSymbolTest('-'); runSectionNoteSymbolTest('x'); - }) + }); + + it('loads-score-twice-without-hickups', () => { + const tex = `\\title Test + \\words test + \\music alphaTab + \\copyright test + \\tempo 200 + \\instrument 30 + \\capo 2 + \\tuning G3 D2 G2 B2 D3 A4 + . + 0.5.2 1.5.4 3.4.4 | 5.3.8 5.3.8 5.3.8 5.3.8 r.2`; + const importer: AlphaTexImporter = new AlphaTexImporter(); + for (const _i of [1, 2]) { + importer.initFromString(tex, new Settings()); + const score = importer.readScore(); + expect(score.title).toEqual('Test'); + expect(score.words).toEqual('test'); + expect(score.music).toEqual('alphaTab'); + expect(score.copyright).toEqual('test'); + expect(score.tempo).toEqual(200); + expect(score.tracks.length).toEqual(1); + expect(score.tracks[0].playbackInfo.program).toEqual(30); + expect(score.tracks[0].staves[0].capo).toEqual(2); + expect(score.tracks[0].staves[0].tuning.join(',')).toEqual('55,38,43,47,50,69'); + expect(score.masterBars.length).toEqual(2); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats.length).toEqual(3); + { + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes.length).toEqual(1); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].duration).toEqual(Duration.Half); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].fret).toEqual(0); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].string).toEqual(2); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes.length).toEqual(1); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].duration).toEqual(Duration.Quarter); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].fret).toEqual(1); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].string).toEqual(2); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes.length).toEqual(1); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].duration).toEqual(Duration.Quarter); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].fret).toEqual(3); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].string).toEqual(3); + } + expect(score.tracks[0].staves[0].bars[1].voices[0].beats.length).toEqual(5); + { + expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes.length).toEqual(1); + expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].duration).toEqual(Duration.Eighth); + expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].fret).toEqual(5); + expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].string).toEqual(4); + expect(score.tracks[0].staves[0].bars[1].voices[0].beats[1].notes.length).toEqual(1); + expect(score.tracks[0].staves[0].bars[1].voices[0].beats[1].duration).toEqual(Duration.Eighth); + expect(score.tracks[0].staves[0].bars[1].voices[0].beats[1].notes[0].fret).toEqual(5); + expect(score.tracks[0].staves[0].bars[1].voices[0].beats[1].notes[0].string).toEqual(4); + expect(score.tracks[0].staves[0].bars[1].voices[0].beats[2].notes.length).toEqual(1); + expect(score.tracks[0].staves[0].bars[1].voices[0].beats[2].duration).toEqual(Duration.Eighth); + expect(score.tracks[0].staves[0].bars[1].voices[0].beats[2].notes[0].fret).toEqual(5); + expect(score.tracks[0].staves[0].bars[1].voices[0].beats[2].notes[0].string).toEqual(4); + expect(score.tracks[0].staves[0].bars[1].voices[0].beats[3].notes.length).toEqual(1); + expect(score.tracks[0].staves[0].bars[1].voices[0].beats[3].duration).toEqual(Duration.Eighth); + expect(score.tracks[0].staves[0].bars[1].voices[0].beats[3].notes[0].fret).toEqual(5); + expect(score.tracks[0].staves[0].bars[1].voices[0].beats[3].notes[0].string).toEqual(4); + expect(score.tracks[0].staves[0].bars[1].voices[0].beats[4].notes.length).toEqual(0); + expect(score.tracks[0].staves[0].bars[1].voices[0].beats[4].duration).toEqual(Duration.Half); + expect(score.tracks[0].staves[0].bars[1].voices[0].beats[4].isRest).toEqual(true); + } + } + }); + + it('error-shows-symbol-data', () => { + const tex = '3.3.ABC'; + expect(() => parseTex(tex)).toThrowError(UnsupportedFormatError); + try { + parseTex(tex); + } catch (e) { + if(!(e instanceof UnsupportedFormatError)) { + fail('Did not throw correct error'); + return; + } + if(!(e.inner instanceof AlphaTexError)) { + fail('Did not contain an AlphaTexError'); + return; + } + const i = e.inner as AlphaTexError; + expect(i.expected).toEqual(AlphaTexSymbols.Number); + expect(i.message?.includes('Number')).toBeTrue(); + expect(i.symbol).toEqual(AlphaTexSymbols.String); + expect(i.message?.includes('String')).toBeTrue(); + expect(i.symbolData).toEqual('ABC'); + expect(i.message?.includes('ABC')).toBeTrue(); + } + }); });