mirror of
				https://github.com/TriliumNext/Notes.git
				synced 2025-10-31 13:01:31 +08:00 
			
		
		
		
	feat(import/markdown): basic support for admonitions
This commit is contained in:
		
							parent
							
								
									6d67e69e2f
								
							
						
					
					
						commit
						535233fec8
					
				| @ -5,6 +5,17 @@ import turndownPluginGfm from "@joplin/turndown-plugin-gfm"; | |||||||
| 
 | 
 | ||||||
| let instance: TurndownService | null = null; | let instance: TurndownService | null = null; | ||||||
| 
 | 
 | ||||||
|  | // TODO: Move this to a dedicated file someday.
 | ||||||
|  | export const ADMONITION_TYPE_MAPPINGS: Record<string, string> = { | ||||||
|  |     note: "NOTE", | ||||||
|  |     tip: "TIP", | ||||||
|  |     important: "IMPORTANT", | ||||||
|  |     caution: "CAUTION", | ||||||
|  |     warning: "WARNING" | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const DEFAULT_ADMONITION_TYPE = ADMONITION_TYPE_MAPPINGS.note; | ||||||
|  | 
 | ||||||
| const fencedCodeBlockFilter: TurndownService.Rule = { | const fencedCodeBlockFilter: TurndownService.Rule = { | ||||||
|     filter: function (node, options) { |     filter: function (node, options) { | ||||||
|         return options.codeBlockStyle === "fenced" && node.nodeName === "PRE" && node.firstChild !== null && node.firstChild.nodeName === "CODE"; |         return options.codeBlockStyle === "fenced" && node.nodeName === "PRE" && node.firstChild !== null && node.firstChild.nodeName === "CODE"; | ||||||
| @ -102,19 +113,9 @@ function buildImageFilter() { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function buildAdmonitionFilter() { | function buildAdmonitionFilter() { | ||||||
|     const admonitionTypeMappings: Record<string, string> = { |  | ||||||
|         note: "NOTE", |  | ||||||
|         tip: "TIP", |  | ||||||
|         important: "IMPORTANT", |  | ||||||
|         caution: "CAUTION", |  | ||||||
|         warning: "WARNING" |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     const defaultAdmonitionType = admonitionTypeMappings.note; |  | ||||||
| 
 |  | ||||||
|     function parseAdmonitionType(_node: Node) { |     function parseAdmonitionType(_node: Node) { | ||||||
|         if (!("getAttribute" in _node)) { |         if (!("getAttribute" in _node)) { | ||||||
|             return defaultAdmonitionType; |             return DEFAULT_ADMONITION_TYPE; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const node = _node as Element; |         const node = _node as Element; | ||||||
| @ -125,13 +126,13 @@ function buildAdmonitionFilter() { | |||||||
|                 continue; |                 continue; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             const mappedType = admonitionTypeMappings[className]; |             const mappedType = ADMONITION_TYPE_MAPPINGS[className]; | ||||||
|             if (mappedType) { |             if (mappedType) { | ||||||
|                 return mappedType; |                 return mappedType; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return defaultAdmonitionType; |         return DEFAULT_ADMONITION_TYPE; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const admonitionFilter: TurndownService.Rule = { |     const admonitionFilter: TurndownService.Rule = { | ||||||
|  | |||||||
| @ -74,4 +74,31 @@ second line 2</code></pre>`; | |||||||
|         expect(markdownService.renderToHtml(input, "Troubleshooting")).toBe(expected); |         expect(markdownService.renderToHtml(input, "Troubleshooting")).toBe(expected); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  |     it("imports admonitions properly", () => { | ||||||
|  |         const space = " ";  // editor config trimming space.
 | ||||||
|  |         const input = trimIndentation`\ | ||||||
|  |             Before | ||||||
|  | 
 | ||||||
|  |             > [!NOTE] | ||||||
|  |             > This is a note. | ||||||
|  | 
 | ||||||
|  |             > [!TIP] | ||||||
|  |             > This is a tip. | ||||||
|  | 
 | ||||||
|  |             > [!IMPORTANT] | ||||||
|  |             > This is a very important information. | ||||||
|  | 
 | ||||||
|  |             > [!CAUTION] | ||||||
|  |             > This is a caution. | ||||||
|  | 
 | ||||||
|  |             > [!WARNING] | ||||||
|  |             > ## Title goes here | ||||||
|  |             >${space} | ||||||
|  |             > This is a warning. | ||||||
|  | 
 | ||||||
|  |             After`;
 | ||||||
|  |         const expected = `<p>Before</p><aside class="admonition note"><p>This is a note.</p></aside><aside class="admonition tip"><p>This is a tip.</p></aside><aside class="admonition important"><p>This is a very important information.</p></aside><aside class="admonition caution"><p>This is a caution.</p></aside><aside class="admonition warning"><h2>Title goes here</h2><p>This is a warning.</p></aside><p>After</p>`; | ||||||
|  |         expect(markdownService.renderToHtml(input, "Title")).toBe(expected); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
| }); | }); | ||||||
|  | |||||||
| @ -3,6 +3,7 @@ | |||||||
| import { parse, Renderer, type Tokens } from "marked"; | import { parse, Renderer, type Tokens } from "marked"; | ||||||
| import { minify as minifyHtml } from "html-minifier"; | import { minify as minifyHtml } from "html-minifier"; | ||||||
| 
 | 
 | ||||||
|  | // Keep renderer code up to date with https://github.com/markedjs/marked/blob/master/src/Renderer.ts.
 | ||||||
| const renderer = new Renderer({ async: false }); | const renderer = new Renderer({ async: false }); | ||||||
| renderer.code = ({ text, lang, escaped }: Tokens.Code) => { | renderer.code = ({ text, lang, escaped }: Tokens.Code) => { | ||||||
|     if (!text) { |     if (!text) { | ||||||
| @ -12,10 +13,29 @@ renderer.code = ({ text, lang, escaped }: Tokens.Code) => { | |||||||
|     const ckEditorLanguage = getNormalizedMimeFromMarkdownLanguage(lang); |     const ckEditorLanguage = getNormalizedMimeFromMarkdownLanguage(lang); | ||||||
|     return `<pre><code class="language-${ckEditorLanguage}">${text}</code></pre>`; |     return `<pre><code class="language-${ckEditorLanguage}">${text}</code></pre>`; | ||||||
| }; | }; | ||||||
|  | renderer.blockquote = ({ tokens }: Tokens.Blockquote) => { | ||||||
|  |     const body = renderer.parser.parse(tokens); | ||||||
|  | 
 | ||||||
|  |     const admonitionMatch = /^<p>\[\!([A-Z]+)\]/.exec(body); | ||||||
|  |     if (Array.isArray(admonitionMatch) && admonitionMatch.length === 2) { | ||||||
|  |         const type = admonitionMatch[1].toLowerCase(); | ||||||
|  | 
 | ||||||
|  |         if (ADMONITION_TYPE_MAPPINGS[type]) { | ||||||
|  |             const bodyWithoutHeader = body | ||||||
|  |                 .replace(/^<p>\[\!([A-Z]+)\]/, "<p>") | ||||||
|  |                 .replace(/^<p><\/p>/, ""); // Having a heading will generate an empty paragraph that we need to remove.
 | ||||||
|  | 
 | ||||||
|  |             return `<aside class="admonition ${type}">\n${bodyWithoutHeader}</aside>\n`; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return `<blockquote>\n${body}</blockquote>\n`; | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
| import htmlSanitizer from "../html_sanitizer.js"; | import htmlSanitizer from "../html_sanitizer.js"; | ||||||
| import importUtils from "./utils.js"; | import importUtils from "./utils.js"; | ||||||
| import { getMimeTypeFromHighlightJs, MIME_TYPE_AUTO, normalizeMimeTypeForCKEditor } from "./mime_type_definitions.js"; | import { getMimeTypeFromHighlightJs, MIME_TYPE_AUTO, normalizeMimeTypeForCKEditor } from "./mime_type_definitions.js"; | ||||||
|  | import { ADMONITION_TYPE_MAPPINGS } from "../export/markdown.js"; | ||||||
| 
 | 
 | ||||||
| function renderToHtml(content: string, title: string) { | function renderToHtml(content: string, title: string) { | ||||||
|     let html = parse(content, { |     let html = parse(content, { | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Elian Doran
						Elian Doran