diff --git a/CHANGELOG.md b/CHANGELOG.md index 8135ac04b..bfd439cfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - -## [1.0.0-alpha] - 2022-12-07 +## [1.0.0-alpha] - 2024-02-20 ### Dependency Updates - Updated to Angular 13 @@ -87,6 +87,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Named entities visualisation - Named entities extraction - OpenSeadragon component with support for manifest file +- Analogues and sources extraction and configuration +- Analogues and sources visualization ### Changed - Routing params keys diff --git a/src/app/app.config.ts b/src/app/app.config.ts index d17b764e4..cd009010e 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,23 @@ 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]]) }); + } + } + export interface EVTConfig { ui: UiConfig; edition: EditionConfig; @@ -100,6 +118,20 @@ export interface EditionConfig { verseNumberPrinter: number; readingColorLight: string; readingColorDark: string; + externalBibliography: Partial<{ + biblAttributeToMatch: string; + elementAttributesToMatch: string[]; + }>; + biblView: Partial<{ + propsToShow: string[]; + showAttrNames: boolean; + showEmptyValues: boolean; + inline: boolean; + commaSeparated: 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 9021ca399..d4b635ef6 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -22,110 +22,119 @@ import { AppComponent } from './app.component'; import { Ng2HandySyntaxHighlighterModule } from 'ng2-handy-syntax-highlighter'; import { AppConfig } from './app.config'; -import { AdditionComponent } from './components/addition/addition.component'; import { AdditionalComponent } from './components/additional/additional.component'; +import { AdditionComponent } from './components/addition/addition.component'; +import { AnalogueDetailComponent } from './components/analogues/analogue-detail/analogue-detail.component'; +import { AnalogueEntryComponent } from './components/analogues/analogue-entry/analogue-entry.component'; +import { AnaloguesComponent } from './components/analogues/analogues.component'; +import { AnnotatorDirective } from './directives/annotator.directive'; +import { AnnotatorService } from './services/annotator/annotator.service'; +import { ApparatusEntryComponent } from './components/apparatus-entry/apparatus-entry.component'; import { ApparatusEntryDetailComponent } from './components/apparatus-entry/apparatus-entry-detail/apparatus-entry-detail.component'; import { ApparatusEntryReadingsComponent } from './components/apparatus-entry/apparatus-entry-readings/apparatus-entry-readings.component'; -import { ApparatusEntryComponent } from './components/apparatus-entry/apparatus-entry.component'; +import { BiblioEntryComponent } from './components/biblio/biblio.component'; +import { BiblioListComponent } from './components/biblioList/biblio-list.component'; import { CharComponent } from './components/char/char.component'; import { ChoiceComponent } from './components/choice/choice.component'; +import { CollationComponent } from './view-modes/collation/collation.component'; import { ContentViewerComponent } from './components/content-viewer/content-viewer.component'; +import { CriticalApparatusComponent } from './components/critical-apparatus/critical-apparatus.component'; import { DamageComponent } from './components/damage/damage.component'; import { DeletionComponent } from './components/deletion/deletion.component'; import { EditionLevelSelectorComponent } from './components/edition-level-selector/edition-level-selector.component'; import { EditionStmtComponent } from './components/edition-stmt/edition-stmt.component'; +import { EditorialConventionLayoutDirective } from './directives/editorial-convention-layout.directive'; import { EditorialDeclComponent } from './components/editorial-decl/editorial-decl.component'; import { EncodingDescComponent } from './components/encoding-desc/encoding-desc.component'; import { EntitiesSelectComponent } from './components/entities-select/entities-select.component'; +import { EvtInfoComponent } from './evt-info/evt-info.component'; import { ExtentComponent } from './components/extent/extent.component'; import { FileDescComponent } from './components/file-desc/file-desc.component'; -import { GComponent } from './components/g/g.component'; +import { FilterPipe } from './pipes/filter.pipe'; import { GapComponent } from './components/gap/gap.component'; +import { GComponent } from './components/g/g.component'; import { GenericElementComponent } from './components/generic-element/generic-element.component'; +import { GenericParserService } from './services/xml-parsers/generic-parser.service'; import { GlobalListsComponent } from './components/global-lists/global-lists.component'; +import { HandleImgErrorDirective } from './directives/handle-img-error.directive'; +import { HighlightDirective } from './directives/highlight.directive'; import { HistoryComponent } from './components/history/history.component'; +import { HtmlAttributesDirective } from './directives/html-attributes.directive'; +import { HumanizePipe } from './pipes/humanize.pipe'; +import { IdbService } from './services/idb.service'; import { IdentifierComponent } from './components/identifier/identifier.component'; +import { ImageImageComponent } from './view-modes/image-image/image-image.component'; +import { ImageGrpPanelComponent } from './panels/imagegrp-panel/imagegrp-panel.component'; +import { ImageOnlyComponent } from './view-modes/image-only/image-only.component'; +import { ImagePanelComponent } from './panels/image-panel/image-panel.component'; +import { ImageTextComponent } from './view-modes/image-text/image-text.component'; import { LbComponent } from './components/lb/lb.component'; +import { MainHeaderComponent } from './main-header/main-header.component'; +import { MainMenuComponent } from './main-menu/main-menu.component'; import { ManuscriptThumbnailsViewerComponent } from './components/manuscript-thumbnails-viewer/manuscript-thumbnails-viewer.component'; import { MsContentsComponent } from './components/ms-contents/ms-contents.component'; -import { MsDescSelectorComponent } from './components/ms-desc-selector/ms-desc-selector.component'; import { MsDescComponent } from './components/ms-desc/ms-desc.component'; +import { MsDescSectionComponent } from './ui-components/ms-desc-section/ms-desc-section.component'; +import { MsDescSelectorComponent } from './components/ms-desc-selector/ms-desc-selector.component'; import { MsFragComponent } from './components/ms-frag/ms-frag.component'; import { MsIdentifierComponent } from './components/ms-identifier/ms-identifier.component'; import { MsItemComponent } from './components/ms-item/ms-item.component'; import { MsPartComponent } from './components/ms-part/ms-part.component'; import { NamedEntitiesListComponent } from './components/named-entities-list/named-entities-list.component'; -import { NamedEntityRefComponent } from './components/named-entity-ref/named-entity-ref.component'; -import { NamedEntityRelationComponent } from './components/named-entity-relation/named-entity-relation.component'; +import { NamedEntityComponent } from './components/named-entity/named-entity.component'; import { NamedEntityDetailComponent } from './components/named-entity/named-entity-detail/named-entity-detail.component'; import { NamedEntityOccurrenceComponent } from './components/named-entity/named-entity-occurrence/named-entity-occurrence.component'; -import { NamedEntityComponent } from './components/named-entity/named-entity.component'; +import { NamedEntityRefComponent } from './components/named-entity-ref/named-entity-ref.component'; +import { NamedEntityRelationComponent } from './components/named-entity-relation/named-entity-relation.component'; import { NamespaceComponent } from './components/namespace/namespace.component'; +import { NavBarComponent } from './nav-bar/nav-bar.component'; import { NoteComponent } from './components/note/note.component'; import { NotesStmtComponent } from './components/notes-stmt/notes-stmt.component'; import { OriginalEncodingViewerComponent } from './components/original-encoding-viewer/original-encoding-viewer.component'; import { OsdComponent } from './components/osd/osd.component'; -import { PageSelectorComponent } from './components/page-selector/page-selector.component'; import { PageComponent } from './components/page/page.component'; +import { PageSelectorComponent } from './components/page-selector/page-selector.component'; import { ParagraphComponent } from './components/paragraph/paragraph.component'; import { PhysDescComponent } from './components/phys-desc/phys-desc.component'; +import { PinboardComponent } from './pinboard/pinboard.component'; +import { PinboardPanelComponent } from './panels/pinboard-panel/pinboard-panel.component'; +import { PinnerComponent } from './pinboard/pinner/pinner.component'; import { ProjectDescComponent } from './components/project-desc/project-desc.component'; import { ProjectInfoComponent } from './components/project-info/project-info.component'; import { PublicationStmtComponent } from './components/publication-stmt/publication-stmt.component'; +import { QuoteEntryComponent } from './components/quote-entry/quote-entry.component'; import { ReadingComponent } from './components/reading/reading.component'; +import { ReadingTextComponent } from './view-modes/reading-text/reading-text.component'; import { RenditionComponent } from './components/rendition/rendition.component'; import { RespStmtComponent } from './components/resp-stmt/resp-stmt.component'; import { SamplingDeclComponent } from './components/sampling-decl/sampling-decl.component'; import { SeriesStmtComponent } from './components/series-stmt/series-stmt.component'; +import { ShortcutsComponent } from './shortcuts/shortcuts.component'; import { SicComponent } from './components/sic/sic.component'; import { SpaceComponent } from './components/space/space.component'; +import { SourceDetailComponent } from './components/sources/source-detail/source-detail.component'; +import { SourceNoteComponent } from './components/sources/source-note/source-note.component'; +import { SourcesComponent } from './components/sources/sources.component'; +import { SourcesPanelComponent } from './panels/sources-panel/sources-panel.component'; +import { StartsWithPipe } from './pipes/starts-with.pipe'; import { SuppliedComponent } from './components/supplied/supplied.component'; import { SurplusComponent } from './components/surplus/surplus.component'; import { TagsDeclComponent } from './components/tags-decl/tags-decl.component'; import { TextComponent } from './components/text/text.component'; +import { TextPanelComponent } from './panels/text-panel/text-panel.component'; +import { TextSourcesComponent } from './view-modes/text-sources/text-sources.component'; +import { TextTextComponent } from './view-modes/text-text/text-text.component'; +import { TextVersionsComponent } from './view-modes/text-versions/text-versions.component'; +import { ThemesService } from './services/themes.service'; import { TitleStmtComponent } from './components/title-stmt/title-stmt.component'; import { VerseComponent } from './components/verse/verse.component'; import { VersesGroupComponent } from './components/verses-group/verses-group.component'; import { WordComponent } from './components/word/word.component'; -import { AnnotatorDirective } from './directives/annotator.directive'; -import { EditorialConventionLayoutDirective } from './directives/editorial-convention-layout.directive'; -import { HighlightDirective } from './directives/highlight.directive'; -import { HtmlAttributesDirective } from './directives/html-attributes.directive'; -import { EvtInfoComponent } from './evt-info/evt-info.component'; -import { MainHeaderComponent } from './main-header/main-header.component'; -import { MainMenuComponent } from './main-menu/main-menu.component'; -import { NavBarComponent } from './nav-bar/nav-bar.component'; import { NavBarImageComponent } from './nav-bar-image/nav-bar-image.component'; -import { ImagePanelComponent } from './panels/image-panel/image-panel.component'; -import { PinboardPanelComponent } from './panels/pinboard-panel/pinboard-panel.component'; -import { SourcesPanelComponent } from './panels/sources-panel/sources-panel.component'; -import { TextPanelComponent } from './panels/text-panel/text-panel.component'; import { VersionPanelComponent } from './panels/version-panel/version-panel.component'; import { WitnessPanelComponent } from './panels/witness-panel/witness-panel.component'; -import { PinboardComponent } from './pinboard/pinboard.component'; -import { PinnerComponent } from './pinboard/pinner/pinner.component'; -import { FilterPipe } from './pipes/filter.pipe'; -import { HumanizePipe } from './pipes/humanize.pipe'; -import { StartsWithPipe } from './pipes/starts-with.pipe'; import { XmlBeautifyPipe } from './pipes/xml-beautify.pipe'; -import { AnnotatorService } from './services/annotator/annotator.service'; -import { IdbService } from './services/idb.service'; -import { ThemesService } from './services/themes.service'; -import { GenericParserService } from './services/xml-parsers/generic-parser.service'; import { XMLParsers } from './services/xml-parsers/xml-parsers'; -import { ShortcutsComponent } from './shortcuts/shortcuts.component'; -import { MsDescSectionComponent } from './ui-components/ms-desc-section/ms-desc-section.component'; -import { CollationComponent } from './view-modes/collation/collation.component'; -import { ImageTextComponent } from './view-modes/image-text/image-text.component'; -import { ReadingTextComponent } from './view-modes/reading-text/reading-text.component'; -import { TextSourcesComponent } from './view-modes/text-sources/text-sources.component'; -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 { ImageOnlyComponent } from './view-modes/image-only/image-only.component'; -import { ImageImageComponent } from './view-modes/image-image/image-image.component'; -import { ImageGrpPanelComponent } from "./panels/imagegrp-panel/imagegrp-panel.component"; - const routes: Routes = [ ]; @@ -137,6 +146,7 @@ export function initializeApp(appConfig: AppConfig) { const DynamicComponents = [ AdditionalComponent, AdditionComponent, + AnalogueEntryComponent, ApparatusEntryComponent, ApparatusEntryDetailComponent, ApparatusEntryReadingsComponent, @@ -173,6 +183,7 @@ const DynamicComponents = [ PhysDescComponent, ProjectDescComponent, PublicationStmtComponent, + QuoteEntryComponent, ReadingComponent, RenditionComponent, RespStmtComponent, @@ -192,10 +203,15 @@ const DynamicComponents = [ @NgModule({ declarations: [ + AnalogueDetailComponent, + AnaloguesComponent, AnnotatorDirective, AppComponent, + BiblioEntryComponent, + BiblioListComponent, CollationComponent, ContentViewerComponent, + CriticalApparatusComponent, EditionLevelSelectorComponent, EditorialConventionLayoutDirective, EntitiesSelectComponent, @@ -229,6 +245,9 @@ const DynamicComponents = [ ProjectInfoComponent, ReadingTextComponent, ShortcutsComponent, + SourceDetailComponent, + SourceNoteComponent, + SourcesComponent, SourcesPanelComponent, StartsWithPipe, TextPanelComponent, 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..5bcd12330 --- /dev/null +++ b/src/app/components/analogues/analogue-detail/analogue-detail.component.html @@ -0,0 +1,88 @@ +
+
+ + + + + + + + +
+ +
+ 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..df9bbd27f --- /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..c9cc31f84 --- /dev/null +++ b/src/app/components/analogues/analogue-detail/analogue-detail.component.ts @@ -0,0 +1,38 @@ +import { Analogue } from 'src/app/models/evt-models'; +import { BehaviorSubject } from 'rxjs'; +import { Component, Input } from '@angular/core'; +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; + public analogueEntry: Analogue; + public headVisible: boolean; + public detailVisible: boolean; + + @Input() set analogue(el: Analogue) { + this.analogueEntry = el; + this.checkVisibility(el); + } + get analogue() { return this.analogueEntry; } + + @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(); + } + + checkVisibility(anl: Analogue) { + this.headVisible = ((anl.sources.length > 0) || (anl.extSources.length > 0) || (anl.text.length > 0) || (anl.extLinkedElements.length > 0)); + this.detailVisible = (anl.sources.length > 0 || anl.extSources.length > 0 || anl.quotedElements.length > 0); + } +} 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..873b75838 --- /dev/null +++ b/src/app/components/analogues/analogue-entry/analogue-entry.component.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + \ 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..4c2c2bfd6 --- /dev/null +++ b/src/app/components/analogues/analogue-entry/analogue-entry.component.ts @@ -0,0 +1,92 @@ +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; + } + + /** 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 || [], + } + } + + stopPropagation(e: MouseEvent) { + e.stopPropagation(); + } + + constructor( + public evtStatusService: EVTStatusService, + ) {} + + ngOnInit() { + if (this.data.text.length === 0) { + this.dataForNote = this.createNote(this.data); + } + } + +} diff --git a/src/app/components/analogues/analogues.component.html b/src/app/components/analogues/analogues.component.html new file mode 100644 index 000000000..54797f3c5 --- /dev/null +++ b/src/app/components/analogues/analogues.component.html @@ -0,0 +1,7 @@ + + + + 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..bc3220722 --- /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..00ad4439e --- /dev/null +++ b/src/app/components/analogues/analogues.component.ts @@ -0,0 +1,45 @@ +import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; +import { BehaviorSubject, distinctUntilChanged } 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'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +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.pipe(distinctUntilChanged()).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..b23e4c8ac 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: rgba(32, 66, 81, 0.18); } .app-detail-readings { diff --git a/src/app/components/apparatus-entry/apparatus-entry-detail/apparatus-entry-detail.component.ts b/src/app/components/apparatus-entry/apparatus-entry-detail/apparatus-entry-detail.component.ts index 0da14a0f6..f5f5cedbf 100644 --- a/src/app/components/apparatus-entry/apparatus-entry-detail/apparatus-entry-detail.component.ts +++ b/src/app/components/apparatus-entry/apparatus-entry-detail/apparatus-entry-detail.component.ts @@ -16,7 +16,7 @@ export class ApparatusEntryDetailComponent implements OnInit { rdgHasCounter = false; get significantRdg(): Reading[] { - return this.data.readings.filter((rdg) => rdg.significant); + return this.data.readings.filter((rdg) => rdg?.significant); } get notSignificantRdg(): Reading[] { 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..c1dccf5e2 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 @@ -4,22 +4,19 @@ so we can't use apparatus-entry-component to render them. --> - - - {{ witID }} + {{ witID }} ] - 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..ecc0cb3ca 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; +} + +.app-wit { + font-size: 90%; + font-weight: 600; + font-family: serif; } \ No newline at end of file diff --git a/src/app/components/apparatus-entry/apparatus-entry-readings/apparatus-entry-readings.component.ts b/src/app/components/apparatus-entry/apparatus-entry-readings/apparatus-entry-readings.component.ts index d06d32653..991dacc44 100644 --- a/src/app/components/apparatus-entry/apparatus-entry-readings/apparatus-entry-readings.component.ts +++ b/src/app/components/apparatus-entry/apparatus-entry-readings/apparatus-entry-readings.component.ts @@ -27,7 +27,7 @@ export class ApparatusEntryReadingsComponent { } get significantRdg(): Reading[] { - return this.data.readings.filter((rdg) => rdg.significant); + return this.data.readings.filter((rdg) => rdg?.significant); } getWits$(witID: string): Observable { diff --git a/src/app/components/biblio/biblio.component.html b/src/app/components/biblio/biblio.component.html new file mode 100644 index 000000000..3a28c67ee --- /dev/null +++ b/src/app/components/biblio/biblio.component.html @@ -0,0 +1,12 @@ + + + + {{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..85bcdfb3b --- /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..687970a8c --- /dev/null +++ b/src/app/components/biblio/biblio.component.ts @@ -0,0 +1,21 @@ +import { Component, Input } 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 { + @Input() data: BibliographicEntry; + + public showList = AppConfig.evtSettings.edition.biblView.propsToShow; + public showAttrNames = AppConfig.evtSettings.edition.biblView.showAttrNames; + public showEmptyValues = AppConfig.evtSettings.edition.biblView.showEmptyValues; + public inline = AppConfig.evtSettings.edition.biblView.inline; + public isCommaSeparated = AppConfig.evtSettings.edition.biblView.commaSeparated; + public showMainElemTextContent = AppConfig.evtSettings.edition.biblView.showMainElemTextContent; + +} + 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..afce81ff8 --- /dev/null +++ b/src/app/components/biblioList/biblio-list.component.html @@ -0,0 +1,43 @@ + + + + + + + +
    + + + + +
  • +
    + + + +
+
+ + + + {{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..ac2ef1958 --- /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..ae0c4c134 --- /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..dbb878f0a --- /dev/null +++ b/src/app/components/biblioList/biblio-list.component.ts @@ -0,0 +1,10 @@ +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..73bf2bb44 --- /dev/null +++ b/src/app/components/critical-apparatus/critical-apparatus.component.html @@ -0,0 +1,4 @@ + + + + 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..58f42b1ca --- /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..9b7b9cc4d --- /dev/null +++ b/src/app/components/critical-apparatus/critical-apparatus.component.ts @@ -0,0 +1,34 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Component, Input, OnInit } from '@angular/core'; +import { EVTStatusService } from '../../services/evt-status.service'; +import { ApparatusEntry } from 'src/app/models/evt-models'; + +@Component({ + selector: 'evt-critical-apparatus', + templateUrl: './critical-apparatus.component.html', + styleUrls: ['./critical-apparatus.component.scss'], +}) +export class CriticalApparatusComponent implements OnInit { + public entries: ApparatusEntry; + private appClasses = ['app']; + public apparatusInCurrentPage = this.evtStatusService.getPageElementsByClassList(this.appClasses) + + @Input() pageID : string; + + public getEntries(data) { + this.entries = data.flat(); + } + + stopPropagation(e: MouseEvent) { + e.stopPropagation(); + } + + constructor( + public evtStatusService: EVTStatusService, + ) {} + + ngOnInit() { + this.apparatusInCurrentPage.subscribe({ next: (data) => { this.getEntries(data) } }); + } + +} 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..237d1b13a 100644 --- a/src/app/components/note/note.component.scss +++ b/src/app/components/note/note.component.scss @@ -74,4 +74,24 @@ } } } + + &[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..88031b06b --- /dev/null +++ b/src/app/components/quote-entry/quote-entry.component.html @@ -0,0 +1,21 @@ + + + + + + + + + + + + \ 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..b4b862b34 --- /dev/null +++ b/src/app/components/quote-entry/quote-entry.component.ts @@ -0,0 +1,93 @@ +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; + public sourceClass = SourceClass; + public dataForNote = {}; + public opened = false; + + @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; } + + 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; + } + + /** 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 || [], + } + } + + stopPropagation(e: MouseEvent) { + e.stopPropagation(); + } + + constructor( + public evtStatusService: EVTStatusService, + ) {} + + ngOnInit() { + if ((this.data.isNoteView) || ((this.data.text.length === 0) && ((this.data.extElements.length !== 0) || (this.data.extSources.length !== 0)))) { + this.dataForNote = this.createNote(this.data); + } + } + +} 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..6d56fe61f --- /dev/null +++ b/src/app/components/sources/source-detail/source-detail.component.html @@ -0,0 +1,111 @@ + +

+
+ + + + + + + + +
+ +
\ 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..19c8d10fe --- /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..c718b1bef --- /dev/null +++ b/src/app/components/sources/source-detail/source-detail.component.ts @@ -0,0 +1,44 @@ +import { Component, Input } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import { EditionLevelType } from 'src/app/app.config'; +import { QuoteEntry } from 'src/app/models/evt-models'; + +@Component({ + selector: 'evt-source-detail', + templateUrl: './source-detail.component.html', + styleUrls: ['../../sources/sources.component.scss'], +}) +export class SourceDetailComponent { + + private edLevel: EditionLevelType; + + public sourceEntry: QuoteEntry; + + public headVisible: boolean; + + public detailVisible: boolean; + + @Input() set source(el: QuoteEntry) { + this.sourceEntry = el; + this.checkVisibility(el); + } + get source() { return this.sourceEntry; } + + @Input() set editionLevel(el: EditionLevelType) { + this.edLevel = el; + this.editionLevelChange.next(el); + } + get editionLevel() { return this.edLevel; } + editionLevelChange = new BehaviorSubject(''); + + checkVisibility(source: QuoteEntry) { + this.headVisible = (source.sources.length > 0 || source.extSources.length > 0 || source.text.length > 0); + this.detailVisible = (source.sources.length > 0 || source.extSources.length > 0 || source.extElements.length > 0); + } + + 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..d53dfda40 --- /dev/null +++ b/src/app/components/sources/source-note/source-note.component.ts @@ -0,0 +1,49 @@ +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; + public dataForNote = {}; + + @Input() set data(dt: Paragraph|Verse|VersesGroup) { + this._data = dt; + } + + get data() { return this._data } + + createNote(v, type): Note { + const item = v[type]; + let content = item.extSources || []; + if (type === 'analogue') { + content.push( item.extLinkedElements ); + content.push( item.sources ); + } + 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..0a5d03555 --- /dev/null +++ b/src/app/components/sources/sources.component.html @@ -0,0 +1,7 @@ + + + + + diff --git a/src/app/components/sources/sources.component.scss b/src/app/components/sources/sources.component.scss new file mode 100644 index 000000000..4e542789b --- /dev/null +++ b/src/app/components/sources/sources.component.scss @@ -0,0 +1,144 @@ +@import "../../../assets/scss/themes"; + +.source-container { + border: 1px solid rgba(128, 128, 128, 0.149); + padding: 0; + margin: 0.5rem 0.5rem 1rem 0.5rem; + font-family: serif; + box-shadow: 1px 1px 1px rgba(0,0,0,0.1); + background-color:rgba(32, 66, 81, 0.18); +} + +.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.3rem; + 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: 1rem; + color: #263238; + margin: 0; + padding: 0 0.3rem 0 0; + display: inline; + font-family: 'evt-icons' !important; + 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: rgba(128, 128, 128, 0.18); +} + +.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.5rem; + padding-left: 1rem; + padding-bottom: 0.5rem; + .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..baf51b0d4 --- /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..10a9058b2 --- /dev/null +++ b/src/app/components/sources/sources.component.ts @@ -0,0 +1,45 @@ +import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; +import { EVTStatusService } from 'src/app/services/evt-status.service'; +import { SourceClass } from '../../models/evt-models'; +import { BehaviorSubject, distinctUntilChanged } from 'rxjs'; +import { EditionLevelType } from 'src/app/app.config'; + +@Component({ + selector: 'evt-sources', + templateUrl: './sources.component.html', + styleUrls: ['./sources.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +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(''); + + public getEntries(data) { + this.entries = data.flat(); + } + + stopPropagation(e: MouseEvent) { + e.stopPropagation(); + } + + constructor( + public evtStatusService: EVTStatusService, + ) {} + + ngOnInit() { + this.quotesInCurrentPage.pipe(distinctUntilChanged()).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 97693dbe1..fece0b98b 100644 --- a/src/app/components/verse/verse.component.html +++ b/src/app/components/verse/verse.component.html @@ -1,8 +1,8 @@ - {{data.n}} - - + \ No newline at end of file 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 4b74e33c2..32cbb46cb 100644 --- a/src/app/models/evt-models.ts +++ b/src/app/models/evt-models.ts @@ -170,6 +170,70 @@ 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[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + extElements: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + analogues: any; + originalEncoding: OriginalEncodingNodeType; + isInsideCit: boolean; + quotedText: string[]; + isNoteView: boolean; + contentToShow: Array> + //rend: string; +} + +export class BibliographicList extends GenericElement { + id: string; + head: string[]; + sources: BibliographicEntry[]; +} + +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; + isInsideCit: boolean; + originalEncoding: OriginalEncodingNodeType; +} + +export class BibliographicStructEntry extends GenericElement { + id: string; + analytic: BibliographicEntry[]; + monogrs: BibliographicEntry[]; + series: BibliographicEntry[]; + originalEncoding: OriginalEncodingNodeType; +} + +export class Analogue extends GenericElement { + id: string; + text: string; + sources: BibliographicEntry[]; + extSources: BibliographicEntry[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + extLinkedElements: any; + quotedElements: [{ id: string, quote: string }]; + contentToShow: Array> + originalEncoding: OriginalEncodingNodeType; +} + export class Reading extends GenericElement { id: string; witIDs: string[]; @@ -194,10 +258,14 @@ export class Note extends GenericElement { noteLayout: NoteLayout; noteType: string; exponent: string; + source: QuoteEntry; + analogue: Analogue; } export class Paragraph extends GenericElement { n: string; + source: QuoteEntry; + analogue: Analogue; } export class Lb extends GenericElement { @@ -297,11 +365,15 @@ export class Choice extends GenericElement { export class Verse extends GenericElement { n: string; + source: QuoteEntry; + analogue: Analogue; } export class VersesGroup extends GenericElement { n: string; groupType: string; + source: QuoteEntry; + analogue: Analogue; } export class Supplied extends GenericElement { @@ -468,13 +540,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 @@ -1060,6 +1132,32 @@ export class Term extends GenericElement { rend?: string; } +export class Milestone extends GenericElement { + id?: string; + unit?: string; + spanText: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + spanElements: any; +} + +export class Anchor extends GenericElement { + id?: string; +} + +export class Span extends GenericElement { + id?: string; + from: string; + to: string; + includedText: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + 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 c47b7ae05..4fce2902a 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,14 +125,55 @@ 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) { diff --git a/src/app/services/evt-model.service.ts b/src/app/services/evt-model.service.ts index 58e2e081c..3cb7ea0b3 100644 --- a/src/app/services/evt-model.service.ts +++ b/src/app/services/evt-model.service.ts @@ -21,6 +21,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'; import { AppConfig } from '../app.config'; @Injectable({ @@ -152,6 +154,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 facsimile$ : Observable = this.editionSource$.pipe( map((source) => this.facsimileParser.parseFacsimile(source)), @@ -333,10 +347,10 @@ return undefined; public readonly msDesc$ = this.editionSource$.pipe( map((source) => this.msDescParser.parseMsDesc(source)), shareReplay(1), -); - + ); constructor( + private analogueParser: AnalogueEntriesParserService, private editionDataService: EditionDataService, private editionStructureParser: StructureXmlParserService, private namedEntitiesParser: NamedEntitiesParserService, @@ -347,6 +361,7 @@ return undefined; private characterDeclarationsParser: CharacterDeclarationsParserService, private linesVersesParser: LinesVersesParserService, private msDescParser: MsDescParserService, + private sourceParser: SourceEntriesParserService, ) { } diff --git a/src/app/services/evt-status.service.ts b/src/app/services/evt-status.service.ts index 9b96db511..65c019f59 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); + public syncImageNavBar$: BehaviorSubject = new BehaviorSubject(false); constructor( @@ -181,6 +184,17 @@ export class EVTStatusService { params, }; } + + getPageElementsByClassList(classList) { + const attributesNotIncludedInSearch = ['originalEncoding','type']; + const maxEffort = 4000; + + return this.currentStatus$.pipe( + map(({ page }) => page.parsedContent), + map((pageSubElements) => deepSearch(pageSubElements, 'class', classList, maxEffort, attributesNotIncludedInSearch)), + ); + } + } 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..e99ba434c --- /dev/null +++ b/src/app/services/xml-parsers/analogue-parser.ts @@ -0,0 +1,133 @@ +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 { AnchorParser, AttributeParser, GenericElemParser, MilestoneParser } from './basic-parsers'; +import { createParser, getID, parseChildren, Parser } from './parser-models'; +import { chainFirstChildTexts, getExternalElements, normalizeSpaces } from '../../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; + notDisplayedInTextFlow = ['Note', 'BibliographicList', 'BibliographicEntry', 'BibliographicStructEntry', + 'Analogue', 'MsDesc']; + evtTextComplexElements = ['choice', 'app', 'l', 'quote', 'p', 'lg']; + evtInnerTextElements = ['#text', 'reg', 'corr', 'rdg']; + + 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 notableElements = ['div','p','l','lg','note']; + const sources = this.buildAnalogueSources(analogue); + const content = parseChildren(analogue, this.genericParse); + + return { + type: Analogue, + id: (notableElements.includes(analogue.tagName)) ? 'EVT-ANALOGUE:' + getID(analogue) : getID(analogue), + class: AnalogueClass, + attributes: this.attributeParser.parse(analogue), + text: normalizeSpaces(chainFirstChildTexts(analogue, this.evtTextComplexElements, this.evtInnerTextElements)), + content: content, + contentToShow: content.filter((x) => !(this.notDisplayedInTextFlow.includes(x['type'].name))), + sources: sources.sources, + extSources: sources.extSources, + extLinkedElements: sources.extLinkedElements, + quotedElements: this.getQuotedTextFromElements(sources.sources.concat(sources.extSources), sources.extLinkedElements), + originalEncoding: 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 + */ + private buildAnalogueSources(analogue: XMLElement) { + 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) { + 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()); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + 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 notDisplayedInText = ['Note','BibliographicList','BibliographicEntry','BibliographicStructEntry','Analogue','MsDesc']; + elements.forEach((el: XMLElement) => { if (!notDisplayedInText.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..12edecfd9 --- /dev/null +++ b/src/app/services/xml-parsers/analogues-entries-parser.service.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@angular/core'; +import { ParserRegister } from '.'; +import { Analogue, AnalogueClass, XMLElement } from '../../models/evt-models'; + +@Injectable({ + providedIn: 'root', +}) +export class AnalogueEntriesParserService { + private className = `.${AnalogueClass}`; + private parserName = 'evt-analogue-entry-parser'; + + public parseAnaloguesEntries(document: XMLElement) { + const analogueParser = ParserRegister.get(this.parserName); + + return Array.from(document.querySelectorAll(this.className)) + .map((analogue) => analogueParser.parse(analogue) as Analogue); + } + +} diff --git a/src/app/services/xml-parsers/apparatus-entries-parser.service.ts b/src/app/services/xml-parsers/apparatus-entries-parser.service.ts index 7a5950001..d02737a44 100644 --- a/src/app/services/xml-parsers/apparatus-entries-parser.service.ts +++ b/src/app/services/xml-parsers/apparatus-entries-parser.service.ts @@ -18,7 +18,7 @@ export class ApparatusEntriesParserService { public getSignificantReadings(apps: ApparatusEntry[]) { const signRdgs = {}; apps.forEach((app) => { - signRdgs[app.id] = app.readings.concat(app.lemma).filter((rdg: Reading) => rdg.significant); + signRdgs[app.id] = app.readings.concat(app.lemma).filter((rdg: Reading) => rdg?.significant); }); return signRdgs; diff --git a/src/app/services/xml-parsers/basic-parsers.ts b/src/app/services/xml-parsers/basic-parsers.ts index 121a2470a..7f146a0cb 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, Space, Supplied, Term, Text, Verse, VersesGroup, Word, XMLElement, + Addition, Analogue, Anchor, Attributes, Damage, Deletion, Gap, GenericElement, Lb, Milestone, Note, NoteLayout, + Paragraph, PlacementType, Ptr, Space, 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 { getElementsBetweenTreeNode, isNestedInElem, xpath } from '../../utils/dom-utils'; +import { getExternalElements, 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; @@ -53,6 +56,15 @@ export class GenericParser extends GenericElemParser { protected genericElemParser = createParser(GenericElemParser, this.genericParse); } +export class DisambiguationParser extends GenericElemParser { + protected elementParser = createParser(GenericElemParser, this.genericParse); + protected attributeParser = createParser(AttributeParser, this.genericParse); + protected analogueParser = createParser(AnalogueParser, this.genericParse); + protected quoteParser = createParser(QuoteParser, this.genericParse); + protected sourceAttr = AppConfig.evtSettings.edition.externalBibliography.elementAttributesToMatch; + protected analogueMarkers = AppConfig.evtSettings.edition.analogueMarkers; +} + @xmlParser('evt-attribute-parser', AttributeParser) export class AttributeParser extends EmptyParser implements Parser { parse(data: HTMLElement): Attributes { @@ -87,13 +99,30 @@ 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; @@ -137,6 +166,13 @@ export class SpaceParser 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)) @@ -147,6 +183,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, @@ -155,6 +199,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, }; @@ -169,7 +215,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'); @@ -178,6 +235,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, @@ -188,18 +249,44 @@ 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; @@ -209,7 +296,21 @@ 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, @@ -218,6 +319,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; @@ -344,3 +447,137 @@ export class TermParser extends GenericElemParser implements Parser }; } } + +@xmlParser('milestone', MilestoneParser) +export class MilestoneParser extends GenericElemParser implements Parser { + parse(xml: XMLElement): Milestone { + + const endElement = (xml.getAttribute('spanTo')) ? getExternalElements(xml, ['spanTo'], 'xml:id', 'anchor') : []; + const includedElements = (endElement.length !== 0) ? getElementsBetweenTreeNode(xml, endElement[0]) : []; + const parsedElements = (includedElements.length !== 0) ? + includedElements.map((x: XMLElement) => (x.nodeType !== 3 && x.nodeType !== 8) ? super.parse(x) : x) : []; + + return { + type: Milestone, + id: xml.getAttribute('xml:id'), + attributes: this.attributeParser.parse(xml), + unit: xml.getAttribute('unit'), + spanText: '', + 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), + } + + } + if (xml.tagName === 'span') { + const endElement = (xml.getAttribute('spanTo')) ? getExternalElements(xml, ['from'], 'xml:id', 'anchor') : []; + const includedElements = (endElement.length !== 0) ? getElementsBetweenTreeNode(xml, endElement[0]) : []; + const parsedElements = (includedElements.length !== 0) ? + includedElements.map((x: XMLElement) => (x.nodeType !== 3 && x.nodeType !== 8) ? super.parse(x) : x) : []; + + return { + type: Span, + id: xml.getAttribute('xml:id'), + attributes: this.attributeParser.parse(xml), + from: xml.getAttribute('from'), + to: xml.getAttribute('to'), + includedText: '', + includedElements: parsedElements, + content: parseChildren(xml, this.genericParse), + }; + } + } +} + +@xmlParser('ref', RefParser) +export class RefParser extends DisambiguationParser implements Parser { + parse(xml: XMLElement): Analogue | QuoteEntry | GenericElement { + if (isAnalogue(xml, this.analogueMarkers)) { + return this.analogueParser.parse(xml); + } + if (isSource(xml, this.sourceAttr)) { + return this.quoteParser.parse(xml); + } + + return this.elementParser.parse(xml) + } +} + +@xmlParser('seg', SegParser) +export class SegParser extends DisambiguationParser implements Parser { + parse(xml: XMLElement): Analogue | QuoteEntry | GenericElement { + if (isAnalogue(xml, this.analogueMarkers)) { + return this.analogueParser.parse(xml); + } + if (isSource(xml, this.sourceAttr)) { + return this.quoteParser.parse(xml); + } + + return this.elementParser.parse(xml) + } +} + +@xmlParser('div', DivParser) +export class DivParser extends DisambiguationParser implements Parser { + parse(xml: XMLElement): Analogue | QuoteEntry | GenericElement { + if (isAnalogue(xml, this.analogueMarkers)) { + return this.analogueParser.parse(xml); + } + if (isSource(xml, this.sourceAttr)) { + return this.quoteParser.parse(xml); + } + + return this.elementParser.parse(xml) + } +} + +@xmlParser('cit', CitParser) +export class CitParser extends DisambiguationParser implements Parser { + content = []; + parse(xml: XMLElement): Analogue | QuoteEntry | GenericElement { + if (isAnalogue(xml, this.analogueMarkers)) { + return this.analogueParser.parse(xml); + } + if (isSource(xml, this.sourceAttr)) { + return this.quoteParser.parse(xml); + } + + const quote = xml.querySelector('quote'); + if (quote) { + return this.quoteParser.parse(quote); + } + } +} 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..71ea2801c --- /dev/null +++ b/src/app/services/xml-parsers/bibliographic-entries-parser.service.ts @@ -0,0 +1,19 @@ +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..8dd144837 --- /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 { 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 quotes = Array.from(target.querySelectorAll('quote')); + if (quotes.length !== 0) { + return normalizeSpaces(quotes[0].textContent); + } + + return null; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + 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), + isInsideCit: (xml.parentNode['tagName'] === 'cit' || xml.parentNode['tagName'] === 'note'), + originalEncoding: 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: xml, + }; + default: + // it should never reach here but we don't want risking to not parse an element anyway... + 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..4bd2d22ea 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']; + 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..5e4f562d8 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, - CustEvent, CustodialHist, DecoDesc, DecoNote, Depth, Dim, Dimensions, Explicit, Filiation, FinalRubric, Foliation, + 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..28c6dfe25 --- /dev/null +++ b/src/app/services/xml-parsers/quotes-parser.ts @@ -0,0 +1,313 @@ +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 { 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; } +} + +/** + * It manages note, cit, ref, seg, div, p, l, lb, ptr elements + * sent on redirect from other parsers if detected as 'sources' +*/ +@xmlParser('quote', 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 + notDisplayedInTextFlow = ['Note', 'BibliographicList', 'BibliographicEntry', + 'BibliographicStructEntry', 'Analogue', 'MsDesc']; + evtTextComplexElements = ['choice', 'app', 'l', 'quote', 'p', 'lg']; + evtInnerTextElements = ['#text', 'reg', 'corr', 'rdg']; + + 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 it will be parsed here later + if (notSource) { + return ParserRegister.get(quote.tagName).parse(quote) as Ptr; + } + break; + case 'ref': + case 'seg': + case 'cit': + // if they are not a source create a generic element, otherwise it will be parsed here later + if (notSource) { + return ParserRegister.get('evt-generic-elem-parser').parse(quote) as GenericElement; + } + break; + case 'quote': + // 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(), + isInsideCit: isInCit, + isNoteView: ((quote.tagName === 'note') || (quote.tagName === 'ptr')) ? true : false, + content: content, + contentToShow: content.filter((el) => !(this.notDisplayedInTextFlow.includes(el['type'].name))), + originalEncoding: this.getXML(quote, isInCit), + }; + } + + /** + * Returns first-level text elements inside this quote entry + */ + 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, this.evtTextComplexElements, this.evtInnerTextElements)); + + return outText; + } + + return chainFirstChildTexts(quote, this.evtTextComplexElements, this.evtInnerTextElements); + } + + /** + * Choose proper XML node + */ + private getXML(quote: XMLElement, isInCitElem: boolean): XMLElement { + if (isInCitElem) { + return quote.parentElement; + } + + return quote; + } + + + /** + * Retrieve attributes linked to external bibl, listBibl, cit elements + */ + private findExtRef(quote: XMLElement, isInCitElem: boolean): XMLElement { + const target = (isInCitElem) ? 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 XML Elements + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + 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 { + 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 + */ + private getExternalElemsOnce(quote: XMLElement, attrSrcNames: string[], attrTrgtName: string) { + 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): QuoteEntry { + // 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-SOURCE:' + 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, + isInsideCit: false, + isNoteView: true, + content: content, + contentToShow: content.filter((el) => !(this.notDisplayedInTextFlow.includes(el['type'].name))), + originalEncoding: this.getXML(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..c75bebf07 --- /dev/null +++ b/src/app/services/xml-parsers/source-entries-parser.service.ts @@ -0,0 +1,17 @@ +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 8e1d9d1d4..b5c650eef 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..4580300d7 100644 --- a/src/app/utils/dom-utils.ts +++ b/src/app/utils/dom-utils.ts @@ -181,11 +181,6 @@ export function balanceXHTML(XHTMLstring: string): string { /** * Get all DOM elements contained between the node elements - * - * @param start starting node - * @param end ending node - * - * @returns list of nodes contained between start node and end node */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function getElementsBetweenTreeNode(start: any, end: any): XMLElement[] { @@ -229,3 +224,38 @@ 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); + }); +} + +/** + * This function searches inside every property of an object for the provided attribute + * it has one of the provided list of values. It stops after a customizable number of iterations to avoid waste of resources. + * The limit counter could be inserted in a config, same as the ignoredProperties + */ +export function deepSearch(obj, attrToMatch: string, valuesToMatch, counter: number = 4000, ignoredProperties = []) { + let results = []; + for (const key in obj) { + if (!ignoredProperties.includes(key)) { + const value = obj[key]; + if ((key === attrToMatch) && (valuesToMatch.includes(obj[attrToMatch]))) { + results.push(obj); + } + if (typeof value === 'object' && value !== null) { + if (counter > 0) { + results = results.concat(deepSearch(value, attrToMatch, valuesToMatch, counter, ignoredProperties)); + counter = counter - 1; + } else { + console.log('EVT WARN: element is too deep, not searching further in', obj, value); + counter = 4000; + } + } + } + } + + return results; +} diff --git a/src/app/utils/xml-utils.ts b/src/app/utils/xml-utils.ts index 96d7722d9..260902d3e 100644 --- a/src/app/utils/xml-utils.ts +++ b/src/app/utils/xml-utils.ts @@ -36,3 +36,83 @@ 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 + */ +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 elements chained +*/ +export function chainFirstChildTexts(elem: XMLElement, evtTextComplexElements: string[], evtInnerTextElements: string[]): string { + if (elem === undefined) { + + return ''; + }; + + const evtTextElements = { + '#text': 'nodeValue', + 'p': 'textContent', + }; + let result = ''; + elem.childNodes.forEach((node) => (evtTextElements[node.nodeName] !== undefined) ? result += node[ evtTextElements[node.nodeName] ] : ( + evtTextComplexElements.includes(node.nodeName) ? result += chainDeepTexts(node, evtInnerTextElements) : '' )) + + return result; +} + +/** + * Retrieve and chain textContent of all descendents + * @param elem ChildNode + * @returns out string + */ +export function chainDeepTexts(elem: ChildNode, evtInnerTextElements: string[]): string { + const textProperty = 'textContent'; + + let result = ''; + elem.childNodes.forEach((x) => (evtInnerTextElements.includes(x.nodeName)) ? ((x[textProperty] !== null) ? result += x[textProperty] : '') : '') + + return result; +} + +/** +* Retrieve elements in all the document 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 +*/ +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))) +} + +/** + * If an element's attribute 'type' has one of the provided values then it is considered an analogue + */ +export function isAnalogue(elem: XMLElement, markerAttrs: string[]): boolean { + return (markerAttrs.includes(elem.getAttribute('type'))); +} + +/** + * If an element has one of the provided attributes then it is considered a source + */ +export function isSource(elem: XMLElement, attrs: string[]): boolean { + let isSelectedAttributes = false; + attrs.forEach((attr) => { + if (elem.getAttribute(attr) !== null) + { isSelectedAttributes = true } + }, + ); + + return (isSelectedAttributes); +} 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..19201a9de 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.3rem !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..ad8514a0b 100644 --- a/src/assets/config/edition_config.json +++ b/src/assets/config/edition_config.json @@ -1,109 +1,142 @@ { - "editionTitle": "My Digital Edition", - "badge": "alpha", - "editionHome": "evt.labcd.unipi.it/", - "availableEditionLevels": [ - { - "id": "diplomatic", - "label": "diplomatic", - "enable": true - }, - { - "id": "interpretative", - "label": "interpretative", - "enable": true - }, - { - "id": "critical", - "label": "critical", - "enable": true - } - ], - "namedEntitiesLists": { - "persons": { - "defaultLabel": "Persons", - "enable": true - }, - "places": { - "defaultLabel": "Places", - "enable": true - }, - "organizations": { - "defaultLabel": "Organizations", - "enable": true - }, - "relations": { - "defaultLabel": "Relations", - "enable": true - }, - "events": { - "defaultLabel": "Events", - "enable": true - } - }, - "showLists": true, - "entitiesSelectItems": [ - { - "label": "Named Entities", - "items": [ - { - "value": "persName", - "label": "persons", - "color": "#ffcdd2", - "enable": true - }, - { - "value": "persName[type='episcopus']", - "label": "bishops", - "color": "rgb(139, 98, 236)", - "enable": true - }, - { - "value": "placeName,geogName", - "label": "places", - "color": "#A5D6A7", - "enable": true - }, - { - "value": "orgName", - "label": "organizations", - "color": "#FFB74D", - "enable": true - } - ], - "enable": true - }, - { - "label": "Other", - "items": [ - { - "value": "event", - "label": "events", - "color": "#fcfc60", - "enable": true - }, - { - "value": "rolename", - "label": "roles", - "enable": true - }, - { - "value": "measure", - "label": "measures", - "enable": true - } - ], - "enable": true - } - ], - "notSignificantVariants": [ - "type=orthographic" - ], - "defaultEdition": "diplomatic", - "defaultViewMode": "collation", - "proseVersesToggler": true, - "defaultTextFlow": "prose", - "verseNumberPrinter": 5, - "readingColorLight": "rgb(208, 220, 255)", - "readingColorDark": "rgb(101, 138, 255)" + "editionTitle": "My Digital Edition", + "badge": "alpha", + "editionHome": "evt.labcd.unipi.it/", + "availableEditionLevels": [ + { + "id": "diplomatic", + "label": "diplomatic", + "enable": true + }, + { + "id": "interpretative", + "label": "interpretative", + "enable": true + }, + { + "id": "critical", + "label": "critical", + "enable": true + } + ], + "namedEntitiesLists": { + "persons": { + "defaultLabel": "Persons", + "enable": true + }, + "places": { + "defaultLabel": "Places", + "enable": true + }, + "organizations": { + "defaultLabel": "Organizations", + "enable": true + }, + "relations": { + "defaultLabel": "Relations", + "enable": true + }, + "events": { + "defaultLabel": "Events", + "enable": true + } + }, + "showLists": true, + "entitiesSelectItems": [ + { + "label": "Named Entities", + "items": [ + { + "value": "persName", + "label": "persons", + "color": "#ffcdd2", + "enable": true + }, + { + "value": "persName[type='episcopus']", + "label": "bishops", + "color": "rgb(139, 98, 236)", + "enable": true + }, + { + "value": "placeName,geogName", + "label": "places", + "color": "#A5D6A7", + "enable": true + }, + { + "value": "orgName", + "label": "organizations", + "color": "#FFB74D", + "enable": true + } + ], + "enable": true + }, + { + "label": "Other", + "items": [ + { + "value": "event", + "label": "events", + "color": "#fcfc60", + "enable": true + }, + { + "value": "rolename", + "label": "roles", + "enable": true + }, + { + "value": "measure", + "label": "measures", + "enable": true + } + ], + "enable": true + } + ], + "notSignificantVariants": [ + "type=orthographic" + ], + "defaultEdition": "diplomatic", + "defaultViewMode": "collation", + "proseVersesToggler": true, + "defaultTextFlow": "prose", + "verseNumberPrinter": 5, + "readingColorLight": "rgb(208, 220, 255)", + "readingColorDark": "rgb(101, 138, 255)", + "externalBibliography": { + "biblAttributeToMatch": "xml:id", + "elementAttributesToMatch": [ + "target", + "source" + ] + }, + "biblView": { + "propsToShow": [ + "author", + "title", + "date", + "editor", + "publisher", + "pubPlace", + "citedRange", + "biblScope" + ], + "showAttrNames": false, + "showEmptyValues": false, + "inline": true, + "commaSeparated": 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 d51774e5c..ec14aae7d 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -173,6 +173,18 @@ "omit": "omit.", "wit": "Wit:", "xml": "XML", + "criticalApparatus":"Critical Apparatus", + "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", "addImage": "Add image", "removeImage": "Remove image" } \ No newline at end of file diff --git a/src/assets/i18n/it.json b/src/assets/i18n/it.json index 040b2e8e3..c41fecfc8 100644 --- a/src/assets/i18n/it.json +++ b/src/assets/i18n/it.json @@ -1,177 +1,189 @@ { - "defaultTitle": "Visualizzatore EVT", - "changeLanguage": "Cambia lingua", - "languageEn": "Inglese", - "languageIt": "Italiano", - "changeTheme": "Cambia tema", - "themeNeutral": "Neutro", - "themeModern": "Moderno", - "themeClassic": "Classico", - "shortcuts": "Scorciatoie da tastiera", - "aboutEVT": "Info su EVT", - "projectInfo": "Informazioni progetto", - "openLists": "Apri liste", - "bookmark": "Segnalibro", - "downloadXML": "Scarica XML", - "pinboard": "Bacheca appunti", - "pinboardLoading": "Caricamento appunti...", - "pinboardEmpty": "Nessun appunto trovato.", - "filteredPinboardEmpty": "Nessun appunto trovato per i filtri impostati.", - "filterPins": "Filtra appunti", - "more": "in più", - "lists": "Liste", - "noEntities": "Nessuna entità in questa lista", - "search": "Cerca", - "noMatches": "Nessun risultato", - "entityNotFound": "Entità non trovata", - "noOccurrences": "Nessuna occorrenza", - "relations": "Relazioni", - "selectItems": "Seleziona elementi", - "persons": "Persone", - "places": "Luoghi", - "organizations": "Organizzazioni", - "events": "Eventi", - "roles": "Ruoli", - "measures": "Misure", - "selectAll": "Seleziona tutto", - "clearAll": "Pulisci selezione", - "bishops": "Vescovi", - "diplomatic": "Diplomatica", - "interpretative": "Interpretativa", - "critical": "Critica", - "line": "linea", - "lines": "linee", - "char": "carattere", - "chars": "caratteri", - "word": "parola", - "words": "parole", - "missingS": "mancante", - "missingP": "mancanti", - "omitted": "omissione", - "prose": "Prosa", - "verses": "Versi", - "page": "Pagina {{value}}", - "front": "Front", - "mainText": "Testo principale", - "addVersion": "Aggiungi versione", - "addWitness": "Aggiungi testimone", - "collection": "Raccolta", - "place": "Luogo", - "msIdentifier": "Identificatore del manoscritto", - "repository": "Archivio", - "institution": "Istituzione", - "msName": "Nome", - "idno": "Identificatore", - "altIdentifier": "Identificatore alternativo", - "msContents": "Contenuto del manoscritto", - "textLang": "Lingua", - "author": "Autore", - "docDate": "Data", - "title": "Titolo", - "summary": "Riassunto", - "items": "Testi", - "incipit": "Incipit", - "explicit": "Explicit", - "colophon": "Colophon", - "note": "Note", - "docImprint": "Sigla editoriale", - "phyDesc": "Descrizione fisica", - "supportDesc": "Supporto", - "extent": "Grandezza", - "layoutDesc": "Layout", - "handDesc": "Scrittura", - "decoDesc": "Decorazioni", - "collation": "Collazione", - "description": "Descrizione", - "bindingDesc": "Rilegatura", - "condition": "Condizione fisica", - "foliation": "Foliazione", - "history": "Storia del manoscritto", - "origin": "Origine", - "provenance": "Provenienza", - "acquisition": "Acquisizione", - "additional": "Ulteriori informazioni", - "listBibl": "Lista di citazioni", - "surrogates": "Surrogati", - "adminInfo": "Informazioni amministrative", - "recordHist": "Storia registrata", - "custodialHist": "Storia della conservazione", - "availability": "Disponibilità", - "msFrag": "Frammento del manoscritto", - "altIdentifierLabel": "Identificatore alternativo", - "msPart": "Parte del manoscritto", - "origPlace": "Luogo di origine", - "origDate": "Data di origine", - "selectMsDesc": "Seleziona MsDesc", - "thumbnails": "Miniature", - "firstPage": "Prima pagina", - "previousPage": "Pagina precedente", - "nextPage": "Pagina successiva", - "lastPage": "Ultima pagina", - "toggleToolbar": "Apri/chiudi barra di navigazione", - "viscoll": "Viscoll", - "editors": "Editori", - "sponsors": "Sponsor", - "funders": "Finanziatori", - "principals": "Ricercatori principali", - "responsibles": "Responsabili", - "publicationStatement": "Informazioni sulla pubblicazione", - "publisher": "Editore", - "distributor": "Distributore", - "authority": "Autorità", - "publicationPlace": "Luogo di pubblicazione", - "address": "Indirizzo", - "date": "Data", - "licence": "Licenza", - "editionStatement": "Informazioni sull'edizione", - "edition": "Edizione", - "seriesStatement": "Informazioni sulla serie", - "notesStatement": "Note", - "sourceDesc": "Descrizione della fonte", - "fileDesc": "Descrizione del file", - "encodingDesc": "Codifica", - "profileDesc": "Profilo del testo", - "revisionDesc": "Revisioni", - "projectDesc": "Descrizione del progetto", - "samplingDecl": "Dichiarazione di campionatura", - "editorialDecl": "Dichiarazione sulle pratiche editoriali", - "tagsDecl": "Dichiarazione sulla marcatura", - "styleDefDecl": "Dichiarazione sul linguaggio stile", - "refsDecl": "Dichiarazione sui riferimenti", - "classDecl": "Dichiarazioni sulla classificazione", - "geoDecl": "Dichiarazione sulle coordinate geografiche", - "unitDecl": "Dichiarazione sulle unità di misurazione", - "schemaSpec": "Specifica dello schema", - "schemaRef": "Riferimento allo schema", - "correction": "Principi di correzione", - "hyphenation": "Principi di sillabazione", - "interpretation": "Principi di interpretazione", - "normalization": "Principi di normalizzazione", - "punctuation": "Principi di punteggiatura", - "quotation": "Gestione delle virgolette", - "segmentation": "Principi di segmentazione", - "stdVals": "Valori standard", - "scope": "Ambito", - "selector": "Selettore", - "scheme": "Schema", - "rules": "Regole", - "renditionFirstLineDesc": "lo stile si applica alla prima riga dell'elemento di destinazione", - "renditionFirstLetterDesc": "lo stile si applica alla prima lettera dell'elemento di destinazione", - "renditionBeforeDesc": "lo stile dovrebbe essere applicato immediatamente prima del contenuto dell'elemento di destinazione", - "renditionAfterDesc": "lo stile dovrebbe essere applicato immediatamente dopo il contenuto dell'elemento di destinazione", - "occurrence": "occorrenza", - "occurrences": "occorrenze", - "withId": "con l'identificativo", - "criticalNotes": "Note critiche", - "ortographicVariants": "Varianti ortografiche", - "info": "Info", - "metadataForLemma": "Metadata per il lemma", - "metadataFor": "Metadata per", - "noDataAvailable": "Nessuna informazione disponibile", - "moreInfoAboutApp": "Maggiori informazioni per l'entrata d'apparato", - "omit": "omit.", - "wit": "Wit:", - "xml": "XML", - "addImage": "Aggiungi immagine", - "removeImage": "Rimuovi immagine" -} \ No newline at end of file + "defaultTitle": "Visualizzatore EVT", + "changeLanguage": "Cambia lingua", + "languageEn": "Inglese", + "languageIt": "Italiano", + "changeTheme": "Cambia tema", + "themeNeutral": "Neutro", + "themeModern": "Moderno", + "themeClassic": "Classico", + "shortcuts": "Scorciatoie da tastiera", + "aboutEVT": "Info su EVT", + "projectInfo": "Informazioni progetto", + "openLists": "Apri liste", + "bookmark": "Segnalibro", + "downloadXML": "Scarica XML", + "pinboard": "Bacheca appunti", + "pinboardLoading": "Caricamento appunti...", + "pinboardEmpty": "Nessun appunto trovato.", + "filteredPinboardEmpty": "Nessun appunto trovato per i filtri impostati.", + "filterPins": "Filtra appunti", + "more": "in più", + "lists": "Liste", + "noEntities": "Nessuna entità in questa lista", + "search": "Cerca", + "noMatches": "Nessun risultato", + "entityNotFound": "Entità non trovata", + "noOccurrences": "Nessuna occorrenza", + "relations": "Relazioni", + "selectItems": "Seleziona elementi", + "persons": "Persone", + "places": "Luoghi", + "organizations": "Organizzazioni", + "events": "Eventi", + "roles": "Ruoli", + "measures": "Misure", + "selectAll": "Seleziona tutto", + "clearAll": "Pulisci selezione", + "bishops": "Vescovi", + "diplomatic": "Diplomatica", + "interpretative": "Interpretativa", + "critical": "Critica", + "line": "linea", + "lines": "linee", + "char": "carattere", + "chars": "caratteri", + "word": "parola", + "words": "parole", + "missingS": "mancante", + "missingP": "mancanti", + "omitted": "omissione", + "prose": "Prosa", + "verses": "Versi", + "page": "Pagina {{value}}", + "front": "Front", + "mainText": "Testo principale", + "addVersion": "Aggiungi versione", + "addWitness": "Aggiungi testimone", + "collection": "Raccolta", + "place": "Luogo", + "msIdentifier": "Identificatore del manoscritto", + "repository": "Archivio", + "institution": "Istituzione", + "msName": "Nome", + "idno": "Identificatore", + "altIdentifier": "Identificatore alternativo", + "msContents": "Contenuto del manoscritto", + "textLang": "Lingua", + "author": "Autore", + "docDate": "Data", + "title": "Titolo", + "summary": "Riassunto", + "items": "Testi", + "incipit": "Incipit", + "explicit": "Explicit", + "colophon": "Colophon", + "note": "Note", + "docImprint": "Sigla editoriale", + "phyDesc": "Descrizione fisica", + "supportDesc": "Supporto", + "extent": "Grandezza", + "layoutDesc": "Layout", + "handDesc": "Scrittura", + "decoDesc": "Decorazioni", + "collation": "Collazione", + "description": "Descrizione", + "bindingDesc": "Rilegatura", + "condition": "Condizione fisica", + "foliation": "Foliazione", + "history": "Storia del manoscritto", + "origin": "Origine", + "provenance": "Provenienza", + "acquisition": "Acquisizione", + "additional": "Ulteriori informazioni", + "listBibl": "Lista di citazioni", + "surrogates": "Surrogati", + "adminInfo": "Informazioni amministrative", + "recordHist": "Storia registrata", + "custodialHist": "Storia della conservazione", + "availability": "Disponibilità", + "msFrag": "Frammento del manoscritto", + "altIdentifierLabel": "Identificatore alternativo", + "msPart": "Parte del manoscritto", + "origPlace": "Luogo di origine", + "origDate": "Data di origine", + "selectMsDesc": "Seleziona MsDesc", + "thumbnails": "Miniature", + "firstPage": "Prima pagina", + "previousPage": "Pagina precedente", + "nextPage": "Pagina successiva", + "lastPage": "Ultima pagina", + "toggleToolbar": "Apri/chiudi barra di navigazione", + "viscoll": "Viscoll", + "editors": "Editori", + "sponsors": "Sponsor", + "funders": "Finanziatori", + "principals": "Ricercatori principali", + "responsibles": "Responsabili", + "publicationStatement": "Informazioni sulla pubblicazione", + "publisher": "Editore", + "distributor": "Distributore", + "authority": "Autorità", + "publicationPlace": "Luogo di pubblicazione", + "address": "Indirizzo", + "date": "Data", + "licence": "Licenza", + "editionStatement": "Informazioni sull'edizione", + "edition": "Edizione", + "seriesStatement": "Informazioni sulla serie", + "notesStatement": "Note", + "sourceDesc": "Descrizione della fonte", + "fileDesc": "Descrizione del file", + "encodingDesc": "Codifica", + "profileDesc": "Profilo del testo", + "revisionDesc": "Revisioni", + "projectDesc": "Descrizione del progetto", + "samplingDecl": "Dichiarazione di campionatura", + "editorialDecl": "Dichiarazione sulle pratiche editoriali", + "tagsDecl": "Dichiarazione sulla marcatura", + "styleDefDecl": "Dichiarazione sul linguaggio stile", + "refsDecl": "Dichiarazione sui riferimenti", + "classDecl": "Dichiarazioni sulla classificazione", + "geoDecl": "Dichiarazione sulle coordinate geografiche", + "unitDecl": "Dichiarazione sulle unità di misurazione", + "schemaSpec": "Specifica dello schema", + "schemaRef": "Riferimento allo schema", + "correction": "Principi di correzione", + "hyphenation": "Principi di sillabazione", + "interpretation": "Principi di interpretazione", + "normalization": "Principi di normalizzazione", + "punctuation": "Principi di punteggiatura", + "quotation": "Gestione delle virgolette", + "segmentation": "Principi di segmentazione", + "stdVals": "Valori standard", + "scope": "Ambito", + "selector": "Selettore", + "scheme": "Schema", + "rules": "Regole", + "renditionFirstLineDesc": "lo stile si applica alla prima riga dell'elemento di destinazione", + "renditionFirstLetterDesc": "lo stile si applica alla prima lettera dell'elemento di destinazione", + "renditionBeforeDesc": "lo stile dovrebbe essere applicato immediatamente prima del contenuto dell'elemento di destinazione", + "renditionAfterDesc": "lo stile dovrebbe essere applicato immediatamente dopo il contenuto dell'elemento di destinazione", + "occurrence": "occorrenza", + "occurrences": "occorrenze", + "withId": "con l'identificativo", + "criticalNotes": "Note critiche", + "ortographicVariants": "Varianti ortografiche", + "info": "Info", + "metadataForLemma": "Metadata per il lemma", + "metadataFor": "Metadata per", + "noDataAvailable": "Nessuna informazione disponibile", + "moreInfoAboutApp": "Maggiori informazioni per l'entrata d'apparato", + "omit": "omit.", + "wit": "Wit:", + "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", + "addImage": "Aggiungi immagine", + "removeImage": "Rimuovi immagine" +} 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 e717cf20a..3afd8e3d8 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -6,192 +6,260 @@ @import "./assets/scss/bootstrapOverrides"; @font-face { - font-family: "Junicode"; - src: url("/assets/fonts/Junicode.woff") format("woff"); - font-weight: normal; - font-style: normal; + font-family: "Junicode"; + src: url("/assets/fonts/Junicode.woff") format("woff"); + font-weight: normal; + font-style: normal; } @font-face { - font-family: "Junicode"; - src: url("/assets/fonts/Junicode-Bold.woff") format("woff"); - font-weight: bold; - font-style: normal; + font-family: "Junicode"; + src: url("/assets/fonts/Junicode-Bold.woff") format("woff"); + font-weight: bold; + font-style: normal; } @font-face { - font-family: "Junicode"; - src: url("/assets/fonts/Junicode-Italic.woff") format("woff"); - font-weight: normal; - font-style: italic; + font-family: "Junicode"; + src: url("/assets/fonts/Junicode-Italic.woff") format("woff"); + font-weight: normal; + font-style: italic; } @font-face { - font-family: "Junicode"; - src: url("/assets/fonts/Junicode-BoldItalic.woff") format("woff"); - font-weight: bold; - font-style: italic; + font-family: "Junicode"; + src: url("/assets/fonts/Junicode-BoldItalic.woff") format("woff"); + font-weight: bold; + font-style: italic; } html, body { - height: 100%; + height: 100%; } body { - overflow: hidden; + overflow: hidden; } .edition-font { - font-family: Junicode, Times, serif; + font-family: Junicode, Times, serif; } .ui-font { - font-family: Arial, sans-serif; // TODO: decide which font to use for the UI + font-family: Arial, sans-serif; // TODO: decide which font to use for the UI } // ng-select overwrites .ng-select { - position: relative; - display: inline-flex !important; + position: relative; + display: inline-flex !important; + vertical-align: middle; + font-size: 0.875rem; + line-height: 1.5; + border-radius: 0.2rem; + + .ng-select-container .ng-value-container .ng-placeholder { + top: 0 !important; + position: relative !important; + } + + &.ng-select-focused:not(.ng-select-opened) > .ng-select-container { + border: none; + } + + .ng-select-container { + height: 31px !important; + min-height: 0.6rem; vertical-align: middle; - font-size: 0.875rem; - line-height: 1.5; - border-radius: 0.2rem; + border: none; + + @include themify($themes) { + background-color: themed("toolsBackground") !important; + color: themed("toolsColor"); + } + } - .ng-select-container .ng-value-container .ng-placeholder { - top: 0 !important; - position: relative !important; + .ng-arrow-wrapper { + padding-left: 0.35rem; + user-select: none; + height: 100%; + width: 1.5rem; + opacity: 0.5; + + &:hover { + opacity: 1; } - &.ng-select-focused:not(.ng-select-opened) > .ng-select-container { - border: none; + @include themify($themes) { + background-color: themed("toolsBackgroundDarker") !important; } - .ng-select-container { - height: 31px !important; - min-height: 31px; - vertical-align: middle; - border: none; + .ng-arrow { + border-width: 5px 5px 0; + vertical-align: text-bottom; - @include themify($themes) { - background-color: themed("toolsBackground") !important; - color: themed("toolsColor"); - } + @include themify($themes) { + border-color: themed("baseColorDark") transparent transparent; + } } + } + &.ng-select-opened > .ng-select-container { .ng-arrow-wrapper { - padding-left: 6px; - user-select: none; - height: 100%; - width: 24px; - opacity: 0.5; - - &:hover { - opacity: 1; - } + opacity: 1; + .ng-arrow { @include themify($themes) { - background-color: themed("toolsBackgroundDarker") !important; - } - - .ng-arrow { - border-width: 5px 5px 0; - vertical-align: text-bottom; - - @include themify($themes) { - border-color: themed("baseColorDark") transparent transparent; - } + border-color: transparent transparent themed("baseColorDark"); } + } } + } - &.ng-select-opened > .ng-select-container { - .ng-arrow-wrapper { - opacity: 1; - - .ng-arrow { - @include themify($themes) { - border-color: transparent transparent themed("baseColorDark"); - } - } - } + .ng-dropdown-panel { + @include themify($themes) { + background-color: themed("toolsBackground") !important; + color: themed("toolsColor"); } + } + .ng-dropdown-panel-items { + @include set("box-shadow", "0 0.125rem 0.25rem rgba(0, 0, 0, 0.1)"); - .ng-dropdown-panel { + .ng-option { + @include themify($themes) { + background-color: themed("toolsBackground") !important; + color: themed("toolsColor"); + } + &.ng-option-marked { @include themify($themes) { - background-color: themed("toolsBackground") !important; - color: themed("toolsColor"); + background-color: rgba(themed("baseColorDark"), 0.1) !important; } - } - .ng-dropdown-panel-items { - @include set("box-shadow", "0 0.125rem 0.25rem rgba(0, 0, 0, 0.1)"); - - .ng-option { - @include themify($themes) { - background-color: themed("toolsBackground") !important; - color: themed("toolsColor"); - } - &.ng-option-marked { - @include themify($themes) { - background-color: rgba(themed("baseColorDark"), 0.1) !important; - } - } - &.ng-option-selected { - @include themify($themes) { - background-color: themed("baseColorDark") !important; - color: themed("baseColorLight") !important; - } - } + } + &.ng-option-selected { + @include themify($themes) { + background-color: themed("baseColorDark") !important; + color: themed("baseColorLight") !important; } + } } + } - &.ng-select-opened { - .ng-arrow-wrapper .ng-arrow { - border-width: 0 4px 4px; - } + &.ng-select-opened { + .ng-arrow-wrapper .ng-arrow { + border-width: 0 4px 4px; } + } } .overflow-y-auto { - overflow-y: auto; + overflow-y: auto; } .pointer { - cursor: pointer; + cursor: pointer; } .abbr, .expan { - color: purple; - text-decoration: underline; + color: purple; + text-decoration: underline; } .ex { - font-style: italic; + font-style: italic; } .hidden { - display: none; + display: none; } .btn-close { - height: 20px !important; - width: 20px !important; - line-height: 0.8rem; - padding: 0; - font-size: 0.9rem; + height: 1.25rem !important; + width: 1.25rem !important; + line-height: 0.8rem; + padding: 0; + font-size: 0.9rem; } -.highlightverse{ - cursor: pointer; - background-color: rgba(230,206,160,0.8); /*d5b17d e4c89b*/ - /*opacity: 0.8; - filter:alpha(opacity=80); For IE8 and earlier */ - // border: 1px solid #b99562; - color: #000000; - padding: 3px 0px; - &.selected{ - background-color: rgb(248, 230, 191); - } +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 { + min-width: 8rem; +} + +.popover-body evt-quote-entry, +.popover-body evt-analogue-entry { + pointer-events: none; +} + +.biblio-list li { + margin-left: 1rem; +} + +.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; +} + +evt-source-detail evt-original-encoding-viewer pre, +evt-analogue-detail evt-original-encoding-viewer pre { + margin-top: -2% !important; + padding: 0 !important; + background-color: transparent !important; +} + +evt-source-detail evt-original-encoding-viewer code, +evt-analogue-detail evt-original-encoding-viewer code { + color: initial; + font-size: 85%; } +evt-source-detail evt-original-encoding-viewer code .token, +evt-analogue-detail evt-original-encoding-viewer code .token { + color: #212529 !important; +} + +.highlightverse{ + cursor: pointer; + background-color: rgba(230,206,160,0.8); + color: #000000; + padding: 3px 0px; + &.selected{ + background-color: rgb(248, 230, 191); + } +} \ No newline at end of file