├── .npmignore ├── lib ├── public_api.ts ├── src │ ├── marked-renderer.ts │ ├── prism-plugin.ts │ ├── marked-extensions.ts │ ├── marked-options.ts │ ├── clipboard-options.ts │ ├── sanitize-options.ts │ ├── index.ts │ ├── provide-markdown.ts │ ├── language.pipe.ts │ ├── clipboard-button.component.ts │ ├── markdown.pipe.ts │ ├── language.pipe.spec.ts │ ├── clipboard-button.component.spec.ts │ ├── markdown.module.ts │ ├── markdown.pipe.spec.ts │ ├── katex-options.ts │ ├── markdown.component.ts │ ├── markdown.module.spec.ts │ ├── markdown.component.spec.ts │ └── markdown.service.ts ├── tsconfig.lib.prod.json ├── ng-package.json ├── ng-package.prod.json ├── tsconfig.spec.json ├── tsconfig.lib.json ├── package.json ├── karma.conf.js └── eslint.config.js ├── demo ├── src │ ├── app │ │ ├── bindings │ │ │ ├── remote │ │ │ │ ├── demo.html │ │ │ │ ├── demo.py │ │ │ │ ├── demo.cpp │ │ │ │ ├── language-pipe.html │ │ │ │ ├── demo.md │ │ │ │ ├── markdown-pipe.html │ │ │ │ └── demo.java │ │ │ ├── bindings.component.scss │ │ │ ├── bindings.component.ts │ │ │ └── bindings.component.html │ │ ├── shared │ │ │ ├── anchor │ │ │ │ ├── index.ts │ │ │ │ └── anchor.service.ts │ │ │ ├── http-raw-loader │ │ │ │ ├── index.ts │ │ │ │ └── http-raw-loader.service.ts │ │ │ ├── scrollspy-nav │ │ │ │ ├── index.ts │ │ │ │ ├── scrollspy-nav.component.html │ │ │ │ ├── scrollspy-nav.component.scss │ │ │ │ ├── scrollspy-nav.component.theme.scss │ │ │ │ └── scrollspy-nav.component.ts │ │ │ ├── clipboard-button │ │ │ │ ├── index.ts │ │ │ │ ├── clipboard-button.component.scss │ │ │ │ ├── clipboard-button.component.html │ │ │ │ └── clipboard-button.component.ts │ │ │ └── scrollspy-nav-layout │ │ │ │ ├── index.ts │ │ │ │ ├── scrollspy-nav-layout.component.scss │ │ │ │ ├── scrollspy-nav-layout.animation.ts │ │ │ │ ├── scrollspy-nav-layout.component.html │ │ │ │ └── scrollspy-nav-layout.component.ts │ │ ├── cheat-sheet │ │ │ ├── cheat-sheet.component.scss │ │ │ ├── remote │ │ │ │ ├── horizontal-rule.md │ │ │ │ ├── headers.md │ │ │ │ ├── emphasis.md │ │ │ │ ├── images.md │ │ │ │ ├── blockquotes.md │ │ │ │ ├── code-and-synthax-highlighting.md │ │ │ │ ├── tables.md │ │ │ │ ├── lists.md │ │ │ │ ├── lists-dot.md │ │ │ │ └── links.md │ │ │ ├── cheat-sheet.component.ts │ │ │ └── cheat-sheet.component.html │ │ ├── get-started │ │ │ ├── get-started.component.scss │ │ │ ├── get-started.component.html │ │ │ └── get-started.component.ts │ │ ├── plugins │ │ │ ├── remote │ │ │ │ ├── emoji.html │ │ │ │ ├── katex.html │ │ │ │ ├── mermaid.html │ │ │ │ ├── root-user-without-output.bash │ │ │ │ ├── katex-options.html │ │ │ │ ├── line-numbers.html │ │ │ │ ├── mermaid-options.html │ │ │ │ ├── windows-powershell-with-filter-output.powershell │ │ │ │ ├── line-highlight.html │ │ │ │ ├── non-root-user-with-output.bash │ │ │ │ └── windows-powershell-with-output.powershell │ │ │ ├── plugins.component.scss │ │ │ ├── plugins.component.ts │ │ │ └── plugins.component.html │ │ ├── syntax-highlight │ │ │ ├── syntax-highlight.component.scss │ │ │ ├── remote │ │ │ │ └── for-loop.js │ │ │ ├── syntax-highlight.component.ts │ │ │ └── syntax-highlight.component.html │ │ ├── rerender │ │ │ ├── rerender.component.scss │ │ │ ├── rerender.component.html │ │ │ └── rerender.component.ts │ │ ├── app.constant.ts │ │ ├── app.models.ts │ │ ├── app.animation.ts │ │ ├── app.component.theme.scss │ │ ├── app-routes.ts │ │ ├── app.component.html │ │ ├── app.marked-config.ts │ │ ├── app.config.ts │ │ ├── app.component.scss │ │ └── app.component.ts │ ├── scss │ │ ├── _typography.scss │ │ ├── _dark-theme.scss │ │ ├── _utils.scss │ │ ├── _light-theme.scss │ │ ├── material-theme.scss │ │ └── prism-theme.scss │ ├── main.ts │ ├── global.d.ts │ ├── index.html │ ├── styles.scss │ └── prism.ts ├── public │ ├── favicon.ico │ ├── ngx-markdown.png │ ├── icon-get-started.svg │ ├── icon-chevron-up.svg │ ├── icon-re-render.svg │ ├── icon-syntax-highlight.svg │ ├── icon-bindings.svg │ ├── icon-light-on.svg │ ├── icon-light-off.svg │ ├── icon-cheat-sheet.svg │ ├── icon-plugins.svg │ └── icon-github.svg ├── tsconfig.app.json ├── .browserslistrc └── eslint.config.js ├── .editorconfig ├── .vscode └── settings.json ├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── tsconfig.json ├── eslint.config.js ├── .circleci └── config.yml ├── package.json └── angular.json /.npmignore: -------------------------------------------------------------------------------- 1 | /* 2 | !/dist -------------------------------------------------------------------------------- /lib/public_api.ts: -------------------------------------------------------------------------------- 1 | export * from './src/index'; 2 | -------------------------------------------------------------------------------- /demo/src/app/bindings/remote/demo.html: -------------------------------------------------------------------------------- 1 |
{{ headers$ | async }}
11 | {{ emphasis$ | async }}
17 | 23 | In this example, leading and trailing spaces are shown with with dots (⋅) 24 |
25 |{{ listsDot$ | async }}
26 | {{ links$ | async }}
32 | {{ images$ | async }}
38 | {{ codeAndSynthaxHighlighting$ | async }}
44 | {{ tables$ | async }}
50 | {{ blockquotes$ | async }}
56 | {{ horizontalRule$ | async }}
62 | Html
', 40 | ]; 41 | 42 | useCases.forEach(data => { 43 | component.data = data; 44 | component.ngOnChanges(); 45 | expect(component.render).toHaveBeenCalledWith(data); 46 | spyRender.calls.reset(); 47 | }); 48 | }); 49 | 50 | it('should return value correctly when get', () => { 51 | 52 | const mockData = '# Markdown'; 53 | 54 | component.data = mockData; 55 | 56 | expect(component.data).toBe(mockData); 57 | }); 58 | }); 59 | 60 | describe('src', () => { 61 | 62 | it('should call render with retreived content when set', () => { 63 | 64 | const mockSrc = './src-example/file.md'; 65 | const mockContent = 'source-content'; 66 | 67 | spyOn(component, 'render').and.returnValue(Promise.resolve()); 68 | spyOn(markdownService, 'getSource').and.returnValue(of(mockContent)); 69 | 70 | component.src = mockSrc; 71 | 72 | component.ngOnChanges(); 73 | 74 | expect(markdownService.getSource).toHaveBeenCalledWith(mockSrc); 75 | expect(component.render).toHaveBeenCalledWith(mockContent); 76 | }); 77 | 78 | it('should return value correctly when get', () => { 79 | 80 | const mockSrc = './src-example/file.md'; 81 | 82 | spyOn(markdownService, 'getSource').and.returnValue(of()); 83 | 84 | component.src = mockSrc; 85 | 86 | expect(component.src).toBe(mockSrc); 87 | }); 88 | 89 | it('should emit load when get', fakeAsync(() => { 90 | 91 | const mockSrc = './src-example/file.md'; 92 | const mockSrcReturn = 'src-return-value'; 93 | 94 | spyOn(markdownService, 'getSource').and.returnValue(of(mockSrcReturn)); 95 | spyOn(component.load, 'emit'); 96 | 97 | component.src = mockSrc; 98 | 99 | component.ngOnChanges(); 100 | tick(); 101 | 102 | expect(component.load.emit).toHaveBeenCalledWith(mockSrcReturn); 103 | })); 104 | 105 | it('should emit error when and error occurs', () => { 106 | 107 | const mockSrc = './src-example/file.md'; 108 | const mockError = 'error-x'; 109 | 110 | spyOn(markdownService, 'getSource').and.returnValue(throwError(mockError)); 111 | spyOn(component.error, 'emit'); 112 | 113 | component.src = mockSrc; 114 | 115 | component.ngOnChanges(); 116 | 117 | expect(component.error.emit).toHaveBeenCalledWith(mockError); 118 | }); 119 | }); 120 | 121 | describe('ngAfterViewInit', () => { 122 | 123 | it('should call render method and decodeHtml when neither data or src input property is provided', () => { 124 | 125 | const mockHtmlElement = document.createElement('div'); 126 | mockHtmlElement.innerHTML = 'inner-html'; 127 | 128 | spyOn(markdownService, 'getSource').and.returnValue(of()); 129 | 130 | component.element = new ElementRef(mockHtmlElement); 131 | component.data = undefined; 132 | component.src = undefined; 133 | 134 | spyOn(component, 'render'); 135 | 136 | component.ngAfterViewInit(); 137 | 138 | expect(component.render).toHaveBeenCalledWith(mockHtmlElement.innerHTML, true); 139 | }); 140 | 141 | it('should not call render method when src is provided', () => { 142 | 143 | const mockHtmlElement = document.createElement('div'); 144 | mockHtmlElement.innerHTML = 'inner-html'; 145 | 146 | spyOn(markdownService, 'getSource').and.returnValue(of()); 147 | 148 | component.element = new ElementRef(mockHtmlElement); 149 | component.src = './src-example/file.md'; 150 | 151 | spyOn(component, 'render'); 152 | 153 | component.ngAfterViewInit(); 154 | 155 | expect(component.render).not.toHaveBeenCalled(); 156 | }); 157 | 158 | it('should not call render method when data is provided', () => { 159 | 160 | const mockHtmlElement = document.createElement('div'); 161 | mockHtmlElement.innerHTML = 'inner-html'; 162 | 163 | component.element = new ElementRef(mockHtmlElement); 164 | component.data = '# Markdown'; 165 | 166 | spyOn(component, 'render'); 167 | 168 | component.ngAfterViewInit(); 169 | 170 | expect(component.render).not.toHaveBeenCalled(); 171 | }); 172 | 173 | it('should rerender content on demand', () => { 174 | 175 | spyOn(component, 'loadContent'); 176 | 177 | markdownService.reload(); 178 | 179 | expect(component.loadContent).toHaveBeenCalled(); 180 | }); 181 | }); 182 | 183 | describe('render', () => { 184 | 185 | it('should parse markdown through MarkdownService', async () => { 186 | 187 | const raw = '### Raw'; 188 | 189 | spyOn(markdownService, 'parse'); 190 | 191 | component.inline = true; 192 | component.emoji = false; 193 | component.mermaid = false; 194 | component.disableSanitizer = true; 195 | await component.render(raw, true); 196 | 197 | expect(markdownService.parse).toHaveBeenCalledWith(raw, { 198 | decodeHtml: true, 199 | inline: true, 200 | emoji: false, 201 | mermaid: false, 202 | disableSanitizer: true, 203 | }); 204 | }); 205 | 206 | it('should set innerHTML with parsed markdown', async () => { 207 | 208 | const raw = '### Raw'; 209 | const parsed = ' elements
366 | const preElements = element.querySelectorAll('pre');
367 | for (let i = 0; i < preElements.length; i++) {
368 | const preElement = preElements.item(i);
369 |
370 | // create wrapper element
371 | const preWrapperElement = document.createElement('div');
372 | preWrapperElement.style.position = 'relative';
373 | preElement.parentNode!.insertBefore(preWrapperElement, preElement);
374 | preWrapperElement.appendChild(preElement);
375 |
376 | // create toolbar element
377 | const toolbarWrapperElement = document.createElement('div');
378 | toolbarWrapperElement.classList.add('markdown-clipboard-toolbar');
379 | toolbarWrapperElement.style.position = 'absolute';
380 | toolbarWrapperElement.style.top = '.5em';
381 | toolbarWrapperElement.style.right = '.5em';
382 | toolbarWrapperElement.style.zIndex = '1';
383 | preWrapperElement.insertAdjacentElement('beforeend', toolbarWrapperElement);
384 |
385 | // register listener to show/hide toolbar
386 | preWrapperElement.onmouseenter = () => toolbarWrapperElement.classList.add('hover');
387 | preWrapperElement.onmouseleave = () => toolbarWrapperElement.classList.remove('hover');
388 |
389 | // declare embeddedViewRef holding variable
390 | let embeddedViewRef: EmbeddedViewRef;
391 |
392 | // use provided component via input property
393 | // or provided via ClipboardOptions provider
394 | if (buttonComponent) {
395 | const componentRef = viewContainerRef.createComponent(buttonComponent);
396 | embeddedViewRef = componentRef.hostView as EmbeddedViewRef;
397 | componentRef.changeDetectorRef.markForCheck();
398 | }
399 | // use provided template via input property
400 | else if (buttonTemplate) {
401 | embeddedViewRef = viewContainerRef.createEmbeddedView(buttonTemplate);
402 | }
403 | // use default component
404 | else {
405 | const componentRef = viewContainerRef.createComponent(ClipboardButtonComponent);
406 | embeddedViewRef = componentRef.hostView as EmbeddedViewRef;
407 | componentRef.changeDetectorRef.markForCheck();
408 | }
409 |
410 | // declare clipboard instance variable
411 | let clipboardInstance: typeof ClipboardJS;
412 |
413 | // attach clipboard.js to root node
414 | embeddedViewRef.rootNodes.forEach((node: HTMLElement) => {
415 | toolbarWrapperElement.appendChild(node);
416 | clipboardInstance = new ClipboardJS(node, { text: () => preElement.innerText });
417 | });
418 |
419 | // destroy clipboard instance when view is destroyed
420 | embeddedViewRef.onDestroy(() => clipboardInstance.destroy());
421 | }
422 | }
423 |
424 | private renderMermaid(element: HTMLElement, options: MermaidAPI.MermaidConfig = this.DEFAULT_MERMAID_OPTIONS): void {
425 | if (!isPlatformBrowser(this.platform)) {
426 | return;
427 | }
428 | if (typeof mermaid === 'undefined' || typeof mermaid.initialize === 'undefined') {
429 | throw new Error(errorMermaidNotLoaded);
430 | }
431 | const mermaidElements = element.querySelectorAll('.mermaid');
432 | if (mermaidElements.length === 0) {
433 | return;
434 | }
435 | mermaid.initialize(options);
436 | mermaid.run({ nodes: mermaidElements });
437 | }
438 |
439 | private trimIndentation(markdown: string): string {
440 | if (!markdown) {
441 | return '';
442 | }
443 | let indentStart: number;
444 | return markdown
445 | .split('\n')
446 | .map(line => {
447 | let lineIdentStart = indentStart;
448 | if (line.length > 0) {
449 | lineIdentStart = isNaN(lineIdentStart)
450 | ? line.search(/\S|$/)
451 | : Math.min(line.search(/\S|$/), lineIdentStart);
452 | }
453 | if (isNaN(indentStart)) {
454 | indentStart = lineIdentStart;
455 | }
456 | return lineIdentStart
457 | ? line.substring(lineIdentStart)
458 | : line;
459 | }).join('\n');
460 | }
461 |
462 | private async sanitizeHtml(html: string | Promise): Promise {
463 | if (isSanitizeFunction(this.sanitize)) {
464 | return this.sanitize(await html);
465 | }
466 | if (this.sanitize !== SecurityContext.NONE) {
467 | return this.sanitizer.sanitize(this.sanitize ?? this.DEFAULT_SECURITY_CONTEXT, html) ?? '';
468 | }
469 | return html;
470 | }
471 | }
472 |
--------------------------------------------------------------------------------
/demo/src/app/plugins/plugins.component.html:
--------------------------------------------------------------------------------
1 |
2 | Plugins
3 |
4 |
5 |
6 | Before to use any plugin, make sure you've installed the required libraries by following the [installation](/get-started#installation) section of the __Get Started__ page.
7 |
8 |
9 |
10 |
11 | Emoji plugin
12 |
13 |
14 | #### Emoji-Toolkit file to include
15 | ```javascript
16 | node_modules/emoji-toolkit/lib/js/joypixels.min.js
17 | ```
18 |
19 | #### Directive
20 | `emoji` - activate emoji plugin
21 |
22 | ### Example
23 |
24 |
25 |
26 | Using `emoji` input property on `markdown` component, directive or pipe allows you to convert shortnames to native unicode emojis.
27 |
28 |
29 |
30 |
31 |
32 | The example below illustrate `emoji` directive in action.
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | > :blue_book: You can refer to this [Emoji Cheat Sheet](https://github.com/ikatyang/emoji-cheat-sheet/blob/master/README.md) for a complete list of _shortnames_.
45 |
46 |
47 |
48 |
49 |
50 | Line Numbers plugin
51 |
52 |
53 | #### Prism files to include
54 | ```javascript
55 | node_modules/prismjs/plugins/line-numbers/prism-line-numbers.css
56 | node_modules/prismjs/plugins/line-numbers/prism-line-numbers.js
57 | ```
58 |
59 | #### Directive
60 | `lineNumbers` - activate line numbers plugin
61 |
62 | #### Attributes
63 | `start` - offset number for the first display line
64 |
65 | ### Example
66 |
67 |
68 |
69 | Using `lineNumbers` input property on `markdown` component, directive or pipe allows you to add line number at the beginning of each lines of code block.
70 |
71 |
72 |
73 |
74 |
75 | The example below uses `lineNumbers` directive which uses default line offset of 1.
76 |
77 |
78 |
79 | ```javascript
80 | var result = square(2);
81 |
82 | function square(number) {
83 | return number * number;
84 | }
85 | ```
86 |
87 |
88 |
89 | Optionally you can use `start` to specify the offset number for the first display line.
90 |
91 |
92 |
93 | In the example below line offset is set to 5 using `start` input property.
94 |
95 |
96 |
97 | ```javascript
98 | var result = root(2);
99 |
100 | function root(x, n) {
101 | try {
102 | var negate = n % 2 == 1 && x < 0;
103 | if (negate)
104 | x = -x;
105 | var possible = Math.pow(x, 1 / n);
106 | n = Math.pow(possible, n);
107 | if (Math.abs(x - n) < 1 && (x > 0 == n > 0))
108 | return negate ? -possible : possible;
109 | } catch (e) { }
110 | }
111 | ```
112 |
113 |
114 |
115 |
116 |
117 | Line Highlight plugin
118 |
119 |
120 | #### Prism files to include
121 | ```javascript
122 | node_modules/prismjs/plugins/line-highlight/prism-line-highlight.css
123 | node_modules/prismjs/plugins/line-highlight/prism-line-highlight.js
124 | ```
125 |
126 | #### Directive
127 | `lineHighlight` - activate line highlight plugin
128 |
129 | #### Attributes
130 | `line` - lines to highlight (i.e.: 6, 11-15)
131 | `lineOffset` - starting offset for line numbers
132 |
133 | ### Example
134 |
135 |
136 |
137 | You can highlight different lines by adding `lineHighlight` directive on the `markdown` component/directive.
138 |
139 | Use `line` input property to specify the line(s) to highlight and optionally there is a `lineOffset` property to specify the starting line of code your snippet represents.
140 |
141 |
142 |
143 |
144 |
145 | In the example below `line` 6 and 10 to 16 are highlight using a `lineOffset` of 5.
146 |
147 |
148 |
149 | ```javascript
150 | var result = root(2);
151 |
152 | function root(x, n) {
153 | try {
154 | var negate = n % 2 == 1 && x < 0;
155 | if (negate)
156 | x = -x;
157 | var possible = Math.pow(x, 1 / n);
158 | n = Math.pow(possible, n);
159 | if (Math.abs(x - n) < 1 && (x > 0 == n > 0))
160 | return negate ? -possible : possible;
161 | } catch (e) { }
162 | }
163 | ```
164 |
165 |
166 |
167 |
168 |
169 | Command Line plugin
170 |
171 |
172 | #### Prism file(s) to include
173 | ```javascript
174 | node_modules/prismjs/plugins/command-line/prism-command-line.css
175 | node_modules/prismjs/plugins/command-line/prism-command-line.min.js
176 | ```
177 |
178 | #### Directive
179 | `commandLine` - activate command-line display
180 |
181 | #### Attributes
182 | `host` - host name
183 | `output` - lines to be presented as output (optional)
184 | `filterOutput` - prefix to automatically present lines as output (optional)
185 | `prompt` - data prompt
186 | `user` - user name
187 |
188 | ### Example
189 |
190 |
191 |
192 | Root user without output
193 |
194 | ```html
195 | <markdown
196 | commandLine
197 | [user]="'root'"
198 | [host]="'localhost'"
199 | [src]="'path/to/file.bash'">
200 | </markdown>
201 | ```
202 |
203 |
204 |
209 |
210 |
211 |
212 | Non-Root User With Output
213 |
214 | ```html
215 | <markdown
216 | commandLine
217 | [user]="'chris'"
218 | [host]="'remotehost'"
219 | [output]="'2, 4-8'"
220 | [src]="'path/to/file.bash'">
221 | </markdown>
222 | ```
223 |
224 |
225 |
230 |
231 |
232 |
233 | Windows PowerShell With Output
234 |
235 | ```html
236 | <markdown
237 | commandLine
238 | [prompt]="'PS C:\Users\Chris>'"
239 | [output]="'2-19'"
240 | [src]="'path/to/file.bash'">
241 | </markdown>
242 | ```
243 |
244 |
245 | '"
248 | [output]="'2-19'"
249 | [src]="'app/plugins/remote/windows-powershell-with-output.powershell'">
250 |
251 |
252 |
253 | Windows PowerShell With Filter Output
254 |
255 | ```html
256 | <markdown
257 | commandLine
258 | [prompt]="'PS C:\Users\Chris>'"
259 | [filterOutput]="'(out)'">
260 | ```powershell
261 | Get-Date
262 | (out)
263 | (out)Sunday, November 7, 2021 8:19:21 PM
264 | (out)
265 | ```
266 | </markdown>
267 | ```
268 |
269 |
270 | '"
273 | [filterOutput]="'(out)'"
274 | [src]="'app/plugins/remote/windows-powershell-with-filter-output.powershell'">
275 |
276 |
277 |
278 |
279 |
280 | KaTeX plugin
281 |
282 |
283 | #### KaTeX files to include
284 | ```javascript
285 | node_modules/katex/dist/katex.min.css
286 | node_modules/katex/dist/katex.min.js
287 | node_modules/katex/dist/contrib/auto-render.min.js
288 | ```
289 |
290 | #### Directive
291 | `katex` - activate KaTeX plugin
292 |
293 | #### Attributes
294 | `katexOptions` - combine [KaTeX options](https://katex.org/docs/options.html) and [Auto-Renderer options](https://katex.org/docs/autorender.html#api)
295 |
296 | ### Example
297 |
298 |
299 |
300 | You can render KaTex expression by adding `katex` directive on the `markdown` component/directive.
301 |
302 |
303 |
304 |
305 |
306 | The example below illustrate `katex` directive in action.
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 | Optionally, you can specify both [KaTeX options](https://katex.org/docs/options.html) and [Auto-Renderer options](https://katex.org/docs/autorender.html#api) using `katexOptions` property.
319 |
320 | **example.component.ts**
321 | ```typescript
322 | import { KatexOptions } from 'ngx-markdown';
323 |
324 | public options: KatexOptions = {
325 | displayMode: true,
326 | throwOnError: false,
327 | errorColor: '#cc0000',
328 | delimiters: [...],
329 | ...
330 | };
331 | ```
332 |
333 | **example.component.html**
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 | Mermaid plugin
342 |
343 |
344 | #### Mermaid file to include
345 | ```javascript
346 | node_modules/mermaid/dist/mermaid.min.js
347 | ```
348 |
349 | #### Directive
350 | `mermaid` - activate mermaid plugin
351 |
352 | #### Attributes
353 | `mermaidOptions` - mermaid [configuration options](https://mermaid.js.org/config/schema-docs/config.html#mermaid-config-properties)
354 |
355 | ### Example
356 |
357 |
358 |
359 | Using `mermaid` input property on `markdown` component, directive or pipe allows you to use [mermaid](https://mermaid-js.github.io/) syntax to generate diagrams and flowcharts.
360 |
361 |
362 |
363 |
364 |
365 | The example below illustrate `mermaid` directive in action.
366 |
367 |
368 |
369 |
370 |
371 |
372 |
373 |
374 |
375 |
376 |
377 | #### Global configuration
378 |
379 | You can provide a global configuration for mermaid [configuration options](https://mermaid.js.org/config/schema-docs/config.html#mermaid-config-properties) to use across your application with the `mermaidOptions` in the `MarkdownModuleConfig` either with `provideMarkdown` provide-function for standalone components or `MarkdownModule.forRoot()` for module configuration.
380 |
381 | ```typescript
382 | // using the `provideMarkdown` function
383 | provideMarkdown({
384 | mermaidOptions: {
385 | provide: MERMAID_OPTIONS,
386 | useValue: {
387 | darkMode: true,
388 | look: 'handDrawn',
389 | ...
390 | },
391 | },
392 | }),
393 |
394 | // using the `MarkdownModule` import
395 | MarkdownModule.forRoot({
396 | mermaidOptions: {
397 | provide: MERMAID_OPTIONS,
398 | useValue: {
399 | darkMode: true,
400 | look: 'handDrawn',
401 | ...
402 | },
403 | },
404 | }),
405 | ```
406 |
407 | #### Component configuration
408 |
409 | Additionally, you can specify mermaid [configuration options](https://mermaid.js.org/config/schema-docs/config.html#mermaid-config-properties) on component directly using `mermaidOptions` property.
410 |
411 | **example.component.ts**
412 | ```typescript
413 | import { MermaidAPI } from 'ngx-markdown';
414 |
415 | public options: MermaidAPI.MermaidConfig = {
416 | darkMode: true,
417 | look: 'handDrawn',
418 | ...
419 | };
420 | ```
421 |
422 | **example.component.html**
423 |
424 |
425 |
426 |
427 |
428 | > :blue_book: You can refer to this [Mermaid](https://mermaid-js.github.io/) documentation for complete usage syntax.
429 |
430 |
431 |
432 |
433 |
434 | Clipboard plugin
435 |
436 |
437 | #### Clipboard file(s) to include
438 |
439 | ```javascript
440 | node_modules/clipboard/dist/clipboard.min.js
441 | ```
442 |
443 | #### Directive
444 | `clipboard` - activate copy-to-clipboard plugin
445 |
446 | #### Attributes
447 | `clipboardButtonComponent` - component `Type<any>` to use as copy-to-clipboard button
448 | `clipboardButtonTemplate` - template reference `TemplateRef<T>` to use as copy-to-clipboard button
449 |
450 | #### CSS Selectors
451 | `markdown-clipboard-toolbar` - toolbar wrapper
452 | `markdown-clipboard-toolbar.hover` - toolbar wrapper during mouse hover
453 | `markdown-clipboard-button` - default button
454 | `markdown-clipboard-button.copied` - default button during "copied" state
455 |
456 | ### Example
457 |
458 |
459 |
460 | #### Default button
461 |
462 | The `clipboard` plugin provide an unstyled default button with a default behavior out of the box if no alternative is used.
463 |
464 | ```javascript
465 | const example = 'the default clipboard button with default behavior';
466 | ```
467 |
468 |
469 |
470 | #### Customize toolbar
471 |
472 | The clipboard button is placed inside a wrapper element that can be customize using the `.markdown-clipboard-toolbar` CSS selector in your global `styles.css/scss` file.
473 |
474 | This allows to override the default positionning of the clipboard button and play with the visibility of the button using the `.hover` CSS selector that is applied on the toolbar when the mouse cursor enters and leaves the code block element.
475 |
476 | ```css
477 | .markdown-clipboard-toolbar {
478 | top: 16px;
479 | right: 16px;
480 | opacity: 0;
481 | transition: opacity 250ms ease-out;
482 | }
483 |
484 | .markdown-clipboard-toolbar.hover {
485 | opacity: 1;
486 | }
487 | ```
488 |
489 |
490 |
491 | #### Customize default button
492 |
493 | The default button can be customized using the `.markdown-clipboard-button` CSS selector in your global `styles.css/scss` file. You can also customized the "copied" state happening after the button is clicked using the `.copied` CSS selector.
494 |
495 | ```css
496 | .markdown-clipboard-button {
497 | background-color: rgba(255, 255, 255, 0.07);
498 | border: none;
499 | border-radius: 4px;
500 | color: #ffffff;
501 | cursor: pointer;
502 | font-size: 11px;
503 | padding: 4px 0;
504 | width: 50px;
505 | transition: all 250ms ease-out;
506 | }
507 |
508 | .markdown-clipboard-button:hover {
509 | background-color: rgba(255, 255, 255, 0.14);
510 | }
511 |
512 | .markdown-clipboard-button:active {
513 | transform: scale(0.95);
514 | }
515 |
516 | .markdown-clipboard-button.copied {
517 | background-color: rgba(0, 255, 0, 0.1);
518 | color: #00ff00;
519 | }
520 | ```
521 |
522 |
523 |
524 | #### Using global configuration
525 |
526 | You can provide a custom component to use globaly across your application with the `clipboardOptions` in the `MarkdownModuleConfig` either with `provideMarkdown` provide-function for standalone components or `MarkdownModule.forRoot()` for module configuration.
527 |
528 | ```typescript
529 | // using the `provideMarkdown` function
530 | provideMarkdown({
531 | clipboardOptions: {
532 | provide: CLIPBOARD_OPTIONS,
533 | useValue: {
534 | buttonComponent: ClipboardButtonComponent,
535 | },
536 | },
537 | })
538 |
539 | // using `MarkdownModule` import
540 | MarkdownModule.forRoot({
541 | clipboardOptions: {
542 | provide: CLIPBOARD_OPTIONS,
543 | useValue: {
544 | buttonComponent: ClipboardButtonComponent,
545 | },
546 | },
547 | }),
548 | ```
549 |
550 |
551 |
552 | #### Using a component
553 |
554 | You can also provide your custom component using the `clipboardButtonComponent` input property when using the `clipboard` directive.
555 |
556 | ```typescript
557 | import { Component } from '@angular/core';
558 |
559 | @Component({
560 | selector: 'app-clipboard-button',
561 | template: `<button (click)="onClick()">Copy</button>`,
562 | })
563 | export class ClipboardButtonComponent {
564 | onClick() {
565 | alert('Copied to clipboard!');
566 | }
567 | }
568 | ```
569 |
570 | ```typescript
571 | import { ClipboardButtonComponent } from './clipboard-button-component';
572 |
573 | @Component({ ... })
574 | export class ExampleComponent {
575 | readonly clipboardButton = ClipboardButtonComponent;
576 | }
577 | ```
578 |
579 | ```html
580 | <markdown clipboard [clipboardButtonComponent]="clipboardButton"></markdown>
581 | ```
582 |
583 |
584 |
585 |
590 |
591 |
592 |
593 | #### Using ng-template
594 |
595 | Alternatively, the `clipboard` directive can be used in conjonction with `ng-template` to provide a custom button implementation via the `clipboardButtonTemplate` input property on the `markdown` component.
596 |
597 | ```html
598 | <ng-template #buttonTemplate>
599 | <button (click)="onCopyToClipboard()">...</button>
600 | </ng-template>
601 |
602 | <markdown clipboard [clipboardButtonTemplate]="buttonTemplate"></markdown>
603 | ```
604 |
605 |
606 |
--------------------------------------------------------------------------------