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/package-lock.json b/package-lock.json index 8c4649d6e0f..3d151711829 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": "^15.0.2", "markdown-it-container": "^4.0.0", "markdown-it-footnote": "^4.0.0", "markdown-it-front-matter": "^0.2.4", @@ -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", @@ -99,7 +99,6 @@ "@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-footnote": "^3.0.4", "@vitejs/plugin-vue": "^6.0.9", "@vitest/coverage-v8": "^5.0.1", @@ -3730,18 +3729,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 +6505,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", @@ -7636,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", @@ -10595,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" @@ -13975,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", @@ -13990,7 +14016,7 @@ ], "license": "MIT", "dependencies": { - "uc.micro": "^2.0.0" + "uc.micro": "^3.0.0" } }, "node_modules/linkifyjs": { @@ -14414,9 +14440,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": "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", @@ -14429,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" @@ -14749,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": { @@ -16049,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", @@ -16588,9 +16601,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", @@ -16598,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", @@ -19196,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 6e870f09c11..ae75c972185 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": "^15.0.2", "markdown-it-container": "^4.0.0", "markdown-it-footnote": "^4.0.0", "markdown-it-front-matter": "^0.2.4", @@ -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", @@ -117,7 +117,6 @@ "@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-footnote": "^3.0.4", "@vitejs/plugin-vue": "^6.0.9", "@vitest/coverage-v8": "^5.0.1", 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 ({ 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/*"] }, { 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 530eca365d8..a38213a427b 100644 --- a/src/declarations.d.ts +++ b/src/declarations.d.ts @@ -13,11 +13,33 @@ 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 '@quartzy/markdown-it-mentions' { + import type { MarkdownIt } from 'markdown-it' + const plugin: (md: MarkdownIt) => void + 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 + 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 4c676afdc31..e5074fa2c85 100644 --- a/src/extensions/Markdown.ts +++ b/src/extensions/Markdown.ts @@ -30,9 +30,8 @@ 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 markdownit from '../markdownit/index.ts' import Link from '../marks/Link.ts' import transformPastedHTML from './transformPastedHTML.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/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/callouts.js b/src/markdownit/callouts.ts similarity index 64% rename from src/markdownit/callouts.js rename to src/markdownit/callouts.ts index cd8871078bc..cc88edda739 100644 --- a/src/markdownit/callouts.js +++ 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/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.ts similarity index 71% rename from src/markdownit/hardbreak.js rename to src/markdownit/hardbreak.ts index facdb136695..746fd0c0e14 100644 --- a/src/markdownit/hardbreak.js +++ b/src/markdownit/hardbreak.ts @@ -3,20 +3,23 @@ * 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' +import type { MarkdownIt } from 'markdown-it' /** * Add information about used markdown syntax to HTML hard breaks * - * @param {import('markdown-it')} md Markdown object + * @param md Markdown object */ -export default function keepHardbreakSyntax(md) { +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 // Add syntax information to hard line breaks using double spaces 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', ' ') } @@ -27,7 +30,8 @@ export default function keepHardbreakSyntax(md) { 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', '\\') } @@ -35,12 +39,14 @@ export default function keepHardbreakSyntax(md) { }) // 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/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 68% rename from src/markdownit/index.js rename to src/markdownit/index.ts index 537ea5e491b..c21efeae3f2 100644 --- a/src/markdownit/index.js +++ b/src/markdownit/index.ts @@ -9,25 +9,36 @@ 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 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 keepSyntax from './keepSyntax.js' +import hardbreak from './hardbreak.ts' +import keepSyntax from './keepSyntax.ts' 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 splitMixedLists from './splitMixedLists.ts' 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 md Markdown object + * @param options multimd-table options + */ +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) @@ -44,17 +55,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(String(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/isValidMarkdown.js b/src/markdownit/isValidMarkdown.ts similarity index 58% rename from src/markdownit/isValidMarkdown.js rename to src/markdownit/isValidMarkdown.ts index 7727cf98c86..6dec330e277 100644 --- a/src/markdownit/isValidMarkdown.js +++ b/src/markdownit/isValidMarkdown.ts @@ -2,16 +2,16 @@ * 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) + markdownit.parse(content, {}) return true } catch { return false 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/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/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/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/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/splitMixedLists.js b/src/markdownit/splitMixedLists.ts similarity index 59% rename from src/markdownit/splitMixedLists.js rename to src/markdownit/splitMixedLists.ts index e605f48fe69..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')} 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/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/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/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 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/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..0a3b5350efe 100644 --- a/src/tests/markdown.spec.js +++ b/src/tests/markdown.spec.js @@ -3,8 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import { typesAvailable } from '../markdownit/callouts.js' -import markdownit from '../markdownit/index.js' +import { typesAvailable } from '../markdownit/callouts.ts' +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..de3a7f34213 100644 --- a/src/tests/markdownit/callouts.spec.js +++ b/src/tests/markdownit/callouts.spec.js @@ -3,8 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import { typesAvailable } from '../../markdownit/callouts.js' -import markdownit from '../../markdownit/index.js' +import { typesAvailable } from '../../markdownit/callouts.ts' +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/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') + }) +}) 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/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) + }) +}) 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) + } + }) +}) 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/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') + }) +}) 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({