package markup
import (
"strings"
"testing"
"golang.org/x/net/html"
"golang.org/x/net/html/atom"
)
func attr(n *html.Node, key string) string {
for _, a := range n.Attr {
if a.Key == key {
return a.Val
}
}
return ""
}
func hasClass(n *html.Node, want string) bool {
for _, c := range strings.Fields(attr(n, "class")) {
if c == want {
return true
}
}
return false
}
func TestTableAlignmentSurvivesSanitizer(t *testing.T) {
doc := parseBody(t, "| a | b | c |\n|:--|:-:|--:|\n| 1 | 2 | 3 |\n")
got := []string{}
for _, th := range findAll(doc, atom.Th) {
got = append(got, attr(th, "align"))
}
if strings.Join(got, ",") != "left,center,right" {
t.Fatalf("th align = %v", got)
}
got = got[:0]
for _, td := range findAll(doc, atom.Td) {
got = append(got, attr(td, "align"))
}
if strings.Join(got, ",") != "left,center,right" {
t.Fatalf("td align = %v", got)
}
}
func TestTaskListItemsCarryState(t *testing.T) {
doc := parseBody(t, "- [ ] open\n- [x] done\n")
lis := findAll(doc, atom.Li)
if len(lis) != 2 || !hasClass(lis[0], "unchecked") || !hasClass(lis[1], "checked") {
t.Fatalf("li classes = %q %q", attr(lis[0], "class"), attr(lis[1], "class"))
}
if uls := findAll(doc, atom.Ul); len(uls) != 1 || !hasClass(uls[0], "md-tasks") {
t.Fatal("task-only list lacks md-tasks")
}
doc = parseBody(t, "- plain\n- [x] done\n")
if uls := findAll(doc, atom.Ul); hasClass(uls[0], "md-tasks") {
t.Fatal("mixed list must not carry md-tasks")
}
}
func TestTableColumnHints(t *testing.T) {
src := "| name | qty | note |\n|---|---|---|\n" +
"| a | 1 | " + strings.Repeat("x", 30) + " |\n" +
"| b | -2.5% | y |\n"
doc := parseBody(t, src)
rows := findAll(doc, atom.Tr)
if len(rows) != 3 {
t.Fatalf("rows = %d", len(rows))
}
cellsOf := func(tr *html.Node) []*html.Node {
var cs []*html.Node
for c := tr.FirstChild; c != nil; c = c.NextSibling {
if c.Type == html.ElementNode {
cs = append(cs, c)
}
}
return cs
}
for _, tr := range rows {
cs := cellsOf(tr)
if !hasClass(cs[0], "md-short") || hasClass(cs[0], "md-num") {
t.Errorf("col 0 classes %q", attr(cs[0], "class"))
}
if !hasClass(cs[1], "md-short") || !hasClass(cs[1], "md-num") {
t.Errorf("col 1 classes %q", attr(cs[1], "class"))
}
if attr(cs[2], "class") != "" {
t.Errorf("col 2 classes %q", attr(cs[2], "class"))
}
}
}
func TestAlertBlockquote(t *testing.T) {
for _, kind := range []string{"NOTE", "TIP", "IMPORTANT", "WARNING", "CAUTION"} {
doc := parseBody(t, "> [!"+strings.ToLower(kind)+"]\n> body line\n")
var alert *html.Node
for _, d := range findAll(doc, atom.Div) {
if hasClass(d, "md-alert") {
alert = d
}
}
if alert == nil || !hasClass(alert, "md-alert-"+strings.ToLower(kind)) {
t.Fatalf("%s: no alert div", kind)
}
if len(findAll(doc, atom.Blockquote)) != 0 {
t.Fatalf("%s: still a blockquote", kind)
}
if strings.Contains(textOf(alert), "[!") {
t.Fatalf("%s: marker not removed", kind)
}
if !strings.Contains(textOf(alert), "body line") {
t.Fatalf("%s: body lost", kind)
}
titles := 0
for _, p := range findAll(alert, atom.P) {
if hasClass(p, "md-alert-title") {
titles++
}
}
if titles != 1 {
t.Fatalf("%s: %d titles", kind, titles)
}
}
}
func TestNonAlertBlockquotesUnchanged(t *testing.T) {
for _, src := range []string{
"> plain\n",
"> [!NOTE] inline text\n> more\n",
"> [!BOGUS]\n> more\n",
"> intro\n> [!NOTE]\n",
} {
doc := parseBody(t, src)
if len(findAll(doc, atom.Blockquote)) != 1 {
t.Errorf("%q: not a plain blockquote", src)
}
}
}
// alertKindsOf is the kind class of every alert in the page, in order.
func alertKindsOf(doc *html.Node) []string {
var kinds []string
for _, d := range findAll(doc, atom.Div) {
if hasClass(d, "md-alert") {
for _, c := range strings.Fields(attr(d, "class")) {
if strings.HasPrefix(c, "md-alert-") {
kinds = append(kinds, c)
}
}
}
}
return kinds
}
func TestStackedAlertsStayApart(t *testing.T) {
for name, tc := range map[string]struct {
src string
kinds []string
blockquotes int
}{
"blank separated": {"> [!NOTE]\n> a\n\n> [!WARNING]\n> b\n\n> [!TIP]\n> c\n", []string{"md-alert-note", "md-alert-warning", "md-alert-tip"}, 0},
"back to back": {"> [!NOTE]\n> a\n> [!WARNING]\n> b\n", []string{"md-alert-note"}, 0},
"many blank lines": {"> [!NOTE]\n> a\n\n\n> [!WARNING]\n> b\n", []string{"md-alert-note", "md-alert-warning"}, 0},
"multi paragraph": {"> [!NOTE]\n> a\n>\n> b\n", []string{"md-alert-note"}, 0},
"plain after": {"> [!NOTE]\n> a\n\n> plain\n", []string{"md-alert-note"}, 1},
"plain before": {"> plain\n\n> [!NOTE]\n> a\n", []string{"md-alert-note"}, 1},
} {
doc := parseBody(t, tc.src)
got := alertKindsOf(doc)
if strings.Join(got, ",") != strings.Join(tc.kinds, ",") {
t.Errorf("%s: alerts %v, want %v", name, got, tc.kinds)
}
if n := len(findAll(doc, atom.Blockquote)); n != tc.blockquotes {
t.Errorf("%s: %d blockquotes, want %d", name, n, tc.blockquotes)
}
}
}