+
0">
+ {{ '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 @@
+
+
+ 0">
+
+
+
+
+
+
+
+
+
+
\ 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 @@
+
+ 0 || source.extSources.length > 0 || source.text.length > 0">
+
+
0">
+ ☚
+ ❝
+
+
+
+
+
+
+
+
+
+
+
\ 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