Skip to content

Commit

Permalink
Merge pull request #238 from evt-project/feature/bibl-visualization
Browse files Browse the repository at this point in the history
This feature adds the 'Bibliography' visualization panel in the project info modal.

The panel features a list of all bibliographic entries in the TEI edition, styled with a specific bibliographic style (for example, the Chicago Author-Date, which is the default style).
  • Loading branch information
laurelled committed Jun 27, 2024
2 parents 7fcaa80 + 488ac0e commit dcc6c82
Show file tree
Hide file tree
Showing 29 changed files with 729 additions and 111 deletions.
31 changes: 31 additions & 0 deletions src/app/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,44 @@ export interface UiConfig {
initNavBarOpened: boolean;
thumbnailsButton: boolean;
viscollButton: boolean;
defaultBibliographicStyle: string;
allowedBibliographicStyles: {
[key: string]: {
id: string;
label: string;
enabled: boolean;
propsOrder: BibliographicProperties[];
properties: BibliographicStyle;
}
};
mainFontFamily: string;
mainFontSize: string;
secondaryFontFamily: string;
secondaryFontSize: string;
theme: 'neutral' | 'modern' | 'classic';
syncZonesHighlightButton: boolean;
}
export type CitingRanges = 'issue' | 'volume' | 'page';
export type BibliographicProperties = 'author'| 'date'| 'title'| 'editor' | 'publication' | 'pubPlace' | 'publisher' | 'doi';
export type BibliographicStyle = Partial<{
propsDelimiter: string;
authorStyle: Partial<{
forenameInitials: boolean;
delimiter: string;
lastDelimiter: string;
order: Array<'forename' | 'surname'>;
maxAuthors: string;
}>;
publicationStyle: Partial<{
citingAcronym: 'all' | 'none' | CitingRanges[];
includeEditor: boolean;
inBrackets: CitingRanges[];
}>;
dateInsidePublication: boolean;
titleQuotes: boolean;
emphasized: BibliographicProperties[];
inBrackets: BibliographicProperties[];
}>;

