feat(editor): add Toggle H5 to markdown editor (#9448)

This commit is contained in:
Jinjing
2026-07-19 12:13:16 -07:00
committed by GitHub
parent b19dccd9b7
commit b4e85d95a6
15 changed files with 107 additions and 17 deletions
+7 -1
View File
@@ -556,7 +556,7 @@
line-height: 1.3;
}
/* Heading toggle sizes mirror the plain h2–h4 scales (1.4 / 1.15 / 1em). */
/* Heading toggle sizes mirror the plain h2–h5 scales (1.4 / 1.15 / 1 / 0.92em). */
.markdown-body details.orca-details[data-orca-toggle='heading-2'] > summary {
font-size: 1.4em;
line-height: 1.3;
@@ -572,6 +572,12 @@
line-height: 1.3;
}
.markdown-body details.orca-details[data-orca-toggle='heading-5'] > summary {
font-size: 0.92em;
color: var(--muted-foreground);
line-height: 1.3;
}
.markdown-body details.orca-details > :not(summary) {
margin-left: 0.15em;
}
@@ -764,7 +764,7 @@
height: 2.405em;
}
/* Heading toggle sizes mirror the plain h2–h4 scales (1.4 / 1.15 / 1em). */
/* Heading toggle sizes mirror the plain h2–h5 scales (1.4 / 1.15 / 1 / 0.92em). */
.rich-markdown-editor details.orca-details[data-orca-toggle='heading-2'] > summary,
.rich-markdown-editor .orca-details[data-type='details'][data-orca-toggle='heading-2'] summary {
font-size: 1.4em;
@@ -787,6 +787,13 @@
line-height: 1.3;
}
.rich-markdown-editor details.orca-details[data-orca-toggle='heading-5'] > summary,
.rich-markdown-editor .orca-details[data-type='details'][data-orca-toggle='heading-5'] summary {
font-size: 0.92em;
color: var(--muted-foreground);
line-height: 1.3;
}
.rich-markdown-editor blockquote {
padding: 0.5em 1em;
border-left: 3px solid var(--border);
@@ -315,7 +315,7 @@ const markdownPreviewSanitizeSchema = {
...(defaultSchema.attributes?.details ?? []),
'open',
['className', 'orca-details'],
['dataOrcaToggle', 'heading-1', 'heading-2', 'heading-3', 'heading-4']
['dataOrcaToggle', 'heading-1', 'heading-2', 'heading-3', 'heading-4', 'heading-5']
],
h1: [...(defaultSchema.attributes?.h1 ?? []), 'id'],
h2: [...(defaultSchema.attributes?.h2 ?? []), 'id'],
@@ -2,6 +2,8 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
import {
extractDetailsSummaryHtml,
isEditableDetailsHtmlBlock,
parseDetailsAttributes,
parseToggleHeadingVariant,
type DetailsHtmlBlock
} from './details-markdown-html'
@@ -45,4 +47,31 @@ describe('details markdown html', () => {
)
expect(usedSummaryCapture).toBe(false)
})
it('accepts heading-5 toggle variants and rejects unsupported levels', () => {
expect(parseToggleHeadingVariant('heading-5')).toBe('heading-5')
expect(parseToggleHeadingVariant('heading-6')).toBeNull()
expect(parseDetailsAttributes(' data-orca-toggle="heading-5"')).toMatchObject({
variant: 'heading-5'
})
expect(parseDetailsAttributes(' data-orca-toggle="heading-6"')).toMatchObject({
variant: null
})
const editableHeading5: DetailsHtmlBlock = {
raw: '',
openingAttributes: ' data-orca-toggle="heading-5"',
inner: '<summary>Toggle</summary><p>Body</p>',
hasNestedDetails: false
}
const unsupportedHeading6: DetailsHtmlBlock = {
raw: '',
openingAttributes: ' data-orca-toggle="heading-6"',
inner: '<summary>Toggle</summary><p>Body</p>',
hasNestedDetails: false
}
expect(isEditableDetailsHtmlBlock(editableHeading5)).toBe(true)
expect(isEditableDetailsHtmlBlock(unsupportedHeading6)).toBe(false)
})
})
@@ -1,14 +1,20 @@
import type { MarkdownToken } from '@tiptap/core'
// Toggle summaries can render at heading scales 1–4, mirroring the plain
// heading levels the slash menu / toolbar dropdown offer (h1–h4).
export type ToggleHeadingVariant = 'heading-1' | 'heading-2' | 'heading-3' | 'heading-4'
// Toggle summaries can render at heading scales 1–5, mirroring the plain
// heading levels the slash menu / toolbar dropdown offer (h1–h5).
export type ToggleHeadingVariant =
| 'heading-1'
| 'heading-2'
| 'heading-3'
| 'heading-4'
| 'heading-5'
export const TOGGLE_HEADING_VARIANTS: readonly ToggleHeadingVariant[] = [
'heading-1',
'heading-2',
'heading-3',
'heading-4'
'heading-4',
'heading-5'
]
export function parseToggleHeadingVariant(value: unknown): ToggleHeadingVariant | null {
@@ -49,7 +55,7 @@ export function parseDetailsAttributes(rawAttributes: string): Record<string, un
// Why: validation accepts normal HTML whitespace around `=`, so parsing
// must accept it too or an editable toggle loses its heading variant.
const variantMatch = rawAttributes.match(
/\sdata-orca-toggle\s*=\s*(?:"(heading-[1-4])"|'(heading-[1-4])'|(heading-[1-4]))(?:\s|$)/i
/\sdata-orca-toggle\s*=\s*(?:"(heading-[1-5])"|'(heading-[1-5])'|(heading-[1-5]))(?:\s|$)/i
)
return {
open: /\sopen(?:\s|=|$)/i.test(rawAttributes),
@@ -180,7 +186,7 @@ function hasOnlySupportedDetailsAttributes(rawAttributes: string): boolean {
.replace(/\s+open(?:\s*=\s*(?:""|"open"|''|'open'|open))?(?=\s|$)/giu, '')
.replace(/\s+class\s*=\s*(?:"orca-details"|'orca-details'|orca-details)(?=\s|$)/giu, '')
.replace(
/\s+data-orca-toggle\s*=\s*(?:"heading-[1-4]"|'heading-[1-4]'|heading-[1-4])(?=\s|$)/giu,
/\s+data-orca-toggle\s*=\s*(?:"heading-[1-5]"|'heading-[1-5]'|heading-[1-5])(?=\s|$)/giu,
''
)
.trim() === ''
@@ -132,7 +132,7 @@ describe('rich markdown round trip', () => {
)
})
it.each(['heading-2', 'heading-3', 'heading-4'])(
it.each(['heading-2', 'heading-3', 'heading-4', 'heading-5'])(
'preserves %s-styled details blocks',
(variant) => {
expect(
@@ -166,6 +166,12 @@ describe('rich markdown round trip', () => {
expect(roundTripMarkdown(input)).toBe(input.trimEnd())
})
it('preserves details blocks with unsupported toggle variants as passthrough html', () => {
const input =
'<details data-orca-toggle="heading-6"><summary>Toggle</summary><p>Body</p></details>\n'
expect(roundTripMarkdown(input)).toBe(input.trimEnd())
})
it('preserves details blocks with closing tags inside fenced code as passthrough html', () => {
const input = [
'<details><summary>Toggle</summary>',
@@ -208,7 +214,8 @@ describe('rich markdown round trip', () => {
it.each([
['toggle-h2', 'heading-2'],
['toggle-h3', 'heading-3'],
['toggle-h4', 'heading-4']
['toggle-h4', 'heading-4'],
['toggle-h5', 'heading-5']
] as const)('inserts editable %s toggles from slash commands', (commandId, variant) => {
expect(slashCommandMarkdown(commandId)).toBe(
`<details class="orca-details" data-orca-toggle="${variant}" open>\n<summary></summary>\n\n\n\n</details>`
@@ -35,6 +35,7 @@ describe('rich markdown slash commands', () => {
expect(getCommand('inline-math').aliases).toContain('latex')
expect(getCommand('math-block').aliases).toContain('equation block')
expect(getCommand('emoji').aliases).toContain('reaction')
expect(getCommand('toggle-h5').aliases).toContain('toggle-h5')
})
it('orders commands under section headers', () => {
@@ -48,6 +49,7 @@ describe('rich markdown slash commands', () => {
'Toggle headings:toggle-h2',
'Toggle headings:toggle-h3',
'Toggle headings:toggle-h4',
'Toggle headings:toggle-h5',
'Basic blocks:blockquote',
'Basic blocks:ordered-list',
'Basic blocks:bullet-list',
@@ -22,7 +22,8 @@ const TOGGLE_HEADING_PLACEHOLDERS: Record<ToggleHeadingVariant, readonly [string
'heading-1': ['auto.components.editor.rich.markdown.slash.commands.e66e7f04c6', 'Heading 1'],
'heading-2': ['auto.components.editor.rich.markdown.slash.commands.c209a116b7', 'Heading 2'],
'heading-3': ['auto.components.editor.rich.markdown.slash.commands.30566ee962', 'Heading 3'],
'heading-4': ['auto.components.editor.rich.markdown.slash.commands.5f9a0ed7c4', 'Heading 4']
'heading-4': ['auto.components.editor.rich.markdown.slash.commands.5f9a0ed7c4', 'Heading 4'],
'heading-5': ['auto.components.editor.rich.markdown.slash.commands.8440fa4acf', 'Heading 5']
}
function toggleHeadingPlaceholder(variant: ToggleHeadingVariant): string {
@@ -198,5 +198,26 @@ export const headingSlashCommands: SlashCommand[] = [
run: (editor) => {
insertToggle(editor, 'heading-4')
}
},
{
id: 'toggle-h5',
get label() {
return translate(
'auto.components.editor.rich.markdown.slash.commands.21d8c463e5',
'Toggle H5'
)
},
aliases: ['toggle-h5'],
icon: icon(ChevronRight),
group: 'Toggle headings',
get description() {
return translate(
'auto.components.editor.rich.markdown.slash.commands.dc239b41ad',
'Create a collapsible section with a deep heading summary.'
)
},
run: (editor) => {
insertToggle(editor, 'heading-5')
}
}
]
@@ -23,6 +23,7 @@ export type SlashCommandId =
| 'heading-4'
| 'toggle-h4'
| 'heading-5'
| 'toggle-h5'
| 'task-list'
| 'bullet-list'
| 'ordered-list'
+3 -1
View File
@@ -12030,7 +12030,9 @@
"2f9d6b4e10": "Toggle H3",
"8c1a3e7d52": "Create a collapsible section with a small heading summary.",
"5e0b9c2a71": "Toggle H4",
"d4f16a8b39": "Create a collapsible section with a nested heading summary."
"d4f16a8b39": "Create a collapsible section with a nested heading summary.",
"21d8c463e5": "Toggle H5",
"dc239b41ad": "Create a collapsible section with a deep heading summary."
}
}
}
+3 -1
View File
@@ -12007,7 +12007,9 @@
"2f9d6b4e10": "Alternar título 3",
"8c1a3e7d52": "Cree una sección plegable con un resumen de encabezado pequeño.",
"5e0b9c2a71": "Alternar título 4",
"d4f16a8b39": "Cree una sección plegable con un resumen de encabezado anidado."
"d4f16a8b39": "Cree una sección plegable con un resumen de encabezado anidado.",
"21d8c463e5": "Alternar título 5",
"dc239b41ad": "Cree una sección plegable con un resumen de encabezado profundo."
}
}
}
+3 -1
View File
@@ -12007,7 +12007,9 @@
"2f9d6b4e10": "見出し 3 を切り替えます",
"8c1a3e7d52": "小さな見出しの概要を含む折りたたみ可能なセクションを作成します。",
"5e0b9c2a71": "見出し 4 を切り替えます",
"d4f16a8b39": "入れ子の見出しの概要を含む折りたたみ可能なセクションを作成します。"
"d4f16a8b39": "入れ子の見出しの概要を含む折りたたみ可能なセクションを作成します。",
"21d8c463e5": "見出し 5 を切り替えます",
"dc239b41ad": "深い見出しの概要を含む折りたたみ可能なセクションを作成します。"
}
}
}
+3 -1
View File
@@ -12007,7 +12007,9 @@
"2f9d6b4e10": "토글 제목 3",
"8c1a3e7d52": "작은 제목 요약이 포함된 접을 수 있는 섹션을 만듭니다.",
"5e0b9c2a71": "토글 제목 4",
"d4f16a8b39": "중첩된 제목 요약이 포함된 접을 수 있는 섹션을 만듭니다."
"d4f16a8b39": "중첩된 제목 요약이 포함된 접을 수 있는 섹션을 만듭니다.",
"21d8c463e5": "토글 제목 5",
"dc239b41ad": "깊은 제목 요약이 포함된 접을 수 있는 섹션을 만듭니다."
}
}
}
+3 -1
View File
@@ -12007,7 +12007,9 @@
"2f9d6b4e10": "切换标题 3",
"8c1a3e7d52": "创建一个带有小标题摘要的可折叠部分。",
"5e0b9c2a71": "切换标题 4",
"d4f16a8b39": "创建一个带有嵌套标题摘要的可折叠部分。"
"d4f16a8b39": "创建一个带有嵌套标题摘要的可折叠部分。",
"21d8c463e5": "切换标题 5",
"dc239b41ad": "创建一个带有深层标题摘要的可折叠部分。"
}
}
}