package preview import ( "reflect" "strings" "testing" ) func TestSplitFrontmatter(t *testing.T) { md := "---\ncreated: 2026-07-08\nstatus: \"🟢 In progress\"\ntags:\n - daily\n - tui\ncategories: [\"[[Projects]]\", Notes]\n---\n\n# Body\n\ntext\n" props, body := splitFrontmatter(md) want := []prop{ {"created", "2026-07-08"}, {"status", "🟢 In progress"}, {"tags", "daily · tui"}, {"categories", "[[Projects]] · Notes"}, } if !reflect.DeepEqual(props, want) { t.Errorf("props = %#v, want %#v", props, want) } if body != "\n# Body\n\ntext\n" { t.Errorf("body = %q", body) } } func TestSplitFrontmatterNone(t *testing.T) { md := "# Body\n\ntext\n" props, body := splitFrontmatter(md) if len(props) != 0 || body != md { t.Errorf("no frontmatter: props=%v body=%q", props, body) } // A "---" that isn't on line one is a horizontal rule, not frontmatter. md2 := "intro\n\n---\nkey: value\n---\n" props, body = splitFrontmatter(md2) if len(props) != 0 || body != md2 { t.Errorf("mid-document rule treated as frontmatter: props=%v", props) } } func TestSplitFrontmatterUnterminated(t *testing.T) { md := "---\nkey: value\n\n# Body\n" props, body := splitFrontmatter(md) if len(props) != 0 || body != md { t.Errorf("unterminated block should be left alone: props=%v body=%q", props, body) } } func TestSplitFrontmatterEmptyValues(t *testing.T) { md := "---\naliases:\ntitle: Note\n---\nbody\n" props, _ := splitFrontmatter(md) want := []prop{{"aliases", ""}, {"title", "Note"}} if !reflect.DeepEqual(props, want) { t.Errorf("props = %#v, want %#v", props, want) } } // A colon inside a value (a URL, a time) must not split the key twice. func TestSplitFrontmatterColonInValue(t *testing.T) { md := "---\nsource: https://example.com/a\nat: 09:30\n---\nbody\n" props, _ := splitFrontmatter(md) want := []prop{{"source", "https://example.com/a"}, {"at", "09:30"}} if !reflect.DeepEqual(props, want) { t.Errorf("props = %#v, want %#v", props, want) } } func TestHeaderTitleFromFrontmatter(t *testing.T) { m := New("") m.SetColors(testColors) m.SetSize(60, 20) m.SetTitle("filename-title") if err := m.Render("---\ntitle: Frontmatter Title\ncreated: 2026-07-08\n---\n\nbody\n"); err != nil { t.Fatal(err) } out := strings.Join(stripANSI(m.vp.View()), "\n") if !strings.Contains(out, "Frontmatter Title") { t.Errorf("frontmatter title not used as doc name:\n%s", out) } if strings.Contains(out, "filename-title") { t.Errorf("filename should lose to frontmatter title:\n%s", out) } // The title key is consumed by the heading, not repeated as a property. if strings.Contains(out, "title ") { t.Errorf("title still listed as a property:\n%s", out) } } func TestHeaderPropsAreTightAndRuleFree(t *testing.T) { m := New("") m.SetColors(testColors) m.SetSize(60, 30) m.SetTitle("My Note") if err := m.Render("---\ncreated: 2026-07-08\nstatus: In progress\ntags:\n - daily\n---\n\nbody text\n"); err != nil { t.Fatal(err) } lines := stripANSI(m.vp.View()) find := func(want string) int { for i, ln := range lines { if strings.HasPrefix(strings.TrimSpace(ln), want) { return i } } t.Fatalf("line starting %q not found in:\n%s", want, strings.Join(lines, "\n")) return -1 } title := find("My Note") created := find("created") // Doc name sits above the properties, with one blank line between. if created != title+2 { t.Errorf("properties start at %d, want %d (one blank line under the title)", created, title+2) } // Properties are consecutive: no blank lines between them. if got := strings.TrimSpace(lines[created+1]); !strings.HasPrefix(got, "status") { t.Errorf("line after created = %q, want status", got) } if got := strings.TrimSpace(lines[created+2]); !strings.HasPrefix(got, "tags") { t.Errorf("line after status = %q, want tags", got) } // The --- fences never render as horizontal rules. for _, ln := range lines { if strings.HasPrefix(strings.TrimSpace(ln), "---") { t.Errorf("frontmatter fence rendered as a rule:\n%s", strings.Join(lines, "\n")) break } } // Values are aligned in a column, keys padded to the widest key. for _, ln := range lines[created : created+3] { if !strings.Contains(ln, " 2026-07-08") && !strings.Contains(ln, " In progress") && !strings.Contains(ln, " daily") { t.Errorf("property value not in an aligned column: %q", ln) } } } func TestHeaderTitleBarIsStyled(t *testing.T) { m := New("") m.SetColors(testColors) m.SetSize(60, 20) m.SetTitle("My Note") if err := m.Render("body\n"); err != nil { t.Fatal(err) } raw := m.vp.View() line := strings.Split(raw, "\n")[0] if !strings.Contains(line, "My Note") { t.Fatalf("title not on the first line: %q", line) } // The heading color paints the bar background, with legible text on top. if !strings.Contains(line, "48;2;209;77;65") { t.Errorf("title bar missing the heading background: %q", line) } if !strings.Contains(line, "38;2;255;252;240") { t.Errorf("title bar missing legible foreground: %q", line) } // The bar spans the full preview width, like a filled H1 block. if w := len(stripANSI(line)[0]); w != 60 { t.Errorf("title bar width = %d, want 60", w) } } func TestHeaderAbsentWithoutTitleOrProps(t *testing.T) { m := New("") m.SetColors(testColors) m.SetSize(60, 20) if err := m.Render("# Heading\n\nbody\n"); err != nil { t.Fatal(err) } if h := m.renderHeader("", nil); h != "" { t.Errorf("renderHeader with nothing to show = %q", h) } // The document opens on glamour's own H1, not a doc-name bar above it. lines := stripANSI(m.vp.View()) if got := strings.TrimSpace(lines[0]); got != "" { t.Errorf("untitled doc grew a header: %q", got) } if got := strings.TrimSpace(lines[1]); got != "Heading" { t.Errorf("body H1 should be the first thing rendered, got %q", got) } } func TestHeaderWrapsLongValues(t *testing.T) { m := New("") m.SetColors(testColors) m.SetSize(40, 20) m.SetTitle("N") if err := m.Render("---\ntags: alpha beta gamma delta epsilon zeta eta theta\n---\nbody\n"); err != nil { t.Fatal(err) } for _, ln := range stripANSI(m.vp.View()) { if w := len([]rune(ln)); w > 40 { t.Errorf("line exceeds width 40 (%d): %q", w, ln) } } }