Files
2026-10-19 15:39:46 +00:00

126 lines
3.9 KiB
Go

package browse
// Internal test: it pins the unexported converter's exact output so
// formatting drift is caught by the package itself.
import (
"strings"
"testing"
)
func TestHTMLToMarkdown(t *testing.T) {
t.Parallel()
cases := []struct {
name string
html string
want string
}{
{
name: "headings and paragraphs",
html: `<html><body><h1>Title</h1><p>First.</p><p>Second <strong>bold</strong> and <em>italic</em>.</p></body></html>`,
want: "# Title\n\nFirst.\n\nSecond **bold** and *italic*.",
},
{
name: "relative links resolve against the page",
html: `<body><p><a href="/docs/guide">Guide</a> and <a href="https://abs.test/x">abs</a></p></body>`,
want: "[Guide](https://page.test/docs/guide) and [abs](https://abs.test/x)",
},
{
name: "dead links keep their text only",
html: `<body><p><a href="#">top</a> <a href="javascript:go()">go</a></p></body>`,
want: "top go",
},
{
name: "lists nest and order",
html: `<body><ul><li>one</li><li>two<ol start="3"><li>deep</li></ol></li></ul></body>`,
want: "- one\n- two\n 3. deep",
},
{
name: "blockquote prefixes every line",
html: `<body><blockquote><p>line one</p><p>line two</p></blockquote></body>`,
want: "> line one\n>\n> line two",
},
{
name: "pre renders as a fenced block",
html: `<body><pre>keep
spacing</pre></body>`,
want: "```\nkeep\n spacing\n```",
},
{
name: "tables render GFM",
html: `<body><table><thead><tr><th>Name</th><th>Size</th></tr></thead>` +
`<tbody><tr><td>a|b</td><td>1</td></tr></tbody></table></body>`,
want: "| Name | Size |\n| --- | --- |\n| a\\|b | 1 |",
},
{
name: "script, style and hidden content vanish",
html: `<body><style>p{}</style><script>var x=1;</script>` +
`<p hidden>nope</p><p aria-hidden="true">nor this</p><p>visible</p></body>`,
want: "visible",
},
{
name: "images keep alt text",
html: `<body><p><img src="/img/logo.png" alt="Logo"> and <img src="/img/x.png"></p></body>`,
want: "![Logo](https://page.test/img/logo.png) and ![x.png](https://page.test/img/x.png)",
},
{
name: "anchors at block position keep their href",
html: `<body><nav><a href="/policy">Policy</a></nav><ul><li><a href="/docs">Docs</a></li></ul></body>`,
want: "[Policy](https://page.test/policy)\n\n- [Docs](https://page.test/docs)",
},
{
name: "consecutive blocks collapse to one blank line",
html: `<body><div><p>a</p></div><div><p>b</p></div></body>`,
want: "a\n\nb",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got, _, err := htmlToMarkdown([]byte(tc.html), "https://page.test/")
if err != nil {
t.Fatalf("htmlToMarkdown: %v", err)
}
if got != tc.want {
t.Errorf("markdown =\n%q\nwant\n%q", got, tc.want)
}
})
}
}
func TestHTMLToMarkdownMetadata(t *testing.T) {
t.Parallel()
html := `<!doctype html><html lang="sv"><head>
<title>Meta sidan</title>
<meta name="description" content="Beskrivning.">
<meta name="robots" content="noai">
<link rel="canonical" href="https://page.test/canon">
</head><body>
<h1>Rubrik</h1>
<p>lite text att räkna</p>
<script src="/x.js"></script><img src="/y.png" alt="y">
</body></html>`
_, meta, err := htmlToMarkdown([]byte(html), "https://page.test/")
if err != nil {
t.Fatalf("htmlToMarkdown: %v", err)
}
if meta.Title != "Meta sidan" || meta.Description != "Beskrivning." {
t.Errorf("head metadata wrong: %+v", meta)
}
if meta.Lang != "sv" || meta.Canonical != "https://page.test/canon" {
t.Errorf("lang/canonical wrong: %+v", meta)
}
if meta.H1 != "Rubrik" {
t.Errorf("h1 = %q", meta.H1)
}
if meta.WordCount != 5 || meta.TextLength == 0 {
t.Errorf("text stats wrong: words=%d chars=%d", meta.WordCount, meta.TextLength)
}
if meta.ScriptCount != 1 || meta.ImageCount != 1 {
t.Errorf("element counts wrong: %+v", meta)
}
if !strings.Contains(strings.ToLower(meta.Robots), "noai") {
t.Errorf("robots = %q", meta.Robots)
}
}