fix(editor): keep rich mode for documents with details blocks

The markdown serializer always injected class="orca-details" into
saved <details> tags. The rich-mode round-trip check compares the
serialized output against the source's literal opening tag, so any
user-authored <details> without that class failed the comparison and
fell back to Source mode, making the details extension unreachable
from a file. The class is already applied to the rendered DOM node
independently, so the serializer no longer needs to write it into the
markdown source.
This commit is contained in:
Frederic Barthelemy
2026-09-18 00:58:47 -07:00
committed by Neil
parent be766569bd
commit 68cf9ea08a
4 changed files with 60 additions and 4 deletions
@@ -78,8 +78,11 @@ export function detailsBodyHtmlToMarkdown(body: string): string {
.trim()
}
// Why: the styling class is applied to the rendered DOM node independently
// (OrcaDetails' HTMLAttributes config), so writing it into the markdown
// source would rewrite a user's plain `<details>` on every save.
export function renderDetailsAttributes(attrs: Record<string, unknown> | undefined): string {
const attributes = ['class="orca-details"']
const attributes: string[] = []
const variant = parseToggleHeadingVariant(attrs?.variant)
if (variant) {
@@ -168,6 +168,58 @@ describe('getMarkdownRichModeUnsupportedMessage', () => {
expect(usedGlobalHtmlFragmentMatch).toBe(false)
})
it('allows a minimal bare details block with no pre-existing styling class', () => {
expect(
getMarkdownRichModeUnsupportedMessage('<details><summary>x</summary>\n\nbody\n\n</details>\n')
).toBeNull()
})
it('allows a minimal bare details block with the open attribute', () => {
expect(
getMarkdownRichModeUnsupportedMessage(
'<details open><summary>x</summary>\n\nbody\n\n</details>\n'
)
).toBeNull()
})
it('allows a document with front matter, prose, and two details blocks', () => {
const content = [
'---',
'title: Details Disclosure Test',
'---',
'',
'# Heading',
'',
'Some prose before the first toggle.',
'',
'<details>',
'<summary>First toggle</summary>',
'',
'**Bold text** and a [link](./target%20file.md).',
'',
'- one',
'- two',
'',
'```ts',
'const answer = 42',
'```',
'',
'</details>',
'',
'A paragraph between the two toggles.',
'',
'<details open>',
'<summary>Second toggle</summary>',
'',
'Short body.',
'',
'</details>',
''
].join('\n')
expect(getMarkdownRichModeUnsupportedMessage(content)).toBeNull()
})
it('keeps unsupported content blocked when it also exceeds the size limit', () => {
const content = `${'a'.repeat(RICH_MARKDOWN_MAX_SIZE_BYTES + 1)}<Widget />`
@@ -282,8 +282,9 @@ const OrcaDetails = Details.extend({
)
const body = helpers.renderChildren(content?.content ?? [], '\n\n').trim()
const attrs = renderDetailsAttributes(node.attrs)
const openingTag = attrs ? `<details ${attrs}>` : '<details>'
return `<details ${attrs}>\n<summary>${summaryText}</summary>\n\n${body}\n\n</details>`
return `${openingTag}\n<summary>${summaryText}</summary>\n\n${body}\n\n</details>`
}
})
@@ -90,12 +90,12 @@ describe('rich markdown details keyboard behavior', () => {
[
'text toggle',
'<details><summary>Toggle</summary><p></p></details>',
'<details class="orca-details">\n<summary>Toggle</summary>\n\n\n\n</details>'
'<details>\n<summary>Toggle</summary>\n\n\n\n</details>'
],
[
'heading toggle',
'<details data-orca-toggle="heading-1"><summary>Toggle</summary><p></p></details>',
'<details class="orca-details" data-orca-toggle="heading-1">\n<summary>Toggle</summary>\n\n\n\n</details>'
'<details data-orca-toggle="heading-1">\n<summary>Toggle</summary>\n\n\n\n</details>'
]
])('moves backspace from an empty %s body to the summary', (_name, content, expected) => {
const editor = createEditor(content)