text rendering component
This commit is contained in:
@@ -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},
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user