Improved performance

This commit is contained in:
Eduard Urbach 2024-04-02 10:44:47 +02:00
parent f75ea823a9
commit 09245c5e92
Signed by: akyoto
GPG Key ID: C874F672B1AF20C0
6 changed files with 138 additions and 40 deletions

View File

@ -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
View File

@ -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
}

View File

@ -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
View 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
View 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
View File

@ -0,0 +1,3 @@
# Title
Text.