From 8b511b29e64dea3c5a705d3e8121c36c36516480 Mon Sep 17 00:00:00 2001
From: Gwyn
Date: Sun, 31 Mar 2024 09:30:46 -0600
Subject: [PATCH 01/14] Adds description for new ordering of pages feature.
---
README.md | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 6073bf0..da97a2c 100644
--- a/README.md
+++ b/README.md
@@ -60,7 +60,12 @@ If a page contains a `` element, Andrew picks it up and uses that as the
If the page does not contain a `` element, then Andrew will use the file name of that file as the link name.
## ordering of pages
-In this release, Andrew serves you page links ascii-betically.
+If a page contains the meta element `
+`
+ contentRoot := fstest.MapFS{
+ "b.html": &fstest.MapFile{ModTime: time.Date(2024, time.March, 29, 6, 0, 0, 0, time.UTC)},
+ "a.html": &fstest.MapFile{ModTime: time.Date(2024, time.March, 29, 5, 0, 0, 0, time.UTC)},
+ "index.html": &fstest.MapFile{Data: []byte("{{ .AndrewIndexBody }}")},
+ }
+
+ page := andrew.AndrewPage{Path: path}
+ received := page.GenerateAndrewIndexBody("index.html")
+
+ if expected != received {
+ t.Errorf(cmp.Diff(expected, received))
+ }
+
+}
+
// startAndrewServer starts an andrew and returns the localhost url that you can run http gets against
// to retrieve data from that server
func startAndrewServer(t *testing.T, contentRoot fs.FS) string {
diff --git a/page_parser.go b/page_parser.go
index d47773f..ee706cb 100644
--- a/page_parser.go
+++ b/page_parser.go
@@ -8,6 +8,9 @@ import (
"golang.org/x/net/html"
)
+type AndrewPage struct {
+}
+
// titleFromHTMLTitleElement returns the content of the "title" tag or an empty string.
// The error value "no title element found" is returned if title is not discovered
// or is set to an empty string.
From 3ec4af7247d898863ee1a2381a024d2a606b81c5 Mon Sep 17 00:00:00 2001
From: Gwyn
Date: Fri, 19 Apr 2024 09:00:14 -0600
Subject: [PATCH 03/14] The test that shows whether we are successfully sorting
on pages.
---
andrew_server_test.go | 52 +++++++++++++++++++++++++++++++++++--------
1 file changed, 43 insertions(+), 9 deletions(-)
diff --git a/andrew_server_test.go b/andrew_server_test.go
index 456c717..b574fd9 100644
--- a/andrew_server_test.go
+++ b/andrew_server_test.go
@@ -174,10 +174,10 @@ func TestGettingADirectoryDefaultsToIndexHtml(t *testing.T) {
`)
+ // fstest.MapFS does not create directory-like objects, so we need a real file system in this test.
contentRoot := t.TempDir()
os.MkdirAll(contentRoot+"/pages", 0o755)
- // fstest.MapFS does not create directory-like objects, so we need a real file system in this test.
err := os.WriteFile(contentRoot+"/pages/index.html", expected, 0o755)
if err != nil {
t.Fatal(err)
@@ -292,7 +292,6 @@ func TestAndrewIndexBodyIsGeneratedCorrectlyInAChildDirectory(t *testing.T) {
}
testUrl := startAndrewServer(t, contentRoot)
-
resp, err := http.Get(testUrl + "/parentDir/index.html")
if err != nil {
@@ -305,6 +304,9 @@ func TestAndrewIndexBodyIsGeneratedCorrectlyInAChildDirectory(t *testing.T) {
t.Fatal(err)
}
+ // The test is displaying parentDir/childDir/1-2-3.html as its link; this is because generateAndrewIndexBody now returns AndrewPages,
+ // and the link that these maintain internally is their URL. Instead of the URL, we need a link path.
+ // GetSiblingsAndChildren maintains a localContentRoot variable that contains the directory we are residing within
expectedIndex := `
@@ -439,21 +441,53 @@ func TestMainCalledWithInvalidAddressPanics(t *testing.T) {
}
+// TestArticlesInAndrewIndexBodyAreDefaultSortedByModTime is verifying that
+// when the list of links andrew generates for the {{.AndrewIndexBody}} are
+// sorted by mtime, not using the ascii sorting order.
func TestArticlesInAndrewIndexBodyAreDefaultSortedByModTime(t *testing.T) {
expected := `
`
- contentRoot := fstest.MapFS{
- "b.html": &fstest.MapFile{ModTime: time.Date(2024, time.March, 29, 6, 0, 0, 0, time.UTC)},
- "a.html": &fstest.MapFile{ModTime: time.Date(2024, time.March, 29, 5, 0, 0, 0, time.UTC)},
- "index.html": &fstest.MapFile{Data: []byte("{{ .AndrewIndexBody }}")},
+
+ contentRoot := t.TempDir()
+
+ // fstest.MapFS does not enforce file permissions, so we need a real file system in this test.
+ // above might be wrong
+ err := os.WriteFile(contentRoot+"/index.html", []byte("{{.AndrewIndexBody}}"), 0o700)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = os.WriteFile(contentRoot+"/a.html", []byte{}, 0o700)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = os.WriteFile(contentRoot+"/b.html", []byte{}, 0o700)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // This test requires having two files which are in one order when sorted
+ // ascii-betically and in another order by date time, so that we can tell
+ // what file attribute andrew is actually sorting on.
+ now := time.Now()
+ older := now.Add(-10 * time.Minute)
+
+ os.Chtimes(contentRoot+"/a.html", now, now)
+ os.Chtimes(contentRoot+"/b.html", older, older)
+
+ server := andrew.AndrewServer{SiteFiles: os.DirFS(contentRoot)}
+ page, err := andrew.NewPage(server, "index.html")
+
+ if err != nil {
+ t.Fatal(err)
}
- page := andrew.NewPage(contentRoot, "index.html")
- received := page.GenerateAndrewIndexBody()
+ received := page.Content
- if expected != received {
+ if expected != string(received) {
t.Errorf(cmp.Diff(expected, received))
}
From 96432b75072d26c5467bb036426415c24ad464bf Mon Sep 17 00:00:00 2001
From: Gwyn
Date: Tue, 23 Apr 2024 22:03:04 -0600
Subject: [PATCH 04/14] Starting to prod at the meta tag
---
README.md | 11 +-
andrew_server.go | 13 +-
andrew_server_test.go | 2 +-
page.go | 77 ++--
page.html | 984 ++++++++++++++++++++++++++++++++++++++++++
5 files changed, 1051 insertions(+), 36 deletions(-)
create mode 100644 page.html
diff --git a/README.md b/README.md
index e79138f..81c97c7 100644
--- a/README.md
+++ b/README.md
@@ -59,8 +59,17 @@ if fanfics/index.html contains `{{ .AndrewIndexBody }}`, that'll be replaced wit
If a page contains a `` element, Andrew picks it up and uses that as the name of a link.
If the page does not contain a `` element, then Andrew will use the file name of that file as the link name.
+## meta elements
+Andrew parses meta tags and makes them accessible on its AndrewPage object.
+
+For a meta element to be picked up, it must be formatted with andrew- prepending the meta element's name, like this ``
+
+### valid meta elements
+
+
+
## ordering of pages
-If a page contains the meta element `` in its ``, Andrew orders on these tags.
If the page does not contain the meta element, it uses the mtime of the file to try and determine ordering. This means that if you edit a page
that does not contain the `andrew-created-on` element, then you will push it back to the top of the list.
diff --git a/andrew_server.go b/andrew_server.go
index 759335f..46ea799 100644
--- a/andrew_server.go
+++ b/andrew_server.go
@@ -19,23 +19,24 @@ type AndrewServer struct {
SiteFiles fs.FS //The files being served
BaseUrl string //The URL used in any links generated for this website that should contain the hostname.
Address string //IpAddress:Port combo to be served on.
- andrewindexbodytemplate string //The string we're searching for inside a Page that should be replaced with a template. Mightn't belong in the Server.
+ Andrewindexbodytemplate string //The string we're searching for inside a Page that should be replaced with a template. Mightn't belong in the Server.
}
const (
- DefaultContentRoot = "."
- DefaultAddress = ":8080"
- DefaultBaseUrl = "http://localhost:8080"
+ AndrewIndexBodyTemplate = "AndrewIndexBody"
+ DefaultContentRoot = "."
+ DefaultAddress = ":8080"
+ DefaultBaseUrl = "http://localhost:8080"
)
func NewAndrewServer(contentRoot fs.FS, address string, baseUrl string) (AndrewServer, error) {
- return AndrewServer{SiteFiles: contentRoot, andrewindexbodytemplate: "AndrewIndexBody", Address: address, BaseUrl: baseUrl}, nil
+ return AndrewServer{SiteFiles: contentRoot, Andrewindexbodytemplate: "AndrewIndexBody", Address: address, BaseUrl: baseUrl}, nil
}
func Main(args []string, printDest io.Writer) int {
help := `Usage: andrew [contentRoot] [address] [baseUrl]
- contentRoot: The root directory of your content. Defaults to '.' if not specified.
- - address: The address to bind to. Defaults to 'localhost:8080' if not specified. If in doubt, you probably want 0.0.0.0:
+ - address: The address to bind to. Defaults to 'localhost:8080' if not specified. If in doubt, you probably want '0.0.0.0:'
- base URL: The protocol://hostname for your server. Defaults to 'http://localhost:8080' if not specified. Used to generate sitemap/rss feed accurately.
-h, --help: Display this help message.
diff --git a/andrew_server_test.go b/andrew_server_test.go
index b574fd9..ed424b1 100644
--- a/andrew_server_test.go
+++ b/andrew_server_test.go
@@ -478,7 +478,7 @@ func TestArticlesInAndrewIndexBodyAreDefaultSortedByModTime(t *testing.T) {
os.Chtimes(contentRoot+"/a.html", now, now)
os.Chtimes(contentRoot+"/b.html", older, older)
- server := andrew.AndrewServer{SiteFiles: os.DirFS(contentRoot)}
+ server := andrew.AndrewServer{SiteFiles: os.DirFS(contentRoot), Andrewindexbodytemplate: andrew.AndrewIndexBodyTemplate}
page, err := andrew.NewPage(server, "index.html")
if err != nil {
diff --git a/page.go b/page.go
index db96184..ca2f286 100644
--- a/page.go
+++ b/page.go
@@ -16,8 +16,8 @@ type AndrewPage struct {
Title string
// According to https://datatracker.ietf.org/doc/html/rfc1738#section-3.1, the subsection of a
// URL after the procol://hostname is the UrlPath.
- UrlPath string
- //
+ UrlPath string
+ Meta []string
Content string
PublishTime time.Time
}
@@ -57,6 +57,8 @@ func NewPage(server AndrewServer, pageUrl string) (AndrewPage, error) {
}
}
+ // pageMeta := getMeta(pagePath, pageContent)
+
return AndrewPage{Content: string(pageContent), UrlPath: pageUrl, Title: pageTitle}, nil
}
@@ -102,7 +104,7 @@ func buildAndrewIndexBody(server AndrewServer, startingPageUrl string, pageConte
panic(err)
}
- err = t.Execute(&templateBuffer, map[string]string{server.andrewindexbodytemplate: links.String()})
+ err = t.Execute(&templateBuffer, map[string]string{server.Andrewindexbodytemplate: links.String()})
if err != nil {
//TODO: swap this for proper error handling
@@ -121,39 +123,41 @@ func buildAndrewIndexLink(page AndrewPage, cssIdNumber int) []byte {
return b
}
-// titleFromHTMLTitleElement returns the content of the "title" tag or an empty string.
-// The error value "no title element found" is returned if title is not discovered
-// or is set to an empty string.
-func titleFromHTMLTitleElement(fileContent []byte) (string, error) {
-
- doc, err := html.Parse(bytes.NewReader(fileContent))
- if err != nil {
- return "", err
+// getAttribute recursively descends an html node tree, searching for
+// the attribute provided. Once the attribute is discovered, it returns.
+func getAttributes(attribute string, n *html.Node) []string {
+ var attributes []string
+
+ //n.Type no longer matches html.ElementNode; n is now a document, not a node
+ if n.Type == html.ElementNode {
+ for _, a := range n.Attr {
+ if a.Key == attribute {
+ attributes = append(attributes, a.Val)
+ }
+ }
}
- title := getAttribute("title", doc)
- if title == "" {
- return "", fmt.Errorf("no title element found")
+ for c := n.FirstChild; c != nil; c = c.NextSibling {
+ attributes = append(attributes, getAttributes(attribute, c)...)
}
- return title, nil
+
+ return attributes
}
-// getAttribute recursively descends an html node tree, searching for
-// the attribute provided. Once the attribute is discovered, it returns.
-func getAttribute(attribute string, n *html.Node) string {
- if n.Type == html.ElementNode && n.Data == attribute {
- if n.FirstChild != nil {
- return n.FirstChild.Data
- }
+func getMeta(htmlContent []byte) ([]string, error) {
+ element := "meta"
+
+ doc, err := html.Parse(bytes.NewReader(htmlContent))
+ if err != nil {
+ return []string{}, err
}
- for c := n.FirstChild; c != nil; c = c.NextSibling {
- result := getAttribute(attribute, c)
- if result != "" {
- return result
- }
+ meta := getAttributes(element, doc)
+
+ if len(meta) == 0 {
+ return meta, fmt.Errorf("no %s element found", element)
}
- return ""
+ return meta, nil
}
func getTitle(htmlFilePath string, htmlContent []byte) (string, error) {
@@ -168,3 +172,20 @@ func getTitle(htmlFilePath string, htmlContent []byte) (string, error) {
}
return title, nil
}
+
+// titleFromHTMLTitleElement returns the content of the "title" tag or an empty string.
+// The error value "no title element found" is returned if title is not discovered
+// or is set to an empty string.
+func titleFromHTMLTitleElement(fileContent []byte) (string, error) {
+
+ doc, err := html.Parse(bytes.NewReader(fileContent))
+ if err != nil {
+ return "", err
+ }
+
+ title := getAttributes("title", doc)
+ if len(title) == 0 {
+ return "", fmt.Errorf("no title element found")
+ }
+ return title[0], nil
+}
diff --git a/page.html b/page.html
new file mode 100644
index 0000000..c9f0c8f
--- /dev/null
+++ b/page.html
@@ -0,0 +1,984 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+April 22nd - 27th Programming
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
From 1e62cc359992a1a553644a8d28eb8ef1ab7ecc41 Mon Sep 17 00:00:00 2001
From: Gwyn
Date: Sun, 12 May 2024 16:55:02 -0600
Subject: [PATCH 05/14] Starting to parse meta
---
page.go | 24 ++++++++++++------------
page_test.go | 4 ++++
2 files changed, 16 insertions(+), 12 deletions(-)
diff --git a/page.go b/page.go
index 93e6167..d878b7b 100644
--- a/page.go
+++ b/page.go
@@ -138,21 +138,21 @@ func getAttributes(attribute string, n *html.Node) []string {
return attributes
}
-// func getMeta(htmlContent []byte) ([]string, error) {
-// element := "meta"
+func getMeta(htmlContent []byte) ([]string, error) {
+ element := "meta"
-// doc, err := html.Parse(bytes.NewReader(htmlContent))
-// if err != nil {
-// return []string{}, err
-// }
+ doc, err := html.Parse(bytes.NewReader(htmlContent))
+ if err != nil {
+ return []string{}, err
+ }
-// meta := getAttributes(element, doc)
+ meta := getAttributes(element, doc)
-// if len(meta) == 0 {
-// return meta, fmt.Errorf("no %s element found", element)
-// }
-// return meta, nil
-// }
+ if len(meta) == 0 {
+ return meta, fmt.Errorf("no %s element found", element)
+ }
+ return meta, nil
+}
func getTitle(htmlFilePath string, htmlContent []byte) (string, error) {
title, err := titleFromHTMLTitleElement(htmlContent)
diff --git a/page_test.go b/page_test.go
index 131f139..4969830 100644
--- a/page_test.go
+++ b/page_test.go
@@ -38,3 +38,7 @@ func TestGetTitleReturnsPageFileNameWhenNoTitleInDocument(t *testing.T) {
t.Fatal(cmp.Diff("", received))
}
}
+
+func TestMetaIsPopulatedWithExpectedElements(t *testing.T) {
+
+}
From e5f73af70a0ea7b649dcd0836d1ffd4bbcc7e286 Mon Sep 17 00:00:00 2001
From: Gwyn
Date: Sat, 1 Jun 2024 21:01:10 -0600
Subject: [PATCH 06/14] Adds an architecture document for new devs to guide
themselves with
---
ARCHITECTURE.md | 15 +++++++++++++++
1 file changed, 15 insertions(+)
create mode 100644 ARCHITECTURE.md
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
new file mode 100644
index 0000000..906a7e1
--- /dev/null
+++ b/ARCHITECTURE.md
@@ -0,0 +1,15 @@
+Andrew has two primary concepts: the Server and the Page.
+
+The Server owns:
+* answering questions about the layout of files in the directory structure
+* understanding the kinds of file that are being served
+* serving those files
+If you are answering a question about files, the Server's got the answers. The server creates Pages
+and serves those Pages.
+
+
+The Page owns the content and metadata.
+
+Page tracks the content of a specific file and various pieces of metadata about it.
+
+For example, the page parses the contents of a file and parses out the andrew metadata headers, so that when the Server wants to present those elements to an end-user they're already built.
\ No newline at end of file
From 3d7348bd46bf9e94d91ce441497a44f218401bea Mon Sep 17 00:00:00 2001
From: Gwyn
Date: Tue, 4 Jun 2024 21:18:41 -0600
Subject: [PATCH 07/14] Refactors getAttributes to be more accurately named as
getTags.
Also fixes the implementation so it's pulling back tags now.
---
page.go | 39 +++++++++++++++++++++++----------------
1 file changed, 23 insertions(+), 16 deletions(-)
diff --git a/page.go b/page.go
index d878b7b..17b9040 100644
--- a/page.go
+++ b/page.go
@@ -117,28 +117,35 @@ func buildAndrewIndexLink(page Page, cssIdNumber int) []byte {
return b
}
-// getAttribute recursively descends an html node tree, searching for
+// getTags recursively descends an html node tree, searching for
// the attribute provided. Once the attribute is discovered, it returns.
-func getAttributes(attribute string, n *html.Node) []string {
- var attributes []string
-
- //n.Type no longer matches html.ElementNode; n is now a document, not a node
- if n.Type == html.ElementNode {
- for _, a := range n.Attr {
- if a.Key == attribute {
- attributes = append(attributes, a.Val)
+func getTags(attribute string, n *html.Node) []string {
+ var tags []string
+
+ // getTag recursively descends an html node tree, searching for
+ // the attribute provided. Once the attribute is discovered, it appends to attributes.
+ var getTag func(n *html.Node)
+
+ getTag = func(n *html.Node) {
+ if n.Type == html.ElementNode && n.Data == attribute {
+ if n.FirstChild != nil {
+ tags = append(tags, n.FirstChild.Data)
+ return
}
}
- }
- for c := n.FirstChild; c != nil; c = c.NextSibling {
- attributes = append(attributes, getAttributes(attribute, c)...)
+ for c := n.FirstChild; c != nil; c = c.NextSibling {
+ getTag(c)
+ }
}
- return attributes
+ // Start the recursion from the root node
+ getTag(n)
+
+ return tags
}
-func getMeta(htmlContent []byte) ([]string, error) {
+func GetMetaElements(htmlContent []byte) ([]string, error) {
element := "meta"
doc, err := html.Parse(bytes.NewReader(htmlContent))
@@ -146,7 +153,7 @@ func getMeta(htmlContent []byte) ([]string, error) {
return []string{}, err
}
- meta := getAttributes(element, doc)
+ meta := getTags(element, doc)
if len(meta) == 0 {
return meta, fmt.Errorf("no %s element found", element)
@@ -176,7 +183,7 @@ func titleFromHTMLTitleElement(fileContent []byte) (string, error) {
return "", err
}
- title := getAttributes("title", doc)
+ title := getTags("title", doc)
if len(title) == 0 {
return "", fmt.Errorf("no title element found")
}
From 5dcf2ac6ac83fd72f5c7c4d43bb227c5db86f90c Mon Sep 17 00:00:00 2001
From: Gwyn
Date: Mon, 10 Jun 2024 20:00:29 -0600
Subject: [PATCH 08/14] Progress: the test now fails with a recognisable data
structure.
---
andrew_server_test.go | 72 +++++++++++++++++++++----------------------
page.go | 26 +++++++++++-----
page_test.go | 25 ++++++++++++++-
3 files changed, 78 insertions(+), 45 deletions(-)
diff --git a/andrew_server_test.go b/andrew_server_test.go
index a5d02a3..cc569f1 100644
--- a/andrew_server_test.go
+++ b/andrew_server_test.go
@@ -426,54 +426,54 @@ func TestMainCalledWithInvalidAddressPanics(t *testing.T) {
// TestArticlesInAndrewIndexBodyAreDefaultSortedByModTime is verifying that
// when the list of links andrew generates for the {{.AndrewIndexBody}} are
// sorted by mtime, not using the ascii sorting order.
-func TestArticlesInAndrewIndexBodyAreDefaultSortedByModTime(t *testing.T) {
+// func TestArticlesInAndrewIndexBodyAreDefaultSortedByModTime(t *testing.T) {
- expected := `
-
-`
+// expected := `
+//
+// `
- contentRoot := t.TempDir()
+// contentRoot := t.TempDir()
- // fstest.MapFS does not enforce file permissions, so we need a real file system in this test.
- // above might be wrong
- err := os.WriteFile(contentRoot+"/index.html", []byte("{{.AndrewIndexBody}}"), 0o700)
- if err != nil {
- t.Fatal(err)
- }
+// // fstest.MapFS does not enforce file permissions, so we need a real file system in this test.
+// // above might be wrong
+// err := os.WriteFile(contentRoot+"/index.html", []byte("{{.AndrewIndexBody}}"), 0o700)
+// if err != nil {
+// t.Fatal(err)
+// }
- err = os.WriteFile(contentRoot+"/a.html", []byte{}, 0o700)
- if err != nil {
- t.Fatal(err)
- }
+// err = os.WriteFile(contentRoot+"/a.html", []byte{}, 0o700)
+// if err != nil {
+// t.Fatal(err)
+// }
- err = os.WriteFile(contentRoot+"/b.html", []byte{}, 0o700)
- if err != nil {
- t.Fatal(err)
- }
+// err = os.WriteFile(contentRoot+"/b.html", []byte{}, 0o700)
+// if err != nil {
+// t.Fatal(err)
+// }
- // This test requires having two files which are in one order when sorted
- // ascii-betically and in another order by date time, so that we can tell
- // what file attribute andrew is actually sorting on.
- now := time.Now()
- older := now.Add(-10 * time.Minute)
+// // This test requires having two files which are in one order when sorted
+// // ascii-betically and in another order by date time, so that we can tell
+// // what file attribute andrew is actually sorting on.
+// now := time.Now()
+// older := now.Add(-10 * time.Minute)
- os.Chtimes(contentRoot+"/a.html", now, now)
- os.Chtimes(contentRoot+"/b.html", older, older)
+// os.Chtimes(contentRoot+"/a.html", now, now)
+// os.Chtimes(contentRoot+"/b.html", older, older)
- server := andrew.Server{SiteFiles: os.DirFS(contentRoot), Andrewindexbodytemplate: andrew.AndrewIndexBodyTemplate}
- page, err := andrew.NewPage(server, "index.html")
+// server := andrew.Server{SiteFiles: os.DirFS(contentRoot), Andrewindexbodytemplate: andrew.AndrewIndexBodyTemplate}
+// page, err := andrew.NewPage(server, "index.html")
- if err != nil {
- t.Fatal(err)
- }
+// if err != nil {
+// t.Fatal(err)
+// }
- received := page.Content
+// received := page.Content
- if expected != string(received) {
- t.Errorf(cmp.Diff(expected, received))
- }
+// if expected != string(received) {
+// t.Errorf(cmp.Diff(expected, received))
+// }
-}
+// }
// newTestAndrewServer starts an andrew and returns the localhost url that you can run http gets against
// to retrieve data from that server
diff --git a/page.go b/page.go
index 17b9040..9c6be51 100644
--- a/page.go
+++ b/page.go
@@ -117,21 +117,31 @@ func buildAndrewIndexLink(page Page, cssIdNumber int) []byte {
return b
}
-// getTags recursively descends an html node tree, searching for
-// the attribute provided. Once the attribute is discovered, it returns.
-func getTags(attribute string, n *html.Node) []string {
- var tags []string
+// getTags recursively descends an html node tree for the requested tag,
+// searching both data and attributes to find information about the node that's requested.
+func getTags(tag string, n *html.Node) []string {
+ var tagContent []string
// getTag recursively descends an html node tree, searching for
// the attribute provided. Once the attribute is discovered, it appends to attributes.
var getTag func(n *html.Node)
getTag = func(n *html.Node) {
- if n.Type == html.ElementNode && n.Data == attribute {
+ if n.Type == html.ElementNode && n.Data == tag {
+
+ if n.Attr != nil {
+ for _, attribute := range n.Attr {
+ tagContent = append(tagContent, attribute.Key)
+ tagContent = append(tagContent, attribute.Val)
+ }
+ }
+
if n.FirstChild != nil {
- tags = append(tags, n.FirstChild.Data)
- return
+ //Tag attributes and tag Data are being conflated here.
+ tagContent = append(tagContent, n.FirstChild.Data)
}
+
+ return
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
@@ -142,7 +152,7 @@ func getTags(attribute string, n *html.Node) []string {
// Start the recursion from the root node
getTag(n)
- return tags
+ return tagContent
}
func GetMetaElements(htmlContent []byte) ([]string, error) {
diff --git a/page_test.go b/page_test.go
index 4969830..f3dd3e9 100644
--- a/page_test.go
+++ b/page_test.go
@@ -1,6 +1,7 @@
package andrew
import (
+ "slices"
"testing"
"github.com/google/go-cmp/cmp"
@@ -39,6 +40,28 @@ func TestGetTitleReturnsPageFileNameWhenNoTitleInDocument(t *testing.T) {
}
}
-func TestMetaIsPopulatedWithExpectedElements(t *testing.T) {
+func TestMetaPopulatesATag(t *testing.T) {
+ expected := []string{"andrew-created-at 2025-03-01"}
+ received, err := GetMetaElements([]byte(""))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if !slices.Equal(expected, received) {
+ t.Fatal(cmp.Diff(expected, received))
+ }
}
+
+// func TestMetaIsPopulatedWithExpectedElements(t *testing.T) {
+// expected := map[string]string{"andrew-created-at": "2025-03-01"}
+// received, err := GetMetaElements([]byte(""))
+
+// if err != nil {
+// t.Fatal(err)
+// }
+
+// if received != expected {
+// t.Fatal(cmp.Diff(expected, received))
+// }
+// }
From d08d0f7aea59a5aafc54b5104a9e9cfc9afc0f2d Mon Sep 17 00:00:00 2001
From: Gwyn
Date: Wed, 12 Jun 2024 06:37:19 -0600
Subject: [PATCH 09/14] Correctly parsing the meta tags as a map now.
---
page.go | 58 +++++++++++++++++++++++++++++++++++-----------------
page_test.go | 28 ++++++++++++-------------
2 files changed, 53 insertions(+), 33 deletions(-)
diff --git a/page.go b/page.go
index 9c6be51..e27f681 100644
--- a/page.go
+++ b/page.go
@@ -33,6 +33,11 @@ type Page struct {
PublishTime time.Time
}
+type TagInfo struct {
+ Data string
+ Attributes map[string]string
+}
+
// NewPage creates a Page from a URL by reading the corresponding file from the
// AndrewServer's SiteFiles.
func NewPage(server Server, pageUrl string) (Page, error) {
@@ -117,28 +122,42 @@ func buildAndrewIndexLink(page Page, cssIdNumber int) []byte {
return b
}
-// getTags recursively descends an html node tree for the requested tag,
+// getTagInfo recursively descends an html node tree for the requested tag,
+// searching both data and attributes to find information about the node that's requested.
+// getTagInfo recursively descends an html node tree for the requested tag,
// searching both data and attributes to find information about the node that's requested.
-func getTags(tag string, n *html.Node) []string {
- var tagContent []string
+func getTagInfo(tag string, n *html.Node) TagInfo {
+ var tagDataAndAttributes TagInfo = TagInfo{
+ Data: "",
+ Attributes: make(map[string]string),
+ }
// getTag recursively descends an html node tree, searching for
- // the attribute provided. Once the attribute is discovered, it appends to attributes.
+ // the attribute provided. Once the attribute is discovered, it first checks
+ // for any Attributes available on the html node. If there are no Attributes,
+ // the key won't exist in the tagDataAndAttributes map.
+ // If there is data, it will append to attributes.
var getTag func(n *html.Node)
getTag = func(n *html.Node) {
if n.Type == html.ElementNode && n.Data == tag {
+ a := ""
+ b := ""
if n.Attr != nil {
- for _, attribute := range n.Attr {
- tagContent = append(tagContent, attribute.Key)
- tagContent = append(tagContent, attribute.Val)
+ for _, attr := range n.Attr {
+ switch attr.Key {
+ case "content":
+ b = attr.Val
+ case "name":
+ a = attr.Val
+ }
+ tagDataAndAttributes.Attributes[a] = b
}
}
if n.FirstChild != nil {
- //Tag attributes and tag Data are being conflated here.
- tagContent = append(tagContent, n.FirstChild.Data)
+ tagDataAndAttributes.Data = n.FirstChild.Data
}
return
@@ -152,23 +171,24 @@ func getTags(tag string, n *html.Node) []string {
// Start the recursion from the root node
getTag(n)
- return tagContent
+ return tagDataAndAttributes
}
-func GetMetaElements(htmlContent []byte) ([]string, error) {
+func GetMetaElements(htmlContent []byte) (map[string]string, error) {
element := "meta"
doc, err := html.Parse(bytes.NewReader(htmlContent))
if err != nil {
- return []string{}, err
+ return map[string]string{}, err
}
- meta := getTags(element, doc)
+ tagInfo := getTagInfo(element, doc)
- if len(meta) == 0 {
- return meta, fmt.Errorf("no %s element found", element)
+ if len(tagInfo.Attributes) == 0 {
+ return tagInfo.Attributes, fmt.Errorf("no %s element found", element)
}
- return meta, nil
+
+ return tagInfo.Attributes, nil
}
func getTitle(htmlFilePath string, htmlContent []byte) (string, error) {
@@ -193,9 +213,9 @@ func titleFromHTMLTitleElement(fileContent []byte) (string, error) {
return "", err
}
- title := getTags("title", doc)
- if len(title) == 0 {
+ tagInfo := getTagInfo("title", doc)
+ if len(tagInfo.Data) == 0 {
return "", fmt.Errorf("no title element found")
}
- return title[0], nil
+ return tagInfo.Data, nil
}
diff --git a/page_test.go b/page_test.go
index f3dd3e9..786089b 100644
--- a/page_test.go
+++ b/page_test.go
@@ -1,7 +1,7 @@
package andrew
import (
- "slices"
+ "maps"
"testing"
"github.com/google/go-cmp/cmp"
@@ -40,28 +40,28 @@ func TestGetTitleReturnsPageFileNameWhenNoTitleInDocument(t *testing.T) {
}
}
-func TestMetaPopulatesATag(t *testing.T) {
- expected := []string{"andrew-created-at 2025-03-01"}
+func TestOneMetaTagPopulatesATag(t *testing.T) {
+ expected := map[string]string{"andrew-created-at": "2025-03-01"}
received, err := GetMetaElements([]byte(""))
if err != nil {
t.Fatal(err)
}
- if !slices.Equal(expected, received) {
+ if !maps.Equal(expected, received) {
t.Fatal(cmp.Diff(expected, received))
}
}
-// func TestMetaIsPopulatedWithExpectedElements(t *testing.T) {
-// expected := map[string]string{"andrew-created-at": "2025-03-01"}
-// received, err := GetMetaElements([]byte(""))
+func TestMultipleMetaTagsPopulatedWithExpectedElements(t *testing.T) {
+ expected := map[string]string{"andrew-created-at": "2025-03-01", "andrew-roflcopter": "hippolol"}
+ received, err := GetMetaElements([]byte(" "))
-// if err != nil {
-// t.Fatal(err)
-// }
+ if err != nil {
+ t.Fatal(err)
+ }
-// if received != expected {
-// t.Fatal(cmp.Diff(expected, received))
-// }
-// }
+ if !maps.Equal(expected, received) {
+ t.Fatal(cmp.Diff(expected, received))
+ }
+}
From 33920f5f535ce9af80ddeb7b940f7e3ae74789bd Mon Sep 17 00:00:00 2001
From: Gwyn
Date: Tue, 25 Jun 2024 08:07:04 -0600
Subject: [PATCH 10/14] Adds a page PublishTime
Extracts a little server functionality into a new linksbuilder file,
just to keep it cleaner, no other reason.
Uncomments the test for link ordering. It fails this moment, but one
step at a time.
---
andrew_server.go | 34 +-
andrew_server_test.go | 77 ++--
linksbuilder.go | 49 +++
page.go | 105 ++---
page.html | 984 ------------------------------------------
5 files changed, 143 insertions(+), 1106 deletions(-)
create mode 100644 linksbuilder.go
delete mode 100644 page.html
diff --git a/andrew_server.go b/andrew_server.go
index aa8ee09..48e0666 100644
--- a/andrew_server.go
+++ b/andrew_server.go
@@ -146,11 +146,8 @@ func CheckPageErrors(err error) (string, int) {
// GetSiblingsAndChildren accepts a path to a file and a filter function.
// It infers the directory that the file resides within, and then recurses the Server's fs.FS
// to return all of the files both in the same directory and further down in the directory structure.
-// To filter these down to only files that you care about, pass in a filter function.
-// The filter is called in the context of fs.WalkDir. It is handed fs.WalkDir's path and directory entry,
-// in that order, and is expected to return a boolean false.
-// If that error is nil then the current file being evaluated is skipped for consideration.
-func (a Server) GetSiblingsAndChildren(pagePath string, filter func(string, fs.DirEntry) bool) ([]Page, error) {
+func (a Server) GetSiblingsAndChildren(pagePath string) ([]Page, error) {
+
pages := []Page{}
localContentRoot := path.Dir(pagePath)
@@ -159,19 +156,28 @@ func (a Server) GetSiblingsAndChildren(pagePath string, filter func(string, fs.D
return err
}
- if filter(path, d) {
- // links require a URL relative to the page we're discovering siblings from, not from
- // the root of the file system
- page, err := NewPage(a, path)
- page = page.SetUrlPath(strings.TrimPrefix(path, localContentRoot+"/"))
+ // We don't list index files in our collection of siblings and children, because I don't
+ // want a link back to a page that contains only links.
+ if strings.Contains(path, "index.html") {
+ return nil
+ }
+
+ // If the file we're considering isn't an html file, let's move on with our day.
+ if !strings.Contains(path, "html") {
+ return nil
+ }
- if err != nil {
- return err
- }
+ // links require a URL relative to the page we're discovering siblings from, not from
+ // the root of the file system
+ page, err := NewPage(a, path)
+ page = page.SetUrlPath(strings.TrimPrefix(path, localContentRoot+"/"))
- pages = append(pages, page)
+ if err != nil {
+ return err
}
+ pages = append(pages, page)
+
return nil
})
diff --git a/andrew_server_test.go b/andrew_server_test.go
index cc569f1..ebbc183 100644
--- a/andrew_server_test.go
+++ b/andrew_server_test.go
@@ -291,9 +291,6 @@ func TestAndrewIndexBodyIsGeneratedCorrectlyInAChildDirectory(t *testing.T) {
t.Fatal(err)
}
- // The test is displaying parentDir/childDir/1-2-3.html as its link; this is because generateAndrewIndexBody now returns AndrewPages,
- // and the link that these maintain internally is their URL. Instead of the URL, we need a link path.
- // GetSiblingsAndChildren maintains a localContentRoot variable that contains the directory we are residing within
expectedIndex := `
@@ -426,54 +423,54 @@ func TestMainCalledWithInvalidAddressPanics(t *testing.T) {
// TestArticlesInAndrewIndexBodyAreDefaultSortedByModTime is verifying that
// when the list of links andrew generates for the {{.AndrewIndexBody}} are
// sorted by mtime, not using the ascii sorting order.
-// func TestArticlesInAndrewIndexBodyAreDefaultSortedByModTime(t *testing.T) {
+// This test requires having two files which are in one order when sorted
+// ascii-betically and in another order by date time, so that we can tell
+// what file attribute andrew is actually sorting on.
+func TestArticlesInAndrewIndexBodyAreDefaultSortedByModTime(t *testing.T) {
+ expected := `b_newer.html
+a_older.html
+`
-// expected := `
-//
-// `
+ contentRoot := t.TempDir()
-// contentRoot := t.TempDir()
+ // fstest.MapFS does not enforce file permissions, so we need a real file system in this test.
+ // above might be wrong
+ err := os.WriteFile(contentRoot+"/index.html", []byte("{{.AndrewIndexBody}}"), 0o700)
+ if err != nil {
+ t.Fatal(err)
+ }
-// // fstest.MapFS does not enforce file permissions, so we need a real file system in this test.
-// // above might be wrong
-// err := os.WriteFile(contentRoot+"/index.html", []byte("{{.AndrewIndexBody}}"), 0o700)
-// if err != nil {
-// t.Fatal(err)
-// }
+ err = os.WriteFile(contentRoot+"/a_older.html", []byte{}, 0o700)
+ if err != nil {
+ t.Fatal(err)
+ }
-// err = os.WriteFile(contentRoot+"/a.html", []byte{}, 0o700)
-// if err != nil {
-// t.Fatal(err)
-// }
+ err = os.WriteFile(contentRoot+"/b_newer.html", []byte{}, 0o700)
+ if err != nil {
+ t.Fatal(err)
+ }
-// err = os.WriteFile(contentRoot+"/b.html", []byte{}, 0o700)
-// if err != nil {
-// t.Fatal(err)
-// }
+ now := time.Now()
+ older := now.Add(-10 * time.Minute)
-// // This test requires having two files which are in one order when sorted
-// // ascii-betically and in another order by date time, so that we can tell
-// // what file attribute andrew is actually sorting on.
-// now := time.Now()
-// older := now.Add(-10 * time.Minute)
+ os.Chtimes(contentRoot+"/b_newer.html", now, now)
+ os.Chtimes(contentRoot+"/a_older.html", older, older)
-// os.Chtimes(contentRoot+"/a.html", now, now)
-// os.Chtimes(contentRoot+"/b.html", older, older)
+ server := andrew.Server{SiteFiles: os.DirFS(contentRoot), Andrewindexbodytemplate: andrew.AndrewIndexBodyTemplate}
-// server := andrew.Server{SiteFiles: os.DirFS(contentRoot), Andrewindexbodytemplate: andrew.AndrewIndexBodyTemplate}
-// page, err := andrew.NewPage(server, "index.html")
+ page, err := andrew.NewPage(server, "index.html")
-// if err != nil {
-// t.Fatal(err)
-// }
+ if err != nil {
+ t.Fatal(err)
+ }
-// received := page.Content
+ received := page.Content
-// if expected != string(received) {
-// t.Errorf(cmp.Diff(expected, received))
-// }
+ if expected != string(received) {
+ t.Errorf(cmp.Diff(expected, received))
+ }
-// }
+}
// newTestAndrewServer starts an andrew and returns the localhost url that you can run http gets against
// to retrieve data from that server
@@ -516,7 +513,5 @@ func newTestAndrewServer(t *testing.T, contentRoot fs.FS) *andrew.Server {
// Wait for server to be confirmed ready
<-ready
- t.Logf("Running server on %s\n", addr)
-
return server
}
diff --git a/linksbuilder.go b/linksbuilder.go
new file mode 100644
index 0000000..080b2eb
--- /dev/null
+++ b/linksbuilder.go
@@ -0,0 +1,49 @@
+package andrew
+
+import (
+ "bytes"
+ "fmt"
+ "text/template"
+)
+
+// Building the template body requires information from both the Server and the Page.
+// The Server supplies information from the file system, such as the siblings of a page
+// in its directory.
+// The Page supplies information from with the page itself, like the created-at date or
+// the title of the page.
+
+// BuildPageBodyWithLinks receives the path to a file, currently normally an index file.
+// It traverses the file system starting at the directory containing
+// that file, finds all html files that are _not_ index.html files and returns them
+// as a list of html links to those pages.
+func BuildPageBodyWithLinks(siblings []Page, startingPageUrl string, startingPage Page) ([]byte, error) {
+
+ var links bytes.Buffer
+
+ for i, sibling := range siblings {
+ links.Write(buildAndrewIndexLink(sibling.UrlPath, sibling.Title, i))
+ }
+
+ templateBuffer := bytes.Buffer{}
+ // execute template here, write it to something and then return it as the pageContent
+ t, err := template.New(startingPage.UrlPath).Parse(startingPage.Content)
+
+ if err != nil {
+ // TODO: swap this for proper error handling
+ panic(err)
+ }
+
+ err = t.Execute(&templateBuffer, map[string]string{"AndrewIndexBody": links.String()})
+ if err != nil {
+ return templateBuffer.Bytes(), err
+ }
+
+ return templateBuffer.Bytes(), nil
+}
+
+// buildAndrewIndexLink encapsulates the format of the link
+func buildAndrewIndexLink(urlPath string, title string, cssIdNumber int) []byte {
+ link := fmt.Sprintf("%s", fmt.Sprint(cssIdNumber), urlPath, title)
+ b := []byte(link)
+ return b
+}
diff --git a/page.go b/page.go
index e27f681..b0814b9 100644
--- a/page.go
+++ b/page.go
@@ -6,18 +6,11 @@ import (
"io/fs"
"path"
"strings"
- "text/template"
"time"
"golang.org/x/net/html"
)
-const (
- // The index.html has overhead associated with processing its internals, so it gets
- // processed separately from other pages.
- indexIdentifier = "index.html"
-)
-
// Page tracks the content of a specific file and various pieces of metadata about it.
// The Page makes creating links and serving content convenient, as it lets me offload
// the parsing of any elements into a constructor, so that when I need to present those
@@ -28,7 +21,6 @@ type Page struct {
// According to https://datatracker.ietf.org/doc/html/rfc1738#section-3.1, the subsection of a
// URL after the procol://hostname is the UrlPath.
UrlPath string
- Meta []string
Content string
PublishTime time.Time
}
@@ -40,86 +32,69 @@ type TagInfo struct {
// NewPage creates a Page from a URL by reading the corresponding file from the
// AndrewServer's SiteFiles.
+// NewPage does this by reading the page content from disk, then parsing out various
+// metadata that are convenient to have quick access to, such as the page title or the
+// publish time.
func NewPage(server Server, pageUrl string) (Page, error) {
pageContent, err := fs.ReadFile(server.SiteFiles, pageUrl)
if err != nil {
return Page{}, err
}
+ pageInfo, err := fs.Stat(server.SiteFiles, pageUrl)
+ if err != nil {
+ return Page{}, err
+ }
+
// The fs.FS documentation notes that paths should not start with a leading slash.
pagePath := strings.TrimPrefix(pageUrl, "/")
+
pageTitle, err := getTitle(pagePath, pageContent)
if err != nil {
return Page{}, err
}
- if strings.Contains(pageUrl, indexIdentifier) {
- pageContent, err = buildAndrewIndexBody(server, pageUrl, pageContent)
- if err != nil {
- return Page{}, err
- }
- }
-
- // pageMeta := getMeta(pagePath, pageContent)
- return Page{Content: string(pageContent), UrlPath: pageUrl, Title: pageTitle}, nil
-}
+ page := Page{Content: string(pageContent), UrlPath: pageUrl, Title: pageTitle}
-// SetUrlPath updates the UrlPath on a pre-existing Page.
-func (a Page) SetUrlPath(urlPath string) Page {
- return Page{Title: a.Title, Content: a.Content, UrlPath: urlPath}
-}
+ if strings.Contains(pageUrl, "index.html") {
+ siblings, err := server.GetSiblingsAndChildren(page.UrlPath)
-// buildAndrewIndexBody receives the path to a file, currently normally an index file.
-// It traverses the file system starting at the directory containing
-// that file, finds all html files that are _not_ index.html files and returns them
-// as a list of html links to those pages.
-func buildAndrewIndexBody(server Server, startingPageUrl string, pageContent []byte) ([]byte, error) {
- filterIndexFiles := func(path string, d fs.DirEntry) bool {
- if strings.Contains(path, "index.html") {
- return false
+ if err != nil {
+ return page, err
}
- if !strings.Contains(path, "html") {
- return false
+ pageContent, err = BuildPageBodyWithLinks(siblings, pageUrl, page)
+ if err != nil {
+ return Page{}, err
}
- return true
+ page.Content = string(pageContent)
}
- siblings, err := server.GetSiblingsAndChildren(startingPageUrl, filterIndexFiles)
+ meta, err := GetMetaElements(pageContent)
if err != nil {
- return pageContent, err
+ return Page{}, err
}
- var links bytes.Buffer
- cssIdNumber := 0
+ publishTime, ok := meta["andrew-created-at"]
- for _, sibling := range siblings {
- links.Write(buildAndrewIndexLink(sibling, cssIdNumber))
- cssIdNumber = cssIdNumber + 1
- }
+ if ok {
+ page.PublishTime, err = time.Parse(time.DateOnly, publishTime)
- templateBuffer := bytes.Buffer{}
- // execute template here, write it to something and then return it as the pageContent
- t, err := template.New(startingPageUrl).Parse(string(pageContent))
- if err != nil {
- // TODO: swap this for proper error handling
- panic(err)
+ if err != nil {
+ // log.Logger("could not parse meta tag andrew-created-at using time.Parse. Defaulting to mod time")
+ page.PublishTime = pageInfo.ModTime()
+ }
+ } else {
+ page.PublishTime = pageInfo.ModTime()
}
- err = t.Execute(&templateBuffer, map[string]string{server.Andrewindexbodytemplate: links.String()})
- if err != nil {
- // TODO: swap this for proper error handling
- panic(err)
- }
- return templateBuffer.Bytes(), nil
+ return page, nil
}
-// buildAndrewIndexLink encapsulates the format of the link
-func buildAndrewIndexLink(page Page, cssIdNumber int) []byte {
- link := fmt.Sprintf("%s", fmt.Sprint(cssIdNumber), page.UrlPath, page.Title)
- b := []byte(link)
- return b
+// SetUrlPath updates the UrlPath on a pre-existing Page.
+func (a Page) SetUrlPath(urlPath string) Page {
+ return Page{Title: a.Title, Content: a.Content, UrlPath: urlPath}
}
// getTagInfo recursively descends an html node tree for the requested tag,
@@ -141,18 +116,18 @@ func getTagInfo(tag string, n *html.Node) TagInfo {
getTag = func(n *html.Node) {
if n.Type == html.ElementNode && n.Data == tag {
- a := ""
- b := ""
+ attrName := ""
+ attrVal := ""
if n.Attr != nil {
for _, attr := range n.Attr {
switch attr.Key {
case "content":
- b = attr.Val
+ attrVal = attr.Val
case "name":
- a = attr.Val
+ attrName = attr.Val
}
- tagDataAndAttributes.Attributes[a] = b
+ tagDataAndAttributes.Attributes[attrName] = attrVal
}
}
@@ -184,10 +159,6 @@ func GetMetaElements(htmlContent []byte) (map[string]string, error) {
tagInfo := getTagInfo(element, doc)
- if len(tagInfo.Attributes) == 0 {
- return tagInfo.Attributes, fmt.Errorf("no %s element found", element)
- }
-
return tagInfo.Attributes, nil
}
diff --git a/page.html b/page.html
deleted file mode 100644
index c9f0c8f..0000000
--- a/page.html
+++ /dev/null
@@ -1,984 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-April 22nd - 27th Programming
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
From b872fe7633d7670521ba2ef997d1d7daef317d49 Mon Sep 17 00:00:00 2001
From: Gwyn
Date: Wed, 26 Jun 2024 06:58:55 -0600
Subject: [PATCH 11/14] Renames AndrewIndexBody to AndrewTableOfContents
This more naturally reflects what this tag is doing: it's a TOC from
here on down.
---
README.md | 16 ++++++++--------
andrew_server.go | 28 ++++++++++++++--------------
andrew_server_test.go | 26 +++++++++++++-------------
linksbuilder.go | 10 +++++-----
todo.md | 2 +-
5 files changed, 41 insertions(+), 41 deletions(-)
diff --git a/README.md b/README.md
index e6e0bf6..5f878ee 100644
--- a/README.md
+++ b/README.md
@@ -25,7 +25,7 @@ baseUrl is the hostname you're serving from. This is a part of sitemaps and rss
e.g. `https://playtechnique.io`
-## rendering the .AndrewIndexBody
+## rendering the .AndrewTableOfContents
Given this file system structure:
```text
index.html
@@ -44,19 +44,19 @@ fanfics/
what-if-elves-rode-mice-pt2.html
```
-if articles/index.html contains `{{ .AndrewIndexBody }}` anywhere, that will be replaced with:
+if articles/index.html contains `{{ .AndrewTableOfContents }}` anywhere, that will be replaced with:
```html
- article 1
- article 2
+ article 1
+ article 2
```
-if fanfics/index.html contains `{{ .AndrewIndexBody }}`, that'll be replaced with:
+if fanfics/index.html contains `{{ .AndrewTableOfContents }}`, that'll be replaced with:
```html
- Potter and Draco
- what-if-elves-rode-mice-pt1.html
- what-if-elves-rode-mice-pt2.html
+ Potter and Draco
+ what-if-elves-rode-mice-pt1.html
+ what-if-elves-rode-mice-pt2.html
```
## page titles
diff --git a/andrew_server.go b/andrew_server.go
index 48e0666..a57d340 100644
--- a/andrew_server.go
+++ b/andrew_server.go
@@ -16,28 +16,28 @@ import (
// When a URL is requested, Server creates an Page for the file referenced
// in that URL and then serves the Page.
type Server struct {
- SiteFiles fs.FS // The files being served
- BaseUrl string // The URL used in any links generated for this website that should contain the hostname.
- Address string // IpAddress:Port combo to be served on.
- Andrewindexbodytemplate string // The string we're searching for inside a Page that should be replaced with a template. Mightn't belong in the Server.
- HTTPServer *http.Server
+ SiteFiles fs.FS // The files being served
+ BaseUrl string // The URL used in any links generated for this website that should contain the hostname.
+ Address string // IpAddress:Port combo to be served on.
+ Andrewtableofcontentstemplate string // The string we're searching for inside a Page that should be replaced with a template. Mightn't belong in the Server.
+ HTTPServer *http.Server
}
const (
- AndrewIndexBodyTemplate = "AndrewIndexBody"
- DefaultContentRoot = "."
- DefaultAddress = ":8080"
- DefaultBaseUrl = "http://localhost:8080"
+ AndrewTableOfContentsTemplate = "AndrewTableOfContents"
+ DefaultContentRoot = "."
+ DefaultAddress = ":8080"
+ DefaultBaseUrl = "http://localhost:8080"
)
-// NewServer is a constructor. Its primary role is setting the default andrewindexbodytemplate.
+// NewServer is a constructor. Its primary role is setting the default andrewtableofcontentstemplate.
// Returns an [Server].
func NewServer(contentRoot fs.FS, address, baseUrl string) *Server {
s := &Server{
- SiteFiles: contentRoot,
- Andrewindexbodytemplate: "AndrewIndexBody",
- Address: address,
- BaseUrl: baseUrl,
+ SiteFiles: contentRoot,
+ Andrewtableofcontentstemplate: "AndrewTableOfContents",
+ Address: address,
+ BaseUrl: baseUrl,
}
mux := http.NewServeMux()
mux.HandleFunc("/", s.Serve)
diff --git a/andrew_server_test.go b/andrew_server_test.go
index ebbc183..637503d 100644
--- a/andrew_server_test.go
+++ b/andrew_server_test.go
@@ -216,7 +216,7 @@ func TestServerServesIndexPageByDefault(t *testing.T) {
}
}
-func TestAndrewIndexBodyIsGeneratedCorrectlyInContentrootDirectory(t *testing.T) {
+func TestAndrewTableOfContentsIsGeneratedCorrectlyInContentrootDirectory(t *testing.T) {
t.Parallel()
contentRoot := fstest.MapFS{
@@ -224,7 +224,7 @@ func TestAndrewIndexBodyIsGeneratedCorrectlyInContentrootDirectory(t *testing.T)
-{{ .AndrewIndexBody }}
+{{ .AndrewTableOfContents }}
`)},
"pages/1-2-3.html": &fstest.MapFile{Data: []byte(`
@@ -251,7 +251,7 @@ func TestAndrewIndexBodyIsGeneratedCorrectlyInContentrootDirectory(t *testing.T)
-1-2-3 Page
+1-2-3 Page
`
@@ -260,7 +260,7 @@ func TestAndrewIndexBodyIsGeneratedCorrectlyInContentrootDirectory(t *testing.T)
}
}
-func TestAndrewIndexBodyIsGeneratedCorrectlyInAChildDirectory(t *testing.T) {
+func TestAndrewTableOfContentsIsGeneratedCorrectlyInAChildDirectory(t *testing.T) {
t.Parallel()
contentRoot := fstest.MapFS{
@@ -268,7 +268,7 @@ func TestAndrewIndexBodyIsGeneratedCorrectlyInAChildDirectory(t *testing.T) {
-{{ .AndrewIndexBody }}
+{{ .AndrewTableOfContents }}
`)},
"parentDir/childDir/1-2-3.html": &fstest.MapFile{Data: []byte(`
@@ -295,7 +295,7 @@ func TestAndrewIndexBodyIsGeneratedCorrectlyInAChildDirectory(t *testing.T) {
-1-2-3 Page
+1-2-3 Page
`
@@ -420,22 +420,22 @@ func TestMainCalledWithInvalidAddressPanics(t *testing.T) {
andrew.Main(args, nullLogger)
}
-// TestArticlesInAndrewIndexBodyAreDefaultSortedByModTime is verifying that
-// when the list of links andrew generates for the {{.AndrewIndexBody}} are
+// TestArticlesInAndrewTableOfContentsAreDefaultSortedByModTime is verifying that
+// when the list of links andrew generates for the {{.AndrewTableOfContents}} are
// sorted by mtime, not using the ascii sorting order.
// This test requires having two files which are in one order when sorted
// ascii-betically and in another order by date time, so that we can tell
// what file attribute andrew is actually sorting on.
-func TestArticlesInAndrewIndexBodyAreDefaultSortedByModTime(t *testing.T) {
- expected := `b_newer.html
-a_older.html
+func TestArticlesInAndrewTableOfContentsAreDefaultSortedByModTime(t *testing.T) {
+ expected := `b_newer.html
+a_older.html
`
contentRoot := t.TempDir()
// fstest.MapFS does not enforce file permissions, so we need a real file system in this test.
// above might be wrong
- err := os.WriteFile(contentRoot+"/index.html", []byte("{{.AndrewIndexBody}}"), 0o700)
+ err := os.WriteFile(contentRoot+"/index.html", []byte("{{.AndrewTableOfContents}}"), 0o700)
if err != nil {
t.Fatal(err)
}
@@ -456,7 +456,7 @@ func TestArticlesInAndrewIndexBodyAreDefaultSortedByModTime(t *testing.T) {
os.Chtimes(contentRoot+"/b_newer.html", now, now)
os.Chtimes(contentRoot+"/a_older.html", older, older)
- server := andrew.Server{SiteFiles: os.DirFS(contentRoot), Andrewindexbodytemplate: andrew.AndrewIndexBodyTemplate}
+ server := andrew.Server{SiteFiles: os.DirFS(contentRoot), Andrewtableofcontentstemplate: andrew.AndrewTableOfContentsTemplate}
page, err := andrew.NewPage(server, "index.html")
diff --git a/linksbuilder.go b/linksbuilder.go
index 080b2eb..24aceee 100644
--- a/linksbuilder.go
+++ b/linksbuilder.go
@@ -21,7 +21,7 @@ func BuildPageBodyWithLinks(siblings []Page, startingPageUrl string, startingPag
var links bytes.Buffer
for i, sibling := range siblings {
- links.Write(buildAndrewIndexLink(sibling.UrlPath, sibling.Title, i))
+ links.Write(buildAndrewTableOfContentsLink(sibling.UrlPath, sibling.Title, i))
}
templateBuffer := bytes.Buffer{}
@@ -33,7 +33,7 @@ func BuildPageBodyWithLinks(siblings []Page, startingPageUrl string, startingPag
panic(err)
}
- err = t.Execute(&templateBuffer, map[string]string{"AndrewIndexBody": links.String()})
+ err = t.Execute(&templateBuffer, map[string]string{"AndrewTableOfContents": links.String()})
if err != nil {
return templateBuffer.Bytes(), err
}
@@ -41,9 +41,9 @@ func BuildPageBodyWithLinks(siblings []Page, startingPageUrl string, startingPag
return templateBuffer.Bytes(), nil
}
-// buildAndrewIndexLink encapsulates the format of the link
-func buildAndrewIndexLink(urlPath string, title string, cssIdNumber int) []byte {
- link := fmt.Sprintf("%s", fmt.Sprint(cssIdNumber), urlPath, title)
+// buildAndrewTableOfContentsLink encapsulates the format of the link
+func buildAndrewTableOfContentsLink(urlPath string, title string, cssIdNumber int) []byte {
+ link := fmt.Sprintf("%s", fmt.Sprint(cssIdNumber), urlPath, title)
b := []byte(link)
return b
}
diff --git a/todo.md b/todo.md
index 5709558..3859bf2 100644
--- a/todo.md
+++ b/todo.md
@@ -11,5 +11,5 @@
* github workflow for homebrew
* html escape the paths you're serving
* pull out any article summaries into parent card
-* extract the function that builds the index body out of serveIndexPage and pass it in instead. You can also pass in AndrewIndexBody; that
+* extract the function that builds the index body out of serveIndexPage and pass it in instead. You can also pass in AndrewTableOfContents; that
would give you the flexibility to render different kinds of functions based upon the presence of different template strings.
\ No newline at end of file
From 782ba99af1a8b18cb3e74010f1844457fa7ec1f9 Mon Sep 17 00:00:00 2001
From: Gwyn
Date: Sat, 29 Jun 2024 08:32:45 -0600
Subject: [PATCH 12/14] Add a sort function that can handle dates, so the links
are ordered.
Also simplifies some of the PublishTime handling using RAII
---
andrew_server.go | 6 ++---
andrew_server_test.go | 5 ++--
linksbuilder.go | 10 ++------
page.go | 54 ++++++++++++++++++++++++++-----------------
4 files changed, 40 insertions(+), 35 deletions(-)
diff --git a/andrew_server.go b/andrew_server.go
index a57d340..d3a2717 100644
--- a/andrew_server.go
+++ b/andrew_server.go
@@ -169,14 +169,14 @@ func (a Server) GetSiblingsAndChildren(pagePath string) ([]Page, error) {
// links require a URL relative to the page we're discovering siblings from, not from
// the root of the file system
- page, err := NewPage(a, path)
- page = page.SetUrlPath(strings.TrimPrefix(path, localContentRoot+"/"))
+ s_page, err := NewPage(a, path)
+ s_page = s_page.SetUrlPath(strings.TrimPrefix(path, localContentRoot+"/"))
if err != nil {
return err
}
- pages = append(pages, page)
+ pages = append(pages, s_page)
return nil
})
diff --git a/andrew_server_test.go b/andrew_server_test.go
index 637503d..4b1d4c4 100644
--- a/andrew_server_test.go
+++ b/andrew_server_test.go
@@ -427,9 +427,8 @@ func TestMainCalledWithInvalidAddressPanics(t *testing.T) {
// ascii-betically and in another order by date time, so that we can tell
// what file attribute andrew is actually sorting on.
func TestArticlesInAndrewTableOfContentsAreDefaultSortedByModTime(t *testing.T) {
- expected := `b_newer.html
-a_older.html
-`
+ expected := `b_newer.html` +
+ `a_older.html`
contentRoot := t.TempDir()
diff --git a/linksbuilder.go b/linksbuilder.go
index 24aceee..d2ce29f 100644
--- a/linksbuilder.go
+++ b/linksbuilder.go
@@ -6,17 +6,11 @@ import (
"text/template"
)
-// Building the template body requires information from both the Server and the Page.
-// The Server supplies information from the file system, such as the siblings of a page
-// in its directory.
-// The Page supplies information from with the page itself, like the created-at date or
-// the title of the page.
-
-// BuildPageBodyWithLinks receives the path to a file, currently normally an index file.
+// BuildAndrewTOCLinks receives the path to a file, currently normally an index file.
// It traverses the file system starting at the directory containing
// that file, finds all html files that are _not_ index.html files and returns them
// as a list of html links to those pages.
-func BuildPageBodyWithLinks(siblings []Page, startingPageUrl string, startingPage Page) ([]byte, error) {
+func BuildAndrewTOCLinks(siblings []Page, startingPage Page) ([]byte, error) {
var links bytes.Buffer
diff --git a/page.go b/page.go
index b0814b9..78dcc0f 100644
--- a/page.go
+++ b/page.go
@@ -5,6 +5,7 @@ import (
"fmt"
"io/fs"
"path"
+ "sort"
"strings"
"time"
@@ -54,22 +55,7 @@ func NewPage(server Server, pageUrl string) (Page, error) {
return Page{}, err
}
- page := Page{Content: string(pageContent), UrlPath: pageUrl, Title: pageTitle}
-
- if strings.Contains(pageUrl, "index.html") {
- siblings, err := server.GetSiblingsAndChildren(page.UrlPath)
-
- if err != nil {
- return page, err
- }
-
- pageContent, err = BuildPageBodyWithLinks(siblings, pageUrl, page)
- if err != nil {
- return Page{}, err
- }
-
- page.Content = string(pageContent)
- }
+ page := Page{Content: string(pageContent), UrlPath: pageUrl, Title: pageTitle, PublishTime: pageInfo.ModTime()}
meta, err := GetMetaElements(pageContent)
if err != nil {
@@ -79,14 +65,31 @@ func NewPage(server Server, pageUrl string) (Page, error) {
publishTime, ok := meta["andrew-created-at"]
if ok {
- page.PublishTime, err = time.Parse(time.DateOnly, publishTime)
+ andrewCreatedAt, err := time.Parse(time.DateOnly, publishTime)
if err != nil {
+ fmt.Println("could not parse meta tag andrew-created-at using time.Parse. Defaulting to mod time")
// log.Logger("could not parse meta tag andrew-created-at using time.Parse. Defaulting to mod time")
- page.PublishTime = pageInfo.ModTime()
+ } else {
+ page.PublishTime = andrewCreatedAt
}
- } else {
- page.PublishTime = pageInfo.ModTime()
+ }
+
+ if strings.Contains(pageUrl, "index.html") {
+ siblings, err := server.GetSiblingsAndChildren(page.UrlPath)
+
+ if err != nil {
+ return page, err
+ }
+
+ orderedSiblings := sortPages(siblings)
+
+ pageContent, err = BuildAndrewTOCLinks(orderedSiblings, page)
+ if err != nil {
+ return Page{}, err
+ }
+
+ page.Content = string(pageContent)
}
return page, nil
@@ -94,7 +97,7 @@ func NewPage(server Server, pageUrl string) (Page, error) {
// SetUrlPath updates the UrlPath on a pre-existing Page.
func (a Page) SetUrlPath(urlPath string) Page {
- return Page{Title: a.Title, Content: a.Content, UrlPath: urlPath}
+ return Page{Title: a.Title, Content: a.Content, UrlPath: urlPath, PublishTime: a.PublishTime}
}
// getTagInfo recursively descends an html node tree for the requested tag,
@@ -190,3 +193,12 @@ func titleFromHTMLTitleElement(fileContent []byte) (string, error) {
}
return tagInfo.Data, nil
}
+
+func sortPages(pages []Page) []Page {
+
+ sort.Slice(pages, func(i, j int) bool {
+ return pages[i].PublishTime.After(pages[j].PublishTime)
+ })
+
+ return pages
+}
From 5939aee8bbe2e8dcfd40aece1b941521cf27d781 Mon Sep 17 00:00:00 2001
From: Gwyn
Date: Sat, 29 Jun 2024 10:46:08 -0600
Subject: [PATCH 13/14] Adds a test to prove andrew-published-at is parsed.
Also updates the meta tag I look for to andrew-published-at.
---
andrew_server_test.go | 66 +++++++++++++++++++++++++++++++++++++++++++
page.go | 6 ++--
page_test.go | 8 +++---
3 files changed, 73 insertions(+), 7 deletions(-)
diff --git a/andrew_server_test.go b/andrew_server_test.go
index 4b1d4c4..f56e37f 100644
--- a/andrew_server_test.go
+++ b/andrew_server_test.go
@@ -3,6 +3,7 @@ package andrew_test
import (
"bytes"
"errors"
+ "fmt"
"io"
"io/fs"
"net"
@@ -471,6 +472,71 @@ func TestArticlesInAndrewTableOfContentsAreDefaultSortedByModTime(t *testing.T)
}
+// TestArticlesOrderInAndrewTableOfContentsIsOverridable is verifying that
+// when a page contains an andrew-publish-time meta element then the list of links andrew
+// generates for the {{.AndrewTableOfContents}} are
+// sorted by the meta element, then the mtime, not using the ascii sorting order.
+// This test requires having several files which are in one order when sorted
+// by modtime and in another order by andrew-publish-time time, so that we can tell
+// what file attribute andrew is actually sorting on.
+func TestArticlesOrderInAndrewTableOfContentsIsOverridable(t *testing.T) {
+ expected := `b_newest.html` +
+ `c_newer.html` +
+ `a_older.html`
+
+ contentRoot := t.TempDir()
+
+ // fstest.MapFS does not enforce file permissions, so we need a real file system in this test.
+ // above might be wrong
+ err := os.WriteFile(contentRoot+"/index.html", []byte("{{.AndrewTableOfContents}}"), 0o700)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = os.WriteFile(contentRoot+"/a_older.html", []byte{}, 0o700)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = os.WriteFile(contentRoot+"/c_newer.html", []byte{}, 0o700)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ now := time.Now()
+
+ newest := now.Add(24 * time.Hour)
+ formattedDate := newest.Format("2006-01-02")
+
+ content := fmt.Sprintf(``, formattedDate)
+
+ err = os.WriteFile(contentRoot+"/b_newest.html", []byte(content), 0o700)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ older := now.Add(-10 * time.Minute)
+
+ os.Chtimes(contentRoot+"/c_newer.html", now, now)
+ os.Chtimes(contentRoot+"/a_older.html", older, older)
+ os.Chtimes(contentRoot+"/b_newest.html", older, older)
+
+ server := andrew.Server{SiteFiles: os.DirFS(contentRoot), Andrewtableofcontentstemplate: andrew.AndrewTableOfContentsTemplate}
+
+ page, err := andrew.NewPage(server, "index.html")
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ received := page.Content
+
+ if expected != string(received) {
+ t.Errorf(cmp.Diff(expected, received))
+ }
+
+}
+
// newTestAndrewServer starts an andrew and returns the localhost url that you can run http gets against
// to retrieve data from that server
func newTestAndrewServer(t *testing.T, contentRoot fs.FS) *andrew.Server {
diff --git a/page.go b/page.go
index 78dcc0f..b6e4851 100644
--- a/page.go
+++ b/page.go
@@ -62,14 +62,14 @@ func NewPage(server Server, pageUrl string) (Page, error) {
return Page{}, err
}
- publishTime, ok := meta["andrew-created-at"]
+ publishTime, ok := meta["andrew-publish-time"]
if ok {
andrewCreatedAt, err := time.Parse(time.DateOnly, publishTime)
if err != nil {
- fmt.Println("could not parse meta tag andrew-created-at using time.Parse. Defaulting to mod time")
- // log.Logger("could not parse meta tag andrew-created-at using time.Parse. Defaulting to mod time")
+ return Page{}, err
+ // log.Logger("could not parse meta tag andrew-publish-time using time.Parse. Defaulting to mod time")
} else {
page.PublishTime = andrewCreatedAt
}
diff --git a/page_test.go b/page_test.go
index 786089b..ee35016 100644
--- a/page_test.go
+++ b/page_test.go
@@ -41,8 +41,8 @@ func TestGetTitleReturnsPageFileNameWhenNoTitleInDocument(t *testing.T) {
}
func TestOneMetaTagPopulatesATag(t *testing.T) {
- expected := map[string]string{"andrew-created-at": "2025-03-01"}
- received, err := GetMetaElements([]byte(""))
+ expected := map[string]string{"andrew-publish-time": "2025-03-01"}
+ received, err := GetMetaElements([]byte(""))
if err != nil {
t.Fatal(err)
@@ -54,8 +54,8 @@ func TestOneMetaTagPopulatesATag(t *testing.T) {
}
func TestMultipleMetaTagsPopulatedWithExpectedElements(t *testing.T) {
- expected := map[string]string{"andrew-created-at": "2025-03-01", "andrew-roflcopter": "hippolol"}
- received, err := GetMetaElements([]byte(" "))
+ expected := map[string]string{"andrew-publish-time": "2025-03-01", "andrew-roflcopter": "hippolol"}
+ received, err := GetMetaElements([]byte(" "))
if err != nil {
t.Fatal(err)
From 6e1af7e54f84287b1140a9b5b88c4bfbd38e2dd2 Mon Sep 17 00:00:00 2001
From: Gwyn
Date: Sat, 29 Jun 2024 10:54:10 -0600
Subject: [PATCH 14/14] Update test to use newer andrew-publish-time meta tag.
---
andrew_server_test.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/andrew_server_test.go b/andrew_server_test.go
index f56e37f..3bb4449 100644
--- a/andrew_server_test.go
+++ b/andrew_server_test.go
@@ -508,7 +508,7 @@ func TestArticlesOrderInAndrewTableOfContentsIsOverridable(t *testing.T) {
newest := now.Add(24 * time.Hour)
formattedDate := newest.Format("2006-01-02")
- content := fmt.Sprintf(``, formattedDate)
+ content := fmt.Sprintf(``, formattedDate)
err = os.WriteFile(contentRoot+"/b_newest.html", []byte(content), 0o700)
if err != nil {