diff --git a/cypress/e2e/shortcuts.spec.js b/cypress/e2e/shortcuts.spec.js
index 7a30afa2b5b..6a53a63b184 100644
--- a/cypress/e2e/shortcuts.spec.js
+++ b/cypress/e2e/shortcuts.spec.js
@@ -37,6 +37,7 @@ describe('keyboard shortcuts', () => {
it('italic', () => testShortcut(`${modKey}i`, 'em'))
it('underline', () => testShortcut(`${modKey}u`, 'u'))
it('strikethrough', () => testShortcut(`${modKey}{shift}s`, 's'))
+ it('highlight', () => testShortcut(`${modKey}{shift}h`, 'mark'))
it('blockquote', () => testShortcut(`${modKey}{shift}b`, 'blockquote'))
it('codeblock', () => testShortcut(`${modKey}{alt}c`, 'pre'))
it('ordered-list', () => testShortcut(`${modKey}{shift}7`, 'ol'))
diff --git a/package-lock.json b/package-lock.json
index 1ac0cbd0e3a..77726ec9879 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -39,6 +39,7 @@
"@tiptap/extension-drag-handle-vue-2": "^3.19.0",
"@tiptap/extension-hard-break": "^3.19.0",
"@tiptap/extension-heading": "^3.19.0",
+ "@tiptap/extension-highlight": "^3.20.1",
"@tiptap/extension-horizontal-rule": "^3.19.0",
"@tiptap/extension-image": "^3.19.0",
"@tiptap/extension-italic": "^3.19.0",
@@ -68,6 +69,7 @@
"markdown-it-container": "^4.0.0",
"markdown-it-front-matter": "^0.2.4",
"markdown-it-image-figures": "^2.1.1",
+ "markdown-it-mark": "^4.0.0",
"markdown-it-multimd-table": "^4.2.3",
"mermaid": "^11.12.2",
"mitt": "^3.0.1",
@@ -5180,9 +5182,9 @@
"license": "MIT"
},
"node_modules/@tiptap/core": {
- "version": "3.19.0",
- "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.19.0.tgz",
- "integrity": "sha512-bpqELwPW+DG8gWiD8iiFtSl4vIBooG5uVJod92Qxn3rA9nFatyXRr4kNbMJmOZ66ezUvmCjXVe/5/G4i5cyzKA==",
+ "version": "3.20.1",
+ "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.20.1.tgz",
+ "integrity": "sha512-SwkPEWIfaDEZjC8SEIi4kZjqIYUbRgLUHUuQezo5GbphUNC8kM1pi3C3EtoOPtxXrEbY6e4pWEzW54Pcrd+rVA==",
"license": "MIT",
"peer": true,
"funding": {
@@ -5190,7 +5192,7 @@
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
- "@tiptap/pm": "^3.19.0"
+ "@tiptap/pm": "^3.20.1"
}
},
"node_modules/@tiptap/extension-blockquote": {
@@ -5406,6 +5408,19 @@
"@tiptap/core": "^3.19.0"
}
},
+ "node_modules/@tiptap/extension-highlight": {
+ "version": "3.20.1",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-highlight/-/extension-highlight-3.20.1.tgz",
+ "integrity": "sha512-Ufv1eNsQBR7NNSxQIvHaI9Bm3/MS8GW9uCXR/20WJ2r0GhDA6vwnS5MHAhP5JUXK6KOsd0Rvz2cfvYB7f3okbQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^3.20.1"
+ }
+ },
"node_modules/@tiptap/extension-horizontal-rule": {
"version": "3.19.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.19.0.tgz",
@@ -5630,9 +5645,9 @@
}
},
"node_modules/@tiptap/pm": {
- "version": "3.19.0",
- "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.19.0.tgz",
- "integrity": "sha512-789zcnM4a8OWzvbD2DL31d0wbSm9BVeO/R7PLQwLIGysDI3qzrcclyZ8yhqOEVuvPitRRwYLq+mY14jz7kY4cw==",
+ "version": "3.20.1",
+ "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.20.1.tgz",
+ "integrity": "sha512-6kCiGLvpES4AxcEuOhb7HR7/xIeJWMjZlb6J7e8zpiIh5BoQc7NoRdctsnmFEjZvC19bIasccshHQ7H2zchWqw==",
"license": "MIT",
"peer": true,
"dependencies": {
@@ -14598,6 +14613,12 @@
"markdown-it": "*"
}
},
+ "node_modules/markdown-it-mark": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/markdown-it-mark/-/markdown-it-mark-4.0.0.tgz",
+ "integrity": "sha512-YLhzaOsU9THO/cal0lUjfMjrqSMPjjyjChYM7oyj4DnyaXEzA8gnW6cVJeyCrCVeyesrY2PlEdUYJSPFYL4Nkg==",
+ "license": "MIT"
+ },
"node_modules/markdown-it-multimd-table": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/markdown-it-multimd-table/-/markdown-it-multimd-table-4.2.3.tgz",
diff --git a/package.json b/package.json
index ac48ef0cb99..629a30a3a01 100644
--- a/package.json
+++ b/package.json
@@ -59,6 +59,7 @@
"@tiptap/extension-drag-handle-vue-2": "^3.19.0",
"@tiptap/extension-hard-break": "^3.19.0",
"@tiptap/extension-heading": "^3.19.0",
+ "@tiptap/extension-highlight": "^3.20.1",
"@tiptap/extension-horizontal-rule": "^3.19.0",
"@tiptap/extension-image": "^3.19.0",
"@tiptap/extension-italic": "^3.19.0",
@@ -88,6 +89,7 @@
"markdown-it-container": "^4.0.0",
"markdown-it-front-matter": "^0.2.4",
"markdown-it-image-figures": "^2.1.1",
+ "markdown-it-mark": "^4.0.0",
"markdown-it-multimd-table": "^4.2.3",
"mermaid": "^11.12.2",
"mitt": "^3.0.1",
diff --git a/playwright/e2e/format-text.spec.ts b/playwright/e2e/format-text.spec.ts
index f71c6e3f35a..be27c9692e4 100644
--- a/playwright/e2e/format-text.spec.ts
+++ b/playwright/e2e/format-text.spec.ts
@@ -26,6 +26,7 @@ new Map([
Italic: 'em',
Underline: 'u',
Strikethrough: 's',
+ Highlight: 'mark',
}
await editor.type('Format me')
for (const [button] of Object.entries(buttons)) {
diff --git a/src/components/HelpModal.vue b/src/components/HelpModal.vue
index 828246c4c55..c33a262d4a8 100644
--- a/src/components/HelpModal.vue
+++ b/src/components/HelpModal.vue
@@ -77,6 +77,17 @@
I
+
+ | {{ t('text', 'Underline') }} |
+
+ __{{ t('text', 'Underlined text') }}__
+ |
+
+ {{ ctrlOrModKey }}
+ +
+ U
+ |
+
| {{ t('text', 'Strikethrough') }} |
@@ -91,14 +102,16 @@
|
- | {{ t('text', 'Underline') }} |
+ {{ t('text', 'Highlight') }} |
- __{{ t('text', 'Underlined text') }}__
+ =={{ t('text', 'Highlighted text') }}==
|
{{ ctrlOrModKey }}
+
- U
+ {{ t('text', 'Shift') }}
+ +
+ H
|
diff --git a/src/components/Menu/entries.ts b/src/components/Menu/entries.ts
index 38aed1e0073..08e71aaed75 100644
--- a/src/components/Menu/entries.ts
+++ b/src/components/Menu/entries.ts
@@ -9,6 +9,7 @@ import {
Danger,
Emoticon,
FormatBold,
+ FormatColorHighlight,
FormatHeader1,
FormatHeader2,
FormatHeader3,
@@ -282,6 +283,18 @@ export const getMenuEntries = (isRichWorkspace: boolean): MenuEntry[] => {
},
priority: 13,
},
+ {
+ key: 'highlight',
+ label: t('text', 'Highlight'),
+ keyChar: 'h',
+ keyModifiers: [MODIFIERS.Mod, MODIFIERS.Shift],
+ icon: FormatColorHighlight,
+ isActive: 'highlight',
+ action: (command) => {
+ return command.toggleHighlight()
+ },
+ priority: 14,
+ },
{
key: 'lists',
label: t('text', 'Lists'),
@@ -477,7 +490,7 @@ export const getMenuEntries = (isRichWorkspace: boolean): MenuEntry[] => {
action: (command) => {
return command.insertTable()
},
- priority: 14,
+ priority: 15,
},
{
key: 'details',
@@ -487,7 +500,7 @@ export const getMenuEntries = (isRichWorkspace: boolean): MenuEntry[] => {
action: (command) => {
return command.toggleDetails()
},
- priority: 15,
+ priority: 16,
},
{
key: 'insert-link',
diff --git a/src/components/icons.js b/src/components/icons.js
index 613119b99c7..b2ec9357ef7 100644
--- a/src/components/icons.js
+++ b/src/components/icons.js
@@ -24,6 +24,7 @@ import MDI_Emoticon from 'vue-material-design-icons/EmoticonOutline.vue'
import MDI_Document from 'vue-material-design-icons/FileDocument.vue'
import MDI_Folder from 'vue-material-design-icons/FolderOutline.vue'
import MDI_FormatBold from 'vue-material-design-icons/FormatBold.vue'
+import MDI_FormatColorHighlight from 'vue-material-design-icons/FormatColorHighlight.vue'
import MDI_FormatHeader1 from 'vue-material-design-icons/FormatHeader1.vue'
import MDI_FormatHeader2 from 'vue-material-design-icons/FormatHeader2.vue'
import MDI_FormatHeader3 from 'vue-material-design-icons/FormatHeader3.vue'
@@ -106,6 +107,7 @@ export const DotsHorizontal = makeIcon(MDI_DotsHorizontal)
export const Emoticon = makeIcon(MDI_Emoticon)
export const Folder = makeIcon(MDI_Folder)
export const FormatBold = makeIcon(MDI_FormatBold)
+export const FormatColorHighlight = makeIcon(MDI_FormatColorHighlight)
export const FormatSize = makeIcon(MDI_FormatSize)
export const FormatHeader1 = makeIcon(MDI_FormatHeader1)
export const FormatHeader2 = makeIcon(MDI_FormatHeader2)
diff --git a/src/css/prosemirror.scss b/src/css/prosemirror.scss
index 5491128c31b..688f802a467 100644
--- a/src/css/prosemirror.scss
+++ b/src/css/prosemirror.scss
@@ -146,6 +146,10 @@ div.ProseMirror {
font-style: italic;
}
+ mark {
+ background-color: var(--color-mark, #fff0c7);
+ }
+
h1,
h2,
h3,
diff --git a/src/extensions/RichText.js b/src/extensions/RichText.js
index e1763c62edb..4a1501bbf39 100644
--- a/src/extensions/RichText.js
+++ b/src/extensions/RichText.js
@@ -26,7 +26,14 @@ import Mention from './../extensions/Mention.js'
import Search from './../extensions/Search.ts'
import TextDirection from './../extensions/TextDirection.ts'
import Typography from './../extensions/Typography.ts'
-import { Italic, Link, Strike, Strong, Underline } from './../marks/index.js'
+import {
+ Highlight,
+ Italic,
+ Link,
+ Strike,
+ Strong,
+ Underline,
+} from './../marks/index.js'
import BulletList from './../nodes/BulletList.js'
import Callouts from './../nodes/Callouts.js'
import CodeBlock from './../nodes/CodeBlock.js'
@@ -73,6 +80,7 @@ export default Extension.create({
HardBreak,
Heading,
Strong,
+ Highlight,
Italic,
Strike,
Blockquote,
diff --git a/src/markdownit/index.js b/src/markdownit/index.js
index e0fc13c32c6..168330a6881 100644
--- a/src/markdownit/index.js
+++ b/src/markdownit/index.js
@@ -7,6 +7,7 @@ import markdownitMentions from '@quartzy/markdown-it-mentions'
import MarkdownIt from 'markdown-it'
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'
@@ -33,6 +34,7 @@ const markdownit = MarkdownIt('commonmark', { html: false, breaks: false })
.use(keepSyntax)
.use(markdownitMentions)
.use(implicitFigures)
+ .use(mark)
.use(mathematics)
.use(multimdTable, {
multiline: true,
diff --git a/src/marks/Highlight.ts b/src/marks/Highlight.ts
new file mode 100644
index 00000000000..acac9c83001
--- /dev/null
+++ b/src/marks/Highlight.ts
@@ -0,0 +1,18 @@
+/**
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import TipTapHighlight from '@tiptap/extension-highlight'
+
+const Highlight = TipTapHighlight.extend({
+ // @ts-expect-error - toMarkdown is a custom field not part of the official Tiptap API
+ toMarkdown: {
+ open: '==',
+ close: '==',
+ mixable: true,
+ expelEnclosingWhitespace: true,
+ },
+})
+
+export default Highlight
diff --git a/src/marks/index.js b/src/marks/index.js
index 20a8b7c151c..4839823e6ec 100644
--- a/src/marks/index.js
+++ b/src/marks/index.js
@@ -4,6 +4,7 @@
*/
import TipTapItalic from '@tiptap/extension-italic'
+import Highlight from './Highlight.ts'
import Link from './Link.js'
import Strike from './Strike.js'
import Strong from './Strong.js'
@@ -13,4 +14,4 @@ const Italic = TipTapItalic.extend({
name: 'em',
})
-export { Italic, Link, Strike, Strong, Underline }
+export { Highlight, Italic, Link, Strike, Strong, Underline }
diff --git a/src/tests/marks/Highlight.spec.ts b/src/tests/marks/Highlight.spec.ts
new file mode 100644
index 00000000000..396574c1c58
--- /dev/null
+++ b/src/tests/marks/Highlight.spec.ts
@@ -0,0 +1,36 @@
+/**
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import createCustomEditor from '../testHelpers/createCustomEditor'
+import Highlight from './../../marks/Highlight'
+
+describe('Highlight extension unit', () => {
+ it('exposes toMarkdown function', () => {
+ // @ts-expect-error - toMarkdown is a custom field not part of the official Tiptap API
+ const toMarkdown = Highlight.config.toMarkdown
+ expect(JSON.stringify(toMarkdown)).to.equal(
+ JSON.stringify({
+ open: '==',
+ close: '==',
+ mixable: true,
+ expelEnclosingWhitespace: true,
+ }),
+ )
+ })
+})
+
+describe('Highlight extension integrated in the editor', () => {
+ it('is not active by default', () => {
+ const editor = createCustomEditor('Test
', [Highlight])
+ expect(editor.isActive('highlight')).to.equal(false)
+ editor.destroy()
+ })
+
+ it('is active within tags', () => {
+ const editor = createCustomEditor('Test
', [Highlight])
+ expect(editor.isActive('highlight')).to.equal(true)
+ editor.destroy()
+ })
+})