diff --git a/src/app/app.config.ts b/src/app/app.config.ts index e30f7195b..0f7459437 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -4,8 +4,9 @@ import { TranslateService } from '@ngx-translate/core'; import { forkJoin } from 'rxjs'; import { map, switchMap } from 'rxjs/operators'; import { EntitiesSelectItemGroup } from './components/entities-select/entities-select.component'; -import { ViewMode, ViewModeId } from './models/evt-models'; +import { AnalogueClass, SourceClass, ViewMode, ViewModeId } from './models/evt-models'; import { Attributes, EditorialConventionLayout } from './models/evt-models'; +import { updateCSS } from './utils/dom-utils'; @Injectable() export class AppConfig { @@ -31,6 +32,7 @@ export class AppConfig { ]).pipe( map(([ui, edition, editorialConventions]) => { console.log(ui, edition, files); + this.updateStyleFromConfig(edition); // Handle default values => TODO: Decide how to handle defaults!! if (ui.defaultLocalization) { if (ui.availableLanguages.find((l) => l.code === ui.defaultLocalization && l.enable)) { @@ -53,7 +55,24 @@ export class AppConfig { }); }); } + + /** + * Update once general css with values from config, + * this way we don't need to inject a style property in each element + * @param edition EditionConfig + */ + updateStyleFromConfig(edition: EditionConfig) { + const rules = []; + rules['.'+AnalogueClass+' .opened'] = `background-color: ${edition.readingColorDark}`; + rules['.'+SourceClass+' .opened'] = `background-color: ${edition.readingColorDark}`; + rules['.'+AnalogueClass+':hover'] = `background-color: ${edition.readingColorLight}; cursor:pointer`; + rules['.'+SourceClass+':hover'] = `background-color: ${edition.readingColorLight}; cursor:pointer`; + Object.entries(rules).forEach(([selector,style]) => { updateCSS([[selector,style]]) }); + console.log('style from config',rules); + } + } + export interface EVTConfig { ui: UiConfig; edition: EditionConfig; @@ -99,6 +118,20 @@ export interface EditionConfig { verseNumberPrinter: number; readingColorLight: string; readingColorDark: string; + externalBibliography: Partial<{ + biblAttributeToMatch: string, + elementAttributesToMatch: string[] + }>; + biblView: Partial<{ + propToShow: string[]; + showAttrNames: boolean; + showEmptyValues: boolean; + inline: boolean; + comaSeparated: boolean; + showMainElemTextContent: boolean; + }>; + analogueMarkers: string[]; + sourcesExcludedFromListByParent: string[]; } export type EditionImagesSources = 'manifest' | 'graphics'; diff --git a/src/app/app.module.ts b/src/app/app.module.ts index a131051b3..b026ea597 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -120,6 +120,16 @@ import { TextSourcesComponent } from './view-modes/text-sources/text-sources.com import { TextTextComponent } from './view-modes/text-text/text-text.component'; import { TextVersionsComponent } from './view-modes/text-versions/text-versions.component'; import { HandleImgErrorDirective } from './directives/handle-img-error.directive'; +import { CriticalApparatusComponent } from './components/critical-apparatus/critical-apparatus.component'; +import { AnaloguesComponent } from './components/analogues/analogues.component'; +import { SourcesComponent } from './components/sources/sources.component'; +import { SourceDetailComponent } from './components/sources/source-detail/source-detail.component'; +import { SourceNoteComponent } from './components/sources/source-note/source-note.component'; +import { QuoteEntryComponent } from './components/quote-entry/quote-entry.component'; +import { AnalogueEntryComponent } from './components/analogues/analogue-entry/analogue-entry.component'; +import { AnalogueDetailComponent } from './components/analogues/analogue-detail/analogue-detail.component'; +import { BiblioEntryComponent } from './components/biblio/biblio.component'; +import { BiblioListComponent } from './components/biblioList/biblio-list.component'; const routes: Routes = [ ]; @@ -181,6 +191,8 @@ const DynamicComponents = [ VerseComponent, VersesGroupComponent, WordComponent, + QuoteEntryComponent, + AnalogueEntryComponent, ]; @NgModule({ @@ -227,6 +239,14 @@ const DynamicComponents = [ VersionPanelComponent, WitnessPanelComponent, XmlBeautifyPipe, + CriticalApparatusComponent, + AnaloguesComponent, + AnalogueDetailComponent, + SourceDetailComponent, + SourceNoteComponent, + SourcesComponent, + BiblioEntryComponent, + BiblioListComponent, ...DynamicComponents, ], imports: [ diff --git a/src/app/components/analogues/analogue-detail/analogue-detail.component.html b/src/app/components/analogues/analogue-detail/analogue-detail.component.html new file mode 100644 index 000000000..9411c11ee --- /dev/null +++ b/src/app/components/analogues/analogue-detail/analogue-detail.component.html @@ -0,0 +1,92 @@ +
+ +
+ + + + + + + + +
+ + + +
+ diff --git a/src/app/components/analogues/analogue-detail/analogue-detail.component.scss b/src/app/components/analogues/analogue-detail/analogue-detail.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/components/analogues/analogue-detail/analogue-detail.component.spec.ts b/src/app/components/analogues/analogue-detail/analogue-detail.component.spec.ts new file mode 100644 index 000000000..667fb6040 --- /dev/null +++ b/src/app/components/analogues/analogue-detail/analogue-detail.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AnalogueDetailComponent } from './analogue-detail.component'; + +describe('AnalogueDetailComponent', () => { + let component: AnalogueDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ AnalogueDetailComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(AnalogueDetailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/analogues/analogue-detail/analogue-detail.component.ts b/src/app/components/analogues/analogue-detail/analogue-detail.component.ts new file mode 100644 index 000000000..6091b0fce --- /dev/null +++ b/src/app/components/analogues/analogue-detail/analogue-detail.component.ts @@ -0,0 +1,28 @@ +import { Component, Input } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import { EditionLevelType } from 'src/app/app.config'; + +@Component({ + selector: 'evt-analogue-detail', + templateUrl: './analogue-detail.component.html', + styleUrls: ['./analogue-detail.component.scss','../../sources/sources.component.scss'], +}) +export class AnalogueDetailComponent { + + private edLevel: EditionLevelType; + + @Input() analogue; + + @Input() set editionLevel(el: EditionLevelType) { + this.edLevel = el; + this.editionLevelChange.next(el); + } + get editionLevel() { return this.edLevel; } + editionLevelChange = new BehaviorSubject(''); + + stopPropagation(e: MouseEvent) { + e.stopPropagation(); + } + +} + diff --git a/src/app/components/analogues/analogue-entry/analogue-entry.component.html b/src/app/components/analogues/analogue-entry/analogue-entry.component.html new file mode 100644 index 000000000..eaf9d8641 --- /dev/null +++ b/src/app/components/analogues/analogue-entry/analogue-entry.component.html @@ -0,0 +1,21 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/app/components/analogues/analogue-entry/analogue-entry.component.scss b/src/app/components/analogues/analogue-entry/analogue-entry.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/components/analogues/analogue-entry/analogue-entry.component.spec.ts b/src/app/components/analogues/analogue-entry/analogue-entry.component.spec.ts new file mode 100644 index 000000000..88ab07b8d --- /dev/null +++ b/src/app/components/analogues/analogue-entry/analogue-entry.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; + +import { AnalogueEntryComponent } from './analogue-entry.component'; + +describe('AnalogueEntryComponent', () => { + let component: AnalogueEntryComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ AnalogueEntryComponent ], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AnalogueEntryComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/analogues/analogue-entry/analogue-entry.component.ts b/src/app/components/analogues/analogue-entry/analogue-entry.component.ts new file mode 100644 index 000000000..8d86aeeed --- /dev/null +++ b/src/app/components/analogues/analogue-entry/analogue-entry.component.ts @@ -0,0 +1,96 @@ +import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; +import { BehaviorSubject, Subject } from 'rxjs'; +import { scan, startWith } from 'rxjs/operators'; + +import { EditorialConventionLayoutData } from '../../../directives/editorial-convention-layout.directive'; + +import { Analogue, AnalogueClass, Note } from '../../../models/evt-models'; +import { register } from '../../../services/component-register.service'; +import { EVTStatusService } from '../../../services/evt-status.service'; +import { EditionLevelType } from 'src/app/app.config'; + +export interface AnalogueEntryComponent { } +@register(Analogue) +@Component({ + selector: 'evt-analogue-entry', + templateUrl: './analogue-entry.component.html', + styleUrls: ['./analogue-entry.component.scss','../../sources/sources.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AnalogueEntryComponent implements OnInit { + + public _data: Analogue; + + private edLevel: EditionLevelType; + + @Input() set data(dt: Analogue) { + this._data = dt; + } + + @Input() set editionLevel(el: EditionLevelType) { + this.edLevel = el; + this.editionLevelChange.next(el); + } + get editionLevel() { return this.edLevel; } + editionLevelChange = new BehaviorSubject(''); + + get editorialConventionData(): EditorialConventionLayoutData { + return { + name: '.analogues', + attributes: this.data?.attributes || {}, + editionLevel: this.editionLevel, + defaultsKey: '.analogues', + }; + } + + get data() { return this._data; } + + public analogueClass = AnalogueClass; + + public dataForNote = {}; + + public opened = false; + + toggleOpened$ = new Subject(); + opened$ = this.toggleOpened$.pipe( + scan((currentState: boolean, val: boolean | undefined) => val === undefined ? !currentState : val, false), + startWith(false), + ); + + toggleAppEntryBox(e: MouseEvent) { + e.stopPropagation(); + this.opened = !this.opened; + } + + closeAppEntryBox() { + this.opened = false; + } + + stopPropagation(e: MouseEvent) { + e.stopPropagation(); + } + + /** If the element has no text then it's displayed as a note.*/ + createNote(v): Note|{} { + + return { + type: Note, + noteType: 'analogue', + noteLayout: 'popover', + exponent: v.path || '', + content: v.extLinkedElements.concat(v.extSources, v.sources) || {}, + attributes: v.attributes || [], + } + } + + ngOnInit() { + if (this.data.text.length === 0) { + this.dataForNote = this.createNote(this.data); + } + } + + constructor( + public evtStatusService: EVTStatusService, + ) {} + +} diff --git a/src/app/components/analogues/analogues.component.html b/src/app/components/analogues/analogues.component.html new file mode 100644 index 000000000..57d29ed4f --- /dev/null +++ b/src/app/components/analogues/analogues.component.html @@ -0,0 +1,10 @@ + + + + + + + diff --git a/src/app/components/analogues/analogues.component.scss b/src/app/components/analogues/analogues.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/components/analogues/analogues.component.spec.ts b/src/app/components/analogues/analogues.component.spec.ts new file mode 100644 index 000000000..fadcc1511 --- /dev/null +++ b/src/app/components/analogues/analogues.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AnaloguesComponent } from './analogues.component'; + +describe('AnaloguesComponent', () => { + let component: AnaloguesComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ AnaloguesComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(AnaloguesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/analogues/analogues.component.ts b/src/app/components/analogues/analogues.component.ts new file mode 100644 index 000000000..470e13dfb --- /dev/null +++ b/src/app/components/analogues/analogues.component.ts @@ -0,0 +1,47 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import { EditionLevelType } from 'src/app/app.config'; +import { AnalogueClass } from 'src/app/models/evt-models'; +import { EVTStatusService } from 'src/app/services/evt-status.service'; + +@Component({ + selector: 'evt-analogues', + templateUrl: './analogues.component.html', + styleUrls: ['./analogues.component.scss','../sources/sources.component.scss'], +}) +export class AnaloguesComponent implements OnInit { + + private edLevel: EditionLevelType; + + public analogues; + private appClasses = [AnalogueClass]; + + public analoguesInCurrentPage = this.evtStatusService.getPageElementsByClassList(this.appClasses) + + @Input() pageID : string; + + @Input() set editionLevel(el: EditionLevelType) { + this.edLevel = el; + this.editionLevelChange.next(el); + } + get editionLevel() { return this.edLevel; } + editionLevelChange = new BehaviorSubject(''); + + stopPropagation(e: MouseEvent) { + e.stopPropagation(); + } + + public getEntries(data) { + this.analogues = data.flat(); + } + + constructor( + public evtStatusService: EVTStatusService, + ) {} + + ngOnInit() { + this.analoguesInCurrentPage.subscribe({ next: (data) => { this.getEntries(data) } }); + } + +} + diff --git a/src/app/components/apparatus-entry/apparatus-entry-detail/apparatus-entry-detail.component.scss b/src/app/components/apparatus-entry/apparatus-entry-detail/apparatus-entry-detail.component.scss index fa1d82dcf..af15be21a 100644 --- a/src/app/components/apparatus-entry/apparatus-entry-detail/apparatus-entry-detail.component.scss +++ b/src/app/components/apparatus-entry/apparatus-entry-detail/apparatus-entry-detail.component.scss @@ -4,6 +4,7 @@ top: -0.063rem; z-index: 0; border-radius: 0; + margin-bottom:0.5em; cursor: auto; @include themify($themes) { background-color: themed("appEntryBoxBackground"); @@ -14,12 +15,14 @@ .app-detail-tabs { background-color: transparent; font-size: 1.063rem; + font-family: Junicode, Times, serif; } .app-detail-content { display: flex; justify-content: space-between; padding: 0.313rem; + background-color: #2042512e; } .app-detail-readings { diff --git a/src/app/components/apparatus-entry/apparatus-entry-readings/apparatus-entry-readings.component.html b/src/app/components/apparatus-entry/apparatus-entry-readings/apparatus-entry-readings.component.html index ae80f0966..6ec6cab4d 100644 --- a/src/app/components/apparatus-entry/apparatus-entry-readings/apparatus-entry-readings.component.html +++ b/src/app/components/apparatus-entry/apparatus-entry-readings/apparatus-entry-readings.component.html @@ -10,7 +10,7 @@ - {{ witID }} + {{ witID }} ] @@ -20,6 +20,6 @@ omit. - {{ wit }} + {{ wit }} \ No newline at end of file diff --git a/src/app/components/apparatus-entry/apparatus-entry-readings/apparatus-entry-readings.component.scss b/src/app/components/apparatus-entry/apparatus-entry-readings/apparatus-entry-readings.component.scss index 2b42b591a..de797ca71 100644 --- a/src/app/components/apparatus-entry/apparatus-entry-readings/apparatus-entry-readings.component.scss +++ b/src/app/components/apparatus-entry/apparatus-entry-readings/apparatus-entry-readings.component.scss @@ -2,4 +2,10 @@ display: inline-block; padding-right: 0.5rem; line-height: 1; +} + +.appa-witn { + font-size: 90%; + font-weight: 600; + font-family: serif; } \ No newline at end of file diff --git a/src/app/components/biblio/biblio.component.html b/src/app/components/biblio/biblio.component.html new file mode 100644 index 000000000..fb2f9857f --- /dev/null +++ b/src/app/components/biblio/biblio.component.html @@ -0,0 +1,14 @@ + + + + + {{element.value}}: + + {{data[element.value]}} + , + +
+
+
+ {{data.text}} +
\ No newline at end of file diff --git a/src/app/components/biblio/biblio.component.scss b/src/app/components/biblio/biblio.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/components/biblio/biblio.component.spec.ts b/src/app/components/biblio/biblio.component.spec.ts new file mode 100644 index 000000000..b645f0667 --- /dev/null +++ b/src/app/components/biblio/biblio.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { BiblioEntryComponent } from './biblio.component'; + +describe('BiblioComponent', () => { + let component: BiblioEntryComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ BiblioEntryComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(BiblioEntryComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/biblio/biblio.component.ts b/src/app/components/biblio/biblio.component.ts new file mode 100644 index 000000000..a0dd383df --- /dev/null +++ b/src/app/components/biblio/biblio.component.ts @@ -0,0 +1,38 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { AppConfig } from 'src/app/app.config'; +import { BibliographicEntry } from 'src/app/models/evt-models'; + +@Component({ + selector: 'evt-biblio-entry', + templateUrl: './biblio.component.html', + styleUrls: ['./biblio.component.scss'], +}) +export class BiblioEntryComponent implements OnInit { + + @Input() data: BibliographicEntry; + + public showList = AppConfig.evtSettings.edition.biblView.propToShow; + public showAttrNames = AppConfig.evtSettings.edition.biblView.showAttrNames; + public showEmptyValues = AppConfig.evtSettings.edition.biblView.showEmptyValues; + public inline = AppConfig.evtSettings.edition.biblView.inline; + public comaSeparated = AppConfig.evtSettings.edition.biblView.comaSeparated; + public showMainElemTextContent = AppConfig.evtSettings.edition.biblView.showMainElemTextContent; + + public showSpan = this.getSpanStyle(); + + public getSpanStyle() { + if (this.comaSeparated) { + return { + 'margin-left': '-0.1rem', + 'display': 'inline-block', + }; + } + + return { 'display': 'none' }; + } + + ngOnInit() { + } + +} + diff --git a/src/app/components/biblioList/biblio-list.component.html b/src/app/components/biblioList/biblio-list.component.html new file mode 100644 index 000000000..8bcb6b37d --- /dev/null +++ b/src/app/components/biblioList/biblio-list.component.html @@ -0,0 +1,49 @@ + + + + + + + + + +
    + + + + +
  • +
    + + + +
+
+ + + + + {{elem.head}} + +
    + +
  • +
    +
+
+ + + +
    +
  • +
+
+ + + +
    +
  • +
+
+ +
\ No newline at end of file diff --git a/src/app/components/biblioList/biblio-list.component.scss b/src/app/components/biblioList/biblio-list.component.scss new file mode 100644 index 000000000..6f5dee953 --- /dev/null +++ b/src/app/components/biblioList/biblio-list.component.scss @@ -0,0 +1,3 @@ +.bibl-head { + margin-left:2rem; +} \ No newline at end of file diff --git a/src/app/components/biblioList/biblio-list.component.spec.ts b/src/app/components/biblioList/biblio-list.component.spec.ts new file mode 100644 index 000000000..783c67d9a --- /dev/null +++ b/src/app/components/biblioList/biblio-list.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { BiblioListComponent } from './biblio-list.component'; + +describe('BiblioListComponent', () => { + let component: BiblioListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ BiblioListComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(BiblioListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/biblioList/biblio-list.component.ts b/src/app/components/biblioList/biblio-list.component.ts new file mode 100644 index 000000000..dc7c65672 --- /dev/null +++ b/src/app/components/biblioList/biblio-list.component.ts @@ -0,0 +1,13 @@ +import { Component, Input } from '@angular/core'; +import { BibliographicList } from '../../models/evt-models'; + +@Component({ + selector: 'evt-biblio-list', + templateUrl: './biblio-list.component.html', + styleUrls: ['./biblio-list.component.scss'], +}) +export class BiblioListComponent { + + @Input() data: BibliographicList; + +} diff --git a/src/app/components/critical-apparatus/critical-apparatus.component.html b/src/app/components/critical-apparatus/critical-apparatus.component.html new file mode 100644 index 000000000..a03bd741a --- /dev/null +++ b/src/app/components/critical-apparatus/critical-apparatus.component.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/app/components/critical-apparatus/critical-apparatus.component.scss b/src/app/components/critical-apparatus/critical-apparatus.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/components/critical-apparatus/critical-apparatus.component.spec.ts b/src/app/components/critical-apparatus/critical-apparatus.component.spec.ts new file mode 100644 index 000000000..8ec02738e --- /dev/null +++ b/src/app/components/critical-apparatus/critical-apparatus.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CriticalApparatusComponent } from './critical-apparatus.component'; + +describe('CriticalApparatusComponent', () => { + let component: CriticalApparatusComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ CriticalApparatusComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CriticalApparatusComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/critical-apparatus/critical-apparatus.component.ts b/src/app/components/critical-apparatus/critical-apparatus.component.ts new file mode 100644 index 000000000..07a032ae0 --- /dev/null +++ b/src/app/components/critical-apparatus/critical-apparatus.component.ts @@ -0,0 +1,35 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Component, Input, OnInit } from '@angular/core'; +import { EVTStatusService } from '../../services/evt-status.service'; + +@Component({ + selector: 'evt-critical-apparatus', + templateUrl: './critical-apparatus.component.html', + styleUrls: ['./critical-apparatus.component.scss'], +}) +export class CriticalApparatusComponent implements OnInit { + + @Input() pageID : string; + + public entries; + private appClasses = ['app']; + + public apparatusInCurrentPage = this.evtStatusService.getPageElementsByClassList(this.appClasses) + + public getEntries(data: any) { + this.entries = data.flat(); + } + + constructor( + public evtStatusService: EVTStatusService, + ) {} + + ngOnInit() { + this.apparatusInCurrentPage.subscribe({ next: (data:any) => { this.getEntries(data) } }); + } + + stopPropagation(e: MouseEvent) { + e.stopPropagation(); + } + +} diff --git a/src/app/components/note/note.component.html b/src/app/components/note/note.component.html index eb0ce45e5..20ad47139 100644 --- a/src/app/components/note/note.component.html +++ b/src/app/components/note/note.component.html @@ -19,7 +19,9 @@ -
+
+ {{ 'analogue' | translate }}: + {{ 'source' | translate }}:
diff --git a/src/app/components/note/note.component.scss b/src/app/components/note/note.component.scss index 7aa07ffb2..2d8406373 100644 --- a/src/app/components/note/note.component.scss +++ b/src/app/components/note/note.component.scss @@ -74,4 +74,25 @@ } } } + + &[data-note-type='analogue'] { + .note-icon { + background: get-color(analogueNotes); + + .arrow { + border-top-color: get-color(criticalNotes); + } + } + } + + &[data-note-type='source'] { + .note-icon { + background: get-color(sourceNotes); + + .arrow { + border-top-color: get-color(criticalNotes); + } + } + } + } \ No newline at end of file diff --git a/src/app/components/paragraph/paragraph.component.html b/src/app/components/paragraph/paragraph.component.html index 5af398f3d..9ee011ae6 100644 --- a/src/app/components/paragraph/paragraph.component.html +++ b/src/app/components/paragraph/paragraph.component.html @@ -1,5 +1,6 @@

{{data.n}} + diff --git a/src/app/components/quote-entry/quote-entry.component.html b/src/app/components/quote-entry/quote-entry.component.html new file mode 100644 index 000000000..8d0d6f515 --- /dev/null +++ b/src/app/components/quote-entry/quote-entry.component.html @@ -0,0 +1,22 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/app/components/quote-entry/quote-entry.component.scss b/src/app/components/quote-entry/quote-entry.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/components/quote-entry/quote-entry.component.spec.ts b/src/app/components/quote-entry/quote-entry.component.spec.ts new file mode 100644 index 000000000..0c3058bf3 --- /dev/null +++ b/src/app/components/quote-entry/quote-entry.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; + +import { QuoteEntryComponent } from './quote-entry.component'; + +describe('QuoteEntryComponent', () => { + let component: QuoteEntryComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ QuoteEntryComponent ], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(QuoteEntryComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/quote-entry/quote-entry.component.ts b/src/app/components/quote-entry/quote-entry.component.ts new file mode 100644 index 000000000..d749a7e27 --- /dev/null +++ b/src/app/components/quote-entry/quote-entry.component.ts @@ -0,0 +1,98 @@ +import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; +import { BehaviorSubject, Subject } from 'rxjs'; +import { scan, startWith } from 'rxjs/operators'; + +import { EditorialConventionLayoutData } from '../../directives/editorial-convention-layout.directive'; + +import { Note, QuoteEntry, SourceClass } from '../../models/evt-models'; +import { register } from '../../services/component-register.service'; +import { EVTStatusService } from '../../services/evt-status.service'; +import { EditionLevelType } from 'src/app/app.config'; +import { EditionlevelSusceptible, Highlightable } from '../components-mixins'; + +export interface QuoteEntryComponent extends EditionlevelSusceptible, Highlightable {} +@register(QuoteEntry) +@Component({ + selector: 'evt-quote-entry', + templateUrl: './quote-entry.component.html', + styleUrls: ['./quote-entry.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class QuoteEntryComponent implements OnInit { + + public _data: QuoteEntry; + + private edLevel: EditionLevelType; + + @Input() set data(dt: QuoteEntry) { + this._data = dt; + } + + @Input() set editionLevel(el: EditionLevelType) { + this.edLevel = el; + this.editionLevelChange.next(el); + } + get editionLevel() { return this.edLevel; } + editionLevelChange = new BehaviorSubject(''); + + get editorialConventionData(): EditorialConventionLayoutData { + return { + name: '.sources', + attributes: this.data?.attributes || {}, + editionLevel: this.editionLevel, + defaultsKey: '.sources', + }; + } + + get data() { return this._data; } + + public sourceClass = SourceClass; + + public dataForNote = {}; + + public opened = false; + + toggleOpened$ = new Subject(); + + opened$ = this.toggleOpened$.pipe( + scan((currentState: boolean, val: boolean | undefined) => val === undefined ? !currentState : val, false), + startWith(false), + ); + + toggleAppEntryBox(e: MouseEvent) { + e.stopPropagation(); + this.opened = !this.opened; + } + + closeAppEntryBox() { + this.opened = false; + } + + stopPropagation(e: MouseEvent) { + e.stopPropagation(); + } + + /** If quote has no text it's displayed as a note.*/ + createNote(v): Note|{} { + + return { + type: Note, + noteType: 'source', + noteLayout: 'popover', + exponent: v.path || '', + content: v.extSources.concat(v.extElements), + attributes: v.attributes || [], + } + } + + ngOnInit() { + if ((this.data.noteView) || ((this.data.text.length === 0) && ((this.data.extElements.length !== 0) || (this.data.extSources.length !== 0)))) { + this.dataForNote = this.createNote(this.data); + } + } + + constructor( + public evtStatusService: EVTStatusService, + ) {} + +} diff --git a/src/app/components/sources/source-detail/source-detail.component.html b/src/app/components/sources/source-detail/source-detail.component.html new file mode 100644 index 000000000..122997e02 --- /dev/null +++ b/src/app/components/sources/source-detail/source-detail.component.html @@ -0,0 +1,115 @@ + +

+ +
+ + + + + + + + +
+ + + +
\ No newline at end of file diff --git a/src/app/components/sources/source-detail/source-detail.component.spec.ts b/src/app/components/sources/source-detail/source-detail.component.spec.ts new file mode 100644 index 000000000..e7fb454e3 --- /dev/null +++ b/src/app/components/sources/source-detail/source-detail.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SourceDetailComponent } from './source-detail.component'; + +describe('SourceDetailComponent', () => { + let component: SourceDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ SourceDetailComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SourceDetailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/sources/source-detail/source-detail.component.ts b/src/app/components/sources/source-detail/source-detail.component.ts new file mode 100644 index 000000000..a2da87466 --- /dev/null +++ b/src/app/components/sources/source-detail/source-detail.component.ts @@ -0,0 +1,28 @@ +import { Component, Input } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import { EditionLevelType } from 'src/app/app.config'; + +@Component({ + selector: 'evt-source-detail', + templateUrl: './source-detail.component.html', + styleUrls: ['../../sources/sources.component.scss'], +}) +export class SourceDetailComponent { + + private edLevel: EditionLevelType; + + @Input() source; + + @Input() set editionLevel(el: EditionLevelType) { + this.edLevel = el; + this.editionLevelChange.next(el); + } + get editionLevel() { return this.edLevel; } + editionLevelChange = new BehaviorSubject(''); + + stopPropagation(e: MouseEvent) { + e.stopPropagation(); + } + +} + diff --git a/src/app/components/sources/source-note/source-note.component.html b/src/app/components/sources/source-note/source-note.component.html new file mode 100644 index 000000000..7423eb0d4 --- /dev/null +++ b/src/app/components/sources/source-note/source-note.component.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/components/sources/source-note/source-note.component.spec.ts b/src/app/components/sources/source-note/source-note.component.spec.ts new file mode 100644 index 000000000..7ffee2fe7 --- /dev/null +++ b/src/app/components/sources/source-note/source-note.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SourceNoteComponent } from './source-note.component'; + +describe('SourceNoteComponent', () => { + let component: SourceNoteComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ SourceNoteComponent ], + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SourceNoteComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/sources/source-note/source-note.component.ts b/src/app/components/sources/source-note/source-note.component.ts new file mode 100644 index 000000000..4fbaf440e --- /dev/null +++ b/src/app/components/sources/source-note/source-note.component.ts @@ -0,0 +1,54 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { Note, Paragraph, Verse, VersesGroup } from 'src/app/models/evt-models'; + +@Component({ + selector: 'evt-source-note', + templateUrl: './source-note.component.html', + styleUrls: ['../../sources/sources.component.scss'], +}) +export class SourceNoteComponent implements OnInit { + + public _data: Paragraph|Verse|VersesGroup; + + @Input() set data(dt: Paragraph|Verse|VersesGroup) { + this._data = dt; + } + + get data() { return this._data } + + public dataForNote = {}; + + createNote(v, type): Note { + + const item = v[type]; + let content = item.extSources || []; + + if (type === 'analogue') { + content.push( item.extLinkedElements ); + content.push( item.sources ); + } else if (type === 'source') { + content.push( item.extElements ); + } + + return { + type: Note, + noteType: type, + noteLayout: 'popover', + exponent: v.path || '', + content: content, + attributes: v.attributes || [], + } + } + + ngOnInit() { + if (this.data.source !== null) { + this.dataForNote = this.createNote(this.data, 'source'); + } + if (this.data.analogue !== null) { + this.dataForNote = this.createNote(this.data, 'analogue'); + } + } + + +} + diff --git a/src/app/components/sources/sources.component.html b/src/app/components/sources/sources.component.html new file mode 100644 index 000000000..8add0cf4a --- /dev/null +++ b/src/app/components/sources/sources.component.html @@ -0,0 +1,9 @@ + + + + + + + diff --git a/src/app/components/sources/sources.component.scss b/src/app/components/sources/sources.component.scss new file mode 100644 index 000000000..c0b9a2d7f --- /dev/null +++ b/src/app/components/sources/sources.component.scss @@ -0,0 +1,145 @@ +@import "../../../assets/scss/themes"; + +.source-container { + border: 1px solid #80808026; + padding: 0em; + margin: 0.5em 0.5em 1em 0.5em; + font-family: serif; + box-shadow: 1px 1px 1px rgba(0,0,0,0.1); + background-color:#2042512e; +} + +.source-detail-content { + display: inline-flex !important; + background-color: unset !important; + padding-bottom: 0.6em !important; + pointer-events: none; +} + +.source-quote-button { + float:right; + margin-right: 0.3em; + font-size: 1.5em; + margin-top: -0.5em; + color: #00000073; + height: 2em; + display: none; + pointer-events: none; +} + +.bibl-head { + margin-left: 2rem; +} + +.source-quote-symbol { + font-size: 1em; + color: #263238; + margin: 0; + padding: 0 0.3em 0 0; + display: inline; + font-family: 'evt-icons' !important; + speak: none; + font-style: normal; + font-feature-settings: normal; + font-variant: normal; + text-transform: none; + line-height: 0; + -webkit-font-smoothing: antialiased; + vertical-align: super; +} + +.sources-cat { + font-variant: small-caps; + font-size: 95%; + position: relative; + font-style: normal; +} + +.source-detail-btn { + background-color: transparent; + color: #000000; + line-height: 1; + padding: 0.25rem 0.375rem; + cursor: pointer; + border-radius: 0; + font-style:normal; +} + +.app-detail-content, +.app-detail-tabs { + background-color: #f1f4f5; + font-size: 1rem; +} + +.app-detail-content { + display: flex; + font-style: italic; + justify-content: space-between; + padding: 0.313rem; + background-color: #8080802e; +} + +.app-detail-tabs { + font-size: 1rem; + margin: 0; + padding: 0.313rem 0 0 0; + .nav-link { + background-color: transparent; + color: #000000; + line-height: 1; + padding: 0.25rem 0.375rem; + cursor: pointer; + border-radius: 0; + &.active { + color: #000000; + } + } + .nav-pills, + .tab-content { + margin-right: 0rem; + margin-left: 0rem; + } + .nav-link.active, + .tab-content { + @include themify($themes) { + background-color: themed("appEntryBoxActiveTabBg"); + } + } + .tab-content { + max-height: 12.5rem; + overflow: auto; + padding-top: 0.5em; + padding-left: 1em; + padding-bottom: 0.5em; + .info-lemma-wrapper { + @include themify($themes) { + border-bottom: 1px solid themed("baseBorder"); + } + padding-bottom: 0.438rem; + margin-bottom: 0.625rem; + } + .info-rdg { + font-style: italic; + font-weight: 600; + font-size: 1.063rem; + } + .info-label { + font-size: 0.813rem; + text-transform: uppercase; + font-weight: 600; + } + .more-info-label { + display: block; + font-size: 0.813rem; + font-weight: 600; + text-transform: uppercase; + margin-bottom: 0.25rem; + } + pre { + white-space: pre-wrap; + font-size: 75%; + margin-bottom: 0; + margin-top: -1rem; + } + } +} \ No newline at end of file diff --git a/src/app/components/sources/sources.component.spec.ts b/src/app/components/sources/sources.component.spec.ts new file mode 100644 index 000000000..56ba944e3 --- /dev/null +++ b/src/app/components/sources/sources.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SourcesComponent } from './sources.component'; + +describe('SourcesComponent', () => { + let component: SourcesComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ SourcesComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SourcesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/sources/sources.component.ts b/src/app/components/sources/sources.component.ts new file mode 100644 index 000000000..b3385c387 --- /dev/null +++ b/src/app/components/sources/sources.component.ts @@ -0,0 +1,46 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { EVTStatusService } from 'src/app/services/evt-status.service'; +import { SourceClass } from '../../models/evt-models'; +import { BehaviorSubject } from 'rxjs'; +import { EditionLevelType } from 'src/app/app.config'; + +@Component({ + selector: 'evt-sources', + templateUrl: './sources.component.html', + styleUrls: ['./sources.component.scss'], +}) +export class SourcesComponent implements OnInit { + + private edLevel: EditionLevelType; + + public entries; + private appClasses = [SourceClass]; + + public quotesInCurrentPage = this.evtStatusService.getPageElementsByClassList(this.appClasses) + + @Input() pageID : string; + + @Input() set editionLevel(el: EditionLevelType) { + this.edLevel = el; + this.editionLevelChange.next(el); + } + get editionLevel() { return this.edLevel; } + editionLevelChange = new BehaviorSubject(''); + + stopPropagation(e: MouseEvent) { + e.stopPropagation(); + } + + public getEntries(data) { + this.entries = data.flat(); + } + + constructor( + public evtStatusService: EVTStatusService, + ) {} + + ngOnInit() { + this.quotesInCurrentPage.subscribe({ next: (data) => { this.getEntries(data) } }); + } + +} diff --git a/src/app/components/verse/verse.component.html b/src/app/components/verse/verse.component.html index 025760ba3..aa6a0eed3 100644 --- a/src/app/components/verse/verse.component.html +++ b/src/app/components/verse/verse.component.html @@ -1,6 +1,7 @@ {{data.n}} + diff --git a/src/app/components/verses-group/verses-group.component.html b/src/app/components/verses-group/verses-group.component.html index 9682c8c87..0f5d4c63b 100644 --- a/src/app/components/verses-group/verses-group.component.html +++ b/src/app/components/verses-group/verses-group.component.html @@ -1,6 +1,7 @@ {{data.n}} + diff --git a/src/app/models/evt-models.ts b/src/app/models/evt-models.ts index 09dedca9a..25ab46371 100644 --- a/src/app/models/evt-models.ts +++ b/src/app/models/evt-models.ts @@ -170,6 +170,68 @@ export class ApparatusEntry extends GenericElement { nestedAppsIDs: string[]; } +export const SourceClass = 'sourceEntry'; +export const AnalogueClass = 'analogueEntry'; +export const BibliographyClass = 'biblioEntry'; + +export class QuoteEntry extends GenericElement { + id: string; + tagName: string; + text: string; + sources: BibliographicEntry[]|BibliographicList; + extSources: BibliographicEntry[]; + extElements: any; + analogues: any; + originalEncoding: string; + insideCit: boolean; + quotedText: string[]; + noteView: boolean; + contentToShow: Array> + //rend: string; +} + +export class BibliographicList extends GenericElement { + id: string; + head: string[]; + sources: BibliographicEntry[]; + //TODO: biblStruct +} + +export class BibliographicEntry extends GenericElement { + id: string; + author: string[]; + editor: string[]; + title: string[]; + date: string[]; + publisher: string[]; + pubPlace: string[]; + citedRange: string[]; + biblScope: string[]; + text: string; + quotedText: string; + insideCit: boolean; + originalEncoding: string; +} + +export class BibliographicStructEntry extends GenericElement { + id: string; + analytic: BibliographicEntry[]; + monogrs: BibliographicEntry[]; + series: BibliographicEntry[]; + originalEncoding: string; +} + +export class Analogue extends GenericElement { + id: string; + text: string; + sources: BibliographicEntry[]; + extSources: BibliographicEntry[]; + extLinkedElements: any; + quotedText: [{ id: string, quote: string }]; + contentToShow: Array> + originalEncoding: string; +} + export class Reading extends GenericElement { id: string; witIDs: string[]; @@ -194,10 +256,14 @@ export class Note extends GenericElement { noteLayout: NoteLayout; noteType: string; exponent: string; + source: QuoteEntry|null; + analogue: Analogue|null; } export class Paragraph extends GenericElement { n: string; + source: QuoteEntry|null; + analogue: Analogue|null; } export class Lb extends GenericElement { @@ -284,11 +350,15 @@ export class Choice extends GenericElement { export class Verse extends GenericElement { n: string; + source: QuoteEntry|null; + analogue: Analogue|null; } export class VersesGroup extends GenericElement { n: string; groupType: string; + source: QuoteEntry|null; + analogue: Analogue|null; } export class Supplied extends GenericElement { @@ -450,13 +520,13 @@ export class MsItemStruct extends GenericElement { titles: Array>; // TODO: Add specific type when title is handled rubric: Rubric; incipit: Incipit; - quote: Array>; // TODO: Add specific type when quote is handled + quote: QuoteEntry; explicit: Explicit; finalRubric: FinalRubric; colophons: Array>; // TODO: Add specific type when colophon is handled decoNote: DecoNote; - listBibl: Array>; // TODO: Add specific type when listBibl is handled - bibl: Array>; // TODO: Add specific type when bibl is handled + listBibl: BibliographicList; + bibl: BibliographicEntry; filiation: Filiation[]; noteEl: Note[]; textLangs: Array>; // TODO: Add specific type when textLang is handled @@ -1042,6 +1112,30 @@ export class Term extends GenericElement { rend?: string; } +export class Milestone extends GenericElement { + id?: string; + unit?: string; + spanText: string; + spanElements: any; +} + +export class Anchor extends GenericElement { + id?: string; +} + +export class Span extends GenericElement { + id?: string; + from: string; + to: string; + includedText: string; + includedElements: any; +} + +export class SpanGrp extends GenericElement { + id?: string; + spans: Span[] +} + export class Keywords extends GenericElement { scheme?: string; terms: Term[]; diff --git a/src/app/services/editorial-conventions.service.ts b/src/app/services/editorial-conventions.service.ts index 5e3b5981d..b66aac4b5 100644 --- a/src/app/services/editorial-conventions.service.ts +++ b/src/app/services/editorial-conventions.service.ts @@ -5,7 +5,7 @@ import { EditorialConvention, EditorialConventionLayouts } from '../models/evt-m // List of handled editorial convention export type EditorialConventionDefaults = 'addition' | 'additionAbove' | 'additionBelow' | 'additionInline' | 'additionLeft' | 'additionRight' | - 'damage' | 'deletion' | 'sicCrux' | 'surplus'; + 'damage' | 'deletion' | 'sicCrux' | 'surplus' | '.sources' | '.analogues' ; @Injectable({ providedIn: 'root', @@ -125,26 +125,61 @@ export class EditorialConventionsService { }, }, }, + '.sources': { + diplomatic: { + style: { + 'font-style': 'italic', + 'font-size': '104%', + }, + }, + interpretative: { + style: { + 'font-style': 'italic', + 'font-size': '104%', + }, + }, + critical: { + style: { + 'font-style': 'italic', + 'font-size': '104%', + }, + }, + }, + '.analogues': { + diplomatic: { + pre: '🗎', + style: { + 'text-decoration': 'underline dotted from-font', + }, + }, + interpretative: { + pre: '🗎', + style: { + 'text-decoration': 'underline dotted from-font', + }, + }, + critical: { + pre: '🗎', + style: { + 'text-decoration': 'underline dotted from-font', + }, + }, + }, }; getLayouts(name: string, attributes: AttributesMap, defaultsKey: EditorialConventionDefaults) { + const excludedFromAttributeControl = ['.sources', '.analogues']; const defaultKeys = this.defaultLayouts[defaultsKey]; let layouts: Partial = defaultKeys; const externalLayouts = this._getExternalConfigs().find((c) => c.element === name && - (!attributes || Object.keys(attributes).concat( + (excludedFromAttributeControl.includes(name) || !attributes || Object.keys(attributes).concat( Object.keys(c.attributes)).every((k) => attributes[k] === c.attributes[k])))?.layouts ?? undefined; if (externalLayouts) { - Object.keys(externalLayouts).forEach((editionLevel) => { - layouts = { - ...defaultKeys || {}, - [editionLevel]: { - ...defaultKeys ? defaultKeys[editionLevel] : {}, - ...externalLayouts[editionLevel], - }, - }; - }); + layouts = { + ...externalLayouts || {}, + } } return layouts; diff --git a/src/app/services/evt-model.service.ts b/src/app/services/evt-model.service.ts index 8401a9fb8..d0817af08 100644 --- a/src/app/services/evt-model.service.ts +++ b/src/app/services/evt-model.service.ts @@ -13,6 +13,8 @@ import { NamedEntitiesParserService } from './xml-parsers/named-entities-parser. import { PrefatoryMatterParserService } from './xml-parsers/prefatory-matter-parser.service'; import { StructureXmlParserService } from './xml-parsers/structure-xml-parser.service'; import { WitnessesParserService } from './xml-parsers/witnesses-parser.service'; +import { SourceEntriesParserService } from './xml-parsers/source-entries-parser.service'; +import { AnalogueEntriesParserService } from './xml-parsers/analogues-entries-parser.service'; @Injectable({ providedIn: 'root', @@ -143,6 +145,18 @@ export class EVTModelService { shareReplay(1), ); + //QUOTED SOURCES + public readonly sourceEntries$ = this.editionSource$.pipe( + map((source) => this.sourceParser.parseSourceEntries(source)), + shareReplay(1), + ); + + // PARALLEL PASSAGES + public readonly analogueEntries$ = this.editionSource$.pipe( + map((source) => this.analogueParser.parseAnaloguesEntries(source)), + shareReplay(1), + ); + // FACSIMILE public readonly surfaces$ = this.editionSource$.pipe( map((source) => this.facsimileParser.parseSurfaces(source)), @@ -180,7 +194,7 @@ export class EVTModelService { public readonly msDesc$ = this.editionSource$.pipe( map((source) => this.msDescParser.parseMsDesc(source)), shareReplay(1), -); + ); constructor( private editionDataService: EditionDataService, @@ -193,6 +207,8 @@ export class EVTModelService { private characterDeclarationsParser: CharacterDeclarationsParserService, private linesVersesParser: LinesVersesParserService, private msDescParser: MsDescParserService, + private sourceParser: SourceEntriesParserService, + private analogueParser: AnalogueEntriesParserService, ) { } diff --git a/src/app/services/evt-status.service.ts b/src/app/services/evt-status.service.ts index 8409fc25f..7e6f155b6 100644 --- a/src/app/services/evt-status.service.ts +++ b/src/app/services/evt-status.service.ts @@ -6,6 +6,7 @@ import { distinctUntilChanged, filter, first, map, mergeMap, shareReplay, switch import { AppConfig, EditionLevelType } from '../app.config'; import { Page, ViewMode } from '../models/evt-models'; import { EVTModelService } from './evt-model.service'; +import { deepSearch } from '../utils/dom-utils'; export type URLParamsKeys = 'd' | 'p' | 'el' | 'ws' | 'vs'; export type URLParams = { [T in URLParamsKeys]: string }; @@ -131,6 +132,8 @@ export class EVTStatusService { public currentNamedEntityId$: BehaviorSubject = new BehaviorSubject(undefined); + public currentQuotedId$: BehaviorSubject = new BehaviorSubject(undefined); + constructor( private evtModelService: EVTModelService, private router: Router, @@ -179,6 +182,15 @@ export class EVTStatusService { params, }; } + + getPageElementsByClassList(classList) { + const pageContent = this.currentStatus$.pipe( + map(({ page }) => page.parsedContent)); + + return pageContent.pipe(map((x) => deepSearch(x, 'class', classList))); + + } + } export interface AppStatus { diff --git a/src/app/services/xml-parsers/analogue-parser.ts b/src/app/services/xml-parsers/analogue-parser.ts new file mode 100644 index 000000000..75be7f57e --- /dev/null +++ b/src/app/services/xml-parsers/analogue-parser.ts @@ -0,0 +1,130 @@ +import { AppConfig } from 'src/app/app.config'; +import { parse, ParserRegister, xmlParser } from '.'; +import { Analogue, AnalogueClass, BibliographicEntry, BibliographicList, GenericElement, Milestone, XMLElement } from '../../models/evt-models'; +import { getOuterHTML } from '../../utils/dom-utils'; +import { AnchorParser, AttributeParser, GenericElemParser, MilestoneParser } from './basic-parsers'; +import { createParser, getID, parseChildren, Parser } from './parser-models'; +import { chainFirstChildTexts, getExternalElements, normalizeSpaces } from 'src/app/utils/xml-utils'; +import { BibliographyParser } from './bilbliography-parsers'; +import { BasicParser } from './quotes-parser'; + +@xmlParser('evt-analogue-entry-parser', AnalogueParser) +export class AnalogueParser extends BasicParser implements Parser { + + elementParser = createParser(GenericElemParser, parse); + attributeParser = createParser(AttributeParser, this.genericParse); + biblParser = createParser(BibliographyParser, this.genericParse); + milestoneParser = createParser(MilestoneParser, this.genericParse); + anchorParser = createParser(AnchorParser, this.genericParse); + + analogueMarker = AppConfig.evtSettings.edition.analogueMarkers; + biblAttributeToMatch = AppConfig.evtSettings.edition.externalBibliography.biblAttributeToMatch; + elemAttributesToMatch = AppConfig.evtSettings.edition.externalBibliography.elementAttributesToMatch; + notNiceInText = ['Note', 'BibliographicList', 'BibliographicEntry', 'BibliographicStructEntry', + 'Analogue', 'MsDesc']; + + public parse(analogue: XMLElement): GenericElement|Analogue { + + if (!(this.analogueMarker.includes(analogue.getAttribute('type'))) || (analogue.parentElement.tagName === 'cit')) { + // no source/target attribute or inside a cit element: not an analogue to display alone + return this.elementParser.parse(analogue) + } + + const noteID = ['div','p','l','lg','note']; + const sources = this.buildAnalogueSources(analogue); + const content = parseChildren(analogue, this.genericParse); + + return { + type: Analogue, + id: (noteID.includes(analogue.tagName)) ? 'EVT-ANG:'+getID(analogue) : getID(analogue), + class: AnalogueClass, + attributes: this.attributeParser.parse(analogue), + text: normalizeSpaces(chainFirstChildTexts(analogue)), + content: content, + contentToShow: content.filter((x) => !(this.notNiceInText.includes(x['type'].name))), + sources: sources.sources, + extSources: sources.extSources, + extLinkedElements: sources.extLinkedElements, + quotedText: this.getQuotedTextFromElements(sources.sources.concat(sources.extSources), sources.extLinkedElements), + originalEncoding: getOuterHTML(analogue), + }; + } + + /** + * Since elements like ref and seg are not only used for parallel passages, + * this function checks if the provided element contains an external link to a bibl element + * and returns that elements or a false + * @param analogue + * @returns any + */ + private buildAnalogueSources(analogue: XMLElement): any { + const selectorsAllowed = 'bibl, bibStruct, listBibl, cit, quote, note, seg, div, l, lg, p, milestone, anchor'; + const elsAllowedForSources = ['bibl','listBibl', 'biblStruct', 'ref', 'cit']; + const sources = this.getInsideSources(analogue); + const extElements = getExternalElements(analogue, this.elemAttributesToMatch, this.biblAttributeToMatch, selectorsAllowed); + const extSources = extElements.map((x) => (elsAllowedForSources.includes(x.tagName)) ? this.biblParser.parse(x) : null).filter((x) => x); + const parallelPassages = this.selectAndParseParallelElements(extElements); + + return { 'sources': sources, 'extSources': extSources, 'extLinkedElements': parallelPassages }; + } + + /** + * Retrieve all Bibliography elements *inside* this analogue element + * @param quote XMLElement + * @returns array of Bibliography Element or a single Bibliography List element + */ + private getInsideSources(analogue: XMLElement): BibliographicEntry[] { + const bibl = ['bibl','listBibl','biblStruct','ref']; + + return Array.from(analogue.children) + .map((x: XMLElement) => bibl.includes(x['tagName']) ? this.biblParser.parse(x) : null) + .filter((x) => x); + } + + /** + * Gather and send to parse allowed linked parallel passages + */ + private selectAndParseParallelElements(suspectPPs): any { + const elemParserAssoc = { + l: ParserRegister.get('l'), + lg: ParserRegister.get('lg'), + p: ParserRegister.get('p'), + div: this.elementParser, + seg: this.elementParser, + anchor: this.anchorParser, + milestone: this.milestoneParser, + quote: this.elementParser, + note: this.elementParser, + } + const addendum = []; + const ppElements = suspectPPs.map((x) => (elemParserAssoc[x['tagName']] !== undefined) ? elemParserAssoc[x['tagName']].parse(x) : null) + .filter((x) => x); + ppElements.map((x) => (x.type === Milestone) ? addendum.push(x.spanElements) : x ); + + return ppElements.concat(addendum.flat()); + } + + private getQuotedTextFromSources(nodes: BibliographicEntry[]): any { + let quotesInSources = []; + nodes.forEach((el: BibliographicEntry|BibliographicList) => { + if (el.type === BibliographicList) { + quotesInSources = quotesInSources.concat(this.getQuotedTextFromSources(el['sources'])); + } else { + if (el['quotedText'] !== null) { + quotesInSources.push({ id: el.id, quote: el['quotedText'] }); + } + }; + }); + + return quotesInSources; + } + + private getQuotedTextFromElements(sources: BibliographicEntry[], elements: XMLElement[]): [{id: string, quote: string}] { + let quotesInSources = this.getQuotedTextFromSources(sources); + const notNiceInText = ['Note','BibliographicList','BibliographicEntry','BibliographicStructEntry','Analogue','MsDesc']; + elements.forEach((el: XMLElement) => { if (!notNiceInText.includes(el['type'])) { quotesInSources.push( { id: el.id, quote: el })} }); + + return quotesInSources; + } + +} diff --git a/src/app/services/xml-parsers/analogues-entries-parser.service.ts b/src/app/services/xml-parsers/analogues-entries-parser.service.ts new file mode 100644 index 000000000..f4ccfc7fe --- /dev/null +++ b/src/app/services/xml-parsers/analogues-entries-parser.service.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@angular/core'; +import { ParserRegister } from '.'; +import { Analogue, AnalogueClass, XMLElement } from '../../models/evt-models'; + +@Injectable({ + providedIn: 'root', +}) +export class AnalogueEntriesParserService { + + private tagName = `.${AnalogueClass}`; + private parserName = 'evt-analogue-entry-parser'; + + public parseAnaloguesEntries(document: XMLElement) { + + const analogueParser = ParserRegister.get(this.parserName); + + return Array.from(document.querySelectorAll(this.tagName)) + .map((analogue) => analogueParser.parse(analogue) as Analogue); + } + +} + diff --git a/src/app/services/xml-parsers/basic-parsers.ts b/src/app/services/xml-parsers/basic-parsers.ts index d21332ce2..b11697981 100644 --- a/src/app/services/xml-parsers/basic-parsers.ts +++ b/src/app/services/xml-parsers/basic-parsers.ts @@ -1,12 +1,15 @@ import { AttributesMap } from 'ng-dynamic-component'; import { ParserRegister, xmlParser } from '.'; import { - Addition, Attributes, Damage, Deletion, Gap, GenericElement, Lb, Note, NoteLayout, - Paragraph, PlacementType, Ptr, Supplied, Term, Text, Verse, VersesGroup, Word, XMLElement, + Addition, Analogue, Anchor, Attributes, Damage, Deletion, Gap, GenericElement, Lb, Milestone, Note, NoteLayout, + Paragraph, PlacementType, Ptr, QuoteEntry, Span, SpanGrp, Supplied, Term, Text, Verse, VersesGroup, Word, XMLElement, } from '../../models/evt-models'; import { isNestedInElem, xpath } from '../../utils/dom-utils'; -import { replaceMultispaces } from '../../utils/xml-utils'; +import { getExternalElements, getSpanToElements, isAnalogue, isSource, replaceMultispaces } from '../../utils/xml-utils'; import { createParser, getClass, getDefaultN, getID, parseChildren, ParseFn, Parser } from './parser-models'; +import { AppConfig } from 'src/app/app.config'; +import { AnalogueParser } from './analogue-parser'; +import { QuoteParser } from './quotes-parser'; export class EmptyParser { genericParse: ParseFn; @@ -87,13 +90,28 @@ export class TextParser implements Parser { @xmlParser('p', ParagraphParser) export class ParagraphParser extends EmptyParser implements Parser { + analogueParser = createParser(AnalogueParser, this.genericParse); + quoteParser = createParser(QuoteParser, this.genericParse); + sourceAttr = AppConfig.evtSettings.edition.externalBibliography.elementAttributesToMatch; + analogueMarkers = AppConfig.evtSettings.edition.analogueMarkers; + source = null; analogue = null; parse(xml: XMLElement): Paragraph { + + if (isAnalogue(xml, this.analogueMarkers)) { + this.analogue = this.analogueParser.parse(xml); + } + if (isSource(xml, this.sourceAttr)) { + this.source = this.quoteParser.parse(xml); + } + const attributes = ParserRegister.get('evt-attribute-parser').parse(xml) as Attributes; const paragraphComponent: Paragraph = { type: Paragraph, content: parseChildren(xml, this.genericParse), attributes, n: getDefaultN(attributes.n), + source: this.source, + analogue: this.analogue, }; return paragraphComponent; @@ -122,6 +140,11 @@ export class LBParser extends EmptyParser implements Parser { @xmlParser('note', NoteParser) export class NoteParser extends EmptyParser implements Parser { attributeParser = createParser(AttributeParser, this.genericParse); + analogueParser = createParser(AnalogueParser, this.genericParse); + quoteParser = createParser(QuoteParser, this.genericParse); + sourceAttr = AppConfig.evtSettings.edition.externalBibliography.elementAttributesToMatch; + analogueMarkers = AppConfig.evtSettings.edition.analogueMarkers; + source = null; analogue = null; parse(xml: XMLElement): Note { const noteLayout: NoteLayout = this.isFooterNote(xml) || this.isNamedEntityNote(xml) || ['person', 'place', 'app', 'msDesc'].some((v) => isNestedInElem(xml, v)) @@ -132,6 +155,14 @@ export class NoteParser extends EmptyParser implements Parser { ? 'critical' : 'comment'; + + if (isAnalogue(xml, this.analogueMarkers)) { + this.analogue = this.analogueParser.parse(xml); + } + if (isSource(xml, this.sourceAttr)) { + this.source = this.quoteParser.parse(xml); + } + const attributes = this.attributeParser.parse(xml); const noteElement = { type: Note, @@ -140,6 +171,8 @@ export class NoteParser extends EmptyParser implements Parser { exponent: attributes.n, path: xpath(xml), content: parseChildren(xml, this.genericParse), + source: this.source, + analogue: this.analogue, attributes, }; @@ -154,7 +187,18 @@ export class NoteParser extends EmptyParser implements Parser { export class PtrParser extends GenericElemParser implements Parser { noteParser = createParser(NoteParser, this.genericParse); elementParser = createParser(GenericElemParser, this.genericParse); - parse(xml: XMLElement): Ptr | Note | GenericElement { + analogueParser = createParser(AnalogueParser, this.genericParse); + quoteParser = createParser(QuoteParser, this.genericParse); + sourceAttr = AppConfig.evtSettings.edition.externalBibliography.biblAttributeToMatch + ptrAttrs = AppConfig.evtSettings.edition.externalBibliography.elementAttributesToMatch; + + parse(xml: XMLElement): Ptr | QuoteEntry | Analogue { + + if (this.isAnalogue(xml)) { + return this.analogueParser.parse(xml); + } + + // note if (xml.getAttribute('type') === 'noteAnchor' && xml.getAttribute('target')) { const noteId = xml.getAttribute('target').replace('#', ''); const rootNode = xml.closest('TEI'); @@ -163,6 +207,10 @@ export class PtrParser extends GenericElemParser implements Parser { return noteEl ? this.noteParser.parse(noteEl) : this.elementParser.parse(xml); } + if (this.isSource(xml)) { + return this.quoteParser.parse(xml); + } + return { ...super.parse(xml), type: Ptr, @@ -173,18 +221,36 @@ export class PtrParser extends GenericElemParser implements Parser { rend: xml.getAttribute('rend'), }; } + + private isAnalogue(xml: XMLElement) { return (AppConfig.evtSettings.edition.analogueMarkers.includes(xml.getAttribute('type'))) }; + private isSource(xml: XMLElement) { return (getExternalElements(xml, this.ptrAttrs, this.sourceAttr, '*')).length !== 0 } } @xmlParser('l', VerseParser) export class VerseParser extends EmptyParser implements Parser { attributeParser = createParser(AttributeParser, this.genericParse); + analogueParser = createParser(AnalogueParser, this.genericParse); + quoteParser = createParser(QuoteParser, this.genericParse); + sourceAttr = AppConfig.evtSettings.edition.externalBibliography.elementAttributesToMatch; + analogueMarkers = AppConfig.evtSettings.edition.analogueMarkers; + source = null; analogue = null; parse(xml: XMLElement): Verse { + + if (isAnalogue(xml, this.analogueMarkers)) { + this.analogue = this.analogueParser.parse(xml); + } + if (isSource(xml, this.sourceAttr)) { + this.source = this.quoteParser.parse(xml); + } + const attributes = this.attributeParser.parse(xml); const lineComponent: Verse = { type: Verse, content: parseChildren(xml, this.genericParse), attributes, n: getDefaultN(attributes.n), + source: this.source, + analogue: this.analogue, }; return lineComponent; @@ -194,7 +260,20 @@ export class VerseParser extends EmptyParser implements Parser { @xmlParser('lg', VersesGroupParser) export class VersesGroupParser extends EmptyParser implements Parser { attributeParser = createParser(AttributeParser, this.genericParse); + analogueParser = createParser(AnalogueParser, this.genericParse); + quoteParser = createParser(QuoteParser, this.genericParse); + sourceAttr = AppConfig.evtSettings.edition.externalBibliography.elementAttributesToMatch; + analogueMarkers = AppConfig.evtSettings.edition.analogueMarkers; + source = null; analogue = null; parse(xml: XMLElement): VersesGroup { + + if (isAnalogue(xml, this.analogueMarkers)) { + this.analogue = this.analogueParser.parse(xml); + } + if (isSource(xml, this.sourceAttr)) { + this.source = this.quoteParser.parse(xml); + } + const attributes = this.attributeParser.parse(xml); const lgComponent: VersesGroup = { type: VersesGroup, @@ -203,6 +282,8 @@ export class VersesGroupParser extends EmptyParser implements Parser attributes, n: getDefaultN(attributes.n), groupType: getDefaultN(attributes.type), + source: this.source, + analogue: this.analogue, }; return lgComponent; @@ -329,3 +410,82 @@ export class TermParser extends GenericElemParser implements Parser }; } } + +@xmlParser('milestone', MilestoneParser) +export class MilestoneParser extends GenericElemParser implements Parser { + parse(xml: XMLElement): Milestone { + + const elements = getSpanToElements(xml, xml.getAttribute('spanTo')); + const parsedElements = elements.elements.map((x) => super.parse(x)); + + return { + type: Milestone, + id: xml.getAttribute('xml:id'), + attributes: this.attributeParser.parse(xml), + unit: xml.getAttribute('unit'), + spanText: elements.text, + spanElements: parsedElements, + content: parseChildren(xml, this.genericParse), + }; + } +} + +@xmlParser('anchor', AnchorParser) +export class AnchorParser extends GenericElemParser implements Parser { + + attributeParser = createParser(AttributeParser, this.genericParse); + //todo: check if a span is referring to this element's @xml:id? + + parse(xml: XMLElement): Anchor { + + return { + type: Anchor, + id: xml.getAttribute('xml:id'), + attributes: this.attributeParser.parse(xml), + content: parseChildren(xml, this.genericParse), + }; + } +} + + +@xmlParser('spanGrp', SpanParser) +@xmlParser('span', SpanParser) +export class SpanParser extends GenericElemParser implements Parser { + + attributeParser = createParser(AttributeParser, this.genericParse); + + parse(xml: XMLElement): Span|SpanGrp { + + if (xml.tagName === 'spanGrp') { + + return { + type: SpanGrp, + id: xml.getAttribute('xml:id'), + attributes: this.attributeParser.parse(xml), + spans: Array.from(xml.querySelectorAll('span')).map((x) => this.parse(x)), + content: parseChildren(xml, this.genericParse), + } + + } else if (xml.tagName === 'span') { + let included = { text: '', elements: [] }; + let parsedElements = []; + const startingElement = getExternalElements(xml, ['from'], 'xml:id', 'anchor'); + if (startingElement.length > 0) { + included = getSpanToElements(startingElement[0], xml.getAttribute('to')); + parsedElements = included.elements.map((x) => super.parse(x)); + } + + return { + type: Span, + id: xml.getAttribute('xml:id'), + attributes: this.attributeParser.parse(xml), + from: xml.getAttribute('from'), + to: xml.getAttribute('to'), + includedText: included.text, + includedElements: parsedElements, + content: parseChildren(xml, this.genericParse), + }; + } + + } +} diff --git a/src/app/services/xml-parsers/bibliographic-entries-parser.service.ts b/src/app/services/xml-parsers/bibliographic-entries-parser.service.ts new file mode 100644 index 000000000..0dff3556e --- /dev/null +++ b/src/app/services/xml-parsers/bibliographic-entries-parser.service.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@angular/core'; +import { ParserRegister } from '.'; +import { BibliographyClass, XMLElement } from '../../models/evt-models'; + +@Injectable({ + providedIn: 'root', +}) +export class BibliographicEntriesParserService { + + private tagName = `.${BibliographyClass}`; + private parserName = 'evt-bibliographic-entry-parser'; + + public parseAnaloguesEntries(document: XMLElement) { + + const bibliographicParser = ParserRegister.get(this.parserName); + + return Array.from(document.querySelectorAll(this.tagName)) + .map((bib) => bibliographicParser.parse(bib)); + } + +} + diff --git a/src/app/services/xml-parsers/bilbliography-parsers.ts b/src/app/services/xml-parsers/bilbliography-parsers.ts new file mode 100644 index 000000000..f72e554d2 --- /dev/null +++ b/src/app/services/xml-parsers/bilbliography-parsers.ts @@ -0,0 +1,97 @@ +import { normalizeSpaces } from 'src/app/utils/xml-utils'; +import { parse, xmlParser } from '.'; +import { BibliographicEntry, BibliographicList, BibliographicStructEntry, BibliographyClass, XMLElement } from '../../models/evt-models'; +import { getOuterHTML } from '../../utils/dom-utils'; +import { AttributeParser, GenericElemParser } from './basic-parsers'; +import { createParser, getID, parseChildren, Parser } from './parser-models'; +import { BasicParser } from './quotes-parser'; + +@xmlParser('listBibl', BibliographyParser) +@xmlParser('biblStruct', BibliographyParser) +@xmlParser('bibl', BibliographyParser) +@xmlParser('evt-bibliographic-entry-parser', BibliographyParser) +export class BibliographyParser extends BasicParser implements Parser { + + protected attributeParser = createParser(AttributeParser, this.genericParse); + protected elementParser = createParser(GenericElemParser, parse); + + protected getTrimmedText = function(s) { + return s.textContent.replace(/[\n\r]+|[\s]{2,}/g, ' ').trim(); + } + + protected getChildrenTextByName = function(xml : XMLElement, name : string) { + return Array.from(xml.querySelectorAll(name)).map((x) => ' '+this.getTrimmedText(x)); + } + + protected getChildrenByNameOnFirstLevelOnly = function(xml : XMLElement, name : string) { + return Array.from(xml.querySelectorAll(':scope > '+name)).map((x) => this.getTrimmedText(x)); + } + + protected getChildrenTextAndSpecificAttribute = function(xml: XMLElement, name: string, attribute: string) { + return Array.from(xml.querySelectorAll(name)).map((x) => x.getAttribute(attribute)+' '+this.getTrimmedText(x)); + } + + protected getQuoteElementText(element: XMLElement): string { + const target = (element.parentNode['tagName'] === 'cit' || element.parentNode['tagName'] === 'note') ? element.parentNode : element; + const search = Array.from(target.querySelectorAll('quote')); + if (search.length !== 0) { + return normalizeSpaces(search[0].textContent); + } + + return null; + } + + parse(xml: XMLElement): any { + + switch (xml.tagName) { + case 'ref': + case 'analytic': + case 'series': + case 'monogr': + case 'bibl': + return { + type: BibliographicEntry, + id: getID(xml), + class: BibliographyClass, + attributes: this.attributeParser.parse(xml), + title: this.getChildrenTextByName(xml,'title'), + author: this.getChildrenTextByName(xml,'author'), + editor: this.getChildrenTextByName(xml,'editor'), + date: this.getChildrenTextByName(xml,'date'), + publisher: this.getChildrenTextByName(xml,'publisher'), + pubPlace: this.getChildrenTextByName(xml,'pubBlace'), + citedRange: this.getChildrenTextByName(xml,'citedRange'), + biblScope: this.getChildrenTextAndSpecificAttribute(xml, 'biblScope', 'unit'), + content: parseChildren(xml, this.genericParse), + text: xml.textContent, + quotedText: this.getQuoteElementText(xml), + insideCit: (xml.parentNode['tagName'] === 'cit' || xml.parentNode['tagName'] === 'note'), + originalEncoding: getOuterHTML(xml), + }; + case 'cit': + case 'listBibl': + case 'note': + return { + type: BibliographicList, + id: getID(xml), + attributes: this.attributeParser.parse(xml), + head: Array.from(xml.querySelectorAll('head')).map((x) => x.textContent), + sources: Array.from(xml.querySelectorAll('bibl')).map((x) => this.parse(x)), + content: parseChildren(xml, this.genericParse), + }; + case 'biblStruct': + return { + type: BibliographicStructEntry, + id: getID(xml), + attributes: this.attributeParser.parse(xml), + analytic: Array.from(xml.querySelectorAll('analytic')).map((x) => this.parse(x)), + monogrs: Array.from(xml.querySelectorAll('monogr')).map((x) => this.parse(x)), + series: Array.from(xml.querySelectorAll('series')).map((x) => this.parse(x)), + content: parseChildren(xml, this.genericParse), + originalEncoding: getOuterHTML(xml), + }; + default: + return this.elementParser.parse(xml) + } + } +} diff --git a/src/app/services/xml-parsers/index.ts b/src/app/services/xml-parsers/index.ts index 058dd84f8..e1b65180d 100644 --- a/src/app/services/xml-parsers/index.ts +++ b/src/app/services/xml-parsers/index.ts @@ -27,11 +27,14 @@ export class ParserRegister { if (nels.includes(tagName)) { return 'evt-named-entities-list-parser'; } + const quote = ['quote', 'cit', 'ref', 'seg', 'div']; + if (quote.includes(tagName)) { + return 'evt-quote-entry-parser'; + } const crit = ['app']; if (crit.includes(tagName)) { return 'evt-apparatus-entry-parser'; } - if (!Object.keys(ParserRegister.PARSER_MAP).includes(tagName)) { return 'evt-generic-elem-parser'; } diff --git a/src/app/services/xml-parsers/msdesc-parser.ts b/src/app/services/xml-parsers/msdesc-parser.ts index 68b0eeb41..a1900c556 100644 --- a/src/app/services/xml-parsers/msdesc-parser.ts +++ b/src/app/services/xml-parsers/msdesc-parser.ts @@ -1,16 +1,17 @@ import { isBoolString } from 'src/app/utils/js-utils'; import { xmlParser } from '.'; import { - AccMat, Acquisition, Additional, Additions, AdminInfo, AltIdentifier, Binding, BindingDesc, Collation, CollectionEl, Condition, + AccMat, Acquisition, Additional, Additions, AdminInfo, AltIdentifier, BibliographicEntry, Binding, BindingDesc, Collation, CollectionEl, Condition, CustEvent, CustodialHist, DecoDesc, DecoNote, Depth, Dim, Dimensions, Explicit, Filiation, FinalRubric, Foliation, G, HandDesc, HandNote, Head, Height, History, Identifier, Incipit, Institution, Layout, LayoutDesc, Locus, LocusGrp, MaterialValues, MsContents, MsDesc, MsFrag, MsIdentifier, MsItem, MsItemStruct, MsName, MsPart, MusicNotation, Note, ObjectDesc, OrigDate, - Origin, OrigPlace, Paragraph, PhysDesc, Provenance, RecordHist, Repository, Rubric, ScriptDesc, Seal, SealDesc, Source, Summary, + Origin, OrigPlace, Paragraph, PhysDesc, Provenance, QuoteEntry, RecordHist, Repository, Rubric, ScriptDesc, Seal, SealDesc, Source, Summary, Support, SupportDesc, Surrogates, Text, TypeDesc, TypeNote, Width, XMLElement, } from '../../models/evt-models'; import { GenericElemParser, queryAndParseElement, queryAndParseElements } from './basic-parsers'; import { GParser } from './character-declarations-parser'; import { createParser, getClass, getDefaultN, getID, parseChildren, Parser, unhandledElement } from './parser-models'; +import { BibliographicList } from '../../models/evt-models'; class GAttrParser extends GenericElemParser { protected gParser = createParser(GParser, this.genericParse); @@ -694,10 +695,10 @@ export class MsItemStructParser extends GenericElemParser implements Parser(xml, 'bibl'), respStmt: unhandledElement(xml, 'respStmt', this.genericParse), - quote: unhandledElement(xml, 'quote', this.genericParse), - listBibl: unhandledElement(xml, 'listBibl', this.genericParse), + quote: queryAndParseElement(xml, 'quote'), + listBibl: queryAndParseElement(xml, 'listBibl'), colophons: unhandledElement(xml, 'colophon', this.genericParse), rubric: queryAndParseElement(xml, 'rubric'), incipit: queryAndParseElement(xml, 'incipit'), diff --git a/src/app/services/xml-parsers/quotes-parser.ts b/src/app/services/xml-parsers/quotes-parser.ts new file mode 100644 index 000000000..6b5179649 --- /dev/null +++ b/src/app/services/xml-parsers/quotes-parser.ts @@ -0,0 +1,335 @@ +import { AppConfig } from 'src/app/app.config'; +import { ParserRegister, xmlParser } from '.'; +import { Analogue, BibliographicEntry, BibliographicList, BibliographicStructEntry, + GenericElement, Note, Paragraph, Ptr, QuoteEntry, SourceClass, Verse, VersesGroup, + XMLElement } from '../../models/evt-models'; +import { AnalogueParser } from './analogue-parser'; +import { createParser, getID, parseChildren, ParseFn, Parser } from './parser-models'; +import { getOuterHTML } from 'src/app/utils/dom-utils'; +import { isAnalogue, isSource, normalizeSpaces } from 'src/app/utils/xml-utils'; +import { BibliographyParser } from './bilbliography-parsers'; +import { chainFirstChildTexts } from '../../utils/xml-utils'; +import { GenericElemParser } from './basic-parsers'; + +export class BasicParser { + genericParse: ParseFn; + constructor(parseFn: ParseFn) { this.genericParse = parseFn; } +} + +@xmlParser('ref', QuoteParser) +@xmlParser('seg', QuoteParser) +@xmlParser('quote', QuoteParser) +@xmlParser('cit', QuoteParser) +@xmlParser('div', QuoteParser) +//@xmlParser('note', QuoteParser) on redirect from their parser +//@xmlParser('p', QuoteParser) " +//@xmlParser('l', QuoteParser) " +//@xmlParser('lb', QuoteParser) " +//@xmlParser('ptr', QuoteParser) " +@xmlParser('evt-quote-entry-parser', QuoteParser) +export class QuoteParser extends BasicParser implements Parser { + + elementParser = createParser(GenericElemParser, this.genericParse); + attributeParser = ParserRegister.get('evt-attribute-parser'); + msDescParser = ParserRegister.get('msDesc'); + biblParser = createParser(BibliographyParser, this.genericParse); + analogueParser = createParser(AnalogueParser, this.genericParse); + + analogueMarkers = AppConfig.evtSettings.edition.analogueMarkers; + extMatch = AppConfig.evtSettings.edition.externalBibliography.biblAttributeToMatch; + intAttrsToMatch = AppConfig.evtSettings.edition.externalBibliography.elementAttributesToMatch; + exceptionParentElements = AppConfig.evtSettings.edition.sourcesExcludedFromListByParent; + elementsAllowedForSources = 'bibl, cit, note, seg'; // bibliography + elementsAllowedForLink = 'seg, ref, quote, cit, div'; // nested quote elements + notNiceInText = ['Note', 'BibliographicList', 'BibliographicEntry', + 'BibliographicStructEntry', 'Analogue', 'MsDesc']; + + xpathRegex = /\sxpath=[\"\'].*[\"\']/g; + + public parse(quote: XMLElement): QuoteEntry | Note | GenericElement | Analogue { + + const isExcluded = (quote.parentElement) && (this.exceptionParentElements.includes(quote.parentElement.tagName)); + + if (isAnalogue(quote, this.analogueMarkers)) { + // the element has the @attribute marker for analogues + return this.analogueParser.parse(quote); + } + + const notSource = ((!isSource(quote, this.intAttrsToMatch)) || (isExcluded)); + + switch(quote.tagName) { + case 'p': + case 'lg': + case 'l': + case 'note': + // if they are not a source send them to their parse + // otherwise create a note + if (notSource) { + return ParserRegister.get(quote.tagName).parse(quote) as Paragraph|VersesGroup|Verse|Note; + } + + return this.createNote(quote); + case 'div': + // if it's not a source create a generic element + // if it's a source add a note to the generic element + const divElement = ParserRegister.get('evt-generic-elem-parser').parse(quote) as GenericElement; + if (notSource) { + return divElement; + } + divElement.content.push( this.createNote(quote) ); + + return divElement; + case 'ptr': + // if it's not a source send it to its parse + // otherwise parse here + if (notSource) { + return ParserRegister.get(quote.tagName).parse(quote) as Ptr; + } + break; + case 'ref': + case 'seg': + // if they are not a source create a generic element + // otherwise parse here + if (notSource) { + return ParserRegister.get('evt-generic-elem-parser').parse(quote) as GenericElement; + } + break; + case 'quote': + case 'cit': + // always parse here + break; + } + + // remains 6 cases: + // inside a + // alone, + // + // + // ptr + // note + + const isInCit = ((quote.parentElement !== null) && ((quote.parentElement.tagName === 'cit') || (quote.parentElement.tagName === 'note'))); + const isCit = ((quote.tagName === 'cit') || (quote.tagName === 'note')); + const isDiv = (quote.tagName === 'div'); + const isQuote = (quote.tagName === 'quote'); + const isAnalogueCheck = ((isAnalogue(quote, this.analogueMarkers)) || ( + (quote.parentElement !== null) && (isAnalogue(quote.parentElement, this.analogueMarkers)) + )); + const inside = this.getInsideSources(quote, isInCit); + const ext = this.getExternalElemsOnce(this.findExtRef(quote, isInCit), this.intAttrsToMatch, this.extMatch); + const content = parseChildren(quote, this.genericParse); + + return { + type: QuoteEntry, + id: getID(quote), + tagName: quote.tagName, + attributes: this.attributeParser.parse(quote), + text: normalizeSpaces(this.getQuotedTextInside(quote, isCit, isDiv)), + sources: inside.sources, + extSources: ext.extSources, + extElements: ext.extElements, + quotedText: this.getQuotedTextFromSources(inside.sources.concat(ext.extSources)), + analogues: ext.analogues.concat(inside.analogues), + class: ( (!isAnalogueCheck) && (!isCit) && ( (!isInCit) || ((isInCit) && (isQuote)) ) ) ? SourceClass : quote.tagName.toLowerCase(), + insideCit: isInCit, + noteView: ((quote.tagName === 'note') || (quote.tagName === 'ptr')) ? true : false, + content: content, + contentToShow: content.filter((x) => !(this.notNiceInText.includes(x['type'].name))), + originalEncoding: this.cleanXMLString(quote, isInCit), + }; + } + + /** + * Returns first-level text elements inside this quote entry + * @param quote XMLElement + * @returns XMLElement + */ + private getQuotedTextInside(quote: XMLElement, isCit: boolean, isDiv: boolean): string { + let outText = ''; + if ((isCit) || (isDiv)) { + const elements = Array.from(quote.querySelectorAll('quote, p, l, lg')); + elements.forEach((el) => outText += chainFirstChildTexts(el)); + + return outText; + } + + return chainFirstChildTexts(quote); + } + + /** + * Remove unwanted output from XML string + * @param quote XMLElement + * @param inCitElem boolean + * @returns String + */ + private cleanXMLString(quote: XMLElement, inCitElem: boolean): string { + if (inCitElem) { + return getOuterHTML(quote.parentElement).replace(this.xpathRegex,''); + } + + return getOuterHTML(quote).replace(this.xpathRegex,''); + } + + + /** + * Retrieve attributes linked to external bibl/listBibl/cit elements + * @param quote XMLElement + * @param insideCit boolean + * @returns XMLElement + */ + private findExtRef(quote: XMLElement, inCitElem: boolean): XMLElement { + const target = (inCitElem) ? quote.parentElement : quote; + const linkAttr = Array.from(target.querySelectorAll('[' +this.intAttrsToMatch.join('], [')+ ']')).map((x) => x); + if (linkAttr.length > 0) { + return linkAttr[0]; + } + + return target; + } + + /** + * Retrieve and send to the proper parsing all bibliography elements inside the quote element + * @param quote XMLElement + * @returns array of Bibliography Element or a single Bibliography List element + */ + private getInsideSources(quote: XMLElement, isInCit: boolean): { sources:any, analogues:any } { + const prsRg = { + bibl: this.biblParser, + listBibl: this.biblParser, + biblStruct: this.biblParser, + msDesc: this.msDescParser, + cit: this.biblParser, + ref: this.biblParser, + note: this.biblParser, + } + + const anlgAttr = AppConfig.evtSettings.edition.analogueMarkers; + const target = (isInCit) ? quote.parentElement.children : quote.children; + const out = { sources: [], analogues: [] } + + Array.from(target).forEach((element: XMLElement) => { + if (prsRg[element['tagName']]) { + if (!anlgAttr.includes(element.getAttribute('type'))) { + out.sources.push( prsRg[element['tagName']].parse(element) ); + } else { + console.log('analogue inside',element['tagName']); + const analogueParsed = this.parse(element); + if (analogueParsed['contentToShow']) { + out.analogues.push( analogueParsed['contentToShow'] ); + } + } + } + }); + + return out; + } + + /** + * Retrieve and send to the proper parsing all elements outside the quote element and linked by their @xml:id + * @param quote XMLElement + * @param attrSrcNames string[] + * @oaram attrTrgtName string + * @returns array of Bibliography Element or a single Bibliography List element + */ + private getExternalElemsOnce(quote: XMLElement, attrSrcNames: string[], attrTrgtName: string): any { + const out = { extElements: [], extSources: [], analogues: [] }; + const sourceIDs = attrSrcNames.map((x) => quote.getAttribute(x)); + const sourcesToFind = sourceIDs.filter((x) => x).map((x) => x.replace('#','')); + const anlgAttr = AppConfig.evtSettings.edition.analogueMarkers; + const elemParserAssoc = { + // bibliographic elements + bibl: { extSources: this.biblParser }, + listBibl: { extSources: this.biblParser }, + msDesc: { extSources: this.msDescParser }, + biblStruct: { extSources: this.biblParser }, + + note: { extElements: this }, + // this parser elements + quote: { extElements: this }, + cit: { extElements: this }, + + seg: { extSources: this.biblParser, extElements: this }, + ref: { extSources: this.biblParser, extElements: this }, + // possibile chained sources/analogues + div: { extElements: this }, + p: { extElements: this }, + l: { extElements: this }, + lg: { extElements: this }, + // generic elements + item: { extElements: this.elementParser }, + } + + if (sourcesToFind.length > 0) { + const partial = Array.from(quote.ownerDocument.querySelectorAll(Object.keys(elemParserAssoc).join(','))) + .filter((x) => sourcesToFind.includes(x.getAttribute(attrTrgtName))) + + partial.forEach((x: XMLElement) => { + if (elemParserAssoc[x['tagName']]) { + Object.keys(elemParserAssoc[x['tagName']]).forEach((destination) => { + if (anlgAttr.includes(x.getAttribute('type'))) { + const analogueParsed = this.parse(x); + if (analogueParsed['contentToShow']) { + out['analogues'].push( analogueParsed['contentToShow'] ) + } + } else { + const relativeParser = elemParserAssoc[x['tagName']][destination]; + out[destination].push( relativeParser.parse(x) ) + } + }) + }; + }); + } + + return out; + } + + /** + * Inside 'quotedText' attribute of BibliographicEntry is stored the quoted text + * @param nodes BibliographicEntry[] + * @returns array of object { id: '', 'quote':'' } + */ + private getQuotedTextFromSources(nodes: BibliographicEntry[]): string[] { + let quotesInSources = []; + + nodes.forEach((el: BibliographicEntry|BibliographicList|BibliographicStructEntry) => { + if (el.type === BibliographicList) { + quotesInSources = quotesInSources.concat(this.getQuotedTextFromSources(el['sources'])); + } else if (el.type === BibliographicEntry) { + if (el['quotedText'] !== null) { + quotesInSources.push({ id: el.id, quote: el['quotedText'] }); + } + }; + }); + + return quotesInSources; + } + + private createNote(quote: XMLElement) { + // const sources = this.getInsideSources(quote, false); + // not parsing inside sources, they will be parsed anyway + const isCit = ((quote.tagName === 'cit') || (quote.tagName === 'note')); + const isDiv = (quote.tagName === 'div'); + const ext = this.getExternalElemsOnce(quote, this.intAttrsToMatch, this.extMatch); + const content = parseChildren(quote, this.genericParse); + + return { + type: QuoteEntry, + id: 'EVT-SRC:'+getID(quote), + tagName: quote.tagName, + attributes: this.attributeParser.parse(quote), + text: normalizeSpaces(this.getQuotedTextInside(quote, isCit, isDiv)), + sources: [], + analogues: ext.analogues, + extSources: ext.extElements.concat(ext.extSources), + extElements: ext.extElements, + quotedText: this.getQuotedTextFromSources(ext.extElements.concat(ext.extSources)), + class: SourceClass, + insideCit: false, + noteView: true, + content: content, + contentToShow: content.filter((x) => !(this.notNiceInText.includes(x['type'].name))), + originalEncoding: this.cleanXMLString(quote, false), + }; + } + +} diff --git a/src/app/services/xml-parsers/source-entries-parser.service.ts b/src/app/services/xml-parsers/source-entries-parser.service.ts new file mode 100644 index 000000000..8d6e64d21 --- /dev/null +++ b/src/app/services/xml-parsers/source-entries-parser.service.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@angular/core'; +import { ParserRegister } from '.'; +import { QuoteEntry, SourceClass, XMLElement } from '../../models/evt-models'; + +@Injectable({ + providedIn: 'root', +}) +export class SourceEntriesParserService { + + public parseSourceEntries(document: XMLElement) { + const quoteParser = ParserRegister.get('evt-quote-entry-parser'); + + return [ + Array.from(document.querySelectorAll(`.${SourceClass}`)) + .map((srcEntry) => quoteParser.parse(srcEntry) as QuoteEntry), + ]; + } + +} diff --git a/src/app/services/xml-parsers/xml-parsers.ts b/src/app/services/xml-parsers/xml-parsers.ts index d00b1bddf..5fdd5c685 100644 --- a/src/app/services/xml-parsers/xml-parsers.ts +++ b/src/app/services/xml-parsers/xml-parsers.ts @@ -1,8 +1,8 @@ import { Injectable, Type } from '@angular/core'; import { AppParser, RdgParser } from './app-parser'; import { - AdditionParser, AttributeMapParser, AttributeParser, DamageParser, DeletionParser, GapParser, - GenericElemParser, LBParser, NoteParser, ParagraphParser, PtrParser, SuppliedParser, + AdditionParser, AnchorParser, AttributeMapParser, AttributeParser, DamageParser, DeletionParser, GapParser, + GenericElemParser, LBParser, MilestoneParser, NoteParser, ParagraphParser, PtrParser, SpanParser, SuppliedParser, TermParser, TextParser, VerseParser, VersesGroupParser, WordParser, } from './basic-parsers'; import { CharParser, GlyphParser, GParser } from './character-declarations-parser'; @@ -38,6 +38,8 @@ import { NamedEntitiesListParser, NamedEntityRefParser, OrganizationParser, PersonGroupParser, PersonParser, PlaceParser, RelationParser, } from './named-entity-parsers'; +import { QuoteParser } from './quotes-parser'; +import { AnalogueParser } from './analogue-parser'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export function ParsersDecl(declarations: Array>) { @@ -58,6 +60,8 @@ export function ParsersDecl(declarations: Array>) { AdditionParser, AdditionsParser, AdminInfoParser, + AnalogueParser, + AnchorParser, AltIdentifierParser, AppParser, AttributeMapParser, @@ -130,6 +134,7 @@ export function ParsersDecl(declarations: Array>) { ListTransposeParser, LocusGrpParser, LocusParser, + MilestoneParser, MsContentsParser, MsDescParser, MsFragParser, @@ -165,6 +170,7 @@ export function ParsersDecl(declarations: Array>) { PublicationStmtParser, PunctuationParser, PurposeParser, + QuoteParser, QuotationParser, RdgParser, RecordHistParser, @@ -188,6 +194,7 @@ export function ParsersDecl(declarations: Array>) { SicParser, SourceDescParser, SourceParser, + SpanParser, StdValsParser, StyleDefDeclParser, SummaryParser, diff --git a/src/app/utils/dom-utils.ts b/src/app/utils/dom-utils.ts index 83bccd0a3..01d6c780c 100644 --- a/src/app/utils/dom-utils.ts +++ b/src/app/utils/dom-utils.ts @@ -229,3 +229,35 @@ export function getCommonAncestor(node1, node2) { export function createNsResolver(doc: Document) { return (prefix: string) => prefix === 'ns' ? doc.documentElement.namespaceURI : undefined; } + +export function updateCSS(rules: Array<[string, string]>) { + const thisCSS = document.styleSheets[0]; + rules.forEach((rule) => { + thisCSS.insertRule(`${rule[0]} {${rule[1]}}`, 0); + }); +} + +export function deepSearch(obj, attrToMatch: string, valuesToMatch: any[], limit: number = 4000) { + const results = []; + let counter = limit; + function search(obj) { + for (const key in obj) { + const value = obj[key]; + if ((key === attrToMatch) && (valuesToMatch.includes(obj[attrToMatch]))) { + results.push(obj); + } + if (typeof value === 'object' && value !== null) { + if (counter > 0) { + search(value); + counter = counter - 1; + } else { + console.log('EVT WARN: element is too deep, not searching further in', obj); + } + } + } + } + search(obj); + + return results; +} + diff --git a/src/app/utils/xml-utils.ts b/src/app/utils/xml-utils.ts index 96d7722d9..c88b67360 100644 --- a/src/app/utils/xml-utils.ts +++ b/src/app/utils/xml-utils.ts @@ -36,3 +36,141 @@ export function replaceNotWordChar(textContent: string) { export function removeSpaces(textContent: string) { return textContent.replace(/\s/g, ''); } + +/** + * It removes excessive spaces, any tabulation, new lines and non-word characters + * @param textContent string + * @returns string + */ +export function normalizeSpaces(textContent: string) { + return textContent.replace(/[\s]{2,}|\n|\t|\r/g, ' ').trimStart().trimEnd(); +} + +/** +* Significant text sometimes is split inside two or more text evt-element inside the main one, especially when it contains new line characters. +* This function returns a string with all the text element chained +* @param n XMLElement +* @returns string +*/ +export function chainFirstChildTexts(elem: XMLElement): string { + if (elem === undefined) { return ''}; + const evtTextElements = { + '#text': 'nodeValue', + 'p': 'textContent', + }; + const evtTextComplexElements = ['choice', 'app', 'l', 'quote', 'p', 'lg']; + let out = ''; + elem.childNodes.forEach((x) => (evtTextElements[x.nodeName] !== undefined) ? out += x[ evtTextElements[x.nodeName] ] : ( + evtTextComplexElements.includes(x.nodeName) ? out += chainDeepTexts(x) : '' )) + + return out; +} + +/** + * Retrieve and chain textContent of all descendents + * @param elem ChildNode + * @returns out string + */ +export function chainDeepTexts(elem: ChildNode): string { + const evtInnerTextElements = ['#text', 'reg', 'corr', 'rdg']; + const textProperty = 'textContent'; + let out = ''; + elem.childNodes.forEach((x) => (evtInnerTextElements.includes(x.nodeName)) ? ((x[textProperty] !== null) ? out += x[textProperty] : '') : '') + + return out; +} + +/** +* Retrieve external elements searching between provided elements types and filtering on attributes base +* It searches all document for a bibl with the correct xml:id. +* It would be faster if we knew the id or unique element to search in +* @param element XMLElement +* @param attrSourceNames +* @param attrTargetName +* @returns array of XMLElement +*/ +export function getExternalElements(elem: XMLElement, attrSourceNames: string[], attrTargetName: string, elTypes: string): XMLElement[] { + const sourceIDs = attrSourceNames.map((x) => elem.getAttribute(x)); + const sourcesToFind = sourceIDs.filter((x) => x).map((x) => x.replace('#','')); + + if (sourcesToFind.length === 0) { + return []; + } + + return Array.from(elem.ownerDocument.querySelectorAll(elTypes)) + .filter((x) => sourcesToFind.includes(x.getAttribute(attrTargetName))) +} + +export function isAnalogue(elem: XMLElement, markerAttrs: string[]): boolean { + return (markerAttrs.includes(elem.getAttribute('type'))); +} + +export function isSource(elem: XMLElement, attrs: string[]): boolean { + let validAttrs = false; + attrs.forEach((attr) => { if (elem.getAttribute(attr) !== null) { validAttrs = true } }); + + return (validAttrs); +} + +/** + * Get an element of the same type with the given xml:id and update it with a copy of the source element + * @param fromElement XMLElement + * @param toXMLID string + */ +export function getCorrespElement(fromElement: XMLElement, toXMLID: string) { + const findID = toXMLID.replace('#',''); + const sameTypeElements = fromElement.ownerDocument.querySelectorAll(fromElement.tagName); + let found = null; + sameTypeElements.forEach((x) => { if (x.getAttribute('xml:id') === findID) { found = x; return }}); + if (found !== null) { + found.appendChild(fromElement.cloneNode()); + } +} + + +/** + * Retrieve textContent and elements between the provided element and the one found with the xml:id provided + * @param fromElement XMLElement + * @param toXMLID string + * @returns object { text: string, elements: any } + */ +export function getSpanToElements(fromElement: XMLElement, toXMLID: string): { text: string, elements: any } { + + if ((fromElement === null) || (fromElement === undefined) || (toXMLID === null)) { + return { text: '', elements: [] }; + } + + const findID = toXMLID.replace('#',''); + let found = false; + // text after the milestone but still inside the parent element + let foundText = fromElement.nextSibling.textContent; + // the milestone is always inside another element? + // otherwise const foundElements = [fromElement.nextSibling]; + let next = (fromElement.nextElementSibling !== null) ? + fromElement.nextElementSibling as XMLElement : + fromElement.parentElement.nextElementSibling as XMLElement; + + // creating a fake element for partial text included from milestone on + const nextEdited = fromElement.parentElement.nextElementSibling.cloneNode(true); + nextEdited.textContent = foundText; + const foundElements = [nextEdited]; + + let maxExec = 1000; + + while(!found && next !== null && maxExec !== 0) { + foundElements.push(next); + foundText = foundText + next.textContent; + if (next.getAttribute('xml:id') === findID) { + found = true; + } else { + maxExec--; + if (next.nextElementSibling === null) { + next = next.parentElement.nextElementSibling as XMLElement; + } else { + next = next.nextElementSibling as XMLElement; + } + } + } + + return { 'text': foundText, 'elements': foundElements }; +} diff --git a/src/app/view-modes/reading-text/reading-text.component.html b/src/app/view-modes/reading-text/reading-text.component.html index 693762a31..7f5c2757e 100644 --- a/src/app/view-modes/reading-text/reading-text.component.html +++ b/src/app/view-modes/reading-text/reading-text.component.html @@ -15,21 +15,40 @@
diff --git a/src/app/view-modes/reading-text/reading-text.component.scss b/src/app/view-modes/reading-text/reading-text.component.scss index 2eeaa2ce0..3d81b5402 100644 --- a/src/app/view-modes/reading-text/reading-text.component.scss +++ b/src/app/view-modes/reading-text/reading-text.component.scss @@ -17,5 +17,20 @@ .tab-pane { padding: 1rem; } + .source-container .tab-pane { + padding: 0.3em !important; + } + + .nav { + font-family: serif; + --bs-nav-link-color: white; + --bs-nav-link-hover-color: white; + } + + .nav-tabs { + color: #ECEFF1; + background-color: #263238; + } + } } \ No newline at end of file diff --git a/src/assets/config/edition_config.json b/src/assets/config/edition_config.json index ef7ee3369..3a634cf2e 100644 --- a/src/assets/config/edition_config.json +++ b/src/assets/config/edition_config.json @@ -105,5 +105,19 @@ "defaultTextFlow": "prose", "verseNumberPrinter": 5, "readingColorLight": "rgb(208, 220, 255)", - "readingColorDark": "rgb(101, 138, 255)" + "readingColorDark": "rgb(101, 138, 255)", + "externalBibliography": { + "biblAttributeToMatch": "xml:id", + "elementAttributesToMatch": ["target","source"] + }, + "biblView": { + "propToShow": ["author","title","date","editor","publisher","pubPlace","citedRange","biblScope"], + "showAttrNames": false, + "showEmptyValues": false, + "inline": true, + "comaSeparated": true, + "showMainElemTextContent": true + }, + "analogueMarkers": ["ParallelPassage", "parallelPassage", "Parallel", "parallel"], + "sourcesExcludedFromListByParent": [ "desc" ] } \ No newline at end of file diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 686e2c0f8..633b9abd5 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -172,5 +172,17 @@ "moreInfoAboutApp": "More info about the apparatus entry", "omit": "omit.", "wit": "Wit:", - "xml": "XML" + "xml": "XML", + "criticalApparatus":"Critical Aparatus", + "sources": "Sources", + "analogues": "Analogues", + "source": "Source", + "analogue": "Analogue", + "notes":"Notes", + "refBibl": "Bibliographic references", + "quotedSources": "Quoted sources", + "quotedText": "Text", + "references": "References", + "analogueIn": "Analogue in", + "corresps": "Correspondences" } \ No newline at end of file diff --git a/src/assets/i18n/it.json b/src/assets/i18n/it.json index c599b88e2..dfb6c40e2 100644 --- a/src/assets/i18n/it.json +++ b/src/assets/i18n/it.json @@ -171,5 +171,17 @@ "moreInfoAboutApp": "Maggiori informazioni per l'entrata d'apparato", "omit": "omit.", "wit": "Wit:", - "xml": "XML" + "xml": "XML", + "criticalApparatus":"Apparato Critico", + "sources": "Fonti", + "analogues": "Passi Paralleli", + "source": "Fonte", + "analogue": "Passo Parallelo", + "notes":"Note Critiche", + "refBibl": "Riferimenti Bibliografici", + "quotedSources": "Fonti citate", + "quotedText": "Testo", + "references": "Riferimenti", + "analogueIn": "Passo parallelo in", + "corresps": "Corrispondenze" } \ No newline at end of file diff --git a/src/assets/scss/_colors.scss b/src/assets/scss/_colors.scss index 8178afda0..d273742ee 100644 --- a/src/assets/scss/_colors.scss +++ b/src/assets/scss/_colors.scss @@ -17,6 +17,8 @@ $editionColors: ( ), commentNotes: rgb(87, 14, 105), criticalNotes: rgb(24, 70, 155), + analogueNotes: rgb(24, 70, 155), + sourceNotes: rgb(24, 155, 90), normalization: #69513a, normalizationBackground: #fffbf3, emendation: #934d4d, diff --git a/src/styles.scss b/src/styles.scss index 581e02b93..81521b019 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -182,3 +182,52 @@ body { font-size: 0.9rem; } +evt-biblio-entry span:last-of-type { + display:none !important; +} + +evt-biblio-list .msIdentifier { + margin-bottom: 2rem; + margin-left: 1em; + font-size: 90%; + line-height: 1.4rem; +} + +evt-quote-entry evt-source-detail > div { + box-shadow: none !important; + margin: 0 !important; +} + +evt-analogue-entry evt-analogue-detail > div { + box-shadow: none !important; + margin: 0 !important; +} + +.popover-body evt-quote-entry, +.popover-body evt-analogue-entry { + pointer-events:none +} + +.biblio-list li { + margin-left: 1em; +} + +.source-detail-content p { + margin-bottom: 0; + margin-top: 0; +} + +evt-analogue-detail .verse-num, +evt-source-detail .verse-num { + display: none !important; +} + +evt-analogue-entry evt-paragraph, +evt-quote-entry evt-paragraph { + display: inline-block; +} + +evt-analogue-entry evt-paragraph p, +evt-quote-entry evt-paragraph p { + margin-bottom:0; +} \ No newline at end of file