Improved performance
This commit is contained in:
parent
f75ea823a9
commit
09245c5e92
@ -1,6 +1,6 @@
|
||||
# markdown
|
||||
|
||||
Markdown renderer.
|
||||
A markdown renderer that supports only a subset of the CommonMark spec in order to make the rendering more efficient and the syntax more consistent.
|
||||
|
||||
## Features
|
||||
|
||||
@ -43,7 +43,9 @@ coverage: 100.0% of statements
|
||||
## Benchmarks
|
||||
|
||||
```
|
||||
BenchmarkSmall-12 2421213 494.2 ns/op 248 B/op 5 allocs/op
|
||||
BenchmarkSmall-12 7223979 164.2 ns/op 64 B/op 2 allocs/op
|
||||
BenchmarkMedium-12 832531 1310 ns/op 992 B/op 2 allocs/op
|
||||
BenchmarkLarge-12 295946 3732 ns/op 3712 B/op 3 allocs/op
|
||||
```
|
||||
|
||||
## License
|
||||
|
101
Render.go
101
Render.go
@ -10,8 +10,9 @@ var (
|
||||
headerEnd = []string{"</h1>", "</h2>", "</h3>", "</h4>", "</h5>", "</h6>"}
|
||||
)
|
||||
|
||||
// renderer represents a Markdown to HTML renderer.
|
||||
type renderer struct {
|
||||
out strings.Builder
|
||||
out []byte
|
||||
paragraphLevel int
|
||||
quoteLevel int
|
||||
listLevel int
|
||||
@ -29,15 +30,17 @@ func Render(markdown string) string {
|
||||
lineStart = 0
|
||||
)
|
||||
|
||||
r.out = make([]byte, 0, nextPowerOf2(uint32(len(markdown)+4)))
|
||||
|
||||
for {
|
||||
if i > len(markdown) {
|
||||
r.closeAll()
|
||||
|
||||
for range r.quoteLevel {
|
||||
r.out.WriteString("</blockquote>")
|
||||
r.WriteString("</blockquote>")
|
||||
}
|
||||
|
||||
return r.out.String()
|
||||
return string(r.out)
|
||||
}
|
||||
|
||||
if i != len(markdown) && markdown[i] != '\n' {
|
||||
@ -56,15 +59,15 @@ func Render(markdown string) string {
|
||||
func (r *renderer) processLine(line string) {
|
||||
if r.inCodeBlock {
|
||||
if strings.HasPrefix(line, "```") {
|
||||
r.out.WriteString("</code></pre>")
|
||||
r.WriteString("</code></pre>")
|
||||
r.inCodeBlock = false
|
||||
r.codeLines = 0
|
||||
} else {
|
||||
if r.codeLines != 0 {
|
||||
r.out.WriteByte('\n')
|
||||
r.WriteByte('\n')
|
||||
}
|
||||
|
||||
r.out.WriteString(html.EscapeString(line))
|
||||
r.WriteString(html.EscapeString(line))
|
||||
r.codeLines++
|
||||
}
|
||||
|
||||
@ -82,13 +85,13 @@ func (r *renderer) processLine(line string) {
|
||||
r.closeParagraphs()
|
||||
|
||||
for range newQuoteLevel - r.quoteLevel {
|
||||
r.out.WriteString("<blockquote>")
|
||||
r.WriteString("<blockquote>")
|
||||
}
|
||||
} else if newQuoteLevel < r.quoteLevel {
|
||||
r.closeParagraphs()
|
||||
|
||||
for range r.quoteLevel - newQuoteLevel {
|
||||
r.out.WriteString("</blockquote>")
|
||||
r.WriteString("</blockquote>")
|
||||
}
|
||||
}
|
||||
|
||||
@ -105,9 +108,9 @@ func (r *renderer) processLine(line string) {
|
||||
space := strings.IndexByte(line, ' ')
|
||||
|
||||
if space > 0 && space <= 6 {
|
||||
r.out.WriteString(headerStart[space-1])
|
||||
r.WriteString(headerStart[space-1])
|
||||
r.writeText(line[space+1:])
|
||||
r.out.WriteString(headerEnd[space-1])
|
||||
r.WriteString(headerEnd[space-1])
|
||||
}
|
||||
|
||||
return
|
||||
@ -117,13 +120,13 @@ func (r *renderer) processLine(line string) {
|
||||
line = strings.TrimSpace(line[1:])
|
||||
|
||||
if r.listLevel == 0 {
|
||||
r.out.WriteString("<ul>")
|
||||
r.WriteString("<ul>")
|
||||
r.listLevel++
|
||||
}
|
||||
|
||||
r.out.WriteString("<li>")
|
||||
r.WriteString("<li>")
|
||||
r.writeText(line)
|
||||
r.out.WriteString("</li>")
|
||||
r.WriteString("</li>")
|
||||
return
|
||||
|
||||
case '`':
|
||||
@ -132,11 +135,11 @@ func (r *renderer) processLine(line string) {
|
||||
|
||||
if !r.inCodeBlock {
|
||||
if language != "" {
|
||||
r.out.WriteString("<pre><code class=\"language-")
|
||||
r.out.WriteString(html.EscapeString(language))
|
||||
r.out.WriteString("\">")
|
||||
r.WriteString("<pre><code class=\"language-")
|
||||
r.WriteString(html.EscapeString(language))
|
||||
r.WriteString("\">")
|
||||
} else {
|
||||
r.out.WriteString("<pre><code>")
|
||||
r.WriteString("<pre><code>")
|
||||
}
|
||||
|
||||
r.inCodeBlock = true
|
||||
@ -150,7 +153,7 @@ func (r *renderer) processLine(line string) {
|
||||
line = line[1:]
|
||||
|
||||
if r.tableLevel == 0 {
|
||||
r.out.WriteString("<table><thead>")
|
||||
r.WriteString("<table><thead>")
|
||||
r.tableLevel++
|
||||
}
|
||||
|
||||
@ -160,30 +163,30 @@ func (r *renderer) processLine(line string) {
|
||||
pipe := strings.IndexByte(line, '|')
|
||||
|
||||
if pipe == -1 {
|
||||
r.out.WriteString("</tr>")
|
||||
r.WriteString("</tr>")
|
||||
return
|
||||
}
|
||||
|
||||
content := strings.TrimSpace(line[:pipe])
|
||||
|
||||
if strings.HasPrefix(content, "---") {
|
||||
r.out.WriteString("</thead><tbody>")
|
||||
r.WriteString("</thead><tbody>")
|
||||
r.tableHeaderWritten = true
|
||||
return
|
||||
}
|
||||
|
||||
if column == 0 {
|
||||
r.out.WriteString("<tr>")
|
||||
r.WriteString("<tr>")
|
||||
}
|
||||
|
||||
if r.tableHeaderWritten {
|
||||
r.out.WriteString("<td>")
|
||||
r.WriteString("<td>")
|
||||
r.writeText(content)
|
||||
r.out.WriteString("</td>")
|
||||
r.WriteString("</td>")
|
||||
} else {
|
||||
r.out.WriteString("<th>")
|
||||
r.WriteString("<th>")
|
||||
r.writeText(content)
|
||||
r.out.WriteString("</th>")
|
||||
r.WriteString("</th>")
|
||||
}
|
||||
|
||||
line = line[pipe+1:]
|
||||
@ -192,13 +195,13 @@ func (r *renderer) processLine(line string) {
|
||||
}
|
||||
|
||||
if r.paragraphLevel == 0 {
|
||||
r.out.WriteString("<p>")
|
||||
r.WriteString("<p>")
|
||||
r.paragraphLevel++
|
||||
r.writeText(line)
|
||||
return
|
||||
}
|
||||
|
||||
r.out.WriteByte(' ')
|
||||
r.WriteByte(' ')
|
||||
r.writeText(line)
|
||||
}
|
||||
|
||||
@ -212,7 +215,7 @@ func (r *renderer) closeAll() {
|
||||
// closeParagraphs closes open paragraphs.
|
||||
func (r *renderer) closeParagraphs() {
|
||||
for range r.paragraphLevel {
|
||||
r.out.WriteString("</p>")
|
||||
r.WriteString("</p>")
|
||||
}
|
||||
|
||||
r.paragraphLevel = 0
|
||||
@ -221,7 +224,7 @@ func (r *renderer) closeParagraphs() {
|
||||
// closeLists closes open lists.
|
||||
func (r *renderer) closeLists() {
|
||||
for range r.listLevel {
|
||||
r.out.WriteString("</ul>")
|
||||
r.WriteString("</ul>")
|
||||
}
|
||||
|
||||
r.listLevel = 0
|
||||
@ -230,7 +233,7 @@ func (r *renderer) closeLists() {
|
||||
// closeTables closes open tables.
|
||||
func (r *renderer) closeTables() {
|
||||
for range r.tableLevel {
|
||||
r.out.WriteString("</tbody></table>")
|
||||
r.WriteString("</tbody></table>")
|
||||
}
|
||||
|
||||
r.tableLevel = 0
|
||||
@ -253,7 +256,7 @@ func (r *renderer) writeText(markdown string) {
|
||||
|
||||
for {
|
||||
if i == len(markdown) {
|
||||
r.out.WriteString(html.EscapeString(markdown[tokenStart:]))
|
||||
r.WriteString(html.EscapeString(markdown[tokenStart:]))
|
||||
return
|
||||
}
|
||||
|
||||
@ -261,7 +264,7 @@ func (r *renderer) writeText(markdown string) {
|
||||
|
||||
switch c {
|
||||
case '[':
|
||||
r.out.WriteString(html.EscapeString(markdown[tokenStart:i]))
|
||||
r.WriteString(html.EscapeString(markdown[tokenStart:i]))
|
||||
tokenStart = i
|
||||
textStart = i
|
||||
case ']':
|
||||
@ -279,11 +282,11 @@ func (r *renderer) writeText(markdown string) {
|
||||
linkText := markdown[textStart+1 : textEnd]
|
||||
linkURL := markdown[urlStart+1 : i]
|
||||
|
||||
r.out.WriteString("<a href=\"")
|
||||
r.out.WriteString(sanitizeURL(linkURL))
|
||||
r.out.WriteString("\">")
|
||||
r.out.WriteString(html.EscapeString(linkText))
|
||||
r.out.WriteString("</a>")
|
||||
r.WriteString("<a href=\"")
|
||||
r.WriteString(sanitizeURL(linkURL))
|
||||
r.WriteString("\">")
|
||||
r.WriteString(html.EscapeString(linkText))
|
||||
r.WriteString("</a>")
|
||||
|
||||
textStart = -1
|
||||
textEnd = -1
|
||||
@ -307,3 +310,27 @@ func sanitizeURL(linkURL string) string {
|
||||
|
||||
return html.EscapeString(linkURL)
|
||||
}
|
||||
|
||||
// WriteByte adds a single byte to the output.
|
||||
func (r *renderer) WriteByte(b byte) error {
|
||||
r.out = append(r.out, b)
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteString adds a string to the output.
|
||||
func (r *renderer) WriteString(text string) (int, error) {
|
||||
r.out = append(r.out, text...)
|
||||
return len(text), nil
|
||||
}
|
||||
|
||||
// nextPowerOf2 calculates the next 32-bit power of 2.
|
||||
func nextPowerOf2(x uint32) uint32 {
|
||||
x--
|
||||
x |= x >> 1
|
||||
x |= x >> 2
|
||||
x |= x >> 4
|
||||
x |= x >> 8
|
||||
x |= x >> 16
|
||||
x++
|
||||
return x
|
||||
}
|
||||
|
@ -1,13 +1,39 @@
|
||||
package markdown_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"git.akyoto.dev/go/assert"
|
||||
"git.akyoto.dev/go/markdown"
|
||||
)
|
||||
|
||||
func BenchmarkSmall(b *testing.B) {
|
||||
small, err := os.ReadFile("testdata/small.md")
|
||||
assert.Nil(b, err)
|
||||
input := string(small)
|
||||
|
||||
for range b.N {
|
||||
markdown.Render("# Header\nText.\nText.\n# Header\nText.\nText.")
|
||||
markdown.Render(input)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMedium(b *testing.B) {
|
||||
medium, err := os.ReadFile("testdata/medium.md")
|
||||
assert.Nil(b, err)
|
||||
input := string(medium)
|
||||
|
||||
for range b.N {
|
||||
markdown.Render(input)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLarge(b *testing.B) {
|
||||
small, err := os.ReadFile("testdata/large.md")
|
||||
assert.Nil(b, err)
|
||||
input := string(small)
|
||||
|
||||
for range b.N {
|
||||
markdown.Render(input)
|
||||
}
|
||||
}
|
||||
|
37
testdata/large.md
vendored
Normal file
37
testdata/large.md
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
# Lorem Ipsum
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
|
||||
# Code
|
||||
|
||||
```shell
|
||||
mkdir example
|
||||
cd example
|
||||
touch example.txt
|
||||
```
|
||||
|
||||
# Formatting
|
||||
|
||||
*Italic*, **bold**, `monospace` and [links](https://example.com/).
|
||||
|
||||
# Lists
|
||||
|
||||
- List entry
|
||||
- List entry
|
||||
- List entry
|
||||
|
||||
# Tables
|
||||
|
||||
| X | Y |
|
||||
| --- | --- |
|
||||
| 0 | 0 |
|
||||
| 1 | 2 |
|
||||
| 2 | 4 |
|
||||
| 3 | 6 |
|
||||
| 4 | 8 |
|
||||
|
||||
# Quotes
|
||||
|
||||
> Blockquote
|
||||
> Blockquote
|
||||
> Blockquote
|
3
testdata/medium.md
vendored
Normal file
3
testdata/medium.md
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
# Lorem Ipsum
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
3
testdata/small.md
vendored
Normal file
3
testdata/small.md
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
# Title
|
||||
|
||||
Text.
|
Loading…
Reference in New Issue
Block a user