diff --git a/environment.go b/environment.go index 2f53451..1ba464e 100644 --- a/environment.go +++ b/environment.go @@ -101,24 +101,24 @@ func (this *Environment) parse (name string, modTime time.Time, input io.Reader) // read entire file and split into front matter and body buffer, err := io.ReadAll(input) if err != nil { return nil, err } - frontMatter, body, err := SplitMeta(string(buffer)) + meta, body, err := SplitMeta(string(buffer)) if err != nil { return nil, err } // assemble the document struct document := &Document { - Meta: frontMatter, + Meta: meta, environment: this, name: name, parseTime: time.Now(), template: template.New(name), } - if author, ok := frontMatter["author"]; ok { + if author := meta.Get("author"); author != "" { document.Author = author } - if title, ok := frontMatter["title"]; ok { + if title := meta.Get("title"); title != "" { document.Title = title } - if extends, ok := frontMatter["extends"]; ok { + if extends := meta.Get("extends"); extends != "" { document.Extends = extends } diff --git a/error.go b/error.go index 70e7972..4695ba0 100644 --- a/error.go +++ b/error.go @@ -5,7 +5,6 @@ type Error string; const ( ErrCircularInheritance Error = "circular inheritance" ErrMetaMalformed Error = "metadata is malformed" ErrMetaNeverClosed Error = "metadata is never closed" - ErrMetaDuplicateKey Error = "duplicate key in front matter" ErrTypeMismatch Error = "type mismatch" ) diff --git a/http/handler.go b/http/handler.go index 7547faf..1afd36c 100644 --- a/http/handler.go +++ b/http/handler.go @@ -112,10 +112,10 @@ func (this *Handler) serveDocument ( recorder.Reset() recorder.Head = res.Header().Clone() } - if contentType, ok := document.Meta["content-type"]; ok { + if contentType := document.Meta.Get("content-type"); contentType != "" { recorder.Header().Set("Content-Type", contentType) } - if status, ok := document.Meta["status"]; ok { + if status := document.Meta.Get("status"); status != "" { if status, err := strconv.Atoi(status); err == nil { recorder.Status = status } diff --git a/meta.go b/meta.go index a2ef3cf..b80b597 100644 --- a/meta.go +++ b/meta.go @@ -1,6 +1,7 @@ package step import "io" +import "slices" import "strconv" import "strings" @@ -8,7 +9,7 @@ const metaRule = "---" // Meta represents optional metadata that can occur at the very start of // a document. -type Meta = map[string] string +type Meta map[string] []string // SplitMeta parses the metadata (if it exists), returning a map representing it // along with the rest of the input as a string. If there is no metadata, an @@ -52,7 +53,7 @@ func ParseMeta (input string) (Meta, error) { if !ok { return nil, ErrMetaMalformed } - key = strings.ToLower(strings.TrimSpace(key)) + key = strings.TrimSpace(key) value = strings.TrimSpace(value) if strings.HasPrefix(value, "\"") || strings.HasPrefix(value, "'") { unquoted, err := strconv.Unquote(value) @@ -64,10 +65,7 @@ func ParseMeta (input string) (Meta, error) { if key == "" { return nil, ErrMetaMalformed } - if _, exists := meta[key]; exists { - return nil, ErrMetaDuplicateKey - } - meta[key] = value + meta.Add(key, value) } return meta, nil } @@ -78,3 +76,46 @@ func DecodeMeta (input io.Reader) (Meta, error) { if err != nil { return nil, err } return ParseMeta(string(buffer)) } + +// Add adds the key/value pair to the metadata. It appends to any existing +// values associated with the key. The key is case insensitive. +func (meta Meta) Add (key, value string) { + key = canonicalMetaKey(key) + meta[key] = append(meta[key], value) +} + +// Clone returns a deep copy of the metadata. +func (meta Meta) Clone () Meta { + clone := make(Meta, len(meta)) + for key, value := range meta { + clone[key] = slices.Clone(value) + } + return clone +} + +// Del deletes the values associated with the key. The key is case insensitive. +func (meta Meta) Del (key string) { + key = canonicalMetaKey(key) + delete(meta, key) +} + +// Get gets the first value associated with the key. The key is case +// insensitive. +func (meta Meta) Get (key string) string { + key = canonicalMetaKey(key) + slice, ok := meta[key] + if !ok { return "" } + if len(slice) == 0 { return "" } + return slice[0] +} + +// Set replaces all values currently associated with key with the single element +// value. The key is case insensitive. +func (meta Meta) Set (key, value string) { + key = canonicalMetaKey(key) + meta[key] = []string { value } +} + +func canonicalMetaKey (key string) string { + return strings.ToLower(key) +} diff --git a/meta_test.go b/meta_test.go index 066d518..1f8126d 100644 --- a/meta_test.go +++ b/meta_test.go @@ -13,7 +13,7 @@ theres another hr that shouldnt break anything break: anything ` - frontMatter, body, err := SplitMeta( + meta, body, err := SplitMeta( `--- FOO: baR fOoo: Bar @@ -34,27 +34,27 @@ Sentence: ` + quickBrownFox + ` } test.Log("META:") - test.Log(frontMatter) - value0, ok := frontMatter["foo"] - if !ok { test.Fatal("missing key") } - value1, ok := frontMatter["fooo"] - if !ok { test.Fatal("missing key") } - value2, ok := frontMatter["shouldn't break anything"] - if !ok { test.Fatal("missing key") } - value3, ok := frontMatter["this"] - if !ok { test.Fatal("missing key") } - value4, ok := frontMatter["sentence"] - if !ok { test.Fatal("missing key") } - if value0 != "baR" { test.Fatal("value is not correct") } - if value1 != "Bar" { test.Fatal("value is not correct") } - if value2 != "---" { test.Fatal("value is not correct") } - if value3 != "that" { test.Fatal("value is not correct") } - if value4 != quickBrownFox { test.Fatal("value is not correct") } + test.Log(meta) + if meta.Get("foo") != "baR" { + test.Fatal("value is not correct") + } + if meta.Get("fooo") != "Bar" { + test.Fatal("value is not correct") + } + if meta.Get("shouldn't break anything") != "---" { + test.Fatal("value is not correct") + } + if meta.Get("this") != "that" { + test.Fatal("value is not correct") + } + if meta.Get("sentence") != quickBrownFox { + test.Fatal("value is not correct") + } } func TestSplitMetaCRLF (test *testing.T) { correctBody := "this is some sample text\r\n---\r\ntheres another hr that shouldnt\r\nbreak anything\r\nbreak: anything\r\n" - frontMatter, body, err := SplitMeta( + meta, body, err := SplitMeta( "---\r\nFOO: baR\r\n fOoo: Bar\r\nShouldn't break anything: ---\r\n\r\nthis : that\r\nSentence: " + quickBrownFox + "\r\n---\r\n" + correctBody) if err != nil { test.Fatal(err) @@ -68,22 +68,22 @@ func TestSplitMetaCRLF (test *testing.T) { } test.Log("META:") - test.Log(frontMatter) - value0, ok := frontMatter["foo"] - if !ok { test.Fatal("missing key") } - value1, ok := frontMatter["fooo"] - if !ok { test.Fatal("missing key") } - value2, ok := frontMatter["shouldn't break anything"] - if !ok { test.Fatal("missing key") } - value3, ok := frontMatter["this"] - if !ok { test.Fatal("missing key") } - value4, ok := frontMatter["sentence"] - if !ok { test.Fatal("missing key") } - if value0 != "baR" { test.Fatal("value is not correct") } - if value1 != "Bar" { test.Fatal("value is not correct") } - if value2 != "---" { test.Fatal("value is not correct") } - if value3 != "that" { test.Fatal("value is not correct") } - if value4 != quickBrownFox { test.Fatal("value is not correct") } + test.Log(meta) + if meta.Get("foo") != "baR" { + test.Fatal("value is not correct") + } + if meta.Get("fooo") != "Bar" { + test.Fatal("value is not correct") + } + if meta.Get("shouldn't break anything") != "---" { + test.Fatal("value is not correct") + } + if meta.Get("this") != "that" { + test.Fatal("value is not correct") + } + if meta.Get("sentence") != quickBrownFox { + test.Fatal("value is not correct") + } } func TestParseMeta (test *testing.T) { @@ -99,13 +99,13 @@ number: 3849 test.Log("META:") test.Log(meta) - value0, ok := meta["thing"] - if !ok { test.Fatal("missing key") } - value1, ok := meta["other-thing"] - if !ok { test.Fatal("missing key") } - value2, ok := meta["number"] - if !ok { test.Fatal("missing key") } - if value0 != "\tQuoted string!!!!!m " { test.Fatal("value is not correct") } - if value1 != "askdjlksajd" { test.Fatal("value is not correct") } - if value2 != "3849" { test.Fatal("value is not correct") } + if meta.Get("thing") != "\tQuoted string!!!!! " { + test.Fatal("value is not correct") + } + if meta.Get("other-thing") != "askdjlksajd" { + test.Fatal("value is not correct") + } + if meta.Get("number") != "3849" { + test.Fatal("value is not correct") + } }