From 111683b59e54b1433b61c4cb1e0b39ac49a570b3 Mon Sep 17 00:00:00 2001 From: Nicolas MASSE Date: Wed, 21 Oct 2020 18:17:15 +0200 Subject: [PATCH] initial commit --- README.md | 16 ++++ go.mod | 8 ++ go.sum | 5 ++ main.go | 191 ++++++++++++++++++++++++++++++++++++++++++++++ test/reference.md | 154 +++++++++++++++++++++++++++++++++++++ 5 files changed, 374 insertions(+) create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 test/reference.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..0944b84 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# A Markdown Renderer for the BlackFriday library + +## Use case + +This renderer enables "Markdown to Markdown" processing. + +## Remaining work + +* Markdown tables +* HTML elements +* Definition lists +* Fenced code blocks, quotes, and paragraph when part of a list + +## License + +Licensed under the MIT License diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..aea9058 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module github.com/nmasse-itix/blackfriday-markdown-renderer + +go 1.14 + +require ( + github.com/russross/blackfriday/v2 v2.0.1 + github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e7c1741 --- /dev/null +++ b/go.sum @@ -0,0 +1,5 @@ +github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= diff --git a/main.go b/main.go new file mode 100644 index 0000000..69ef387 --- /dev/null +++ b/main.go @@ -0,0 +1,191 @@ +package bfmdrenderer + +import ( + "io" + "log" + "strconv" + + bf "github.com/russross/blackfriday/v2" +) + +// Option defines the functional option type +type Option func(r *Renderer) + +// NewRenderer will return a new renderer with sane defaults +func NewRenderer(options ...Option) *Renderer { + r := &Renderer{} + for _, option := range options { + option(r) + } + return r +} + +// Renderer is a custom Blackfriday renderer +type Renderer struct { + paragraphDecoration []byte + nestedListLevel int + nestedListDecoration []byte + orderedListCounters []int +} + +// Taken from the black friday HTML renderer +func skipParagraphTags(node *bf.Node) bool { + parent := node.Parent + if parent != nil && parent.Type == bf.BlockQuote { + return true + } + + grandparent := node.Parent.Parent + if grandparent == nil || grandparent.Type != bf.List { + return false + } + + return grandparent.Type == bf.List && grandparent.Tight +} + +// RenderNode satisfies the Renderer interface +func (r *Renderer) RenderNode(w io.Writer, node *bf.Node, entering bool) bf.WalkStatus { + switch node.Type { + case bf.Document: + return bf.GoToNext + case bf.BlockQuote: + if entering { + r.paragraphDecoration = append(r.paragraphDecoration, byte('>'), byte(' ')) + } else { + r.paragraphDecoration = r.paragraphDecoration[:len(r.paragraphDecoration)-2] + } + return bf.GoToNext + case bf.List: + if entering { + r.orderedListCounters = append(r.orderedListCounters, 0) + r.nestedListLevel++ + if r.nestedListLevel > 1 { + r.nestedListDecoration = append(r.nestedListDecoration, byte(' '), byte(' ')) + } + } else { + if r.nestedListLevel > 1 { + r.nestedListDecoration = r.nestedListDecoration[:len(r.nestedListDecoration)-2] + } else { + w.Write([]byte("\n")) + } + r.nestedListLevel-- + r.orderedListCounters = r.orderedListCounters[:len(r.orderedListCounters)-1] + } + + return bf.GoToNext + case bf.Item: + if entering { + w.Write(r.nestedListDecoration) + if node.Parent.ListFlags&bf.ListTypeOrdered != 0 { + r.orderedListCounters[len(r.orderedListCounters)-1]++ + w.Write([]byte(strconv.Itoa(r.orderedListCounters[len(r.orderedListCounters)-1]))) + w.Write([]byte{node.ListData.Delimiter}) + w.Write([]byte(" ")) + } else if node.Parent.ListFlags&bf.ListTypeTerm != 0 { + log.Println("Definition lists not implemented by Renderer") + } else { + w.Write([]byte{node.ListData.BulletChar}) + w.Write([]byte(" ")) + } + } + return bf.GoToNext + case bf.Paragraph: + if entering { + w.Write(r.paragraphDecoration) + } else { + w.Write([]byte("\n")) + if !skipParagraphTags(node) { + w.Write([]byte("\n")) + } + } + return bf.GoToNext + case bf.Heading: + if entering { + for i := 0; i < node.Level; i++ { + w.Write([]byte("#")) + } + w.Write([]byte(" ")) + } else { + w.Write([]byte("\n\n")) + } + return bf.GoToNext + case bf.HorizontalRule: + w.Write([]byte("---\n\n")) + return bf.GoToNext + case bf.Emph: + w.Write([]byte("*")) + return bf.GoToNext + case bf.Strong: + w.Write([]byte("**")) + return bf.GoToNext + case bf.Del: + w.Write([]byte("~~")) + return bf.GoToNext + case bf.Link: + if entering { + w.Write([]byte("[")) + } else { + w.Write([]byte("](")) + w.Write(node.LinkData.Destination) + w.Write([]byte(")")) + } + return bf.GoToNext + case bf.Image: + if entering { + w.Write([]byte("![")) + } else { + w.Write([]byte("](")) + w.Write(node.LinkData.Destination) + w.Write([]byte(")")) + } + return bf.GoToNext + case bf.Code: + w.Write([]byte("`")) + w.Write(node.Literal) + w.Write([]byte("`")) + return bf.GoToNext + case bf.Text: + w.Write(node.Literal) + return bf.GoToNext + case bf.CodeBlock: + w.Write([]byte("```")) + w.Write(node.CodeBlockData.Info) + w.Write([]byte("\n")) + w.Write(node.Literal) + w.Write([]byte("```\n\n")) + return bf.GoToNext + case bf.Softbreak: + log.Println("Soft breaks not implemented by renderer") + case bf.Hardbreak: + w.Write([]byte(" \n")) + return bf.GoToNext + case bf.HTMLBlock: + fallthrough + case bf.HTMLSpan: + log.Println("HTML elements not implemented by renderer") + case bf.Table: + fallthrough + case bf.TableCell: + fallthrough + case bf.TableHead: + fallthrough + case bf.TableBody: + fallthrough + case bf.TableRow: + log.Println("Markdown tables not implemented by renderer") + default: + log.Printf("Unknown BlackFriday Node type '%s'\n", node.Type) + } + + return bf.SkipChildren +} + +// RenderHeader satisfies the Renderer interface +func (r *Renderer) RenderHeader(w io.Writer, ast *bf.Node) { + // Nothing required here +} + +// RenderFooter satisfies the Renderer interface +func (r *Renderer) RenderFooter(w io.Writer, ast *bf.Node) { + // Nothing required here +} diff --git a/test/reference.md b/test/reference.md new file mode 100644 index 0000000..d54abb0 --- /dev/null +++ b/test/reference.md @@ -0,0 +1,154 @@ +# An h1 header + +Paragraphs are separated by a blank line. + +2nd paragraph. *Italic*, **bold**, `monospace`. Itemized lists look like: + +- this one +- that one +- the other one + +Nothing to note here. + +> Block quotes are written like so. +> +> > They can be nested. +> +> They can span multiple paragraphs, if you like. + +- Item 1 +- Item 2 + - Item 2a + - Item 2a + - Item 2b +- Item 3 + +Hmm. + +1. Item 1 +2. Item 2 + 1. Blah. + 2. Blah. +3. Item 3 + - Item 3a + - Item 3b + +Large spacing... + +1. An entire paragraph is written here, and bigger spacing between list items is desired. This is supported too. + +2. Item 2 + + 1. Blah. + + 2. Blah. + +3. Item 3 + + - Item 3a + + - Item 3b + +Last paragraph here. + +## An h2 header + +- Paragraph right away. +- **Big item**: Right away after header. + +[Visit GitHub!](www.github.com) + +![Hmm](http://example.org/image.png) + +![Alt text](/path/to/img.jpg "Optional title") ![Alt text](/path/to/img.jpg "Hello \" 世界") + +~~Mistaken text.~~ + +This (**should** be *fine*). + +A \> B. + +It's possible to backslash escape \ tags and \`backticks\`. They are treated as text. + +1986\. What a great season. + +The year was 1986. What a great season. + +\*literal asterisks\*. + +--- + +http://example.com + +Now a [link](www.github.com) in a paragraph. End with [link_underscore.go](www.github.com). + +- [Link](www.example.com) + +### An h3 header + +Here's a numbered list: + +1. first item +2. second item +3. third item + +By the way, instead of indenting the block, you can use delimited blocks, if you like: + +``` +define foobar() { + print "Welcome to flavor country!"; +} +``` + +(which makes copying & pasting easier). You can optionally mark the delimited block for Pandoc to syntax highlight it: + +```Go +func main() { + println("Hi.") +} +``` + +## Nested Lists + +### Codeblock within list + +- list1 + + ```C + if (i == 5) + break; + ``` + +### Blockquote within list + +- list1 + + > This a quote within a list. + +### Multi-level nested + +- Item 1 + + Another paragraph inside this list item is indented just like the previous paragraph. + +- Item 2 + + - Item 2a + + Things go here. + + > This a quote within a list. + + And they stay here. + + - Item 2b + +- Item 3 + +## Line Breaks + +Some text with two trailing spaces for linebreak. +More text immediately after. +Useful for writing poems. + +Done.