Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/frontmatter yaml parser #206

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion ansi/codeblock.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,12 @@ func (e *CodeBlockElement) Render(w io.Writer, ctx RenderContext) error {
mutex.Unlock()
}

ic := " "
if rules.IndentToken != nil {
ic = *rules.IndentToken
}
iw := indent.NewWriterPipe(w, indentation+margin, func(wr io.Writer) {
renderText(w, ctx.options.ColorProfile, bs.Current().Style.StylePrimitive, " ")
renderText(w, ctx.options.ColorProfile, bs.Current().Style.StylePrimitive, ic)
})

if len(theme) > 0 {
Expand Down
2 changes: 0 additions & 2 deletions ansi/elements.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@ type Element struct {
// NewElement returns the appropriate render Element for a given node.
func (tr *ANSIRenderer) NewElement(node ast.Node, source []byte) Element {
ctx := tr.context
// fmt.Print(strings.Repeat(" ", ctx.blockStack.Len()), node.Type(), node.Kind())
// defer fmt.Println()

switch node.Kind() {
// Document
Expand Down
6 changes: 6 additions & 0 deletions ansi/renderer.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ansi

import (
localast "github.com/charmbracelet/glamour/extension/ast"
"io"
"net/url"
"strings"
Expand Down Expand Up @@ -37,6 +38,7 @@ func NewRenderer(options Options) *ANSIRenderer {
// RegisterFuncs implements NodeRenderer.RegisterFuncs.
func (r *ANSIRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
// blocks
reg.Register(localast.KindFrontmatter, r.handleFrontmatter)
reg.Register(ast.KindDocument, r.renderNode)
reg.Register(ast.KindHeading, r.renderNode)
reg.Register(ast.KindBlockquote, r.renderNode)
Expand Down Expand Up @@ -86,6 +88,10 @@ func (r *ANSIRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(east.KindEmoji, r.renderNode)
}

func (r *ANSIRenderer) handleFrontmatter(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
return ast.WalkContinue, nil
}

func (r *ANSIRenderer) renderNode(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
// _, _ = w.Write([]byte(node.Type.String()))
writeTo := io.Writer(w)
Expand Down
18 changes: 18 additions & 0 deletions extension/_test/frontmatter.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
1
//- - - - - - - - -//
---
title: A Title
longText: |
Long text spanning multiple
lines of input
number: 120
---
//- - - - - - - - -//
<!--
longText: Long text spanning multiple
lines of input

number: 120
title: A Title
-->
//= = = = = = = = = = = = = = = = = = = = = = = =//
29 changes: 29 additions & 0 deletions extension/ast/frontmatter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package ast

import (
"fmt"
"github.com/yuin/goldmark/ast"
)

// Frontmatter AST Node holding parsed YAML Frontmatter.
// Status Parse Error or empty.
type Frontmatter struct {
ast.BaseBlock
MetaData map[interface{}]interface{}
Status string
}

var KindFrontmatter = ast.NewNodeKind("Frontmatter")

func (f Frontmatter) Kind() ast.NodeKind {
return KindFrontmatter
}

func (f *Frontmatter) Dump(source []byte, level int) {
m := map[string]string{}
m["_status"] = fmt.Sprintf("%v", f.Status)
for key, value := range f.MetaData {
m[fmt.Sprintf("%v", key)] = fmt.Sprintf("%v", value)
}
ast.DumpHelper(f, source, level, m, nil)
}
138 changes: 138 additions & 0 deletions extension/frontmatter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package extension

import (
"bytes"
"fmt"
localast "github.com/charmbracelet/glamour/extension/ast"
"github.com/yuin/goldmark"
gast "github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
"gopkg.in/yaml.v3"
"regexp"
"sort"
"strings"
)

type FrontmatterResultConsumer interface {
HandleFrontmatter(frontmatter map[string]interface{})
}

type frontMatterParser struct {
Handler FrontmatterResultConsumer
}

func (f frontMatterParser) Trigger() []byte {
return []byte("---")
}

func (f frontMatterParser) Open(parent gast.Node, reader text.Reader, pc parser.Context) (gast.Node, parser.State) {
line, _ := reader.PeekLine()
line = bytes.TrimRight(line, "\r\n")
if matched, _ := regexp.Match("^-{3,}$", line); matched {
reader.AdvanceLine()

// read all lines and interpret as yaml
var buf bytes.Buffer
var y map[interface{}]interface{}
for {
line, _ = reader.PeekLine()
if matched, _ := regexp.Match("^-{3,}$", []byte(strings.TrimRight(string(line), "\r\n"))); !matched {

buf.Write(line)
} else {
break
}
reader.AdvanceLine()
}
if err := yaml.Unmarshal(buf.Bytes(), &y); err != nil {
fmt.Errorf("unable to parse Frontmatter as YAML %s", err.Error())
return &localast.Frontmatter{MetaData: nil, Status: err.Error()}, parser.NoChildren
}
result := localast.Frontmatter{MetaData: y}
if f.Handler != nil {
var m = make(map[string]interface{})
for key, value := range y {
m[fmt.Sprintf("%v", key)] = value
}
f.Handler.HandleFrontmatter(m)
}
return &result, parser.NoChildren
}
return nil, parser.NoChildren
}

func (f frontMatterParser) Continue(node gast.Node, reader text.Reader, pc parser.Context) parser.State {
// all parsing done in Open already
return parser.Close
}

func (f frontMatterParser) Close(node gast.Node, reader text.Reader, pc parser.Context) {
// nothing to do
}

func (f frontMatterParser) CanInterruptParagraph() bool {
return false
}

func (f frontMatterParser) CanAcceptIndentedLine() bool {
return false
}

var DefaultFrontMatterParser = &frontMatterParser{}

func NewFrontMatterParser() parser.BlockParser {
return DefaultFrontMatterParser
}

type FrontmatterHTMLRenderer struct {
Config html.Config
}

// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
func (r *FrontmatterHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(localast.KindFrontmatter, r.renderFrontmatterStart)
}

func (r *FrontmatterHTMLRenderer) renderFrontmatterStart(w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
if entering {
w.WriteString("<!--\n")
if node, ok := n.(*localast.Frontmatter); ok {
if node.Status != "" || node.MetaData == nil {
w.WriteString(node.Status)
} else {
keys := make([]string, 0, len(node.MetaData))
for key := range node.MetaData {
keys = append(keys, fmt.Sprintf("%v", key))
}
sort.Strings(keys)
for _, key := range keys {
w.WriteString(fmt.Sprintf("%s: %v\n", key, node.MetaData[key]))
}
}
}
} else {
w.WriteString("-->\n")
}
return gast.WalkContinue, nil
}

func NewFrontmatterHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
r := &FrontmatterHTMLRenderer{
Config: html.NewConfig(),
}
for _, opt := range opts {
opt.SetHTMLOption(&r.Config)
}
return r
}

func (f frontMatterParser) Extend(m goldmark.Markdown) {
m.Parser().AddOptions(
parser.WithBlockParsers(
util.Prioritized(NewFrontMatterParser(), 99)))
m.Renderer().AddOptions(renderer.WithNodeRenderers(util.Prioritized(NewFrontmatterHTMLRenderer(), 99)))
}
20 changes: 20 additions & 0 deletions extension/frontmatter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package extension

import (
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/testutil"
"testing"
)

func TestFrontMatterParsing(t *testing.T) {
markdown := goldmark.New(
goldmark.WithRendererOptions(
html.WithUnsafe(),
),
goldmark.WithExtensions(
DefaultFrontMatterParser,
),
)
testutil.DoTestCaseFile(markdown, "_test/frontmatter.txt", t, testutil.ParseCliCaseArg()...)
}
33 changes: 29 additions & 4 deletions glamour.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"os"

ext "github.com/charmbracelet/glamour/extension"
"github.com/muesli/termenv"
"github.com/yuin/goldmark"
emoji "github.com/yuin/goldmark-emoji"
Expand Down Expand Up @@ -39,10 +40,11 @@ type TermRendererOption func(*TermRenderer) error
// TermRenderer can be used to render markdown content, posing a depth of
// customization and styles to fit your needs.
type TermRenderer struct {
md goldmark.Markdown
ansiOptions ansi.Options
buf bytes.Buffer
renderBuf bytes.Buffer
md goldmark.Markdown
ansiOptions ansi.Options
buf bytes.Buffer
renderBuf bytes.Buffer
frontMatterHandler ext.FrontmatterResultConsumer
}

// Render initializes a new TermRenderer and renders a markdown with a specific
Expand Down Expand Up @@ -71,13 +73,20 @@ func RenderBytes(in []byte, stylePath string) ([]byte, error) {
return r.RenderBytes(in)
}

func (tr *TermRenderer) HandleFrontmatter(frontmatter map[string]interface{}) {
if tr.frontMatterHandler != nil {
tr.frontMatterHandler.HandleFrontmatter(frontmatter)
}
}

// NewTermRenderer returns a new TermRenderer the given options.
func NewTermRenderer(options ...TermRendererOption) (*TermRenderer, error) {
tr := &TermRenderer{
md: goldmark.New(
goldmark.WithExtensions(
extension.GFM,
extension.DefinitionList,
ext.DefaultFrontMatterParser,
),
goldmark.WithParserOptions(
parser.WithAutoHeadingID(),
Expand All @@ -88,6 +97,8 @@ func NewTermRenderer(options ...TermRendererOption) (*TermRenderer, error) {
ColorProfile: termenv.TrueColor,
},
}
// register Termrenderer as Callback for Frontmatter
ext.DefaultFrontMatterParser.Handler = tr
for _, o := range options {
if err := o(tr); err != nil {
return nil, err
Expand All @@ -112,6 +123,20 @@ func WithBaseURL(baseURL string) TermRendererOption {
}
}

func WithFrontMatterRenderer(r renderer.Renderer) TermRendererOption {
return func(termRenderer *TermRenderer) error {
termRenderer.md.Renderer().AddOptions(renderer.WithNodeRenderers(util.Prioritized(r, 99)))
return nil
}
}

func WithFrontMatterHandler(consumer ext.FrontmatterResultConsumer) TermRendererOption {
return func(termRenderer *TermRenderer) error {
termRenderer.frontMatterHandler = consumer
return nil
}
}

// WithColorProfile sets the TermRenderer's color profile
// (TrueColor / ANSI256 / ANSI).
func WithColorProfile(profile termenv.Profile) TermRendererOption {
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/olekukonko/tablewriter v0.0.5
github.com/yuin/goldmark v1.7.4
github.com/yuin/goldmark-emoji v1.0.3
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c
)

require (
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,6 @@ golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Loading