Skip to content

Commit 134a945

Browse files
committed
fix(markdown): added toc support
1 parent 9aaf880 commit 134a945

File tree

6 files changed

+107
-8
lines changed

6 files changed

+107
-8
lines changed

angular.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
"cedar-embeddable-editor",
3030
"cedar-artifact-viewer",
3131
"markdown-it-video",
32+
"markdown-it-anchor",
33+
"markdown-it-toc-done-right",
3234
"ace-builds/src-noconflict/ext-language_tools",
3335
"@traptitech/markdown-it-katex",
3436
"@citation-js/core",

package-lock.json

Lines changed: 20 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@
5858
"chart.js": "^4.4.9",
5959
"diff": "^8.0.2",
6060
"markdown-it": "^14.1.0",
61+
"markdown-it-anchor": "^9.2.0",
62+
"markdown-it-toc-done-right": "^4.2.0",
6163
"markdown-it-video": "^0.6.3",
6264
"ngx-captcha": "^13.0.0",
6365
"ngx-cookie-service": "^19.1.2",
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
declare module 'markdown-it-toc-done-right' {
2+
import { PluginWithOptions } from 'markdown-it';
3+
4+
export interface TocOptions {
5+
placeholder: string;
6+
slugify: (s: string) => string;
7+
uniqueSlugStartIndex: number;
8+
containerClass: string;
9+
containerId: string;
10+
listClass: string;
11+
itemClass: string;
12+
linkClass: string;
13+
level: number | number[];
14+
listType: 'ol' | 'ul';
15+
format: (s: string) => string;
16+
callback: (tocCode: string, ast: TocAst) => void;
17+
}
18+
19+
export interface TocAst {
20+
l: number;
21+
n: string;
22+
c: TocAst[];
23+
}
24+
25+
const markdownItTocDoneRight: PluginWithOptions<Partial<TocOptions>>;
26+
export default markdownItTocDoneRight;
27+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<div class="md-editor-container" [innerHTML]="renderedHtml()"></div>
1+
<div #container class="md-editor-container" [innerHTML]="renderedHtml()"></div>

src/app/shared/components/markdown/markdown.component.ts

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,21 @@
1-
import { ChangeDetectionStrategy, Component, computed, inject, input, Signal } from '@angular/core';
1+
import {
2+
AfterViewInit,
3+
ChangeDetectionStrategy,
4+
Component,
5+
computed,
6+
DestroyRef,
7+
ElementRef,
8+
inject,
9+
input,
10+
Signal,
11+
ViewChild,
12+
} from '@angular/core';
213
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
314

415
import markdownItKatex from '@traptitech/markdown-it-katex';
516
import MarkdownIt from 'markdown-it';
17+
import markdownItAnchor from 'markdown-it-anchor';
18+
import markdownItTocDoneRight from 'markdown-it-toc-done-right';
619
import markdownItVideo from 'markdown-it-video';
720

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

31+
@ViewChild('container', { static: false }) containerRef?: ElementRef<HTMLElement>;
32+
1833
private md: MarkdownIt;
1934
private sanitizer = inject(DomSanitizer);
35+
private destroyRef = inject(DestroyRef);
36+
private clickHandler?: (event: MouseEvent) => void;
2037

2138
renderedHtml: Signal<SafeHtml> = computed(() => {
2239
const result = this.md.render(this.markdownText());
@@ -37,6 +54,42 @@ export class MarkdownComponent {
3754
.use(markdownItKatex, {
3855
output: 'mathml',
3956
throwOnError: false,
57+
})
58+
.use(markdownItAnchor)
59+
.use(markdownItTocDoneRight, {
60+
placeholder: '@\\[toc\\]',
61+
listType: 'ul',
4062
});
4163
}
64+
65+
ngAfterViewInit(): void {
66+
this.setupClickHandler();
67+
}
68+
69+
private setupClickHandler(): void {
70+
if (!this.containerRef?.nativeElement) {
71+
return;
72+
}
73+
74+
const container = this.containerRef.nativeElement;
75+
76+
this.clickHandler = (event: MouseEvent) => {
77+
const anchor = (event.target as HTMLElement).closest('a');
78+
if (!anchor?.hash) {
79+
return;
80+
}
81+
82+
const targetElement = document.getElementById(anchor.hash.substring(1));
83+
if (targetElement) {
84+
event.preventDefault();
85+
targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
86+
}
87+
};
88+
89+
container.addEventListener('click', this.clickHandler);
90+
91+
this.destroyRef.onDestroy(() => {
92+
container.removeEventListener('click', this.clickHandler!);
93+
});
94+
}
4295
}

0 commit comments

Comments
 (0)