• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jfcere / ngx-markdown / 3fd6366a-f091-402b-aa3c-ca4a17b31cfc

01 Sep 2025 04:49PM UTC coverage: 61.982% (-34.9%) from 96.89%
3fd6366a-f091-402b-aa3c-ca4a17b31cfc

Pull #597

circleci

jfcere
add support for sanitizer function
Pull Request #597: feat: add support for sanitizer function

84 of 121 branches covered (69.42%)

Branch coverage included in aggregate %.

12 of 12 new or added lines in 3 files covered. (100.0%)

123 existing lines in 6 files now uncovered.

185 of 313 relevant lines covered (59.11%)

12.86 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.89
/lib/src/markdown.component.ts
1
import {
2
  AfterViewInit,
3
  Component,
4
  ElementRef,
5
  EventEmitter,
6
  Input,
7
  OnChanges,
8
  OnDestroy,
9
  Output,
10
  TemplateRef,
11
  Type,
12
  ViewContainerRef,
13
} from '@angular/core';
14
import { Subject } from 'rxjs';
15
import { takeUntil } from 'rxjs/operators';
16
import { ClipboardRenderOptions } from './clipboard-options';
17
import { KatexOptions } from './katex-options';
18
import { MarkdownService, ParseOptions, RenderOptions } from './markdown.service';
19
import { MermaidAPI } from './mermaid-options';
20
import { PrismPlugin } from './prism-plugin';
21

