123 lines
3.4 KiB
Go
123 lines
3.4 KiB
Go
package step
|
|
|
|
import "io"
|
|
import "slices"
|
|
import "strconv"
|
|
import "strings"
|
|
|
|
const metaRule = "---"
|
|
|
|
// Meta represents optional metadata that can occur at the very start of
|
|
// a document.
|
|
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
|
|
// empty map will be returned.
|
|
func SplitMeta (input string) (Meta, string, error) {
|
|
// i hate crlf!!!!! uwehhh!!! TODO remove
|
|
input = strings.ReplaceAll(input, "\r\n", "\n")
|
|
// TODO call internal function that takes in an io.Reader and scans it
|
|
// by line instead of operating directly on the string. have that call
|
|
// yet another function which will solve #11
|
|
|
|
// stop if there is no metadata
|
|
if !strings.HasPrefix(input, metaRule + "\n") {
|
|
return Meta { }, input, nil
|
|
}
|
|
|
|
// get the start and the end of the metadata
|
|
input = strings.TrimPrefix(input, metaRule)
|
|
index := strings.Index(input, "\n" + metaRule + "\n")
|
|
if index < 0 {
|
|
return nil, "", ErrMetaNeverClosed
|
|
}
|
|
metaStr := input[:index]
|
|
bodyStr := input[index + len(metaRule) + 2:]
|
|
|
|
// parse metadata
|
|
meta, err := ParseMeta(metaStr)
|
|
if err != nil { return Meta { }, "", err }
|
|
|
|
return meta, bodyStr, nil
|
|
}
|
|
|
|
// ParseMeta parses isolated metadata (without the horizontal starting and
|
|
// ending rules).
|
|
func ParseMeta (input string) (Meta, error) {
|
|
meta := make(Meta)
|
|
for _, line := range strings.Split(input, "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" { continue }
|
|
if strings.HasPrefix(line, "#") { continue }
|
|
key, value, ok := strings.Cut(line, ":")
|
|
if !ok {
|
|
return nil, ErrMetaMalformed
|
|
}
|
|
key = strings.TrimSpace(key)
|
|
value = strings.TrimSpace(value)
|
|
if strings.HasPrefix(value, "\"") || strings.HasPrefix(value, "'") {
|
|
unquoted, err := strconv.Unquote(value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
value = unquoted
|
|
}
|
|
if key == "" {
|
|
return nil, ErrMetaMalformed
|
|
}
|
|
meta.Add(key, value)
|
|
}
|
|
return meta, nil
|
|
}
|
|
|
|
// DecodeMeta decodes metadata from an io.Reader. The entire reader is consumed.
|
|
func DecodeMeta (input io.Reader) (Meta, error) {
|
|
buffer, err := io.ReadAll(input)
|
|
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)
|
|
}
|