From 1eba7612ae4b813093575dd963629c2b5833d06f Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Mon, 12 Feb 2024 16:56:06 +0100 Subject: [PATCH 01/24] DSC-1564 cherrypick from main for missing labels --- src/assets/i18n/en.json5 | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index e1fa6450c7c..9f3059b9fb2 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -3367,14 +3367,6 @@ "item.preview.oairecerif.oamandate": "OA Mandate", - "item.preview.oairecerif.identifier.url": "Link", - - "item.preview.organization.identifier.ror": "ROR ID", - - "item.preview.organization.foundingDate": "Founding Date", - - "item.preview.organization.address.addressCountry": "Country", - "item.preview.organization.parentOrganization": "Parent Organization", "item.preview.person.familyName": "Surname:", @@ -3393,6 +3385,20 @@ "item.preview.oaire.fundingStream": "Funding Stream:", + "item.preview.oairecerif.identifier.url" : "URL", + + "item.preview.organization.address.addressCountry" : "Country", + + "item.preview.organization.foundingDate" : "Founding Date", + + "item.preview.organization.identifier.crossrefid" : "CrossRef ID", + + "item.preview.organization.identifier.isni" : "ISNI", + + "item.preview.organization.identifier.ror" : "ROR ID", + + "item.preview.organization.legalName" : "Legal Name", + "item.preview.person.identifier.rid": "Researcher ID:", "item.preview.person.identifier.scopus-author-id": "Scopus Author Id", From 28d2cfa6cef5b3b59c1770e6746186c48c718619 Mon Sep 17 00:00:00 2001 From: Mattia Vianelli Date: Mon, 4 Mar 2024 16:11:18 +0100 Subject: [PATCH 02/24] DSC-1564 run yarn lint fix --- src/assets/i18n/en.json5 | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 9f3059b9fb2..736bbd374be 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -3385,19 +3385,19 @@ "item.preview.oaire.fundingStream": "Funding Stream:", - "item.preview.oairecerif.identifier.url" : "URL", + "item.preview.oairecerif.identifier.url": "URL", - "item.preview.organization.address.addressCountry" : "Country", + "item.preview.organization.address.addressCountry": "Country", - "item.preview.organization.foundingDate" : "Founding Date", + "item.preview.organization.foundingDate": "Founding Date", - "item.preview.organization.identifier.crossrefid" : "CrossRef ID", + "item.preview.organization.identifier.crossrefid": "CrossRef ID", - "item.preview.organization.identifier.isni" : "ISNI", + "item.preview.organization.identifier.isni": "ISNI", - "item.preview.organization.identifier.ror" : "ROR ID", + "item.preview.organization.identifier.ror": "ROR ID", - "item.preview.organization.legalName" : "Legal Name", + "item.preview.organization.legalName": "Legal Name", "item.preview.person.identifier.rid": "Researcher ID:", From 6b98794546381be4dbd80a6308a02f65fb9398f3 Mon Sep 17 00:00:00 2001 From: Mattia Vianelli Date: Mon, 4 Mar 2024 16:33:30 +0100 Subject: [PATCH 03/24] DSC-1564 Updated item.preview.organization.identifier.crossrefid label to reflect ticket request --- src/assets/i18n/en.json5 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 736bbd374be..5607330cdf6 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -3391,7 +3391,7 @@ "item.preview.organization.foundingDate": "Founding Date", - "item.preview.organization.identifier.crossrefid": "CrossRef ID", + "item.preview.organization.identifier.crossrefid": "Crossref Funder ID", "item.preview.organization.identifier.isni": "ISNI", From cfb80d44e06386bba405e8fb9e57d7c90d4ad039 Mon Sep 17 00:00:00 2001 From: Mattia Vianelli Date: Mon, 4 Mar 2024 16:35:22 +0100 Subject: [PATCH 04/24] DSC-1564 Fix typo --- src/assets/i18n/en.json5 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 5607330cdf6..44972d5d4dc 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -3391,7 +3391,7 @@ "item.preview.organization.foundingDate": "Founding Date", - "item.preview.organization.identifier.crossrefid": "Crossref Funder ID", + "item.preview.organization.identifier.crossrefid": "Crossref Founder ID", "item.preview.organization.identifier.isni": "ISNI", From 64841d8b6bd1b7b5af78fe351b688a8ca22be485 Mon Sep 17 00:00:00 2001 From: Mattia Vianelli Date: Thu, 21 Mar 2024 19:07:08 +0100 Subject: [PATCH 05/24] DSC-1564 Correct label for crossref id --- src/assets/i18n/en.json5 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 44972d5d4dc..736bbd374be 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -3391,7 +3391,7 @@ "item.preview.organization.foundingDate": "Founding Date", - "item.preview.organization.identifier.crossrefid": "Crossref Founder ID", + "item.preview.organization.identifier.crossrefid": "CrossRef ID", "item.preview.organization.identifier.isni": "ISNI", From 761c84a1468623c03dee05a3b518e5301b64e4ec Mon Sep 17 00:00:00 2001 From: Simone Ramundi Date: Wed, 3 Apr 2024 18:47:35 +0200 Subject: [PATCH 06/24] [DSC-1590] Changed placeholder text and styling --- .../models/tag/dynamic-tag.component.html | 2 +- .../form/builder/parsers/tag-field-parser.ts | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.html index 887e0f00588..669de36ad6c 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.html @@ -6,7 +6,7 @@ + [wrapperClass]="'border-bottom'"> 0) { this.setVocabularyOptions(tagModelConfig, this.parserOptions.collectionUUID); } - this.setValues(tagModelConfig, fieldValue, null, true); - - const tagModel = new DynamicTagModel(tagModelConfig); - + tagModelConfig.placeholder = 'Enter the Keywords'; + const tagModel = new DynamicTagModel(tagModelConfig, clsTag); return tagModel; } From 883f8b5160382d17cc61a6878cb2d07cff8d7d80 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Tue, 20 Feb 2024 19:42:49 +0100 Subject: [PATCH 07/24] [UXP-114] Refactoring markdown-viewer.component by using markdown.pipe --- .../shared/markdown-viewer/markdown-viewer.component.html | 3 ++- .../markdown-viewer/markdown-viewer.component.spec.ts | 7 +++++-- src/app/shared/markdown-viewer/markdown-viewer.module.ts | 3 ++- src/app/shared/utils/markdown.pipe.ts | 4 ++-- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/app/shared/markdown-viewer/markdown-viewer.component.html b/src/app/shared/markdown-viewer/markdown-viewer.component.html index 5715948bbef..f6d311a1e66 100644 --- a/src/app/shared/markdown-viewer/markdown-viewer.component.html +++ b/src/app/shared/markdown-viewer/markdown-viewer.component.html @@ -1 +1,2 @@ - + + diff --git a/src/app/shared/markdown-viewer/markdown-viewer.component.spec.ts b/src/app/shared/markdown-viewer/markdown-viewer.component.spec.ts index bf969b397de..0e750e1425b 100644 --- a/src/app/shared/markdown-viewer/markdown-viewer.component.spec.ts +++ b/src/app/shared/markdown-viewer/markdown-viewer.component.spec.ts @@ -1,6 +1,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MarkdownViewerComponent } from './markdown-viewer.component'; +import { MarkdownPipe } from '../utils/markdown.pipe'; +import { By } from '@angular/platform-browser'; describe('DsMarkdownViewerComponent', () => { let component: MarkdownViewerComponent; @@ -8,7 +10,7 @@ describe('DsMarkdownViewerComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ MarkdownViewerComponent ] + declarations: [ MarkdownViewerComponent, MarkdownPipe ] }) .compileComponents(); }); @@ -20,6 +22,7 @@ describe('DsMarkdownViewerComponent', () => { }); it('should create', () => { - expect(component).toBeTruthy(); + const span = fixture.debugElement.query(By.css('span')); + expect(span).toBeTruthy(); }); }); diff --git a/src/app/shared/markdown-viewer/markdown-viewer.module.ts b/src/app/shared/markdown-viewer/markdown-viewer.module.ts index ea5329a60dd..e9d53b0e40e 100644 --- a/src/app/shared/markdown-viewer/markdown-viewer.module.ts +++ b/src/app/shared/markdown-viewer/markdown-viewer.module.ts @@ -2,10 +2,11 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MarkdownViewerComponent } from './markdown-viewer.component'; import { NuMarkdownModule } from '@ng-util/markdown'; +import { SharedModule } from '../shared.module'; @NgModule({ declarations: [ MarkdownViewerComponent ], exports: [ MarkdownViewerComponent ], - imports: [ CommonModule, NuMarkdownModule ] + imports: [CommonModule, NuMarkdownModule, SharedModule] }) export class MarkdownViewerModule { } diff --git a/src/app/shared/utils/markdown.pipe.ts b/src/app/shared/utils/markdown.pipe.ts index 90b6d25731a..7e624c1b52e 100644 --- a/src/app/shared/utils/markdown.pipe.ts +++ b/src/app/shared/utils/markdown.pipe.ts @@ -49,8 +49,8 @@ export class MarkdownPipe implements PipeTransform { ) { } - async transform(value: string): Promise { - if (!environment.markdown.enabled) { + async transform(value: string, forcePreview = false): Promise { + if (!environment.markdown.enabled && !forcePreview) { return value; } const MarkdownIt = await this.markdownIt; From d1fc68c661c74ed7c4ca04670ad2d71321561a5c Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Tue, 20 Feb 2024 19:54:15 +0100 Subject: [PATCH 08/24] [UXP-114] Restore markdown-viewer.component for text-section.component --- .../text-section/text-section.component.html | 5 ++++- .../text-section/text-section.component.spec.ts | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/app/shared/explore/section-component/text-section/text-section.component.html b/src/app/shared/explore/section-component/text-section/text-section.component.html index d5824d1454f..8f477955fcb 100644 --- a/src/app/shared/explore/section-component/text-section/text-section.component.html +++ b/src/app/shared/explore/section-component/text-section/text-section.component.html @@ -2,7 +2,10 @@
{{ ('explore.text-section.' + textRowSection.content) | translate }}
{{ textRowSection.content }}
-
+
+ +
diff --git a/src/app/shared/explore/section-component/text-section/text-section.component.spec.ts b/src/app/shared/explore/section-component/text-section/text-section.component.spec.ts index edd8bdea1dd..34d92888d77 100644 --- a/src/app/shared/explore/section-component/text-section/text-section.component.spec.ts +++ b/src/app/shared/explore/section-component/text-section/text-section.component.spec.ts @@ -62,10 +62,10 @@ describe('TextSectionComponent', () => { expect(component).toBeTruthy(); }); // FIXME: complete scenarios - it('should render text-metadata with innerHtml', () => { + it('should render text-metadata with ds-markdown-viewer', () => { component.sectionId = 'site'; fixture.detectChanges(); - const name = fixture.debugElement.queryAll(By.css('div'))[0].nativeElement; - expect(name.innerHTML).toContain(component.site.metadata['cms.homepage.footer'][0].value); + const dsMarkdownViewer = fixture.debugElement.query(By.css('[data-test="ds-markdown-viewer"]')); + expect(dsMarkdownViewer).toBeTruthy(); }); }); From d614db187e4ee6f4b80c747aa4c57397c2625c2a Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Tue, 20 Feb 2024 20:00:32 +0100 Subject: [PATCH 09/24] [UXP-114] Remove unused import --- src/app/shared/markdown-viewer/markdown-viewer.module.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/shared/markdown-viewer/markdown-viewer.module.ts b/src/app/shared/markdown-viewer/markdown-viewer.module.ts index e9d53b0e40e..52d0f77acc2 100644 --- a/src/app/shared/markdown-viewer/markdown-viewer.module.ts +++ b/src/app/shared/markdown-viewer/markdown-viewer.module.ts @@ -1,12 +1,11 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MarkdownViewerComponent } from './markdown-viewer.component'; -import { NuMarkdownModule } from '@ng-util/markdown'; import { SharedModule } from '../shared.module'; @NgModule({ declarations: [ MarkdownViewerComponent ], exports: [ MarkdownViewerComponent ], - imports: [CommonModule, NuMarkdownModule, SharedModule] + imports: [ CommonModule, SharedModule ] }) export class MarkdownViewerModule { } From 5e9c83e6a308fba00bcbd29d9a5cc5a1ac68d478 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 21 Feb 2024 16:29:36 +0100 Subject: [PATCH 10/24] [UXP-114] Improve markdown pipe in order to check for empty values --- src/app/shared/utils/markdown.pipe.spec.ts | 14 ++++++++++++++ src/app/shared/utils/markdown.pipe.ts | 4 +++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/app/shared/utils/markdown.pipe.spec.ts b/src/app/shared/utils/markdown.pipe.spec.ts index 50f772097db..f8f147dc890 100644 --- a/src/app/shared/utils/markdown.pipe.spec.ts +++ b/src/app/shared/utils/markdown.pipe.spec.ts @@ -54,6 +54,20 @@ describe('Markdown Pipe', () => { ); }); + it('should render undefined value', async () => { + await testTransform( + undefined, + undefined + ); + }); + + it('should render null value', async () => { + await testTransform( + null, + null + ); + }); + async function testTransform(input: string, output: string) { expect( await markdownPipe.transform(input) diff --git a/src/app/shared/utils/markdown.pipe.ts b/src/app/shared/utils/markdown.pipe.ts index 7e624c1b52e..d33cdb6c638 100644 --- a/src/app/shared/utils/markdown.pipe.ts +++ b/src/app/shared/utils/markdown.pipe.ts @@ -1,6 +1,8 @@ import { Inject, InjectionToken, Pipe, PipeTransform, SecurityContext } from '@angular/core'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; + import { environment } from '../../../environments/environment'; +import { isEmpty } from '../empty.util'; const markdownItLoader = async () => (await import('markdown-it')).default; type LazyMarkdownIt = ReturnType; @@ -50,7 +52,7 @@ export class MarkdownPipe implements PipeTransform { } async transform(value: string, forcePreview = false): Promise { - if (!environment.markdown.enabled && !forcePreview) { + if (isEmpty(value) || (!environment.markdown.enabled && !forcePreview)) { return value; } const MarkdownIt = await this.markdownIt; From c5e24c0cc97655443279dae803c2ac3b1e3ef01e Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 21 Feb 2024 16:29:48 +0100 Subject: [PATCH 11/24] [UXP-114] Fix test --- src/app/shared/markdown-viewer/markdown-viewer.component.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/shared/markdown-viewer/markdown-viewer.component.spec.ts b/src/app/shared/markdown-viewer/markdown-viewer.component.spec.ts index 0e750e1425b..f8b347f4f77 100644 --- a/src/app/shared/markdown-viewer/markdown-viewer.component.spec.ts +++ b/src/app/shared/markdown-viewer/markdown-viewer.component.spec.ts @@ -18,6 +18,7 @@ describe('DsMarkdownViewerComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(MarkdownViewerComponent); component = fixture.componentInstance; + component.value = 'Test markdown'; fixture.detectChanges(); }); From af8e73ad288c8ef6be51a5e22701b11eba2b2be6 Mon Sep 17 00:00:00 2001 From: Andrea Barbasso <´andrea.barbasso@4science.com´> Date: Mon, 26 Feb 2024 10:45:27 +0100 Subject: [PATCH 12/24] [UXP-114] pre-tests changes --- src/app/item-page/full/full-item-page.component.html | 2 +- src/app/item-page/item-page.module.ts | 3 +++ src/app/shared/utils/markdown.pipe.ts | 2 ++ src/themes/custom/lazy-theme.module.ts | 4 +++- 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/app/item-page/full/full-item-page.component.html b/src/app/item-page/full/full-item-page.component.html index 651fe4ff1c4..9da5d102621 100644 --- a/src/app/item-page/full/full-item-page.component.html +++ b/src/app/item-page/full/full-item-page.component.html @@ -22,7 +22,7 @@ {{mdEntry.key}} - {{mdValue.value}} + {{mdValue.language}} diff --git a/src/app/item-page/item-page.module.ts b/src/app/item-page/item-page.module.ts index c490a41782e..d104852480d 100644 --- a/src/app/item-page/item-page.module.ts +++ b/src/app/item-page/item-page.module.ts @@ -61,6 +61,8 @@ import { ThemedItemAlertsComponent } from './alerts/themed-item-alerts.component import { ThemedFullFileSectionComponent } from './full/field-components/file-section/themed-full-file-section.component'; +import { MarkdownViewerModule } from '../shared/markdown-viewer/markdown-viewer.module'; + const ENTRY_COMPONENTS = [ // put only entry components that use custom decorator @@ -123,6 +125,7 @@ const DECLARATIONS = [ CrisItemPageModule, ContextMenuModule.withEntryComponents(), MiradorViewerModule, + MarkdownViewerModule, ], declarations: [ ...DECLARATIONS, diff --git a/src/app/shared/utils/markdown.pipe.ts b/src/app/shared/utils/markdown.pipe.ts index d33cdb6c638..150ef02e71e 100644 --- a/src/app/shared/utils/markdown.pipe.ts +++ b/src/app/shared/utils/markdown.pipe.ts @@ -63,7 +63,9 @@ export class MarkdownPipe implements PipeTransform { let html: string; if (environment.markdown.mathjax) { + // TODO: instead of using md.use with mathjax, use ng-katex rendering from its service md.use(await this.mathjax); + // TODO: keep this as is const sanitizeHtml = await this.sanitizeHtml; html = sanitizeHtml(md.render(value), { // sanitize-html doesn't let through SVG by default, so we extend its allowlists to cover MathJax SVG diff --git a/src/themes/custom/lazy-theme.module.ts b/src/themes/custom/lazy-theme.module.ts index 7cb35458d8e..445a63aa1b6 100644 --- a/src/themes/custom/lazy-theme.module.ts +++ b/src/themes/custom/lazy-theme.module.ts @@ -170,6 +170,7 @@ import { RequestCopyModule } from 'src/app/request-copy/request-copy.module'; import {UserMenuComponent} from './app/shared/auth-nav-menu/user-menu/user-menu.component'; import { BrowseByComponent } from './app/shared/browse-by/browse-by.component'; import { RegisterEmailFormComponent } from './app/register-email-form/register-email-form.component'; +import { MarkdownViewerModule } from '../../app/shared/markdown-viewer/markdown-viewer.module'; const DECLARATIONS = [ FileSectionComponent, @@ -324,7 +325,8 @@ const DECLARATIONS = [ MiradorViewerModule, FooterModule, ExploreModule, - SocialModule + SocialModule, + MarkdownViewerModule, ], declarations: DECLARATIONS, exports: [ From 00f565e0b0e316b93cf6558dfb68c9a02e43aec7 Mon Sep 17 00:00:00 2001 From: Andrea Barbasso <´andrea.barbasso@4science.com´> Date: Mon, 26 Feb 2024 16:03:12 +0100 Subject: [PATCH 13/24] [UXP-114] working directive need to change markdown pipe to markdown directive and put everything in there --- src/app/core/shared/math.directive.spec.ts | 8 ++ src/app/core/shared/math.directive.ts | 47 ++++++++++ src/app/core/shared/math.service.spec.ts | 16 ++++ src/app/core/shared/math.service.ts | 89 +++++++++++++++++++ .../markdown-viewer.component.html | 1 + src/app/shared/shared.module.ts | 2 + src/app/shared/utils/markdown.pipe.ts | 5 +- src/config/default-app-config.ts | 4 +- 8 files changed, 167 insertions(+), 5 deletions(-) create mode 100644 src/app/core/shared/math.directive.spec.ts create mode 100644 src/app/core/shared/math.directive.ts create mode 100644 src/app/core/shared/math.service.spec.ts create mode 100644 src/app/core/shared/math.service.ts diff --git a/src/app/core/shared/math.directive.spec.ts b/src/app/core/shared/math.directive.spec.ts new file mode 100644 index 00000000000..a8517c87b25 --- /dev/null +++ b/src/app/core/shared/math.directive.spec.ts @@ -0,0 +1,8 @@ +// import { MathDirective } from './math.directive'; + +describe('MathDirective', () => { + it('should create an instance', () => { + // const directive = new MathDirective(); + // expect(directive).toBeTruthy(); + }); +}); diff --git a/src/app/core/shared/math.directive.ts b/src/app/core/shared/math.directive.ts new file mode 100644 index 00000000000..7b1f7c01be8 --- /dev/null +++ b/src/app/core/shared/math.directive.ts @@ -0,0 +1,47 @@ +import { Directive, OnChanges, OnInit, Input, ElementRef, OnDestroy, SimpleChanges } from '@angular/core'; +import { Subject } from 'rxjs'; +import { MathService } from './math.service'; +import { take, takeUntil } from 'rxjs/operators'; + +@Directive({ + selector: '[dsMath]' +}) +export class MathDirective implements OnInit, OnChanges, OnDestroy { + @Input() dsMath: string; + private alive$ = new Subject(); + private readonly el: HTMLElement; + + constructor(private mathService: MathService, private elementRef: ElementRef) { + this.el = elementRef.nativeElement; + } + + ngOnInit() { + this.render(); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes?.dsMath?.currentValue) { + this.render(); + } + } + + private render() { + this.mathService.ready().pipe( + take(1), + takeUntil(this.alive$) + ).subscribe(() => { + // if this.dsMath begins with "The observation of the" + if (this.dsMath.startsWith('The observation of the')) { + console.warn('rendering math after ready'); + console.warn('this.dsMath', this.dsMath); + console.warn('this.el', this.el); + } + this.mathService.render(this.el, this.dsMath); + }); + } + + ngOnDestroy() { + this.alive$.next(false); + } + +} diff --git a/src/app/core/shared/math.service.spec.ts b/src/app/core/shared/math.service.spec.ts new file mode 100644 index 00000000000..aa71ca226b4 --- /dev/null +++ b/src/app/core/shared/math.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { MathService } from './math.service'; + +describe('MathService', () => { + let service: MathService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(MathService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/core/shared/math.service.ts b/src/app/core/shared/math.service.ts new file mode 100644 index 00000000000..c4d32d61d73 --- /dev/null +++ b/src/app/core/shared/math.service.ts @@ -0,0 +1,89 @@ +import { Injectable } from '@angular/core'; +import { Observable, ReplaySubject, Subject } from 'rxjs'; + +interface MathJaxConfig { + source: string; + integrity: string; + id: string; +} + +declare global { + interface Window { + MathJax: any; + } +} + +@Injectable({ + providedIn: 'root' +}) +export class MathService { + + private signal: Subject; + + private mathJaxOptions = { + tex: { + inlineMath: [['$', '$'], ['\\(', '\\)']] + }, + svg: { + fontCache: 'global' + }, + startup: { + typeset: false + } + }; + + private mathJax: MathJaxConfig = { + source: 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js', + integrity: 'sha256-CnzfCXjFj1REmPHgWvm/OQv8gFaxwbLKUi41yCU7N2s=', + id: 'MathJaxScript' + }; + private mathJaxFallback: MathJaxConfig = { + source: 'assets/mathjax/mml-chtml.js', + integrity: 'sha256-CnzfCXjFj1REmPHgWvm/OQv8gFaxwbLKUi41yCU7N2s=', + id: 'MathJaxBackupScript' + }; + + constructor() { + + this.signal = new ReplaySubject(); + + void this.registerMathJaxAsync(this.mathJax) + .then(() => this.signal.next(true)) + .catch(_ => { + void this.registerMathJaxAsync(this.mathJaxFallback) + .then(() => this.signal.next(true)) + .catch((error) => console.log(error)); + }); + } + + private async registerMathJaxAsync(config: MathJaxConfig): Promise { + return new Promise((resolve, reject) => { + const optionsScript: HTMLScriptElement = document.createElement('script'); + optionsScript.type = 'text/javascript'; + optionsScript.text = `MathJax = ${JSON.stringify(this.mathJaxOptions)};`; + document.head.appendChild(optionsScript); + + const script: HTMLScriptElement = document.createElement('script'); + script.id = config.id; + script.type = 'text/javascript'; + script.src = config.source; + script.crossOrigin = 'anonymous'; + script.async = true; + script.onload = () => resolve(); + script.onerror = error => reject(error); + document.head.appendChild(script); + }); + } + + ready(): Observable { + return this.signal; + } + + render(element: HTMLElement, value: string) { + // Take initial typesetting which MathJax performs into account + // window.MathJax.startup.promise.then(() => { + element.innerHTML = value; + window.MathJax.typesetPromise([element]); + // }); + } +} diff --git a/src/app/shared/markdown-viewer/markdown-viewer.component.html b/src/app/shared/markdown-viewer/markdown-viewer.component.html index f6d311a1e66..ecdb3c58d7a 100644 --- a/src/app/shared/markdown-viewer/markdown-viewer.component.html +++ b/src/app/shared/markdown-viewer/markdown-viewer.component.html @@ -1,2 +1,3 @@ + diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 55662d9d9bd..97e2d9ea54e 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -352,6 +352,7 @@ import { import { ItemCollectionComponent } from './object-collection/shared/mydspace-item-collection/item-collection.component'; import { ThemedItemListPreviewComponent } from './object-list/my-dspace-result-list-element/item-list-preview/themed-item-list-preview.component'; import { ItemListPreviewComponent } from './object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component'; +import { MathDirective } from '../core/shared/math.directive'; const MODULES = [ CommonModule, @@ -612,6 +613,7 @@ const DIRECTIVES = [ HoverClassDirective, ContextHelpDirective, EntityIconDirective, + MathDirective ]; @NgModule({ diff --git a/src/app/shared/utils/markdown.pipe.ts b/src/app/shared/utils/markdown.pipe.ts index 150ef02e71e..2868176932e 100644 --- a/src/app/shared/utils/markdown.pipe.ts +++ b/src/app/shared/utils/markdown.pipe.ts @@ -1,5 +1,6 @@ import { Inject, InjectionToken, Pipe, PipeTransform, SecurityContext } from '@angular/core'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; +import { MathService } from 'src/app/core/shared/math.service'; import { environment } from '../../../environments/environment'; import { isEmpty } from '../empty.util'; @@ -48,6 +49,7 @@ export class MarkdownPipe implements PipeTransform { @Inject(MARKDOWN_IT) private markdownIt: LazyMarkdownIt, @Inject(MATHJAX) private mathjax: Mathjax, @Inject(SANITIZE_HTML) private sanitizeHtml: SanitizeHtml, + private mathService: MathService ) { } @@ -63,9 +65,6 @@ export class MarkdownPipe implements PipeTransform { let html: string; if (environment.markdown.mathjax) { - // TODO: instead of using md.use with mathjax, use ng-katex rendering from its service - md.use(await this.mathjax); - // TODO: keep this as is const sanitizeHtml = await this.sanitizeHtml; html = sanitizeHtml(md.render(value), { // sanitize-html doesn't let through SVG by default, so we extend its allowlists to cover MathJax SVG diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts index 947100950e0..103bb6615a8 100644 --- a/src/config/default-app-config.ts +++ b/src/config/default-app-config.ts @@ -507,8 +507,8 @@ export class DefaultAppConfig implements AppConfig { // Whether to enable Markdown (https://commonmark.org/) and MathJax (https://www.mathjax.org/) // display in supported metadata fields. By default, only dc.description.abstract is supported. markdown: MarkdownConfig = { - enabled: false, - mathjax: false, + enabled: true, + mathjax: true, }; // Which vocabularies should be used for which search filters From ba5c05e75cb22f15e7745e1383a64f84e9d47729 Mon Sep 17 00:00:00 2001 From: Andrea Barbasso <´andrea.barbasso@4science.com´> Date: Mon, 26 Feb 2024 17:39:34 +0100 Subject: [PATCH 14/24] [UXP-114] refactor markdownPipe to markdownDirective missing tests and splitting the service into a browser one and a server one --- src/app/core/shared/math.directive.ts | 47 ------- src/app/core/shared/math.service.ts | 18 +-- .../metadata-values.component.html | 2 +- .../metadata-values.component.ts | 2 +- .../item-page-abstract-field.component.ts | 2 +- .../generic-item-page-field.component.ts | 2 +- .../item-page-field.component.ts | 2 +- .../markdown-viewer.component.html | 4 +- src/app/shared/shared.module.ts | 6 +- .../shared/utils/markdown.directive.spec.ts | 8 ++ src/app/shared/utils/markdown.directive.ts | 77 ++++++++++++ src/app/shared/utils/markdown.pipe.spec.ts | 78 ------------ src/app/shared/utils/markdown.pipe.ts | 117 ------------------ src/config/markdown-config.interface.ts | 10 +- 14 files changed, 101 insertions(+), 274 deletions(-) delete mode 100644 src/app/core/shared/math.directive.ts create mode 100644 src/app/shared/utils/markdown.directive.spec.ts create mode 100644 src/app/shared/utils/markdown.directive.ts delete mode 100644 src/app/shared/utils/markdown.pipe.spec.ts delete mode 100644 src/app/shared/utils/markdown.pipe.ts diff --git a/src/app/core/shared/math.directive.ts b/src/app/core/shared/math.directive.ts deleted file mode 100644 index 7b1f7c01be8..00000000000 --- a/src/app/core/shared/math.directive.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Directive, OnChanges, OnInit, Input, ElementRef, OnDestroy, SimpleChanges } from '@angular/core'; -import { Subject } from 'rxjs'; -import { MathService } from './math.service'; -import { take, takeUntil } from 'rxjs/operators'; - -@Directive({ - selector: '[dsMath]' -}) -export class MathDirective implements OnInit, OnChanges, OnDestroy { - @Input() dsMath: string; - private alive$ = new Subject(); - private readonly el: HTMLElement; - - constructor(private mathService: MathService, private elementRef: ElementRef) { - this.el = elementRef.nativeElement; - } - - ngOnInit() { - this.render(); - } - - ngOnChanges(changes: SimpleChanges) { - if (changes?.dsMath?.currentValue) { - this.render(); - } - } - - private render() { - this.mathService.ready().pipe( - take(1), - takeUntil(this.alive$) - ).subscribe(() => { - // if this.dsMath begins with "The observation of the" - if (this.dsMath.startsWith('The observation of the')) { - console.warn('rendering math after ready'); - console.warn('this.dsMath', this.dsMath); - console.warn('this.el', this.el); - } - this.mathService.render(this.el, this.dsMath); - }); - } - - ngOnDestroy() { - this.alive$.next(false); - } - -} diff --git a/src/app/core/shared/math.service.ts b/src/app/core/shared/math.service.ts index c4d32d61d73..5c8b5b90c60 100644 --- a/src/app/core/shared/math.service.ts +++ b/src/app/core/shared/math.service.ts @@ -3,16 +3,9 @@ import { Observable, ReplaySubject, Subject } from 'rxjs'; interface MathJaxConfig { source: string; - integrity: string; id: string; } -declare global { - interface Window { - MathJax: any; - } -} - @Injectable({ providedIn: 'root' }) @@ -34,12 +27,10 @@ export class MathService { private mathJax: MathJaxConfig = { source: 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js', - integrity: 'sha256-CnzfCXjFj1REmPHgWvm/OQv8gFaxwbLKUi41yCU7N2s=', id: 'MathJaxScript' }; private mathJaxFallback: MathJaxConfig = { source: 'assets/mathjax/mml-chtml.js', - integrity: 'sha256-CnzfCXjFj1REmPHgWvm/OQv8gFaxwbLKUi41yCU7N2s=', id: 'MathJaxBackupScript' }; @@ -58,6 +49,7 @@ export class MathService { private async registerMathJaxAsync(config: MathJaxConfig): Promise { return new Promise((resolve, reject) => { + const optionsScript: HTMLScriptElement = document.createElement('script'); optionsScript.type = 'text/javascript'; optionsScript.text = `MathJax = ${JSON.stringify(this.mathJaxOptions)};`; @@ -79,11 +71,7 @@ export class MathService { return this.signal; } - render(element: HTMLElement, value: string) { - // Take initial typesetting which MathJax performs into account - // window.MathJax.startup.promise.then(() => { - element.innerHTML = value; - window.MathJax.typesetPromise([element]); - // }); + render(element: HTMLElement) { + (window as any).MathJax.typesetPromise([element]); } } diff --git a/src/app/item-page/field-components/metadata-values/metadata-values.component.html b/src/app/item-page/field-components/metadata-values/metadata-values.component.html index 61088edd164..4744a21823e 100644 --- a/src/app/item-page/field-components/metadata-values/metadata-values.component.html +++ b/src/app/item-page/field-components/metadata-values/metadata-values.component.html @@ -12,7 +12,7 @@ - + diff --git a/src/app/item-page/field-components/metadata-values/metadata-values.component.ts b/src/app/item-page/field-components/metadata-values/metadata-values.component.ts index cbbae9006da..befed0bc14a 100644 --- a/src/app/item-page/field-components/metadata-values/metadata-values.component.ts +++ b/src/app/item-page/field-components/metadata-values/metadata-values.component.ts @@ -37,7 +37,7 @@ export class MetadataValuesComponent implements OnChanges { @Input() label: string; /** - * Whether the {@link MarkdownPipe} should be used to render these metadata values. + * Whether the {@link MarkdownDirective} should be used to render these metadata values. * This will only have effect if {@link MarkdownConfig#enabled} is true. * Mathjax will only be rendered if {@link MarkdownConfig#mathjax} is true. */ diff --git a/src/app/item-page/simple/field-components/specific-field/abstract/item-page-abstract-field.component.ts b/src/app/item-page/simple/field-components/specific-field/abstract/item-page-abstract-field.component.ts index f29bef31a76..1e85ab743be 100644 --- a/src/app/item-page/simple/field-components/specific-field/abstract/item-page-abstract-field.component.ts +++ b/src/app/item-page/simple/field-components/specific-field/abstract/item-page-abstract-field.component.ts @@ -37,7 +37,7 @@ export class ItemPageAbstractFieldComponent extends ItemPageFieldComponent { label = 'item.page.abstract'; /** - * Use the {@link MarkdownPipe} to render dc.description.abstract values + * Use the {@link MarkdownDirective} to render dc.description.abstract values */ enableMarkdown = true; } diff --git a/src/app/item-page/simple/field-components/specific-field/generic/generic-item-page-field.component.ts b/src/app/item-page/simple/field-components/specific-field/generic/generic-item-page-field.component.ts index 53d2f6aa20d..5e32fa62f4b 100644 --- a/src/app/item-page/simple/field-components/specific-field/generic/generic-item-page-field.component.ts +++ b/src/app/item-page/simple/field-components/specific-field/generic/generic-item-page-field.component.ts @@ -36,7 +36,7 @@ export class GenericItemPageFieldComponent extends ItemPageFieldComponent { @Input() label: string; /** - * Whether the {@link MarkdownPipe} should be used to render this metadata. + * Whether the {@link MarkdownDirective} should be used to render this metadata. */ @Input() enableMarkdown = false; diff --git a/src/app/item-page/simple/field-components/specific-field/item-page-field.component.ts b/src/app/item-page/simple/field-components/specific-field/item-page-field.component.ts index fc526dabcc5..a715dd97250 100644 --- a/src/app/item-page/simple/field-components/specific-field/item-page-field.component.ts +++ b/src/app/item-page/simple/field-components/specific-field/item-page-field.component.ts @@ -26,7 +26,7 @@ export class ItemPageFieldComponent { @Input() item: Item; /** - * Whether the {@link MarkdownPipe} should be used to render this metadata. + * Whether the {@link MarkdownDirective} should be used to render this metadata. */ enableMarkdown = false; diff --git a/src/app/shared/markdown-viewer/markdown-viewer.component.html b/src/app/shared/markdown-viewer/markdown-viewer.component.html index ecdb3c58d7a..d5ca0a636e3 100644 --- a/src/app/shared/markdown-viewer/markdown-viewer.component.html +++ b/src/app/shared/markdown-viewer/markdown-viewer.component.html @@ -1,3 +1 @@ - - - + diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 97e2d9ea54e..e4e88c3c9ea 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -300,7 +300,6 @@ import { SearchExportCsvComponent } from './search/search-export-csv/search-expo import { ItemPageTitleFieldComponent } from '../item-page/simple/field-components/specific-field/title/item-page-title-field.component'; -import { MarkdownPipe } from './utils/markdown.pipe'; import { GoogleRecaptchaModule } from '../core/google-recaptcha/google-recaptcha.module'; import { MenuModule } from './menu/menu.module'; import { @@ -352,7 +351,7 @@ import { import { ItemCollectionComponent } from './object-collection/shared/mydspace-item-collection/item-collection.component'; import { ThemedItemListPreviewComponent } from './object-list/my-dspace-result-list-element/item-list-preview/themed-item-list-preview.component'; import { ItemListPreviewComponent } from './object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component'; -import { MathDirective } from '../core/shared/math.directive'; +import { MarkdownDirective } from './utils/markdown.directive'; const MODULES = [ CommonModule, @@ -399,7 +398,6 @@ const PIPES = [ ConsolePipe, ObjNgFor, BrowserOnlyPipe, - MarkdownPipe, ShortNumberPipe ]; @@ -613,7 +611,7 @@ const DIRECTIVES = [ HoverClassDirective, ContextHelpDirective, EntityIconDirective, - MathDirective + MarkdownDirective, ]; @NgModule({ diff --git a/src/app/shared/utils/markdown.directive.spec.ts b/src/app/shared/utils/markdown.directive.spec.ts new file mode 100644 index 00000000000..621fdd45b62 --- /dev/null +++ b/src/app/shared/utils/markdown.directive.spec.ts @@ -0,0 +1,8 @@ +import { MarkdownDirective } from './markdown.directive'; + +describe('MarkdownDirective', () => { + it('should create an instance', () => { + const directive = new MarkdownDirective(); + expect(directive).toBeTruthy(); + }); +}); diff --git a/src/app/shared/utils/markdown.directive.ts b/src/app/shared/utils/markdown.directive.ts new file mode 100644 index 00000000000..3b85eebe0fd --- /dev/null +++ b/src/app/shared/utils/markdown.directive.ts @@ -0,0 +1,77 @@ +import { + Directive, + ElementRef, + Inject, + InjectionToken, + Input, + OnDestroy, + OnInit, + SecurityContext +} from '@angular/core'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; +import { MathService } from '../../core/shared/math.service'; +import { isEmpty } from '../empty.util'; +import { environment } from '../../../environments/environment'; +import { Subject } from 'rxjs'; +import { take, takeUntil } from 'rxjs/operators'; + +const markdownItLoader = async () => (await import('markdown-it')).default; +type LazyMarkdownIt = ReturnType; +const MARKDOWN_IT = new InjectionToken( + 'Lazily loaded MarkdownIt', + {providedIn: 'root', factory: markdownItLoader} +); + +@Directive({ + selector: '[dsMarkdown]' +}) +export class MarkdownDirective implements OnInit, OnDestroy { + + @Input() dsMarkdown: string; + private alive$ = new Subject(); + + el: HTMLElement; + + constructor( + protected sanitizer: DomSanitizer, + @Inject(MARKDOWN_IT) private markdownIt: LazyMarkdownIt, + private mathService: MathService, + private elementRef: ElementRef) { + this.el = elementRef.nativeElement; + } + + ngOnInit() { + this.render(this.dsMarkdown); + } + + async render(value: string, forcePreview = false): Promise { + if (isEmpty(value) || (!environment.markdown.enabled && !forcePreview)) { + return value; + } + const MarkdownIt = await this.markdownIt; + const md = new MarkdownIt({ + html: true, + linkify: true, + }); + + const html = this.sanitizer.sanitize(SecurityContext.HTML, md.render(value)); + this.el.innerHTML = html; + + if (environment.markdown.mathjax) { + this.renderMathjax(); + } + } + + private renderMathjax() { + this.mathService.ready().pipe( + take(1), + takeUntil(this.alive$) + ).subscribe(() => { + this.mathService.render(this.el); + }); + } + + ngOnDestroy() { + this.alive$.next(false); + } +} diff --git a/src/app/shared/utils/markdown.pipe.spec.ts b/src/app/shared/utils/markdown.pipe.spec.ts deleted file mode 100644 index f8f147dc890..00000000000 --- a/src/app/shared/utils/markdown.pipe.spec.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { MarkdownPipe } from './markdown.pipe'; -import { TestBed } from '@angular/core/testing'; -import { APP_CONFIG } from '../../../config/app-config.interface'; -import { environment } from '../../../environments/environment'; - -describe('Markdown Pipe', () => { - - let markdownPipe: MarkdownPipe; - - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [ - MarkdownPipe, - { - provide: APP_CONFIG, - useValue: Object.assign(environment, { - markdown: { - enabled: true, - mathjax: true, - } - }) - }, - ], - }).compileComponents(); - - markdownPipe = TestBed.inject(MarkdownPipe); - }); - - it('should render markdown', async () => { - await testTransform( - '# Header', - '

Header

' - ); - }); - - it('should render mathjax', async () => { - await testTransform( - '$\\sqrt{2}^2$', - '.*' - ); - }); - - it('should render regular links', async () => { - await testTransform( - 'DSpace', - 'DSpace' - ); - }); - - it('should not render javascript links', async () => { - await testTransform( - 'exploit', - 'exploit' - ); - }); - - it('should render undefined value', async () => { - await testTransform( - undefined, - undefined - ); - }); - - it('should render null value', async () => { - await testTransform( - null, - null - ); - }); - - async function testTransform(input: string, output: string) { - expect( - await markdownPipe.transform(input) - ).toMatch( - new RegExp('.*' + output + '.*') - ); - } -}); diff --git a/src/app/shared/utils/markdown.pipe.ts b/src/app/shared/utils/markdown.pipe.ts deleted file mode 100644 index 2868176932e..00000000000 --- a/src/app/shared/utils/markdown.pipe.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { Inject, InjectionToken, Pipe, PipeTransform, SecurityContext } from '@angular/core'; -import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; -import { MathService } from 'src/app/core/shared/math.service'; - -import { environment } from '../../../environments/environment'; -import { isEmpty } from '../empty.util'; - -const markdownItLoader = async () => (await import('markdown-it')).default; -type LazyMarkdownIt = ReturnType; -const MARKDOWN_IT = new InjectionToken( - 'Lazily loaded MarkdownIt', - { providedIn: 'root', factory: markdownItLoader } -); - -const mathjaxLoader = async () => (await import('markdown-it-mathjax3')).default; -type Mathjax = ReturnType; -const MATHJAX = new InjectionToken( - 'Lazily loaded mathjax', - { providedIn: 'root', factory: mathjaxLoader } -); - -const sanitizeHtmlLoader = async () => (await import('sanitize-html') as any).default; -type SanitizeHtml = ReturnType; -const SANITIZE_HTML = new InjectionToken( - 'Lazily loaded sanitize-html', - { providedIn: 'root', factory: sanitizeHtmlLoader } -); - -/** - * Pipe for rendering markdown and mathjax. - * - markdown will only be rendered if {@link MarkdownConfig#enabled} is true - * - mathjax will only be rendered if both {@link MarkdownConfig#enabled} and {@link MarkdownConfig#mathjax} are true - * - * This pipe should be used on the 'innerHTML' attribute of a component, in combination with an async pipe. - * Example usage: - * - * Result: - * - *

title

- *
- */ -@Pipe({ - name: 'dsMarkdown' -}) -export class MarkdownPipe implements PipeTransform { - - constructor( - protected sanitizer: DomSanitizer, - @Inject(MARKDOWN_IT) private markdownIt: LazyMarkdownIt, - @Inject(MATHJAX) private mathjax: Mathjax, - @Inject(SANITIZE_HTML) private sanitizeHtml: SanitizeHtml, - private mathService: MathService - ) { - } - - async transform(value: string, forcePreview = false): Promise { - if (isEmpty(value) || (!environment.markdown.enabled && !forcePreview)) { - return value; - } - const MarkdownIt = await this.markdownIt; - const md = new MarkdownIt({ - html: true, - linkify: true, - }); - - let html: string; - if (environment.markdown.mathjax) { - const sanitizeHtml = await this.sanitizeHtml; - html = sanitizeHtml(md.render(value), { - // sanitize-html doesn't let through SVG by default, so we extend its allowlists to cover MathJax SVG - allowedTags: [ - ...sanitizeHtml.defaults.allowedTags, - 'mjx-container', 'svg', 'g', 'path', 'rect', 'text', - // Also let the mjx-assistive-mml tag (and it's children) through (for screen readers) - 'mjx-assistive-mml', 'math', 'mrow', 'mi', - ], - allowedAttributes: { - ...sanitizeHtml.defaults.allowedAttributes, - 'mjx-container': [ - 'class', 'style', 'jax' - ], - svg: [ - 'xmlns', 'viewBox', 'style', 'width', 'height', 'role', 'focusable', 'alt', 'aria-label' - ], - g: [ - 'data-mml-node', 'style', 'stroke', 'fill', 'stroke-width', 'transform' - ], - path: [ - 'd', 'style', 'transform' - ], - rect: [ - 'width', 'height', 'x', 'y', 'transform', 'style' - ], - text: [ - 'transform', 'font-size' - ], - 'mjx-assistive-mml': [ - 'unselectable', 'display', 'style', - ], - math: [ - 'xmlns', - ], - mrow: [ - 'data-mjx-texclass', - ], - }, - parser: { - lowerCaseAttributeNames: false, - }, - }); - } else { - html = this.sanitizer.sanitize(SecurityContext.HTML, md.render(value)); - } - - return this.sanitizer.bypassSecurityTrustHtml(html); - } -} diff --git a/src/config/markdown-config.interface.ts b/src/config/markdown-config.interface.ts index 6ba40f50fb3..71c5e45547b 100644 --- a/src/config/markdown-config.interface.ts +++ b/src/config/markdown-config.interface.ts @@ -1,20 +1,20 @@ import { Config } from './config.interface'; /** - * Config related to the {@link MarkdownPipe}. + * Config related to the {@link MarkdownDirective}. */ export interface MarkdownConfig extends Config { /** - * Enable Markdown (https://commonmark.org/) syntax for values passed to the {@link MarkdownPipe}. - * - If this is true, values passed to the MarkdownPipe will be transformed to html according to the markdown syntax + * Enable Markdown (https://commonmark.org/) syntax for values passed to the {@link MarkdownDirective}. + * - If this is true, values passed to the MarkdownDirective will be transformed to html according to the markdown syntax * rules. - * - If this is false, using the MarkdownPipe will have no effect. + * - If this is false, using the MarkdownDirective will have no effect. */ enabled: boolean; /** - * Enable MathJax (https://www.mathjax.org/) syntax for values passed to the {@link MarkdownPipe}. + * Enable MathJax (https://www.mathjax.org/) syntax for values passed to the {@link MarkdownDirective}. * Requires {@link enabled} to also be true before MathJax will display. */ mathjax: boolean; From c90651f1eda03e4c92d0db0b93077fef00e95088 Mon Sep 17 00:00:00 2001 From: Andrea Barbasso <´andrea.barbasso@4science.com´> Date: Tue, 27 Feb 2024 10:21:42 +0100 Subject: [PATCH 15/24] [UXP-114] split service into SSR and CSR --- src/app/core/shared/client-math.service.ts | 80 ++++++++++++++++++++++ src/app/core/shared/math.service.ts | 79 +++------------------ src/app/core/shared/server-math.service.ts | 41 +++++++++++ src/modules/app/browser-app.module.ts | 6 ++ src/modules/app/server-app.module.ts | 6 ++ 5 files changed, 142 insertions(+), 70 deletions(-) create mode 100644 src/app/core/shared/client-math.service.ts create mode 100644 src/app/core/shared/server-math.service.ts diff --git a/src/app/core/shared/client-math.service.ts b/src/app/core/shared/client-math.service.ts new file mode 100644 index 00000000000..3791a3a8d79 --- /dev/null +++ b/src/app/core/shared/client-math.service.ts @@ -0,0 +1,80 @@ +import { Injectable } from '@angular/core'; +import { Observable, ReplaySubject, Subject } from 'rxjs'; +import { environment } from 'src/environments/environment'; +import { MathJaxConfig, MathService } from './math.service'; + +@Injectable({ + providedIn: 'root' +}) +export class ClientMathService extends MathService { + + protected signal: Subject; + + protected mathJaxOptions = { + tex: { + inlineMath: [['$', '$'], ['\\(', '\\)']] + }, + svg: { + fontCache: 'global' + }, + startup: { + typeset: false + } + }; + + protected mathJax: MathJaxConfig = { + source: 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js', + id: 'MathJaxScript' + }; + protected mathJaxFallback: MathJaxConfig = { + source: 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.2.2/es5/tex-chtml.min.js', + id: 'MathJaxBackupScript' + }; + + constructor() { + super(); + + this.signal = new ReplaySubject(); + + void this.registerMathJaxAsync(this.mathJax) + .then(() => this.signal.next(true)) + .catch(_ => { + void this.registerMathJaxAsync(this.mathJaxFallback) + .then(() => this.signal.next(true)) + .catch((error) => console.log(error)); + }); + } + + protected async registerMathJaxAsync(config: MathJaxConfig): Promise { + if (environment.markdown.mathjax) { + return new Promise((resolve, reject) => { + + const optionsScript: HTMLScriptElement = document.createElement('script'); + optionsScript.type = 'text/javascript'; + optionsScript.text = `MathJax = ${JSON.stringify(this.mathJaxOptions)};`; + document.head.appendChild(optionsScript); + + const script: HTMLScriptElement = document.createElement('script'); + script.id = config.id; + script.type = 'text/javascript'; + script.src = config.source; + script.crossOrigin = 'anonymous'; + script.async = true; + script.onload = () => resolve(); + script.onerror = error => reject(error); + document.head.appendChild(script); + }); + } + return Promise.resolve(); + } + + ready(): Observable { + return this.signal; + } + + render(element: HTMLElement) { + if (environment.markdown.mathjax) { + (window as any).MathJax.typesetPromise([element]); + } + } +} diff --git a/src/app/core/shared/math.service.ts b/src/app/core/shared/math.service.ts index 5c8b5b90c60..1d5271f7a7b 100644 --- a/src/app/core/shared/math.service.ts +++ b/src/app/core/shared/math.service.ts @@ -1,77 +1,16 @@ -import { Injectable } from '@angular/core'; -import { Observable, ReplaySubject, Subject } from 'rxjs'; +import { Observable } from 'rxjs'; -interface MathJaxConfig { +export interface MathJaxConfig { source: string; id: string; } -@Injectable({ - providedIn: 'root' -}) -export class MathService { +export abstract class MathService { + protected abstract mathJaxOptions: any; + protected abstract mathJax: MathJaxConfig; + protected abstract mathJaxFallback: MathJaxConfig; - private signal: Subject; - - private mathJaxOptions = { - tex: { - inlineMath: [['$', '$'], ['\\(', '\\)']] - }, - svg: { - fontCache: 'global' - }, - startup: { - typeset: false - } - }; - - private mathJax: MathJaxConfig = { - source: 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js', - id: 'MathJaxScript' - }; - private mathJaxFallback: MathJaxConfig = { - source: 'assets/mathjax/mml-chtml.js', - id: 'MathJaxBackupScript' - }; - - constructor() { - - this.signal = new ReplaySubject(); - - void this.registerMathJaxAsync(this.mathJax) - .then(() => this.signal.next(true)) - .catch(_ => { - void this.registerMathJaxAsync(this.mathJaxFallback) - .then(() => this.signal.next(true)) - .catch((error) => console.log(error)); - }); - } - - private async registerMathJaxAsync(config: MathJaxConfig): Promise { - return new Promise((resolve, reject) => { - - const optionsScript: HTMLScriptElement = document.createElement('script'); - optionsScript.type = 'text/javascript'; - optionsScript.text = `MathJax = ${JSON.stringify(this.mathJaxOptions)};`; - document.head.appendChild(optionsScript); - - const script: HTMLScriptElement = document.createElement('script'); - script.id = config.id; - script.type = 'text/javascript'; - script.src = config.source; - script.crossOrigin = 'anonymous'; - script.async = true; - script.onload = () => resolve(); - script.onerror = error => reject(error); - document.head.appendChild(script); - }); - } - - ready(): Observable { - return this.signal; - } - - render(element: HTMLElement) { - (window as any).MathJax.typesetPromise([element]); - } + protected abstract registerMathJaxAsync(config: MathJaxConfig): Promise; + abstract ready(): Observable; + abstract render(element: HTMLElement): void; } diff --git a/src/app/core/shared/server-math.service.ts b/src/app/core/shared/server-math.service.ts new file mode 100644 index 00000000000..cfddadf114f --- /dev/null +++ b/src/app/core/shared/server-math.service.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@angular/core'; +import { Observable, ReplaySubject, Subject } from 'rxjs'; +import { MathJaxConfig, MathService } from './math.service'; + +@Injectable({ + providedIn: 'root' +}) +export class ServerMathService extends MathService { + + protected signal: Subject; + + protected mathJaxOptions = {}; + + protected mathJax: MathJaxConfig = { + source: '', + id: '' + }; + protected mathJaxFallback: MathJaxConfig = { + source: '', + id: '' + }; + + constructor() { + super(); + + this.signal = new ReplaySubject(); + this.signal.next(true); + } + + protected async registerMathJaxAsync(config: MathJaxConfig): Promise { + return Promise.resolve(); + } + + ready(): Observable { + return this.signal; + } + + render(element: HTMLElement) { + return; + } +} diff --git a/src/modules/app/browser-app.module.ts b/src/modules/app/browser-app.module.ts index d6295cf791d..c9ab093c8c0 100644 --- a/src/modules/app/browser-app.module.ts +++ b/src/modules/app/browser-app.module.ts @@ -33,6 +33,8 @@ import { BrowserAuthRequestService } from '../../app/core/auth/browser-auth-requ import { BrowserInitService } from './browser-init.service'; import { ReferrerService } from '../../app/core/services/referrer.service'; import { BrowserReferrerService } from '../../app/core/services/browser.referrer.service'; +import { MathService } from '../../app/core/shared/math.service'; +import { ClientMathService } from '../../app/core/shared/client-math.service'; export const REQ_KEY = makeStateKey('req'); @@ -116,6 +118,10 @@ export function getRequest(transferState: TransferState): any { { provide: LocationToken, useFactory: locationProvider, + }, + { + provide: MathService, + useClass: ClientMathService } ] }) diff --git a/src/modules/app/server-app.module.ts b/src/modules/app/server-app.module.ts index b3d718b0b2b..45bc4e05532 100644 --- a/src/modules/app/server-app.module.ts +++ b/src/modules/app/server-app.module.ts @@ -37,6 +37,8 @@ import { XhrFactory } from '@angular/common'; import { ServerXhrService } from '../../app/core/services/server-xhr.service'; import { ReferrerService } from '../../app/core/services/referrer.service'; import { ServerReferrerService } from '../../app/core/services/server.referrer.service'; +import { MathService } from '../../app/core/shared/math.service'; +import { ServerMathService } from '../../app/core/shared/server-math.service'; export function createTranslateLoader(transferState: TransferState) { return new TranslateServerLoader(transferState, 'dist/server/assets/i18n/', '.json'); @@ -116,6 +118,10 @@ export function createTranslateLoader(transferState: TransferState) { provide: ReferrerService, useClass: ServerReferrerService, }, + { + provide: MathService, + useClass: ServerMathService + } ] }) export class ServerAppModule { From 20e1f9701191146dcc54a6845d125812594d992a Mon Sep 17 00:00:00 2001 From: Andrea Barbasso <´andrea.barbasso@4science.com´> Date: Tue, 27 Feb 2024 10:23:56 +0100 Subject: [PATCH 16/24] [UXP-114] refactor variable --- src/app/core/shared/client-math.service.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/app/core/shared/client-math.service.ts b/src/app/core/shared/client-math.service.ts index 3791a3a8d79..e25988a11f1 100644 --- a/src/app/core/shared/client-math.service.ts +++ b/src/app/core/shared/client-math.service.ts @@ -8,7 +8,7 @@ import { MathJaxConfig, MathService } from './math.service'; }) export class ClientMathService extends MathService { - protected signal: Subject; + protected isReady$: Subject; protected mathJaxOptions = { tex: { @@ -34,14 +34,13 @@ export class ClientMathService extends MathService { constructor() { super(); - this.signal = new ReplaySubject(); + this.isReady$ = new ReplaySubject(); void this.registerMathJaxAsync(this.mathJax) - .then(() => this.signal.next(true)) + .then(() => this.isReady$.next(true)) .catch(_ => { void this.registerMathJaxAsync(this.mathJaxFallback) - .then(() => this.signal.next(true)) - .catch((error) => console.log(error)); + .then(() => this.isReady$.next(true)); }); } @@ -69,7 +68,7 @@ export class ClientMathService extends MathService { } ready(): Observable { - return this.signal; + return this.isReady$; } render(element: HTMLElement) { From 8272ced21f975fb444ff20d8c68957b81fad40d3 Mon Sep 17 00:00:00 2001 From: Andrea Barbasso <´andrea.barbasso@4science.com´> Date: Tue, 27 Feb 2024 12:17:47 +0100 Subject: [PATCH 17/24] [UXP-114] add and fix tests --- src/app/core/shared/math.directive.spec.ts | 8 ---- src/app/core/shared/math.service.spec.ts | 37 +++++++++++++++++-- .../full/full-item-page.component.html | 2 +- .../item-page-field.component.spec.ts | 8 ++-- .../markdown-viewer.component.spec.ts | 9 ++++- .../shared/utils/markdown.directive.spec.ts | 33 +++++++++++++++-- 6 files changed, 77 insertions(+), 20 deletions(-) delete mode 100644 src/app/core/shared/math.directive.spec.ts diff --git a/src/app/core/shared/math.directive.spec.ts b/src/app/core/shared/math.directive.spec.ts deleted file mode 100644 index a8517c87b25..00000000000 --- a/src/app/core/shared/math.directive.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -// import { MathDirective } from './math.directive'; - -describe('MathDirective', () => { - it('should create an instance', () => { - // const directive = new MathDirective(); - // expect(directive).toBeTruthy(); - }); -}); diff --git a/src/app/core/shared/math.service.spec.ts b/src/app/core/shared/math.service.spec.ts index aa71ca226b4..aa632272bd2 100644 --- a/src/app/core/shared/math.service.spec.ts +++ b/src/app/core/shared/math.service.spec.ts @@ -1,16 +1,47 @@ import { TestBed } from '@angular/core/testing'; +import { Observable, of } from 'rxjs'; +import { MathService, MathJaxConfig } from './math.service'; -import { MathService } from './math.service'; +export class MockMathService extends MathService { + protected mathJaxOptions: any = {}; + protected mathJax: MathJaxConfig = { source: '', id: '' }; + protected mathJaxFallback: MathJaxConfig = { source: '', id: '' }; + + protected registerMathJaxAsync(config: MathJaxConfig): Promise { + return Promise.resolve(); + } + + ready(): Observable { + return of(true); + } + + render(element: HTMLElement): void { + return; + } +} describe('MathService', () => { - let service: MathService; + let service: MockMathService; beforeEach(() => { TestBed.configureTestingModule({}); - service = TestBed.inject(MathService); + service = new MockMathService(); + spyOn(service, 'render'); }); it('should be created', () => { expect(service).toBeTruthy(); }); + + it('should be ready', (done) => { + service.ready().subscribe(isReady => { + expect(isReady).toBe(true); + done(); + }); + }); + + it('should render', () => { + service.render(document.createElement('div')); + expect(service.render).toHaveBeenCalled(); + }); }); diff --git a/src/app/item-page/full/full-item-page.component.html b/src/app/item-page/full/full-item-page.component.html index 9da5d102621..651fe4ff1c4 100644 --- a/src/app/item-page/full/full-item-page.component.html +++ b/src/app/item-page/full/full-item-page.component.html @@ -22,7 +22,7 @@ {{mdEntry.key}} - + {{mdValue.value}} {{mdValue.language}} diff --git a/src/app/item-page/simple/field-components/specific-field/item-page-field.component.spec.ts b/src/app/item-page/simple/field-components/specific-field/item-page-field.component.spec.ts index 15b7a9df212..f3afdcb43d4 100644 --- a/src/app/item-page/simple/field-components/specific-field/item-page-field.component.spec.ts +++ b/src/app/item-page/simple/field-components/specific-field/item-page-field.component.spec.ts @@ -9,13 +9,14 @@ import { MetadataMap, MetadataValue } from '../../../../core/shared/metadata.mod import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; import { createPaginatedList } from '../../../../shared/testing/utils.test'; import { environment } from '../../../../../environments/environment'; -import { MarkdownPipe } from '../../../../shared/utils/markdown.pipe'; import { SharedModule } from '../../../../shared/shared.module'; import { APP_CONFIG } from '../../../../../config/app-config.interface'; import { By } from '@angular/platform-browser'; import { BrowseDefinitionDataService } from '../../../../core/browse/browse-definition-data.service'; import { BrowseDefinitionDataServiceStub } from '../../../../shared/testing/browse-definition-data-service.stub'; import { RouterTestingModule } from '@angular/router/testing'; +import { MarkdownDirective } from '../../../../shared/utils/markdown.directive'; +import { MathService } from '../../../../core/shared/math.service'; let comp: ItemPageFieldComponent; let fixture: ComponentFixture; @@ -51,14 +52,15 @@ describe('ItemPageFieldComponent', () => { ], providers: [ { provide: APP_CONFIG, useValue: appConfig }, - { provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub } + { provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub }, + { provide: MathService, useValue: {} } ], declarations: [ItemPageFieldComponent, MetadataValuesComponent], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(ItemPageFieldComponent, { set: { changeDetection: ChangeDetectionStrategy.Default } }).compileComponents(); - markdownSpy = spyOn(MarkdownPipe.prototype, 'transform'); + markdownSpy = spyOn(MarkdownDirective.prototype, 'render'); fixture = TestBed.createComponent(ItemPageFieldComponent); comp = fixture.componentInstance; comp.item = mockItemWithMetadataFieldsAndValue(mockFields, mockValue); diff --git a/src/app/shared/markdown-viewer/markdown-viewer.component.spec.ts b/src/app/shared/markdown-viewer/markdown-viewer.component.spec.ts index f8b347f4f77..0e72a31ef0f 100644 --- a/src/app/shared/markdown-viewer/markdown-viewer.component.spec.ts +++ b/src/app/shared/markdown-viewer/markdown-viewer.component.spec.ts @@ -1,8 +1,9 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MarkdownViewerComponent } from './markdown-viewer.component'; -import { MarkdownPipe } from '../utils/markdown.pipe'; import { By } from '@angular/platform-browser'; +import { MarkdownDirective } from '../utils/markdown.directive'; +import { MathService } from '../../core/shared/math.service'; describe('DsMarkdownViewerComponent', () => { let component: MarkdownViewerComponent; @@ -10,7 +11,11 @@ describe('DsMarkdownViewerComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ MarkdownViewerComponent, MarkdownPipe ] + declarations: [ MarkdownViewerComponent, MarkdownDirective ], + providers: [{ + provide: MathService, + useValue: {} + } ] }) .compileComponents(); }); diff --git a/src/app/shared/utils/markdown.directive.spec.ts b/src/app/shared/utils/markdown.directive.spec.ts index 621fdd45b62..d06d148d27b 100644 --- a/src/app/shared/utils/markdown.directive.spec.ts +++ b/src/app/shared/utils/markdown.directive.spec.ts @@ -1,8 +1,35 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MarkdownDirective } from './markdown.directive'; +import { Component, DebugElement } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { MathService } from '../../core/shared/math.service'; +import { MockMathService } from '../../core/shared/math.service.spec'; + +@Component({ + template: `
` +}) +class TestComponent {} describe('MarkdownDirective', () => { - it('should create an instance', () => { - const directive = new MarkdownDirective(); - expect(directive).toBeTruthy(); + let component: TestComponent; + let fixture: ComponentFixture; + let divEl: DebugElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ TestComponent, MarkdownDirective ], + providers: [ + { provide: MathService, useClass: MockMathService }, + ] + }).compileComponents(); + spyOn(MarkdownDirective.prototype, 'render'); + fixture = TestBed.createComponent(TestComponent); + component = fixture.componentInstance; + divEl = fixture.debugElement.query(By.css('div')); + }); + + it('should call render method', () => { + fixture.detectChanges(); + expect(MarkdownDirective.prototype.render).toHaveBeenCalled(); }); }); From e8e77f59da1dd49076e9d59b620fc6e78fb7c244 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Tue, 27 Feb 2024 20:52:57 +0100 Subject: [PATCH 18/24] [UXP-114] Fix issue when markdown is disabled --- src/app/shared/testing/math-service.stub.ts | 6 ++++++ src/app/shared/utils/markdown.directive.ts | 24 +++++++++++---------- 2 files changed, 19 insertions(+), 11 deletions(-) create mode 100644 src/app/shared/testing/math-service.stub.ts diff --git a/src/app/shared/testing/math-service.stub.ts b/src/app/shared/testing/math-service.stub.ts new file mode 100644 index 00000000000..0a5ca4843c8 --- /dev/null +++ b/src/app/shared/testing/math-service.stub.ts @@ -0,0 +1,6 @@ +import { of as observableOf } from 'rxjs'; + +export const MathServiceMock = jasmine.createSpyObj('MathService', { + ready: jasmine.createSpy('ready').and.returnValue(observableOf(true)), + render: jasmine.createSpy('render'), +}); diff --git a/src/app/shared/utils/markdown.directive.ts b/src/app/shared/utils/markdown.directive.ts index 3b85eebe0fd..a9a59125de4 100644 --- a/src/app/shared/utils/markdown.directive.ts +++ b/src/app/shared/utils/markdown.directive.ts @@ -46,19 +46,21 @@ export class MarkdownDirective implements OnInit, OnDestroy { async render(value: string, forcePreview = false): Promise { if (isEmpty(value) || (!environment.markdown.enabled && !forcePreview)) { - return value; - } - const MarkdownIt = await this.markdownIt; - const md = new MarkdownIt({ - html: true, - linkify: true, - }); + this.el.innerHTML = value; + return; + } else { + const MarkdownIt = await this.markdownIt; + const md = new MarkdownIt({ + html: true, + linkify: true, + }); - const html = this.sanitizer.sanitize(SecurityContext.HTML, md.render(value)); - this.el.innerHTML = html; + const html = this.sanitizer.sanitize(SecurityContext.HTML, md.render(value)); + this.el.innerHTML = html; - if (environment.markdown.mathjax) { - this.renderMathjax(); + if (environment.markdown.mathjax) { + this.renderMathjax(); + } } } From b46a87bd38b4aa5fc7f5479bba211b058e92cea4 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 21 Mar 2024 09:46:14 +0100 Subject: [PATCH 19/24] [DURACOM-240] Assure the MathJax script has been registered before rendering --- src/app/core/shared/client-math.service.ts | 42 +++++++++++++++++----- src/app/core/shared/math.service.ts | 3 ++ src/app/core/shared/server-math.service.ts | 3 ++ src/app/shared/utils/markdown.directive.ts | 19 +++++++--- 4 files changed, 54 insertions(+), 13 deletions(-) diff --git a/src/app/core/shared/client-math.service.ts b/src/app/core/shared/client-math.service.ts index e25988a11f1..9c7ab784c9f 100644 --- a/src/app/core/shared/client-math.service.ts +++ b/src/app/core/shared/client-math.service.ts @@ -1,11 +1,21 @@ -import { Injectable } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { + Inject, + Injectable, +} from '@angular/core'; import { Observable, ReplaySubject, Subject } from 'rxjs'; import { environment } from 'src/environments/environment'; -import { MathJaxConfig, MathService } from './math.service'; +import { + NativeWindowRef, + NativeWindowService, +} from '../services/window.service';import { MathJaxConfig, MathService } from './math.service'; @Injectable({ providedIn: 'root' }) +/** + * Provide the MathService for CSR + */ export class ClientMathService extends MathService { protected isReady$: Subject; @@ -31,7 +41,10 @@ export class ClientMathService extends MathService { id: 'MathJaxBackupScript' }; - constructor() { + constructor( + @Inject(DOCUMENT) private _document: Document, + @Inject(NativeWindowService) protected _window: NativeWindowRef, + ) { super(); this.isReady$ = new ReplaySubject(); @@ -44,16 +57,21 @@ export class ClientMathService extends MathService { }); } + /** + * Register the specified MathJax script in the document + * + * @param config The configuration object for the script + */ protected async registerMathJaxAsync(config: MathJaxConfig): Promise { if (environment.markdown.mathjax) { return new Promise((resolve, reject) => { - const optionsScript: HTMLScriptElement = document.createElement('script'); + const optionsScript: HTMLScriptElement = this._document.createElement('script'); optionsScript.type = 'text/javascript'; optionsScript.text = `MathJax = ${JSON.stringify(this.mathJaxOptions)};`; - document.head.appendChild(optionsScript); + this._document.head.appendChild(optionsScript); - const script: HTMLScriptElement = document.createElement('script'); + const script: HTMLScriptElement = this._document.createElement('script'); script.id = config.id; script.type = 'text/javascript'; script.src = config.source; @@ -61,19 +79,27 @@ export class ClientMathService extends MathService { script.async = true; script.onload = () => resolve(); script.onerror = error => reject(error); - document.head.appendChild(script); + this._document.head.appendChild(script); }); } return Promise.resolve(); } + /** + * Return the status of the script registration + */ ready(): Observable { return this.isReady$; } + /** + * Render the specified element using the MathJax JavaScript + * + * @param element The element to render with MathJax + */ render(element: HTMLElement) { if (environment.markdown.mathjax) { - (window as any).MathJax.typesetPromise([element]); + this._window.nativeWindow.MathJax.typesetPromise([element]); } } } diff --git a/src/app/core/shared/math.service.ts b/src/app/core/shared/math.service.ts index 1d5271f7a7b..c06ce06220b 100644 --- a/src/app/core/shared/math.service.ts +++ b/src/app/core/shared/math.service.ts @@ -5,6 +5,9 @@ export interface MathJaxConfig { id: string; } +/** + * This service is used to provide the MathJax library with the ability to render markdown code + */ export abstract class MathService { protected abstract mathJaxOptions: any; protected abstract mathJax: MathJaxConfig; diff --git a/src/app/core/shared/server-math.service.ts b/src/app/core/shared/server-math.service.ts index cfddadf114f..1484e84537e 100644 --- a/src/app/core/shared/server-math.service.ts +++ b/src/app/core/shared/server-math.service.ts @@ -5,6 +5,9 @@ import { MathJaxConfig, MathService } from './math.service'; @Injectable({ providedIn: 'root' }) +/** + * Provide the MathService for SSR + */ export class ServerMathService extends MathService { protected signal: Subject; diff --git a/src/app/shared/utils/markdown.directive.ts b/src/app/shared/utils/markdown.directive.ts index a9a59125de4..82301ee8e6f 100644 --- a/src/app/shared/utils/markdown.directive.ts +++ b/src/app/shared/utils/markdown.directive.ts @@ -8,12 +8,20 @@ import { OnInit, SecurityContext } from '@angular/core'; -import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; +import { + DomSanitizer, + SafeHtml, +} from '@angular/platform-browser'; +import { Subject } from 'rxjs'; +import { + filter, + take, + takeUntil, +} from 'rxjs/operators'; + +import { environment } from '../../../environments/environment'; import { MathService } from '../../core/shared/math.service'; import { isEmpty } from '../empty.util'; -import { environment } from '../../../environments/environment'; -import { Subject } from 'rxjs'; -import { take, takeUntil } from 'rxjs/operators'; const markdownItLoader = async () => (await import('markdown-it')).default; type LazyMarkdownIt = ReturnType; @@ -33,8 +41,8 @@ export class MarkdownDirective implements OnInit, OnDestroy { el: HTMLElement; constructor( - protected sanitizer: DomSanitizer, @Inject(MARKDOWN_IT) private markdownIt: LazyMarkdownIt, + protected sanitizer: DomSanitizer, private mathService: MathService, private elementRef: ElementRef) { this.el = elementRef.nativeElement; @@ -66,6 +74,7 @@ export class MarkdownDirective implements OnInit, OnDestroy { private renderMathjax() { this.mathService.ready().pipe( + filter((ready) => ready), take(1), takeUntil(this.alive$) ).subscribe(() => { From 9467f4ea677eba5bd6a19d4296f9502de43b7cbb Mon Sep 17 00:00:00 2001 From: Andrea Barbasso <´andrea.barbasso@4science.com´> Date: Tue, 2 Apr 2024 12:29:19 +0200 Subject: [PATCH 20/24] [DSC-1401] fix missing import --- src/app/shared/explore/explore.module.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/app/shared/explore/explore.module.ts b/src/app/shared/explore/explore.module.ts index c04a5a6b650..d370cb92e17 100644 --- a/src/app/shared/explore/explore.module.ts +++ b/src/app/shared/explore/explore.module.ts @@ -20,6 +20,7 @@ import { ThemedSearchSectionComponent } from './section-component/search-section import { TextSectionComponent } from './section-component/text-section/text-section.component'; import { ThemedTextSectionComponent } from './section-component/text-section/themed-text-section.component'; import { SharedModule } from '../shared.module'; +import { MarkdownViewerModule } from '../markdown-viewer/markdown-viewer.module'; const COMPONENTS = [ BrowseSectionComponent, @@ -42,10 +43,11 @@ const COMPONENTS = [ declarations: [ ...COMPONENTS ], - imports: [ - CommonModule, - SharedModule - ], + imports: [ + CommonModule, + SharedModule, + MarkdownViewerModule + ], exports: [ ...COMPONENTS ] From edaf9630ea9567f0cb55a4b4b64dd97d2a71400b Mon Sep 17 00:00:00 2001 From: Andrea Barbasso <´andrea.barbasso@4science.com´> Date: Wed, 17 Apr 2024 11:58:22 +0200 Subject: [PATCH 21/24] [DSC-1652] add warning and error messages to loader component --- src/app/shared/loading/loading.component.html | 44 +++++++----- .../shared/loading/loading.component.spec.ts | 25 ++++++- src/app/shared/loading/loading.component.ts | 71 ++++++++++++++++--- src/assets/i18n/en.json5 | 4 ++ src/assets/i18n/it.json5 | 6 ++ src/config/app-config.interface.ts | 2 + src/config/default-app-config.ts | 7 ++ src/config/loader-config.interfaces.ts | 7 ++ src/environments/environment.test.ts | 6 ++ 9 files changed, 143 insertions(+), 29 deletions(-) create mode 100644 src/config/loader-config.interfaces.ts diff --git a/src/app/shared/loading/loading.component.html b/src/app/shared/loading/loading.component.html index bb407098d78..8b120751baf 100644 --- a/src/app/shared/loading/loading.component.html +++ b/src/app/shared/loading/loading.component.html @@ -1,18 +1,28 @@ -
- -
- - - - - - - - - - -
-
-
- {{ message }} +
+ + + + + + + + + + +
+ + + + + + + + + + +
+
+ + {{ message }} +
diff --git a/src/app/shared/loading/loading.component.spec.ts b/src/app/shared/loading/loading.component.spec.ts index d564d43dc0d..9f358a6158b 100644 --- a/src/app/shared/loading/loading.component.spec.ts +++ b/src/app/shared/loading/loading.component.spec.ts @@ -35,10 +35,10 @@ describe('LoadingComponent (inline template)', () => { comp = fixture.componentInstance; // LoadingComponent test instance comp.message = 'test message'; + comp.warningMessage = 'test warning message'; + comp.errorMessage = 'test error message'; + fixture.detectChanges(); - // query for the message
- +
diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts index 100b5e9156b..2051340decf 100644 --- a/src/config/default-app-config.ts +++ b/src/config/default-app-config.ts @@ -763,8 +763,8 @@ export class DefaultAppConfig implements AppConfig { }; loader: LoaderConfig = { - enableFallbackMessagesByDefault: false, - warningMessageDelay: 10000, // 10 seconds - errorMessageDelay: 30000, // 30 seconds + showFallbackMessagesByDefault: false, + warningMessageDelay: 5000, // 5 seconds + errorMessageDelay: 15000, // 15 seconds }; } diff --git a/src/config/loader-config.interfaces.ts b/src/config/loader-config.interfaces.ts index 4ebb798e6b0..f9523959f98 100644 --- a/src/config/loader-config.interfaces.ts +++ b/src/config/loader-config.interfaces.ts @@ -1,7 +1,7 @@ import { Config } from './config.interface'; export interface LoaderConfig extends Config { - enableFallbackMessagesByDefault: boolean; + showFallbackMessagesByDefault: boolean; warningMessageDelay: number; errorMessageDelay: number; } diff --git a/src/environments/environment.test.ts b/src/environments/environment.test.ts index f495f7acff1..2346a3f55e7 100644 --- a/src/environments/environment.test.ts +++ b/src/environments/environment.test.ts @@ -579,7 +579,7 @@ export const environment: BuildConfig = { }, loader: { - enableFallbackMessagesByDefault: true, + showFallbackMessagesByDefault: true, warningMessageDelay: 1000, errorMessageDelay: 2000, }, From 0e5121447842894cdbf8d75383c472ba414623ee Mon Sep 17 00:00:00 2001 From: Andrea Barbasso <´andrea.barbasso@4science.com´> Date: Thu, 18 Apr 2024 14:39:21 +0200 Subject: [PATCH 23/24] [DSC-1652] fix lint --- src/app/shared/loading/loading.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/shared/loading/loading.component.ts b/src/app/shared/loading/loading.component.ts index 153bd21e7a6..f21a41c4f55 100644 --- a/src/app/shared/loading/loading.component.ts +++ b/src/app/shared/loading/loading.component.ts @@ -2,7 +2,6 @@ import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; import { hasValue } from '../empty.util'; import { environment } from '../../../environments/environment'; import { AlertType } from '../alert/alert-type'; From 838a0d208cafce2af9748fbb9ab9790e0a819efb Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Tue, 23 Apr 2024 19:48:06 +0200 Subject: [PATCH 24/24] [CST-9683] Fix visualization order of the metrics boxes in the item page --- src/app/core/core.module.ts | 4 ++-- ...pec.ts => metrics-components.service.spec.ts} | 12 ++++++------ ....service.ts => metrics-components.service.ts} | 16 ++++++---------- .../cris-layout-metrics-box.component.spec.ts | 8 ++++---- .../metrics/cris-layout-metrics-box.component.ts | 4 ++-- 5 files changed, 20 insertions(+), 24 deletions(-) rename src/app/core/layout/{metrics-components-data.service.spec.ts => metrics-components.service.spec.ts} (88%) rename src/app/core/layout/{metrics-components-data.service.ts => metrics-components.service.ts} (74%) diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index bab4d48346d..c8f2fb45185 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -162,7 +162,7 @@ import { EditItemMode } from './submission/models/edititem-mode.model'; import { AuditDataService } from './audit/audit-data.service'; import { Audit } from './audit/model/audit.model'; import { ItemExportFormat } from './itemexportformat/model/item-export-format.model'; -import { MetricsComponentsDataService } from './layout/metrics-components-data.service'; +import { MetricsComponentsService } from './layout/metrics-components.service'; import { MetricsComponent } from './layout/models/metrics-component.model'; import { Metric } from './shared/metric.model'; import { MetricsDataService } from './data/metrics-data.service'; @@ -354,7 +354,7 @@ const PROVIDERS = [ FilteredDiscoveryPageResponseParsingService, { provide: NativeWindowService, useFactory: NativeWindowFactory }, TabDataService, - MetricsComponentsDataService, + MetricsComponentsService, MetricsDataService, VocabularyService, VocabularyDataService, diff --git a/src/app/core/layout/metrics-components-data.service.spec.ts b/src/app/core/layout/metrics-components.service.spec.ts similarity index 88% rename from src/app/core/layout/metrics-components-data.service.spec.ts rename to src/app/core/layout/metrics-components.service.spec.ts index dafcfb1cb2b..199e0dd150d 100644 --- a/src/app/core/layout/metrics-components-data.service.spec.ts +++ b/src/app/core/layout/metrics-components.service.spec.ts @@ -1,12 +1,12 @@ import { Metric } from '../shared/metric.model'; -import { MetricsComponentsDataService } from './metrics-components-data.service'; +import { MetricsComponentsService } from './metrics-components.service'; -describe('MetricsComponentsDataService', () => { +describe('MetricsComponentsService', () => { - let service: MetricsComponentsDataService; + let service: MetricsComponentsService; beforeEach(() => { - service = new MetricsComponentsDataService(); + service = new MetricsComponentsService(); }); @@ -44,8 +44,8 @@ describe('MetricsComponentsDataService', () => { expect(result.length).toBe(1); expect(result[0].metrics.length).toBe(2); - expect(result[0].metrics[0].metricType).toBe('views'); - expect(result[0].metrics[1].metricType).toBe('downloads'); + expect(result[0].metrics[0].metricType).toBe('downloads'); + expect(result[0].metrics[1].metricType).toBe('views'); }); diff --git a/src/app/core/layout/metrics-components-data.service.ts b/src/app/core/layout/metrics-components.service.ts similarity index 74% rename from src/app/core/layout/metrics-components-data.service.ts rename to src/app/core/layout/metrics-components.service.ts index 4a415e242fa..46bbb7d25e0 100644 --- a/src/app/core/layout/metrics-components-data.service.ts +++ b/src/app/core/layout/metrics-components.service.ts @@ -4,7 +4,7 @@ import { CrisLayoutMetricRow } from './models/tab.model'; /** * A service responsible for managing metrics objects */ -export class MetricsComponentsDataService { +export class MetricsComponentsService { /** * Get matching metrics for item. @@ -20,17 +20,13 @@ export class MetricsComponentsDataService { computeMetricsRows(itemMetrics: Metric[], maxColumn, metricTypes: string[]): CrisLayoutMetricRow[] { - // support - const typeMap = {}; - metricTypes.forEach((type) => typeMap[type] = type); - // filter, enrich, order - const metrics = itemMetrics - .filter((metric) => typeMap[metric.metricType]) - .map((metric) => { - return { ...metric, position: typeMap[metric.metricType].position }; + const metrics: Metric[] = itemMetrics + .filter((metric: Metric) => metricTypes.includes(metric.metricType)) + .map((metric: Metric) => { + return { ...metric, position: metricTypes.indexOf(metric.metricType) }; }) - .sort((metric) => metric.position); + .sort((metricA, metricB ) => metricA.position - metricB.position); // chunker const totalRow = Math.ceil(metrics.length / maxColumn); diff --git a/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metrics/cris-layout-metrics-box.component.spec.ts b/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metrics/cris-layout-metrics-box.component.spec.ts index f69017b6342..565972f6a15 100644 --- a/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metrics/cris-layout-metrics-box.component.spec.ts +++ b/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metrics/cris-layout-metrics-box.component.spec.ts @@ -1,5 +1,5 @@ /* eslint-disable max-classes-per-file */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { Observable, of } from 'rxjs'; import { RemoteData } from '../../../../../core/data/remote-data'; @@ -16,7 +16,7 @@ import { SharedModule } from '../../../../../shared/shared.module'; import { CrisLayoutMetricsBoxComponent } from './cris-layout-metrics-box.component'; import { metricsComponent } from '../../../../../shared/testing/metrics-components.mock'; import { MetricsComponent } from '../../../../../core/layout/models/metrics-component.model'; -import { MetricsComponentsDataService } from '../../../../../core/layout/metrics-components-data.service'; +import { MetricsComponentsService } from '../../../../../core/layout/metrics-components.service'; import { Metric } from '../../../../../core/shared/metric.model'; import { ItemDataService } from '../../../../../core/data/item-data.service'; import { CrisLayoutMetricRow } from '../../../../../core/layout/models/tab.model'; @@ -78,7 +78,7 @@ describe('CrisLayoutMetricsBoxComponent', () => { let component: CrisLayoutMetricsBoxComponent; let fixture: ComponentFixture; - beforeEach(async(() => { + beforeEach(waitForAsync(() => { itemDataService = jasmine.createSpyObj('ItemDataService', { getMetrics: jasmine.createSpy('getMetrics') @@ -94,7 +94,7 @@ describe('CrisLayoutMetricsBoxComponent', () => { BrowserAnimationsModule, SharedModule], providers: [ - { provide: MetricsComponentsDataService, useClass: MetricsComponentsDataServiceMock }, + { provide: MetricsComponentsService, useClass: MetricsComponentsDataServiceMock }, { provide: ItemDataService, useValue: itemDataService }, { provide: 'boxProvider', useClass: boxMetrics }, { provide: 'itemProvider', useClass: { metrics: [metric1Mock, metric2Mock] } }, diff --git a/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metrics/cris-layout-metrics-box.component.ts b/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metrics/cris-layout-metrics-box.component.ts index a6b771a8ad8..05de62cfe76 100644 --- a/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metrics/cris-layout-metrics-box.component.ts +++ b/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metrics/cris-layout-metrics-box.component.ts @@ -8,7 +8,7 @@ import { RenderCrisLayoutBoxFor } from '../../../../decorators/cris-layout-box.d import { LayoutBox } from '../../../../enums/layout-box.enum'; import { getFirstSucceededRemoteDataPayload } from '../../../../../core/shared/operators'; import { hasValue } from '../../../../../shared/empty.util'; -import { MetricsComponentsDataService } from '../../../../../core/layout/metrics-components-data.service'; +import { MetricsComponentsService } from '../../../../../core/layout/metrics-components.service'; import { ItemDataService } from '../../../../../core/data/item-data.service'; import { CrisLayoutBox, MetricsBoxConfiguration, } from '../../../../../core/layout/models/box.model'; import { Item } from '../../../../../core/shared/item.model'; @@ -51,7 +51,7 @@ export class CrisLayoutMetricsBoxComponent extends CrisLayoutBoxModelComponent i subs: Subscription[] = []; constructor( - protected metricsComponentService: MetricsComponentsDataService, + protected metricsComponentService: MetricsComponentsService, protected itemService: ItemDataService, protected translateService: TranslateService, @Inject('boxProvider') public boxProvider: CrisLayoutBox,