summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md10
-rw-r--r--main.go26
-rw-r--r--main_test.go19
3 files changed, 36 insertions, 19 deletions
diff --git a/README.md b/README.md
index 3a19cb4..79405e6 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,8 @@ go build -o weft .
Markdown uses CommonMark with GFM tables, strikethrough, task lists, and
autolinks. Raw Markdown HTML is disabled. Optional YAML frontmatter is exposed
-as `.Meta`; the first H1 becomes `.Title` and is removed from `.Sections`.
+as `.Meta`; the first H1 becomes `.Title`, while `.Sections` preserves every
+top-level Markdown node, including that H1.
## Site templates
@@ -46,10 +47,10 @@ The current `Page` is embedded, so its fields can be accessed as either
| `.OutputPath` | Slash-separated generated path relative to the site root. |
| `.URL` | Root-relative URL. Markdown `index.html` outputs use their directory URL. |
| `.CanonicalURL` | `.URL` beneath `canonical-root` |
-| `.Title` | First H1, then frontmatter `title`, then a filename-derived fallback. The extracted H1 is removed from the body. |
+| `.Title` | First H1, then frontmatter `title`, then a filename-derived fallback. |
| `.Meta` | `map[string]any` containing optional YAML frontmatter. |
| `.ModTime` | Source-file modification time as `time.Time`. |
-| `.Sections` | `[]template.HTML` containing one rendered fragment per top-level Markdown node. Raw Markdown HTML is disabled before these trusted fragments are created. |
+| `.Sections` | `[]template.HTML` containing every rendered top-level Markdown node in source order, including the H1 used for `.Title`. Raw Markdown HTML is disabled before these trusted fragments are created. |
| `.Pages` | All discovered Markdown pages grouped by ancestor directory. This field belongs to the root template value, not `Page`. |
`.Pages` uses slash-separated directory keys. The root key is `""`. A page at
@@ -67,7 +68,7 @@ have no Markdown title, modification time, or sections.
| Function | Result |
| --- | --- |
| `rel from target` | Makes a root-relative site target relative to the directory containing `from`. HTTP(S), `mailto:`, and fragment targets pass through. A trailing `/` is preserved, so `rel "about/index.html" "/"` returns `../`. |
-| `sortPages pages key` | Returns a copy. `"title"` sorts ascending case-insensitively; `"mod_time"` sorts newest first; every other key sorts newest first using that frontmatter value as a date. |
+| `sortPages pages keys...` | Returns a copy. `"title"` sorts ascending case-insensitively. Date keys sort newest first, using the first valid value for each page; `"mod_time"` selects `.ModTime` and other keys select frontmatter values. |
| `filterPages pages key value` | Keeps pages whose value stringifies equally. `"title"` and `"output"` select `.Title` and `.OutputPath`; other keys select `.Meta[key]`. |
| `slicePages pages start end` | Returns the half-open range `[start:end]`, clamping both bounds instead of failing. |
| `joinSections sections` | Concatenates rendered Markdown sections and returns trusted `template.HTML`. |
@@ -80,6 +81,7 @@ Examples:
```gotemplate
<a href="{{rel .OutputPath "/notes/"}}">notes</a>
{{range sortPages (index .Pages "weblog") "post_date"}}{{.Title}}{{end}}
+{{range sortPages (index .Pages "weblog") "post_date" "mod_time"}}{{.Title}}{{end}}
{{with index .Sections 0}}{{.}}{{end}}
{{joinSections (slice .Sections 1)}}
<content type="html">{{xml (joinSections .Sections)}}</content>
diff --git a/main.go b/main.go
index c7fb8ce..8fabda0 100644
--- a/main.go
+++ b/main.go
@@ -312,9 +312,7 @@ func extractTitle(doc ast.Node, source []byte) string {
if !ok || heading.Level != 1 {
continue
}
- title := strings.TrimSpace(string(heading.Text(source)))
- doc.RemoveChild(doc, node)
- return title
+ return strings.TrimSpace(string(heading.Text(source)))
}
return ""
}
@@ -427,17 +425,25 @@ func relativePath(from, target string) string {
return rel
}
-func sortPages(pages []*Page, key string) []*Page {
+func sortPages(pages []*Page, keys ...string) []*Page {
result := slices.Clone(pages)
- sort.SliceStable(result, func(i, j int) bool {
- if key == "mod_time" {
- return result[i].ModTime.After(result[j].ModTime)
+ date := func(page *Page) time.Time {
+ for _, key := range keys {
+ if key == "mod_time" {
+ return page.ModTime
+ }
+ if value, ok := asTime(page.Meta[key]); ok {
+ return value
+ }
}
- if key == "title" {
+ return time.Time{}
+ }
+ sort.SliceStable(result, func(i, j int) bool {
+ if len(keys) == 1 && keys[0] == "title" {
return strings.ToLower(result[i].Title) < strings.ToLower(result[j].Title)
}
- a, _ := asTime(result[i].Meta[key])
- b, _ := asTime(result[j].Meta[key])
+ a := date(result[i])
+ b := date(result[j])
return a.After(b)
})
return result
diff --git a/main_test.go b/main_test.go
index f2aae0a..a1eda14 100644
--- a/main_test.go
+++ b/main_test.go
@@ -38,6 +38,8 @@ func TestMarkdownGFMFrontmatterTitleSectionsAndLinks(t *testing.T) {
writeTestFile(t, root, "page.md", `---
summary: hello
---
+> Read this first.
+
# My *Page*
First paragraph with [local](other.md#part), [web](https://example.com/a.md), and [fragment](#part).
@@ -56,13 +58,16 @@ First paragraph with [local](other.md#part), [web](https://example.com/a.md), an
t.Fatal(err)
}
got := string(raw)
- for _, want := range []string{"<title>My Page</title>", `href="other.html#part"`, `href="https://example.com/a.md"`, `href="#part"`, "<table>"} {
+ for _, want := range []string{"<title>My Page</title>", "<blockquote>", "<h1", `href="other.html#part"`, `href="https://example.com/a.md"`, `href="#part"`, "<table>"} {
if !strings.Contains(got, want) {
t.Errorf("output missing %q:\n%s", want, got)
}
}
- if strings.Contains(got, "<h1") || strings.Contains(got, "<script>") {
- t.Errorf("H1 duplicated or raw HTML rendered:\n%s", got)
+ if strings.Index(got, "<blockquote>") > strings.Index(got, "<h1") {
+ t.Errorf("sections rendered out of source order:\n%s", got)
+ }
+ if strings.Contains(got, "<script>") {
+ t.Errorf("raw HTML rendered:\n%s", got)
}
s := &site{root: root, pages: map[string][]*Page{}}
@@ -75,7 +80,7 @@ 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) != 3 {
+ if page == nil || page.Meta["summary"] != "hello" || len(page.Sections) != 5 {
t.Fatalf("unexpected page model: %#v", page)
}
}
@@ -350,10 +355,14 @@ func TestGitignoreTracksOnlyCurrentGeneratedOutputs(t *testing.T) {
func TestSortingFilteringSlicing(t *testing.T) {
a := &Page{Title: "A", Meta: map[string]any{"post_date": "2024-01-01", "kind": "x"}, ModTime: time.Unix(1, 0)}
- b := &Page{Title: "B", Meta: map[string]any{"post_date": "2025-01-01", "kind": "y"}, ModTime: time.Unix(2, 0)}
+ b := &Page{Title: "B", Meta: map[string]any{"post_date": "2025-01-01", "kind": "y"}, ModTime: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)}
if got := sortPages([]*Page{a, b}, "post_date"); got[0] != b {
t.Fatal("date sort")
}
+ b.Meta["post_date"] = "invalid"
+ if got := sortPages([]*Page{a, b}, "post_date", "mod_time"); got[0] != b {
+ t.Fatal("date fallback sort")
+ }
if got := filterPages([]*Page{a, b}, "kind", "x"); len(got) != 1 || got[0] != a {
t.Fatal("filter")
}