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])) } // The hashes are muted; the heading text is heading-colored and bold. if out[0][0].Style.GetForeground() != th.Muted { t.Errorf("heading marker not muted, got %v", out[0][0].Style.GetForeground()) } last := out[0][len(out[0])-1] if last.Style.GetForeground() != th.Heading || !last.Style.GetBold() { t.Errorf("heading text not bold heading color") } } func TestScanEmphasisUsesEmphasisToneDimMarkers(t *testing.T) { th := theme.FlexokiDark() out := ScanLines([]string{"a **bold** b"}, th) if spanText(out[0]) != "a **bold** b" { t.Fatalf("text altered: %q", spanText(out[0])) } var sawMutedMarker, sawBoldContent bool for _, sp := range out[0] { if sp.Text == "**" && sp.Style.GetForeground() == th.Muted { sawMutedMarker = true } if sp.Text == "bold" && sp.Style.GetForeground() == th.Emphasis && sp.Style.GetBold() { sawBoldContent = true } } if !sawMutedMarker { t.Error("** markers not muted") } if !sawBoldContent { t.Error("bold content not Emphasis+bold") } } func TestScanLinkTextColoredUrlAndBracketsMuted(t *testing.T) { th := theme.FlexokiDark() out := ScanLines([]string{"see [text](http://x)"}, th) if spanText(out[0]) != "see [text](http://x)" { t.Fatalf("text altered: %q", spanText(out[0])) } var linkText, dimmed bool for _, sp := range out[0] { if sp.Text == "text" && sp.Style.GetForeground() == th.Link { linkText = true } if (sp.Text == "http://x" || sp.Text == "[" || sp.Text == "](" || sp.Text == ")") && sp.Style.GetForeground() == th.Muted { dimmed = true } } if !linkText { t.Error("link text not Link-colored") } if !dimmed { t.Error("url/brackets not muted") } } func TestScanReferenceLink(t *testing.T) { th := theme.FlexokiDark() out := ScanLines([]string{"a [Glamour][glamour] b"}, th) if spanText(out[0]) != "a [Glamour][glamour] b" { t.Fatalf("text altered: %q", spanText(out[0])) } found := false for _, sp := range out[0] { if sp.Text == "Glamour" && sp.Style.GetForeground() == th.Link { found = true } } if !found { t.Error("reference-link text not Link-colored") } } func TestScanReferenceDefinition(t *testing.T) { th := theme.FlexokiDark() out := ScanLines([]string{"[glamour]: https://github.com/x"}, th) if spanText(out[0]) != "[glamour]: https://github.com/x" { t.Fatalf("text altered: %q", spanText(out[0])) } last := out[0][len(out[0])-1] if last.Text != "https://github.com/x" || last.Style.GetForeground() != th.Link { t.Errorf("ref-def url not Link-colored: %+v", last) } if out[0][0].Style.GetForeground() != th.Muted { t.Error("ref-def [ref]: not muted") } } func TestScanHighlightGetsBackground(t *testing.T) { th := theme.FlexokiDark() out := ScanLines([]string{"a ==mark== b"}, th) if spanText(out[0]) != "a ==mark== b" { t.Fatalf("text altered: %q", spanText(out[0])) } found := false for _, sp := range out[0] { if sp.Text == "mark" && sp.Style.GetBackground() == th.Highlight { found = true } } if !found { t.Error("highlight content has no Highlight background") } } func TestScanCommentVisible(t *testing.T) { th := theme.FlexokiDark() out := ScanLines([]string{""}, th) if out[0][0].Style.GetForeground() != th.Comment { t.Errorf("comment not Comment-colored, got %v", out[0][0].Style.GetForeground()) } } func TestScanBlockquoteItalicTextDimMarker(t *testing.T) { th := theme.FlexokiDark() out := ScanLines([]string{"> quoted"}, th) if spanText(out[0]) != "> quoted" { t.Fatalf("text altered: %q", spanText(out[0])) } if out[0][0].Style.GetForeground() != th.Muted { t.Error("blockquote marker not muted") } last := out[0][len(out[0])-1] if last.Style.GetForeground() != th.Text || !last.Style.GetItalic() { t.Error("blockquote text not italic base color") } } 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") } }