From f86f8aff957e67e2e7c25e96c67946077cdd49ff Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 26 Sep 2026 00:28:07 +0000 Subject: [PATCH 01/11] fix(deps): update dependency prosemirror-markdown to ^1.13.8 --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8c4649d6e0f..e806f98d522 100644 --- a/package-lock.json +++ b/package-lock.json @@ -77,7 +77,7 @@ "mermaid": "^12.0.0", "mitt": "^3.0.1", "path-normalize": "^8.0.0", - "prosemirror-markdown": "^1.13.7", + "prosemirror-markdown": "^1.13.8", "proxy-polyfill": "^0.3.2", "slug": "^12.0.1", "tippy.js": "^6.3.7", @@ -16588,9 +16588,9 @@ } }, "node_modules/prosemirror-markdown": { - "version": "1.13.7", - "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.7.tgz", - "integrity": "sha512-NV57Oxe3fXR+bNGXS5XW4sKt/XGIyY3n0eHVUoJORcismeF4FFMpLBnl0YESy2BvJQHloexX+9P+zT3VqFj/CQ==", + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.8.tgz", + "integrity": "sha512-Zi43BXX28jmxDJKvk8TDS2gMkBY/mgP8ynd6l/Vxrv8rgNFs5hsV1G0VscxTID1UCzVPcidADP9fUOBY3xxUeA==", "license": "MIT", "dependencies": { "@types/markdown-it": "^14.0.0", diff --git a/package.json b/package.json index 6e870f09c11..df5b0bd0c70 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "mermaid": "^12.0.0", "mitt": "^3.0.1", "path-normalize": "^8.0.0", - "prosemirror-markdown": "^1.13.7", + "prosemirror-markdown": "^1.13.8", "proxy-polyfill": "^0.3.2", "slug": "^12.0.1", "tippy.js": "^6.3.7", From 05f3a7c86218e6899b1af792e41c0ca5e9675b1e Mon Sep 17 00:00:00 2001 From: Jonas Date: Mon, 28 Sep 2026 09:57:35 +0200 Subject: [PATCH 02/11] test(playwright): use autosave to ensure changes were saved in confict test Clicking save too fast after typing might mean there's still steps in tue queue to be sent. This should fix the possible race condition in the test "[conflict] automatic resolution if no unsaved changes" that made it flaky from time to time. Signed-off-by: Jonas Assisted-by: OpenCode:claude-fable-5.1 --- playwright/e2e/conflict.spec.ts | 48 ++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/playwright/e2e/conflict.spec.ts b/playwright/e2e/conflict.spec.ts index ec54956829d..678bf17cc91 100644 --- a/playwright/e2e/conflict.spec.ts +++ b/playwright/e2e/conflict.spec.ts @@ -161,31 +161,35 @@ test.describe('Plaintext conflict resolution', () => { }) }) -test('[conflict] automatic resolution if no unsaved changes', async ({ - container, - editor, - file, - reader, - user, -}) => { - await expect(editor.el).toBeVisible() - await editor.typeHeading('Hello world') - // Wait for the steps to be pushed so the editor knows about unsaved changes - await expect(editor.saveIndicator).toHaveAccessibleName(/Unsaved changes/) - await editor.saveIndicator.click() - // Wait until client has successfully saved - await expect(editor.saveIndicator).not.toHaveAccessibleName(/Unsaved changes/) +test.describe('without unsaved changes', () => { + // Files were created 10 seconds ago so the server accepts the first autosave. + test.use({ mtime: Date.now() / 1000 - 10 }) - await user.uploadFile({ name: file.name, content: '## Good bye' }) + test('[conflict] automatic resolution if no unsaved changes', async ({ + container, + editor, + file, + reader, + user, + }) => { + await expect(editor.el).toBeVisible() + await editor.typeHeading('Hello world') + // Wait for the steps to be pushed so the editor knows about unsaved changes + await expect(editor.saveIndicator).toHaveAccessibleName(/Unsaved changes/) + // Autosave is debounced until all pushed steps are acknowledged. + await expect(editor.saveIndicator).not.toHaveAccessibleName(/Unsaved changes/) - // Should show latest content, no conflict dialog - await expect(editor.getHeading({ name: 'Good bye' })).toBeVisible({ - timeout: 10_000, + await user.uploadFile({ name: file.name, content: '## Good bye' }) + + // Should show latest content, no conflict dialog + await expect(editor.getHeading({ name: 'Good bye' })).toBeVisible({ + timeout: 10_000, + }) + await expect(reader.content).not.toBeVisible() + await expect(container.getButton({ + name: /Keep my local changes and overwrite the remote version/, + })).not.toBeVisible() }) - await expect(reader.content).not.toBeVisible() - await expect(container.getButton({ - name: /Keep my local changes and overwrite the remote version/, - })).not.toBeVisible() }) test('readonly session hides conflict dialog', async ({ From 4b84d2be638adcf03e437faed4d21bb6e48b1c13 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 27 Sep 2026 18:55:02 +0000 Subject: [PATCH 03/11] fix(deps): update markdown-it --- package-lock.json | 45 +++++++++++++++++++++++++++++++-------------- package.json | 6 +++--- renovate.json | 2 +- 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index e806f98d522..ba7d025afc1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@floating-ui/dom": "^1.8.0", "@mdi/svg": "^7.4.47", - "@mdit/plugin-tex": "^1.0.2", + "@mdit/plugin-tex": "^1.1.1", "@nextcloud/auth": "^2.6.0", "@nextcloud/axios": "^2.6.0", "@nextcloud/browser-storage": "^0.5.0", @@ -67,7 +67,7 @@ "katex": "^0.18.9", "lib0": "^0.2.117", "lowlight": "^3.3.0", - "markdown-it": "^14.3.0", + "markdown-it": "^14.3.2", "markdown-it-container": "^4.0.0", "markdown-it-footnote": "^4.0.0", "markdown-it-front-matter": "^0.2.4", @@ -99,7 +99,7 @@ "@nextcloud/vite-config": "^2.5.4", "@playwright/test": "^1.63.0", "@types/jsdom": "^30.0.0", - "@types/markdown-it": "^14.1.2", + "@types/markdown-it": "^14.2.0", "@types/markdown-it-footnote": "^3.0.4", "@vitejs/plugin-vue": "^6.0.9", "@vitest/coverage-v8": "^5.0.1", @@ -3730,18 +3730,35 @@ "license": "Apache-2.0" }, "node_modules/@mdit/plugin-tex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@mdit/plugin-tex/-/plugin-tex-1.0.2.tgz", - "integrity": "sha512-pj4xSLKT7ag7oTSLwQTW+QjEi8+/DgEzDXOI1r9bTX9dmFH15H8kbcZMs6wI5bgW26r4+yIU5OBaVsnesBgzYg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@mdit/plugin-tex/-/plugin-tex-1.1.2.tgz", + "integrity": "sha512-J6boAvyxJzzu87+3mR81YpEqeVBylbA1UKy90lJYSrUPK2jxiN78eC50amP2KhGq83LixcpCFLElATYYW14clw==", "license": "MIT", "dependencies": { - "@types/markdown-it": "^14.1.2" + "@mdit/helper": "1.1.2" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "markdown-it": "^15.0.2" }, + "peerDependenciesMeta": { + "markdown-it": { + "optional": true + } + } + }, + "node_modules/@mdit/plugin-tex/node_modules/@mdit/helper": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@mdit/helper/-/helper-1.1.2.tgz", + "integrity": "sha512-xZywQ7aXrmkRCuGvFXguXVKeiQ7kHCxPkjtR+nK/n31YiSPPi/Rmb/ll+CYt0LdyeXR0Y1I16da7gxDT4/VXWg==", + "license": "MIT", "engines": { "node": ">=22" }, "peerDependencies": { - "markdown-it": "^14.2.0" + "markdown-it": "^15.0.2" }, "peerDependenciesMeta": { "markdown-it": { @@ -6489,9 +6506,9 @@ "license": "MIT" }, "node_modules/@types/markdown-it": { - "version": "14.1.2", - "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", - "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.2.0.tgz", + "integrity": "sha512-NoQ2yGlLWj4wpxMs+TYmRKk3thDrQ97agr7sFqfLsAlvoS8SNQuTrlObhFqG9iugdTtgOE9jpJ6FNM4ZGsa5xQ==", "license": "MIT", "dependencies": { "@types/linkify-it": "^5", @@ -14414,9 +14431,9 @@ } }, "node_modules/markdown-it": { - "version": "14.3.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.0.tgz", - "integrity": "sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==", + "version": "14.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.2.tgz", + "integrity": "sha512-sHHjZ5fJKlgrG4qns2YwVcdNep35h5fERrfkD2YNsb9UFk0UIHarbiTaHKVMlPuWAoiilyK8Fv/jAm11slsY7Q==", "funding": [ { "type": "github", diff --git a/package.json b/package.json index df5b0bd0c70..3d40ccad69e 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "dependencies": { "@floating-ui/dom": "^1.8.0", "@mdi/svg": "^7.4.47", - "@mdit/plugin-tex": "^1.0.2", + "@mdit/plugin-tex": "^1.1.1", "@nextcloud/auth": "^2.6.0", "@nextcloud/axios": "^2.6.0", "@nextcloud/browser-storage": "^0.5.0", @@ -85,7 +85,7 @@ "katex": "^0.18.9", "lib0": "^0.2.117", "lowlight": "^3.3.0", - "markdown-it": "^14.3.0", + "markdown-it": "^14.3.2", "markdown-it-container": "^4.0.0", "markdown-it-footnote": "^4.0.0", "markdown-it-front-matter": "^0.2.4", @@ -117,7 +117,7 @@ "@nextcloud/vite-config": "^2.5.4", "@playwright/test": "^1.63.0", "@types/jsdom": "^30.0.0", - "@types/markdown-it": "^14.1.2", + "@types/markdown-it": "^14.2.0", "@types/markdown-it-footnote": "^3.0.4", "@vitejs/plugin-vue": "^6.0.9", "@vitest/coverage-v8": "^5.0.1", diff --git a/renovate.json b/renovate.json index 598e8fdb710..e64c26217c7 100644 --- a/renovate.json +++ b/renovate.json @@ -50,7 +50,7 @@ "matchPackageNames": ["typescript", "typedoc"] }, { - "groupName": "markdown-id", + "groupName": "markdown-it", "matchPackageNames": ["**{/,}markdown-it*", "@mdit/*"] }, { From ece676793b6fcf37332e882c0e1b3381ab929a27 Mon Sep 17 00:00:00 2001 From: Jonas Date: Mon, 28 Sep 2026 10:54:47 +0200 Subject: [PATCH 04/11] chore(markdownit): migrate to markdown-it v15 * Markdown-it v15 brings typing on its own now, `@types/markdown-it` is obsolete. * Some `markdown-it/lib/*` deep imports and got moved to `md.utils.*`. * markdown-it-multimd-table calls `md.utils.assign` which got removed in v15. Use `Object.assign` instead. Signed-off-by: Jonas Assisted-by: ClaudeCode:claude-opus-5.5 --- package-lock.json | 148 +++++++++++++++++++++--------- package.json | 3 +- src/declarations.d.ts | 7 -- src/extensions/Markdown.ts | 3 +- src/markdownit/comments.ts | 15 ++- src/markdownit/details.ts | 4 +- src/markdownit/footnotes.ts | 8 +- src/markdownit/hardbreak.js | 8 +- src/markdownit/index.js | 18 +++- src/markdownit/mathematics.ts | 2 +- src/markdownit/referenceLinks.ts | 8 +- src/markdownit/splitMixedLists.js | 2 +- src/markdownit/taskLists.ts | 13 ++- src/markdownit/wikiLinks.ts | 2 +- 14 files changed, 150 insertions(+), 91 deletions(-) diff --git a/package-lock.json b/package-lock.json index ba7d025afc1..3d151711829 100644 --- a/package-lock.json +++ b/package-lock.json @@ -67,7 +67,7 @@ "katex": "^0.18.9", "lib0": "^0.2.117", "lowlight": "^3.3.0", - "markdown-it": "^14.3.2", + "markdown-it": "^15.0.2", "markdown-it-container": "^4.0.0", "markdown-it-footnote": "^4.0.0", "markdown-it-front-matter": "^0.2.4", @@ -99,7 +99,6 @@ "@nextcloud/vite-config": "^2.5.4", "@playwright/test": "^1.63.0", "@types/jsdom": "^30.0.0", - "@types/markdown-it": "^14.2.0", "@types/markdown-it-footnote": "^3.0.4", "@vitejs/plugin-vue": "^6.0.9", "@vitest/coverage-v8": "^5.0.1", @@ -7653,10 +7652,20 @@ "license": "MIT" }, "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-3.0.2.tgz", + "integrity": "sha512-mFdDM6WqWKraGLsVb+C9CahPnzTXOefAOLq3jYcca2YZ8bEWpr++Tzj+zSaKW9+X9L5uSxcm1AZ3Y6aZJ09OhQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "PSF-2.0" }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", @@ -10612,12 +10621,12 @@ } }, "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", + "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", "license": "BSD-2-Clause", "engines": { - "node": ">=0.12" + "node": ">=20.19.0" }, "funding": { "url": "https://github.com/fb55/entities?sponsor=1" @@ -13992,9 +14001,9 @@ } }, "node_modules/linkify-it": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", - "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-6.1.0.tgz", + "integrity": "sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw==", "funding": [ { "type": "github", @@ -14007,7 +14016,7 @@ ], "license": "MIT", "dependencies": { - "uc.micro": "^2.0.0" + "uc.micro": "^3.0.0" } }, "node_modules/linkifyjs": { @@ -14431,9 +14440,9 @@ } }, "node_modules/markdown-it": { - "version": "14.3.2", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.2.tgz", - "integrity": "sha512-sHHjZ5fJKlgrG4qns2YwVcdNep35h5fERrfkD2YNsb9UFk0UIHarbiTaHKVMlPuWAoiilyK8Fv/jAm11slsY7Q==", + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-15.0.2.tgz", + "integrity": "sha512-q4IGxMv56jCqT4OCRCADBoDP3LO4MhmTXjFbphHPXs4g3j9Xg5RDnxqN8IF/3vIWEU+VCnUq+7JUg/cfy2E6Qw==", "funding": [ { "type": "github", @@ -14446,12 +14455,12 @@ ], "license": "MIT", "dependencies": { - "argparse": "^2.0.1", - "entities": "^4.5.0", - "linkify-it": "^5.0.2", - "mdurl": "^2.0.0", + "argparse": "^3.0.0", + "entities": "^8.0.0", + "linkify-it": "^6.0.0", + "mdurl": "^2.1.0", "punycode.js": "^2.3.1", - "uc.micro": "^2.1.0" + "uc.micro": "^3.0.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" @@ -14766,9 +14775,9 @@ "license": "CC0-1.0" }, "node_modules/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", "license": "MIT" }, "node_modules/meow": { @@ -16066,19 +16075,6 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/parse5/node_modules/entities": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", - "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -16615,6 +16611,76 @@ "prosemirror-model": "^1.25.0" } }, + "node_modules/prosemirror-markdown/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/prosemirror-markdown/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/prosemirror-markdown/node_modules/linkify-it": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/prosemirror-markdown/node_modules/markdown-it": { + "version": "14.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.2.tgz", + "integrity": "sha512-sHHjZ5fJKlgrG4qns2YwVcdNep35h5fERrfkD2YNsb9UFk0UIHarbiTaHKVMlPuWAoiilyK8Fv/jAm11slsY7Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.5.0", + "linkify-it": "^5.0.2", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/prosemirror-markdown/node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, "node_modules/prosemirror-model": { "version": "1.25.11", "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.11.tgz", @@ -19213,9 +19279,9 @@ "license": "MIT" }, "node_modules/uc.micro": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", - "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-3.0.0.tgz", + "integrity": "sha512-U3PppEkleoTnIfi8BozMx3yju3qc/L6SwqWo2Sw+54PX+PX0q9I+r1Um5HCmqD7n9VDX5/v3vQH/AjA6deDdtw==", "license": "MIT" }, "node_modules/ufo": { diff --git a/package.json b/package.json index 3d40ccad69e..ae75c972185 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "katex": "^0.18.9", "lib0": "^0.2.117", "lowlight": "^3.3.0", - "markdown-it": "^14.3.2", + "markdown-it": "^15.0.2", "markdown-it-container": "^4.0.0", "markdown-it-footnote": "^4.0.0", "markdown-it-front-matter": "^0.2.4", @@ -117,7 +117,6 @@ "@nextcloud/vite-config": "^2.5.4", "@playwright/test": "^1.63.0", "@types/jsdom": "^30.0.0", - "@types/markdown-it": "^14.2.0", "@types/markdown-it-footnote": "^3.0.4", "@vitejs/plugin-vue": "^6.0.9", "@vitest/coverage-v8": "^5.0.1", diff --git a/src/declarations.d.ts b/src/declarations.d.ts index 530eca365d8..22878eace96 100644 --- a/src/declarations.d.ts +++ b/src/declarations.d.ts @@ -13,11 +13,4 @@ declare module '@nextcloud/vue/composables/useIsMobile' { export function useIsMobile(): Ref } -// We use `StateInline` in our custom referenceLinks markdown-it plugin -declare module 'markdown-it/lib/rules_inline/link.mjs' { - import type StateInline from 'markdown-it/lib/rules_inline/state_inline.mjs' - const rule: (state: StateInline, silent: boolean) => boolean - export default rule -} - declare module 'vite/modulepreload-polyfill' diff --git a/src/extensions/Markdown.ts b/src/extensions/Markdown.ts index 4c676afdc31..4c4e89c42fa 100644 --- a/src/extensions/Markdown.ts +++ b/src/extensions/Markdown.ts @@ -30,7 +30,6 @@ import type { MarkdownSerializerState } from 'prosemirror-markdown' import { Extension, getExtensionField } from '@tiptap/core' import { DOMParser } from '@tiptap/pm/model' import { Plugin, PluginKey } from '@tiptap/pm/state' -import { normalizeReference } from 'markdown-it/lib/common/utils.mjs' import { defaultMarkdownSerializer, MarkdownSerializer } from 'prosemirror-markdown' import markdownit from '../markdownit/index.js' import Link from '../marks/Link.ts' @@ -210,7 +209,7 @@ function createMarkdownSerializer(schema: { title?: string } if (label && type) { - const key = normalizeReference(label) + const key = markdownit.utils.normalizeReference(label) if (!referenceDefinitions.has(key)) { referenceDefinitions.set(key, { label, diff --git a/src/markdownit/comments.ts b/src/markdownit/comments.ts index d76a3b27205..83115da5103 100644 --- a/src/markdownit/comments.ts +++ b/src/markdownit/comments.ts @@ -3,11 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import type MarkdownIt from 'markdown-it' -import type StateCore from 'markdown-it/lib/rules_core/state_core.mjs' -import type Token from 'markdown-it/lib/token.mjs' - -import { escapeHtml } from 'markdown-it/lib/common/utils.mjs' +import type { MarkdownIt, StateCore, Token } from 'markdown-it' const COMMENT_REF_PREFIX = 'comment-' @@ -17,7 +13,7 @@ const COMMENT_REF_PREFIX = 'comment-' * @param token markdown-it token */ function labelOf(token: Token): string { - return token.meta?.label || String(token.meta?.id ?? '') + return String(token.meta?.label || (token.meta?.id ?? '')) } /** @@ -354,6 +350,7 @@ function extractMetadata(inline: Token): Metadata { * @param md markdown-it Markdown object */ export default function comments(md: MarkdownIt): void { + const { escapeHtml } = md.utils md.core.ruler.after('footnote_tail', 'split_comments', splitComments) md.core.ruler.after('split_comments', 'extract_comment_metadata', extractCommentMetadata) @@ -370,9 +367,9 @@ export default function comments(md: MarkdownIt): void { md.renderer.rules.comment_close = () => '\n' md.renderer.rules.comment_item_open = (tokens, idx) => { const token = tokens[idx] - const author = token.attrGet('data-author') || '' - const authorLabel = token.attrGet('data-author-label') || '' - const timestamp = token.attrGet('data-timestamp') || '' + const author = String(token.attrGet('data-author') || '') + const authorLabel = String(token.attrGet('data-author-label') || '') + const timestamp = String(token.attrGet('data-timestamp') || '') return `
\n` } md.renderer.rules.comment_item_close = () => '
\n' diff --git a/src/markdownit/details.ts b/src/markdownit/details.ts index 6a1f64214ea..56455bc1135 100644 --- a/src/markdownit/details.ts +++ b/src/markdownit/details.ts @@ -3,9 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import type MarkdownIt from 'markdown-it' -import type StateBlock from 'markdown-it/lib/rules_block/state_block.mjs' -import type Token from 'markdown-it/lib/token.mjs' +import type { MarkdownIt, StateBlock, Token } from 'markdown-it' const DETAILS_START_REGEX = /^\s+open(?:=(?:""|''|open))?)?>\s*$/ const DETAILS_AND_SUMMARY_START_REGEX = /^\s+open(?:=(?:""|''|open))?)?>\s*(?.*)<\/summary>\s*$/ diff --git a/src/markdownit/footnotes.ts b/src/markdownit/footnotes.ts index acb55beeec9..29b127b876c 100644 --- a/src/markdownit/footnotes.ts +++ b/src/markdownit/footnotes.ts @@ -3,11 +3,9 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import type MarkdownIt from 'markdown-it' -import type Token from 'markdown-it/lib/token.mjs' +import type { MarkdownIt, Token } from 'markdown-it' import footnote from 'markdown-it-footnote' -import { escapeHtml } from 'markdown-it/lib/common/utils.mjs' /** * Return footnote label @@ -15,7 +13,7 @@ import { escapeHtml } from 'markdown-it/lib/common/utils.mjs' * @param token markdown-it token */ function labelOf(token: Token): string { - return token.meta?.label || String(token.meta?.id ?? '') + return String(token.meta?.label || (token.meta?.id ?? '')) } /** @@ -24,6 +22,8 @@ function labelOf(token: Token): string { * @param md markdown-it markdown object */ export default function footnotes(md: MarkdownIt): void { + const { escapeHtml } = md.utils + md.use(footnote) md.renderer.rules.footnote_ref = (tokens, idx) => { diff --git a/src/markdownit/hardbreak.js b/src/markdownit/hardbreak.js index facdb136695..e8781e71a9e 100644 --- a/src/markdownit/hardbreak.js +++ b/src/markdownit/hardbreak.js @@ -3,15 +3,15 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import markdownitEscape from 'markdown-it/lib/rules_inline/escape.mjs' -import markdownitNewline from 'markdown-it/lib/rules_inline/newline.mjs' - /** * Add information about used markdown syntax to HTML hard breaks * - * @param {import('markdown-it')} md Markdown object + * @param {import('markdown-it').MarkdownIt} md Markdown object */ export default function keepHardbreakSyntax(md) { + const { ruler } = md.inline + const markdownitNewline = ruler.__rules__[ruler.__find__('newline')].fn + const markdownitEscape = ruler.__rules__[ruler.__find__('escape')].fn // Add syntax information to hard line breaks using double spaces md.inline.ruler.at('newline', (state, silent) => { const rval = markdownitNewline(state, silent) diff --git a/src/markdownit/index.js b/src/markdownit/index.js index 537ea5e491b..d9651f8b51e 100644 --- a/src/markdownit/index.js +++ b/src/markdownit/index.js @@ -9,7 +9,6 @@ import frontMatter from 'markdown-it-front-matter' import implicitFigures from 'markdown-it-image-figures' import mark from 'markdown-it-mark' import multimdTable from 'markdown-it-multimd-table' -import { escapeHtml } from 'markdown-it/lib/common/utils.mjs' import callouts from './callouts.js' import comments from './comments.ts' import details from './details.ts' @@ -24,6 +23,17 @@ import taskLists from './taskLists.ts' import underline from './underline.js' import wikiLinks from './wikiLinks.ts' +/** + * markdown-it-multimd-table calls `md.utils.assign`, which markdown-it 15 removed + * + * @param {import('markdown-it').MarkdownIt} md Markdown object + * @param {object} options multimd-table options + */ +function multimdTableCompat(md, options) { + md.utils = { ...md.utils, assign: Object.assign } + multimdTable(md, options) +} + const markdownit = MarkdownIt('commonmark', { html: false, breaks: false }) .enable('strikethrough') .enable('table') @@ -44,17 +54,17 @@ const markdownit = MarkdownIt('commonmark', { html: false, breaks: false }) .use(implicitFigures) .use(mark) .use(mathematics) - .use(multimdTable, { + .use(multimdTableCompat, { multiline: true, rowspan: false, multibody: false, }) // Render front matter tokens -markdownit.renderer.rules.front_matter = (tokens, idx) => `
${escapeHtml(tokens[idx].meta)}
` +markdownit.renderer.rules.front_matter = (tokens, idx) => `
${markdownit.utils.escapeHtml(tokens[idx].meta)}
` // Render horizontal rules with markup attribute -markdownit.renderer.rules.hr = (tokens, idx) => `
\n` +markdownit.renderer.rules.hr = (tokens, idx) => `
\n` // Render lists with bullet attribute markdownit.renderer.rules.bullet_list_open = (tokens, idx, options) => { diff --git a/src/markdownit/mathematics.ts b/src/markdownit/mathematics.ts index d5b3474352b..f711b18e545 100644 --- a/src/markdownit/mathematics.ts +++ b/src/markdownit/mathematics.ts @@ -9,7 +9,7 @@ * These will then be picked up by the mathematics nodes. */ -import type MarkdownIt from 'markdown-it' +import type { MarkdownIt } from 'markdown-it' import { tex } from '@mdit/plugin-tex' diff --git a/src/markdownit/referenceLinks.ts b/src/markdownit/referenceLinks.ts index a6a1fcc15aa..0509cafd782 100644 --- a/src/markdownit/referenceLinks.ts +++ b/src/markdownit/referenceLinks.ts @@ -3,10 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import type MarkdownIt from 'markdown-it' -import type StateInline from 'markdown-it/lib/rules_inline/state_inline.mjs' - -import linkRule from 'markdown-it/lib/rules_inline/link.mjs' +import type { MarkdownIt, StateInline } from 'markdown-it' type RefType = 'full' | 'collapsed' | 'shortcut' @@ -98,5 +95,6 @@ function wrap(original: (state: StateInline, silent: boolean) => boolean) { * @param md - markdown-it instance to extend */ export default function referenceLinks(md: MarkdownIt): void { - md.inline.ruler.at('link', wrap(linkRule)) + const { ruler } = md.inline + ruler.at('link', wrap(ruler.__rules__[ruler.__find__('link')].fn)) } diff --git a/src/markdownit/splitMixedLists.js b/src/markdownit/splitMixedLists.js index e605f48fe69..be46924c0a9 100644 --- a/src/markdownit/splitMixedLists.js +++ b/src/markdownit/splitMixedLists.js @@ -4,7 +4,7 @@ */ /** - * @param {import('markdown-it')} md Markdown object + * @param {import('markdown-it').MarkdownIt} md Markdown object */ export default function splitMixedLists(md) { md.core.ruler.after('task-lists', 'split-mixed-task-lists', (state) => { diff --git a/src/markdownit/taskLists.ts b/src/markdownit/taskLists.ts index a2c5b185505..56275aefc11 100644 --- a/src/markdownit/taskLists.ts +++ b/src/markdownit/taskLists.ts @@ -9,10 +9,9 @@ // https://github.com/blog/1375-task-lists-in-gfm-issues-pulls-comments // https://github.com/blog/1825-task-lists-in-all-markdown-documents -import type MarkdownIt from 'markdown-it' -import type StateCore from 'markdown-it/lib/rules_core/state_core.mjs' +import type { MarkdownIt, StateCore, Token } from 'markdown-it' -import Token from 'markdown-it/lib/token.mjs' +import markdownit from 'markdown-it' interface TaskListsOptions { enabled: boolean @@ -75,7 +74,7 @@ function processToken(state: StateCore, options: TaskListsOptions): boolean { const parentToken = findParentToken(allTokens, i - 2) if (parentToken) { - const classes = parentToken.attrGet('class') ?? '' + const classes = String(parentToken.attrGet('class') ?? '') if (!classes.match(/(^| )contains-task-list/)) { parentToken.attrJoin('class', 'contains-task-list') } @@ -157,7 +156,7 @@ function generateIdForToken(token: Token): string { * @param id - ID of the taskLists item */ function createCheckboxToken(token: Token, enabled: boolean, id: string): Token { - const checkbox = new Token('taskListItemCheckbox', '', 0) + const checkbox = new markdownit.Token('taskListItemCheckbox', '', 0) if (!enabled) { checkbox.attrSet('disabled', 'true') } @@ -181,7 +180,7 @@ function createCheckboxToken(token: Token, enabled: boolean, id: string): Token * @param id - ID of the taskLists item */ function createLabelBeginToken(id: string): Token { - const labelBeginToken = new Token('taskListItemLabel_open', '', 1) + const labelBeginToken = new markdownit.Token('taskListItemLabel_open', '', 1) labelBeginToken.attrSet('id', id) return labelBeginToken } @@ -190,7 +189,7 @@ function createLabelBeginToken(id: string): Token { * */ function createLabelEndToken(): Token { - return new Token('taskListItemLabel_close', '', -1) + return new markdownit.Token('taskListItemLabel_close', '', -1) } /** diff --git a/src/markdownit/wikiLinks.ts b/src/markdownit/wikiLinks.ts index d75722ddabe..a91164efdf0 100644 --- a/src/markdownit/wikiLinks.ts +++ b/src/markdownit/wikiLinks.ts @@ -5,7 +5,7 @@ /* eslint-disable @stylistic/max-statements-per-line */ -import type MarkdownIt from 'markdown-it' +import type { MarkdownIt } from 'markdown-it' /** * markdown-it plugin: parse Obsidian-style wiki links and wiki image links From 46680d1d71bc572dc525426e0134637aae427494 Mon Sep 17 00:00:00 2001 From: Jonas Date: Mon, 28 Sep 2026 11:17:50 +0200 Subject: [PATCH 05/11] chore(markdownit): migrate some files to Typescript Signed-off-by: Jonas Assisted-by: OpenCode:claude-fable-5.1 --- cypress/e2e/nodes/Table.spec.js | 2 +- cypress/e2e/nodes/helpers.js | 2 +- src/components/CollaborativeEditor.vue | 2 +- src/components/Menu/AssistantAction.vue | 4 +-- .../Suggestion/LinkPicker/suggestions.js | 4 +-- src/composables/useEditorMethods.ts | 2 +- src/declarations.d.ts | 18 +++++++++++++ src/extensions/Markdown.ts | 2 +- src/helpers/setInitialYjsState.ts | 2 +- src/helpers/updateFromContent.ts | 2 +- ...MarkdownSyntax.js => hasMarkdownSyntax.ts} | 4 +-- src/markdownit/{index.js => index.ts} | 17 ++++++------ ...{isValidMarkdown.js => isValidMarkdown.ts} | 6 ++--- src/markdownit/{preview.js => preview.ts} | 26 ++++++++++--------- ...rkdown.js => shouldInterpretAsMarkdown.ts} | 8 +++--- src/markdownit/{underline.js => underline.ts} | 6 +++-- src/nodes/CommentReference.ts | 2 +- src/tests/markdown.spec.js | 2 +- src/tests/markdownit/bulletList.spec.js | 2 +- src/tests/markdownit/callouts.spec.js | 2 +- src/tests/markdownit/comments.spec.ts | 2 +- src/tests/markdownit/commonmark.spec.js | 2 +- src/tests/markdownit/details.spec.js | 2 +- src/tests/markdownit/footnotes.spec.ts | 2 +- src/tests/markdownit/imageFigures.spec.js | 2 +- src/tests/markdownit/mentions.spec.js | 2 +- src/tests/markdownit/multimd-tables.spec.js | 2 +- src/tests/markdownit/preview.spec.js | 2 +- src/tests/markdownit/referenceLinks.spec.ts | 2 +- src/tests/markdownit/taskLists.spec.js | 2 +- src/tests/markdownit/wikiLinks.spec.ts | 2 +- src/tests/nodes/ListConversion.spec.ts | 2 +- src/tests/nodes/Mathematics.spec.ts | 2 +- src/tests/nodes/Table.spec.js | 2 +- src/tests/testHelpers/markdown.js | 2 +- src/tests/testHelpers/testEditor.ts | 2 +- src/tests/tiptap.spec.js | 2 +- 37 files changed, 86 insertions(+), 63 deletions(-) rename src/markdownit/{hasMarkdownSyntax.js => hasMarkdownSyntax.ts} (86%) rename src/markdownit/{index.js => index.ts} (83%) rename src/markdownit/{isValidMarkdown.js => isValidMarkdown.ts} (66%) rename src/markdownit/{preview.js => preview.ts} (63%) rename src/markdownit/{shouldInterpretAsMarkdown.js => shouldInterpretAsMarkdown.ts} (54%) rename src/markdownit/{underline.js => underline.ts} (80%) diff --git a/cypress/e2e/nodes/Table.spec.js b/cypress/e2e/nodes/Table.spec.js index c2cf7b33892..cdecf605031 100644 --- a/cypress/e2e/nodes/Table.spec.js +++ b/cypress/e2e/nodes/Table.spec.js @@ -7,7 +7,7 @@ import testData from '../../fixtures/Table.md?raw' import { initUserAndFiles, randUser } from '../../utils/index.js' import Markdown from './../../../src/extensions/Markdown.js' -import markdownit from './../../../src/markdownit/index.js' +import markdownit from './../../../src/markdownit/index.ts' import EditableTable from './../../../src/nodes/EditableTable.js' import { createCustomEditor } from './../../support/components.js' import { expectMarkdown, runCommands } from './helpers.js' diff --git a/cypress/e2e/nodes/helpers.js b/cypress/e2e/nodes/helpers.js index 8cbc514045a..1457a03e11c 100644 --- a/cypress/e2e/nodes/helpers.js +++ b/cypress/e2e/nodes/helpers.js @@ -5,7 +5,7 @@ import { createMarkdownSerializer } from './../../../src/extensions/Markdown.js' import { findChildren } from './../../../src/helpers/prosemirrorUtils.js' -import markdownit from './../../../src/markdownit/index.js' +import markdownit from './../../../src/markdownit/index.ts' /** * diff --git a/src/components/CollaborativeEditor.vue b/src/components/CollaborativeEditor.vue index 8f964c597ff..8f98c2a0024 100644 --- a/src/components/CollaborativeEditor.vue +++ b/src/components/CollaborativeEditor.vue @@ -123,7 +123,7 @@ import { serializePlainText, } from './../EditorFactory.ts' import { createMarkdownSerializer } from './../extensions/Markdown.js' -import markdownit from './../markdownit/index.js' +import markdownit from './../markdownit/index.ts' import isMobile from './../mixins/isMobile.js' import AttachmentResolver from './../services/AttachmentResolver.js' import createSyncServiceProvider from './../services/SyncServiceProvider.js' diff --git a/src/components/Menu/AssistantAction.vue b/src/components/Menu/AssistantAction.vue index 370a2a2a7ee..ccba9c97f05 100644 --- a/src/components/Menu/AssistantAction.vue +++ b/src/components/Menu/AssistantAction.vue @@ -178,8 +178,8 @@ import DeleteOutlineIcon from 'vue-material-design-icons/TrashCanOutline.vue' import { markFileAsAiGenerated } from '../../apis/ai.ts' import { useFileProps } from '../../composables/useFileProps.ts' import { logger } from '../../helpers/logger.ts' -import markdownit from '../../markdownit/index.js' -import shouldInterpretAsMarkdown from '../../markdownit/shouldInterpretAsMarkdown.js' +import markdownit from '../../markdownit/index.ts' +import shouldInterpretAsMarkdown from '../../markdownit/shouldInterpretAsMarkdown.ts' import { BaseActionEntry } from './BaseActionEntry.js' import { useMenuIDMixin } from './MenuBar.provider.js' diff --git a/src/components/Suggestion/LinkPicker/suggestions.js b/src/components/Suggestion/LinkPicker/suggestions.js index fba582cc042..e61b861192f 100644 --- a/src/components/Suggestion/LinkPicker/suggestions.js +++ b/src/components/Suggestion/LinkPicker/suggestions.js @@ -10,8 +10,8 @@ import { } from '@nextcloud/vue/components/NcRichText' import LinkPickerList from './LinkPickerList.vue' import { logger } from '../../../helpers/logger.ts' -import markdownit from '../../../markdownit/index.js' -import shouldInterpretAsMarkdown from '../../../markdownit/shouldInterpretAsMarkdown.js' +import markdownit from '../../../markdownit/index.ts' +import shouldInterpretAsMarkdown from '../../../markdownit/shouldInterpretAsMarkdown.ts' import { getIsActive } from '../../Menu/utils.js' import createSuggestions from '../suggestions.js' import { getMenuEntries } from './../../Menu/entries.ts' diff --git a/src/composables/useEditorMethods.ts b/src/composables/useEditorMethods.ts index 01dbf17d417..c0da775eeb8 100644 --- a/src/composables/useEditorMethods.ts +++ b/src/composables/useEditorMethods.ts @@ -9,7 +9,7 @@ import type { Session } from '../services/SyncService.ts' import escapeHtml from 'escape-html' import Markdown from '../extensions/Markdown.js' -import markdownit from '../markdownit/index.js' +import markdownit from '../markdownit/index.ts' import { isUser } from '../services/SyncService.ts' /** diff --git a/src/declarations.d.ts b/src/declarations.d.ts index 22878eace96..2c5257de47f 100644 --- a/src/declarations.d.ts +++ b/src/declarations.d.ts @@ -13,4 +13,22 @@ declare module '@nextcloud/vue/composables/useIsMobile' { export function useIsMobile(): Ref } +declare module '@quartzy/markdown-it-mentions' { + import type { MarkdownIt } from 'markdown-it' + const plugin: (md: MarkdownIt) => void + export default plugin +} + +declare module 'markdown-it-image-figures' { + import type { MarkdownIt } from 'markdown-it' + const plugin: (md: MarkdownIt) => void + export default plugin +} + +declare module 'markdown-it-mark' { + import type { MarkdownIt } from 'markdown-it' + const plugin: (md: MarkdownIt) => void + export default plugin +} + declare module 'vite/modulepreload-polyfill' diff --git a/src/extensions/Markdown.ts b/src/extensions/Markdown.ts index 4c4e89c42fa..e5074fa2c85 100644 --- a/src/extensions/Markdown.ts +++ b/src/extensions/Markdown.ts @@ -31,7 +31,7 @@ import { Extension, getExtensionField } from '@tiptap/core' import { DOMParser } from '@tiptap/pm/model' import { Plugin, PluginKey } from '@tiptap/pm/state' import { defaultMarkdownSerializer, MarkdownSerializer } from 'prosemirror-markdown' -import markdownit from '../markdownit/index.js' +import markdownit from '../markdownit/index.ts' import Link from '../marks/Link.ts' import transformPastedHTML from './transformPastedHTML.ts' diff --git a/src/helpers/setInitialYjsState.ts b/src/helpers/setInitialYjsState.ts index 543dbe3ddc6..c9304a5bae8 100644 --- a/src/helpers/setInitialYjsState.ts +++ b/src/helpers/setInitialYjsState.ts @@ -8,7 +8,7 @@ import escapeHtml from 'escape-html' import { prosemirrorToYXmlFragment } from 'y-prosemirror' import { applyUpdate, Doc, encodeStateAsUpdate, XmlFragment } from 'yjs' import { createPlainEditor, createRichEditor } from '../EditorFactory.js' -import markdownit from '../markdownit/index.js' +import markdownit from '../markdownit/index.ts' /** * Apply an update to the ydoc that will change it to match content diff --git a/src/helpers/updateFromContent.ts b/src/helpers/updateFromContent.ts index 2e319bf85e0..0f31496e96e 100644 --- a/src/helpers/updateFromContent.ts +++ b/src/helpers/updateFromContent.ts @@ -7,7 +7,7 @@ import { Collaboration } from '@tiptap/extension-collaboration' import escapeHtml from 'escape-html' import { applyUpdate, Doc, encodeStateAsUpdate, encodeStateVector } from 'yjs' import { createPlainEditor, createRichEditor } from '../EditorFactory.js' -import markdownit from '../markdownit/index.js' +import markdownit from '../markdownit/index.ts' /** * Compute an update that changes baseDoc to have content diff --git a/src/markdownit/hasMarkdownSyntax.js b/src/markdownit/hasMarkdownSyntax.ts similarity index 86% rename from src/markdownit/hasMarkdownSyntax.js rename to src/markdownit/hasMarkdownSyntax.ts index 46cbb0cc635..23c8be6b390 100644 --- a/src/markdownit/hasMarkdownSyntax.js +++ b/src/markdownit/hasMarkdownSyntax.ts @@ -6,9 +6,9 @@ /** * Check if the content has Markdown syntax * - * @param {string} content Markdown object + * @param content Markdown object */ -export default function hasMarkdownSyntax(content) { +export default function hasMarkdownSyntax(content: string) { // Regular expressions for common Markdown patterns const markdownPatterns = [ /\*\*.*?\*\*/, // Bold: **text** diff --git a/src/markdownit/index.js b/src/markdownit/index.ts similarity index 83% rename from src/markdownit/index.js rename to src/markdownit/index.ts index d9651f8b51e..f858aadfea3 100644 --- a/src/markdownit/index.js +++ b/src/markdownit/index.ts @@ -16,28 +16,29 @@ import footnotes from './footnotes.ts' import hardbreak from './hardbreak.js' import keepSyntax from './keepSyntax.js' import mathematics from './mathematics.ts' -import preview from './preview.js' +import preview from './preview.ts' import referenceLinks from './referenceLinks.ts' import splitMixedLists from './splitMixedLists.js' import taskLists from './taskLists.ts' -import underline from './underline.js' +import underline from './underline.ts' import wikiLinks from './wikiLinks.ts' /** * markdown-it-multimd-table calls `md.utils.assign`, which markdown-it 15 removed * - * @param {import('markdown-it').MarkdownIt} md Markdown object - * @param {object} options multimd-table options + * @param md Markdown object + * @param options multimd-table options */ -function multimdTableCompat(md, options) { - md.utils = { ...md.utils, assign: Object.assign } +function multimdTableCompat(md: InstanceType, options: Parameters[1]): void { + const utils = { ...md.utils, assign: Object.assign } + md.utils = utils multimdTable(md, options) } const markdownit = MarkdownIt('commonmark', { html: false, breaks: false }) .enable('strikethrough') .enable('table') - .use(taskLists, { enable: true, labelAfter: true }) + .use(taskLists) .use(frontMatter, () => {}) .use(splitMixedLists) // needs task Lists to be used first .use(underline) @@ -61,7 +62,7 @@ const markdownit = MarkdownIt('commonmark', { html: false, breaks: false }) }) // Render front matter tokens -markdownit.renderer.rules.front_matter = (tokens, idx) => `
${markdownit.utils.escapeHtml(tokens[idx].meta)}
` +markdownit.renderer.rules.front_matter = (tokens, idx) => `
${markdownit.utils.escapeHtml(String(tokens[idx].meta))}
` // Render horizontal rules with markup attribute markdownit.renderer.rules.hr = (tokens, idx) => `
\n` diff --git a/src/markdownit/isValidMarkdown.js b/src/markdownit/isValidMarkdown.ts similarity index 66% rename from src/markdownit/isValidMarkdown.js rename to src/markdownit/isValidMarkdown.ts index 7727cf98c86..22e1fc695b0 100644 --- a/src/markdownit/isValidMarkdown.js +++ b/src/markdownit/isValidMarkdown.ts @@ -2,14 +2,14 @@ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ -import markdownit from './index.js' +import markdownit from './index.ts' /** * Check if the content is valid Markdown syntax * - * @param {string} content Markdown object + * @param content Markdown object */ -export default function isValidMarkdown(content) { +export default function isValidMarkdown(content: string): boolean { try { markdownit.parse(content) return true diff --git a/src/markdownit/preview.js b/src/markdownit/preview.ts similarity index 63% rename from src/markdownit/preview.js rename to src/markdownit/preview.ts index 71217180602..abdc3282258 100644 --- a/src/markdownit/preview.js +++ b/src/markdownit/preview.ts @@ -3,18 +3,20 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ +import type { MarkdownIt, StateCore, Token } from 'markdown-it' + /** * Check if the given tokens represent a preview in a paragraph * - * @param {Array} tokens - tokens to check - * @param {number} i - offset into the tokens + * @param tokens - tokens to check + * @param i - offset into the tokens */ -function isPreviewLinkInParagraph(tokens, i) { +function isPreviewLinkInParagraph(tokens: Token[], i: number): boolean { const [prev, cur, next] = tokens.slice(i - 1, i + 2) return ( prev?.type === 'paragraph_open' && cur.type === 'inline' - && cur.children + && cur.children !== null && cur.children.length === 3 && cur.children[0].type === 'link_open' && cur.children[0].attrGet('title') === 'preview' @@ -27,26 +29,26 @@ function isPreviewLinkInParagraph(tokens, i) { /** * Remove wrapping tokens * - * @param {Array} tokens - the token stream to modify - * @param {number} i - index of the token to unwrap + * @param tokens - the token stream to modify + * @param i - index of the token to unwrap */ -function unwrapToken(tokens, i) { +function unwrapToken(tokens: Token[], i: number): void { // Start from the end so indexes stay the same. tokens.splice(i + 1, 1) tokens.splice(i - 1, 1) } /** - * @param {object} md Markdown object + * @param md Markdown object */ -export default (md) => { +export default (md: MarkdownIt): void => { /** * Markdownit plugin to unwrap previews from a paragraph * - * @param {object} state handed to the plugin - * @param {Array} state.tokens current token stream + * @param state handed to the plugin + * @param state.tokens current token stream */ - function linkPreviews({ tokens }) { + function linkPreviews({ tokens }: StateCore): void { // do not process first and last token for (let i = 1, l = tokens.length; i < l - 1; ++i) { if (isPreviewLinkInParagraph(tokens, i)) { diff --git a/src/markdownit/shouldInterpretAsMarkdown.js b/src/markdownit/shouldInterpretAsMarkdown.ts similarity index 54% rename from src/markdownit/shouldInterpretAsMarkdown.js rename to src/markdownit/shouldInterpretAsMarkdown.ts index 25fa6022974..ef7e640226e 100644 --- a/src/markdownit/shouldInterpretAsMarkdown.js +++ b/src/markdownit/shouldInterpretAsMarkdown.ts @@ -3,14 +3,14 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import hasMarkdownSyntax from './hasMarkdownSyntax.js' -import isValidMarkdown from './isValidMarkdown.js' +import hasMarkdownSyntax from './hasMarkdownSyntax.ts' +import isValidMarkdown from './isValidMarkdown.ts' /** * Check if the content has Markdown syntax * - * @param {string} content Markdown object + * @param content Markdown object */ -export default function shouldInterpretAsMarkdown(content) { +export default function shouldInterpretAsMarkdown(content: string) { return hasMarkdownSyntax(content) && isValidMarkdown(content) } diff --git a/src/markdownit/underline.js b/src/markdownit/underline.ts similarity index 80% rename from src/markdownit/underline.js rename to src/markdownit/underline.ts index 5e467f78749..4b50fa6287d 100644 --- a/src/markdownit/underline.js +++ b/src/markdownit/underline.ts @@ -3,10 +3,12 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ +import type { MarkdownIt } from 'markdown-it' + /** - * @param {object} md Markdown object + * @param md Markdown object */ -export default function markdownUnderlines(md) { +export default function markdownUnderlines(md: MarkdownIt) { md.inline.ruler2.after('emphasis', 'underline', (state) => { const tokens = state.tokens diff --git a/src/nodes/CommentReference.ts b/src/nodes/CommentReference.ts index a2c292dce68..cbbab6dcdf8 100644 --- a/src/nodes/CommentReference.ts +++ b/src/nodes/CommentReference.ts @@ -9,7 +9,7 @@ import { getCurrentUser } from '@nextcloud/auth' import { InputRule, mergeAttributes, Node } from '@tiptap/core' import { DOMParser } from '@tiptap/pm/model' import { TextSelection } from '@tiptap/pm/state' -import markdownit from '../markdownit/index.js' +import markdownit from '../markdownit/index.ts' import { commentBubbleKey } from '../plugins/commentBubble.ts' import { generateReferenceId, isInsideCommentOrFootnote } from '../plugins/referenceHelpers.ts' diff --git a/src/tests/markdown.spec.js b/src/tests/markdown.spec.js index f67768a5c50..2740f060fa7 100644 --- a/src/tests/markdown.spec.js +++ b/src/tests/markdown.spec.js @@ -4,7 +4,7 @@ */ import { typesAvailable } from '../markdownit/callouts.js' -import markdownit from '../markdownit/index.js' +import markdownit from '../markdownit/index.ts' import testEditor from './testHelpers/testEditor.js' const test = testEditor diff --git a/src/tests/markdownit/bulletList.spec.js b/src/tests/markdownit/bulletList.spec.js index 4c3e3d7b15a..498ee1e80bf 100644 --- a/src/tests/markdownit/bulletList.spec.js +++ b/src/tests/markdownit/bulletList.spec.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import markdownit from '../../markdownit/index.js' +import markdownit from '../../markdownit/index.ts' import stripIndent from './stripIndent.js' describe('markdownit', () => { diff --git a/src/tests/markdownit/callouts.spec.js b/src/tests/markdownit/callouts.spec.js index ff11eeb747b..038bf9846e8 100644 --- a/src/tests/markdownit/callouts.spec.js +++ b/src/tests/markdownit/callouts.spec.js @@ -4,7 +4,7 @@ */ import { typesAvailable } from '../../markdownit/callouts.js' -import markdownit from '../../markdownit/index.js' +import markdownit from '../../markdownit/index.ts' import stripIndent from './stripIndent.js' describe('callouts', () => { diff --git a/src/tests/markdownit/comments.spec.ts b/src/tests/markdownit/comments.spec.ts index 03cec48d5aa..d9d73c43720 100644 --- a/src/tests/markdownit/comments.spec.ts +++ b/src/tests/markdownit/comments.spec.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import markdownit from '../../markdownit/index.js' +import markdownit from '../../markdownit/index.ts' describe('comments (markdown-it)', () => { it('standard comment', () => { diff --git a/src/tests/markdownit/commonmark.spec.js b/src/tests/markdownit/commonmark.spec.js index c4b35949064..7c16c5967dc 100644 --- a/src/tests/markdownit/commonmark.spec.js +++ b/src/tests/markdownit/commonmark.spec.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import markdownit from '../../markdownit/index.js' +import markdownit from '../../markdownit/index.ts' import spec from '../fixtures/spec.js' describe('Commonmark', () => { diff --git a/src/tests/markdownit/details.spec.js b/src/tests/markdownit/details.spec.js index 86f1c636f08..724aad87bca 100644 --- a/src/tests/markdownit/details.spec.js +++ b/src/tests/markdownit/details.spec.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import markdownit from '../../markdownit/index.js' +import markdownit from '../../markdownit/index.ts' import stripIndent from './stripIndent.js' describe('Details extension', () => { diff --git a/src/tests/markdownit/footnotes.spec.ts b/src/tests/markdownit/footnotes.spec.ts index f15bf733f04..5d61938cba7 100644 --- a/src/tests/markdownit/footnotes.spec.ts +++ b/src/tests/markdownit/footnotes.spec.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import markdownit from '../../markdownit/index.js' +import markdownit from '../../markdownit/index.ts' describe('footnotes (markdown-it)', () => { it('simple footnote', () => { diff --git a/src/tests/markdownit/imageFigures.spec.js b/src/tests/markdownit/imageFigures.spec.js index d7f1a2f8729..131fc793948 100644 --- a/src/tests/markdownit/imageFigures.spec.js +++ b/src/tests/markdownit/imageFigures.spec.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import markdownit from '../../markdownit/index.js' +import markdownit from '../../markdownit/index.ts' describe('image figures extension', () => { it('renders images as figures', () => { diff --git a/src/tests/markdownit/mentions.spec.js b/src/tests/markdownit/mentions.spec.js index ad5280b811d..043be142e1f 100644 --- a/src/tests/markdownit/mentions.spec.js +++ b/src/tests/markdownit/mentions.spec.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import markdownit from '../../markdownit/index.js' +import markdownit from '../../markdownit/index.ts' import stripIndent from './stripIndent.js' describe('markdownit', () => { diff --git a/src/tests/markdownit/multimd-tables.spec.js b/src/tests/markdownit/multimd-tables.spec.js index e845cceacd1..90db73d5eb2 100644 --- a/src/tests/markdownit/multimd-tables.spec.js +++ b/src/tests/markdownit/multimd-tables.spec.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import markdownit from '../../markdownit/index.js' +import markdownit from '../../markdownit/index.ts' import stripIndent from './stripIndent.js' describe('multimd-table extension', () => { diff --git a/src/tests/markdownit/preview.spec.js b/src/tests/markdownit/preview.spec.js index 7ecc3c62c4f..6b9fe9fc6f3 100644 --- a/src/tests/markdownit/preview.spec.js +++ b/src/tests/markdownit/preview.spec.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import markdownit from '../../markdownit/index.js' +import markdownit from '../../markdownit/index.ts' describe('Preview extension', () => { const link = { diff --git a/src/tests/markdownit/referenceLinks.spec.ts b/src/tests/markdownit/referenceLinks.spec.ts index 870a850e8fe..a7a560ff529 100644 --- a/src/tests/markdownit/referenceLinks.spec.ts +++ b/src/tests/markdownit/referenceLinks.spec.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import markdownit from '../../markdownit/index.js' +import markdownit from '../../markdownit/index.ts' describe('reference style links (markdown-it)', () => { it('renders a reference link of type shortcut (omitted label)', () => { diff --git a/src/tests/markdownit/taskLists.spec.js b/src/tests/markdownit/taskLists.spec.js index 88211ad583d..94089e690d5 100644 --- a/src/tests/markdownit/taskLists.spec.js +++ b/src/tests/markdownit/taskLists.spec.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import markdownit from '../../markdownit/index.js' +import markdownit from '../../markdownit/index.ts' import stripIndent from './stripIndent.js' describe('task list extension', () => { diff --git a/src/tests/markdownit/wikiLinks.spec.ts b/src/tests/markdownit/wikiLinks.spec.ts index 09f0f89c4c3..a366ed4600b 100644 --- a/src/tests/markdownit/wikiLinks.spec.ts +++ b/src/tests/markdownit/wikiLinks.spec.ts @@ -4,7 +4,7 @@ */ import { describe, expect, it } from 'vitest' -import markdownit from '../../markdownit/index.js' +import markdownit from '../../markdownit/index.ts' describe('wiki image links (markdown-it)', () => { it('renders a standalone wiki image as a figure', () => { diff --git a/src/tests/nodes/ListConversion.spec.ts b/src/tests/nodes/ListConversion.spec.ts index 74145b8cfdf..c63c37c9eca 100644 --- a/src/tests/nodes/ListConversion.spec.ts +++ b/src/tests/nodes/ListConversion.spec.ts @@ -5,7 +5,7 @@ import { ListItem } from '@tiptap/extension-list' import { describe, expect } from 'vitest' -import markdownit from '../../markdownit/index.js' +import markdownit from '../../markdownit/index.ts' import BulletList from '../../nodes/BulletList.ts' import HardBreak from '../../nodes/HardBreak.js' import OrderedList from '../../nodes/OrderedList.ts' diff --git a/src/tests/nodes/Mathematics.spec.ts b/src/tests/nodes/Mathematics.spec.ts index 331940a97eb..4b271e6e354 100644 --- a/src/tests/nodes/Mathematics.spec.ts +++ b/src/tests/nodes/Mathematics.spec.ts @@ -5,7 +5,7 @@ import { getExtensionField } from '@tiptap/core' import { describe, expect } from 'vitest' -import markdownit from '../../markdownit/index.js' +import markdownit from '../../markdownit/index.ts' import { MathBlock, MathInline } from '../../nodes/Mathematics.js' import testEditor from '../testHelpers/testEditor.ts' diff --git a/src/tests/nodes/Table.spec.js b/src/tests/nodes/Table.spec.js index 80fb1360ba4..2ffdb483843 100644 --- a/src/tests/nodes/Table.spec.js +++ b/src/tests/nodes/Table.spec.js @@ -6,7 +6,7 @@ import { builders } from 'prosemirror-test-builder' import RichText from '../../extensions/RichText.js' -import markdownit from '../../markdownit/index.js' +import markdownit from '../../markdownit/index.ts' import EditableTable from '../../nodes/EditableTable.js' import output from '../fixtures/tables/basic/table.html?raw' import input from '../fixtures/tables/basic/table.md?raw' diff --git a/src/tests/testHelpers/markdown.js b/src/tests/testHelpers/markdown.js index 343420dad9b..2738b419f59 100644 --- a/src/tests/testHelpers/markdown.js +++ b/src/tests/testHelpers/markdown.js @@ -5,7 +5,7 @@ import { createRichEditor } from '../../EditorFactory.ts' import { createMarkdownSerializer } from '../../extensions/Markdown.js' -import markdownit from '../../markdownit/index.js' +import markdownit from '../../markdownit/index.ts' /** * Ease markdown through TipTap editor and return serialized markdown diff --git a/src/tests/testHelpers/testEditor.ts b/src/tests/testHelpers/testEditor.ts index bbb5ef90c09..642c60f077e 100644 --- a/src/tests/testHelpers/testEditor.ts +++ b/src/tests/testHelpers/testEditor.ts @@ -15,7 +15,7 @@ import { test as baseTest, expect } from 'vitest' import { createMarkdownSerializer } from '../../extensions/Markdown.js' import Markdown from '../../extensions/Markdown.js' import RichText from '../../extensions/RichText.ts' -import markdownit from '../../markdownit/index.js' +import markdownit from '../../markdownit/index.ts' import EditableTable from '../../nodes/EditableTable.js' import Paragraph from '../../nodes/Paragraph.js' import { createDocumentString } from './createDocumentString.ts' diff --git a/src/tests/tiptap.spec.js b/src/tests/tiptap.spec.js index 413352ed211..2d36f1196cd 100644 --- a/src/tests/tiptap.spec.js +++ b/src/tests/tiptap.spec.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import markdownit from '../markdownit/index.js' +import markdownit from '../markdownit/index.ts' import testEditor from './testHelpers/testEditor.js' const test = testEditor.extend({ From e4e0af13724b685c33d56fa01dacf101d8973ada Mon Sep 17 00:00:00 2001 From: Jonas Date: Mon, 28 Sep 2026 11:32:52 +0200 Subject: [PATCH 06/11] fix(markdownit): fix isValidMarkdown helper `markdownit.parse()` requires an env object as second argument. Signed-off-by: Jonas Assisted-by: OpenCode:claude-fable-5.1 --- src/markdownit/isValidMarkdown.ts | 2 +- src/tests/markdownit/isValidMarkdown.spec.ts | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 src/tests/markdownit/isValidMarkdown.spec.ts diff --git a/src/markdownit/isValidMarkdown.ts b/src/markdownit/isValidMarkdown.ts index 22e1fc695b0..6dec330e277 100644 --- a/src/markdownit/isValidMarkdown.ts +++ b/src/markdownit/isValidMarkdown.ts @@ -11,7 +11,7 @@ import markdownit from './index.ts' */ export default function isValidMarkdown(content: string): boolean { try { - markdownit.parse(content) + markdownit.parse(content, {}) return true } catch { return false diff --git a/src/tests/markdownit/isValidMarkdown.spec.ts b/src/tests/markdownit/isValidMarkdown.spec.ts new file mode 100644 index 00000000000..b5dc9308d19 --- /dev/null +++ b/src/tests/markdownit/isValidMarkdown.spec.ts @@ -0,0 +1,20 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import isValidMarkdown from '../../markdownit/isValidMarkdown.ts' + +describe('isValidMarkdown', () => { + it('accepts plain markdown', () => { + expect(isValidMarkdown('Some *emphasis* and a [link](https://example.com)')).to.equal(true) + }) + + it('accepts reference definitions', () => { + expect(isValidMarkdown('[foo]\n\n[foo]: https://example.com')).to.equal(true) + }) + + it('accepts footnotes', () => { + expect(isValidMarkdown('Foo[^1]\n\n[^1]: bar')).to.equal(true) + }) +}) From 204b726b875eaf758b75d66f365519bd1cbf5581 Mon Sep 17 00:00:00 2001 From: Jonas Date: Mon, 28 Sep 2026 11:45:23 +0200 Subject: [PATCH 07/11] chore(markdownit): migrate keepSyntax plugin to Typescript Signed-off-by: Jonas Assisted-by: OpenCode:claude-fable-5.1 --- src/markdownit/{callouts.js => callouts.ts} | 0 src/markdownit/index.ts | 2 +- .../{keepSyntax.js => keepSyntax.ts} | 34 ++++++++++++------- src/tests/markdownit/keepSyntax.spec.ts | 21 ++++++++++++ 4 files changed, 44 insertions(+), 13 deletions(-) rename src/markdownit/{callouts.js => callouts.ts} (100%) rename src/markdownit/{keepSyntax.js => keepSyntax.ts} (58%) create mode 100644 src/tests/markdownit/keepSyntax.spec.ts diff --git a/src/markdownit/callouts.js b/src/markdownit/callouts.ts similarity index 100% rename from src/markdownit/callouts.js rename to src/markdownit/callouts.ts diff --git a/src/markdownit/index.ts b/src/markdownit/index.ts index f858aadfea3..546e68161c7 100644 --- a/src/markdownit/index.ts +++ b/src/markdownit/index.ts @@ -14,7 +14,7 @@ import comments from './comments.ts' import details from './details.ts' import footnotes from './footnotes.ts' import hardbreak from './hardbreak.js' -import keepSyntax from './keepSyntax.js' +import keepSyntax from './keepSyntax.ts' import mathematics from './mathematics.ts' import preview from './preview.ts' import referenceLinks from './referenceLinks.ts' diff --git a/src/markdownit/keepSyntax.js b/src/markdownit/keepSyntax.ts similarity index 58% rename from src/markdownit/keepSyntax.js rename to src/markdownit/keepSyntax.ts index 160242a0fbd..8056f750f7a 100644 --- a/src/markdownit/keepSyntax.js +++ b/src/markdownit/keepSyntax.ts @@ -3,12 +3,25 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ +import type { MarkdownIt, StateCore, Token } from 'markdown-it' + +/** + * Create a copy of a token, optionally with different content + * + * @param state markdown-it state + * @param token token to copy + * @param content content of the copy + */ +function copyToken(state: StateCore, token: Token, content = token.content): Token { + return Object.assign(new state.Token(token.type, token.tag, token.nesting), token, { content }) +} + /** * Add a mark for keeping special markdown syntax unescaped * - * @param {object} md Markdown object + * @param md Markdown object */ -export default function keepSyntax(md) { +export default function keepSyntax(md: MarkdownIt): void { // Extracting named groups as positive lookbehind patterns are not supported by Safari const escaped = /(\n(?[#\-*+>])|(?[`*\\~[\]]+))/ @@ -19,7 +32,7 @@ export default function keepSyntax(md) { for (let i = 0; i < state.tokens.length; i++) { const block = state.tokens[i] - if (block.type !== 'inline') { + if (block.type !== 'inline' || !block.children) { continue } @@ -27,7 +40,7 @@ export default function keepSyntax(md) { const token = block.children[j] if (token.type === 'text') { const match = escaped.exec(token.content) - if (match) { + if (match?.groups) { const index = match.groups.linestart ? match.index + 1 : match.index @@ -37,14 +50,11 @@ export default function keepSyntax(md) { block.children.splice( j, 1, - { ...token, content: token.content.slice(0, index) }, - { ...open }, - { - ...token, - content: token.content.slice(index, contentNext), - }, - { ...close }, - { ...token, content: token.content.slice(contentNext) }, + copyToken(state, token, token.content.slice(0, index)), + copyToken(state, open), + copyToken(state, token, token.content.slice(index, contentNext)), + copyToken(state, close), + copyToken(state, token, token.content.slice(contentNext)), ) j += 3 } diff --git a/src/tests/markdownit/keepSyntax.spec.ts b/src/tests/markdownit/keepSyntax.spec.ts new file mode 100644 index 00000000000..6fb2d864b46 --- /dev/null +++ b/src/tests/markdownit/keepSyntax.spec.ts @@ -0,0 +1,21 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import MarkdownIt from 'markdown-it' +import markdownit from '../../markdownit/index.ts' + +describe('keepSyntax (markdown-it)', () => { + it('wraps special characters', () => { + expect(markdownit.render('a ~ b')) + .to.equal('

a ~ b

\n') + }) + + it('creates markdown-it tokens', () => { + const [, inline] = markdownit.parse('a ~ b', {}) + for (const token of inline.children ?? []) { + expect(token).to.be.instanceOf(MarkdownIt.Token) + } + }) +}) From 5e1a7b798217ba2d1d88c7aded5e56ef523b1010 Mon Sep 17 00:00:00 2001 From: Jonas Date: Mon, 28 Sep 2026 11:49:20 +0200 Subject: [PATCH 08/11] chore(markdownit): migrate callouts plugin to Typescript Signed-off-by: Jonas Assisted-by: OpenCode:claude-fable-5.1 --- src/declarations.d.ts | 11 +++++++++++ src/markdownit/callouts.ts | 14 ++++++++------ src/markdownit/index.ts | 2 +- src/nodes/Callout.js | 2 +- src/tests/markdown.spec.js | 2 +- src/tests/markdownit/callouts.spec.js | 2 +- 6 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/declarations.d.ts b/src/declarations.d.ts index 2c5257de47f..a38213a427b 100644 --- a/src/declarations.d.ts +++ b/src/declarations.d.ts @@ -19,6 +19,17 @@ declare module '@quartzy/markdown-it-mentions' { export default plugin } +declare module 'markdown-it-container' { + import type { MarkdownIt, RendererRule } from 'markdown-it' + interface ContainerOptions { + marker?: string + validate?: (params: string, markup: string) => boolean + render?: RendererRule + } + const plugin: (md: MarkdownIt, name: string, options?: ContainerOptions) => void + export default plugin +} + declare module 'markdown-it-image-figures' { import type { MarkdownIt } from 'markdown-it' const plugin: (md: MarkdownIt) => void diff --git a/src/markdownit/callouts.ts b/src/markdownit/callouts.ts index cd8871078bc..cc88edda739 100644 --- a/src/markdownit/callouts.ts +++ b/src/markdownit/callouts.ts @@ -3,16 +3,18 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ +import type { MarkdownIt, RendererRule } from 'markdown-it' + import container from 'markdown-it-container' export const typesAvailable = ['info', 'warn', 'error', 'success', 'question'] /** * - * @param {string} type one of 'info', 'warn', 'error' and 'success' + * @param type one of 'info', 'warn', 'error' and 'success' */ -function buildRender(type) { - return (tokens, idx, options, env, slf) => { +function buildRender(type: string): RendererRule { + return (tokens, idx, options, _env, slf) => { const tag = tokens[idx] // add attributes to the opening tag @@ -21,14 +23,14 @@ function buildRender(type) { tag.attrJoin('class', `callout callout-${type}`) } - return slf.renderToken(tokens, idx, options, env, slf) + return slf.renderToken(tokens, idx, options) } } /** - * @param {object} md Markdown object + * @param md Markdown object */ -export default (md) => { +export default (md: MarkdownIt): MarkdownIt => { // create a custom container to each callout type typesAvailable.forEach((type) => { md.use(container, type, { diff --git a/src/markdownit/index.ts b/src/markdownit/index.ts index 546e68161c7..6acae7747fe 100644 --- a/src/markdownit/index.ts +++ b/src/markdownit/index.ts @@ -9,7 +9,7 @@ import frontMatter from 'markdown-it-front-matter' import implicitFigures from 'markdown-it-image-figures' import mark from 'markdown-it-mark' import multimdTable from 'markdown-it-multimd-table' -import callouts from './callouts.js' +import callouts from './callouts.ts' import comments from './comments.ts' import details from './details.ts' import footnotes from './footnotes.ts' diff --git a/src/nodes/Callout.js b/src/nodes/Callout.js index a9c17385953..a48bf43448e 100644 --- a/src/nodes/Callout.js +++ b/src/nodes/Callout.js @@ -6,7 +6,7 @@ import { isNodeActive, mergeAttributes, Node } from '@tiptap/core' import { VueNodeViewRenderer } from '@tiptap/vue-3' import CalloutView from './CalloutView.vue' -import { typesAvailable } from '../markdownit/callouts.js' +import { typesAvailable } from '../markdownit/callouts.ts' export default Node.create({ name: 'callout', diff --git a/src/tests/markdown.spec.js b/src/tests/markdown.spec.js index 2740f060fa7..0a3b5350efe 100644 --- a/src/tests/markdown.spec.js +++ b/src/tests/markdown.spec.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import { typesAvailable } from '../markdownit/callouts.js' +import { typesAvailable } from '../markdownit/callouts.ts' import markdownit from '../markdownit/index.ts' import testEditor from './testHelpers/testEditor.js' diff --git a/src/tests/markdownit/callouts.spec.js b/src/tests/markdownit/callouts.spec.js index 038bf9846e8..de3a7f34213 100644 --- a/src/tests/markdownit/callouts.spec.js +++ b/src/tests/markdownit/callouts.spec.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import { typesAvailable } from '../../markdownit/callouts.js' +import { typesAvailable } from '../../markdownit/callouts.ts' import markdownit from '../../markdownit/index.ts' import stripIndent from './stripIndent.js' From 6a4cb68fbf339b90a735d7395e84fab425062f62 Mon Sep 17 00:00:00 2001 From: Jonas Date: Mon, 28 Sep 2026 11:54:41 +0200 Subject: [PATCH 09/11] chore(markdownit): migrate hardbreak plugin to Typescript Signed-off-by: Jonas Assisted-by: OpenCode:claude-fable-5.1 --- src/markdownit/{hardbreak.js => hardbreak.ts} | 6 ++++-- src/markdownit/index.ts | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) rename src/markdownit/{hardbreak.js => hardbreak.ts} (92%) diff --git a/src/markdownit/hardbreak.js b/src/markdownit/hardbreak.ts similarity index 92% rename from src/markdownit/hardbreak.js rename to src/markdownit/hardbreak.ts index e8781e71a9e..863b0fb34da 100644 --- a/src/markdownit/hardbreak.js +++ b/src/markdownit/hardbreak.ts @@ -3,12 +3,14 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ +import type { MarkdownIt } from 'markdown-it' + /** * Add information about used markdown syntax to HTML hard breaks * - * @param {import('markdown-it').MarkdownIt} md Markdown object + * @param md Markdown object */ -export default function keepHardbreakSyntax(md) { +export default function keepHardbreakSyntax(md: MarkdownIt) { const { ruler } = md.inline const markdownitNewline = ruler.__rules__[ruler.__find__('newline')].fn const markdownitEscape = ruler.__rules__[ruler.__find__('escape')].fn diff --git a/src/markdownit/index.ts b/src/markdownit/index.ts index 6acae7747fe..2a372e27bfa 100644 --- a/src/markdownit/index.ts +++ b/src/markdownit/index.ts @@ -13,7 +13,7 @@ import callouts from './callouts.ts' import comments from './comments.ts' import details from './details.ts' import footnotes from './footnotes.ts' -import hardbreak from './hardbreak.js' +import hardbreak from './hardbreak.ts' import keepSyntax from './keepSyntax.ts' import mathematics from './mathematics.ts' import preview from './preview.ts' From 4a7089705e55c55f5a1e22c755cf76d95ad20842 Mon Sep 17 00:00:00 2001 From: Jonas Date: Mon, 28 Sep 2026 12:02:06 +0200 Subject: [PATCH 10/11] fix(markdownit): don't emit html breaks in silent mode Fixes html breaks in link labels. Signed-off-by: Jonas Assisted-by: OpenCode:claude-fable-5.1 --- src/markdownit/hardbreak.ts | 16 +++++++++------ src/tests/markdownit/hardbreak.spec.ts | 28 ++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 6 deletions(-) create mode 100644 src/tests/markdownit/hardbreak.spec.ts diff --git a/src/markdownit/hardbreak.ts b/src/markdownit/hardbreak.ts index 863b0fb34da..746fd0c0e14 100644 --- a/src/markdownit/hardbreak.ts +++ b/src/markdownit/hardbreak.ts @@ -10,7 +10,7 @@ import type { MarkdownIt } from 'markdown-it' * * @param md Markdown object */ -export default function keepHardbreakSyntax(md: MarkdownIt) { +export default function keepHardbreakSyntax(md: MarkdownIt): void { const { ruler } = md.inline const markdownitNewline = ruler.__rules__[ruler.__find__('newline')].fn const markdownitEscape = ruler.__rules__[ruler.__find__('escape')].fn @@ -18,7 +18,8 @@ export default function keepHardbreakSyntax(md: MarkdownIt) { md.inline.ruler.at('newline', (state, silent) => { const rval = markdownitNewline(state, silent) if ( - rval + !silent + && rval && state.tokens.length && state.tokens[state.tokens.length - 1].type === 'hardbreak' ) { state.tokens[state.tokens.length - 1].attrSet('syntax', ' ') } @@ -29,7 +30,8 @@ export default function keepHardbreakSyntax(md: MarkdownIt) { md.inline.ruler.at('escape', (state, silent) => { const rval = markdownitEscape(state, silent) if ( - rval + !silent + && rval && state.tokens.length && state.tokens[state.tokens.length - 1].type === 'hardbreak' ) { state.tokens[state.tokens.length - 1].attrSet('syntax', '\\') } @@ -37,12 +39,14 @@ export default function keepHardbreakSyntax(md: MarkdownIt) { }) // Add rule for parsing `
` tags (as we have HTML disabled) - md.inline.ruler.after('html_inline', 'html_breaks', (state) => { + md.inline.ruler.after('html_inline', 'html_breaks', (state, silent) => { const res = state.src.slice(state.pos).match(/^\s*/) if (res) { - const token = state.push('hardbreak', 'br', 0) - token.attrPush(['syntax', 'html']) + if (!silent) { + const token = state.push('hardbreak', 'br', 0) + token.attrPush(['syntax', 'html']) + } state.pos += res[0].length return true } diff --git a/src/tests/markdownit/hardbreak.spec.ts b/src/tests/markdownit/hardbreak.spec.ts new file mode 100644 index 00000000000..dc3cf809875 --- /dev/null +++ b/src/tests/markdownit/hardbreak.spec.ts @@ -0,0 +1,28 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import markdownit from '../../markdownit/index.ts' + +describe('hardbreak (markdown-it)', () => { + it('keeps double space syntax', () => { + expect(markdownit.render('a \nb')) + .to.equal('

a
b

\n') + }) + + it('keeps backslash syntax', () => { + expect(markdownit.render('a\\\nb')) + .to.equal('

a
b

\n') + }) + + it('parses html breaks', () => { + expect(markdownit.render('a
b')) + .to.equal('

a
b

\n') + }) + + it('parses html breaks in link labels', () => { + expect(markdownit.render('[a
b](https://example.com)')) + .to.equal('

a
b

\n') + }) +}) From 9299562a9f0e726a96f78fbfcafaded7f00edc0e Mon Sep 17 00:00:00 2001 From: Jonas Date: Mon, 28 Sep 2026 12:13:28 +0200 Subject: [PATCH 11/11] chore(markdownit): migrate splitMixedLists plugin to Typescript Signed-off-by: Jonas Assisted-by: OpenCode:claude-fable-5.1 --- src/markdownit/index.ts | 2 +- ...{splitMixedLists.js => splitMixedLists.ts} | 34 ++++++++++--------- src/tests/markdownit/splitMixedLists.spec.ts | 30 ++++++++++++++++ 3 files changed, 49 insertions(+), 17 deletions(-) rename src/markdownit/{splitMixedLists.js => splitMixedLists.ts} (59%) create mode 100644 src/tests/markdownit/splitMixedLists.spec.ts diff --git a/src/markdownit/index.ts b/src/markdownit/index.ts index 2a372e27bfa..c21efeae3f2 100644 --- a/src/markdownit/index.ts +++ b/src/markdownit/index.ts @@ -18,7 +18,7 @@ import keepSyntax from './keepSyntax.ts' import mathematics from './mathematics.ts' import preview from './preview.ts' import referenceLinks from './referenceLinks.ts' -import splitMixedLists from './splitMixedLists.js' +import splitMixedLists from './splitMixedLists.ts' import taskLists from './taskLists.ts' import underline from './underline.ts' import wikiLinks from './wikiLinks.ts' diff --git a/src/markdownit/splitMixedLists.js b/src/markdownit/splitMixedLists.ts similarity index 59% rename from src/markdownit/splitMixedLists.js rename to src/markdownit/splitMixedLists.ts index be46924c0a9..8cbf6d32469 100644 --- a/src/markdownit/splitMixedLists.js +++ b/src/markdownit/splitMixedLists.ts @@ -3,10 +3,12 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ +import type { MarkdownIt, StateCore, Token } from 'markdown-it' + /** - * @param {import('markdown-it').MarkdownIt} md Markdown object + * @param md Markdown object */ -export default function splitMixedLists(md) { +export default function splitMixedLists(md: MarkdownIt): void { md.core.ruler.after('task-lists', 'split-mixed-task-lists', (state) => { const tokens = state.tokens @@ -17,8 +19,8 @@ export default function splitMixedLists(md) { } const firstChild = tokens[i + 1] const startsWithTask = includesClass(firstChild, 'task-list-item') - if (!startsWithTask) { - token.attrs.splice(token.attrIndex('class')) + if (!startsWithTask && token.attrs) { + token.attrs.splice(token.attrIndex('class'), 1) if (token.attrs.length === 0) { token.attrs = null } @@ -39,19 +41,19 @@ export default function splitMixedLists(md) { } /** - * @param {object} token MarkdownIT token - * @param {string} cls Class name to query + * @param token MarkdownIT token + * @param cls Class name to query */ -function includesClass(token, cls) { - return token.attrGet('class')?.split(' ').includes(cls) || false +function includesClass(token: Token, cls: string): boolean { + return String(token.attrGet('class') ?? '').split(' ').includes(cls) } /** - * @param {Array} tokens - all the tokens in the doc - * @param {number} index - index into the tokens array where to split - * @param {object} TokenConstructor - constructor provided by Markdown-it + * @param tokens - all the tokens in the doc + * @param index - index into the tokens array where to split + * @param TokenConstructor - constructor provided by Markdown-it */ -function splitListAt(tokens, index, TokenConstructor) { +function splitListAt(tokens: Token[], index: number, TokenConstructor: StateCore['Token']): void { const closeList = new TokenConstructor('bullet_list_close', 'ul', -1) closeList.block = true const openList = new TokenConstructor('bullet_list_open', 'ul', 1) @@ -62,11 +64,11 @@ function splitListAt(tokens, index, TokenConstructor) { } /** - * @param {Array} tokens - all the tokens in the doc - * @param {number} parentIndex - index of the parent in the tokens array - * @param {() => boolean} predicate - test function returned child needs to pass + * @param tokens - all the tokens in the doc + * @param parentIndex - index of the parent in the tokens array + * @param predicate - test function returned child needs to pass */ -function findChildOf(tokens, parentIndex, predicate) { +function findChildOf(tokens: Token[], parentIndex: number, predicate: (child: Token) => boolean): number { const searchLevel = tokens[parentIndex].level + 1 for (let i = parentIndex + 1; i < tokens.length; i++) { const token = tokens[i] diff --git a/src/tests/markdownit/splitMixedLists.spec.ts b/src/tests/markdownit/splitMixedLists.spec.ts new file mode 100644 index 00000000000..a74f1d47214 --- /dev/null +++ b/src/tests/markdownit/splitMixedLists.spec.ts @@ -0,0 +1,30 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import markdownit from '../../markdownit/index.ts' + +const taskItem = (id: number) => `
  • task
  • ` + +describe('splitMixedLists (markdown-it)', () => { + it('splits a list starting with a task', () => { + expect(markdownit.render('* [ ] task\n* not a task')) + .to.equal('
      \n' + + `${taskItem(0)}\n` + + '
    \n' + + '
      \n' + + '
    • not a task
    • \n' + + '
    \n') + }) + + it('splits a list starting with a regular item', () => { + expect(markdownit.render('* not a task\n* [ ] task')) + .to.equal('
      \n' + + '
    • not a task
    • \n' + + '
    \n' + + '
      \n' + + `${taskItem(1)}\n` + + '
    \n') + }) +})