diff --git a/cmd/md2gmn/main.go b/cmd/md2gmn/main.go
index 5a30380..3d7ed9a 100644
--- a/cmd/md2gmn/main.go
+++ b/cmd/md2gmn/main.go
@@ -13,7 +13,8 @@
// You should have received a copy of the GNU General Public License
// along with gmnhg. If not, see .
-// 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)
}
diff --git a/go.mod b/go.mod
index 8f09c36..19fa428 100644
--- a/go.mod
+++ b/go.mod
@@ -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
+)
diff --git a/go.sum b/go.sum
index 6b74de0..08d7486 100644
--- a/go.sum
+++ b/go.sum
@@ -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=
diff --git a/internal/gemini/renderer.go b/internal/gemini/renderer.go
index d4abc01..5b1792e 100644
--- a/internal/gemini/renderer.go
+++ b/internal/gemini/renderer.go
@@ -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) {}
diff --git a/render.go b/render.go
index 5bbe75e..ab1d7f7 100644
--- a/render.go
+++ b/render.go
@@ -14,20 +14,64 @@
// along with gmnhg. If not, see .
// 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
}