22
@Component({
23
  // eslint-disable-next-line @angular-eslint/component-selector
24
  selector: 'markdown, [markdown]',
25
  template: '<ng-content></ng-content>',
26
})
27
export class MarkdownComponent implements OnChanges, AfterViewInit, OnDestroy {
1✔
28

29
  protected static ngAcceptInputType_clipboard: boolean | '';
30
  protected static ngAcceptInputType_emoji: boolean | '';
31
  protected static ngAcceptInputType_katex: boolean | '';
32
  protected static ngAcceptInputType_mermaid: boolean | '';
33
  protected static ngAcceptInputType_lineHighlight: boolean | '';
34
  protected static ngAcceptInputType_lineNumbers: boolean | '';
35
  protected static ngAcceptInputType_commandLine: boolean | '';
36

37
  @Input() data: string | null | undefined;
38
  @Input() src: string | null | undefined;
39

40
  @Input()
UNCOV
41
  get disableSanitizer(): boolean { return this._disableSanitizer; }
×
UNCOV
42
  set disableSanitizer(value: boolean) { this._disableSanitizer = this.coerceBooleanProperty(value); }
×
43

44
  @Input()
UNCOV
45
  get inline(): boolean { return this._inline; }
×
UNCOV
46
  set inline(value: boolean) { this._inline = this.coerceBooleanProperty(value); }
×
47

48
  // Plugin - clipboard
49
  @Input()
UNCOV
50
  get clipboard(): boolean { return this._clipboard; }
×
UNCOV
51
  set clipboard(value: boolean) { this._clipboard = this.coerceBooleanProperty(value); }
×
52

53
  @Input() clipboardButtonComponent: Type<unknown> | undefined;
54
  @Input() clipboardButtonTemplate: TemplateRef<unknown> | undefined;
55

56
  // Plugin - emoji
57
  @Input()
UNCOV
58
  get emoji(): boolean { return this._emoji; }
×
UNCOV
59
  set emoji(value: boolean) { this._emoji = this.coerceBooleanProperty(value); }
×
60

61
  // Plugin - katex
62
  @Input()
UNCOV
63
  get katex(): boolean { return this._katex; }
×
UNCOV
64
  set katex(value: boolean) { this._katex = this.coerceBooleanProperty(value); }
×
65

66
  @Input() katexOptions: KatexOptions | undefined;
67

68
  // Plugin - mermaid
69
  @Input()
UNCOV
70
  get mermaid(): boolean { return this._mermaid; }
×
UNCOV
71
  set mermaid(value: boolean) { this._mermaid = this.coerceBooleanProperty(value); }
×
72

73
  @Input() mermaidOptions: MermaidAPI.MermaidConfig | undefined;
74

75
  // Plugin - lineHighlight
76
  @Input()
UNCOV
77
  get lineHighlight(): boolean { return this._lineHighlight; }
×
UNCOV
78
  set lineHighlight(value: boolean) { this._lineHighlight = this.coerceBooleanProperty(value); }
×
79

80
  @Input() line: string | string[] | undefined;
81
  @Input() lineOffset: number | undefined;
82

83
  // Plugin - lineNumbers
84
  @Input()
UNCOV
85
  get lineNumbers(): boolean { return this._lineNumbers; }
×
UNCOV
86
  set lineNumbers(value: boolean) { this._lineNumbers = this.coerceBooleanProperty(value); }
×
87

88
  @Input() start: number | undefined;
89

90
  // Plugin - commandLine
91
  @Input()
UNCOV
92
  get commandLine(): boolean { return this._commandLine; }
×
UNCOV
93
  set commandLine(value: boolean) { this._commandLine = this.coerceBooleanProperty(value); }
×
94

95
  @Input() filterOutput: string | undefined;
96
  @Input() host: string | undefined;
97
  @Input() prompt: string | undefined;
98
  @Input() output: string | undefined;
99
  @Input() user: string | undefined;
100

101
  // Event emitters
UNCOV
102
  @Output() error = new EventEmitter<string | Error>();
×
UNCOV
103
  @Output() load = new EventEmitter<string>();
×
UNCOV
104
  @Output() ready = new EventEmitter<void>();
×
105

UNCOV
106
  private _clipboard = false;
×
UNCOV
107
  private _commandLine = false;
×
UNCOV
108
  private _disableSanitizer = false;
×
UNCOV
109
  private _emoji = false;
×
UNCOV
110
  private _inline = false;
×
UNCOV
111
  private _katex = false;
×
UNCOV
112
  private _lineHighlight = false;
×
UNCOV
113
  private _lineNumbers = false;
×
UNCOV
114
  private _mermaid = false;
×
115

UNCOV
116
  private readonly destroyed$ = new Subject<void>();
×
117

118
  constructor(
UNCOV
119
    public element: ElementRef<HTMLElement>,
×
UNCOV
120
    public markdownService: MarkdownService,
×
UNCOV
121
    public viewContainerRef: ViewContainerRef,
×
122
  ) { }
123

124
  ngOnChanges(): void {
UNCOV
125
    this.loadContent();
×
126
  }
127

128
  loadContent(): void {
UNCOV
129
    if (this.data != null) {
×
UNCOV
130
      this.handleData();
×
UNCOV
131
      return;
×
132
    }
UNCOV
133
    if (this.src != null) {
×
UNCOV
134
      this.handleSrc();
×
UNCOV
135
      return;
×
136
    }
137
  }
138

139
  ngAfterViewInit(): void {
UNCOV
140
    if (!this.data && !this.src) {
×
UNCOV
141
      this.handleTransclusion();
×
142
    }
143

UNCOV
144
    this.markdownService.reload$
×
145
      .pipe(takeUntil(this.destroyed$))
UNCOV
146
      .subscribe(() => this.loadContent());
×
147
  }
148

149
  ngOnDestroy(): void {
UNCOV
150
    this.destroyed$.next();
×
UNCOV
151
    this.destroyed$.complete();
×
152
  }
153

154
  async render(markdown: string, decodeHtml = false): Promise<void> {
×
UNCOV
155
    const parsedOptions: ParseOptions = {
×
156
      decodeHtml,
157
      inline: this.inline,
158
      emoji: this.emoji,
159
      mermaid: this.mermaid,
160
      disableSanitizer: this.disableSanitizer,
161
    };
162

UNCOV
163
    const renderOptions: RenderOptions = {
×
164
      clipboard: this.clipboard,
165
      clipboardOptions: this.getClipboardOptions(),
166
      katex: this.katex,
167
      katexOptions: this.katexOptions,
168
      mermaid: this.mermaid,
169
      mermaidOptions: this.mermaidOptions,
170
    };
171

UNCOV
172
    const parsed = await this.markdownService.parse(markdown, parsedOptions);
×
173

UNCOV
174
    this.element.nativeElement.innerHTML = parsed;
×
175

UNCOV
176
    this.handlePlugins();
×
177

UNCOV
178
    this.markdownService.render(this.element.nativeElement, renderOptions, this.viewContainerRef);
×
179

UNCOV
180
    this.ready.emit();
×
181
  }
182

183
  private coerceBooleanProperty(value: boolean | ''): boolean {
UNCOV
184
    return value != null && `${String(value)}` !== 'false';
×
185
  }
186

187
  private getClipboardOptions(): ClipboardRenderOptions | undefined {
UNCOV
188
    if (this.clipboardButtonComponent || this.clipboardButtonTemplate) {
×
UNCOV
189
      return {
×
190
        buttonComponent: this.clipboardButtonComponent,
191
        buttonTemplate: this.clipboardButtonTemplate,
192
      };
193
    }
UNCOV
194
    return undefined;
×
195
  }
196

197
  private handleData(): void {
UNCOV
198
    this.render(this.data!);
×
199
  }
200

201
  private handleSrc(): void {
UNCOV
202
    this.markdownService
×
203
      .getSource(this.src!)
204
      .subscribe({
205
        next: markdown => {
UNCOV
206
          this.render(markdown).then(() => {
×
UNCOV
207
            this.load.emit(markdown);
×
208
          });
209
        },
UNCOV
210
        error: (error: string | Error) => this.error.emit(error),
×
211
      });
212
  }
213

214
  private handleTransclusion(): void {
UNCOV
215
    this.render(this.element.nativeElement.innerHTML, true);
×
216
  }
217

218
  private handlePlugins(): void {
UNCOV
219
    if (this.commandLine) {
×
UNCOV
220
      this.setPluginClass(this.element.nativeElement, PrismPlugin.CommandLine);
×
UNCOV
221
      this.setPluginOptions(this.element.nativeElement, {
×
222
        dataFilterOutput: this.filterOutput,
223
        dataHost: this.host,
224
        dataPrompt: this.prompt,
225
        dataOutput: this.output,
226
        dataUser: this.user,
227
      });
228
    }
UNCOV
229
    if (this.lineHighlight) {
×
UNCOV
230
      this.setPluginOptions(this.element.nativeElement, { dataLine: this.line, dataLineOffset: this.lineOffset });
×
231
    }
UNCOV
232
    if (this.lineNumbers) {
×
UNCOV
233
      this.setPluginClass(this.element.nativeElement, PrismPlugin.LineNumbers);
×
UNCOV
234
      this.setPluginOptions(this.element.nativeElement, { dataStart: this.start });
×
235
    }
236
  }
237

238
  private setPluginClass(element: HTMLElement, plugin: string | string[]): void {
UNCOV
239
    const preElements = element.querySelectorAll('pre');
×
UNCOV
240
    for (let i = 0; i < preElements.length; i++) {
×
UNCOV
241
      const classes = plugin instanceof Array ? plugin : [plugin];
×
UNCOV
242
      preElements.item(i).classList.add(...classes);
×
243
    }
244
  }
245

246
  private setPluginOptions(element: HTMLElement, options: Record<string, number | string | string[] | undefined>): void {
UNCOV
247
    const preElements = element.querySelectorAll('pre');
×
UNCOV
248
    for (let i = 0; i < preElements.length; i++) {
×
UNCOV
249
      Object.keys(options).forEach(option => {
×
UNCOV
250
        const attributeValue = options[option];
×
UNCOV
251
        if (attributeValue) {
×
UNCOV
252
          const attributeName = this.toLispCase(option);
×
UNCOV
253
          preElements.item(i).setAttribute(attributeName, attributeValue.toString());
×
254
        }
255
      });
256
    }
257
  }
258

259
  private toLispCase(value: string): string {
UNCOV
260
    const upperChars = value.match(/([A-Z])/g);
×
UNCOV
261
    if (!upperChars) {
×
262
      return value;
×
263
    }
UNCOV
264
    let str = value.toString();
×
UNCOV
265
    for (let i = 0, n = upperChars.length; i < n; i++) {
×
UNCOV
266
      str = str.replace(new RegExp(upperChars[i]), '-' + upperChars[i].toLowerCase());
×
267
    }
UNCOV
268
    if (str.slice(0, 1) === '-') {
×
269
      str = str.slice(1);
×
270
    }
UNCOV
271
    return str;
×
272
  }
273
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc