Add support for YAML frontmatters

pull/6/head
Mickaël Menu 3 years ago
parent 225b0c2603
commit 0512bf78b4
No known key found for this signature in database
GPG Key ID: 53D73664CD359895

@ -2,12 +2,15 @@ package markdown
import (
"bufio"
"regexp"
"strings"
"github.com/mickael-menu/zk/core/note"
"github.com/mickael-menu/zk/util/opt"
"github.com/yuin/goldmark"
meta "github.com/yuin/goldmark-meta"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/text"
)
@ -19,7 +22,11 @@ type Parser struct {
// NewParser creates a new Markdown Parser.
func NewParser() *Parser {
return &Parser{
md: goldmark.New(),
md: goldmark.New(
goldmark.WithExtensions(
meta.Meta,
),
),
}
}
@ -28,22 +35,37 @@ func (p *Parser) Parse(source string) (note.Content, error) {
out := note.Content{}
bytes := []byte(source)
root := p.md.Parser().Parse(text.NewReader(bytes))
title, titleNode, err := parseTitle(root, bytes)
context := parser.NewContext()
root := p.md.Parser().Parse(
text.NewReader(bytes),
parser.WithContext(context),
)
frontmatter, err := parseFrontmatter(context, bytes)
if err != nil {
return out, err
}
title, bodyStart, err := parseTitle(frontmatter, root, bytes)
if err != nil {
return out, err
}
out.Title = title
out.Body = parseBody(titleNode, bytes)
out.Body = parseBody(bodyStart, bytes)
out.Lead = parseLead(out.Body)
return out, nil
}
// parseTitle extracts the note title with its node.
func parseTitle(root ast.Node, source []byte) (title opt.String, node ast.Node, err error) {
func parseTitle(frontmatter frontmatter, root ast.Node, source []byte) (title opt.String, bodyStart int, err error) {
if title = frontmatter.getString("title", "Title"); !title.IsNull() {
bodyStart = frontmatter.end
return
}
var titleNode *ast.Heading
err = ast.Walk(root, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
if heading, ok := n.(*ast.Heading); ok && entering &&
@ -62,24 +84,20 @@ func parseTitle(root ast.Node, source []byte) (title opt.String, node ast.Node,
}
if titleNode != nil {
node = titleNode
title = opt.NewNotEmptyString(string(titleNode.Text(source)))
}
return
}
// parseBody extracts the whole content after the title.
func parseBody(titleNode ast.Node, source []byte) opt.String {
start := 0
if titleNode != nil {
if lines := titleNode.Lines(); lines.Len() > 0 {
start = lines.At(lines.Len() - 1).Stop
bodyStart = lines.At(lines.Len() - 1).Stop
}
}
return
}
// parseBody extracts the whole content after the title.
func parseBody(startIndex int, source []byte) opt.String {
return opt.NewNotEmptyString(
strings.TrimSpace(
string(source[start:]),
string(source[startIndex:]),
),
)
}
@ -97,3 +115,38 @@ func parseLead(body opt.String) opt.String {
return opt.NewNotEmptyString(strings.TrimSpace(lead))
}
// frontmatter contains metadata parsed from a YAML frontmatter.
type frontmatter struct {
values map[string]interface{}
start int
end int
}
var frontmatterRegex = regexp.MustCompile(`(?ms)^\s*-+\s*$.*?^\s*-+\s*$`)
func parseFrontmatter(context parser.Context, source []byte) (front frontmatter, err error) {
index := frontmatterRegex.FindIndex(source)
if index != nil {
front.start = index[0]
front.end = index[1]
front.values, err = meta.TryGet(context)
}
return
}
// getString returns the first string value found for any of the given keys.
func (m frontmatter) getString(keys ...string) opt.String {
if m.values == nil {
return opt.NullString
}
for _, key := range keys {
if val, ok := m.values[key]; ok {
if val, ok := val.(string); ok {
return opt.NewNotEmptyString(val)
}
}
}
return opt.NullString
}

@ -25,6 +25,22 @@ func TestParseTitle(t *testing.T) {
test("# Heading 1\n## Heading 1.a\n# Heading 2", "Heading 1")
test("## Small Heading\n# Bigger Heading", "Bigger Heading")
test("# A **title** with [formatting](http://stripped)", "A title with formatting")
// From a YAML frontmatter
test(`---
Title: A title
Tags:
- tag1
- tag2
---
# Heading
`, "A title")
test(`---
title: lowercase key
---
Paragraph
`, "lowercase key")
}
func TestParseBody(t *testing.T) {
@ -63,6 +79,12 @@ Paragraph:
* item1
* item2`,
)
test(`---
title: A title
---
Paragraph
`, "Paragraph")
}
func TestParseLead(t *testing.T) {

@ -25,6 +25,7 @@ require (
github.com/tebeka/strftime v0.1.5 // indirect
github.com/tj/go-naturaldate v1.3.0
github.com/yuin/goldmark v1.3.1
github.com/yuin/goldmark-meta v1.0.0
golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78 // indirect
gopkg.in/djherbis/times.v1 v1.2.0
gopkg.in/yaml.v2 v2.4.0 // indirect

@ -165,8 +165,11 @@ github.com/tj/assert v0.0.0-20190920132354-ee03d75cd160/go.mod h1:mZ9/Rh9oLWpLLD
github.com/tj/go-naturaldate v1.3.0 h1:OgJIPkR/Jk4bFMBLbxZ8w+QUxwjqSvzd9x+yXocY4RI=
github.com/tj/go-naturaldate v1.3.0/go.mod h1:rpUbjivDKiS1BlfMGc2qUKNZ/yxgthOfmytQs8d8hKk=
github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.1 h1:eVwehsLsZlCJCwXyGLgg+Q4iFWE/eTIMG0e8waCmm/I=
github.com/yuin/goldmark v1.3.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark-meta v1.0.0 h1:ScsatUIT2gFS6azqzLGUjgOnELsBOxMXerM3ogdJhAM=
github.com/yuin/goldmark-meta v1.0.0/go.mod h1:zsNNOrZ4nLuyHAJeLQEZcQat8dm70SmB2kHbls092Gc=
github.com/zclconf/go-cty v1.2.0 h1:sPHsy7ADcIZQP3vILvTjrh74ZA175TFP5vqiNK1UmlI=
github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
@ -238,5 +241,6 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

Loading…
Cancel
Save