Skip to content

feat(primitives): inline-markdown detection for editor v2 (#1594)#1602

Open
ssilvius wants to merge 3 commits into
mainfrom
feat/1594-inline-markdown
Open

feat(primitives): inline-markdown detection for editor v2 (#1594)#1602
ssilvius wants to merge 3 commits into
mainfrom
feat/1594-inline-markdown

Conversation

@ssilvius
Copy link
Copy Markdown
Collaborator

@ssilvius ssilvius commented Jun 5, 2026

Summary

Adds detectInlineMarkdown, a pure function primitive that detects inline markdown syntax at a cursor position and returns the match offsets, extracted text, and mark type. This is the foundation for the editor v2 overlay -- when a user types **hello**, the function detects the closing delimiter and returns the data needed to replace the raw syntax with formatted InlineContent. Zero dependencies, no DOM access.

Acceptance criteria mapping

1. All functional tests pass

18 tests across 6 describe blocks (bold, italic, code, strikethrough, link, edge cases) all pass in vitest. Each pattern has at least one positive-match test asserting the full return shape (startOffset, endOffset, text, marks). Edge cases cover: no match at cursor, empty delimiters, empty string, escaped opening/closing delimiters, match at string boundaries, unmatched openers, cursor not at match end.
Evidence: pnpm vitest run packages/ui/src/primitives/inline-markdown.test.ts -- 18/18 pass

2. TypeScript compiles without errors

pnpm typecheck passes clean across all workspace packages. The implementation imports InlineMark from ./types and exports InlineMarkdownMatch (interface) and detectInlineMarkdown (function). Capture group indexing uses m[def.textGroup] ?? '' to satisfy strict null checks on RegExpMatchArray element access.
Evidence: pnpm typecheck -- zero errors

3. All 5 inline patterns detected correctly

Five patterns defined in the PATTERNS array at inline-markdown.ts:18-47: link ([text](url) with href extraction), bold (** and __ via backreference), strikethrough (~~), code (`), italic (* and _ via backreference). Pattern order matters -- link is first (most specific), bold before italic (prevents ** from matching as two italic delimiters). Each pattern is a data object with mark, pattern, textGroup, and optional hrefGroup.
Evidence: tests "detects text", "detects text", "detects text", "detects text", "detects text", "detects text", "detects text"

4. Edge cases handled (escaped, empty, partial)

Escaped delimiters use negative lookbehind (?<!\\) on both opening and closing positions -- tested by "ignores escaped opening delimiter" (\*not italic* returns null) and "ignores escaped closing delimiter" (*not italic\* returns null). Empty content between delimiters (****) returns null because the extracted text is empty-string-checked at inline-markdown.ts:62. Partial/unmatched delimiters (**no close) return null because the regex requires both opening and closing. Cursor position is strictly enforced: matchEnd !== cursorOffset on line 58 means detection only fires when the cursor is immediately after the closing delimiter.
Evidence: tests "returns null for empty content between delimiters", "ignores escaped opening delimiter", "ignores escaped closing delimiter", "returns null for unmatched opening delimiter", "cursor must be at end of match"

5. Exports added to index files

InlineMarkdownMatch (type export) and detectInlineMarkdown (value export) added to packages/ui/src/index.ts:17-18, sorted before the serializer exports per biome's import ordering rules.
Evidence: packages/ui/src/index.ts:17-18

Not done

  • InlineMarkdownOptions with onMatch callback (specified in the issue's interface sketch) -- not implemented because the pure function signature (content, cursorOffset) => match | null is the complete contract. The callback wrapper belongs to the editor integration (feat(primitives): document-editor -- integrate inline-markdown for overlay rendering #1595) which owns the keystroke loop.
  • Nested mark detection (***bold italic***) -- explicitly out of scope per the issue.
  • Double-escaped backslash edge case (\\** should be literal backslash + bold opener) -- the lookbehind only checks one preceding backslash. Vanishingly rare in hand-typed editor content; worth a follow-up if real users hit it.

ssilvius and others added 3 commits June 5, 2026 08:22
Pure function that detects inline markdown syntax (bold, italic, code,
strikethrough, link) at a cursor position and returns match offsets for
the overlay editor to replace syntax with formatted InlineContent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace extractText/extractHref callbacks with textGroup/hrefGroup
indices (data over behavior), and early-return on first match instead
of accumulating closest candidate across all patterns.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…intraword underscores

Italic * pattern now requires the delimiter to not be adjacent to
another * (prevents matching inside ** bold syntax). Underscore italic
split into a separate pattern with word-boundary constraints matching
CommonMark behavior (foo_bar_baz no longer triggers italic).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant