goldmark-mmd

A different way of handling Markdown Metadata
git clone git://src.gearsix.net/goldmark-mmd
Log | Files | Refs | Atom | README | LICENSE

commit 91ef461d76584b602ff9e6e8ae2abb434072f3e0
parent c86faa6ef073de09500c1c60d09abdb314fcf1f1
Author: gearsix <gearsix@tuta.io>
Date:   Mon, 20 Feb 2023 15:45:39 +0000

added unmarshalling for *json* and *toml* (using dati)

metadata isn't being parsed line-by-line anymore, an open token and
close token can be on the same line (Open checks for a Close) and the
metadata content will all be parsed and loaded if they are (skipping any
parsing outside of the Open call).

- minor fixes to meta_test.go sources
- added notabug.org/gearsix/dati dependency for loading metdata
- fixed json parsing (requiring diff open / close signals)
- fixes that make sure yaml parsing works correctly

bugs:
- Currently *toml* parsing is causing the body paragraph to be rendered
  twice?

Diffstat:
Mmeta.go | 83+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Mmeta_test.go | 35++++++++++++++++++++---------------
2 files changed, 72 insertions(+), 46 deletions(-)

diff --git a/meta.go b/meta.go @@ -13,15 +13,13 @@ import ( "github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/text" "github.com/yuin/goldmark/util" - - "gopkg.in/yaml.v2" + "notabug.org/gearsix/dati" ) type metadata map[string]interface{} type data struct { Map metadata - Items yaml.MapSlice Error error Node gast.Node } @@ -61,7 +59,8 @@ const openToken = "<!--" const closeToken = "-->" const formatYaml = ':' const formatToml = '#' -const formatJson = '{' +const formatJsonOpen = '{' +const formatJsonClose = '}' type metaParser struct { format byte @@ -84,7 +83,7 @@ func isOpen(line []byte) bool { fallthrough case formatToml: fallthrough - case formatJson: + case formatJsonOpen: return true default: break @@ -94,17 +93,24 @@ func isOpen(line []byte) bool { return false } -func isClose(line []byte, signal byte) bool { - line = util.TrimRightSpace(util.TrimLeftSpace(line)) +// isClose will check `line` for the closing token. +// If found, the integer returned will be the *nth* byte of `line` that the close token starts at. +// If not found, then -1 is returned. +func isClose(line []byte, signal byte) int { + //line = util.TrimRightSpace(util.TrimLeftSpace(line)) for i := 0; i < len(line); i++ { - if len(line[i:]) >= len(closeToken)+1 && line[i] == signal { + if line[i] == signal && len(line[i:]) >= len(closeToken)+1 { i++ if string(line[i:i+len(closeToken)]) == closeToken { - return true + if signal == formatJsonClose { + return i + } else { + return i - 1 + } } } } - return false + return -1 } func (b *metaParser) Trigger() []byte { @@ -117,25 +123,53 @@ func (b *metaParser) Open(parent gast.Node, reader text.Reader, pc parser.Contex return nil, parser.NoChildren } line, _ := reader.PeekLine() + if isOpen(line) { reader.Advance(len(openToken)) - b.format = reader.Peek() - reader.Advance(1) - return gast.NewTextBlock(), parser.NoChildren + if b.format = reader.Peek(); b.format == formatJsonOpen { + b.format = formatJsonClose + } else { + reader.Advance(1) + } + + node := gast.NewTextBlock() + if state := b.Continue(node, reader, pc); state == parser.Close { + parent.AppendChild(parent, node) + b.Close(node, reader, pc) + } + return node, parser.NoChildren } return nil, parser.NoChildren } func (b *metaParser) Continue(node gast.Node, reader text.Reader, pc parser.Context) parser.State { line, segment := reader.PeekLine() - if isClose(line, b.format) && !util.IsBlank(line) { - reader.Advance(segment.Len()) + if n := isClose(line, b.format); n != -1 && !util.IsBlank(line) { + segment.Stop -= len(line[n:]) + node.Lines().Append(segment) + reader.Advance((n + 1) + len(closeToken)) return parser.Close } node.Lines().Append(segment) return parser.Continue | parser.NoChildren } +func (b *metaParser) loadMetadata(buf []byte) (meta metadata, err error) { + var format dati.DataFormat + switch b.format { + case formatYaml: + format = dati.YAML + case formatToml: + format = dati.TOML + case formatJsonClose: + format = dati.JSON + default: + return meta, dati.ErrUnsupportedData(string(b.format)) + } + err = dati.LoadData(format, bytes.NewReader(buf), &meta) + return meta, err +} + func (b *metaParser) Close(node gast.Node, reader text.Reader, pc parser.Context) { lines := node.Lines() var buf bytes.Buffer @@ -143,21 +177,8 @@ func (b *metaParser) Close(node gast.Node, reader text.Reader, pc parser.Context segment := lines.At(i) buf.Write(segment.Value(reader.Source())) } - d := &data{} - d.Node = node - meta := metadata{} - if err := yaml.Unmarshal(buf.Bytes(), &meta); err != nil { - d.Error = err - } else { - d.Map = meta - } - - metaMapSlice := yaml.MapSlice{} - if err := yaml.Unmarshal(buf.Bytes(), &metaMapSlice); err != nil { - d.Error = err - } else { - d.Items = metaMapSlice - } + d := &data{Node: node} + d.Map, d.Error = b.loadMetadata(buf.Bytes()) pc.Set(contextKey, d) @@ -167,7 +188,7 @@ func (b *metaParser) Close(node gast.Node, reader text.Reader, pc parser.Context } func (b *metaParser) CanInterruptParagraph() bool { - return false + return true } func (b *metaParser) CanAcceptIndentedLine() bool { diff --git a/meta_test.go b/meta_test.go @@ -14,16 +14,19 @@ var validSource = map[string]string{ Title: mmd Summary: Add YAML metadata to the document Tags: -- markdown -- goldmark + - markdown + - goldmark :--> -This is markdown with YAML metadata +Markdown with metadata `, - "json": `<!--{ "Title": "mmd", "Summary": "Add JSON metadata to the document", "Tags": [ "markdown", "goldmark" ] }-->`, + "json": `<!--{ "Title": "mmd", "Summary": "Add JSON metadata to the document", "Tags": [ "markdown", "goldmark" ] }--> +Markdown with metadata`, "toml": `<!--# Title = "mmd" - Summary = "Add TOML metadata to the document - Tags = [ "markdown", "goldmark" ] #-->`, + Summary = "Add TOML metadata to the document" + Tags = [ "markdown", "goldmark" ] #--> +Markdown with metadata +`, } var invalidSource = map[string]string{ "yaml": `<!--: @@ -36,12 +39,14 @@ Tags: - goldmark :--> -This is markdown with YAML metadata -`, - "json": `<!--{ "Title:" "mmd", "Summary": "Add JSON metadata to the document", "Tags": [ "markdown", "goldmark" ] }-->`, +Markdown with metadata`, + "json": `<!--{ "Title:" "mmd", "Summary": "Add JSON metadata to the document", "Tags": [ "markdown", "goldmark" ] }--> +Markdown with metadata`, "toml": `<!--# Title = "mmd" Summary = "Add TOML metadata to the document - Tags == [ markdown", "goldmark ] #-->`, + Tags == [ markdown", "goldmark ] #--> +Markdown with metadata +`, } func TestMeta(t *testing.T) { @@ -64,12 +69,12 @@ func TestMeta(t *testing.T) { title := metaData["Title"] if s, ok := title.(string); !ok { t.Errorf("%s: Title not found in meta data or is not a string", format) - } else if s != "goldmark-meta" { - t.Errorf("%s: Title must be %s, but got %v", "goldmark-meta", format, s) + } else if s != "mmd" { + t.Errorf("%s: Title must be 'mmd', but got %v", format, s) } - if buf.String() != "<p>Hello goldmark-meta</p>\n" { - t.Errorf("%s: should render '<p>Hello goldmark-meta</p>', but '%s'", format, buf.String()) + if buf.String() != "<p>Markdown with metadata</p>\n" { + t.Errorf("%s: should render '<p>Markdown with metadata</p>', but '%s'", format, buf.String()) } if tags, ok := metaData["Tags"].([]interface{}); !ok { @@ -116,7 +121,7 @@ func TestMetaError(t *testing.T) { t.Fatal(err) } if buf.String() != `<!-- toml: line 3: did not find expected key --> -<p>This is markdown with TOML metadata</p> +<p>Markdown with metadata</p> ` { t.Error("toml: invalid error output") }