Implemented block quotes
This commit is contained in:
@ -7,6 +7,7 @@ Markdown renderer.
|
|||||||
- Links
|
- Links
|
||||||
- Headers
|
- Headers
|
||||||
- Paragraphs
|
- Paragraphs
|
||||||
|
- Quotes
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@ -27,6 +28,7 @@ PASS: TestEmpty
|
|||||||
PASS: TestParagraphs
|
PASS: TestParagraphs
|
||||||
PASS: TestHeader
|
PASS: TestHeader
|
||||||
PASS: TestLink
|
PASS: TestLink
|
||||||
|
PASS: TestQuote
|
||||||
PASS: TestCombined
|
PASS: TestCombined
|
||||||
PASS: TestSecurity
|
PASS: TestSecurity
|
||||||
coverage: 100.0% of statements
|
coverage: 100.0% of statements
|
||||||
@ -35,7 +37,7 @@ coverage: 100.0% of statements
|
|||||||
## Benchmarks
|
## Benchmarks
|
||||||
|
|
||||||
```
|
```
|
||||||
BenchmarkSmall-12 2019103 591.4 ns/op 296 B/op 9 allocs/op
|
BenchmarkSmall-12 2425053 492.8 ns/op 248 B/op 5 allocs/op
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
142
Render.go
142
Render.go
@ -10,30 +10,29 @@ var (
|
|||||||
headerEnd = []string{"</h1>", "</h2>", "</h3>", "</h4>", "</h5>", "</h6>"}
|
headerEnd = []string{"</h1>", "</h2>", "</h3>", "</h4>", "</h5>", "</h6>"}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type renderer struct {
|
||||||
|
out strings.Builder
|
||||||
|
paragraphLevel int
|
||||||
|
quoteLevel int
|
||||||
|
}
|
||||||
|
|
||||||
// Render creates HTML from the supplied markdown text.
|
// Render creates HTML from the supplied markdown text.
|
||||||
func Render(markdown string) string {
|
func Render(markdown string) string {
|
||||||
var (
|
var (
|
||||||
out = strings.Builder{}
|
r renderer
|
||||||
paragraph = strings.Builder{}
|
|
||||||
i = 0
|
i = 0
|
||||||
lineStart = 0
|
lineStart = 0
|
||||||
)
|
)
|
||||||
|
|
||||||
flush := func() {
|
|
||||||
if paragraph.Len() == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
out.WriteString("<p>")
|
|
||||||
writeText(&out, paragraph.String())
|
|
||||||
out.WriteString("</p>")
|
|
||||||
paragraph.Reset()
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
if i > len(markdown) {
|
if i > len(markdown) {
|
||||||
flush()
|
r.closeParagraphs()
|
||||||
return out.String()
|
|
||||||
|
for range r.quoteLevel {
|
||||||
|
r.out.WriteString("</blockquote>")
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.out.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
if i != len(markdown) && markdown[i] != '\n' {
|
if i != len(markdown) && markdown[i] != '\n' {
|
||||||
@ -45,33 +44,74 @@ func Render(markdown string) string {
|
|||||||
lineStart = i + 1
|
lineStart = i + 1
|
||||||
i++
|
i++
|
||||||
|
|
||||||
switch {
|
r.processLine(line)
|
||||||
case strings.HasPrefix(line, "#"):
|
|
||||||
flush()
|
|
||||||
space := strings.IndexByte(line, ' ')
|
|
||||||
|
|
||||||
if space > 0 && space <= 6 {
|
|
||||||
out.WriteString(headerStart[space-1])
|
|
||||||
writeText(&out, line[space+1:])
|
|
||||||
out.WriteString(headerEnd[space-1])
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
if len(line) == 0 {
|
|
||||||
flush()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if paragraph.Len() > 0 {
|
|
||||||
paragraph.WriteByte(' ')
|
|
||||||
}
|
|
||||||
|
|
||||||
paragraph.WriteString(line)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeText(out *strings.Builder, text string) {
|
func (r *renderer) processLine(line string) {
|
||||||
|
newQuoteLevel := 0
|
||||||
|
|
||||||
|
for strings.HasPrefix(line, ">") {
|
||||||
|
line = strings.TrimSpace(line[1:])
|
||||||
|
newQuoteLevel++
|
||||||
|
}
|
||||||
|
|
||||||
|
if newQuoteLevel > r.quoteLevel {
|
||||||
|
r.closeParagraphs()
|
||||||
|
|
||||||
|
for range newQuoteLevel - r.quoteLevel {
|
||||||
|
r.out.WriteString("<blockquote>")
|
||||||
|
}
|
||||||
|
} else if newQuoteLevel < r.quoteLevel {
|
||||||
|
r.closeParagraphs()
|
||||||
|
|
||||||
|
for range r.quoteLevel - newQuoteLevel {
|
||||||
|
r.out.WriteString("</blockquote>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.quoteLevel = newQuoteLevel
|
||||||
|
|
||||||
|
if strings.HasPrefix(line, "#") {
|
||||||
|
r.closeParagraphs()
|
||||||
|
space := strings.IndexByte(line, ' ')
|
||||||
|
|
||||||
|
if space > 0 && space <= 6 {
|
||||||
|
r.out.WriteString(headerStart[space-1])
|
||||||
|
r.writeText(line[space+1:])
|
||||||
|
r.out.WriteString(headerEnd[space-1])
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(line) == 0 {
|
||||||
|
r.closeParagraphs()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.paragraphLevel == 0 {
|
||||||
|
r.out.WriteString("<p>")
|
||||||
|
r.paragraphLevel++
|
||||||
|
r.writeText(line)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.out.WriteByte(' ')
|
||||||
|
r.writeText(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
// closeParagraphs closes open paragraphs.
|
||||||
|
func (r *renderer) closeParagraphs() {
|
||||||
|
for range r.paragraphLevel {
|
||||||
|
r.out.WriteString("</p>")
|
||||||
|
}
|
||||||
|
|
||||||
|
r.paragraphLevel = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeText converts inline markdown to HTML.
|
||||||
|
func (r *renderer) writeText(markdown string) {
|
||||||
var (
|
var (
|
||||||
i = 0
|
i = 0
|
||||||
tokenStart = 0
|
tokenStart = 0
|
||||||
@ -85,16 +125,16 @@ func writeText(out *strings.Builder, text string) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
if i == len(text) {
|
if i == len(markdown) {
|
||||||
out.WriteString(html.EscapeString(text[tokenStart:]))
|
r.out.WriteString(html.EscapeString(markdown[tokenStart:]))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c := text[i]
|
c := markdown[i]
|
||||||
|
|
||||||
switch c {
|
switch c {
|
||||||
case '[':
|
case '[':
|
||||||
out.WriteString(html.EscapeString(text[tokenStart:i]))
|
r.out.WriteString(html.EscapeString(markdown[tokenStart:i]))
|
||||||
tokenStart = i
|
tokenStart = i
|
||||||
textStart = i
|
textStart = i
|
||||||
case ']':
|
case ']':
|
||||||
@ -109,14 +149,14 @@ func writeText(out *strings.Builder, text string) {
|
|||||||
parentheses--
|
parentheses--
|
||||||
|
|
||||||
if parentheses == 0 && textStart >= 0 && textEnd >= 0 && urlStart >= 0 {
|
if parentheses == 0 && textStart >= 0 && textEnd >= 0 && urlStart >= 0 {
|
||||||
linkText := text[textStart+1 : textEnd]
|
linkText := markdown[textStart+1 : textEnd]
|
||||||
linkURL := text[urlStart+1 : i]
|
linkURL := markdown[urlStart+1 : i]
|
||||||
|
|
||||||
out.WriteString("<a href=\"")
|
r.out.WriteString("<a href=\"")
|
||||||
out.WriteString(formatURL(linkURL))
|
r.out.WriteString(sanitizeURL(linkURL))
|
||||||
out.WriteString("\">")
|
r.out.WriteString("\">")
|
||||||
out.WriteString(html.EscapeString(linkText))
|
r.out.WriteString(html.EscapeString(linkText))
|
||||||
out.WriteString("</a>")
|
r.out.WriteString("</a>")
|
||||||
|
|
||||||
textStart = -1
|
textStart = -1
|
||||||
textEnd = -1
|
textEnd = -1
|
||||||
@ -130,7 +170,7 @@ func writeText(out *strings.Builder, text string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatURL(linkURL string) string {
|
func sanitizeURL(linkURL string) string {
|
||||||
if strings.HasPrefix(strings.ToLower(linkURL), "javascript:") {
|
if strings.HasPrefix(strings.ToLower(linkURL), "javascript:") {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,14 @@ func TestLink(t *testing.T) {
|
|||||||
assert.Equal(t, markdown.Render("Prefix [text](https://example.com/) suffix."), "<p>Prefix <a href=\"https://example.com/\">text</a> suffix.</p>")
|
assert.Equal(t, markdown.Render("Prefix [text](https://example.com/) suffix."), "<p>Prefix <a href=\"https://example.com/\">text</a> suffix.</p>")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestQuote(t *testing.T) {
|
||||||
|
assert.Equal(t, markdown.Render("> Line"), "<blockquote><p>Line</p></blockquote>")
|
||||||
|
assert.Equal(t, markdown.Render("> Line 1\n> Line 2"), "<blockquote><p>Line 1 Line 2</p></blockquote>")
|
||||||
|
assert.Equal(t, markdown.Render("> Line 1\n\nLine 2"), "<blockquote><p>Line 1</p></blockquote><p>Line 2</p>")
|
||||||
|
assert.Equal(t, markdown.Render("> Line 1\n>>Line 2"), "<blockquote><p>Line 1</p><blockquote><p>Line 2</p></blockquote></blockquote>")
|
||||||
|
assert.Equal(t, markdown.Render("Line 1\n> Line 2\n> Line 3\nLine 4"), "<p>Line 1</p><blockquote><p>Line 2 Line 3</p></blockquote><p>Line 4</p>")
|
||||||
|
}
|
||||||
|
|
||||||
func TestCombined(t *testing.T) {
|
func TestCombined(t *testing.T) {
|
||||||
assert.Equal(t, markdown.Render("# Header\n\nLine 1."), "<h1>Header</h1><p>Line 1.</p>")
|
assert.Equal(t, markdown.Render("# Header\n\nLine 1."), "<h1>Header</h1><p>Line 1.</p>")
|
||||||
assert.Equal(t, markdown.Render("# Header\nLine 1.\nLine 2.\nLine 3."), "<h1>Header</h1><p>Line 1. Line 2. Line 3.</p>")
|
assert.Equal(t, markdown.Render("# Header\nLine 1.\nLine 2.\nLine 3."), "<h1>Header</h1><p>Line 1. Line 2. Line 3.</p>")
|
||||||
|
Reference in New Issue
Block a user