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