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 0000000000..a8517c87b2 --- /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 0000000000..3702369ae9 --- /dev/null +++ b/src/app/core/shared/math.directive.ts @@ -0,0 +1,60 @@ +import { + Directive, + ElementRef, + Input, + OnChanges, + OnDestroy, + OnInit, + SimpleChanges, +} from '@angular/core'; +import { Subject } from 'rxjs'; +import { + take, + takeUntil, +} from 'rxjs/operators'; + +import { MathService } from './math.service'; + +@Directive({ + selector: '[dsMath]', + standalone: true, +}) +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 0000000000..aa71ca226b --- /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 0000000000..794f3b16e5 --- /dev/null +++ b/src/app/core/shared/math.service.ts @@ -0,0 +1,93 @@ +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(1); + + 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/utils/markdown.pipe.ts b/src/app/shared/utils/markdown.pipe.ts index e543825328..767d5f6a8b 100644 --- a/src/app/shared/utils/markdown.pipe.ts +++ b/src/app/shared/utils/markdown.pipe.ts @@ -9,6 +9,7 @@ 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'; @@ -58,6 +59,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, ) { } @@ -73,9 +75,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 d148dee148..85042034f6 100644 --- a/src/config/default-app-config.ts +++ b/src/config/default-app-config.ts @@ -476,8 +476,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