Я смотрел на ту же проблему, что и две недели назад, и сегодня мне наконец удалось заставить ее работать.Я не специалист по угловым характеристикам, поэтому может потребоваться некоторая оптимизация, но функциональность ядра работает.
Шаг первый
Добавьте @types/mathjax
зависимость к вашему package.json
файлу.
Шаг второй
Добавьте недавно добавленный тип к tsconfig.app.json
, например:
{
"compilerOptions": {
"types": ["mathjax"]
}
}
Это позволит вам использовать такие конструкции, как MathJax.Callback.Queue
, и ваша IDE не будет жаловаться на неизвестностьвведите и т. д.
Шаг третий: создайте объект-обертку для вашего математического содержимого (необязательно)
У меня возникли некоторые проблемы с Mathml, поэтому я создал обертку для математики, которая выглядит следующим образом:
export interface MathContent {
latex?: string;
mathml?: string;
}
Шаг четвертый
Теперь нам нужно определить модуль, который будет вводить тег сценария MathJax с настройкой.Поскольку он будет загружаться динамически async
, мы должны убедиться, что набор текста не начинается до полной загрузки MathJax.Самый простой способ - сохранить Observer<any>
в window
объекте.Observer
и функции рендеринга могут быть включены в сервис.
// see https://stackoverflow.com/a/12709880/1203690
declare global {
interface Window {
hubReady: Observer<boolean>;
}
}
@Injectable()
export class MathServiceImpl {
private readonly notifier: ReplaySubject<boolean>;
constructor() {
this.notifier = new ReplaySubject<boolean>();
window.hubReady = this.notifier; // as said, bind to window object
}
ready(): Observable<boolean> {
return this.notifier;
}
render(element: HTMLElement, math?: MathContent): void {
if (math) {
if (math.latex) {
element.innerText = math.latex;
} else {
element.innerHTML = math.mathml;
}
}
MathJax.Hub.Queue(['Typeset', MathJax.Hub, element]);
}
}
Шаг пятый
Теперь мы создадим директиву, которая будет запускать рендеринг после загрузки MathJax.Директива может выглядеть так:
@Directive({
selector: '[appMath]'
})
export class MathDirective implements OnInit, OnChanges, OnDestroy {
private alive$ = new Subject<boolean>();
@Input()
private appMath: MathContent;
private readonly _el: HTMLElement;
constructor(private service: MathServiceImpl,
private el: ElementRef) {
this._el = el.nativeElement as HTMLElement;
}
ngOnInit(): void {
this.service
.ready()
.pipe(
take(1),
takeUntil(this.alive$)
).subscribe(res => {
this.service.render(this._el, this.appMath);
});
}
ngOnChanges(changes: SimpleChanges): void {
console.log(changes);
}
ngOnDestroy(): void {
this.alive$.next(false);
}
}
Шаг шестой (почти там)
На четвертом шаге я упомянул async
loading.Согласно документации по MathJax это делается с использованием document.createElement
.Угловой модуль - идеальное место для этой логики.Чтобы вызвать метод .ready()
на нашем MathService
, мы будем использовать MathJax.Hub.Register.StartupHook
и передать наблюдаемый от MathService
Таким образом, наш модуль будет выглядеть следующим образом:
@NgModule({
declarations: [MathDirective],
exports: [MathDirective]
})
export class MathModule {
constructor(mathService: MathServiceImpl) {
// see https://docs.mathjax.org/en/latest/advanced/dynamic.html
const script = document.createElement('script') as HTMLScriptElement;
script.type = 'text/javascript';
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML';
script.async = true;
document.getElementsByTagName('head')[0].appendChild(script);
const config = document.createElement('script') as HTMLScriptElement;
config.type = 'text/x-mathjax-config';
// register notifier to StartupHook and trigger .next() for all subscribers
config.text = `
MathJax.Hub.Config({
skipStartupTypeset: true,
tex2jax: { inlineMath: [["$", "$"]],displayMath:[["$$", "$$"]] }
});
MathJax.Hub.Register.StartupHook('End', () => {
window.hubReady.next();
window.hubReady.complete();
});
`;
document.getElementsByTagName('head')[0].appendChild(config);
}
// this is needed so service constructor which will bind
// notifier to window object before module constructor is called
public static forRoot(): ModuleWithProviders {
return {
ngModule: MathModule,
providers: [{provide: MathServiceImpl, useClass: MathServiceImpl}]
};
}
}
Шаг седьмой: рендеринг математики
Теперь все готово, просто импортируйте MathModule.forRoot()
в модуль, где вы хотите сделать математику.Компонент будет выглядеть следующим образом:
export class AppComponent {
mathLatex: MathContent = {
latex: 'When $a \\ne 0$, there are two solutions to $\\frac{5}{9}$'
};
mathMl: MathContent = {
mathml: `<math xmlns="http://www.w3.org/1998/Math/MathML">
<mrow>
<mover>
<munder>
<mo>∫</mo>
<mn>0</mn>
</munder>
<mi>∞</mi>
</mover>
<mtext> versus </mtext>
<munderover>
<mo>∫</mo>
<mn>0</mn>
<mi>∞</mi>
</munderover>
</mrow>
</math>`
};
}
и шаблон
<div [appMath]="mathLatex"></div>
<div [appMath]="mathMl"></div>
<!-- will render inline element math -->
<div [appMath]>
$E = mc^2$
</div>
, которые должны отображаться для этого
Шаг восьмой (бонус)
Вот пример рабочего стекаблика https://stackblitz.com/edit/mathjax-example, чтобы вы могли проверить свой прогресс в реализации