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;