diff --git a/apps/server/src/services/import/markdown.spec.ts b/apps/server/src/services/import/markdown.spec.ts index 916d461cb..1dcf488b1 100644 --- a/apps/server/src/services/import/markdown.spec.ts +++ b/apps/server/src/services/import/markdown.spec.ts @@ -293,4 +293,10 @@ $$`; expect(markdownService.renderToHtml(input, "Title")).toStrictEqual(expected); }); + it("supports wikilink with image (transclusion)", () => { + const input = `heres the handsome boy ![[assets/2025-06-20_14-05-20.jpeg]]`; + const expected = `

heres the handsome boy

`; + expect(markdownService.renderToHtml(input, "Title")).toStrictEqual(expected); + }); + }); diff --git a/apps/server/src/services/import/markdown.ts b/apps/server/src/services/import/markdown.ts index 9546b0615..177211427 100644 --- a/apps/server/src/services/import/markdown.ts +++ b/apps/server/src/services/import/markdown.ts @@ -1,12 +1,14 @@ "use strict"; -import { parse, Renderer, type Tokens } from "marked"; +import { parse, Renderer, use, type Tokens } from "marked"; import htmlSanitizer from "../html_sanitizer.js"; import importUtils from "./utils.js"; import { getMimeTypeFromMarkdownName, MIME_TYPE_AUTO } from "@triliumnext/commons"; import { ADMONITION_TYPE_MAPPINGS } from "../export/markdown.js"; import utils from "../utils.js"; import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons"; +import wikiLinkTransclusion from "./markdown/wikilink_transclusion.js"; +import wikiLinkInternalLink from "./markdown/wikilink_internal_link.js"; /** * Keep renderer code up to date with https://github.com/markedjs/marked/blob/master/src/Renderer.ts. @@ -115,12 +117,6 @@ class CustomMarkdownRenderer extends Renderer { return `
${body}
`; } - text(token: Tokens.Text | Tokens.Escape): string { - let text = super.text(token); - text = processWikiLinks(text); - return text; - } - } function renderToHtml(content: string, title: string) { @@ -130,6 +126,14 @@ function renderToHtml(content: string, title: string) { // Extract formulas and replace them with placeholders to prevent interference from Markdown rendering const { processedText, placeholderMap: formulaMap } = extractFormulas(content); + use({ + // Order is important, especially for wikilinks. + extensions: [ + wikiLinkTransclusion, + wikiLinkInternalLink + ] + }); + let html = parse(processedText, { async: false, renderer: renderer @@ -218,11 +222,6 @@ function restoreFromMap(text: string, map: Map): string { return text.replace(new RegExp(pattern, 'g'), match => map.get(match) ?? match); } -function processWikiLinks(paragraph: string) { - paragraph = paragraph.replaceAll(/\[\[([^\[\]]+)\]\]/g, `$1`); - return paragraph; -} - const renderer = new CustomMarkdownRenderer({ async: false }); export default { diff --git a/apps/server/src/services/import/markdown/wikilink_internal_link.ts b/apps/server/src/services/import/markdown/wikilink_internal_link.ts new file mode 100644 index 000000000..dde084700 --- /dev/null +++ b/apps/server/src/services/import/markdown/wikilink_internal_link.ts @@ -0,0 +1,29 @@ +import { TokenizerAndRendererExtension } from "marked"; + +const wikiLinkInternalLink: TokenizerAndRendererExtension = { + name: "wikilinkInternalLink", + level: "inline", + + start(src: string) { + return src.indexOf('[['); + }, + + tokenizer(src) { + const match = /^\[\[([^\]]+?)\]\]/.exec(src); + if (match) { + return { + type: 'wikilinkInternalLink', + raw: match[0], + text: match[1].trim(), // what shows as link text + href: match[1].trim() + }; + } + }, + + renderer(token) { + return `${token.text}`; + } + +} + +export default wikiLinkInternalLink; diff --git a/apps/server/src/services/import/markdown/wikilink_transclusion.ts b/apps/server/src/services/import/markdown/wikilink_transclusion.ts new file mode 100644 index 000000000..e15f9426a --- /dev/null +++ b/apps/server/src/services/import/markdown/wikilink_transclusion.ts @@ -0,0 +1,30 @@ +import type { TokenizerAndRendererExtension } from "marked"; + +/** + * The terminology is inspired by https://silverbullet.md/Transclusions. + */ +const wikiLinkTransclusion: TokenizerAndRendererExtension = { + name: "wikiLinkTransclusion", + level: "inline", + + start(src: string) { + return src.match(/!\[\[/)?.index; + }, + + tokenizer(src) { + const match = /^!\[\[([^\]]+?)\]\]/.exec(src); + if (match) { + return { + type: "wikiLinkTransclusion", + raw: match[0], + href: match[1].trim(), + }; + } + }, + + renderer(token) { + return ``; + } +}; + +export default wikiLinkTransclusion;