diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..538c8c5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +*~ diff --git a/main.go b/main.go new file mode 100644 index 0000000..06ab7d0 --- /dev/null +++ b/main.go @@ -0,0 +1 @@ +package main diff --git a/model.go b/model.go new file mode 100644 index 0000000..6c19930 --- /dev/null +++ b/model.go @@ -0,0 +1,85 @@ +package main + +import ( + "fmt" + "strings" +) + +var ErrPageNotFound = fmt.Errorf("page not found") + +// A Page is a single unit of content on the site. +type Page struct { + Title string + Contents []byte +} + +// Index is a set of pages that exist at this level, +// as well as a set of "folders" that contain sub-indexes. +// The 'index' key in Pages is special, and will be returned +// if no key is provided. +type Index struct { + Children map[string]Index + Pages map[string]Page +} + +// Page returns the requested page from the index, recursively +// key is assumed to be a `/`-separated string; Page will split on the slashes, +// descending into an index to find the page, if possible. If no match is found, +// return an empty page and `ErrPageNotFound`. +// `foo/` will return a page named `foo` in the current index, if it exists. +// Otherwise, if a child index named "foo" exists, page will attempt to return its index page. +// Page strips leading / from keys. +func (i *Index) Page(key string) (*Page, error) { + if key == "" || key == "/" { + key = "index" + } + if key[0] == '/' { // strip leading slash + key = key[1:] + } + curr, rest, found := strings.Cut(key, "/") + page, pageok := i.Pages[curr] + child, childok := i.Children[curr] + if !found && !pageok { + return &Page{}, ErrPageNotFound + } + if rest == "" && pageok { + return &page, nil + } + if !childok { + return &Page{}, ErrPageNotFound + } + return (&child).Page(rest) +} + +// Save stores a page in the index, recursively, +// overwriting any that may have existed before. +// `foo/` is stored as a page named 'foo' in the current index; +// default 'index' files should be explicitly passed as such. +// The empty key or `/` are invalid and result in an error. +// Leading slashes are stripped. +func (i *Index) Save(key string, page *Page) error { + if key == "" || key == "/" { + return fmt.Errorf("invalid page key") + } + if key[0] == '/' { // strip leading slash + key = key[1:] + } + if i.Pages == nil { + i.Pages = map[string]Page{} + } + if i.Children == nil { + i.Children = map[string]Index{} + } + curr, rest, _ := strings.Cut(key, "/") + if rest == "" { + i.Pages[curr] = *page + } else { + children := i.Children[curr] + err := (&children).Save(rest, page) + if err != nil { + return err + } + i.Children[curr] = children + } + return nil +} diff --git a/model_test.go b/model_test.go new file mode 100644 index 0000000..0abe80f --- /dev/null +++ b/model_test.go @@ -0,0 +1,200 @@ +package main + +import ( + "bytes" + "testing" +) + +func TestPage(t *testing.T) { + i := &Index{ + Children: map[string]Index{ + "foo": { + Children: map[string]Index{}, + Pages: map[string]Page{ + "bar": { + Contents: []byte("bar"), + }, + "index": { + Contents: []byte("quuz"), + }, + }, + }, + "bar": { + Children: map[string]Index{}, + Pages: map[string]Page{ + "index": { + Contents: []byte("quuz"), + }, + "goof": { + Contents: []byte("quuz2"), + }, + }, + }, + }, + Pages: map[string]Page{ + "index": { + Contents: []byte("indextest"), + }, + "foo": { + Contents: []byte("rootfoo"), + }, + }, + } + + p, err := i.Page("") + if err != nil { + t.Logf("expected no err, received err: %v", err) + t.Fail() + } + if !bytes.Equal(p.Contents, []byte("indextest")) { + t.Logf("expected contents to be 'indextest', received '%v'", p.Contents) + t.Fail() + } + + p, err = i.Page("/") + if err != nil { + t.Logf("expected no err, received err: %v", err) + t.Fail() + } + if !bytes.Equal(p.Contents, []byte("indextest")) { + t.Logf("expected contents to be 'indextest', received '%v'", p.Contents) + t.Fail() + } + + p, err = i.Page("foo/bar") + if err != nil { + t.Logf("expected no err, received err: %v", err) + t.Fail() + } + if !bytes.Equal(p.Contents, []byte("bar")) { + t.Logf("expected contents to be 'bar', received '%v'", p.Contents) + t.Fail() + } + + p, err = i.Page("/foo/bar") + if err != nil { + t.Logf("expected no err, received err: %v", err) + t.Fail() + } + if !bytes.Equal(p.Contents, []byte("bar")) { + t.Logf("expected contents to be 'bar', received '%v'", p.Contents) + t.Fail() + } + + p, err = i.Page("foo") + if err != nil { + t.Logf("expected no err, received err: %v", err) + t.Fail() + } + if !bytes.Equal(p.Contents, []byte("rootfoo")) { + t.Logf("expected contents to be 'rootfoo', received '%v'", p.Contents) + t.Fail() + } + + p, err = i.Page("foo/") + if err != nil { + t.Logf("expected no err, received err: %v", err) + t.Fail() + } + if !bytes.Equal(p.Contents, []byte("rootfoo")) { + t.Logf("expected contents to be 'rootfoo', received '%v'", p.Contents) + t.Fail() + } + + p, err = i.Page("bar/") + if err != nil { + t.Logf("expected no err, received err: %v", err) + t.Fail() + } + if !bytes.Equal(p.Contents, []byte("quuz")) { + t.Logf("expected contents to be 'quuz', received '%v'", p.Contents) + t.Fail() + } + + p, err = i.Page("bar/goof") + if err != nil { + t.Logf("expected no err, received err: %v", err) + t.Fail() + } + if !bytes.Equal(p.Contents, []byte("quuz2")) { + t.Logf("expected contents to be 'quuz2', received '%v'", p.Contents) + t.Fail() + } + + p, err = i.Page("foo/quuz") + if err == nil { + t.Logf("expected err, received nil err") + t.Fail() + } + if !bytes.Equal(p.Contents, []byte("")) { + t.Logf("expected no content, received '%v'", p.Contents) + t.Fail() + } + + p, err = i.Page("quuz/bar") + if err == nil { + t.Logf("expected err, received nil err") + t.Fail() + } + if !bytes.Equal(p.Contents, []byte("")) { + t.Logf("expected no content, received '%v'", p.Contents) + t.Fail() + } + + p, err = i.Page("//////////") + if err == nil { + t.Logf("expected err, received nil err") + t.Fail() + } + if !bytes.Equal(p.Contents, []byte("")) { + t.Logf("expected no content, received '%v'", p.Contents) + t.Fail() + } +} + +func TestSave(t *testing.T) { + i := &Index{} + + err := i.Save("foo", &Page{Title: "fooroot"}) + if err != nil { + t.Logf("expected no err, received %v", err) + } + p, err := i.Page("foo") + if err != nil { + t.Logf("expected no err, received err: %v", err) + t.Fail() + } + if p.Title != "fooroot" { + t.Logf("expected title to be 'fooroot', received '%v'", p.Title) + t.Fail() + } + + err = i.Save("foo/", &Page{Title: "fooroot2"}) + if err != nil { + t.Logf("expected no err, received %v", err) + } + p, err = i.Page("foo") + if err != nil { + t.Logf("expected no err, received err: %v", err) + t.Fail() + } + if p.Title != "fooroot2" { + t.Logf("expected title to be 'fooroot2', received '%v'", p.Title) + t.Fail() + } + + err = i.Save("bar/baz", &Page{Title: "quuz"}) + if err != nil { + t.Logf("expected no err, received %v", err) + } + p, err = i.Page("bar/baz") + if err != nil { + t.Logf("expected no err, received err: %v", err) + t.Logf("%+v", i) + t.Fail() + } + if p.Title != "quuz" { + t.Logf("expected title to be 'quuz', received '%v'", p.Title) + t.Fail() + } +}