text rendering component

This commit is contained in:
2020-03-21 23:37:37 +01:00
committed by smuddy
parent 0cb8875f8d
commit 4cd9222a8a
9 changed files with 201 additions and 14 deletions

View File

@@ -35,9 +35,13 @@ Cool bridge without any chords
const service: TextRenderingService = TestBed.get(TextRenderingService); const service: TextRenderingService = TestBed.get(TextRenderingService);
const sections = service.parse(testText); const sections = service.parse(testText);
expect(sections[0].type).toBe(SectionType.Verse); expect(sections[0].type).toBe(SectionType.Verse);
expect(sections[0].number).toBe(0);
expect(sections[1].type).toBe(SectionType.Verse); expect(sections[1].type).toBe(SectionType.Verse);
expect(sections[1].number).toBe(1);
expect(sections[2].type).toBe(SectionType.Chorus); expect(sections[2].type).toBe(SectionType.Chorus);
expect(sections[2].number).toBe(0);
expect(sections[3].type).toBe(SectionType.Bridge); expect(sections[3].type).toBe(SectionType.Bridge);
expect(sections[3].number).toBe(0);
}); });
it('should parse text lines', () => { it('should parse text lines', () => {
@@ -60,6 +64,7 @@ Cool bridge without any chords
it('should parse chord lines', () => { it('should parse chord lines', () => {
const service: TextRenderingService = TestBed.get(TextRenderingService); const service: TextRenderingService = TestBed.get(TextRenderingService);
const sections = service.parse(testText); const sections = service.parse(testText);
console.log(sections);
expect(sections[0].lines[0].type).toBe(LineType.chord); expect(sections[0].lines[0].type).toBe(LineType.chord);
expect(sections[0].lines[0].text).toBe('C D E F G A H'); expect(sections[0].lines[0].text).toBe('C D E F G A H');
expect(sections[0].lines[2].type).toBe(LineType.chord); expect(sections[0].lines[2].type).toBe(LineType.chord);
@@ -72,7 +77,6 @@ Cool bridge without any chords
expect(sections[2].lines[0].text).toBe('c c# db c7 cmaj7 c/e'); expect(sections[2].lines[0].text).toBe('c c# db c7 cmaj7 c/e');
// c c# db c7 cmaj7 c/e // c c# db c7 cmaj7 c/e
console.log(sections[2].lines[0].chords);
expect(sections[2].lines[0].chords).toEqual([ expect(sections[2].lines[0].chords).toEqual([
{chord: 'c', length: 2, position: 0}, {chord: 'c', length: 2, position: 0},
{chord: 'c#', length: 3, position: 2}, {chord: 'c#', length: 3, position: 2},

View File

@@ -35,8 +35,6 @@ export interface Section {
providedIn: 'root' providedIn: 'root'
}) })
export class TextRenderingService { export class TextRenderingService {
private regexSection = /(Strophe|Refrain|Bridge)/; private regexSection = /(Strophe|Refrain|Bridge)/;
constructor() { constructor() {
@@ -44,29 +42,40 @@ export class TextRenderingService {
public parse(text: string): Section[] { public parse(text: string): Section[] {
const arrayOfLines = text.split(/\r?\n/).filter(_ => _); const arrayOfLines = text.split(/\r?\n/).filter(_ => _);
return arrayOfLines.reduce((array, line) => { const indices = {
[SectionType.Bridge]: 0,
[SectionType.Chorus]: 0,
[SectionType.Verse]: 0,
};
const sections = arrayOfLines.reduce((array, line) => {
const type = this.getSectionTypeOfLine(line);
if (line.match(this.regexSection)) return [...array, { if (line.match(this.regexSection)) return [...array, {
type: this.getSectionTypeOfLine(line), type: type,
number: -1, number: indices[type]++,
lines: [] lines: []
}]; }];
array[array.length - 1].lines.push(this.getLineOfLineText(line)); array[array.length - 1].lines.push(this.getLineOfLineText(line));
return array; return array;
}, [] as Section[]); }, [] as Section[]);
console.log(indices);
return sections;
} }
private getLineOfLineText(text: string): Line { private getLineOfLineText(text: string): Line {
const regex = /\b(C#|C|Db|D#|D|Eb|E|F#|F|Gb|G#|G|Ab|A#|A|B|H|c#|c|db|d#|d|eb|e|f#|f|gb|g#|g|ab|a#|a|b|h)(\/(C#|C|Db|D#|D|Eb|E|F#|F|Gb|G#|G|Ab|A#|A|B|H|c#|c|db|d#|d|eb|e|f#|f|gb|g#|g|ab|a#|a|b|h))?(\d+|maj7)?.?\b/mg; if (!text) return null;
const cords = this.readChords(text);
const match = text.match(regex); const hasMatches = cords.length > 0;
const hasMatches = !!match;
const type = hasMatches ? LineType.chord : LineType.text; const type = hasMatches ? LineType.chord : LineType.text;
return {type, text, chords: hasMatches ? this.readChords(text) : undefined} return {type, text, chords: hasMatches ? cords : undefined}
} }
private getSectionTypeOfLine(line: string): SectionType { private getSectionTypeOfLine(line: string): SectionType {
const typeString = line.match(this.regexSection)[1]; if (!line) return null;
const match = line.match(this.regexSection);
if (!match || match.length < 2) return null;
const typeString = match[1];
switch (typeString) { switch (typeString) {
case "Strophe": case "Strophe":
return SectionType.Verse; return SectionType.Verse;
@@ -81,7 +90,8 @@ export class TextRenderingService {
let match; let match;
const chords: Chord[] = []; const chords: Chord[] = [];
const regex = /\b(C#|C|Db|D#|D|Eb|E|F#|F|Gb|G#|G|Ab|A#|A|B|H|c#|c|db|d#|d|eb|e|f#|f|gb|g#|g|ab|a#|a|b|h)(\/(C#|C|Db|D#|D|Eb|E|F#|F|Gb|G#|G|Ab|A#|A|B|H|c#|c|db|d#|d|eb|e|f#|f|gb|g#|g|ab|a#|a|b|h))?(\d+|maj7)?.?\b/mg; // https://regex101.com/r/68jMB8/3
const regex = /\b(C#|C|Db|D#|D|Eb|E|F#|F|Gb|G#|G|Ab|A#|A|B|H|c#|c|db|d#|d|eb|e|f#|f|gb|g#|g|ab|a#|a|b|h)(\/(C#|C|Db|D#|D|Eb|E|F#|F|Gb|G#|G|Ab|A#|A|B|H|c#|c|db|d#|d|eb|e|f#|f|gb|g#|g|ab|a#|a|b|h))?(\d+|maj7)?\s?\b/mg;
while ((match = regex.exec(chordLine)) !== null) { while ((match = regex.exec(chordLine)) !== null) {
const chord: Chord = { const chord: Chord = {

View File

@@ -15,7 +15,10 @@
</div> </div>
</div> </div>
<div class="text">{{song.text}}</div> <!-- <div class="text">{{song.text}}</div>-->
<app-song-text [text]="song.text"></app-song-text>
<div class="text">{{song.comment}}</div> <div class="text">{{song.comment}}</div>
<div> <div>
<h3>Anhänge</h3> <h3>Anhänge</h3>

View File

@@ -7,6 +7,7 @@ import {MatButtonModule} from '@angular/material/button';
import {ButtonRowModule} from '../../../widget-modules/components/button-row/button-row.module'; import {ButtonRowModule} from '../../../widget-modules/components/button-row/button-row.module';
import {RouterModule} from '@angular/router'; import {RouterModule} from '@angular/router';
import {LegalOwnerTranslatorModule} from '../../../widget-modules/pipes/legal-owner-translator/legal-owner-translator.module'; import {LegalOwnerTranslatorModule} from '../../../widget-modules/pipes/legal-owner-translator/legal-owner-translator.module';
import {SongTextModule} from '../../../widget-modules/components/song-text/song-text.module';
@NgModule({ @NgModule({
@@ -21,6 +22,7 @@ import {LegalOwnerTranslatorModule} from '../../../widget-modules/pipes/legal-ow
MatButtonModule, MatButtonModule,
ButtonRowModule, ButtonRowModule,
LegalOwnerTranslatorModule, LegalOwnerTranslatorModule,
SongTextModule,
] ]
}) })
export class SongModule { export class SongModule {

View File

@@ -0,0 +1,12 @@
<div [class.chords]="chordMode!=='hide'" class="song-text">
<div (click)="onChordClick()" class="menu">
<fa-icon [icon]="faLines"></fa-icon>
</div>
<div *ngFor="let section of sections" [class.chorus]="section.type===1" class="section">
<div *ngFor="let line of getLines(section)" [class.chord]="line.type===0" class="line">
{{line.text}}
</div>
</div>
</div>

View File

@@ -0,0 +1,48 @@
.song-text {
white-space: pre-wrap;
position: relative;
&.chords {
font-family: 'Ubuntu Mono', monospace;
}
&:hover .menu {
opacity: 1;
}
}
.menu {
position: absolute;
right: 0;
background: #eee;
border-radius: 8px;
padding: 5px;
width: 20px;
height: 20px;
display: flex;
justify-content: center;
align-items: center;
transition: 300ms all ease-in-out;
cursor: pointer;
opacity: 0;
&:hover {
background: #ddd;
}
}
.section {
margin-bottom: 20px;
border-left: 3px solid #ddd;
}
.chorus {
margin-left: 20px;
border-left: none;
}
.chord {
font-weight: bold;
}

View File

@@ -0,0 +1,25 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {SongTextComponent} from './song-text.component';
describe('SongTextComponent', () => {
let component: SongTextComponent;
let fixture: ComponentFixture<SongTextComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [SongTextComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SongTextComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,67 @@
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {
Line,
LineType,
Section,
SectionType,
TextRenderingService
} from '../../../modules/songs/services/text-rendering.service';
import {faGripLines} from '@fortawesome/free-solid-svg-icons/faGripLines';
export type ChordMode = 'show' | 'hide' | 'onlyFirst'
@Component({
selector: 'app-song-text',
templateUrl: './song-text.component.html',
styleUrls: ['./song-text.component.less']
})
export class SongTextComponent implements OnInit {
public sections: Section[];
public chordMode: ChordMode = 'hide';
@Output() public chordModeChanged = new EventEmitter<ChordMode>();
public faLines = faGripLines;
constructor(private textRenderingService: TextRenderingService) {
}
@Input()
public set text(value: string) {
this.sections = this.textRenderingService.parse(value).sort((a, b) => a.type - b.type);
console.log(this.sections)
}
ngOnInit(): void {
}
public getLines(section: Section): Line[] {
return section.lines.filter(_ => {
if (_.type !== LineType.chord) return true;
switch (this.chordMode) {
case 'show':
return true;
case 'hide':
return false;
case 'onlyFirst':
return section.number === 0 || section.type !== SectionType.Verse;
}
});
}
public onChordClick(): void {
const next = this.getNextChordMode();
this.chordMode = next;
this.chordModeChanged.next(next);
}
private getNextChordMode(): ChordMode {
switch (this.chordMode) {
case 'show':
return 'hide';
case 'hide':
return 'onlyFirst';
case 'onlyFirst':
return 'show';
}
}
}

View File

@@ -0,0 +1,16 @@
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {SongTextComponent} from './song-text.component';
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
@NgModule({
declarations: [SongTextComponent],
exports: [SongTextComponent],
imports: [
CommonModule,
FontAwesomeModule
]
})
export class SongTextModule {
}