package editor import ( "regexp" "strings" "glint/internal/theme" "github.com/charmbracelet/lipgloss" ) var ( headingRe = regexp.MustCompile(`^\s*#{1,6}\s`) listRe = regexp.MustCompile(`^\s*([-*+]|\d+\.)\s`) blockquoteRe = regexp.MustCompile(`^\s*>\s?`) refDefRe = regexp.MustCompile(`^\s*\[[^\]]+\]:\s*`) // [ref]: url ) // blockState carries cross-line context (fenced code, leading frontmatter). type blockState struct { inFence bool inFrontmatter bool frontmatterDone bool } // ScanLines styles every line, threading block state across lines. The returned // slice is parallel to the input: out[i] are the spans for lines[i], whose // concatenated text equals lines[i] exactly (the markup-visible invariant). // // Highlighting model: prose stays at Text; all markup punctuation is Muted; // color is reserved for headings, code, links, list markers, comments, and a // highlight background; bold/italic render as real styling in the Emphasis tone. func ScanLines(lines []string, th theme.Theme) [][]Span { st := &blockState{} out := make([][]Span, len(lines)) for i, ln := range lines { out[i] = scanLine(i, ln, st, th) } return out } func scanLine(row int, line string, st *blockState, th theme.Theme) []Span { muted := lipgloss.NewStyle().Foreground(th.Muted) code := lipgloss.NewStyle().Foreground(th.Code) trimmed := strings.TrimSpace(line) // Fenced code block: delimiters muted, body entirely code-colored. if st.inFence { if strings.HasPrefix(trimmed, "```") { st.inFence = false return wholeLine(line, muted) } return wholeLine(line, code) } if strings.HasPrefix(trimmed, "```") { st.inFence = true return wholeLine(line, muted) } // Leading YAML frontmatter: only valid starting at row 0. if st.inFrontmatter { if trimmed == "---" { st.inFrontmatter = false st.frontmatterDone = true return wholeLine(line, muted) } return scanFrontmatter(line, th) } if row == 0 && trimmed == "---" { st.inFrontmatter = true return wholeLine(line, muted) } // Comment line: visible, not dimmed. if strings.HasPrefix(trimmed, "