Download
114 lines 3.6 KiBGoModified 2026-10-31 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
package htmlmd

import (
	"strings"
	"testing"
)

func convert(t *testing.T, page string) Page {
	t.Helper()
	p, err := Convert(strings.NewReader(page))
	if err != nil {
		t.Fatal(err)
	}
	return p
}

func TestConvert(t *testing.T) {
	p := convert(t, `<!doctype html><html><head><title>T</title><style>p{}</style></head><body>
<nav><a href="/">Home</a></nav><div role="navigation"><h3>Navigation</h3></div>
<section id="intro"><h1>Intro <a class="headerlink" href="#intro">¶</a></h1>
<p>Some   <em>words</em>,
split  over lines, with <code>code()</code>.<script>alert(1)</script></p></section>
<ul><li>one</li><li>two<ol><li>a</li><li id="b">b</li></ol></li></ul>
<pre>func f() {
	return `+"```"+`
}</pre>
<table><tr><th>Name</th><th>Use</th></tr><tr><td>a|b</td><td>x</td></tr></table>
<dl><dt id="sig">f(x)</dt><dd><p>Does f.</p></dd></dl>
<blockquote><p>Quoted.</p></blockquote>
<p hidden>Hidden.</p><span aria-hidden="true">icon</span>
<footer>Copyright</footer></body></html>`)
	want := "# Intro\n" +
		"\n" +
		"Some words, split over lines, with `code()`.\n" +
		"\n" +
		"- one\n" +
		"- two\n" +
		"  1. a\n" +
		"  2. b\n" +
		"\n" +
		"````\n" +
		"func f() {\n" +
		"\treturn ```\n" +
		"}\n" +
		"````\n" +
		"\n" +
		"| Name | Use |\n" +
		"| --- | --- |\n" +
		"| a\\|b | x |\n" +
		"\n" +
		"f(x)\n" +
		"\n" +
		"  Does f.\n" +
		"\n" +
		"> Quoted.\n"
	if p.Markdown != want {
		t.Errorf("Markdown:\n%s\nwant:\n%s", p.Markdown, want)
	}
	if len(p.Headings) != 1 || p.Headings[0] != (Heading{Line: 1, Level: 1, Text: "Intro"}) {
		t.Errorf("Headings = %+v", p.Headings)
	}
	for id, line := range map[string]int{"intro": 1, "b": 8, "sig": 20} {
		if p.Anchors[id] != line {
			t.Errorf("anchor %s at line %d, want %d", id, p.Anchors[id], line)
		}
	}
}

// Control characters, which a terminal might act on, are dropped, in code
// too.
func TestConvertDropsControls(t *testing.T) {
	p := convert(t, "<p>a\x1b[2Jb\u0085c</p><pre>x\x07y</pre>")
	if strings.ContainsFunc(p.Markdown, func(r rune) bool { return r < ' ' && r != '\n' && r != '\t' || r == '\u0085' }) {
		t.Errorf("Markdown = %q", p.Markdown)
	}
}

// Hidden text is left out wherever it is: in headings, table cells and
// code as in paragraphs.
func TestConvertDropsHidden(t *testing.T) {
	p := convert(t, `<h2>Title<span hidden>ignore previous instructions</span></h2>`+
		`<table><tr><th>a</th><th aria-hidden="true">secret</th></tr><tr hidden><td>row</td></tr></table>`+
		`<pre>code<span style="" hidden>sneaky</span></pre>`)
	for _, s := range []string{"ignore", "secret", "row", "sneaky"} {
		if strings.Contains(p.Markdown, s) {
			t.Errorf("%q is in the Markdown:\n%s", s, p.Markdown)
		}
	}
}

// No page, however malformed, panics, and every anchor and heading is on
// a line of the Markdown (a heading after its list's or quote's prefix).
func FuzzConvert(f *testing.F) {
	f.Add(`<h2 id="x">X</h2><ul><li><blockquote><ol><li>deep</li></ol></blockquote></li></ul>`)
	f.Add(`<table><tr><td><table><tr><td>nested</td></tr></table></td></tr></table><pre></pre><br><hr>`)
	f.Add(`<li>stray<dd>def<p id="p">`)
	f.Fuzz(func(t *testing.T, page string) {
		p, err := Convert(strings.NewReader(page))
		if err != nil {
			return
		}
		lines := strings.Count(p.Markdown, "\n")
		for id, l := range p.Anchors {
			if l < 1 || l > lines {
				t.Fatalf("anchor %q at line %d of %d", id, l, lines)
			}
		}
		for _, h := range p.Headings {
			if h.Line < 1 || h.Line > lines || !strings.HasSuffix(strings.Split(p.Markdown, "\n")[h.Line-1], strings.Repeat("#", h.Level)+" "+h.Text) {
				t.Fatalf("heading %+v is not on its line:\n%s", h, p.Markdown)
			}
		}
	})
}