diff --git a/README.md b/README.md index b0d3057..ea5cc1b 100644 --- a/README.md +++ b/README.md @@ -4,20 +4,23 @@ A markdown renderer that supports only a subset of the CommonMark spec in order ## Features -- Code blocks -- Formatting (bold, italic, monospace) +- Bold +- Code +- Italic - Links - Lists +- Images - Headers - Paragraphs - Quotes - Separators +- Strikethrough - Tables ## Installation ```shell -go get git.akyoto.dev/go/markdown +go get git.urbach.dev/go/markdown ``` ## Usage @@ -34,7 +37,9 @@ PASS: TestParagraph PASS: TestHeader PASS: TestItalic PASS: TestBold +PASS: TestStrike PASS: TestLink +PASS: TestImage PASS: TestList PASS: TestTables PASS: TestCode @@ -48,14 +53,14 @@ coverage: 100.0% of statements ## Benchmarks ``` -BenchmarkSmall-12 5986922 187.5 ns/op 32 B/op 1 allocs/op -BenchmarkMedium-12 1000000 1077 ns/op 512 B/op 1 allocs/op -BenchmarkLarge-12 255178 4055 ns/op 2560 B/op 2 allocs/op +BenchmarkSmall-12 5884641 201.5 ns/op 32 B/op 1 allocs/op +BenchmarkMedium-12 938371 1124 ns/op 512 B/op 1 allocs/op +BenchmarkLarge-12 277065 4115 ns/op 2560 B/op 2 allocs/op ``` ## License -Please see the [license documentation](https://akyoto.dev/license). +Please see the [license documentation](https://urbach.dev/license). ## Copyright diff --git a/Render.go b/Render.go index 3bc3df4..d9facae 100644 --- a/Render.go +++ b/Render.go @@ -17,6 +17,7 @@ type renderer struct { paragraphLevel int quoteLevel int listLevel int + olistLevel int tableLevel int codeLines int tableHeaderWritten bool @@ -200,6 +201,26 @@ func (r *renderer) processLine(line string) { } } + pos := 0 + + for pos < len(line) && line[pos] >= '0' && line[pos] <= '9' { + pos++ + + if pos < len(line) && (line[pos] == '.' || line[pos] == ')') { + line = strings.TrimSpace(line[pos+1:]) + + if r.olistLevel == 0 { + r.WriteString("
") r.paragraphLevel++ @@ -233,7 +254,12 @@ func (r *renderer) closeLists() { r.WriteString("") } + for range r.olistLevel { + r.WriteString("
")
- r.WriteString(html.EscapeString(markdown[codeStart:i]))
- r.WriteString("
")
- codeStart = -1
- tokenStart = i + 1
- } else {
- r.WriteString(html.EscapeString(markdown[tokenStart:i]))
- tokenStart = i
- codeStart = i + 1
+ end := strings.IndexByte(markdown[searchStart:], '`')
+
+ if end == -1 {
+ continue
}
+ r.WriteString(html.EscapeString(markdown[tokenStart:i]))
+ r.WriteString("")
+ r.WriteString(html.EscapeString(markdown[searchStart : searchStart+end]))
+ r.WriteString("
")
+
+ searchStart += end + 1
+ tokenStart = searchStart
+
case '*', '_':
if i == emStart {
strongStart = i + 1
@@ -343,6 +394,34 @@ func (r *renderer) writeText(markdown string) {
tokenStart = i
emStart = i + 1
}
+
+ case '~':
+ if i+1 >= len(markdown) || markdown[i+1] != '~' {
+ continue
+ }
+
+ if strikeStart != -1 {
+ r.WriteString("bold
") } +func TestStrike(t *testing.T) { + assert.Equal(t, markdown.Render("~normal text~"), "~normal text~
") + assert.Equal(t, markdown.Render("~~deleted text~~"), "deleted text
[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.
") + assert.Equal(t, markdown.Render("[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("text](https://example.com/)"), `text](https://example.com/)
`) + assert.Equal(t, markdown.Render("[text](https://example.com/_test_)"), ``) + assert.Equal(t, markdown.Render("Prefix [text](https://example.com/) suffix."), `Prefix text suffix.
`) +} + +func TestImage(t *testing.T) { + assert.Equal(t, markdown.Render("!"), `!
`) + assert.Equal(t, markdown.Render("!["), `![
`) + assert.Equal(t, markdown.Render("![]"), `![]
`) + assert.Equal(t, markdown.Render(", ` + assert.Equal(t, markdown.Render("![]()"), `-
") + assert.Equal(t, markdown.Render("- "), "-
") +} + +func TestOrderedList(t *testing.T) { + assert.Equal(t, markdown.Render("1"), "1
") + assert.Equal(t, markdown.Render("1."), "Text
")
assert.Equal(t, markdown.Render("```go\ntype A struct {\n\t\n}\n```"), "type A struct {\n\t\n}
")
assert.Equal(t, markdown.Render("`monospace`"), "monospace
`unfinished
") assert.Equal(t, markdown.Render("Inline `monospace` text."), "Inline monospace
text.
Line 1.
") - 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/)"), "Text.
") - assert.Equal(t, markdown.Render("- Entry\n# Header"), "") - assert.Equal(t, markdown.Render("> **bold** and *italic* text."), "
- Entry
Header
") - assert.Equal(t, markdown.Render("> __bold__ and _italic_ text."), "bold and italic text.
") + assert.Equal(t, markdown.Render("# Header\n\nLine 1."), `bold and italic text.
Line 1.
`) + 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/)"), `Text.
`) + assert.Equal(t, markdown.Render("- Entry\n# Header"), ``) + assert.Equal(t, markdown.Render("> **bold** and *italic* text."), `
- Entry
Header
`) + assert.Equal(t, markdown.Render("> __bold__ and _italic_ text."), `bold and italic text.
`) } func TestSecurity(t *testing.T) { - assert.Equal(t, markdown.Render("[text](javascript:alert(\"xss\"))"), "") - assert.Equal(t, markdown.Render("[text](javAscRipt:alert(\"xss\"))"), "") - assert.Equal(t, markdown.Render("[text]( javascript:alert(\"xss\"))"), "") - assert.Equal(t, markdown.Render("[text]('javAscRipt:alert(\"xss\")')"), "") - assert.Equal(t, markdown.Render("[text](\">)"), "") - assert.Equal(t, markdown.Render("[]()"), "") -} + assert.Equal(t, markdown.Render(`[text](javascript:alert("xss"))`), ``) + assert.Equal(t, markdown.Render(`[text](javAscRipt:alert("xss"))`), ``) + assert.Equal(t, markdown.Render(`[text]( javascript:alert("xss"))`), ``) + assert.Equal(t, markdown.Render(`[text]('javAscRipt:alert("xss")')`), ``) + assert.Equal(t, markdown.Render(`[text](">)`), ``) + assert.Equal(t, markdown.Render(`[]()`), ``) +} \ No newline at end of file diff --git a/benchmarks_test.go b/benchmarks_test.go index c9231e9..50501be 100644 --- a/benchmarks_test.go +++ b/benchmarks_test.go @@ -4,8 +4,8 @@ import ( "os" "testing" - "git.akyoto.dev/go/assert" - "git.akyoto.dev/go/markdown" + "git.urbach.dev/go/assert" + "git.urbach.dev/go/markdown" ) func BenchmarkSmall(b *testing.B) { @@ -13,7 +13,7 @@ func BenchmarkSmall(b *testing.B) { assert.Nil(b, err) input := string(small) - for range b.N { + for b.Loop() { markdown.Render(input) } } @@ -23,7 +23,7 @@ func BenchmarkMedium(b *testing.B) { assert.Nil(b, err) input := string(medium) - for range b.N { + for b.Loop() { markdown.Render(input) } } @@ -33,7 +33,7 @@ func BenchmarkLarge(b *testing.B) { assert.Nil(b, err) input := string(large) - for range b.N { + for b.Loop() { markdown.Render(input) } -} +} \ No newline at end of file diff --git a/go.mod b/go.mod index f178769..229a4be 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,5 @@ -module git.akyoto.dev/go/markdown +module git.urbach.dev/go/markdown -go 1.22.1 +go 1.24 -require git.akyoto.dev/go/assert v0.1.3 +require git.urbach.dev/go/assert v0.0.0-20250606150337-559d3d3afcda diff --git a/go.sum b/go.sum index 9fc2547..7684754 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,2 @@ -git.akyoto.dev/go/assert v0.1.3 h1:QwCUbmG4aZYsNk/OuRBz1zWVKmGlDUHhOnnDBfn8Qw8= -git.akyoto.dev/go/assert v0.1.3/go.mod h1:0GzMaM0eURuDwtGkJJkCsI7r2aUKr+5GmWNTFPgDocM= +git.urbach.dev/go/assert v0.0.0-20250606150337-559d3d3afcda h1:VN6ZQwtwLOm2xTms+v8IIeeNjvs55qyEBNArv3dPq9g= +git.urbach.dev/go/assert v0.0.0-20250606150337-559d3d3afcda/go.mod h1:PNI/NSBOqvoeU58/7eBsIR09Yoq2S/qtSRiTrctkiq0=bold and italic text.