/** * Inline emphasis: bold (`**`/`__`), italic (`*`/`_`), strikethrough (`~~`). * * Applies a content class to the span and hides the surrounding markers unless * the cursor is near (same line, within PROXIMITY_CHARS). Both `Emphasis` and * `StrongEmphasis` use `EmphasisMark` for their markers — there is no * `StrongEmphasisMark` in @lezer/markdown; the parent node name disambiguates. * Hidden marker ranges are registered as atomic so cursor motion skips them. */ import { type EditorState } from '@codemirror/state'; import { Decoration } from '@codemirror/view'; import { syntaxTree } from '@codemirror/language'; import { type BuildResult, type Range, DecoCollector, fullRanges, isCursorNearSameLine, decorationExtension, } from './context'; import { inlineFormattingHiddenMark } from './hiddenClasses'; const boldMark = Decoration.mark({ class: 'cm-strong' }); const italicMark = Decoration.mark({ class: 'cm-em' }); const strikethroughMark = Decoration.mark({ class: 'cm-strikethrough' }); /** Lezer container node name → content decoration. */ const CONTENT_MARKS: Record = { StrongEmphasis: boldMark, Emphasis: italicMark, Strikethrough: strikethroughMark, }; const CONTAINER_NAMES = new Set(Object.keys(CONTENT_MARKS)); export function buildInlineFormatDecorations( state: EditorState, ranges: readonly Range[] = fullRanges(state), ): BuildResult { const out = new DecoCollector(); for (const { from, to } of ranges) { syntaxTree(state).iterate({ from, to, enter(node) { const contentMark = CONTENT_MARKS[node.name]; if (contentMark) { out.push(node.from, node.to, contentMark); return; // keep descending to reach the markers } if (node.name === 'EmphasisMark' || node.name === 'StrikethroughMark') { // Widen the proximity window to the whole formatted span when the // parent is a known container; otherwise fall back to the marker. const parent = node.node.parent; const scoped = parent && CONTAINER_NAMES.has(parent.name); const checkFrom = scoped ? parent.from : node.from; const checkTo = scoped ? parent.to : node.to; if (!isCursorNearSameLine(state, checkFrom, checkTo)) { out.push(node.from, node.to, inlineFormattingHiddenMark, true); } } }, }); } return out.result(); } export const inlineFormatDecoration = decorationExtension( buildInlineFormatDecorations, );