diff --git a/services/web/frontend/js/features/source-editor/languages/markdown/document-outline.ts b/services/web/frontend/js/features/source-editor/languages/markdown/document-outline.ts index 7f7a5ea4d8..8c32f667b7 100644 --- a/services/web/frontend/js/features/source-editor/languages/markdown/document-outline.ts +++ b/services/web/frontend/js/features/source-editor/languages/markdown/document-outline.ts @@ -33,21 +33,23 @@ const enterMarkdownNode = ( return } - // Collect heading text, skipping the HeaderMark (the leading # characters) - let title = '' - const cursor = node.node.cursor() - if (cursor.firstChild()) { - do { - if (cursor.name !== 'HeaderMark') { - title += state.sliceDoc(cursor.from, cursor.to) - } - } while (cursor.nextSibling()) - } + // In the Lezer Markdown grammar the heading text is NOT a child node — + // the only children are HeaderMark nodes ('#'s for ATX, the '='/'-' + // underline for Setext). The title text sits in the gaps between marks. + // So we slice the whole heading and strip the marker characters: + // - ATX: leading '#'s and any optional trailing '#'s + // - Setext: the trailing underline line ('===' / '---') + const raw = state.sliceDoc(node.from, node.to) + const title = raw + .replace(/^\s*#+\s*/, '') // ATX: leading ### markers + .replace(/\s*#+\s*$/, '') // ATX: optional closing ### markers + .replace(/\n[=-]+\s*$/, '') // Setext: underline line + .trim() items.push({ line: state.doc.lineAt(node.from).number, toLine: state.doc.lineAt(node.to).number, - title: title.trim(), + title, from: node.from, to: node.to, level,