From 3756ef366eb2e9cfbfbdbbc1c07b369e830fe58c Mon Sep 17 00:00:00 2001 From: Chad Kunde Date: Sun, 24 Jan 2021 13:12:11 +0800 Subject: [PATCH] use goldmark ecosystem to define options Allow extensions to register rendering functions. --- extended/main_test.go | 62 +++++++++++++++++++++++++++++++++ go.mod | 3 ++ go.sum | 8 +++++ main.go | 3 +- markdown/renderer.go | 71 +++++++++++++++++++++++++++++--------- markdown/writer_indent.go | 5 +-- markdownfmt/markdownfmt.go | 16 ++++----- 7 files changed, 140 insertions(+), 28 deletions(-) create mode 100644 extended/main_test.go diff --git a/extended/main_test.go b/extended/main_test.go new file mode 100644 index 0000000..9427db9 --- /dev/null +++ b/extended/main_test.go @@ -0,0 +1,62 @@ +package main + +import ( + "bytes" + "fmt" + "testing" + + "github.com/Kunde21/markdownfmt/v2/markdownfmt" + meta "github.com/yuin/goldmark-meta" + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/renderer" + "github.com/yuin/goldmark/util" +) + +type metaRender struct{} + +// RegisterFuncs ... +func (m metaRender) RegisterFuncs(r renderer.NodeRendererFuncRegisterer) { + r.Register(meta.KindMetadata, renderMeta) +} + +func renderMeta(w util.BufWriter, src []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { + if !entering { + return ast.WalkContinue, nil + } + m, ok := n.(*meta.Metadata) + if !ok { + fmt.Fprintf(w, "%v", n) + } + fmt.Fprintln(w, "---------------------") + for _, v := range m.Items { + fmt.Fprintf(w, "%s: %s\n", v.Key, v.Value) + } + fmt.Fprintln(w, "---------------------") + return ast.WalkContinue, nil +} + +func TestMeta(t *testing.T) { + mdfmt := markdownfmt.NewGoldmark() + meta.New().Extend(mdfmt) + mdfmt.Renderer().AddOptions( + renderer.WithNodeRenderers( + util.Prioritized(metaRender{}, 500), + ), + ) + source := `--- +Title: goldmark-meta +Summary: Add YAML metadata to the document +Tags: + - markdown + - goldmark +--- + +# Hello goldmark-meta +` + + var buf bytes.Buffer + if err := mdfmt.Convert([]byte(source), &buf); err != nil { + panic(err) + } + fmt.Print(buf.String()) +} diff --git a/go.mod b/go.mod index ee1eff2..668a6a7 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,9 @@ require ( github.com/mattn/go-runewidth v0.0.9 github.com/pkg/errors v0.9.1 github.com/yuin/goldmark v1.3.1 + github.com/yuin/goldmark-meta v1.0.0 ) +replace github.com/yuin/goldmark-meta => github.com/13rac1/goldmark-meta v1.0.1-0.20201214084408-d2487db1f3f5 + go 1.13 diff --git a/go.sum b/go.sum index d7a782d..a8c51cc 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,14 @@ +github.com/13rac1/goldmark-meta v1.0.1-0.20201214084408-d2487db1f3f5 h1:mGs3zaTiNwPYyUpHfSdv2SOzQBXWHQNws7FtHrgeyrI= +github.com/13rac1/goldmark-meta v1.0.1-0.20201214084408-d2487db1f3f5/go.mod h1:zsNNOrZ4nLuyHAJeLQEZcQat8dm70SmB2kHbls092Gc= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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= +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/main.go b/main.go index ab6976c..e803119 100644 --- a/main.go +++ b/main.go @@ -15,6 +15,7 @@ import ( "github.com/Kunde21/markdownfmt/v2/markdown" "github.com/Kunde21/markdownfmt/v2/markdownfmt" + "github.com/yuin/goldmark/renderer" ) var ( @@ -58,7 +59,7 @@ func processFile(filename string, in io.Reader, out io.Writer) error { return err } - var opts []markdown.Option + var opts []renderer.Option if *underlineHeadings { opts = append(opts, markdown.WithUnderlineHeadings()) } diff --git a/markdown/renderer.go b/markdown/renderer.go index 9d4cddc..8918890 100644 --- a/markdown/renderer.go +++ b/markdown/renderer.go @@ -33,56 +33,85 @@ var _ renderer.Renderer = &Renderer{} // Renderer allows to render markdown AST into markdown bytes in consistent format. // Render is reusable across Renders, it holds configuration only. type Renderer struct { + conf *renderer.Config underlineHeadings bool } -func (mr *Renderer) AddOptions(...renderer.Option) { - // goldmark weirdness, just ignore (called with just HTML options...) -} - -func (mr *Renderer) AddMarkdownOptions(opts ...Option) { +func (mr *Renderer) AddOptions(opts ...renderer.Option) { for _, o := range opts { - o(mr) + o.SetConfig(mr.conf) } } -type Option func(r *Renderer) +type Option func(r *renderer.Config) -func WithUnderlineHeadings() Option { - return func(r *Renderer) { - r.underlineHeadings = true - } +func (o Option) SetConfig(r *renderer.Config) { o(r) } + +func WithUnderlineHeadings() renderer.Option { + return Option(func(r *renderer.Config) { + if r.Options == nil { + r.Options = map[renderer.OptionName]interface{}{} + } + r.Options["markdownfmt.underlineHeadings"] = true + }) } func NewRenderer() *Renderer { - return &Renderer{} + return &Renderer{conf: &renderer.Config{}} } // render represents a single markdown rendering operation. type render struct { - mr *Renderer + mr *Renderer + overrides map[ast.NodeKind]renderer.NodeRendererFunc // TODO(bwplotka): Wrap it with something that catch errors. w *lineIndentWriter source []byte } +// Register override method +func (r *render) Register(k ast.NodeKind, f renderer.NodeRendererFunc) { + if r.overrides == nil { + r.overrides = map[ast.NodeKind]renderer.NodeRendererFunc{} + } + r.overrides[k] = f +} + func (mr *Renderer) newRender(w io.Writer, source []byte) *render { - return &render{ + if op, ok := mr.conf.Options["markdownfmt.underlineHeadings"]; ok { + mr.underlineHeadings = op.(bool) + } + mr.conf.NodeRenderers.Sort() + rdr := &render{ mr: mr, w: wrapWithLineIndentWriter(w), source: source, } + + for _, nr := range mr.conf.NodeRenderers { + if nr.Value == nil { + continue + } + if r, ok := nr.Value.(renderer.NodeRenderer); ok { + r.RegisterFuncs(rdr) + } + } + + return rdr } // Render renders the given AST node to the given buffer with the given Renderer. // NOTE: This is the entry point used by Goldmark. func (mr *Renderer) Render(w io.Writer, source []byte, node ast.Node) error { // Perform DFS. - return ast.Walk(node, mr.newRender(w, source).renderNode) + rdr := mr.newRender(w, source) + defer rdr.w.Flush() + return ast.Walk(node, rdr.renderNode) } func (r *render) renderNode(node ast.Node, entering bool) (ast.WalkStatus, error) { + defer r.w.Flush() if entering && node.PreviousSibling() != nil { switch node.(type) { // All Block types (except few) usually have 2x new lines before itself when they are non-first siblings. @@ -105,6 +134,12 @@ func (r *render) renderNode(node ast.Node, entering bool) (ast.WalkStatus, error } } + // NOTE: checking override here risks picking up render options from extensions + // like GFM, which registers all of its HTML render functions. + // if ovr, ok := r.overrides[node.Kind()]; ok { + // return ovr(r.w, r.source, node, entering) + // } + switch tnode := node.(type) { case *ast.Document: if entering { @@ -303,7 +338,11 @@ func (r *render) renderNode(node ast.Node, entering bool) (ast.WalkStatus, error case *extAST.TableRow, *extAST.TableHeader: return ast.WalkStop, errors.Errorf("%v element detected, but table should be rendered in renderTable instead", tnode.Kind().String()) default: - return ast.WalkStop, errors.Errorf("detected unexpected tree type %s", tnode.Kind().String()) + if ovr, ok := r.overrides[node.Kind()]; ok { + fmt.Println("extended kind:", node.Kind()) + return ovr(r.w, r.source, node, entering) + } + return ast.WalkStop, errors.Errorf("detected unexpected node %s", tnode.Kind().String()) } return ast.WalkContinue, nil } diff --git a/markdown/writer_indent.go b/markdown/writer_indent.go index 62cd331..e517702 100644 --- a/markdown/writer_indent.go +++ b/markdown/writer_indent.go @@ -1,6 +1,7 @@ package markdown import ( + "bufio" "bytes" "io" @@ -9,7 +10,7 @@ import ( // lineIndentWriter wraps io.Writer and adds given indent everytime new line is created . type lineIndentWriter struct { - io.Writer + *bufio.Writer indent []byte whitespace []byte @@ -19,7 +20,7 @@ type lineIndentWriter struct { } func wrapWithLineIndentWriter(w io.Writer) *lineIndentWriter { - return &lineIndentWriter{Writer: w, previousCharWasNewLine: true} + return &lineIndentWriter{Writer: bufio.NewWriter(w), previousCharWasNewLine: true} } func (l *lineIndentWriter) UpdateIndent(node ast.Node, entering bool) { diff --git a/markdownfmt/markdownfmt.go b/markdownfmt/markdownfmt.go index c4ded95..a57fdaa 100644 --- a/markdownfmt/markdownfmt.go +++ b/markdownfmt/markdownfmt.go @@ -8,16 +8,11 @@ import ( "github.com/yuin/goldmark" "github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/renderer" ) -// TODO(karel): unused, can we delete? -func NewParser() parser.Parser { - return NewGoldmark().Parser() -} - -func NewGoldmark(opts ...markdown.Option) goldmark.Markdown { +func NewGoldmark(opts ...renderer.Option) goldmark.Markdown { mr := markdown.NewRenderer() - mr.AddMarkdownOptions(opts...) extensions := []goldmark.Extender{ extension.GFM, } @@ -28,14 +23,17 @@ func NewGoldmark(opts ...markdown.Option) goldmark.Markdown { gm := goldmark.New( goldmark.WithExtensions(extensions...), goldmark.WithParserOptions(parserOptions...), - goldmark.WithRenderer(mr), ) + // Set renderer outside constructor to reset the + // html render functions registered by GFM. + gm.SetRenderer(mr) + gm.Renderer().AddOptions(opts...) return gm } // Process formats given Markdown. -func Process(filename string, src []byte, opts ...markdown.Option) ([]byte, error) { +func Process(filename string, src []byte, opts ...renderer.Option) ([]byte, error) { text, err := readSource(filename, src) if err != nil { return nil, err