Skip to content

feat: inline markdown formatting (bold, italic, strikethrough, underline) #172

@oobagi

Description

@oobagi

Problem

The editor renders block-level markdown beautifully — headings, bullets, checklists, code blocks with syntax highlighting, quotes with bars. But inline formatting is completely absent: **bold**, *italic*, ~~strikethrough~~, and __underline__ all display as literal delimiter characters. For a markdown editor targeting developers, this is the most visible gap.

Context

The rendering pipeline processes blocks individually through three paths in internal/editor/render.go:

  • renderInactiveBlock() (line 549) — blocks not being edited; renders static styled text via lipgloss
  • renderViewBlock() (line 718) — view mode rendering; same static pattern
  • renderActiveBlock() (line 88) — the block currently being edited; uses forked textarea on raw runes

All three follow the same pattern: get content → wrap text → apply block-level style → render. Inline formatting is a text transformation step inserted between wrap and render.

Existing pattern to extend: Checked checklist items already apply lipgloss.NewStyle().Faint(true) to entire wrapped text (render.go:616-619). This feature extends that from whole-block to span-level styling.

Wrap ordering: wrapText() calls textarea.Wrap() on raw runes. Wrap first → then parse and style inline formatting on wrapped text. Delimiters consume visual width during wrapping but are hidden after — acceptable, consistent with how glow and other terminal markdown renderers work.

Scope boundary: The active textarea (block being edited) shows raw markdown. Formatting appears when you click away. This matches Notion, Typora source mode, etc.

Approach: State-machine inline parser

Write a renderInlineMarkdown(text string) string function that walks text character-by-character, tracking delimiter open/close state, and emits lipgloss-styled spans.

  • Handles nesting correctly (***bold italic***, **bold with *italic* inside**)
  • No external dependencies — pure string processing + lipgloss
  • Ignores _ in snake_case (only match __ at word boundaries or standalone)
  • Extracted to internal/format/ for reuse and easy unit testing
  • Skips code blocks entirely (already syntax-highlighted)

Tasks

  • Add RenderInlineMarkdown(text string) string in internal/format/inline.go
  • Support **bold**, *italic*, ~~strikethrough~~, __underline__, and nesting
  • Handle edge cases: empty delimiters (****), snake_case not treated as underline, unmatched delimiters rendered literally
  • Insert call in renderInactiveBlock() after wrapText(), before block-type styling — skip for code blocks
  • Insert call in renderViewBlock() at the same pipeline position — skip for code blocks
  • Active blocks unchanged (show raw markdown)
  • Add unit tests for the inline parser

Test plan

  • Inactive paragraph with **bold** renders bold, no asterisks visible
  • View mode renders all four formatting types correctly
  • ***bold italic*** renders with both styles
  • **bold with *nested italic* inside** renders correctly
  • Active block shows raw delimiters (no formatting)
  • Code blocks show literal **text** without formatting
  • snake_case_variables are NOT treated as underline
  • Unmatched **text renders literally (no crash, no style leak)
  • All existing tests pass

Scope

Type: enhancement
Size: medium

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestphase-5Rich Experience

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions