package doc import ( "strings" "testing" ) func kinds(bs []Block) string { var out []string for _, b := range bs { out = append(out, b.Kind) } return strings.Join(out, ",") } func TestSplitBlocks(t *testing.T) { cases := []struct { name, in, want string }{ {"paragraphs", "one\ntwo\n\nthree", "paragraph,paragraph"}, {"atx interrupts", "text\n# Head\nmore", "paragraph,heading,paragraph"}, {"setext", "Title\n=====\n\nbody", "heading,paragraph"}, {"thematic vs setext", "para\n\n---\n\nnext", "paragraph,break,paragraph"}, {"fence with blank lines", "```go\na\n\nb\n```\nafter", "code,paragraph"}, {"unclosed fence", "```\nnever closed\n\nstill code", "code"}, {"list items", "- a\n- b\n continued\n- c", "list_item,list_item,list_item"}, {"nested list stays", "- a\n - nested\n - nested2\n- b", "list_item,list_item"}, {"loose item paragraphs", "1. first\n\n second para\n\n2. next", "list_item,list_item"}, {"list ends at outdent", "- a\n\nparagraph", "list_item,paragraph"}, {"quote", "> a\n> b\nlazy\n\npara", "quote,paragraph"}, {"table", "| a | b |\n|---|:-:|\n| 1 | 2 |\n\nafter", "table,paragraph"}, {"html", "
\nx\n
\n\np", "html,paragraph"}, {"indented code", "para\n\n code\n\n more\n\nend", "paragraph,code,paragraph"}, {"ordered 2 does not interrupt", "In\n2026. was long", "paragraph"}, {"star break not list", "* * *", "break"}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { if got := kinds(SplitBlocks(c.in)); got != c.want { t.Fatalf("got %s, want %s", got, c.want) } }) } } func TestBlockSourcesCoverFences(t *testing.T) { bs := SplitBlocks("```\ncode\n```") if bs[0].Source != "```\ncode\n```" || bs[0].Text != "code" { t.Fatalf("got %+v", bs[0]) } } func TestPlainText(t *testing.T) { cases := map[string]string{ "A [link](https://x.y) and **bold** _em_ `code`.": "A link and bold em code.", "# Heading *one*": "Heading one", "- item with ": "item with https://auto.link", "| a | **b** |\n|---|---|\n| 1 | 2 |": "a | b\n1 | 2", "line one\nline two": "line one line two", "![alt text](img.png) after": "alt text after", "> quoted *text*": "quoted text", "Ação & reação": "Ação & reação", "hard \nbreak": "hard break", `escaped \*stars\* and é`: "escaped *stars* and é", "`a & b` code": "a & b code", } for in, want := range cases { bs := SplitBlocks(in) if len(bs) != 1 { t.Fatalf("%q split into %d blocks", in, len(bs)) } if bs[0].Text != want { t.Errorf("plainText(%q) = %q, want %q", in, bs[0].Text, want) } } } func TestTokenizeRoundTrip(t *testing.T) { for _, s := range []string{"Don't stop, d'água! Ação\tok", "", "🌙 moon", "a-b_c 12.5%"} { toks := Tokenize(s) if strings.Join(toks, "") != s { t.Fatalf("round trip of %q: %q", s, toks) } } got := Tokenize("don't d'água x'") want := []string{"don't", " ", "d'água", " ", "x", "'"} if strings.Join(got, "|") != strings.Join(want, "|") { t.Fatalf("got %q", got) } } func TestFrontmatterAndHashes(t *testing.T) { opt := DefaultOptions() a := Parse([]byte("---\ntitle: Moon\nrevision: First note\nmaturity: seed\nflowerColor: '#fff'\n---\nBody text.\n"), opt) if a.FrontmatterErr != nil || a.Note != "First note" || a.Maturity != "seed" || a.Title != "Moon" { t.Fatalf("parsed %+v", a) } // Only the note and an ignored key change: content is the same. b := Parse([]byte("---\ntitle: Moon\nrevision: Second note\nmaturity: seed\nflowerColor: '#000'\n---\nBody text.\n"), opt) if a.ContentHash != b.ContentHash || a.RawHash == b.RawHash { t.Fatal("note-only edit should keep ContentHash and change RawHash") } // CRLF, BOM and trailing spaces are not edits. c := Parse([]byte("\xef\xbb\xbf---\r\ntitle: Moon\r\nrevision: First note\r\nmaturity: seed\r\nflowerColor: '#fff'\r\n---\r\nBody text. \r\n\r\n"), opt) if c.RawHash != a.RawHash { t.Fatal("CRLF/BOM/trailing space should not change RawHash") } // NFD and NFC spellings are the same text. nfc := Parse([]byte("Ação"), opt) nfd := Parse([]byte("Ação"), opt) if nfc.RawHash != nfd.RawHash { t.Fatal("NFD input should normalize to NFC") } // A title change is a content change. d := Parse([]byte("---\ntitle: Sun\nrevision: First note\nmaturity: seed\n---\nBody text.\n"), opt) if d.ContentHash == a.ContentHash { t.Fatal("title change should change ContentHash") } } func TestMalformedFrontmatter(t *testing.T) { d := Parse([]byte("---\ntitle: [unclosed\n---\nBody"), DefaultOptions()) if d.FrontmatterErr == nil { t.Fatal("want YAML error") } if d.Body != "Body" || len(d.Blocks) != 1 { t.Fatalf("body should survive: %+v", d) } } func TestNoFrontmatterUsesHeadingTitle(t *testing.T) { d := Parse([]byte("# Jardim de agentes autônomos\n\ntext"), DefaultOptions()) if d.Title != "Jardim de agentes autônomos" { t.Fatalf("title %q", d.Title) } unclosed := Parse([]byte("---\nnot frontmatter"), DefaultOptions()) if len(unclosed.Frontmatter) != 0 || unclosed.Body != "---\nnot frontmatter" { t.Fatalf("unclosed delimiter is body: %+v", unclosed) } } func TestIgnoreRegions(t *testing.T) { opt := DefaultOptions() a := Parse([]byte("Keep.\n\n\nUpdated 2026-10-01\n\n\nEnd."), opt) b := Parse([]byte("Keep.\n\n\nUpdated 2026-10-23\n\n\nEnd."), opt) if a.ContentHash != b.ContentHash { t.Fatal("ignored region edits must not change ContentHash") } if kinds(a.Blocks) != "paragraph,paragraph" { t.Fatalf("blocks %s", kinds(a.Blocks)) } fenced := Parse([]byte("```\n\nshown\n```"), opt) if !strings.Contains(fenced.Blocks[0].Text, "shown") { t.Fatal("markers inside code fences must be literal") } } func TestYAMLScalarsAsText(t *testing.T) { d := Parse([]byte("---\nrevision: 2\nmaturity: yes\n---\n"), DefaultOptions()) // yaml.v3 follows YAML 1.2, where "yes" is a plain string. if d.Note != "2" || d.Maturity != "yes" { t.Fatalf("note %q maturity %q", d.Note, d.Maturity) } }