Commits
Yury Bondarenko authored 674a4829ce5
1 - | import { Inject, InjectionToken, Pipe, PipeTransform } from '@angular/core'; |
2 - | import MarkdownIt from 'markdown-it'; |
3 - | import * as sanitizeHtml from 'sanitize-html'; |
1 + | import { Inject, InjectionToken, Pipe, PipeTransform, SecurityContext } from '@angular/core'; |
4 2 | import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; |
5 3 | import { environment } from '../../../environments/environment'; |
6 4 | |
5 + | const markdownItLoader = async () => (await import('markdown-it')).default; |
6 + | type LazyMarkdownIt = ReturnType<typeof markdownItLoader>; |
7 + | const MARKDOWN_IT = new InjectionToken<LazyMarkdownIt>( |
8 + | 'Lazily loaded MarkdownIt', |
9 + | { providedIn: 'root', factory: markdownItLoader } |
10 + | ); |
11 + | |
7 12 | const mathjaxLoader = async () => (await import('markdown-it-mathjax3')).default; |
8 13 | type Mathjax = ReturnType<typeof mathjaxLoader>; |
9 14 | const MATHJAX = new InjectionToken<Mathjax>( |
10 15 | 'Lazily loaded mathjax', |
11 16 | { providedIn: 'root', factory: mathjaxLoader } |
12 17 | ); |
13 18 | |
19 + | const sanitizeHtmlLoader = async () => (await import('sanitize-html') as any).default; |
20 + | type SanitizeHtml = ReturnType<typeof sanitizeHtmlLoader>; |
21 + | const SANITIZE_HTML = new InjectionToken<SanitizeHtml>( |
22 + | 'Lazily loaded sanitize-html', |
23 + | { providedIn: 'root', factory: sanitizeHtmlLoader } |
24 + | ); |
25 + | |
14 26 | /** |
15 27 | * Pipe for rendering markdown and mathjax. |
16 28 | * - markdown will only be rendered if {@link MarkdownConfig#enabled} is true |
17 29 | * - mathjax will only be rendered if both {@link MarkdownConfig#enabled} and {@link MarkdownConfig#mathjax} are true |
18 30 | * |
19 31 | * This pipe should be used on the 'innerHTML' attribute of a component, in combination with an async pipe. |
20 32 | * Example usage: |
21 33 | * <span class="example" [innerHTML]="'# title' | dsMarkdown | async"></span> |
22 34 | * Result: |
23 35 | * <span class="example"> |
24 36 | * <h1>title</h1> |
25 37 | * </span> |
26 38 | */ |
27 39 | @Pipe({ |
28 40 | name: 'dsMarkdown' |
29 41 | }) |
30 42 | export class MarkdownPipe implements PipeTransform { |
31 43 | |
32 44 | constructor( |
33 45 | protected sanitizer: DomSanitizer, |
46 + | @Inject(MARKDOWN_IT) private markdownIt: LazyMarkdownIt, |
34 47 | @Inject(MATHJAX) private mathjax: Mathjax, |
48 + | @Inject(SANITIZE_HTML) private sanitizeHtml: SanitizeHtml, |
35 49 | ) { |
36 50 | } |
37 51 | |
38 52 | async transform(value: string): Promise<SafeHtml> { |
39 53 | if (!environment.markdown.enabled) { |
40 54 | return value; |
41 55 | } |
56 + | const MarkdownIt = await this.markdownIt; |
42 57 | const md = new MarkdownIt({ |
43 58 | html: true, |
44 59 | linkify: true, |
45 60 | }); |
61 + | |
62 + | let html: string; |
46 63 | if (environment.markdown.mathjax) { |
47 64 | md.use(await this.mathjax); |
48 - | } |
49 - | return this.sanitizer.bypassSecurityTrustHtml( |
50 - | sanitizeHtml(md.render(value), { |
65 + | const sanitizeHtml = await this.sanitizeHtml |
66 + | html = sanitizeHtml(md.render(value), { |
51 67 | // sanitize-html doesn't let through SVG by default, so we extend its allowlists to cover MathJax SVG |
52 68 | allowedTags: [ |
53 69 | sanitizeHtml.defaults.allowedTags, |
54 70 | 'mjx-container', 'svg', 'g', 'path', 'rect', 'text' |
55 71 | ], |
56 72 | allowedAttributes: { |
57 73 | sanitizeHtml.defaults.allowedAttributes, |
58 74 | 'mjx-container': [ |
59 75 | 'class', 'style', 'jax' |
60 76 | ], |
70 86 | rect: [ |
71 87 | 'width', 'height', 'x', 'y', 'transform', 'style' |
72 88 | ], |
73 89 | text: [ |
74 90 | 'transform', 'font-size' |
75 91 | ] |
76 92 | }, |
77 93 | parser: { |
78 94 | lowerCaseAttributeNames: false, |
79 95 | }, |
80 - | }) |
81 - | ); |
96 + | }); |
97 + | } else { |
98 + | html = this.sanitizer.sanitize(SecurityContext.HTML, md.render(value)); |
99 + | } |
100 + | |
101 + | return this.sanitizer.bypassSecurityTrustHtml(html) |
82 102 | } |
83 103 | } |