diff --git a/apps/server/package.json b/apps/server/package.json
index bb099d98b..6b2c56ca7 100644
--- a/apps/server/package.json
+++ b/apps/server/package.json
@@ -126,7 +126,7 @@
"swagger-jsdoc": "6.2.8",
"time2fa": "1.4.2",
"tmp": "0.2.5",
- "turndown": "7.2.2",
+ "turnish": "1.7.1",
"unescape": "1.0.1",
"vite": "7.3.1",
"ws": "8.19.0",
diff --git a/apps/server/src/services/export/markdown.spec.ts b/apps/server/src/services/export/markdown.spec.ts
index 4fa2913fc..c7370a84a 100644
--- a/apps/server/src/services/export/markdown.spec.ts
+++ b/apps/server/src/services/export/markdown.spec.ts
@@ -387,4 +387,23 @@ describe("Markdown export", () => {
expect(markdownExportService.toMarkdown(html)).toBe(expected);
});
+ it("maintains escaped HTML tags", () => {
+ const html = /*html*/`
<div>Hello World</div>
`;
+ const expected = `\\Hello World\\
`;
+ expect(markdownExportService.toMarkdown(html)).toBe(expected);
+ });
+
+ it("escapes HTML tags inside list", () => {
+ const html = trimIndentation/*html*/`\
+
+ `;
+ const expected = trimIndentation`\
+ * \\ is note.`;
+ expect(markdownExportService.toMarkdown(html)).toBe(expected);
+ });
+
});
diff --git a/apps/server/src/services/export/markdown.ts b/apps/server/src/services/export/markdown.ts
index ed16c6b30..0d7a8deb4 100644
--- a/apps/server/src/services/export/markdown.ts
+++ b/apps/server/src/services/export/markdown.ts
@@ -1,9 +1,7 @@
-"use strict";
-
-import TurndownService, { type Rule } from "turndown";
import { gfm } from "@triliumnext/turndown-plugin-gfm";
+import Turnish, { type Rule } from "turnish";
-let instance: TurndownService | null = null;
+let instance: Turnish | null = null;
// TODO: Move this to a dedicated file someday.
export const ADMONITION_TYPE_MAPPINGS: Record = {
@@ -16,12 +14,12 @@ export const ADMONITION_TYPE_MAPPINGS: Record = {
export const DEFAULT_ADMONITION_TYPE = ADMONITION_TYPE_MAPPINGS.note;
-const fencedCodeBlockFilter: TurndownService.Rule = {
- filter: function (node, options) {
+const fencedCodeBlockFilter: Turnish.Rule = {
+ filter (node, options) {
return options.codeBlockStyle === "fenced" && node.nodeName === "PRE" && node.firstChild !== null && node.firstChild.nodeName === "CODE";
},
- replacement: function (content, node, options) {
+ replacement (content, node, options) {
if (!node.firstChild || !("getAttribute" in node.firstChild) || typeof node.firstChild.getAttribute !== "function") {
return content;
}
@@ -29,14 +27,15 @@ const fencedCodeBlockFilter: TurndownService.Rule = {
const className = node.firstChild.getAttribute("class") || "";
const language = rewriteLanguageTag((className.match(/language-(\S+)/) || [null, ""])[1]);
- return "\n\n" + options.fence + language + "\n" + node.firstChild.textContent + "\n" + options.fence + "\n\n";
+ return `\n\n${ options.fence }${language }\n${ node.firstChild.textContent }\n${ options.fence }\n\n`;
}
};
function toMarkdown(content: string) {
if (instance === null) {
- instance = new TurndownService({
+ instance = new Turnish({
headingStyle: "atx",
+ bulletListMarker: "*",
codeBlockStyle: "fenced",
blankReplacement(content, node, options) {
if (node.nodeName === "SECTION" && (node as HTMLElement).classList.contains("include-note")) {
@@ -44,7 +43,7 @@ function toMarkdown(content: string) {
}
// Original implementation as per https://github.com/mixmark-io/turndown/blob/master/src/turndown.js.
- return ("isBlock" in node && node.isBlock) ? '\n\n' : ''
+ return ("isBlock" in node && node.isBlock) ? '\n\n' : '';
}
});
// Filter is heavily based on: https://github.com/mixmark-io/turndown/issues/274#issuecomment-458730974
@@ -59,7 +58,7 @@ function toMarkdown(content: string) {
instance.keep([ "kbd", "sup", "sub" ]);
}
- return instance.turndown(content);
+ return instance.render(content);
}
function rewriteLanguageTag(source: string) {
@@ -85,14 +84,14 @@ function buildImageFilter() {
const ESCAPE_PATTERNS = {
before: /([\\*`[\]_]|(?:^[-+>])|(?:^~~~)|(?:^#{1-6}))/g,
after: /((?:^\d+(?=\.)))/
- }
+ };
- const escapePattern = new RegExp('(?:' + ESCAPE_PATTERNS.before.source + '|' + ESCAPE_PATTERNS.after.source + ')', 'g');
+ const escapePattern = new RegExp(`(?:${ ESCAPE_PATTERNS.before.source }|${ ESCAPE_PATTERNS.after.source })`, 'g');
function escapeMarkdown (content: string) {
- return content.replace(escapePattern, function (match, before, after) {
- return before ? '\\' + before : after + '\\'
- })
+ return content.replace(escapePattern, (match, before, after) => {
+ return before ? `\\${ before}` : `${after }\\`;
+ });
}
function escapeLinkDestination(destination: string) {
@@ -102,10 +101,10 @@ function buildImageFilter() {
}
function escapeLinkTitle (title: string) {
- return title.replace(/"/g, '\\"')
+ return title.replace(/"/g, '\\"');
}
- const imageFilter: TurndownService.Rule = {
+ const imageFilter: Turnish.Rule = {
filter: "img",
replacement(content, _node) {
const node = _node as HTMLElement;
@@ -117,12 +116,12 @@ function buildImageFilter() {
// TODO: Deduplicate with upstream.
const untypedNode = (node as any);
- const alt = escapeMarkdown(cleanAttribute(untypedNode.getAttribute('alt')))
- const src = escapeLinkDestination(untypedNode.getAttribute('src') || '')
- const title = cleanAttribute(untypedNode.getAttribute('title'))
- const titlePart = title ? ' "' + escapeLinkTitle(title) + '"' : ''
+ const alt = escapeMarkdown(cleanAttribute(untypedNode.getAttribute('alt')));
+ const src = escapeLinkDestination(untypedNode.getAttribute('src') || '');
+ const title = cleanAttribute(untypedNode.getAttribute('title'));
+ const titlePart = title ? ` "${ escapeLinkTitle(title) }"` : '';
- return src ? '![' + alt + ']' + '(' + src + titlePart + ')' : ''
+ return src ? `![${ alt }]` + `(${ src }${titlePart })` : '';
}
};
return imageFilter;
@@ -151,7 +150,7 @@ function buildAdmonitionFilter() {
return DEFAULT_ADMONITION_TYPE;
}
- const admonitionFilter: TurndownService.Rule = {
+ const admonitionFilter: Turnish.Rule = {
filter(node, options) {
return node.nodeName === "ASIDE" && node.classList.contains("admonition");
},
@@ -161,11 +160,11 @@ function buildAdmonitionFilter() {
content = content.replace(/^\n+|\n+$/g, '');
content = content.replace(/^/gm, '> ');
- content = `> [!${admonitionType}]\n` + content;
+ content = `> [!${admonitionType}]\n${ content}`;
- return "\n\n" + content + "\n\n";
+ return `\n\n${ content }\n\n`;
}
- }
+ };
return admonitionFilter;
}
@@ -178,15 +177,15 @@ function buildAdmonitionFilter() {
*/
function buildInlineLinkFilter(): Rule {
return {
- filter: function (node, options) {
+ filter (node, options) {
return (
options.linkStyle === 'inlined' &&
node.nodeName === 'A' &&
!!node.getAttribute('href')
- )
+ );
},
- replacement: function (content, _node) {
+ replacement (content, _node) {
const node = _node as HTMLElement;
// Return reference links verbatim.
@@ -196,13 +195,13 @@ function buildInlineLinkFilter(): Rule {
// Otherwise treat as normal.
// TODO: Call super() somehow instead of duplicating the implementation.
- let href = node.getAttribute('href')
- if (href) href = href.replace(/([()])/g, '\\$1')
- let title = cleanAttribute(node.getAttribute('title'))
- if (title) title = ' "' + title.replace(/"/g, '\\"') + '"'
- return '[' + content + '](' + href + title + ')'
+ let href = node.getAttribute('href');
+ if (href) href = href.replace(/([()])/g, '\\$1');
+ let title = cleanAttribute(node.getAttribute('title'));
+ if (title) title = ` "${ title.replace(/"/g, '\\"') }"`;
+ return `[${ content }](${ href }${title })`;
}
- }
+ };
}
function buildFigureFilter(): Rule {
@@ -214,7 +213,7 @@ function buildFigureFilter(): Rule {
replacement(content, node) {
return (node as HTMLElement).outerHTML;
}
- }
+ };
}
// Keep in line with https://github.com/mixmark-io/turndown/blob/master/src/commonmark-rules.js.
@@ -224,13 +223,13 @@ function buildListItemFilter(): Rule {
replacement(content, node, options) {
content = content
.trim()
- .replace(/\n/gm, '\n ') // indent
- let prefix = options.bulletListMarker + ' '
+ .replace(/\n/gm, '\n '); // indent
+ let prefix = `${options.bulletListMarker } `;
const parent = node.parentNode as HTMLElement;
if (parent.nodeName === 'OL') {
- var start = parent.getAttribute('start')
- var index = Array.prototype.indexOf.call(parent.children, node)
- prefix = (start ? Number(start) + index : index + 1) + '. '
+ const start = parent.getAttribute('start');
+ const index = Array.prototype.indexOf.call(parent.children, node);
+ prefix = `${start ? Number(start) + index : index + 1 }. `;
} else if (parent.classList.contains("todo-list")) {
const isChecked = node.querySelector("input[type=checkbox]:checked");
prefix = (isChecked ? "- [x] " : "- [ ] ");
@@ -239,7 +238,7 @@ function buildListItemFilter(): Rule {
const result = prefix + content + (node.nextSibling && !/\n$/.test(content) ? '\n' : '');
return result;
}
- }
+ };
}
function buildMathFilter(): Rule {
@@ -270,13 +269,13 @@ function buildMathFilter(): Rule {
// Unknown.
return content;
}
- }
+ };
}
// Taken from upstream since it's not exposed.
// https://github.com/mixmark-io/turndown/blob/master/src/commonmark-rules.js
function cleanAttribute(attribute: string | null | undefined) {
- return attribute ? attribute.replace(/(\n+\s*)+/g, '\n') : ''
+ return attribute ? attribute.replace(/(\n+\s*)+/g, '\n') : '';
}
export default {
diff --git a/apps/server/src/services/import/markdown.spec.ts b/apps/server/src/services/import/markdown.spec.ts
index 1ac49f613..453db33b8 100644
--- a/apps/server/src/services/import/markdown.spec.ts
+++ b/apps/server/src/services/import/markdown.spec.ts
@@ -314,4 +314,9 @@ $$`;
expect(markdownService.renderToHtml(input, "Title")).toStrictEqual(expected);
});
+ it("doesn't unescape HTML in list", () => {
+ const input = `* <note> is note.`;
+ const expected = /*html*/``;
+ expect(markdownService.renderToHtml(input, "Title")).toStrictEqual(expected);
+ });
});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index be475fc6d..1c748bf83 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -792,9 +792,9 @@ importers:
tmp:
specifier: 0.2.5
version: 0.2.5
- turndown:
- specifier: 7.2.2
- version: 7.2.2
+ turnish:
+ specifier: 1.7.1
+ version: 1.7.1
unescape:
specifier: 1.0.1
version: 1.0.1
@@ -1461,6 +1461,9 @@ importers:
packages:
+ '@adobe/css-tools@4.4.4':
+ resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==}
+
'@ampproject/remapping@2.3.0':
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
engines: {node: '>=6.0.0'}
@@ -13466,6 +13469,9 @@ packages:
turndown@7.2.2:
resolution: {integrity: sha512-1F7db8BiExOKxjSMU2b7if62D/XOyQyZbPKq/nUwopfgnHlqXHqQ0lvfUTeUIr1lZJzOPFn43dODyMSIfvWRKQ==}
+ turnish@1.7.1:
+ resolution: {integrity: sha512-NgyY7pIDABjKyg2isRgZyFPav6tOyvmqpTx3HROsKrOaE3JccP4C1P2IhAtkAZ8DkQb/O1R7HOFAkxY8uaJmcQ==}
+
type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
@@ -14407,6 +14413,8 @@ packages:
snapshots:
+ '@adobe/css-tools@4.4.4': {}
+
'@ampproject/remapping@2.3.0':
dependencies:
'@jridgewell/gen-mapping': 0.3.13
@@ -15078,6 +15086,8 @@ snapshots:
'@ckeditor/ckeditor5-core': 47.4.0
'@ckeditor/ckeditor5-upload': 47.4.0
ckeditor5: 47.4.0
+ transitivePeerDependencies:
+ - supports-color
'@ckeditor/ckeditor5-ai@47.4.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)':
dependencies:
@@ -15218,12 +15228,16 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.4.0
'@ckeditor/ckeditor5-widget': 47.4.0
es-toolkit: 1.39.5
+ transitivePeerDependencies:
+ - supports-color
'@ckeditor/ckeditor5-cloud-services@47.4.0':
dependencies:
'@ckeditor/ckeditor5-core': 47.4.0
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
+ transitivePeerDependencies:
+ - supports-color
'@ckeditor/ckeditor5-code-block@47.4.0(patch_hash=2361d8caad7d6b5bddacc3a3b4aa37dbfba260b1c1b22a450413a79c1bb1ce95)':
dependencies:
@@ -15416,6 +15430,8 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
es-toolkit: 1.39.5
+ transitivePeerDependencies:
+ - supports-color
'@ckeditor/ckeditor5-editor-classic@47.4.0':
dependencies:
@@ -15425,6 +15441,8 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
es-toolkit: 1.39.5
+ transitivePeerDependencies:
+ - supports-color
'@ckeditor/ckeditor5-editor-decoupled@47.4.0':
dependencies:
@@ -15434,6 +15452,8 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
es-toolkit: 1.39.5
+ transitivePeerDependencies:
+ - supports-color
'@ckeditor/ckeditor5-editor-inline@47.4.0':
dependencies:
@@ -15467,8 +15487,6 @@ snapshots:
'@ckeditor/ckeditor5-table': 47.4.0
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
- transitivePeerDependencies:
- - supports-color
'@ckeditor/ckeditor5-emoji@47.4.0':
dependencies:
@@ -15525,8 +15543,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.4.0
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
- transitivePeerDependencies:
- - supports-color
'@ckeditor/ckeditor5-export-word@47.4.0':
dependencies:
@@ -15551,6 +15567,8 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
es-toolkit: 1.39.5
+ transitivePeerDependencies:
+ - supports-color
'@ckeditor/ckeditor5-font@47.4.0':
dependencies:
@@ -15625,6 +15643,8 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.4.0
'@ckeditor/ckeditor5-widget': 47.4.0
ckeditor5: 47.4.0
+ transitivePeerDependencies:
+ - supports-color
'@ckeditor/ckeditor5-html-embed@47.4.0':
dependencies:
@@ -15670,8 +15690,6 @@ snapshots:
'@ckeditor/ckeditor5-widget': 47.4.0
ckeditor5: 47.4.0
es-toolkit: 1.39.5
- transitivePeerDependencies:
- - supports-color
'@ckeditor/ckeditor5-import-word@47.4.0':
dependencies:
@@ -15684,8 +15702,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.4.0
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
- transitivePeerDependencies:
- - supports-color
'@ckeditor/ckeditor5-indent@47.4.0':
dependencies:
@@ -15697,8 +15713,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.4.0
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
- transitivePeerDependencies:
- - supports-color
'@ckeditor/ckeditor5-inspector@5.0.0': {}
@@ -15708,8 +15722,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.4.0
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
- transitivePeerDependencies:
- - supports-color
'@ckeditor/ckeditor5-line-height@47.4.0':
dependencies:
@@ -15734,8 +15746,6 @@ snapshots:
'@ckeditor/ckeditor5-widget': 47.4.0
ckeditor5: 47.4.0
es-toolkit: 1.39.5
- transitivePeerDependencies:
- - supports-color
'@ckeditor/ckeditor5-list-multi-level@47.4.0':
dependencies:
@@ -15759,8 +15769,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.4.0
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
- transitivePeerDependencies:
- - supports-color
'@ckeditor/ckeditor5-markdown-gfm@47.4.0':
dependencies:
@@ -15798,8 +15806,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.4.0
'@ckeditor/ckeditor5-widget': 47.4.0
ckeditor5: 47.4.0
- transitivePeerDependencies:
- - supports-color
'@ckeditor/ckeditor5-mention@47.4.0(patch_hash=5981fb59ba35829e4dff1d39cf771000f8a8fdfa7a34b51d8af9549541f2d62d)':
dependencies:
@@ -15809,8 +15815,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
es-toolkit: 1.39.5
- transitivePeerDependencies:
- - supports-color
'@ckeditor/ckeditor5-merge-fields@47.4.0':
dependencies:
@@ -15823,8 +15827,6 @@ snapshots:
'@ckeditor/ckeditor5-widget': 47.4.0
ckeditor5: 47.4.0
es-toolkit: 1.39.5
- transitivePeerDependencies:
- - supports-color
'@ckeditor/ckeditor5-minimap@47.4.0':
dependencies:
@@ -15833,8 +15835,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.4.0
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
- transitivePeerDependencies:
- - supports-color
'@ckeditor/ckeditor5-operations-compressor@47.4.0':
dependencies:
@@ -15889,8 +15889,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.4.0
'@ckeditor/ckeditor5-widget': 47.4.0
ckeditor5: 47.4.0
- transitivePeerDependencies:
- - supports-color
'@ckeditor/ckeditor5-pagination@47.4.0':
dependencies:
@@ -15998,8 +15996,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.4.0
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
- transitivePeerDependencies:
- - supports-color
'@ckeditor/ckeditor5-slash-command@47.4.0':
dependencies:
@@ -16012,8 +16008,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.4.0
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
- transitivePeerDependencies:
- - supports-color
'@ckeditor/ckeditor5-source-editing-enhanced@47.4.0':
dependencies:
@@ -16061,8 +16055,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
es-toolkit: 1.39.5
- transitivePeerDependencies:
- - supports-color
'@ckeditor/ckeditor5-table@47.4.0':
dependencies:
@@ -16075,8 +16067,6 @@ snapshots:
'@ckeditor/ckeditor5-widget': 47.4.0
ckeditor5: 47.4.0
es-toolkit: 1.39.5
- transitivePeerDependencies:
- - supports-color
'@ckeditor/ckeditor5-template@47.4.0':
dependencies:
@@ -16187,8 +16177,6 @@ snapshots:
'@ckeditor/ckeditor5-engine': 47.4.0
'@ckeditor/ckeditor5-utils': 47.4.0
es-toolkit: 1.39.5
- transitivePeerDependencies:
- - supports-color
'@ckeditor/ckeditor5-widget@47.4.0':
dependencies:
@@ -16208,8 +16196,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
es-toolkit: 1.39.5
- transitivePeerDependencies:
- - supports-color
'@codemirror/autocomplete@6.18.6':
dependencies:
@@ -21676,8 +21662,6 @@ snapshots:
ckeditor5-collaboration@47.4.0:
dependencies:
'@ckeditor/ckeditor5-collaboration-core': 47.4.0
- transitivePeerDependencies:
- - supports-color
ckeditor5-premium-features@47.4.0(bufferutil@4.0.9)(ckeditor5@47.4.0)(utf-8-validate@6.0.5):
dependencies:
@@ -30073,6 +30057,11 @@ snapshots:
dependencies:
'@mixmark-io/domino': 2.2.0
+ turnish@1.7.1:
+ dependencies:
+ '@adobe/css-tools': 4.4.4
+ '@mixmark-io/domino': 2.2.0
+
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1