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
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
}

View File

@ -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"
)

View File

@ -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
}

53
meta.go
View File

@ -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)
}

View File

@ -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")
}
}