export interface EditionConfig {
editionTitle: string;
Expand Down
6 changes: 6 additions & 0 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import { ApparatusEntryComponent } from './components/apparatus-entry/apparatus-
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 { BiblioEntryComponent } from './components/biblio/biblio.component';
import { BibliographyInfoComponent } from './components/bibliography-info/bibliography-info.component';
import { BibliographicStyleSelectorComponent } from './components/bibliography-info/bibliographic-style-selector/bibliographic-style-selector';
import { BiblioListComponent } from './components/biblioList/biblio-list.component';
import { ChangeLayerSelectorComponent } from './components/change-layer-selector/change-layer-selector.component';
import { CharComponent } from './components/char/char.component';
Expand Down Expand Up @@ -124,6 +126,7 @@ import { SourceNoteComponent } from './components/sources/source-note/source-not
import { SourcesComponent } from './components/sources/sources.component';
import { SourcesPanelComponent } from './panels/sources-panel/sources-panel.component';
import { StartsWithPipe } from './pipes/starts-with.pipe';
import { StyledBiblioEntryComponent } from './components/bibliography-info/biblio-styled/biblio-styled.component';
import { SubstitutionComponent } from './components/substitution/substitution.component';
import { SuppliedComponent } from './components/supplied/supplied.component';
import { SurplusComponent } from './components/surplus/surplus.component';
Expand Down Expand Up @@ -215,6 +218,8 @@ const DynamicComponents = [
AnnotatorDirective,
AppComponent,
BiblioEntryComponent,
BibliographyInfoComponent,
BibliographicStyleSelectorComponent,
BiblioListComponent,
ChangeLayerSelectorComponent,
CollationComponent,
Expand Down Expand Up @@ -263,6 +268,7 @@ const DynamicComponents = [
SourcesComponent,
SourcesPanelComponent,
StartsWithPipe,
StyledBiblioEntryComponent,
SubstitutionComponent,
TextPanelComponent,
TextSourcesComponent,
Expand Down
2 changes: 1 addition & 1 deletion src/app/components/biblio/biblio.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<ng-container *ngIf="showEmptyValues || (data[element.value] && data[element.value].length > 0)">
<ng-container *ngIf="showAttrNames">{{element.value}}: </ng-container>
<ng-container *ngIf="data[element.value]">
{{ data[element.value] }} <span *ngIf="data[element.value].length > 0" [class.hidden]="(!isCommaSeparated)">,</span>
{{ data[element.value] }} <span *ngIf="data[element.value].length > 0" [class.hidden]="(!isCommaSeparated)">,</span>&nbsp;
</ng-container>
<br *ngIf="!inline && data[element.value].length > 0">
</ng-container>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<ng-container *ngIf="biblEntry.type && (biblEntry.type.name === 'BibliographicEntry')">
<ng-container *ngIf="isStructured(biblEntry); else plainText">
<ng-container *ngFor="let element of showList">
<ng-template *ngTemplateOutlet="styledBiblioTemplate; context: { element, entries: [biblEntry] }"></ng-template>
</ng-container>
</ng-container>
<ng-template #plainText>{{ biblEntry.text }}</ng-template>
</ng-container>
<ng-container *ngIf="biblEntry.type && (biblEntry.type.name === 'BibliographicStructEntry')">
<ng-container *ngFor="let element of showList">
<ng-template *ngTemplateOutlet="styledBiblioTemplate; context: { element, entries: getContextForElement(element, biblEntry)}"></ng-template>
</ng-container>
</ng-container>

<ng-template #styledBiblioTemplate let-element="element" let-entries="entries">
<ng-container *ngIf="entries.length > 0 && firstContainsProperty(entries, element)">
<span [ngClass]="styleProperties?.emphasized && styleProperties.emphasized.includes(element) ? 'font-italic' : ''">
<ng-container *ngIf="styleProperties?.inBrackets && styleProperties?.inBrackets.includes(element)">(</ng-container><ng-template [ngTemplateOutlet]="this[element]" [ngTemplateOutletContext]="{ $implicit: entries }"></ng-template><ng-container *ngIf="styleProperties?.inBrackets && styleProperties?.inBrackets.includes(element)">)</ng-container>{{styleProperties?.propsDelimiter || ","}}
</span>
</ng-container>
</ng-template>

<ng-template #title let-entries>
<ng-container *ngIf="styleProperties?.titleQuotes">«</ng-container>{{ entries[0]?.titleDetails ? entries[0].titleDetails.title : entries[0].title }}<ng-container *ngIf="styleProperties?.titleQuotes">»</ng-container>
</ng-template>

<ng-template #author let-entries>
<ng-container *ngFor="let author of getAuthorsDetails(entries); last as last; index as index; count as count; ">
<ng-container *ngIf="author.forename && author.surname; else authorFullName">
<ng-container *ngFor="let name of (styleProperties.authorStyle?.order || ['surname', 'forename']); last as isLast">{{name === "forename" && styleProperties.authorStyle?.forenameInitials ? author.forenameInitials : author[name] }}<ng-container *ngIf="!isLast">,&nbsp;</ng-container></ng-container>
</ng-container>
<ng-template #authorFullName>{{ author.fullName }}</ng-template>
<ng-container *ngIf="!last">{{ index < count - 2 ? (styleProperties.authorStyle?.delimiter || ',') : (styleProperties.authorStyle?.lastDelimiter || " and") }}&nbsp;</ng-container>
</ng-container>
</ng-template>

<ng-template #publication let-entries>
<ng-container *ngFor="let monogr of entries; index as index; last as last">
<ng-container>{{ monogr.publication }}</ng-container>
<ng-container *ngIf="monogr?.volumeNumber">&nbsp;<span *ngIf="requiresAcronym('volume')">vol. </span>{{monogr.volumeNumber}}</ng-container>
<span class="font-normal">
<ng-container *ngIf="(index > 0 || styleProperties.publicationStyle?.includeEditor) && !containsOnlyEmptyValues(monogr.editor)">, <ng-template [ngTemplateOutlet]="editor" [ngTemplateOutletContext]="{ $implicit: [ monogr ]}"></ng-template>,</ng-container>
<ng-container *ngIf="monogr?.issueNumber">{{ styleProperties.publicationStyle?.inBrackets && styleProperties.publicationStyle?.inBrackets.includes('issue') ? '(' : ', ' }}<span *ngIf="requiresAcronym('issue')">no. </span>{{styleProperties?.publicationStyle?.inBrackets && styleProperties.publicationStyle?.inBrackets.includes('issue')? monogr.issueNumber + ')' : monogr.issueNumber }}</ng-container>
<ng-container *ngIf="styleProperties?.dateInsidePublication || index > 0">,&nbsp;<ng-container *ngIf="styleProperties?.inBrackets && styleProperties.inBrackets.includes('date')">(</ng-container><ng-template [ngTemplateOutlet]="date" [ngTemplateOutletContext]="{ $implicit: [ monogr ]}"></ng-template><ng-container *ngIf="styleProperties?.inBrackets && styleProperties.inBrackets.includes('date')">)</ng-container></ng-container>
<ng-container *ngIf="monogr?.pageNumber">: <span *ngIf="requiresAcronym('issue')">pp. </span>{{monogr.pageNumber}}</ng-container>
<ng-container *ngIf="index > 0">, <ng-container *ngFor="let property of getPublisherDetailsOrder(); last as last">{{monogr[property]}}<ng-container *ngIf="!last">: </ng-container></ng-container></ng-container>
<ng-container *ngIf="!last">&nbsp;{{ "reprintedIn" | translate }}&nbsp;</ng-container>
</span>
</ng-container>
</ng-template>

<ng-template #date let-entries>{{ entries[0].date[0] || 's.d.' }}</ng-template>

<ng-template #publisher let-entries>{{ entries[0].publisher }}</ng-template>

<ng-template #pubPlace let-entries>{{ entries[0].pubPlace }}</ng-template>

<ng-template #editor let-entries>
<ng-container *ngIf="!containsOnlyEmptyValues(entries[0].editor)">{{ "editedBy" | translate}}&nbsp;<ng-container *ngFor="let editor of entries[0].editor; last as last">{{editor}}<ng-container *ngIf="!last">,</ng-container></ng-container>
</ng-container>
</ng-template>

<ng-template #doi let-entries><ng-container *ngFor="let entry of entries"><a *ngIf="entry.doi" target="_blank" [href]="'https://doi.org/' + entry.doi">https://doi.org/{{entry.doi}}</a></ng-container></ng-template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.font-italic {
font-style: italic;
}

.font-normal {
font-style: normal !important;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { BiblioEntryComponent } from './biblio.component';

describe('BiblioComponent', () => {
let component: BiblioEntryComponent;
let fixture: ComponentFixture<BiblioEntryComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ BiblioEntryComponent ],
})
.compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(BiblioEntryComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { AfterViewInit, ChangeDetectorRef, Component, Input, OnChanges, SimpleChanges, TemplateRef, ViewChild } from '@angular/core';
import { AppConfig, BibliographicStyle, CitingRanges } from 'src/app/app.config';
import { AuthorDetail, BibliographicEntry, BibliographicStructEntry } from 'src/app/models/evt-models';

@Component({
selector: 'evt-styled-biblio-entry',
templateUrl: './biblio-styled.component.html',
styleUrls: ['./biblio-styled.component.scss'],
})
export class StyledBiblioEntryComponent implements OnChanges, AfterViewInit {

@ViewChild('title', { static: false }) title: TemplateRef<any>;
@ViewChild('author', { static: false }) author: TemplateRef<any>;
@ViewChild('publication', { static: false }) publication: TemplateRef<any>;
@ViewChild('editor', { static: false }) editor: TemplateRef<any>;
@ViewChild('date', { static: false }) date: TemplateRef<any>;
@ViewChild('pubPlace', { static: false }) pubPlace: TemplateRef<any>;
@ViewChild('publisher', { static: false }) publisher: TemplateRef<any>;
@ViewChild('doi', { static: false }) doi: TemplateRef<any>;


@Input() data: BibliographicEntry | BibliographicStructEntry;
@Input() style: string = AppConfig.evtSettings.ui.defaultBibliographicStyle;

public biblEntry: any;
public showList: string[];
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;
public styleProperties : BibliographicStyle;

flattenBiblStruct(entry: BibliographicStructEntry): BibliographicEntry[] {
return entry.analytic.concat(entry.monogrs.concat(entry.series));
}

getContextForElement(element: string, structEntry: BibliographicStructEntry): BibliographicEntry[]{
let context: BibliographicEntry[];
switch(element){
case 'title':
context = structEntry.analytic;
break;
case 'publication':
case 'date':
case 'publPlace':
case 'publisher':
case 'editor':
context = structEntry.monogrs;
break;
default:
context = this.flattenBiblStruct(structEntry);
break;
}

return context;
}

firstContainsProperty(entries: BibliographicEntry[], element:string): boolean{
const elementInFirstEntry = entries[0]?.[element];

return elementInFirstEntry && elementInFirstEntry.length > 0;
}

containsOnlyEmptyValues(arr: string[]): boolean{
return arr.reduce((prev, x) => (x === '') && prev, true);
}

requiresAcronym(elem: CitingRanges): boolean{
const publicationStyle = this.styleProperties.publicationStyle || { citingAcronym: 'none' };
if(!publicationStyle?.citingAcronym) { return false; }

if(publicationStyle.citingAcronym === 'all'){
return true;
}else if(publicationStyle.citingAcronym === 'none'){
return false;
}

return publicationStyle.citingAcronym.includes(elem);
}

getPublisherDetailsOrder(): string[]{
return this.showList.filter((x) => x === 'publisher' || x === 'pubPlace');
}

getAuthorsDetails(entries: BibliographicEntry[]): AuthorDetail[]{
return entries.reduce((prev, e) => prev.concat(e.authorsDetails), []);
}

isStructured(entry: BibliographicEntry): boolean{
// searching for the most relevant signs of a structured entry.
return entry.originalEncoding.querySelectorAll('title, author, date').length > 0;
}

ngOnChanges(changes: SimpleChanges): void {
if(this.data.type && this.data.type === BibliographicEntry){
this.biblEntry = this.data as BibliographicEntry;
}
if(this.data.type && this.data.type === BibliographicStructEntry){
this.biblEntry = this.data as BibliographicStructEntry;
}
if(changes.style){
this.showList = AppConfig.evtSettings.ui.allowedBibliographicStyles[this.style].propsOrder;
this.styleProperties = AppConfig.evtSettings.ui.allowedBibliographicStyles[this.style].properties;
}
}

constructor(private cd: ChangeDetectorRef){}
ngAfterViewInit(): void {
this.cd.detectChanges();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<div class="container p-0 m-0">
<div class="d-flex align-items-center">
<label for="biblStyleSelect">{{ 'style' | translate }}</label>
<div>
<ng-select
id="biblStyleSelect"
[items]="bibliographicStyles"
bindLabel="label"
bindValue="id"
[clearable]="false"
[searchable]="true"
[(ngModel)]="selectedStyleID"
(change)="changeStyle()"
(click)="stopPropagation($event)"
class="p-2">
</ng-select>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';

import { BibliographicStyleSelectorComponent } from './bibliographic-style-selector';

describe('BibliographicStyleSelectorComponent', () => {
let component: BibliographicStyleSelectorComponent;
let fixture: ComponentFixture<BibliographicStyleSelectorComponent>;

beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ BibliographicStyleSelectorComponent ],
})
.compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(BibliographicStyleSelectorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Component, EventEmitter, OnInit, Output } from '@angular/core';
import { AppConfig } from 'src/app/app.config';

@Component({
selector: 'evt-bibliographic-style-selector',
templateUrl: './bibliographic-style-selector.component.html',
styleUrls: ['./bibliographic-style-selector.component.scss'],
})
export class BibliographicStyleSelectorComponent implements OnInit {
public bibliographicStyles = (Object.values(AppConfig.evtSettings.ui.allowedBibliographicStyles) || []).filter((el) => el.enabled);
public selectedStyleID : string;

@Output() selectionChange: EventEmitter<string> = new EventEmitter<string>();

ngOnInit(){
this.selectedStyleID = AppConfig.evtSettings.ui.defaultBibliographicStyle;
this.selectionChange.emit(this.selectedStyleID);
}

stopPropagation(event: MouseEvent) {
event.stopPropagation();
}

changeStyle(){
this.selectionChange.emit(this.selectedStyleID);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<div>
<evt-bibliographic-style-selector (selectionChange)="setCurrentStyle($event)"></evt-bibliographic-style-selector>
</div>
<div class="evt-bibliography-content list-group">
<ng-container *ngFor="let elem of biblList">
<span>
<evt-styled-biblio-entry [data]="elem" [style]="currentStyle"></evt-styled-biblio-entry>
</span>
</ng-container>
</div>
Loading

0 comments on commit dcc6c82

Please sign in to comment.