Metadata keys can have multiple values

Closes #6
This commit is contained in:
Sasha Koshka 2024-12-09 15:53:29 -05:00
parent ccff4e56c0
commit 380a5b9223
5 changed files with 97 additions and 57 deletions

View File

@ -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 // read entire file and split into front matter and body
buffer, err := io.ReadAll(input) buffer, err := io.ReadAll(input)
if err != nil { return nil, err } if err != nil { return nil, err }
frontMatter, body, err := SplitMeta(string(buffer)) meta, body, err := SplitMeta(string(buffer))
if err != nil { return nil, err } if err != nil { return nil, err }
// assemble the document struct // assemble the document struct
document := &Document { document := &Document {
Meta: frontMatter, Meta: meta,
environment: this, environment: this,
name: name, name: name,
parseTime: time.Now(), parseTime: time.Now(),
template: template.New(name), template: template.New(name),
} }
if author, ok := frontMatter["author"]; ok { if author := meta.Get("author"); author != "" {
document.Author = author document.Author = author
} }
if title, ok := frontMatter["title"]; ok { if title := meta.Get("title"); title != "" {
document.Title = title document.Title = title
} }
if extends, ok := frontMatter["extends"]; ok { if extends := meta.Get("extends"); extends != "" {
document.Extends = extends document.Extends = extends
} }

View File

@ -5,7 +5,6 @@ type Error string; const (
ErrCircularInheritance Error = "circular inheritance" ErrCircularInheritance Error = "circular inheritance"
ErrMetaMalformed Error = "metadata is malformed" ErrMetaMalformed Error = "metadata is malformed"
ErrMetaNeverClosed Error = "metadata is never closed" ErrMetaNeverClosed Error = "metadata is never closed"
ErrMetaDuplicateKey Error = "duplicate key in front matter"
ErrTypeMismatch Error = "type mismatch" ErrTypeMismatch Error = "type mismatch"
) )

View File

@ -112,10 +112,10 @@ func (this *Handler) serveDocument (
recorder.Reset() recorder.Reset()
recorder.Head = res.Header().Clone() 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) 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 { if status, err := strconv.Atoi(status); err == nil {
recorder.Status = status recorder.Status = status
} }

53
meta.go
View File

@ -1,6 +1,7 @@
package step package step
import "io" import "io"
import "slices"
import "strconv" import "strconv"
import "strings" import "strings"
@ -8,7 +9,7 @@ const metaRule = "---"
// Meta represents optional metadata that can occur at the very start of // Meta represents optional metadata that can occur at the very start of
// a document. // a document.
type Meta = map[string] string type Meta map[string] []string
// SplitMeta parses the metadata (if it exists), returning a map representing it // 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 // 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 { if !ok {
return nil, ErrMetaMalformed return nil, ErrMetaMalformed
} }
key = strings.ToLower(strings.TrimSpace(key)) key = strings.TrimSpace(key)
value = strings.TrimSpace(value) value = strings.TrimSpace(value)
if strings.HasPrefix(value, "\"") || strings.HasPrefix(value, "'") { if strings.HasPrefix(value, "\"") || strings.HasPrefix(value, "'") {
unquoted, err := strconv.Unquote(value) unquoted, err := strconv.Unquote(value)
@ -64,10 +65,7 @@ func ParseMeta (input string) (Meta, error) {
if key == "" { if key == "" {
return nil, ErrMetaMalformed return nil, ErrMetaMalformed
} }
if _, exists := meta[key]; exists { meta.Add(key, value)
return nil, ErrMetaDuplicateKey
}
meta[key] = value
} }
return meta, nil return meta, nil
} }
@ -78,3 +76,46 @@ func DecodeMeta (input io.Reader) (Meta, error) {
if err != nil { return nil, err } if err != nil { return nil, err }
return ParseMeta(string(buffer)) 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)
}

View File

