Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
"cedar-embeddable-editor",
"cedar-artifact-viewer",
"markdown-it-video",
"markdown-it-anchor",
"markdown-it-toc-done-right",
"ace-builds/src-noconflict/ext-language_tools",
"@traptitech/markdown-it-katex",
"@citation-js/core",
Expand Down
25 changes: 20 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@
"chart.js": "^4.4.9",
"diff": "^8.0.2",
"markdown-it": "^14.1.0",
"markdown-it-anchor": "^9.2.0",
"markdown-it-toc-done-right": "^4.2.0",
"markdown-it-video": "^0.6.3",
"ngx-captcha": "^13.0.0",
"ngx-cookie-service": "^19.1.2",
Expand Down
27 changes: 27 additions & 0 deletions src/@types/markdown-it-toc-done-right.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
declare module 'markdown-it-toc-done-right' {
import { PluginWithOptions } from 'markdown-it';

export interface TocOptions {
placeholder: string;
slugify: (s: string) => string;
uniqueSlugStartIndex: number;
containerClass: string;
containerId: string;
listClass: string;
itemClass: string;
linkClass: string;
level: number | number[];
listType: 'ol' | 'ul';
format: (s: string) => string;
callback: (tocCode: string, ast: TocAst) => void;
}

export interface TocAst {
l: number;
n: string;
c: TocAst[];
}

const markdownItTocDoneRight: PluginWithOptions<Partial<TocOptions>>;
export default markdownItTocDoneRight;
}
2 changes: 1 addition & 1 deletion src/app/shared/components/markdown/markdown.component.html
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<div class="md-editor-container" [innerHTML]="renderedHtml()"></div>
<div #container class="md-editor-container" [innerHTML]="renderedHtml()"></div>
57 changes: 55 additions & 2 deletions src/app/shared/components/markdown/markdown.component.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
import { ChangeDetectionStrategy, Component, computed, inject, input, Signal } from '@angular/core';
import {
AfterViewInit,
ChangeDetectionStrategy,
Component,
computed,
DestroyRef,
ElementRef,
inject,
input,
Signal,
ViewChild,
} from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';

import markdownItKatex from '@traptitech/markdown-it-katex';
import MarkdownIt from 'markdown-it';
import markdownItAnchor from 'markdown-it-anchor';
import markdownItTocDoneRight from 'markdown-it-toc-done-right';
import markdownItVideo from 'markdown-it-video';

@Component({
Expand All @@ -12,11 +25,15 @@ import markdownItVideo from 'markdown-it-video';
styleUrl: './markdown.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MarkdownComponent {
export class MarkdownComponent implements AfterViewInit {
markdownText = input<string>('');

@ViewChild('container', { static: false }) containerRef?: ElementRef<HTMLElement>;

private md: MarkdownIt;
private sanitizer = inject(DomSanitizer);
private destroyRef = inject(DestroyRef);
private clickHandler?: (event: MouseEvent) => void;

renderedHtml: Signal<SafeHtml> = computed(() => {
const result = this.md.render(this.markdownText());
Expand All @@ -37,6 +54,42 @@ export class MarkdownComponent {
.use(markdownItKatex, {
output: 'mathml',
throwOnError: false,
})
.use(markdownItAnchor)
.use(markdownItTocDoneRight, {
placeholder: '@\\[toc\\]',
listType: 'ul',
});
}

ngAfterViewInit(): void {
this.setupClickHandler();
}

private setupClickHandler(): void {
if (!this.containerRef?.nativeElement) {
return;
}

const container = this.containerRef.nativeElement;

this.clickHandler = (event: MouseEvent) => {
const anchor = (event.target as HTMLElement).closest('a');
if (!anchor?.hash) {
return;
}

const targetElement = document.getElementById(anchor.hash.substring(1));
if (targetElement) {
event.preventDefault();
targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
};

container.addEventListener('click', this.clickHandler);

this.destroyRef.onDestroy(() => {
container.removeEventListener('click', this.clickHandler!);
});
}
}