Skip to content
quarry Artifact [c47d46f79c]
Login

Artifact [c47d46f79c]

Artifact c47d46f79c65d407952e60f910a4ebf32f5653c5e2df5b3b1c8ea427295dce47:


package markup

import (
	"bytes"
	"strings"
	"testing"

	"golang.org/x/net/html"
	"golang.org/x/net/html/atom"
)

// sqliteA7f5 is the description of sqlite ticket a7f5b0856f, byte for
// byte: CRLF line endings, and a fence opened on a quote line ("> ~~~~")
// whose body and closing fence carry no ">" at all. Fossil keeps all of
// it inside the quote.
const sqliteA7f5 = "As reported by [forum post 2ed79a01ae](forum:/forumpost/2ed79a01ae), the\r\n" +
	"query planner is missing an opportunity to optimization WHERE clause\r\n" +
	"expressions of the form:\r\n\r\n" +
	">      constant IN (indexed-column-1, indexed-column-2)\r\n\r\n" +
	"The script below illustrates the missed optimization opportunity.\r\n" +
	"The first query (using the IN operator) runs about 180 times more\r\n" +
	"opcodes to compute the answer than does the second query where the\r\n" +
	"optimization is implemented manually.\r\n\r\n" +
	"> ~~~~\r\n" + sqliteA7f5Script + "~~~~\r\n\r\n" +
	"The desired result is that both queries should generate the same\r\n" +
	"query plan and perform equally as well."

const sqliteA7f5Script = ".echo on\r\n.mode qbox\r\nCREATE TABLE edge(\r\n  src INT,\r\n  dst INT,\r\n" +
	"  PRIMARY KEY(src, dst)\r\n) WITHOUT ROWID;\r\n" +
	"WITH RECURSIVE c(x) AS (VALUES(1) UNION ALL SELECT x+1 FROM c WHERE x<1000)\r\n" +
	"  INSERT INTO edge SELECT x, x+1000 FROM c;\r\nCREATE INDEX edge_x1 ON edge(dst,src);\r\n" +
	".parameter set :vertex 1234\r\n.stat vmstep\r\n.eqp on\r\n" +
	"SELECT * FROM edge WHERE :vertex IN (src,dst);\r\n" +
	"SELECT * FROM edge WHERE :vertex=src OR :vertex=dst;\r\n"

func parseBody(t *testing.T, src string) *html.Node {
	t.Helper()
	out, err := Render([]byte(src), nil)
	if err != nil {
		t.Fatal(err)
	}
	doc, err := html.Parse(bytes.NewReader(out))
	if err != nil {
		t.Fatal(err)
	}
	return doc
}

func textOf(n *html.Node) string {
	var b strings.Builder
	var walk func(*html.Node)
	walk = func(n *html.Node) {
		if n.Type == html.TextNode {
			b.WriteString(n.Data)
		}
		for c := n.FirstChild; c != nil; c = c.NextSibling {
			walk(c)
		}
	}
	walk(n)
	return b.String()
}

func findAll(n *html.Node, a atom.Atom) []*html.Node {
	var got []*html.Node
	var walk func(*html.Node)
	walk = func(n *html.Node) {
		if n.Type == html.ElementNode && n.DataAtom == a {
			got = append(got, n)
		}
		for c := n.FirstChild; c != nil; c = c.NextSibling {
			walk(c)
		}
	}
	walk(n)
	return got
}

func insideA(n *html.Node, a atom.Atom) bool {
	for p := n.Parent; p != nil; p = p.Parent {
		if p.Type == html.ElementNode && p.DataAtom == a {
			return true
		}
	}
	return false
}

// A fence opened on a quote line runs to its closing fence inside the
// quote, as fossil's markdown has it: a quote continues over every
// following line until a blank line.
func TestQuotedFenceWithoutMarkersStaysQuoted(t *testing.T) {
	doc := parseBody(t, sqliteA7f5)
	quotes := findAll(doc, atom.Blockquote)
	if len(quotes) != 2 {
		t.Fatalf("blockquotes = %d, want 2", len(quotes))
	}
	for i, q := range quotes {
		if strings.TrimSpace(textOf(q)) == "" {
			t.Errorf("blockquote %d is empty", i)
		}
	}
	codes := findAll(quotes[1], atom.Code)
	if len(codes) != 1 {
		t.Fatalf("code blocks in second quote = %d, want 1", len(codes))
	}
	want := strings.ReplaceAll(sqliteA7f5Script, "\r\n", "\n")
	if got := strings.ReplaceAll(textOf(codes[0]), "\r\n", "\n"); got != want {
		t.Errorf("quoted code = %q, want %q", got, want)
	}
	for _, pre := range findAll(doc, atom.Pre) {
		if !insideA(pre, atom.Blockquote) {
			t.Errorf("code block outside any quote: %q", textOf(pre))
		}
	}
	ps := findAll(doc, atom.P)
	if len(ps) != 3 {
		t.Errorf("paragraphs = %d, want 3", len(ps))
	}
}

func TestQuoteContinuation(t *testing.T) {
	cases := []struct {
		name string
		src  string
		// quotes is the number of blockquotes; quoted and unquoted are
		// input lines that must, and must not, end up inside one.
		quotes   int
		quoted   []string
		unquoted []string
	}{
		{"blank line ends the quote", "> ~~~\nin\n\nout\n", 1, []string{"in"}, []string{"out"}},
		{"lazy paragraph line", "> first\nsecond\n\nafter\n", 1, []string{"second"}, []string{"after"}},
		{"blank line inside a quoted fence does not end it", "> ~~~\nin\n\nstill\n~~~\n\nafter\n", 1, []string{"in", "still"}, []string{"after"}},
		{"blank line between quote lines keeps one quote", "> one\n\n> two\n", 1, []string{"one", "two"}, nil},
		{"quote marker inside a top-level fence is code", "~~~\n> not a quote\nplain\n~~~\n", 0, nil, []string{"plain"}},
		{"quote nested in a list keeps the list", "- item\n  > ~~~\n  code\n  ~~~\n\nafter\n", 1, []string{"code"}, []string{"after"}},
		{"indented line is quoted with its indent", "> ~~~\n  x = 1\n~~~\n", 1, []string{"  x = 1"}, nil},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			doc := parseBody(t, c.src)
			quotes := findAll(doc, atom.Blockquote)
			if len(quotes) != c.quotes {
				t.Fatalf("blockquotes = %d, want %d", len(quotes), c.quotes)
			}
			var in strings.Builder
			for _, q := range quotes {
				in.WriteString(textOf(q))
			}
			for _, s := range c.quoted {
				if !strings.Contains(in.String(), s) {
					t.Errorf("%q not inside a quote", s)
				}
			}
			for _, s := range c.unquoted {
				if strings.Contains(in.String(), s) {
					t.Errorf("%q inside a quote", s)
				}
				if !strings.Contains(textOf(doc), s) {
					t.Errorf("%q lost", s)
				}
			}
			if strings.Contains(c.name, "list") {
				li := findAll(doc, atom.Li)
				if len(li) != 1 || len(findAll(li[0], atom.Blockquote)) != 1 {
					t.Errorf("quote not inside the list item")
				}
			}
		})
	}
}