@ -13,7 +13,7 @@ theres another hr that shouldnt
break anything break anything
break: anything break: anything
` `
frontMatter, body, err := SplitMeta( meta, body, err := SplitMeta(
`--- `---
FOO: baR FOO: baR
fOoo: Bar fOoo: Bar
@ -34,27 +34,27 @@ Sentence: ` + quickBrownFox + `
} }
test.Log("META:") test.Log("META:")
test.Log(frontMatter) test.Log(meta)
value0, ok := frontMatter["foo"] if meta.Get("foo") != "baR" {
if !ok { test.Fatal("missing key") } test.Fatal("value is not correct")
value1, ok := frontMatter["fooo"] }
if !ok { test.Fatal("missing key") } if meta.Get("fooo") != "Bar" {
value2, ok := frontMatter["shouldn't break anything"] test.Fatal("value is not correct")
if !ok { test.Fatal("missing key") } }
value3, ok := frontMatter["this"] if meta.Get("shouldn't break anything") != "---" {
if !ok { test.Fatal("missing key") } test.Fatal("value is not correct")
value4, ok := frontMatter["sentence"] }
if !ok { test.Fatal("missing key") } if meta.Get("this") != "that" {
if value0 != "baR" { test.Fatal("value is not correct") } test.Fatal("value is not correct")
if value1 != "Bar" { test.Fatal("value is not correct") } }
if value2 != "---" { test.Fatal("value is not correct") } if meta.Get("sentence") != quickBrownFox {
if value3 != "that" { test.Fatal("value is not correct") } test.Fatal("value is not correct")
if value4 != quickBrownFox { test.Fatal("value is not correct") } }
} }
func TestSplitMetaCRLF (test *testing.T) { 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" 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) "---\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 { if err != nil {
test.Fatal(err) test.Fatal(err)
@ -68,22 +68,22 @@ func TestSplitMetaCRLF (test *testing.T) {
} }
test.Log("META:") test.Log("META:")
test.Log(frontMatter) test.Log(meta)
value0, ok := frontMatter["foo"] if meta.Get("foo") != "baR" {
if !ok { test.Fatal("missing key") } test.Fatal("value is not correct")
value1, ok := frontMatter["fooo"] }
if !ok { test.Fatal("missing key") } if meta.Get("fooo") != "Bar" {
value2, ok := frontMatter["shouldn't break anything"] test.Fatal("value is not correct")
if !ok { test.Fatal("missing key") } }
value3, ok := frontMatter["this"] if meta.Get("shouldn't break anything") != "---" {
if !ok { test.Fatal("missing key") } test.Fatal("value is not correct")
value4, ok := frontMatter["sentence"] }
if !ok { test.Fatal("missing key") } if meta.Get("this") != "that" {
if value0 != "baR" { test.Fatal("value is not correct") } test.Fatal("value is not correct")
if value1 != "Bar" { test.Fatal("value is not correct") } }
if value2 != "---" { test.Fatal("value is not correct") } if meta.Get("sentence") != quickBrownFox {
if value3 != "that" { test.Fatal("value is not correct") } test.Fatal("value is not correct")
if value4 != quickBrownFox { test.Fatal("value is not correct") } }
} }
func TestParseMeta (test *testing.T) { func TestParseMeta (test *testing.T) {
@ -99,13 +99,13 @@ number: 3849
test.Log("META:") test.Log("META:")
test.Log(meta) test.Log(meta)
value0, ok := meta["thing"] if meta.Get("thing") != "\tQuoted string!!!!! " {
if !ok { test.Fatal("missing key") } test.Fatal("value is not correct")
value1, ok := meta["other-thing"] }
if !ok { test.Fatal("missing key") } if meta.Get("other-thing") != "askdjlksajd" {
value2, ok := meta["number"] test.Fatal("value is not correct")
if !ok { test.Fatal("missing key") } }
if value0 != "\tQuoted string!!!!!m " { test.Fatal("value is not correct") } if meta.Get("number") != "3849" {
if value1 != "askdjlksajd" { test.Fatal("value is not correct") } test.Fatal("value is not correct")
if value2 != "3849" { test.Fatal("value is not correct") } }
} }