diff --git a/src/extensions/RichText.js b/src/extensions/RichText.js
index d5807c3e737..0590c4b11b8 100644
--- a/src/extensions/RichText.js
+++ b/src/extensions/RichText.js
@@ -8,7 +8,6 @@ import { Extension } from '@tiptap/core'
/* eslint-disable import/no-named-as-default */
import Blockquote from '@tiptap/extension-blockquote'
import Document from '@tiptap/extension-document'
-import HorizontalRule from '@tiptap/extension-horizontal-rule'
import { ListItem } from '@tiptap/extension-list'
import Placeholder from '@tiptap/extension-placeholder'
import Text from '@tiptap/extension-text'
@@ -17,6 +16,7 @@ import { CharacterCount, Dropcursor, Gapcursor } from '@tiptap/extensions'
import { common, createLowlight } from 'lowlight'
import MentionSuggestion from '../components/Suggestion/Mention/suggestions.js'
import Heading from '../nodes/Heading.js'
+import HorizontalRule from '../nodes/HorizontalRule.ts'
import EmojiSuggestion from './../components/Suggestion/Emoji/suggestions.js'
import LinkBubble from './../extensions/LinkBubble.js'
import LinkPicker from './../extensions/LinkPicker.js'
diff --git a/src/markdownit/index.js b/src/markdownit/index.js
index d2ca8c7c809..2a4f8898d0a 100644
--- a/src/markdownit/index.js
+++ b/src/markdownit/index.js
@@ -48,6 +48,10 @@ const markdownit = MarkdownIt('commonmark', { html: false, breaks: false })
markdownit.renderer.rules.front_matter = (tokens, idx, options) =>
`
${escapeHtml(tokens[idx].meta)}
`
+// Render horizontal rules with markup attribute
+markdownit.renderer.rules.hr = (tokens, idx) =>
+ `
\n`
+
// Render lists with bullet attribute
markdownit.renderer.rules.bullet_list_open = (tokens, idx, options) => {
tokens[idx].attrs = [
diff --git a/src/nodes/HorizontalRule.ts b/src/nodes/HorizontalRule.ts
new file mode 100644
index 00000000000..1483d5c1fab
--- /dev/null
+++ b/src/nodes/HorizontalRule.ts
@@ -0,0 +1,39 @@
+/**
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { nodeInputRule } from '@tiptap/core'
+import TiptapHorizontalRule from '@tiptap/extension-horizontal-rule'
+
+const HorizontalRule = TiptapHorizontalRule.extend({
+ addAttributes() {
+ return {
+ markup: {
+ default: '---',
+ parseHTML: (el) => el.getAttribute('data-markup') || '---',
+ renderHTML: (attrs) => {
+ if (!attrs.markup || attrs.markup === '---') {
+ return {}
+ }
+ return { 'data-markup': attrs.markup }
+ },
+ },
+ }
+ },
+
+ // Same triggers as upsream, but capture typed marker so it can be preserved
+ addInputRules() {
+ return [
+ nodeInputRule({
+ find: /^(?:(---|—-)|(___|\*\*\*)\s)$/,
+ type: this.type,
+ getAttributes: (match) => ({
+ markup: match[2] ?? '---',
+ }),
+ }),
+ ]
+ },
+})
+
+export default HorizontalRule
diff --git a/src/tests/markdown.spec.js b/src/tests/markdown.spec.js
index c117ffbbf0b..357464f9932 100644
--- a/src/tests/markdown.spec.js
+++ b/src/tests/markdown.spec.js
@@ -135,6 +135,12 @@ describe('Markdown though editor', () => {
expect(markdownThroughEditor('foo\n\n---\n\nfoobar')).toBe(
'foo\n\n---\n\nfoobar',
)
+ expect(markdownThroughEditor('***\n\n- [ ] task\n\n***')).toBe(
+ '***\n\n- [ ] task\n\n***',
+ )
+ expect(markdownThroughEditor('___\n\n- [ ] task\n\n___')).toBe(
+ '___\n\n- [ ] task\n\n___',
+ )
})
test('table', () => {
diff --git a/src/tests/markdownit/commonmark.spec.js b/src/tests/markdownit/commonmark.spec.js
index c32d1278a89..c25bd7f6a96 100644
--- a/src/tests/markdownit/commonmark.spec.js
+++ b/src/tests/markdownit/commonmark.spec.js
@@ -24,6 +24,7 @@ describe('Commonmark', () => {
.replace(/<\/blockquote>/g, '\n
')
.replace(/([^<]+)<\/span>/g, '$1')
.replace(/
/g, '
\n')
+ .replace(/