Skip to content

Commit

Permalink
document: clean up attributes parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
adambabik committed Jan 24, 2025
1 parent a04f98b commit 44b5d59
Show file tree
Hide file tree
Showing 5 changed files with 289 additions and 278 deletions.
273 changes: 134 additions & 139 deletions pkg/document/attributes.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,71 +8,136 @@ import (
"slices"
"strings"

"github.com/pkg/errors"
"github.com/yuin/goldmark/ast"
"go.uber.org/multierr"
)

var _defaultAttributeParserWriter = &multiParserWriter{
parsers: []attributesParserWriter{
&jsonParserWriter{},
&htmlAttributesParserWriter{},
},
writer: &jsonParserWriter{},
}

// Attributes represents a set of key-value pairs applicable to [Cell]s.
// More: https://docs.runme.dev/configuration/cell-level
type Attributes map[string]string

type attributeParser interface {
Parse(raw []byte) (Attributes, error)
Write(attr Attributes, w io.Writer) error
// WriteAttributes writes [Attributes] to [io.Writer].
func WriteAttributes(w io.Writer, attr Attributes) error {
return _defaultAttributeParserWriter.Write(w, attr)
}

func getRawAttributes(source []byte) []byte {
start, stop := -1, -1
// parseAttributes parses [Attributes] from raw bytes.
func parseAttributes(raw []byte) (Attributes, error) {
return _defaultAttributeParserWriter.Parse(raw)
}

for i := 0; i < len(source); i++ {
if start == -1 && source[i] == '{' && i+1 < len(source) && source[i+1] != '}' {
start = i + 1
}
if stop == -1 && source[i] == '}' {
stop = i
break
}
type attributesParserWriter interface {
Parse([]byte) (Attributes, error)
Write(io.Writer, Attributes) error
}

func newAttributesFromFencedCodeBlock(
node *ast.FencedCodeBlock,
source []byte,
) (Attributes, error) {
attributes := make(map[string]string)

if node.Info == nil {
return attributes, nil
}

if start >= 0 && stop >= 0 {
return bytes.TrimSpace(source[start-1 : stop+1])
content := node.Info.Value(source)

rawAttributes := extractAttributes(content)
if len(rawAttributes) > 0 {
var err error
attributes, err = parseAttributes(rawAttributes)
if err != nil {
return nil, err
}
}

return nil
return attributes, nil
}

func getAttributes(node *ast.FencedCodeBlock, source []byte, parser attributeParser) (Attributes, error) {
attributes := make(map[string]string)
// jsonParserWriter parses all values as strings.
//
// The correct format is as follows:
//
// { key: "value", hello: "world", string_value: "2" }
type jsonParserWriter struct{}

func (p *jsonParserWriter) Parse(raw []byte) (Attributes, error) {
// Parse first to a generic map.
parsed := make(map[string]interface{})
if err := json.Unmarshal(raw, &parsed); err != nil {
return nil, errors.WithStack(err)
}

if node.Info != nil {
codeBlockInfo := node.Info.Text(source)
rawAttrs := getRawAttributes(codeBlockInfo)
result := make(Attributes, len(parsed))

if len(bytes.TrimSpace(rawAttrs)) > 0 {
attr, err := parser.Parse(rawAttrs)
if err != nil {
return nil, err
// Convert all values to strings.
for k, v := range parsed {
if strVal, ok := v.(string); ok {
result[k] = strVal
} else {
if stringified, err := json.Marshal(v); err == nil {
result[k] = string(stringified)
}

attributes = attr
}
}
return attributes, nil

return result, nil
}

// Original attribute language used by runme prior to v1.3.0
func (p *jsonParserWriter) Write(w io.Writer, attr Attributes) error {
// TODO: name at front...
res, err := json.Marshal(attr)
if err != nil {
return errors.WithStack(err)
}
_, err = w.Write(bytes.TrimSpace(res))
return errors.WithStack(err)
}

// htmlAttributesParserWriter parses and writes options as HTML attributes.
//
// Only supports strings, and does not support spaces. Example:
// For example:
//
// { key=value hello=world string_value=2 }
// { key=value hello=world string_value=2 }
//
// Pioneered by Adam Babik
type babikMLParser struct{}
// Deprecated: Use the JSON parser instead.
type htmlAttributesParserWriter struct{}

func (p *htmlAttributesParserWriter) Parse(raw []byte) (Attributes, error) {
rawAttributes := extractAttributes(raw)
return p.parseRawAttributes(rawAttributes), nil
}

func (p *htmlAttributesParserWriter) Write(w io.Writer, attr Attributes) error {
keys := p.getSortedKeys(attr)

_, _ = w.Write([]byte{'{'})
i := 0
for _, k := range keys {
if i == 0 {
_, _ = w.Write([]byte{' '})
}
v := attr[k]
_, _ = w.Write([]byte(fmt.Sprintf("%s=%s ", k, v)))
i++
}
_, _ = w.Write([]byte{'}'})

func (p *babikMLParser) Parse(raw []byte) (Attributes, error) {
return p.parseRawAttributes(p.rawAttributes(raw)), nil
return nil
}

func (*babikMLParser) parseRawAttributes(source []byte) map[string]string {
items := bytes.Split(source, []byte{' '})
func (*htmlAttributesParserWriter) parseRawAttributes(raw []byte) map[string]string {
items := bytes.Split(raw, []byte{' '})
if len(items) == 0 {
return nil
}
Expand All @@ -90,27 +155,7 @@ func (*babikMLParser) parseRawAttributes(source []byte) map[string]string {
return result
}

func (*babikMLParser) rawAttributes(source []byte) []byte {
start, stop := -1, -1

for i := 0; i < len(source); i++ {
if start == -1 && source[i] == '{' && i+1 < len(source) && source[i+1] != '}' {
start = i + 1
}
if stop == -1 && source[i] == '}' {
stop = i
break
}
}

if start >= 0 && stop >= 0 {
return bytes.TrimSpace(source[start:stop])
}

return nil
}

func (*babikMLParser) sortedAttrs(attr Attributes) []string {
func (*htmlAttributesParserWriter) getSortedKeys(attr Attributes) []string {
keys := make([]string, 0, len(attr))

for k := range attr {
Expand All @@ -136,98 +181,48 @@ func (*babikMLParser) sortedAttrs(attr Attributes) []string {
return keys
}

func (p *babikMLParser) Write(attr Attributes, w io.Writer) error {
keys := p.sortedAttrs(attr)

_, _ = w.Write([]byte{'{', ' '})
i := 0
for _, k := range keys {
v := attr[k]
_, _ = w.Write([]byte(fmt.Sprintf("%s=%v", k, v)))
i++
if i < len(keys) {
_, _ = w.Write([]byte{' '})
}
}
_, _ = w.Write([]byte{' ', '}'})

return nil
// multiParserWriter parses attributes using the provided parsers
// in the order they are provided. If a parser fails, the next one
// is used.
// Writer is used to write the attributes back.
type multiParserWriter struct {
parsers []attributesParserWriter
writer attributesParserWriter
}

// JSON parser
//
// Example:
//
// { key: "value", hello: "world", string_value: "2" }
type jsonParser struct{}

func (p *jsonParser) Parse(raw []byte) (Attributes, error) {
bytes := raw

parsedAttr := make(map[string]interface{})

if err := json.Unmarshal(bytes, &parsedAttr); err != nil {
return nil, err
}

attr := make(Attributes, len(parsedAttr))

for k, v := range parsedAttr {
if strVal, ok := v.(string); ok {
attr[k] = strVal
} else {
if stringified, err := json.Marshal(v); err == nil {
attr[k] = string(stringified)
}
func (p *multiParserWriter) Parse(raw []byte) (_ Attributes, finalErr error) {
for _, parser := range p.parsers {
attr, err := parser.Parse(raw)
if err == nil {
return attr, nil
}
finalErr = multierr.Append(finalErr, err)
}

return attr, nil
}

func (p *jsonParser) Write(attr Attributes, w io.Writer) error {
// TODO: name at front...
res, err := json.Marshal(attr)
if err != nil {
return err
}

res = bytes.TrimSpace(res)

_, _ = w.Write(res)

return nil
}

// failoverAttributeParser tries to parse attributes using one of the provided ordered parsers
// until it finds a non-failing one.
// Attributes are written using the provided writer.
type failoverAttributeParser struct {
parsers []attributeParser
writer attributeParser
return
}

func newFailoverAttributeParser(parsers []attributeParser, writer attributeParser) *failoverAttributeParser {
return &failoverAttributeParser{
parsers,
writer,
}
func (p *multiParserWriter) Write(w io.Writer, attr Attributes) error {
return p.writer.Write(w, attr)
}

func (p *failoverAttributeParser) Parse(raw []byte) (attr Attributes, finalErr error) {
for _, parser := range p.parsers {
attr, err := parser.Parse(raw)
// extractAttributes extracts attributes from the source
// by finding the first `{` and last `}` characters.
func extractAttributes(source []byte) []byte {
start, stop := -1, -1

if err == nil {
return attr, nil
for i := 0; i < len(source); i++ {
if start == -1 && source[i] == '{' && i+1 < len(source) && source[i+1] != '}' {
start = i + 1
}
if stop == -1 && source[i] == '}' {
stop = i
break
}

finalErr = multierr.Append(finalErr, err)
}

return
}
if start >= 0 && stop >= 0 {
return bytes.TrimSpace(source[start-1 : stop+1])
}

func (p *failoverAttributeParser) Write(attr Attributes, w io.Writer) error {
return p.writer.Write(attr, w)
return nil
}
Loading

0 comments on commit 44b5d59

Please sign in to comment.