summaryrefslogtreecommitdiff
path: root/main_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'main_test.go')
-rw-r--r--main_test.go328
1 files changed, 326 insertions, 2 deletions
diff --git a/main_test.go b/main_test.go
index f6c943e..cf292a6 100644
--- a/main_test.go
+++ b/main_test.go
@@ -1,14 +1,19 @@
package main
import (
+ "bytes"
"encoding/json"
"encoding/xml"
+ "errors"
"html/template"
+ "io"
"os"
"path/filepath"
"strings"
"testing"
"time"
+
+ "golang.org/x/net/html"
)
func writeTestFile(t *testing.T, root, name, contents string) {
@@ -81,8 +86,17 @@ First paragraph with [local](other.md#part), [web](https://example.com/a.md), an
page = src.page
}
}
- if page == nil || page.Meta["summary"] != "hello" || len(page.Sections) != 5 {
- t.Fatalf("unexpected page model: %#v", page)
+ if page == nil || page.Meta["summary"] != "hello" || len(page.Sections) != 0 || page.markdownAST == nil {
+ t.Fatalf("unexpected discovery model: %#v", page)
+ }
+ if err := s.loadTemplates(); err != nil {
+ t.Fatal(err)
+ }
+ if err := s.prepareMarkdown(); err != nil {
+ t.Fatal(err)
+ }
+ if len(page.Sections) != 5 {
+ t.Fatalf("prepared sections = %d, want 5", len(page.Sections))
}
}
@@ -440,3 +454,313 @@ func TestUnchangedOutputsKeepMtime(t *testing.T) {
t.Error("changed output mtime not updated")
}
}
+
+func TestMarkdownNoOverrideMatchesGoldmark(t *testing.T) {
+ root := t.TempDir()
+ writeTestFile(t, root, "layouts.tmpl", `{{define "page"}}{{joinSections .Sections}}{{end}}`)
+ markdown := "# Heading\n\nParagraph with **strong**, ~~strike~~, and [link](#heading).\n\n| A | B |\n| - | - |\n| 1 | 2 |\n\n<script>bad()</script>\n"
+ writeTestFile(t, root, "page.md", markdown)
+ if err := build(root); err != nil {
+ t.Fatal(err)
+ }
+ got, err := os.ReadFile(filepath.Join(root, "page.html"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ var want bytes.Buffer
+ if err := newMarkdown().Convert([]byte(markdown), &want); err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(got, want.Bytes()) {
+ t.Fatalf("no-override output changed:\n got: %s\nwant: %s", got, want.Bytes())
+ }
+}
+
+func TestMarkdownCoreOverrideContract(t *testing.T) {
+ root := t.TempDir()
+ writeTestFile(t, root, "layouts.tmpl", `{{define "page"}}{{joinSections .Sections}}{{end}}
+{{define "markdown/heading1"}}<H1 data-id="{{.ID}}">{{.Content}}</H1>{{end}}
+{{define "markdown/paragraph"}}<P>{{.Content}}</P>{{end}}
+{{define "markdown/blockquote"}}<BQ>{{.Content}}</BQ>{{end}}
+{{define "markdown/code_block"}}<I>{{.Content}}</I>{{end}}
+{{define "markdown/fenced_code_block"}}<F lang="{{.Language}}" highlighted="{{if .HighlightedContent}}yes{{else}}no{{end}}">{{if .HighlightedContent}}{{.HighlightedContent}}{{else}}{{.Content}}{{end}}</F>{{end}}
+{{define "markdown/ordered_list"}}<OL start="{{.Start}}" tight="{{.Tight}}">{{.Content}}</OL>{{end}}
+{{define "markdown/unordered_list"}}<UL tight="{{.Tight}}">{{.Content}}</UL>{{end}}
+{{define "markdown/list_item"}}<LI>{{.Content}}</LI>{{end}}
+{{define "markdown/emphasis"}}<E>{{.Content}}</E>{{end}}
+{{define "markdown/strong"}}<S>{{.Content}}</S>{{end}}
+{{define "markdown/code_span"}}<C>{{.Content}}</C>{{end}}
+{{define "markdown/link"}}<L href="{{.Destination}}" title="{{.Title}}">{{.Content}}</L>{{end}}
+{{define "markdown/autolink"}}<AU dest="{{.Destination}}">{{.Content}}</AU>{{end}}
+{{define "markdown/email_autolink"}}<AE dest="{{.Destination}}">{{.Content}}</AE>{{end}}
+{{define "markdown/image"}}<IMG alt="{{.Alt}}" dest="{{.Destination}}" title="{{.Title}}">{{end}}`)
+ writeTestFile(t, root, "target.md", "# Target\n")
+ markdown := "# Root Heading\n\n## Default *heading*\n\nParagraph *em **deep*** ` a b ` [local](target.md \"Title\") [unsafe](javascript:alert(1)).\n\n<https://example.com/a> <person@example.com> ![A **very** & black](cat.jpg \"Mochi\")\n\n> quoted **strong**\n\n <script>indented</script>\n\n" +
+ "```go extra\nx := \"</span><script>bad()</script>\"\n```\n\n```unknown\n<a>\n```\n\n```\n<b>\n```\n\n" +
+ "1. one\n1. two\n\n### split one\n\n3. three\n3. four\n\n### split two\n\n5. five\n99. six\n\n### split three\n\n- tight\n 1. nested\n\n### split four\n\n- loose\n\n- list\n"
+ writeTestFile(t, root, "page.md", markdown)
+ if err := build(root); err != nil {
+ t.Fatal(err)
+ }
+ raw, err := os.ReadFile(filepath.Join(root, "page.html"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ got := string(raw)
+ for _, want := range []string{
+ `<H1 data-id="root-heading">Root Heading</H1>`,
+ `<h2 id="default-heading">Default <E>heading</E></h2>`,
+ `<P>Paragraph <E>em <S>deep</S></E> <C>a b</C>`,
+ `<L href="target.html" title="Title">local</L>`,
+ `<L href="#ZgotmplZ" title="">unsafe</L>`,
+ `<AU dest="https://example.com/a">https://example.com/a</AU>`,
+ `<AE dest="mailto:person@example.com">person@example.com</AE>`,
+ `<IMG alt="A very &amp; black" dest="cat.jpg" title="Mochi">`,
+ `<BQ><P>quoted <S>strong</S></P>`,
+ `<I>&lt;script&gt;indented&lt;/script&gt;`,
+ `<F lang="go" highlighted="yes">`,
+ `<F lang="unknown" highlighted="no">&lt;a&gt;`,
+ `<F lang="" highlighted="no">&lt;b&gt;`,
+ `<OL start="1" tight="true">`,
+ `<OL start="3" tight="true">`,
+ `<OL start="5" tight="true">`,
+ `<UL tight="true"><LI>tight`,
+ `<OL start="1" tight="true"><LI>nested`,
+ `<UL tight="false"><LI><P>loose</P>`,
+ } {
+ if !strings.Contains(got, want) {
+ t.Errorf("output missing %q:\n%s", want, got)
+ }
+ }
+ if strings.Contains(got, "<script>") || !strings.Contains(got, "&lt;/span&gt;") {
+ t.Errorf("highlighted hostile source was not escaped:\n%s", got)
+ }
+ if strings.Count(got, `<OL start="1"`) != 2 || strings.Count(got, `<OL start="3"`) != 1 || strings.Count(got, `<OL start="5"`) != 1 {
+ t.Errorf("ordered list starts are wrong:\n%s", got)
+ }
+}
+
+func TestMarkdownSubtypeFallbacks(t *testing.T) {
+ root := t.TempDir()
+ writeTestFile(t, root, "layouts.tmpl", `{{define "page"}}{{joinSections .Sections}}{{end}}
+{{define "markdown/heading2"}}<h2 class="custom">{{.Content}}</h2>{{end}}
+{{define "markdown/strong"}}<strong class="custom">{{.Content}}</strong>{{end}}
+{{define "markdown/ordered_list"}}<ol class="custom">{{.Content}}</ol>{{end}}`)
+ writeTestFile(t, root, "page.md", "# Default H1\n\n## Custom H2\n\n*default emphasis with **custom strong***\n\n- default unordered **nested**\n\n1. custom ordered *nested default*\n")
+ if err := build(root); err != nil {
+ t.Fatal(err)
+ }
+ raw, err := os.ReadFile(filepath.Join(root, "page.html"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ got := string(raw)
+ for _, want := range []string{
+ `<h1 id="default-h1">Default H1</h1>`,
+ `<h2 class="custom">Custom H2</h2>`,
+ `<em>default emphasis with <strong class="custom">custom strong</strong></em>`,
+ `<ul>`, `default unordered <strong class="custom">nested</strong>`,
+ `<ol class="custom">`, `custom ordered <em>nested default</em>`,
+ } {
+ if !strings.Contains(got, want) {
+ t.Errorf("output missing %q:\n%s", want, got)
+ }
+ }
+}
+
+func TestMarkdownGFMAndTableContract(t *testing.T) {
+ root := t.TempDir()
+ writeTestFile(t, root, "layouts.tmpl", `{{define "page"}}{{joinSections .Sections}}{{end}}
+{{define "markdown/strikethrough"}}<DEL>{{.Content}}</DEL>{{end}}
+{{define "markdown/task_checkbox"}}<CHECKED value="{{.Checked}}">{{end}}
+{{define "markdown/table"}}<TABLE>{{.Content}}</TABLE>{{end}}
+{{define "markdown/table_header"}}<THEAD>{{.Content}}</THEAD>{{end}}
+{{define "markdown/table_body"}}<TBODY>{{.Content}}</TBODY>{{end}}
+{{define "markdown/table_row"}}<TR>{{.Content}}</TR>{{end}}
+{{define "markdown/table_header_cell"}}<TH align="{{.Alignment}}">{{.Content}}</TH>{{end}}
+{{define "markdown/table_cell"}}<TD align="{{.Alignment}}">{{.Content}}</TD>{{end}}`)
+ writeTestFile(t, root, "page.md", "# GFM\n\n~~gone~~\n\n- [ ] no\n- [x] yes\n\n| L | C | R | N |\n| :-- | :-: | --: | --- |\n| a | b | c | d |\n\n| Empty |\n| --- |\n")
+ if err := build(root); err != nil {
+ t.Fatal(err)
+ }
+ raw, err := os.ReadFile(filepath.Join(root, "page.html"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ got := string(raw)
+ for _, want := range []string{
+ `<DEL>gone</DEL>`, `<CHECKED value="false">`, `<CHECKED value="true">`,
+ `<TH align="left">L</TH>`, `<TH align="center">C</TH>`, `<TH align="right">R</TH>`, `<TH align="none">N</TH>`,
+ `<TD align="left">a</TD>`, `<TD align="center">b</TD>`, `<TD align="right">c</TD>`, `<TD align="none">d</TD>`,
+ } {
+ if !strings.Contains(got, want) {
+ t.Errorf("output missing %q:\n%s", want, got)
+ }
+ }
+ if strings.Count(got, "<TABLE>") != 2 || strings.Count(got, "<THEAD>") != 2 || strings.Count(got, "<TBODY>") != 1 || strings.Count(got, "<TR>") != 1 {
+ t.Errorf("table semantic pieces are wrong:\n%s", got)
+ }
+}
+
+func TestMarkdownEveryElementCanBeOverridden(t *testing.T) {
+ root := t.TempDir()
+ writeTestFile(t, root, "layouts.tmpl", `{{define "page"}}{{joinSections .Sections}}{{end}}
+{{define "markdown/heading1"}}<h1 class="weft" id="{{.ID}}">{{.Content}}</h1>{{end}}
+{{define "markdown/heading2"}}<h2 class="weft" id="{{.ID}}">{{.Content}}</h2>{{end}}
+{{define "markdown/heading3"}}<h3 class="weft" id="{{.ID}}">{{.Content}}</h3>{{end}}
+{{define "markdown/heading4"}}<h4 class="weft" id="{{.ID}}">{{.Content}}</h4>{{end}}
+{{define "markdown/heading5"}}<h5 class="weft" id="{{.ID}}">{{.Content}}</h5>{{end}}
+{{define "markdown/heading6"}}<h6 class="weft" id="{{.ID}}">{{.Content}}</h6>{{end}}
+{{define "markdown/paragraph"}}<p class="weft">{{.Content}}</p>{{end}}
+{{define "markdown/blockquote"}}<blockquote class="weft">{{.Content}}</blockquote>{{end}}
+{{define "markdown/code_block"}}<pre class="weft"><code class="weft">{{.Content}}</code></pre>{{end}}
+{{define "markdown/fenced_code_block"}}<pre class="weft"><code class="weft">{{.Content}}</code></pre>{{end}}
+{{define "markdown/unordered_list"}}<ul class="weft">{{.Content}}</ul>{{end}}
+{{define "markdown/ordered_list"}}<ol class="weft" start="{{.Start}}">{{.Content}}</ol>{{end}}
+{{define "markdown/list_item"}}<li class="weft">{{.Content}}</li>{{end}}
+{{define "markdown/thematic_break"}}<hr class="weft">{{end}}
+{{define "markdown/emphasis"}}<em class="weft">{{.Content}}</em>{{end}}
+{{define "markdown/strong"}}<strong class="weft">{{.Content}}</strong>{{end}}
+{{define "markdown/code_span"}}<code class="weft">{{.Content}}</code>{{end}}
+{{define "markdown/link"}}<a class="weft" href="{{.Destination}}">{{.Content}}</a>{{end}}
+{{define "markdown/autolink"}}<a class="weft" href="{{.Destination}}">{{.Content}}</a>{{end}}
+{{define "markdown/email_autolink"}}<a class="weft" href="{{.Destination}}">{{.Content}}</a>{{end}}
+{{define "markdown/image"}}<img class="weft" alt="{{.Alt}}" src="{{.Destination}}">{{end}}
+{{define "markdown/hard_break"}}<br class="weft">{{end}}
+{{define "markdown/strikethrough"}}<s class="weft">{{.Content}}</s>{{end}}
+{{define "markdown/task_checkbox"}}<input class="weft" type="checkbox" disabled{{if .Checked}} checked{{end}}>{{end}}
+{{define "markdown/table"}}<table class="weft">{{.Content}}</table>{{end}}
+{{define "markdown/table_header"}}<thead class="weft"><tr class="weft">{{.Content}}</tr></thead>{{end}}
+{{define "markdown/table_body"}}<tbody class="weft">{{.Content}}</tbody>{{end}}
+{{define "markdown/table_row"}}<tr class="weft">{{.Content}}</tr>{{end}}
+{{define "markdown/table_header_cell"}}<th class="weft">{{.Content}}</th>{{end}}
+{{define "markdown/table_cell"}}<td class="weft">{{.Content}}</td>{{end}}`)
+ markdown := "# H1\n## H2\n### H3\n#### H4\n##### H5\n###### H6\n\n> quote\n\n code\n\n```unknown\nfenced\n```\n\n- item\n\n1. ordered\n\n---\n\n*em* **strong** `code` [link](#h1) <https://example.com> <a@example.com> ![alt](image.png) ~~gone~~ \nbreak\n\n- [x] task\n\n| H |\n| - |\n| C |\n\n<div>raw</div>\n"
+ writeTestFile(t, root, "page.md", markdown)
+ if err := build(root); err != nil {
+ t.Fatal(err)
+ }
+ raw, err := os.ReadFile(filepath.Join(root, "page.html"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ z := html.NewTokenizer(bytes.NewReader(raw))
+ elements := 0
+ for {
+ typ := z.Next()
+ if typ == html.ErrorToken {
+ if !errors.Is(z.Err(), io.EOF) {
+ t.Fatal(z.Err())
+ }
+ break
+ }
+ if typ != html.StartTagToken && typ != html.SelfClosingTagToken {
+ continue
+ }
+ token := z.Token()
+ elements++
+ found := false
+ for _, attr := range token.Attr {
+ if attr.Key == "class" && strings.Contains(" "+attr.Val+" ", " weft ") {
+ found = true
+ }
+ }
+ if !found {
+ t.Errorf("<%s> lacks class=weft in:\n%s", token.Data, raw)
+ }
+ }
+ if elements < len(supportedMarkdownTemplates) {
+ t.Errorf("only saw %d elements for %d supported templates", elements, len(supportedMarkdownTemplates))
+ }
+ if strings.Contains(string(raw), "<div>") {
+ t.Errorf("raw HTML was re-enabled:\n%s", raw)
+ }
+}
+
+func TestMarkdownPartialTableOverrides(t *testing.T) {
+ for _, tc := range []struct {
+ name string
+ definition string
+ want string
+ }{
+ {"cell", `{{define "markdown/table_cell"}}<td class="only-cell">{{.Content}}</td>{{end}}`, `<td class="only-cell">B</td>`},
+ {"body", `{{define "markdown/table_body"}}<tbody class="only-body">{{.Content}}</tbody>{{end}}`, `<tbody class="only-body">`},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ root := t.TempDir()
+ writeTestFile(t, root, "layouts.tmpl", `{{define "page"}}{{joinSections .Sections}}{{end}}`+tc.definition)
+ writeTestFile(t, root, "page.md", "# Table\n\n| A |\n| - |\n| B |\n")
+ if err := build(root); err != nil {
+ t.Fatal(err)
+ }
+ raw, err := os.ReadFile(filepath.Join(root, "page.html"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ got := string(raw)
+ if !strings.Contains(got, tc.want) || strings.Count(got, "<table>") != 1 || strings.Count(got, "</table>") != 1 || strings.Count(got, "<thead>") != 1 || strings.Count(got, "</thead>") != 1 || strings.Count(got, "<tr>") != strings.Count(got, "</tr>") {
+ t.Errorf("invalid partial table output:\n%s", got)
+ }
+ })
+ }
+}
+
+func TestMarkdownTemplateErrorsAreAtomic(t *testing.T) {
+ t.Run("unknown name", func(t *testing.T) {
+ root := t.TempDir()
+ writeTestFile(t, root, "layouts.tmpl", `{{define "page"}}{{joinSections .Sections}}{{end}}{{define "markdown/paragaph"}}bad{{end}}`)
+ writeTestFile(t, root, "page.md", "# Page\n")
+ err := build(root)
+ if err == nil || !strings.Contains(err.Error(), `unsupported Markdown template "markdown/paragaph"`) {
+ t.Fatalf("error = %v", err)
+ }
+ if _, err := os.Stat(filepath.Join(root, "page.html")); !os.IsNotExist(err) {
+ t.Fatalf("failed build wrote page.html: %v", err)
+ }
+ })
+
+ t.Run("execution context and preservation", func(t *testing.T) {
+ root := t.TempDir()
+ writeTestFile(t, root, "layouts.tmpl", `{{define "page"}}{{joinSections .Sections}}{{end}}`)
+ writeTestFile(t, root, "target.md", "# Target\n")
+ writeTestFile(t, root, "page.md", "# Old\n")
+ if err := build(root); err != nil {
+ t.Fatal(err)
+ }
+ before, err := os.ReadFile(filepath.Join(root, "page.html"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ writeTestFile(t, root, "layouts.tmpl", `{{define "page"}}{{joinSections .Sections}}{{end}}{{define "markdown/link"}}{{.Missing}}{{end}}`)
+ writeTestFile(t, root, "page.md", "# New\n\n[link](target.md)\n")
+ err = build(root)
+ if err == nil || !strings.Contains(err.Error(), "render page.md markdown/link") {
+ t.Fatalf("error = %v", err)
+ }
+ after, readErr := os.ReadFile(filepath.Join(root, "page.html"))
+ if readErr != nil || !bytes.Equal(after, before) {
+ t.Fatalf("failed build changed output: %v\n%s", readErr, after)
+ }
+ })
+}
+
+func TestExplicitOutputSeesAllPreparedSections(t *testing.T) {
+ root := t.TempDir()
+ writeTestFile(t, root, "layouts.tmpl", `{{define "page"}}{{joinSections .Sections}}{{end}}{{define "markdown/paragraph"}}<p class="prepared">{{.Content}}</p>{{end}}`)
+ writeTestFile(t, root, "a.xml.tmpl", `<pages>{{range index .Pages ""}}<page>{{xml (joinSections .Sections)}}</page>{{end}}</pages>`)
+ writeTestFile(t, root, "z.md", "# Zed\n\nlast source\n")
+ writeTestFile(t, root, "b.md", "# Bee\n\nsecond source\n")
+ if err := build(root); err != nil {
+ t.Fatal(err)
+ }
+ raw, err := os.ReadFile(filepath.Join(root, "a.xml"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ got := string(raw)
+ for _, want := range []string{"&lt;h1 id=&#34;bee&#34;&gt;Bee&lt;/h1&gt;", "&lt;p class=&#34;prepared&#34;&gt;second source&lt;/p&gt;", "&lt;h1 id=&#34;zed&#34;&gt;Zed&lt;/h1&gt;", "&lt;p class=&#34;prepared&#34;&gt;last source&lt;/p&gt;"} {
+ if !strings.Contains(got, want) {
+ t.Errorf("explicit output missing %q:\n%s", want, got)
+ }
+ }
+}