validate chords #2
This commit is contained in:
@@ -13,7 +13,7 @@ export interface ChordValidationIssue {
|
|||||||
lineText: string;
|
lineText: string;
|
||||||
token: string;
|
token: string;
|
||||||
suggestion: string | null;
|
suggestion: string | null;
|
||||||
reason: 'alias' | 'minor_format' | 'major_format' | 'invalid_suffix' | 'unknown_token';
|
reason: 'alias' | 'minor_format' | 'major_format' | 'invalid_suffix' | 'unknown_token' | 'tab_character';
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,4 +24,6 @@ export interface Chord {
|
|||||||
slashChord: string | null;
|
slashChord: string | null;
|
||||||
add: string | null;
|
add: string | null;
|
||||||
addDescriptor?: ChordAddDescriptor | null;
|
addDescriptor?: ChordAddDescriptor | null;
|
||||||
|
prefix?: string;
|
||||||
|
suffix?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -276,6 +276,39 @@ Text`;
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should keep parentheses around alternative chord groups', () => {
|
||||||
|
const service: TextRenderingService = TestBed.inject(TextRenderingService);
|
||||||
|
const text = `Strophe
|
||||||
|
C F G e C (F G)
|
||||||
|
Text`;
|
||||||
|
|
||||||
|
const sections = service.parse(text, null);
|
||||||
|
|
||||||
|
void expect(sections[0].lines[0].type).toBe(LineType.chord);
|
||||||
|
void expect(sections[0].lines[0].text).toBe('C F G e C (F G)');
|
||||||
|
void expect(sections[0].lines[0].chords).toEqual([
|
||||||
|
{chord: 'C', length: 1, position: 0, add: null, slashChord: null, addDescriptor: null},
|
||||||
|
{chord: 'F', length: 1, position: 2, add: null, slashChord: null, addDescriptor: null},
|
||||||
|
{chord: 'G', length: 1, position: 4, add: null, slashChord: null, addDescriptor: null},
|
||||||
|
{chord: 'e', length: 1, position: 6, add: null, slashChord: null, addDescriptor: null},
|
||||||
|
{chord: 'C', length: 1, position: 8, add: null, slashChord: null, addDescriptor: null},
|
||||||
|
{chord: 'F', length: 2, position: 11, add: null, slashChord: null, addDescriptor: null, prefix: '('},
|
||||||
|
{chord: 'G', length: 2, position: 14, add: null, slashChord: null, addDescriptor: null, suffix: ')'},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should transpose multiple chords inside a parenthesized group', () => {
|
||||||
|
const service: TextRenderingService = TestBed.inject(TextRenderingService);
|
||||||
|
const text = `Strophe
|
||||||
|
C F G e C (F G)
|
||||||
|
Text`;
|
||||||
|
|
||||||
|
const sections = service.parse(text, {baseKey: 'C', targetKey: 'D'});
|
||||||
|
|
||||||
|
void expect(sections[0].lines[0].type).toBe(LineType.chord);
|
||||||
|
void expect(sections[0].lines[0].text).toBe('D G A f♯ D (G A)');
|
||||||
|
});
|
||||||
|
|
||||||
it('should treat compact prose-like tokens as text when the chord ratio is too low', () => {
|
it('should treat compact prose-like tokens as text when the chord ratio is too low', () => {
|
||||||
const service: TextRenderingService = TestBed.inject(TextRenderingService);
|
const service: TextRenderingService = TestBed.inject(TextRenderingService);
|
||||||
const text = `Strophe
|
const text = `Strophe
|
||||||
@@ -399,4 +432,24 @@ Text`;
|
|||||||
jasmine.objectContaining({lineNumber: 2, token: 'Foo', reason: 'unknown_token', suggestion: null}),
|
jasmine.objectContaining({lineNumber: 2, token: 'Foo', reason: 'unknown_token', suggestion: null}),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should reject tabs on chord lines', () => {
|
||||||
|
const service: TextRenderingService = TestBed.inject(TextRenderingService);
|
||||||
|
const text = 'Strophe\nC\tG\ta\nText';
|
||||||
|
|
||||||
|
void expect(service.validateChordNotation(text)).toContain(
|
||||||
|
jasmine.objectContaining({
|
||||||
|
lineNumber: 2,
|
||||||
|
token: '\t',
|
||||||
|
reason: 'tab_character',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not flag tabs on non chord lines', () => {
|
||||||
|
const service: TextRenderingService = TestBed.inject(TextRenderingService);
|
||||||
|
const text = 'Strophe\nDas\tist normaler Text\nText';
|
||||||
|
|
||||||
|
void expect(service.validateChordNotation(text)).toEqual([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,11 +8,13 @@ import {Chord, ChordAddDescriptor, ChordValidationIssue} from './chord';
|
|||||||
import {Line} from './line';
|
import {Line} from './line';
|
||||||
|
|
||||||
interface ParsedValidationToken {
|
interface ParsedValidationToken {
|
||||||
|
prefix: string;
|
||||||
root: string;
|
root: string;
|
||||||
suffix: string;
|
suffix: string;
|
||||||
slashChord: string | null;
|
slashChord: string | null;
|
||||||
rootWasAlias: boolean;
|
rootWasAlias: boolean;
|
||||||
slashWasAlias: boolean;
|
slashWasAlias: boolean;
|
||||||
|
tokenSuffix: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ChordLineValidationResult {
|
interface ChordLineValidationResult {
|
||||||
@@ -225,8 +227,15 @@ export class TextRenderingService {
|
|||||||
return {issues: [], isChordLike: false};
|
return {issues: [], isChordLike: false};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const issues: ChordValidationIssue[] = [];
|
||||||
|
if (line.includes('\t')) {
|
||||||
|
issues.push(this.createTabCharacterIssue(line, lineNumber));
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
issues: parsedTokens
|
issues: [
|
||||||
|
...issues,
|
||||||
|
...parsedTokens
|
||||||
.map(entry => {
|
.map(entry => {
|
||||||
if (!entry.parsed) {
|
if (!entry.parsed) {
|
||||||
return this.createUnknownTokenIssue(line, lineNumber, entry.token);
|
return this.createUnknownTokenIssue(line, lineNumber, entry.token);
|
||||||
@@ -235,6 +244,7 @@ export class TextRenderingService {
|
|||||||
return this.createValidationIssue(line, lineNumber, entry.token, entry.parsed);
|
return this.createValidationIssue(line, lineNumber, entry.token, entry.parsed);
|
||||||
})
|
})
|
||||||
.filter((issue): issue is ChordValidationIssue => issue !== null),
|
.filter((issue): issue is ChordValidationIssue => issue !== null),
|
||||||
|
],
|
||||||
isChordLike: true,
|
isChordLike: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -244,18 +254,19 @@ export class TextRenderingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private parseChordToken(token: string, position: number): Chord | null {
|
private parseChordToken(token: string, position: number): Chord | null {
|
||||||
const root = this.readChordRoot(token, 0);
|
const decoratedToken = this.splitTokenDecorators(token);
|
||||||
|
const root = this.readChordRoot(decoratedToken.core, 0);
|
||||||
if (!root) {
|
if (!root) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let cursor = root.length;
|
let cursor = root.length;
|
||||||
const suffix = this.readChordSuffix(token, cursor);
|
const suffix = this.readChordSuffix(decoratedToken.core, cursor);
|
||||||
cursor += suffix.length;
|
cursor += suffix.length;
|
||||||
|
|
||||||
let slashChord: string | null = null;
|
let slashChord: string | null = null;
|
||||||
if (token[cursor] === '/') {
|
if (decoratedToken.core[cursor] === '/') {
|
||||||
const slash = this.readChordRoot(token, cursor + 1);
|
const slash = this.readChordRoot(decoratedToken.core, cursor + 1);
|
||||||
if (!slash) {
|
if (!slash) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -263,7 +274,7 @@ export class TextRenderingService {
|
|||||||
cursor += 1 + slash.length;
|
cursor += 1 + slash.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cursor !== token.length) {
|
if (cursor !== decoratedToken.core.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,6 +285,8 @@ export class TextRenderingService {
|
|||||||
slashChord,
|
slashChord,
|
||||||
add: suffix || null,
|
add: suffix || null,
|
||||||
addDescriptor: this.parseChordAddDescriptor(suffix || null),
|
addDescriptor: this.parseChordAddDescriptor(suffix || null),
|
||||||
|
prefix: decoratedToken.prefix || undefined,
|
||||||
|
suffix: decoratedToken.suffix || undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,19 +363,20 @@ export class TextRenderingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private parseValidationToken(token: string): ParsedValidationToken | null {
|
private parseValidationToken(token: string): ParsedValidationToken | null {
|
||||||
const root = this.readValidationChordRoot(token, 0);
|
const decoratedToken = this.splitTokenDecorators(token);
|
||||||
|
const root = this.readValidationChordRoot(decoratedToken.core, 0);
|
||||||
if (!root) {
|
if (!root) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let cursor = root.length;
|
let cursor = root.length;
|
||||||
const suffix = this.readValidationChordSuffix(token, cursor);
|
const suffix = this.readValidationChordSuffix(decoratedToken.core, cursor);
|
||||||
cursor += suffix.length;
|
cursor += suffix.length;
|
||||||
|
|
||||||
let slashChord: string | null = null;
|
let slashChord: string | null = null;
|
||||||
let slashWasAlias = false;
|
let slashWasAlias = false;
|
||||||
if (token[cursor] === '/') {
|
if (decoratedToken.core[cursor] === '/') {
|
||||||
const slash = this.readValidationChordRoot(token, cursor + 1);
|
const slash = this.readValidationChordRoot(decoratedToken.core, cursor + 1);
|
||||||
if (!slash) {
|
if (!slash) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -371,16 +385,18 @@ export class TextRenderingService {
|
|||||||
cursor += 1 + slash.length;
|
cursor += 1 + slash.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cursor !== token.length) {
|
if (cursor !== decoratedToken.core.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
prefix: decoratedToken.prefix,
|
||||||
root: root.root,
|
root: root.root,
|
||||||
suffix,
|
suffix,
|
||||||
slashChord,
|
slashChord,
|
||||||
rootWasAlias: root.wasAlias,
|
rootWasAlias: root.wasAlias,
|
||||||
slashWasAlias,
|
slashWasAlias,
|
||||||
|
tokenSuffix: decoratedToken.suffix,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,6 +465,17 @@ export class TextRenderingService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private createTabCharacterIssue(line: string, lineNumber: number): ChordValidationIssue {
|
||||||
|
return {
|
||||||
|
lineNumber,
|
||||||
|
lineText: line,
|
||||||
|
token: '\t',
|
||||||
|
suggestion: null,
|
||||||
|
reason: 'tab_character',
|
||||||
|
message: 'Tabulatoren sind in Akkordzeilen nicht erlaubt, bitte Leerzeichen verwenden',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private getCanonicalChordToken(parsed: ParsedValidationToken): string {
|
private getCanonicalChordToken(parsed: ParsedValidationToken): string {
|
||||||
const descriptor = this.parseChordAddDescriptor(parsed.suffix);
|
const descriptor = this.parseChordAddDescriptor(parsed.suffix);
|
||||||
const normalizedSuffix = this.normalizeSuffix(parsed.suffix);
|
const normalizedSuffix = this.normalizeSuffix(parsed.suffix);
|
||||||
@@ -468,7 +495,7 @@ export class TextRenderingService {
|
|||||||
? (this.isMinorRoot(root) ? this.toMinorRoot(parsed.slashChord) : this.toMajorRoot(parsed.slashChord))
|
? (this.isMinorRoot(root) ? this.toMinorRoot(parsed.slashChord) : this.toMajorRoot(parsed.slashChord))
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return root + suffix + (slashChord ? `/${slashChord}` : '');
|
return parsed.prefix + root + suffix + (slashChord ? `/${slashChord}` : '') + parsed.tokenSuffix;
|
||||||
}
|
}
|
||||||
|
|
||||||
private normalizeSuffix(suffix: string): string {
|
private normalizeSuffix(suffix: string): string {
|
||||||
@@ -531,6 +558,20 @@ export class TextRenderingService {
|
|||||||
return root[0].toUpperCase() + root.slice(1);
|
return root[0].toUpperCase() + root.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private splitTokenDecorators(token: string): {prefix: string; core: string; suffix: string} {
|
||||||
|
const prefixMatch = token.match(/^\(+/);
|
||||||
|
const suffixMatch = token.match(/\)+$/);
|
||||||
|
const prefix = prefixMatch?.[0] ?? '';
|
||||||
|
const suffix = suffixMatch?.[0] ?? '';
|
||||||
|
const core = token.slice(prefix.length, token.length - suffix.length);
|
||||||
|
|
||||||
|
return {
|
||||||
|
prefix,
|
||||||
|
core,
|
||||||
|
suffix,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private parseChordAddDescriptor(suffix: string | null): ChordAddDescriptor | null {
|
private parseChordAddDescriptor(suffix: string | null): ChordAddDescriptor | null {
|
||||||
if (!suffix) {
|
if (!suffix) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -174,8 +174,10 @@ export class TransposeService {
|
|||||||
// Do not replace this with the original token without explicit product approval.
|
// Do not replace this with the original token without explicit product approval.
|
||||||
const renderedChord = scaleMapping[chord.chord] ?? 'X';
|
const renderedChord = scaleMapping[chord.chord] ?? 'X';
|
||||||
const renderedSlashChord = chord.slashChord ? (scaleMapping[chord.slashChord] ?? 'X') : '';
|
const renderedSlashChord = chord.slashChord ? (scaleMapping[chord.slashChord] ?? 'X') : '';
|
||||||
|
const prefix = chord.prefix ?? '';
|
||||||
|
const suffix = chord.suffix ?? '';
|
||||||
|
|
||||||
return renderedChord + (chord.add ?? '') + (renderedSlashChord ? '/' + renderedSlashChord : '');
|
return prefix + renderedChord + (chord.add ?? '') + (renderedSlashChord ? '/' + renderedSlashChord : '') + suffix;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getSemitone(key: string): number | undefined {
|
private getSemitone(key: string): number | undefined {
|
||||||
|
|||||||
Reference in New Issue
Block a user