From 862f9837e46f7ccb6523979cdc4012931c946663 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 1 Apr 2024 11:54:14 +0200 Subject: [PATCH] Implemented links --- README.md | 6 +++-- Render.go | 71 ++++++++++++++++++++++++++++++++++++++++++++++++-- Render_test.go | 19 +++++++++++++- 3 files changed, 91 insertions(+), 5 deletions(-) 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"), "
Header
") } +func TestLink(t *testing.T) { + assert.Equal(t, markdown.Render("[text](https://example.com/)"), "

text

") + 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("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."), "

Header

Line 1. Line 2. Line 3.

") assert.Equal(t, markdown.Render("# Header 1\nLine 1.\n# Header 2\nLine 2."), "

Header 1

Line 1.

Header 2

Line 2.

") + assert.Equal(t, markdown.Render("# [Header Link](https://example.com/)"), "

Header Link

") +} + +func TestSecurity(t *testing.T) { + assert.Equal(t, markdown.Render("[text](javascript:alert(\"xss\"))"), "

text

") + assert.Equal(t, markdown.Render("[text](javAscRipt:alert(\"xss\"))"), "

text

") + assert.Equal(t, markdown.Render("[text](\">
html
)"), "

text

") + assert.Equal(t, markdown.Render("[
html
]()"), "

<div>html</div>

") }