Converting Markdown to HTML using Go
I recently ported a blog of mine from Python to Go (to improve speed and performance) and while all is great so far, I'd like some help optimising the Markdown
function to improve the general performance, maintenance and readability of the function.
I have this function because I write my blog articles in Markdown (.md
) and then use Python Go to convert the raw Markdown to HTML for output as this saves me from having to write ridiculous amounts of HTML. (which can be tedious to say the least)
The Markdown
function takes one argument (raw
) which is a string that contains the raw Markdown (obtained using ioutil.ReadFile
).
It then splits the Markdown by n
(removing the empty lines) and converts:
- Bold and italic text (***,**,*)
- Strikethrough text (~~blah blah blah~~)
- Underscored text (__blah blah blah__)
- Links ([https://example.com](Example Link))
- Blockquotes (> sample quote by an important person)
- Inline code (`abcccc`)
- Headings (h1-h6)
While some of the supported features aren't exactly standard, this function works and outputs the expected result without any errors but being a new Go programmer and this being my first "real" Go project I'd like to know whether or not my code could be optimised for better performance, maintainability and readability.
Here a few questions I have regarding optimisation:
- Would it make a difference to performance if I reduced the amount of imports?
- Would it improve readability if I put the
regexp.MustCompile
functions into variables above theMarkdown
function? - Would it improve performance if I used individual regexes to convert Markdown headings instead of using
for i := 6; i >= 1; i-- {...}
? - If not, is there a way to convert
i
(an integer) to a string without usingstrconv.Itoa(i)
(to help reduce the amount of imports)?
Here is my code:
package parse
import (
"regexp"
"strings"
"strconv"
)
func Markdown(raw string) string {
// ignore empty lines with "string.Split(...)"
lines := strings.FieldsFunc(raw, func(c rune) bool {
return c == 'n'
})
for i, line := range lines {
// wrap bold and italic text in "<b>" and "<i>" elements
line = regexp.MustCompile(`***(.*?)***`).ReplaceAllString(line, `<b><i>$1</i></b>`)
line = regexp.MustCompile(`**(.*?)**`).ReplaceAllString(line, `<b>$1</b>`)
line = regexp.MustCompile(`*(.*?)*`).ReplaceAllString(line, `<i>$1</i>`)
// wrap strikethrough text in "<s>" tags
line = regexp.MustCompile(`~~(.*?)~~`).ReplaceAllString(line, `<s>$1</s>`)
// wrap underscored text in "<u>" tags
line = regexp.MustCompile(`__(.*?)__`).ReplaceAllString(line, `<u>$1</u>`)
// convert links to anchor tags
line = regexp.MustCompile(`[(.*?)]((.*?))[^)]`).ReplaceAllString(line, `<a href="$2">$1</a>`)
// escape and wrap blockquotes in "<blockquote>" tags
line = regexp.MustCompile(`^>(s|)`).ReplaceAllString(line, `>`)
line = regexp.MustCompile(`>(.*?)$`).ReplaceAllString(line, `<blockquote>$1</blockquote>`)
// wrap the content of backticks inside of "<code>" tags
line = regexp.MustCompile("`(.*?)`").ReplaceAllString(line, `<code>$1</code>`)
// convert headings
for i := 6; i >= 1; i-- {
size, md_header := strconv.Itoa(i), strings.Repeat("#", i)
line = regexp.MustCompile(`^` + md_header + `(s|)(.*?)$`).ReplaceAllString(line, `<h` + size + `>$2</h` + size + `>`)
}
// update the line
lines[i] = line
}
// return the joined lines
return strings.Join(lines, "n")
}
performance html regex go markdown
New contributor
add a comment |
I recently ported a blog of mine from Python to Go (to improve speed and performance) and while all is great so far, I'd like some help optimising the Markdown
function to improve the general performance, maintenance and readability of the function.
I have this function because I write my blog articles in Markdown (.md
) and then use Python Go to convert the raw Markdown to HTML for output as this saves me from having to write ridiculous amounts of HTML. (which can be tedious to say the least)
The Markdown
function takes one argument (raw
) which is a string that contains the raw Markdown (obtained using ioutil.ReadFile
).
It then splits the Markdown by n
(removing the empty lines) and converts:
- Bold and italic text (***,**,*)
- Strikethrough text (~~blah blah blah~~)
- Underscored text (__blah blah blah__)
- Links ([https://example.com](Example Link))
- Blockquotes (> sample quote by an important person)
- Inline code (`abcccc`)
- Headings (h1-h6)
While some of the supported features aren't exactly standard, this function works and outputs the expected result without any errors but being a new Go programmer and this being my first "real" Go project I'd like to know whether or not my code could be optimised for better performance, maintainability and readability.
Here a few questions I have regarding optimisation:
- Would it make a difference to performance if I reduced the amount of imports?
- Would it improve readability if I put the
regexp.MustCompile
functions into variables above theMarkdown
function? - Would it improve performance if I used individual regexes to convert Markdown headings instead of using
for i := 6; i >= 1; i-- {...}
? - If not, is there a way to convert
i
(an integer) to a string without usingstrconv.Itoa(i)
(to help reduce the amount of imports)?
Here is my code:
package parse
import (
"regexp"
"strings"
"strconv"
)
func Markdown(raw string) string {
// ignore empty lines with "string.Split(...)"
lines := strings.FieldsFunc(raw, func(c rune) bool {
return c == 'n'
})
for i, line := range lines {
// wrap bold and italic text in "<b>" and "<i>" elements
line = regexp.MustCompile(`***(.*?)***`).ReplaceAllString(line, `<b><i>$1</i></b>`)
line = regexp.MustCompile(`**(.*?)**`).ReplaceAllString(line, `<b>$1</b>`)
line = regexp.MustCompile(`*(.*?)*`).ReplaceAllString(line, `<i>$1</i>`)
// wrap strikethrough text in "<s>" tags
line = regexp.MustCompile(`~~(.*?)~~`).ReplaceAllString(line, `<s>$1</s>`)
// wrap underscored text in "<u>" tags
line = regexp.MustCompile(`__(.*?)__`).ReplaceAllString(line, `<u>$1</u>`)
// convert links to anchor tags
line = regexp.MustCompile(`[(.*?)]((.*?))[^)]`).ReplaceAllString(line, `<a href="$2">$1</a>`)
// escape and wrap blockquotes in "<blockquote>" tags
line = regexp.MustCompile(`^>(s|)`).ReplaceAllString(line, `>`)
line = regexp.MustCompile(`>(.*?)$`).ReplaceAllString(line, `<blockquote>$1</blockquote>`)
// wrap the content of backticks inside of "<code>" tags
line = regexp.MustCompile("`(.*?)`").ReplaceAllString(line, `<code>$1</code>`)
// convert headings
for i := 6; i >= 1; i-- {
size, md_header := strconv.Itoa(i), strings.Repeat("#", i)
line = regexp.MustCompile(`^` + md_header + `(s|)(.*?)$`).ReplaceAllString(line, `<h` + size + `>$2</h` + size + `>`)
}
// update the line
lines[i] = line
}
// return the joined lines
return strings.Join(lines, "n")
}
performance html regex go markdown
New contributor
add a comment |
I recently ported a blog of mine from Python to Go (to improve speed and performance) and while all is great so far, I'd like some help optimising the Markdown
function to improve the general performance, maintenance and readability of the function.
I have this function because I write my blog articles in Markdown (.md
) and then use Python Go to convert the raw Markdown to HTML for output as this saves me from having to write ridiculous amounts of HTML. (which can be tedious to say the least)
The Markdown
function takes one argument (raw
) which is a string that contains the raw Markdown (obtained using ioutil.ReadFile
).
It then splits the Markdown by n
(removing the empty lines) and converts:
- Bold and italic text (***,**,*)
- Strikethrough text (~~blah blah blah~~)
- Underscored text (__blah blah blah__)
- Links ([https://example.com](Example Link))
- Blockquotes (> sample quote by an important person)
- Inline code (`abcccc`)
- Headings (h1-h6)
While some of the supported features aren't exactly standard, this function works and outputs the expected result without any errors but being a new Go programmer and this being my first "real" Go project I'd like to know whether or not my code could be optimised for better performance, maintainability and readability.
Here a few questions I have regarding optimisation:
- Would it make a difference to performance if I reduced the amount of imports?
- Would it improve readability if I put the
regexp.MustCompile
functions into variables above theMarkdown
function? - Would it improve performance if I used individual regexes to convert Markdown headings instead of using
for i := 6; i >= 1; i-- {...}
? - If not, is there a way to convert
i
(an integer) to a string without usingstrconv.Itoa(i)
(to help reduce the amount of imports)?
Here is my code:
package parse
import (
"regexp"
"strings"
"strconv"
)
func Markdown(raw string) string {
// ignore empty lines with "string.Split(...)"
lines := strings.FieldsFunc(raw, func(c rune) bool {
return c == 'n'
})
for i, line := range lines {
// wrap bold and italic text in "<b>" and "<i>" elements
line = regexp.MustCompile(`***(.*?)***`).ReplaceAllString(line, `<b><i>$1</i></b>`)
line = regexp.MustCompile(`**(.*?)**`).ReplaceAllString(line, `<b>$1</b>`)
line = regexp.MustCompile(`*(.*?)*`).ReplaceAllString(line, `<i>$1</i>`)
// wrap strikethrough text in "<s>" tags
line = regexp.MustCompile(`~~(.*?)~~`).ReplaceAllString(line, `<s>$1</s>`)
// wrap underscored text in "<u>" tags
line = regexp.MustCompile(`__(.*?)__`).ReplaceAllString(line, `<u>$1</u>`)
// convert links to anchor tags
line = regexp.MustCompile(`[(.*?)]((.*?))[^)]`).ReplaceAllString(line, `<a href="$2">$1</a>`)
// escape and wrap blockquotes in "<blockquote>" tags
line = regexp.MustCompile(`^>(s|)`).ReplaceAllString(line, `>`)
line = regexp.MustCompile(`>(.*?)$`).ReplaceAllString(line, `<blockquote>$1</blockquote>`)
// wrap the content of backticks inside of "<code>" tags
line = regexp.MustCompile("`(.*?)`").ReplaceAllString(line, `<code>$1</code>`)
// convert headings
for i := 6; i >= 1; i-- {
size, md_header := strconv.Itoa(i), strings.Repeat("#", i)
line = regexp.MustCompile(`^` + md_header + `(s|)(.*?)$`).ReplaceAllString(line, `<h` + size + `>$2</h` + size + `>`)
}
// update the line
lines[i] = line
}
// return the joined lines
return strings.Join(lines, "n")
}
performance html regex go markdown
New contributor
I recently ported a blog of mine from Python to Go (to improve speed and performance) and while all is great so far, I'd like some help optimising the Markdown
function to improve the general performance, maintenance and readability of the function.
I have this function because I write my blog articles in Markdown (.md
) and then use Python Go to convert the raw Markdown to HTML for output as this saves me from having to write ridiculous amounts of HTML. (which can be tedious to say the least)
The Markdown
function takes one argument (raw
) which is a string that contains the raw Markdown (obtained using ioutil.ReadFile
).
It then splits the Markdown by n
(removing the empty lines) and converts:
- Bold and italic text (***,**,*)
- Strikethrough text (~~blah blah blah~~)
- Underscored text (__blah blah blah__)
- Links ([https://example.com](Example Link))
- Blockquotes (> sample quote by an important person)
- Inline code (`abcccc`)
- Headings (h1-h6)
While some of the supported features aren't exactly standard, this function works and outputs the expected result without any errors but being a new Go programmer and this being my first "real" Go project I'd like to know whether or not my code could be optimised for better performance, maintainability and readability.
Here a few questions I have regarding optimisation:
- Would it make a difference to performance if I reduced the amount of imports?
- Would it improve readability if I put the
regexp.MustCompile
functions into variables above theMarkdown
function? - Would it improve performance if I used individual regexes to convert Markdown headings instead of using
for i := 6; i >= 1; i-- {...}
? - If not, is there a way to convert
i
(an integer) to a string without usingstrconv.Itoa(i)
(to help reduce the amount of imports)?
Here is my code:
package parse
import (
"regexp"
"strings"
"strconv"
)
func Markdown(raw string) string {
// ignore empty lines with "string.Split(...)"
lines := strings.FieldsFunc(raw, func(c rune) bool {
return c == 'n'
})
for i, line := range lines {
// wrap bold and italic text in "<b>" and "<i>" elements
line = regexp.MustCompile(`***(.*?)***`).ReplaceAllString(line, `<b><i>$1</i></b>`)
line = regexp.MustCompile(`**(.*?)**`).ReplaceAllString(line, `<b>$1</b>`)
line = regexp.MustCompile(`*(.*?)*`).ReplaceAllString(line, `<i>$1</i>`)
// wrap strikethrough text in "<s>" tags
line = regexp.MustCompile(`~~(.*?)~~`).ReplaceAllString(line, `<s>$1</s>`)
// wrap underscored text in "<u>" tags
line = regexp.MustCompile(`__(.*?)__`).ReplaceAllString(line, `<u>$1</u>`)
// convert links to anchor tags
line = regexp.MustCompile(`[(.*?)]((.*?))[^)]`).ReplaceAllString(line, `<a href="$2">$1</a>`)
// escape and wrap blockquotes in "<blockquote>" tags
line = regexp.MustCompile(`^>(s|)`).ReplaceAllString(line, `>`)
line = regexp.MustCompile(`>(.*?)$`).ReplaceAllString(line, `<blockquote>$1</blockquote>`)
// wrap the content of backticks inside of "<code>" tags
line = regexp.MustCompile("`(.*?)`").ReplaceAllString(line, `<code>$1</code>`)
// convert headings
for i := 6; i >= 1; i-- {
size, md_header := strconv.Itoa(i), strings.Repeat("#", i)
line = regexp.MustCompile(`^` + md_header + `(s|)(.*?)$`).ReplaceAllString(line, `<h` + size + `>$2</h` + size + `>`)
}
// update the line
lines[i] = line
}
// return the joined lines
return strings.Join(lines, "n")
}
performance html regex go markdown
performance html regex go markdown
New contributor
New contributor
edited Dec 27 '18 at 15:46
200_success
128k15150413
128k15150413
New contributor
asked Dec 27 '18 at 12:56
LogicalBranch
235
235
New contributor
New contributor
add a comment |
add a comment |
1 Answer
1
active
oldest
votes
Performance
Regex
regex.MustCompile()
is very expensive! Do not use this method inside a loop !
instead, define your regex as global variables only once:
var (
boldItalicReg = regexp.MustCompile(`***(.*?)***`)
boldReg = regexp.MustCompile(`**(.*?)**`)
...
)
Headers
If a line is a header, it will start by a #
. We can check for this before calling ReplaceAllString()
6 times ! All we need
to do is to trim the line, and then check if it starts with #
:
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "#") {
// convert headings
...
}
We could go further and unrolling the loop to avoid unecessary allocations:
count := strings.Count(line, "#")
switch count {
case 1:
line = h1Reg.ReplaceAllString(line, `<h1>$2</h1>`)
case 2:
...
}
Use a scanner
The idiomatic way to read a file line by line in go is to use a scanner
. It takes an io.Reader
as parameters, so you can directly pass
your mardown file instead of converting it into a string first:
func NewMarkdown(input io.Reader) string {
scanner := bufio.NewScanner(input)
for scanner.Scan() {
line := scanner.Text()
...
}
}
Use byte
instead of string
In go, a string
is a read-only slice of bytes. Working with strings is usually more expensive than working with slice of bytes,
so use byte
instead of strings
when you can:
line := scanner.Bytes()
line = boldItalicReg.ReplaceAll(line, byte(`<b><i>$1</i></b>`))
Write result to a bytes.Buffer
Instead of string.Join()
, we can use a buffer to write each line in order to further reduce the number of allocations:
buf := bytes.NewBuffer(nil)
scanner := bufio.NewScanner(input)
for scanner.Scan() {
line := scanner.Bytes()
...
buf.Write(line)
buf.WriteByte('n')
}
return buf.String()
final code:
package parse
import (
"bufio"
"bytes"
"io"
"regexp"
)
var (
boldItalicReg = regexp.MustCompile(`***(.*?)***`)
boldReg = regexp.MustCompile(`**(.*?)**`)
italicReg = regexp.MustCompile(`*(.*?)*`)
strikeReg = regexp.MustCompile(`~~(.*?)~~`)
underscoreReg = regexp.MustCompile(`__(.*?)__`)
anchorReg = regexp.MustCompile(`[(.*?)]((.*?))[^)]`)
escapeReg = regexp.MustCompile(`^>(s|)`)
blockquoteReg = regexp.MustCompile(`>(.*?)$`)
backtipReg = regexp.MustCompile("`(.*?)`")
h1Reg = regexp.MustCompile(`^#(s|)(.*?)$`)
h2Reg = regexp.MustCompile(`^##(s|)(.*?)$`)
h3Reg = regexp.MustCompile(`^###(s|)(.*?)$`)
h4Reg = regexp.MustCompile(`^####(s|)(.*?)$`)
h5Reg = regexp.MustCompile(`^#####(s|)(.*?)$`)
h6Reg = regexp.MustCompile(`^######(s|)(.*?)$`)
)
func NewMarkdown(input io.Reader) string {
buf := bytes.NewBuffer(nil)
scanner := bufio.NewScanner(input)
for scanner.Scan() {
line := bytes.TrimSpace(scanner.Bytes())
if len(line) == 0 {
buf.WriteByte('n')
continue
}
// wrap bold and italic text in "<b>" and "<i>" elements
line = boldItalicReg.ReplaceAll(line, byte(`<b><i>$1</i></b>`))
line = boldReg.ReplaceAll(line, byte(`<b>$1</b>`))
line = italicReg.ReplaceAll(line, byte(`<i>$1</i>`))
// wrap strikethrough text in "<s>" tags
line = strikeReg.ReplaceAll(line, byte(`<s>$1</s>`))
// wrap underscored text in "<u>" tags
line = underscoreReg.ReplaceAll(line, byte(`<u>$1</u>`))
// convert links to anchor tags
line = anchorReg.ReplaceAll(line, byte(`<a href="$2">$1</a>`))
// escape and wrap blockquotes in "<blockquote>" tags
line = escapeReg.ReplaceAll(line, byte(`>`))
line = blockquoteReg.ReplaceAll(line, byte(`<blockquote>$1</blockquote>`))
// wrap the content of backticks inside of "<code>" tags
line = backtipReg.ReplaceAll(line, byte(`<code>$1</code>`))
// convert headings
if line[0] == '#' {
count := bytes.Count(line, byte(`#`))
switch count {
case 1:
line = h1Reg.ReplaceAll(line, byte(`<h1>$2</h1>`))
case 2:
line = h2Reg.ReplaceAll(line, byte(`<h2>$2</h2>`))
case 3:
line = h3Reg.ReplaceAll(line, byte(`<h3>$2</h3>`))
case 4:
line = h4Reg.ReplaceAll(line, byte(`<h4>$2</h4>`))
case 5:
line = h5Reg.ReplaceAll(line, byte(`<h5>$2</h5>`))
case 6:
line = h6Reg.ReplaceAll(line, byte(`<h6>$2</h6>`))
}
}
buf.Write(line)
buf.WriteByte('n')
}
return buf.String()
}
Benchmarks
I used the folowing code for benchmarks, on a 20kB md file:
func BenchmarkMarkdown(b *testing.B) {
md, err := ioutil.ReadFile("README.md")
if err != nil {
b.Fail()
}
raw := string(md)
b.ResetTimer()
for n := 0; n < b.N; n++ {
_ = Markdown(raw)
}
}
func BenchmarkMarkdownNew(b *testing.B) {
for n := 0; n < b.N; n++ {
file, err := os.Open("README.md")
if err != nil {
b.Fail()
}
_ = NewMarkdown(file)
file.Close()
}
}
Results:
> go test -bench=. -benchmem
goos: linux
goarch: amd64
BenchmarkMarkdown-4 10 104990431 ns/op 364617427 B/op 493813 allocs/op
BenchmarkMarkdownNew-4 1000 1464745 ns/op 379376 B/op 11085 allocs/op
benchstat diff:
name old time/op new time/op delta
Markdown-4 105ms ± 0% 1ms ± 0% ~ (p=1.000 n=1+1)
name old alloc/op new alloc/op delta
Markdown-4 365MB ± 0% 0MB ± 0% ~ (p=1.000 n=1+1)
name old allocs/op new allocs/op delta
Markdown-4 494k ± 0% 11k ± 0% ~ (p=1.000 n=1+1)
Thank you very much for the answer!
– LogicalBranch
2 days ago
add a comment |
Your Answer
StackExchange.ifUsing("editor", function () {
return StackExchange.using("mathjaxEditing", function () {
StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix) {
StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
});
});
}, "mathjax-editing");
StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "196"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: false,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: null,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
LogicalBranch is a new contributor. Be nice, and check out our Code of Conduct.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f210422%2fconverting-markdown-to-html-using-go%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
1 Answer
1
active
oldest
votes
1 Answer
1
active
oldest
votes
active
oldest
votes
active
oldest
votes
Performance
Regex
regex.MustCompile()
is very expensive! Do not use this method inside a loop !
instead, define your regex as global variables only once:
var (
boldItalicReg = regexp.MustCompile(`***(.*?)***`)
boldReg = regexp.MustCompile(`**(.*?)**`)
...
)
Headers
If a line is a header, it will start by a #
. We can check for this before calling ReplaceAllString()
6 times ! All we need
to do is to trim the line, and then check if it starts with #
:
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "#") {
// convert headings
...
}
We could go further and unrolling the loop to avoid unecessary allocations:
count := strings.Count(line, "#")
switch count {
case 1:
line = h1Reg.ReplaceAllString(line, `<h1>$2</h1>`)
case 2:
...
}
Use a scanner
The idiomatic way to read a file line by line in go is to use a scanner
. It takes an io.Reader
as parameters, so you can directly pass
your mardown file instead of converting it into a string first:
func NewMarkdown(input io.Reader) string {
scanner := bufio.NewScanner(input)
for scanner.Scan() {
line := scanner.Text()
...
}
}
Use byte
instead of string
In go, a string
is a read-only slice of bytes. Working with strings is usually more expensive than working with slice of bytes,
so use byte
instead of strings
when you can:
line := scanner.Bytes()
line = boldItalicReg.ReplaceAll(line, byte(`<b><i>$1</i></b>`))
Write result to a bytes.Buffer
Instead of string.Join()
, we can use a buffer to write each line in order to further reduce the number of allocations:
buf := bytes.NewBuffer(nil)
scanner := bufio.NewScanner(input)
for scanner.Scan() {
line := scanner.Bytes()
...
buf.Write(line)
buf.WriteByte('n')
}
return buf.String()
final code:
package parse
import (
"bufio"
"bytes"
"io"
"regexp"
)
var (
boldItalicReg = regexp.MustCompile(`***(.*?)***`)
boldReg = regexp.MustCompile(`**(.*?)**`)
italicReg = regexp.MustCompile(`*(.*?)*`)
strikeReg = regexp.MustCompile(`~~(.*?)~~`)
underscoreReg = regexp.MustCompile(`__(.*?)__`)
anchorReg = regexp.MustCompile(`[(.*?)]((.*?))[^)]`)
escapeReg = regexp.MustCompile(`^>(s|)`)
blockquoteReg = regexp.MustCompile(`>(.*?)$`)
backtipReg = regexp.MustCompile("`(.*?)`")
h1Reg = regexp.MustCompile(`^#(s|)(.*?)$`)
h2Reg = regexp.MustCompile(`^##(s|)(.*?)$`)
h3Reg = regexp.MustCompile(`^###(s|)(.*?)$`)
h4Reg = regexp.MustCompile(`^####(s|)(.*?)$`)
h5Reg = regexp.MustCompile(`^#####(s|)(.*?)$`)
h6Reg = regexp.MustCompile(`^######(s|)(.*?)$`)
)
func NewMarkdown(input io.Reader) string {
buf := bytes.NewBuffer(nil)
scanner := bufio.NewScanner(input)
for scanner.Scan() {
line := bytes.TrimSpace(scanner.Bytes())
if len(line) == 0 {
buf.WriteByte('n')
continue
}
// wrap bold and italic text in "<b>" and "<i>" elements
line = boldItalicReg.ReplaceAll(line, byte(`<b><i>$1</i></b>`))
line = boldReg.ReplaceAll(line, byte(`<b>$1</b>`))
line = italicReg.ReplaceAll(line, byte(`<i>$1</i>`))
// wrap strikethrough text in "<s>" tags
line = strikeReg.ReplaceAll(line, byte(`<s>$1</s>`))
// wrap underscored text in "<u>" tags
line = underscoreReg.ReplaceAll(line, byte(`<u>$1</u>`))
// convert links to anchor tags
line = anchorReg.ReplaceAll(line, byte(`<a href="$2">$1</a>`))
// escape and wrap blockquotes in "<blockquote>" tags
line = escapeReg.ReplaceAll(line, byte(`>`))
line = blockquoteReg.ReplaceAll(line, byte(`<blockquote>$1</blockquote>`))
// wrap the content of backticks inside of "<code>" tags
line = backtipReg.ReplaceAll(line, byte(`<code>$1</code>`))
// convert headings
if line[0] == '#' {
count := bytes.Count(line, byte(`#`))
switch count {
case 1:
line = h1Reg.ReplaceAll(line, byte(`<h1>$2</h1>`))
case 2:
line = h2Reg.ReplaceAll(line, byte(`<h2>$2</h2>`))
case 3:
line = h3Reg.ReplaceAll(line, byte(`<h3>$2</h3>`))
case 4:
line = h4Reg.ReplaceAll(line, byte(`<h4>$2</h4>`))
case 5:
line = h5Reg.ReplaceAll(line, byte(`<h5>$2</h5>`))
case 6:
line = h6Reg.ReplaceAll(line, byte(`<h6>$2</h6>`))
}
}
buf.Write(line)
buf.WriteByte('n')
}
return buf.String()
}
Benchmarks
I used the folowing code for benchmarks, on a 20kB md file:
func BenchmarkMarkdown(b *testing.B) {
md, err := ioutil.ReadFile("README.md")
if err != nil {
b.Fail()
}
raw := string(md)
b.ResetTimer()
for n := 0; n < b.N; n++ {
_ = Markdown(raw)
}
}
func BenchmarkMarkdownNew(b *testing.B) {
for n := 0; n < b.N; n++ {
file, err := os.Open("README.md")
if err != nil {
b.Fail()
}
_ = NewMarkdown(file)
file.Close()
}
}
Results:
> go test -bench=. -benchmem
goos: linux
goarch: amd64
BenchmarkMarkdown-4 10 104990431 ns/op 364617427 B/op 493813 allocs/op
BenchmarkMarkdownNew-4 1000 1464745 ns/op 379376 B/op 11085 allocs/op
benchstat diff:
name old time/op new time/op delta
Markdown-4 105ms ± 0% 1ms ± 0% ~ (p=1.000 n=1+1)
name old alloc/op new alloc/op delta
Markdown-4 365MB ± 0% 0MB ± 0% ~ (p=1.000 n=1+1)
name old allocs/op new allocs/op delta
Markdown-4 494k ± 0% 11k ± 0% ~ (p=1.000 n=1+1)
Thank you very much for the answer!
– LogicalBranch
2 days ago
add a comment |
Performance
Regex
regex.MustCompile()
is very expensive! Do not use this method inside a loop !
instead, define your regex as global variables only once:
var (
boldItalicReg = regexp.MustCompile(`***(.*?)***`)
boldReg = regexp.MustCompile(`**(.*?)**`)
...
)
Headers
If a line is a header, it will start by a #
. We can check for this before calling ReplaceAllString()
6 times ! All we need
to do is to trim the line, and then check if it starts with #
:
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "#") {
// convert headings
...
}
We could go further and unrolling the loop to avoid unecessary allocations:
count := strings.Count(line, "#")
switch count {
case 1:
line = h1Reg.ReplaceAllString(line, `<h1>$2</h1>`)
case 2:
...
}
Use a scanner
The idiomatic way to read a file line by line in go is to use a scanner
. It takes an io.Reader
as parameters, so you can directly pass
your mardown file instead of converting it into a string first:
func NewMarkdown(input io.Reader) string {
scanner := bufio.NewScanner(input)
for scanner.Scan() {
line := scanner.Text()
...
}
}
Use byte
instead of string
In go, a string
is a read-only slice of bytes. Working with strings is usually more expensive than working with slice of bytes,
so use byte
instead of strings
when you can:
line := scanner.Bytes()
line = boldItalicReg.ReplaceAll(line, byte(`<b><i>$1</i></b>`))
Write result to a bytes.Buffer
Instead of string.Join()
, we can use a buffer to write each line in order to further reduce the number of allocations:
buf := bytes.NewBuffer(nil)
scanner := bufio.NewScanner(input)
for scanner.Scan() {
line := scanner.Bytes()
...
buf.Write(line)
buf.WriteByte('n')
}
return buf.String()
final code:
package parse
import (
"bufio"
"bytes"
"io"
"regexp"
)
var (
boldItalicReg = regexp.MustCompile(`***(.*?)***`)
boldReg = regexp.MustCompile(`**(.*?)**`)
italicReg = regexp.MustCompile(`*(.*?)*`)
strikeReg = regexp.MustCompile(`~~(.*?)~~`)
underscoreReg = regexp.MustCompile(`__(.*?)__`)
anchorReg = regexp.MustCompile(`[(.*?)]((.*?))[^)]`)
escapeReg = regexp.MustCompile(`^>(s|)`)
blockquoteReg = regexp.MustCompile(`>(.*?)$`)
backtipReg = regexp.MustCompile("`(.*?)`")
h1Reg = regexp.MustCompile(`^#(s|)(.*?)$`)
h2Reg = regexp.MustCompile(`^##(s|)(.*?)$`)
h3Reg = regexp.MustCompile(`^###(s|)(.*?)$`)
h4Reg = regexp.MustCompile(`^####(s|)(.*?)$`)
h5Reg = regexp.MustCompile(`^#####(s|)(.*?)$`)
h6Reg = regexp.MustCompile(`^######(s|)(.*?)$`)
)
func NewMarkdown(input io.Reader) string {
buf := bytes.NewBuffer(nil)
scanner := bufio.NewScanner(input)
for scanner.Scan() {
line := bytes.TrimSpace(scanner.Bytes())
if len(line) == 0 {
buf.WriteByte('n')
continue
}
// wrap bold and italic text in "<b>" and "<i>" elements
line = boldItalicReg.ReplaceAll(line, byte(`<b><i>$1</i></b>`))
line = boldReg.ReplaceAll(line, byte(`<b>$1</b>`))
line = italicReg.ReplaceAll(line, byte(`<i>$1</i>`))
// wrap strikethrough text in "<s>" tags
line = strikeReg.ReplaceAll(line, byte(`<s>$1</s>`))
// wrap underscored text in "<u>" tags
line = underscoreReg.ReplaceAll(line, byte(`<u>$1</u>`))
// convert links to anchor tags
line = anchorReg.ReplaceAll(line, byte(`<a href="$2">$1</a>`))
// escape and wrap blockquotes in "<blockquote>" tags
line = escapeReg.ReplaceAll(line, byte(`>`))
line = blockquoteReg.ReplaceAll(line, byte(`<blockquote>$1</blockquote>`))
// wrap the content of backticks inside of "<code>" tags
line = backtipReg.ReplaceAll(line, byte(`<code>$1</code>`))
// convert headings
if line[0] == '#' {
count := bytes.Count(line, byte(`#`))
switch count {
case 1:
line = h1Reg.ReplaceAll(line, byte(`<h1>$2</h1>`))
case 2:
line = h2Reg.ReplaceAll(line, byte(`<h2>$2</h2>`))
case 3:
line = h3Reg.ReplaceAll(line, byte(`<h3>$2</h3>`))
case 4:
line = h4Reg.ReplaceAll(line, byte(`<h4>$2</h4>`))
case 5:
line = h5Reg.ReplaceAll(line, byte(`<h5>$2</h5>`))
case 6:
line = h6Reg.ReplaceAll(line, byte(`<h6>$2</h6>`))
}
}
buf.Write(line)
buf.WriteByte('n')
}
return buf.String()
}
Benchmarks
I used the folowing code for benchmarks, on a 20kB md file:
func BenchmarkMarkdown(b *testing.B) {
md, err := ioutil.ReadFile("README.md")
if err != nil {
b.Fail()
}
raw := string(md)
b.ResetTimer()
for n := 0; n < b.N; n++ {
_ = Markdown(raw)
}
}
func BenchmarkMarkdownNew(b *testing.B) {
for n := 0; n < b.N; n++ {
file, err := os.Open("README.md")
if err != nil {
b.Fail()
}
_ = NewMarkdown(file)
file.Close()
}
}
Results:
> go test -bench=. -benchmem
goos: linux
goarch: amd64
BenchmarkMarkdown-4 10 104990431 ns/op 364617427 B/op 493813 allocs/op
BenchmarkMarkdownNew-4 1000 1464745 ns/op 379376 B/op 11085 allocs/op
benchstat diff:
name old time/op new time/op delta
Markdown-4 105ms ± 0% 1ms ± 0% ~ (p=1.000 n=1+1)
name old alloc/op new alloc/op delta
Markdown-4 365MB ± 0% 0MB ± 0% ~ (p=1.000 n=1+1)
name old allocs/op new allocs/op delta
Markdown-4 494k ± 0% 11k ± 0% ~ (p=1.000 n=1+1)
Thank you very much for the answer!
– LogicalBranch
2 days ago
add a comment |
Performance
Regex
regex.MustCompile()
is very expensive! Do not use this method inside a loop !
instead, define your regex as global variables only once:
var (
boldItalicReg = regexp.MustCompile(`***(.*?)***`)
boldReg = regexp.MustCompile(`**(.*?)**`)
...
)
Headers
If a line is a header, it will start by a #
. We can check for this before calling ReplaceAllString()
6 times ! All we need
to do is to trim the line, and then check if it starts with #
:
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "#") {
// convert headings
...
}
We could go further and unrolling the loop to avoid unecessary allocations:
count := strings.Count(line, "#")
switch count {
case 1:
line = h1Reg.ReplaceAllString(line, `<h1>$2</h1>`)
case 2:
...
}
Use a scanner
The idiomatic way to read a file line by line in go is to use a scanner
. It takes an io.Reader
as parameters, so you can directly pass
your mardown file instead of converting it into a string first:
func NewMarkdown(input io.Reader) string {
scanner := bufio.NewScanner(input)
for scanner.Scan() {
line := scanner.Text()
...
}
}
Use byte
instead of string
In go, a string
is a read-only slice of bytes. Working with strings is usually more expensive than working with slice of bytes,
so use byte
instead of strings
when you can:
line := scanner.Bytes()
line = boldItalicReg.ReplaceAll(line, byte(`<b><i>$1</i></b>`))
Write result to a bytes.Buffer
Instead of string.Join()
, we can use a buffer to write each line in order to further reduce the number of allocations:
buf := bytes.NewBuffer(nil)
scanner := bufio.NewScanner(input)
for scanner.Scan() {
line := scanner.Bytes()
...
buf.Write(line)
buf.WriteByte('n')
}
return buf.String()
final code:
package parse
import (
"bufio"
"bytes"
"io"
"regexp"
)
var (
boldItalicReg = regexp.MustCompile(`***(.*?)***`)
boldReg = regexp.MustCompile(`**(.*?)**`)
italicReg = regexp.MustCompile(`*(.*?)*`)
strikeReg = regexp.MustCompile(`~~(.*?)~~`)
underscoreReg = regexp.MustCompile(`__(.*?)__`)
anchorReg = regexp.MustCompile(`[(.*?)]((.*?))[^)]`)
escapeReg = regexp.MustCompile(`^>(s|)`)
blockquoteReg = regexp.MustCompile(`>(.*?)$`)
backtipReg = regexp.MustCompile("`(.*?)`")
h1Reg = regexp.MustCompile(`^#(s|)(.*?)$`)
h2Reg = regexp.MustCompile(`^##(s|)(.*?)$`)
h3Reg = regexp.MustCompile(`^###(s|)(.*?)$`)
h4Reg = regexp.MustCompile(`^####(s|)(.*?)$`)
h5Reg = regexp.MustCompile(`^#####(s|)(.*?)$`)
h6Reg = regexp.MustCompile(`^######(s|)(.*?)$`)
)
func NewMarkdown(input io.Reader) string {
buf := bytes.NewBuffer(nil)
scanner := bufio.NewScanner(input)
for scanner.Scan() {
line := bytes.TrimSpace(scanner.Bytes())
if len(line) == 0 {
buf.WriteByte('n')
continue
}
// wrap bold and italic text in "<b>" and "<i>" elements
line = boldItalicReg.ReplaceAll(line, byte(`<b><i>$1</i></b>`))
line = boldReg.ReplaceAll(line, byte(`<b>$1</b>`))
line = italicReg.ReplaceAll(line, byte(`<i>$1</i>`))
// wrap strikethrough text in "<s>" tags
line = strikeReg.ReplaceAll(line, byte(`<s>$1</s>`))
// wrap underscored text in "<u>" tags
line = underscoreReg.ReplaceAll(line, byte(`<u>$1</u>`))
// convert links to anchor tags
line = anchorReg.ReplaceAll(line, byte(`<a href="$2">$1</a>`))
// escape and wrap blockquotes in "<blockquote>" tags
line = escapeReg.ReplaceAll(line, byte(`>`))
line = blockquoteReg.ReplaceAll(line, byte(`<blockquote>$1</blockquote>`))
// wrap the content of backticks inside of "<code>" tags
line = backtipReg.ReplaceAll(line, byte(`<code>$1</code>`))
// convert headings
if line[0] == '#' {
count := bytes.Count(line, byte(`#`))
switch count {
case 1:
line = h1Reg.ReplaceAll(line, byte(`<h1>$2</h1>`))
case 2:
line = h2Reg.ReplaceAll(line, byte(`<h2>$2</h2>`))
case 3:
line = h3Reg.ReplaceAll(line, byte(`<h3>$2</h3>`))
case 4:
line = h4Reg.ReplaceAll(line, byte(`<h4>$2</h4>`))
case 5:
line = h5Reg.ReplaceAll(line, byte(`<h5>$2</h5>`))
case 6:
line = h6Reg.ReplaceAll(line, byte(`<h6>$2</h6>`))
}
}
buf.Write(line)
buf.WriteByte('n')
}
return buf.String()
}
Benchmarks
I used the folowing code for benchmarks, on a 20kB md file:
func BenchmarkMarkdown(b *testing.B) {
md, err := ioutil.ReadFile("README.md")
if err != nil {
b.Fail()
}
raw := string(md)
b.ResetTimer()
for n := 0; n < b.N; n++ {
_ = Markdown(raw)
}
}
func BenchmarkMarkdownNew(b *testing.B) {
for n := 0; n < b.N; n++ {
file, err := os.Open("README.md")
if err != nil {
b.Fail()
}
_ = NewMarkdown(file)
file.Close()
}
}
Results:
> go test -bench=. -benchmem
goos: linux
goarch: amd64
BenchmarkMarkdown-4 10 104990431 ns/op 364617427 B/op 493813 allocs/op
BenchmarkMarkdownNew-4 1000 1464745 ns/op 379376 B/op 11085 allocs/op
benchstat diff:
name old time/op new time/op delta
Markdown-4 105ms ± 0% 1ms ± 0% ~ (p=1.000 n=1+1)
name old alloc/op new alloc/op delta
Markdown-4 365MB ± 0% 0MB ± 0% ~ (p=1.000 n=1+1)
name old allocs/op new allocs/op delta
Markdown-4 494k ± 0% 11k ± 0% ~ (p=1.000 n=1+1)
Performance
Regex
regex.MustCompile()
is very expensive! Do not use this method inside a loop !
instead, define your regex as global variables only once:
var (
boldItalicReg = regexp.MustCompile(`***(.*?)***`)
boldReg = regexp.MustCompile(`**(.*?)**`)
...
)
Headers
If a line is a header, it will start by a #
. We can check for this before calling ReplaceAllString()
6 times ! All we need
to do is to trim the line, and then check if it starts with #
:
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "#") {
// convert headings
...
}
We could go further and unrolling the loop to avoid unecessary allocations:
count := strings.Count(line, "#")
switch count {
case 1:
line = h1Reg.ReplaceAllString(line, `<h1>$2</h1>`)
case 2:
...
}
Use a scanner
The idiomatic way to read a file line by line in go is to use a scanner
. It takes an io.Reader
as parameters, so you can directly pass
your mardown file instead of converting it into a string first:
func NewMarkdown(input io.Reader) string {
scanner := bufio.NewScanner(input)
for scanner.Scan() {
line := scanner.Text()
...
}
}
Use byte
instead of string
In go, a string
is a read-only slice of bytes. Working with strings is usually more expensive than working with slice of bytes,
so use byte
instead of strings
when you can:
line := scanner.Bytes()
line = boldItalicReg.ReplaceAll(line, byte(`<b><i>$1</i></b>`))
Write result to a bytes.Buffer
Instead of string.Join()
, we can use a buffer to write each line in order to further reduce the number of allocations:
buf := bytes.NewBuffer(nil)
scanner := bufio.NewScanner(input)
for scanner.Scan() {
line := scanner.Bytes()
...
buf.Write(line)
buf.WriteByte('n')
}
return buf.String()
final code:
package parse
import (
"bufio"
"bytes"
"io"
"regexp"
)
var (
boldItalicReg = regexp.MustCompile(`***(.*?)***`)
boldReg = regexp.MustCompile(`**(.*?)**`)
italicReg = regexp.MustCompile(`*(.*?)*`)
strikeReg = regexp.MustCompile(`~~(.*?)~~`)
underscoreReg = regexp.MustCompile(`__(.*?)__`)
anchorReg = regexp.MustCompile(`[(.*?)]((.*?))[^)]`)
escapeReg = regexp.MustCompile(`^>(s|)`)
blockquoteReg = regexp.MustCompile(`>(.*?)$`)
backtipReg = regexp.MustCompile("`(.*?)`")
h1Reg = regexp.MustCompile(`^#(s|)(.*?)$`)
h2Reg = regexp.MustCompile(`^##(s|)(.*?)$`)
h3Reg = regexp.MustCompile(`^###(s|)(.*?)$`)
h4Reg = regexp.MustCompile(`^####(s|)(.*?)$`)
h5Reg = regexp.MustCompile(`^#####(s|)(.*?)$`)
h6Reg = regexp.MustCompile(`^######(s|)(.*?)$`)
)
func NewMarkdown(input io.Reader) string {
buf := bytes.NewBuffer(nil)
scanner := bufio.NewScanner(input)
for scanner.Scan() {
line := bytes.TrimSpace(scanner.Bytes())
if len(line) == 0 {
buf.WriteByte('n')
continue
}
// wrap bold and italic text in "<b>" and "<i>" elements
line = boldItalicReg.ReplaceAll(line, byte(`<b><i>$1</i></b>`))
line = boldReg.ReplaceAll(line, byte(`<b>$1</b>`))
line = italicReg.ReplaceAll(line, byte(`<i>$1</i>`))
// wrap strikethrough text in "<s>" tags
line = strikeReg.ReplaceAll(line, byte(`<s>$1</s>`))
// wrap underscored text in "<u>" tags
line = underscoreReg.ReplaceAll(line, byte(`<u>$1</u>`))
// convert links to anchor tags
line = anchorReg.ReplaceAll(line, byte(`<a href="$2">$1</a>`))
// escape and wrap blockquotes in "<blockquote>" tags
line = escapeReg.ReplaceAll(line, byte(`>`))
line = blockquoteReg.ReplaceAll(line, byte(`<blockquote>$1</blockquote>`))
// wrap the content of backticks inside of "<code>" tags
line = backtipReg.ReplaceAll(line, byte(`<code>$1</code>`))
// convert headings
if line[0] == '#' {
count := bytes.Count(line, byte(`#`))
switch count {
case 1:
line = h1Reg.ReplaceAll(line, byte(`<h1>$2</h1>`))
case 2:
line = h2Reg.ReplaceAll(line, byte(`<h2>$2</h2>`))
case 3:
line = h3Reg.ReplaceAll(line, byte(`<h3>$2</h3>`))
case 4:
line = h4Reg.ReplaceAll(line, byte(`<h4>$2</h4>`))
case 5:
line = h5Reg.ReplaceAll(line, byte(`<h5>$2</h5>`))
case 6:
line = h6Reg.ReplaceAll(line, byte(`<h6>$2</h6>`))
}
}
buf.Write(line)
buf.WriteByte('n')
}
return buf.String()
}
Benchmarks
I used the folowing code for benchmarks, on a 20kB md file:
func BenchmarkMarkdown(b *testing.B) {
md, err := ioutil.ReadFile("README.md")
if err != nil {
b.Fail()
}
raw := string(md)
b.ResetTimer()
for n := 0; n < b.N; n++ {
_ = Markdown(raw)
}
}
func BenchmarkMarkdownNew(b *testing.B) {
for n := 0; n < b.N; n++ {
file, err := os.Open("README.md")
if err != nil {
b.Fail()
}
_ = NewMarkdown(file)
file.Close()
}
}
Results:
> go test -bench=. -benchmem
goos: linux
goarch: amd64
BenchmarkMarkdown-4 10 104990431 ns/op 364617427 B/op 493813 allocs/op
BenchmarkMarkdownNew-4 1000 1464745 ns/op 379376 B/op 11085 allocs/op
benchstat diff:
name old time/op new time/op delta
Markdown-4 105ms ± 0% 1ms ± 0% ~ (p=1.000 n=1+1)
name old alloc/op new alloc/op delta
Markdown-4 365MB ± 0% 0MB ± 0% ~ (p=1.000 n=1+1)
name old allocs/op new allocs/op delta
Markdown-4 494k ± 0% 11k ± 0% ~ (p=1.000 n=1+1)
answered Dec 28 '18 at 10:48
felix
75339
75339
Thank you very much for the answer!
– LogicalBranch
2 days ago
add a comment |
Thank you very much for the answer!
– LogicalBranch
2 days ago
Thank you very much for the answer!
– LogicalBranch
2 days ago
Thank you very much for the answer!
– LogicalBranch
2 days ago
add a comment |
LogicalBranch is a new contributor. Be nice, and check out our Code of Conduct.
LogicalBranch is a new contributor. Be nice, and check out our Code of Conduct.
LogicalBranch is a new contributor. Be nice, and check out our Code of Conduct.
LogicalBranch is a new contributor. Be nice, and check out our Code of Conduct.
Thanks for contributing an answer to Code Review Stack Exchange!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
Use MathJax to format equations. MathJax reference.
To learn more, see our tips on writing great answers.
Some of your past answers have not been well-received, and you're in danger of being blocked from answering.
Please pay close attention to the following guidance:
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f210422%2fconverting-markdown-to-html-using-go%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown