package editor import ( "testing" "glint/internal/theme" "github.com/charmbracelet/lipgloss" ) // spanText concatenates the raw text of a line's spans. It must equal the // original raw line exactly (markup-visible invariant). func spanText(spans []Span) string { s := "" for _, sp := range spans { s += sp.Text } return s } func TestRenderSpansPreservesText(t *testing.T) { spans := []Span{ {Text: "ab", Style: lipgloss.NewStyle()}, {Text: "cd", Style: lipgloss.NewStyle()}, } // rendering may add ANSI, but stripped content is checked elsewhere; // here we only assert it does not panic and is non-empty. if renderSpans(spans) == "" { t.Error("renderSpans returned empty for non-empty spans") } } func TestScanPlainTextGetsExplicitForeground(t *testing.T) { th := theme.FlexokiDark() out := ScanLines([]string{"hello world"}, th) if len(out) != 1 || len(out[0]) == 0 { t.Fatalf("expected spans for one line, got %v", out) } for _, sp := range out[0] { // A style with no foreground returns "" from GetForeground(). if sp.Style.GetForeground() == lipgloss.Color("") { t.Errorf("plain span %q has no explicit foreground", sp.Text) } } } func TestScanPreservesRawTextAcrossConstructs(t *testing.T) { th := theme.FlexokiDark() lines := []string{ "# Heading", "plain **bold** and *italic* and `code`", "- a list item with a [link](http://x) and [[wikilink]]", "> a quote", "```", "raw **not bold** here", "```", "---", } out := ScanLines(lines, th) for i, raw := range lines { if got := spanText(out[i]); got != raw { t.Errorf("line %d: span text %q != raw %q", i, got, raw) } } } func TestScanFencedCodeSuppressesInline(t *testing.T) { th := theme.FlexokiDark() lines := []string{"```", "**x**", "```"} out := ScanLines(lines, th) // The fence body line should be a single code-colored span, not split // into bold spans. if len(out[1]) != 1 { t.Errorf("fenced line split into %d spans, want 1", len(out[1])) } if out[1][0].Style.GetForeground() != th.Code { t.Errorf("fenced body not code-colored") } } func TestScanLeadingFrontmatter(t *testing.T) { th := theme.FlexokiDark() lines := []string{"---", "title: x", "---", "body"} out := ScanLines(lines, th) if out[0][0].Style.GetForeground() != th.Muted { t.Errorf("opening --- not muted") } if out[1][0].Style.GetForeground() != th.Accent { t.Errorf("frontmatter key not accent-colored") } if out[3][0].Style.GetForeground() != th.Text { t.Errorf("post-frontmatter line should be plain text") } } func TestScanEmptyLineYieldsNoSpans(t *testing.T) { out := ScanLines([]string{""}, theme.FlexokiDark()) if len(out[0]) != 0 { t.Errorf("empty line should yield no spans, got %d", len(out[0])) } } func TestScanHeadingKeepsHashes(t *testing.T) { th := theme.FlexokiDark() out := ScanLines([]string{"### Title"}, th) if spanText(out[0]) != "### Title" { t.Errorf("heading hashes dropped: %q", spanText(out[0])) } if out[0][0].Style.GetForeground() != th.Heading { t.Errorf("heading not heading-colored") } } func TestScanListMarkerThenInline(t *testing.T) { th := theme.FlexokiDark() out := ScanLines([]string{"- **x**"}, th) if out[0][0].Style.GetForeground() != th.ListMarker { t.Errorf("first span should be the list marker, got fg %v", out[0][0].Style.GetForeground()) } if spanText(out[0]) != "- **x**" { t.Errorf("list line text altered: %q", spanText(out[0])) } } func TestScanFrontmatterHighlightsKeysAndValues(t *testing.T) { th := theme.FlexokiDark() lines := []string{"---", "title: Hello", "tags:", " - one", " - two", "# a comment", "---", "body"} out := ScanLines(lines, th) // Char-for-char preservation across every frontmatter line. for i, raw := range lines { if got := spanText(out[i]); got != raw { t.Errorf("line %d: span text %q != raw %q", i, got, raw) } } // Opening and closing --- are muted. if out[0][0].Style.GetForeground() != th.Muted { t.Errorf("opening --- not muted") } if out[6][0].Style.GetForeground() != th.Muted { t.Errorf("closing --- not muted") } // "title:" key portion is Accent; the value is Text. if out[1][0].Style.GetForeground() != th.Accent { t.Errorf("key not Accent-colored, got %v", out[1][0].Style.GetForeground()) } if last := out[1][len(out[1])-1]; last.Style.GetForeground() != th.Text { t.Errorf("value not Text-colored, got %v", last.Style.GetForeground()) } // List item marker "-" is ListMarker-colored. foundMarker := false for _, sp := range out[3] { if sp.Style.GetForeground() == th.ListMarker { foundMarker = true } } if !foundMarker { t.Errorf("list item dash not ListMarker-colored") } // Comment line is muted. if out[5][0].Style.GetForeground() != th.Muted { t.Errorf("comment not muted") } // After the closing ---, normal text resumes. if out[7][0].Style.GetForeground() != th.Text { t.Errorf("post-frontmatter line should be plain text") } }