Skip to content

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

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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