diff --git a/README.md b/README.md index b3d4f7f..762274f 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ Markdown renderer. ## Features +- Links - Headers - Paragraphs @@ -17,7 +18,6 @@ go get git.akyoto.dev/go/markdown ```go html := markdown.Render("# Header") -fmt.Println(html) ``` ## Tests @@ -26,14 +26,16 @@ fmt.Println(html) PASS: TestEmpty PASS: TestParagraphs PASS: TestHeader +PASS: TestLink PASS: TestCombined +PASS: TestSecurity coverage: 100.0% of statements ``` ## Benchmarks ``` -BenchmarkSmall-12 2187232 544.9 ns/op 296 B/op 9 allocs/op +BenchmarkSmall-12 2019103 591.4 ns/op 296 B/op 9 allocs/op ``` ## License diff --git a/Render.go b/Render.go index 55c3f83..07e23e5 100644 --- a/Render.go +++ b/Render.go @@ -25,7 +25,7 @@ func Render(markdown string) string { } out.WriteString("
") - out.WriteString(html.EscapeString(paragraph.String())) + writeText(&out, paragraph.String()) out.WriteString("
") paragraph.Reset() } @@ -52,7 +52,7 @@ func Render(markdown string) string { if space > 0 && space <= 6 { out.WriteString(headerStart[space-1]) - out.WriteString(html.EscapeString(line[space+1:])) + writeText(&out, line[space+1:]) out.WriteString(headerEnd[space-1]) } @@ -69,3 +69,70 @@ func Render(markdown string) string { } } } + +func writeText(out *strings.Builder, text string) { + var ( + i = 0 + tokenStart = 0 + ) + + var ( + textStart = -1 + textEnd = -1 + urlStart = -1 + parentheses = 0 + ) + + for { + if i == len(text) { + out.WriteString(html.EscapeString(text[tokenStart:])) + return + } + + c := text[i] + + switch c { + case '[': + out.WriteString(html.EscapeString(text[tokenStart:i])) + tokenStart = i + textStart = i + case ']': + textEnd = i + case '(': + if parentheses == 0 { + urlStart = i + } + + parentheses++ + case ')': + parentheses-- + + if parentheses == 0 && textStart >= 0 && textEnd >= 0 && urlStart >= 0 { + linkText := text[textStart+1 : textEnd] + linkURL := text[urlStart+1 : i] + + out.WriteString("") + out.WriteString(html.EscapeString(linkText)) + out.WriteString("") + + textStart = -1 + textEnd = -1 + urlStart = -1 + + tokenStart = i + 1 + } + } + + i++ + } +} + +func formatURL(linkURL string) string { + if strings.HasPrefix(strings.ToLower(linkURL), "javascript:") { + return "" + } + + return html.EscapeString(linkURL) +} diff --git a/Render_test.go b/Render_test.go index d20a666..74108d1 100644 --- a/Render_test.go +++ b/Render_test.go @@ -11,7 +11,7 @@ func TestEmpty(t *testing.T) { assert.Equal(t, markdown.Render(""), "") } -func TestParagraphs(t *testing.T) { +func TestParagraph(t *testing.T) { assert.Equal(t, markdown.Render("Text"), "Text
") assert.Equal(t, markdown.Render("Text\n"), "Text
") assert.Equal(t, markdown.Render("Text\n\n"), "Text
") @@ -28,7 +28,24 @@ func TestHeader(t *testing.T) { assert.Equal(t, markdown.Render("###### Header"), "[text](https://example.com/
") + assert.Equal(t, markdown.Render("[text]https://example.com/)"), "[text]https://example.com/)
") + assert.Equal(t, markdown.Render("[text(https://example.com/)"), "[text(https://example.com/)
") + assert.Equal(t, markdown.Render("text](https://example.com/)"), "text](https://example.com/)
") + assert.Equal(t, markdown.Render("Prefix [text](https://example.com/) suffix."), "Prefix text suffix.
") +} + func TestCombined(t *testing.T) { assert.Equal(t, markdown.Render("# Header\nLine 1.\nLine 2.\nLine 3."), "Line 1. Line 2. Line 3.
") assert.Equal(t, markdown.Render("# Header 1\nLine 1.\n# Header 2\nLine 2."), "Line 1.
Line 2.
") + assert.Equal(t, markdown.Render("# [Header Link](https://example.com/)"), "