Add YAML front matter parsing support

Fixes #1. Only a subset of Hugo front matter props are supported, namely
title/date.
This commit is contained in:
Timur Demin 2020-11-10 00:51:11 +05:00
parent 326dc63112
commit b181afc5f1
No known key found for this signature in database
GPG key ID: 9EDF3F9D9286FA20
5 changed files with 93 additions and 17 deletions

View file

@ -13,7 +13,8 @@
// You should have received a copy of the GNU General Public License
// along with gmnhg. If not, see <https://www.gnu.org/licenses/>.
// md2gmn converts Markdown text files to text/gemini.
// md2gmn converts Markdown text files to text/gemini. It panics on
// invalid input.
package main
import (
@ -46,5 +47,10 @@ func main() {
panic(err)
}
os.Stdout.Write(gemini.RenderMarkdown(text))
geminiContent, err := gemini.RenderMarkdown(text)
if err != nil {
panic(err)
}
os.Stdout.Write(geminiContent)
}

5
go.mod
View file

@ -2,4 +2,7 @@ module git.tdem.in/tdemin/gmnhg
go 1.15
require github.com/gomarkdown/markdown v0.0.0-20201024011455-45c732cc8a6b
require (
github.com/gomarkdown/markdown v0.0.0-20201024011455-45c732cc8a6b
gopkg.in/yaml.v2 v2.3.0
)

3
go.sum
View file

@ -1,3 +1,6 @@
github.com/gomarkdown/markdown v0.0.0-20201024011455-45c732cc8a6b h1:Om9FdD4lzIJELyJxwr9EWSjaG6GMUNS3iebnhrGevhI=
github.com/gomarkdown/markdown v0.0.0-20201024011455-45c732cc8a6b/go.mod h1:aii0r/K0ZnHv7G0KF7xy1v0A7s2Ljrb5byB7MO5p6TU=
golang.org/dl v0.0.0-20190829154251-82a15e2f2ead/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View file

@ -22,6 +22,7 @@ import (
"bytes"
"fmt"
"io"
"time"
"github.com/gomarkdown/markdown/ast"
)
@ -35,14 +36,30 @@ var (
itemIndent = []byte{'\t'}
)
const timestampFormat = "2006-01-02 15:04"
// Metadata provides data necessary for proper post rendering.
type Metadata interface {
Title() string
Date() time.Time
}
// Renderer implements markdown.Renderer.
type Renderer struct{}
type Renderer struct {
Metadata Metadata
}
// NewRenderer returns a new Renderer.
func NewRenderer() Renderer {
return Renderer{}
}
// NewRendererWithMetadata returns a new Renderer initialized with post
// metadata.
func NewRendererWithMetadata(m Metadata) Renderer {
return Renderer{Metadata: m}
}
func (r Renderer) link(w io.Writer, node *ast.Link, entering bool) {
if entering {
w.Write(linkPrefix)
@ -290,12 +307,15 @@ func (r Renderer) RenderNode(w io.Writer, node ast.Node, entering bool) ast.Walk
return ast.GoToNext
}
// RenderHeader implements Renderer.RenderHeader().
// RenderHeader implements Renderer.RenderHeader(). It renders metadata
// at the top of the post if any has been provided.
func (r Renderer) RenderHeader(w io.Writer, node ast.Node) {
// likely doesn't need any code
if r.Metadata != nil {
// TODO: Renderer.RenderHeader: check whether date is mandatory
// in Hugo
w.Write([]byte(fmt.Sprintf("# %s\n\n%s\n\n", r.Metadata.Title(), r.Metadata.Date().Format(timestampFormat))))
}
}
// RenderFooter implements Renderer.RenderFooter().
func (r Renderer) RenderFooter(w io.Writer, node ast.Node) {
// likely doesn't need any code either
}
func (r Renderer) RenderFooter(w io.Writer, node ast.Node) {}

View file

@ -14,20 +14,64 @@
// along with gmnhg. If not, see <https://www.gnu.org/licenses/>.
// Package gemini provides functions to convert Markdown files to
// Gemtext.
// Gemtext. It supports the use of YAML front matter in Markdown.
package gemini
import (
"bytes"
"fmt"
"time"
"git.tdem.in/tdemin/gmnhg/internal/gemini"
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/parser"
"gopkg.in/yaml.v2"
)
// RenderMarkdown converts Markdown text to text/gemini using gomarkdown.
//
// gomarkdown doesn't return any errors, nor does this function.
func RenderMarkdown(md []byte) (geminiText []byte) {
ast := markdown.Parse(md, parser.NewWithExtensions(parser.CommonExtensions))
geminiContent := markdown.Render(ast, gemini.NewRenderer())
return geminiContent
// hugoMetadata implements gemini.Metadata, providing the bare minimum
// of possible post props.
type hugoMetadata struct {
PostTitle string `yaml:"title"`
PostDate time.Time `yaml:"date"`
}
func (h hugoMetadata) Title() string {
return h.PostTitle
}
func (h hugoMetadata) Date() time.Time {
return h.PostDate
}
var yamlDelimiter = []byte("---\n")
// RenderMarkdown converts Markdown text to text/gemini using
// gomarkdown, appending Hugo YAML front matter data if any is present
// to the post header.
func RenderMarkdown(md []byte) (geminiText []byte, err error) {
var metadata hugoMetadata
if len(md) > len(yamlDelimiter)*2 {
// only allow front matter at file start
if bytes.Index(md, yamlDelimiter) != 0 {
goto parse
}
blockEnd := bytes.Index(md[len(yamlDelimiter):], yamlDelimiter)
if blockEnd == -1 {
goto parse
}
yamlContent := md[len(yamlDelimiter) : blockEnd+len(yamlDelimiter)]
if err := yaml.Unmarshal(yamlContent, &metadata); err != nil {
return nil, fmt.Errorf("invalid front matter: %w", err)
}
md = md[blockEnd+len(yamlDelimiter)*2:]
}
parse:
ast := markdown.Parse(md, parser.NewWithExtensions(parser.CommonExtensions))
var geminiContent []byte
if metadata.PostTitle != "" {
geminiContent = markdown.Render(ast, gemini.NewRendererWithMetadata(metadata))
} else {
geminiContent = markdown.Render(ast, gemini.NewRenderer())
}
return geminiContent, nil
}