aboutsummaryrefslogtreecommitdiff
path: root/markdown
diff options
context:
space:
mode:
authorAnhgelus Morhtuuzh <william@herges.fr>2025-10-02 19:52:38 +0200
committerAnhgelus Morhtuuzh <william@herges.fr>2025-10-02 19:52:38 +0200
commit94dceb4f7c1740de9215b36ec183f93ca4337ee7 (patch)
tree5ad184efb0b74dd3aa4da7585f88a3e3f6cb4ecc /markdown
parent8b249c9ce8bd1c351daf80c6c9b095fb1bccafe2 (diff)
style(markdown): fix typo in package name
Diffstat (limited to 'markdown')
-rw-r--r--markdown/ast.go107
-rw-r--r--markdown/ast_code.go66
-rw-r--r--markdown/ast_external.go179
-rw-r--r--markdown/ast_external_test.go49
-rw-r--r--markdown/ast_header.go39
-rw-r--r--markdown/ast_list.go79
-rw-r--r--markdown/ast_list_test.go44
-rw-r--r--markdown/ast_modifier.go150
-rw-r--r--markdown/ast_modifier_test.go22
-rw-r--r--markdown/ast_paragraph.go101
-rw-r--r--markdown/ast_paragraph_test.go20
-rw-r--r--markdown/ast_quote.go81
-rw-r--r--markdown/ast_quote_test.go23
-rw-r--r--markdown/ast_test.go62
-rw-r--r--markdown/error.go43
-rw-r--r--markdown/error_test.go26
-rw-r--r--markdown/eval.go16
-rw-r--r--markdown/lexer.go132
-rw-r--r--markdown/lexer_test.go30
19 files changed, 1269 insertions, 0 deletions
diff --git a/markdown/ast.go b/markdown/ast.go
new file mode 100644
index 0000000..c45eb1e
--- /dev/null
+++ b/markdown/ast.go
@@ -0,0 +1,107 @@
+package markdown
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "html/template"
+ "strings"
+)
+
+var ErrUnkownLexType = errors.New("unkown lex type")
+
+type block interface {
+ Eval() (template.HTML, *ParseError)
+}
+
+type tree struct {
+ blocks []block
+}
+
+func (t *tree) Eval() (template.HTML, *ParseError) {
+ var content template.HTML
+ for _, c := range t.blocks {
+ ct, err := c.Eval()
+ if err != nil {
+ return "", err
+ }
+ content += ct
+ }
+ return content, nil
+}
+
+func (t *tree) String() string {
+ b, _ := json.MarshalIndent(t, "", " ")
+ return string(b)
+}
+
+func ast(lxs *lexers) (*tree, *ParseError) {
+ tr := new(tree)
+ newLine := true
+ for lxs.Next() {
+ b, err := getBlock(lxs, newLine)
+ if err != nil {
+ return nil, err
+ }
+ if b != nil {
+ tr.blocks = append(tr.blocks, b)
+ }
+ if !lxs.Finished() {
+ newLine = lxs.Current().Type == lexerBreak
+ }
+ }
+ return tr, nil
+}
+
+func getBlock(lxs *lexers, newLine bool) (block, *ParseError) {
+ var b block
+ var err *ParseError
+ switch lxs.Current().Type {
+ case lexerHeader:
+ if !newLine {
+ b, err = paragraph(lxs, false)
+ } else {
+ b, err = header(lxs)
+ }
+ case lexerExternal:
+ if newLine && lxs.Current().Value == "![" {
+ b, err = external(lxs)
+ } else {
+ b, err = paragraph(lxs, false)
+ }
+ case lexerQuote:
+ if newLine {
+ b, err = quote(lxs)
+ } else {
+ b, err = paragraph(lxs, false)
+ }
+ case lexerList:
+ if newLine {
+ b, err = list(lxs)
+ } else {
+ b, err = paragraph(lxs, false)
+ }
+ case lexerCode:
+ if !newLine && len(lxs.Current().Value) == 3 {
+ return nil, &ParseError{lxs: *lxs, internal: ErrInvalidCodeBlockPosition}
+ }
+ if len(lxs.Current().Value) == 1 {
+ b, err = paragraph(lxs, false)
+ } else {
+ b, err = code(lxs)
+ }
+ case lexerLiteral, lexerModifier:
+ b, err = paragraph(lxs, false)
+ case lexerBreak: // do nothing
+ default:
+ err = &ParseError{
+ lxs: *lxs,
+ internal: errors.Join(ErrUnkownLexType, fmt.Errorf("type received: %s", lxs.Current().Type)),
+ }
+ }
+ return b, err
+}
+
+func trimSpace(s template.HTML) template.HTML {
+ return template.HTML(strings.TrimSpace(string(s)))
+}
diff --git a/markdown/ast_code.go b/markdown/ast_code.go
new file mode 100644
index 0000000..85a3668
--- /dev/null
+++ b/markdown/ast_code.go
@@ -0,0 +1,66 @@
+package markdown
+
+import (
+ "errors"
+ "fmt"
+ "html/template"
+)
+
+var (
+ ErrUnknownCodeType = errors.New("unkown code type")
+ ErrInvalidCodeFormat = errors.New("invalid code format")
+ ErrInvalidCodeBlockPosition = errors.Join(ErrInvalidParagraph, errors.New("invalid code block position"))
+)
+
+type codeType uint
+
+const (
+ codeOneLine codeType = 1
+ codeMultiLine codeType = 2
+)
+
+type astCode struct {
+ content string
+ before string
+ codeType codeType
+}
+
+func (a *astCode) Eval() (template.HTML, *ParseError) {
+ switch a.codeType {
+ case codeOneLine:
+ return template.HTML(fmt.Sprintf("<code>%s</code>", template.HTMLEscapeString(a.content))), nil
+ case codeMultiLine:
+ return template.HTML(fmt.Sprintf("<pre><code>%s</code></pre>", template.HTMLEscapeString(a.content))), nil
+ default:
+ return "", &ParseError{lxs: lexers{}, internal: ErrUnknownCodeType}
+ }
+}
+
+func code(lxs *lexers) (*astCode, *ParseError) {
+ tree := new(astCode)
+ current := lxs.Current().Value
+ if len(current) == 3 {
+ tree.codeType = codeMultiLine
+ } else if len(current) == 1 {
+ tree.codeType = codeOneLine
+ } else {
+ return nil, &ParseError{lxs: *lxs, internal: ErrInvalidCodeFormat}
+ }
+ started := false
+ for lxs.Next() && lxs.Current().Value != current {
+ if lxs.Current().Type == lexerBreak {
+ if tree.codeType == codeOneLine {
+ return nil, &ParseError{lxs: *lxs, internal: ErrInvalidCodeFormat}
+ }
+ if !started {
+ started = true
+ }
+ }
+ if started || tree.codeType == codeOneLine {
+ tree.content += lxs.Current().Value
+ } else {
+ tree.before += lxs.Current().Value
+ }
+ }
+ return tree, nil
+}
diff --git a/markdown/ast_external.go b/markdown/ast_external.go
new file mode 100644
index 0000000..a172b2b
--- /dev/null
+++ b/markdown/ast_external.go
@@ -0,0 +1,179 @@
+package markdown
+
+import (
+ "fmt"
+ "html/template"
+)
+
+type astLink struct {
+ content block
+ href block
+}
+
+func (a *astLink) Eval() (template.HTML, *ParseError) {
+ content, err := a.content.Eval()
+ if err != nil {
+ return "", err
+ }
+ href, err := a.href.Eval()
+ if err != nil {
+ return "", err
+ }
+ return template.HTML(fmt.Sprintf(`<a href="%s">%s</a>`, href, content)), nil
+}
+
+type astImage struct {
+ alt block
+ src block
+ source []*astParagraph
+}
+
+func (a *astImage) Eval() (template.HTML, *ParseError) {
+ alt, err := a.alt.Eval()
+ if err != nil {
+ return "", err
+ }
+ src, err := a.src.Eval()
+ if err != nil {
+ return "", err
+ }
+ if a.source == nil {
+ return template.HTML(fmt.Sprintf(`<figure><img alt="%s" src="%s"></figure>`, alt, src)), nil
+ }
+ var s template.HTML
+ for _, c := range a.source {
+ ct, err := c.Eval()
+ if err != nil {
+ return "", err
+ }
+ s += ct + " "
+ }
+ s = s[:len(s)-1]
+ return template.HTML(fmt.Sprintf(`<figure><img alt="%s" src="%s"><figcaption>%s</figcaption></figure>`, alt, src, s)), nil
+}
+
+func external(lxs *lexers) (block, *ParseError) {
+ tp := lxs.Current().Value
+ if !lxs.Next() {
+ return astLiteral(tp), nil
+ }
+ lxs.Before() // because we call Next
+ var b block
+ var err *ParseError
+ switch tp {
+ case "![":
+ b, err = image(lxs)
+ case "[":
+ b, err = link(lxs)
+ default:
+ b = astLiteral(tp)
+ }
+ return b, err
+}
+
+func link(lxs *lexers) (block, *ParseError) {
+ lk := new(astLink)
+ start := lxs.current
+ content, href, _, ok := parseExternal(lxs, false)
+ if !ok {
+ return reset(lxs, start), nil
+ }
+ lk.content = astLiteral(content)
+ lk.href = astLiteral(href)
+ return lk, nil
+}
+
+func image(lxs *lexers) (block, *ParseError) {
+ img := new(astImage)
+ start := lxs.current
+ alt, src, source, ok := parseExternal(lxs, true)
+ if !ok {
+ return reset(lxs, start), nil
+ }
+ img.alt = astLiteral(alt)
+ img.src = astLiteral(src)
+ img.source = source
+ return img, nil
+}
+
+func parseExternal(lxs *lexers, withSource bool) (string, string, []*astParagraph, bool) {
+ next := false
+ var s string
+ var first string
+ var end string
+ var ps []*astParagraph
+ n := 0
+ fn := func() bool {
+ p, err := paragraph(lxs, true)
+ if err != nil {
+ return false
+ }
+ ps = append(ps, p)
+ n = 0
+ return true
+ }
+ for lxs.Next() && n < 2 {
+ switch lxs.Current().Type {
+ case lexerBreak:
+ if !withSource {
+ return "", "", nil, false
+ }
+ n += len(lxs.Current().Value)
+ if first != "" && end != "" {
+ if !lxs.Next() {
+ return first, end, ps, true
+ }
+ ok := fn()
+ if !ok {
+ return "", "", nil, false
+ }
+ lxs.Before() // because we must parse lexerBreak
+ }
+ case lexerExternal:
+ if first != "" && end != "" {
+ return "", "", nil, false
+ }
+ if n > 0 && (first == "" || end == "") {
+ return "", "", nil, false
+ }
+ n = 0
+ if !next {
+ if lxs.Current().Value != "](" || !lxs.Next() {
+ return "", "", nil, false
+ }
+ lxs.Before() // because we called Next
+ first = s
+ s = ""
+ next = true
+ } else {
+ if lxs.Current().Value != ")" {
+ return "", "", nil, false
+ }
+ if !withSource {
+ return first, s, nil, true
+ }
+ end = s
+ s = ""
+ if lxs.Next() && lxs.Current().Type != lexerBreak {
+ return "", "", nil, false
+ }
+ lxs.Before() // because we called Next
+ }
+ default:
+ if ps != nil {
+ return "", "", nil, false
+ }
+ n = 0
+ s += lxs.Current().Value
+ }
+ }
+ if !withSource {
+ return "", "", nil, false
+ }
+ return first, end, ps, true
+}
+
+func reset(lxs *lexers, start int) block {
+ lxs.current = start
+ return astLiteral(lxs.Current().Value)
+}
diff --git a/markdown/ast_external_test.go b/markdown/ast_external_test.go
new file mode 100644
index 0000000..6de512b
--- /dev/null
+++ b/markdown/ast_external_test.go
@@ -0,0 +1,49 @@
+package markdown
+
+import "testing"
+
+func TestExternal(t *testing.T) {
+ lxs := lex("[content](href)")
+ tree, err := ast(lxs)
+ if err != nil {
+ t.Fatal(err)
+ }
+ got, err := tree.Eval()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if string(got) != `<p><a href="href">content</a></p>` {
+ t.Errorf("invalid value, got %s", got)
+ }
+
+ lxs = lex("![image alt](image src)")
+ tree, err = ast(lxs)
+ if err != nil {
+ t.Fatal(err)
+ }
+ got, err = tree.Eval()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if string(got) != `<figure><img alt="image alt" src="image src"></figure>` {
+ t.Errorf("invalid value, got %s", got)
+ }
+
+ lxs = lex(`
+![image alt](image src)
+source 1
+source 2
+`)
+ tree, err = ast(lxs)
+ if err != nil {
+ t.Fatal(err)
+ }
+ got, err = tree.Eval()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if string(got) != `<figure><img alt="image alt" src="image src"><figcaption>source 1 source 2</figcaption></figure>` {
+ t.Errorf("invalid value, got %s", got)
+ }
+
+}
diff --git a/markdown/ast_header.go b/markdown/ast_header.go
new file mode 100644
index 0000000..d51b046
--- /dev/null
+++ b/markdown/ast_header.go
@@ -0,0 +1,39 @@
+package markdown
+
+import (
+ "errors"
+ "fmt"
+ "html/template"
+)
+
+var ErrInvalidHeader = errors.New("invalid header")
+
+type astHeader struct {
+ level uint
+ content *astParagraph
+}
+
+func (a *astHeader) Eval() (template.HTML, *ParseError) {
+ if a.level > 6 {
+ return "", &ParseError{lxs: lexers{}, internal: ErrInvalidCodeFormat}
+ }
+ var content template.HTML
+ content, err := a.content.Eval()
+ if err != nil {
+ return "", err
+ }
+ return template.HTML(fmt.Sprintf("<h%d>%s</h%d>", a.level, trimSpace(content), a.level)), nil
+}
+
+func header(lxs *lexers) (*astHeader, *ParseError) {
+ b := &astHeader{level: uint(len(lxs.Current().Value))}
+ if !lxs.Next() {
+ return nil, &ParseError{lxs: *lxs, internal: ErrInvalidHeader}
+ }
+ var err *ParseError
+ b.content, err = paragraph(lxs, true)
+ if err != nil {
+ return nil, err
+ }
+ return b, nil
+}
diff --git a/markdown/ast_list.go b/markdown/ast_list.go
new file mode 100644
index 0000000..9aa36e7
--- /dev/null
+++ b/markdown/ast_list.go
@@ -0,0 +1,79 @@
+package markdown
+
+import (
+ "fmt"
+ "html/template"
+ "regexp"
+)
+
+var regexOrdered = regexp.MustCompile(`\d+\.`)
+
+type listType string
+
+const (
+ listUnordered listType = "ul"
+ listOrdered listType = "ol"
+)
+
+type astList struct {
+ tag listType
+ content []*astParagraph
+}
+
+func (a *astList) Eval() (template.HTML, *ParseError) {
+ var content template.HTML
+ for _, c := range a.content {
+ ct, err := c.Eval()
+ if err != nil {
+ return "", err
+ }
+ content += template.HTML(fmt.Sprintf("<li>%s</li>", trimSpace(ct)))
+ }
+ return template.HTML(fmt.Sprintf("<%s>%s</%s>", a.tag, content, a.tag)), nil
+}
+
+func list(lxs *lexers) (block, *ParseError) {
+ tree := new(astList)
+ tree.tag = detectListType(lxs.Current().Value)
+ if len(tree.tag) == 0 {
+ return paragraph(lxs, false)
+ }
+ n := 0
+ for lxs.Next() && n < 2 {
+ switch lxs.Current().Type {
+ case lexerBreak:
+ n += len(lxs.Current().Value)
+ case lexerList:
+ n = 0
+ tp := detectListType(lxs.Current().Value)
+ if tp != tree.tag {
+ lxs.Before() // because we dit not use it
+ return tree, nil
+ }
+ default:
+ n = 0
+ c, err := paragraph(lxs, true)
+ if err != nil {
+ return nil, err
+ }
+ lxs.Before() // because we must parse the last char
+ tree.content = append(tree.content, c)
+ }
+ }
+ lxs.Before() // because we did not use it
+ return tree, nil
+}
+
+func detectListType(val string) listType {
+ first := []rune(val)[0]
+ if first == '-' || first == '*' {
+ if len(val) > 1 {
+ return ""
+ }
+ return listUnordered
+ }
+ if !regexOrdered.MatchString(val) {
+ return ""
+ }
+ return listOrdered
+}
diff --git a/markdown/ast_list_test.go b/markdown/ast_list_test.go
new file mode 100644
index 0000000..2819814
--- /dev/null
+++ b/markdown/ast_list_test.go
@@ -0,0 +1,44 @@
+package markdown
+
+import (
+ "strings"
+ "testing"
+)
+
+var rw = `
+- item A
+- item B
+* item C
+
+1. item 1
+2. item 2
+`
+
+var expected = `
+<ul>
+<li>item A</li>
+<li>item B</li>
+<li>item C</li>
+</ul>
+<ol>
+<li>item 1</li>
+<li>item 2</li>
+</ol>
+`
+
+func TestList(t *testing.T) {
+ lxs := lex(rw)
+ tree, err := ast(lxs)
+ if err != nil {
+ t.Fatal(err)
+ }
+ got, err := tree.Eval()
+ if err != nil {
+ t.Fatal(err)
+ }
+ exp := strings.ReplaceAll(expected, "\n", "")
+ if string(got) != exp {
+ t.Errorf("invalid value, got %s", got)
+ t.Logf("expected %s", exp)
+ }
+}
diff --git a/markdown/ast_modifier.go b/markdown/ast_modifier.go
new file mode 100644
index 0000000..205786a
--- /dev/null
+++ b/markdown/ast_modifier.go
@@ -0,0 +1,150 @@
+package markdown
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "html/template"
+)
+
+var (
+ ErrInvalidUsage = errors.Join(ErrInvalidParagraph, errors.New("invalid modifier usage"))
+ ErrInvalidTypeInModifier = errors.Join(ErrInvalidParagraph, errors.New("invalid type in modifier"))
+)
+
+type modifierTag string
+
+const (
+ boldTag modifierTag = "b"
+ emTag modifierTag = "em"
+)
+
+type astModifier struct {
+ symbols string
+ tag modifierTag
+ content []block
+ super bool
+}
+
+func (a *astModifier) Eval() (template.HTML, *ParseError) {
+ var content template.HTML
+ for _, c := range a.content {
+ ct, err := c.Eval()
+ if err != nil {
+ return "", &ParseError{lxs: lexers{}, internal: err}
+ }
+ content += ct
+ }
+ if a.super {
+ return content, nil
+ }
+ return template.HTML(fmt.Sprintf("<%s>%s</%s>", a.tag, content, a.tag)), nil
+}
+
+func (a *astModifier) String() string {
+ content := "["
+ for _, c := range a.content {
+ content += "\n\t"
+ if v, ok := c.(fmt.Stringer); ok {
+ content += v.String()
+ } else {
+ b, _ := json.MarshalIndent(a.content, "\t", " ")
+ content += string(b)
+ }
+ content += ",\n\t"
+ }
+ content += "]"
+ return fmt.Sprintf("modifier{sym: %s, tag: %s, super: %v, content: %s\n}", a.symbols, a.tag, a.super, content)
+}
+
+func modifier(lxs *lexers) (*astModifier, error) {
+ current := lxs.Current().Value
+ mod, err := modifierDetect(current)
+ if err != nil {
+ return nil, err
+ }
+ var s string
+ for lxs.Next() {
+ switch lxs.Current().Type {
+ case lexerLiteral, lexerHeader, lexerList:
+ s += lxs.Current().Value
+ case lexerModifier:
+ if mod.super && []rune(mod.symbols)[0] == []rune(lxs.Current().Value)[0] &&
+ len(mod.symbols) >= len(lxs.Current().Value) {
+ mod.symbols = mod.symbols[len(lxs.Current().Value):]
+ subMod, err := modifierDetect(lxs.Current().Value)
+ if err != nil {
+ return nil, err
+ }
+ if !subMod.super {
+ subMod.content = append(subMod.content, astLiteral(s))
+ mod, err = modifierDetect(mod.symbols) // this trick is so cool :D
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ subMod, _ = modifierDetect("**")
+ subEm, _ := modifierDetect("*")
+ subEm.content = append(subEm.content, astLiteral(s))
+ subMod.content = append(subMod.content, subEm)
+ }
+ s = ""
+ mod.content = append(mod.content, subMod)
+ if len(mod.symbols) == 0 {
+ return mod, nil
+ }
+ } else {
+ if lxs.Current().Value == mod.symbols {
+ mod.content = append(mod.content, astLiteral(s))
+ return mod, nil
+ } else if len(s) != 0 {
+ mod.content = append(mod.content, astLiteral(s))
+ s = ""
+ }
+ c, err := modifier(lxs)
+ if err != nil {
+ return nil, err
+ }
+ mod.content = append(mod.content, c)
+ }
+ case lexerBreak:
+ lxs.Before() // because we did not use it
+ if len(s) != 0 {
+ return nil, ErrInvalidUsage
+ }
+ return mod, nil
+ case lexerExternal:
+ if lxs.Current().Value == "!" {
+ s += lxs.Current().Value
+ } else {
+ ext, err := external(lxs)
+ if err != nil {
+ return nil, err
+ }
+ mod.content = append(mod.content, ext)
+ }
+ default:
+ return nil, ErrInvalidTypeInModifier
+ }
+ }
+ if len(s) != 0 {
+ return nil, ErrInvalidUsage
+ }
+ return mod, nil
+}
+
+func modifierDetect(val string) (*astModifier, error) {
+ mod := new(astModifier)
+ mod.symbols = val
+ switch len(val) {
+ case 1:
+ mod.tag = emTag
+ case 2:
+ mod.tag = boldTag
+ case 3:
+ mod.super = true
+ default:
+ return nil, ErrInvalidUsage
+ }
+ return mod, nil
+}
diff --git a/markdown/ast_modifier_test.go b/markdown/ast_modifier_test.go
new file mode 100644
index 0000000..3cdb955
--- /dev/null
+++ b/markdown/ast_modifier_test.go
@@ -0,0 +1,22 @@
+package markdown
+
+import "testing"
+
+func TestModifier(t *testing.T) {
+ content := `
+**bo*n*soir**, ça ***va* bien** ?
+`
+ lxs := lex(content)
+ tree, err := ast(lxs)
+ if err != nil {
+ t.Fatal(err)
+ }
+ c, err := tree.Eval()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if c != "<p><b>bo<em>n</em>soir</b>, ça <b><em>va</em> bien</b> ?</p>" {
+ t.Errorf("failed, got %s", c)
+ t.Logf("lxs: %s\ntree: %s", lxs, tree)
+ }
+}
diff --git a/markdown/ast_paragraph.go b/markdown/ast_paragraph.go
new file mode 100644
index 0000000..8b34e9f
--- /dev/null
+++ b/markdown/ast_paragraph.go
@@ -0,0 +1,101 @@
+package markdown
+
+import (
+ "errors"
+ "fmt"
+ "html/template"
+)
+
+var (
+ ErrInvalidParagraph = errors.New("invalid paragraph")
+)
+
+type astParagraph struct {
+ content []block
+ oneLine bool
+}
+
+func (a *astParagraph) Eval() (template.HTML, *ParseError) {
+ var content template.HTML
+ for _, c := range a.content {
+ ct, err := c.Eval()
+ if err != nil {
+ return "", err
+ }
+ content += ct
+ }
+ if a.oneLine {
+ return content, nil
+ }
+ return template.HTML(fmt.Sprintf("<p>%s</p>", trimSpace(content))), nil
+}
+
+func paragraph(lxs *lexers, oneLine bool) (*astParagraph, *ParseError) {
+ tree := new(astParagraph)
+ tree.oneLine = oneLine
+ maxBreak := 2
+ if oneLine {
+ maxBreak = 1
+ }
+ n := 0
+ lxs.current-- // because we do not use it before the next
+ for lxs.Next() && n < maxBreak {
+ switch lxs.Current().Type {
+ case lexerBreak:
+ n += len(lxs.Current().Value)
+ case lexerQuote, lexerList:
+ if n > 0 {
+ lxs.Before() // because we did not use it
+ return tree, nil
+ }
+ tree.content = append(tree.content, astLiteral(lxs.Current().Value))
+ case lexerLiteral, lexerHeader:
+ s := lxs.Current().Value
+ // replace line break by space
+ if n > 0 {
+ s = " " + s
+ }
+ n = 0
+ tree.content = append(tree.content, astLiteral(s))
+ case lexerModifier:
+ n = 0
+ mod, err := modifier(lxs)
+ if err != nil {
+ return nil, &ParseError{lxs: *lxs, internal: err}
+ }
+ tree.content = append(tree.content, mod)
+ case lexerExternal:
+ n = 0
+ if lxs.Current().Value == "!" {
+ tree.content = append(tree.content, astLiteral(lxs.Current().Value))
+ } else {
+ ext, err := external(lxs)
+ if err != nil {
+ return nil, err
+ }
+ tree.content = append(tree.content, ext)
+ }
+ case lexerCode:
+ if len(lxs.Current().Value) > 1 {
+ return nil, &ParseError{lxs: *lxs, internal: ErrInvalidCodeBlockPosition}
+ }
+ n = 0
+ b, err := code(lxs)
+ if err != nil {
+ return nil, err
+ }
+ tree.content = append(tree.content, b)
+ return tree, nil
+ }
+ }
+ if !lxs.Finished() {
+ lxs.Before() // because we never handle the last item
+ }
+ return tree, nil
+}
+
+type astLiteral string
+
+func (a astLiteral) Eval() (template.HTML, *ParseError) {
+ return template.HTML(template.HTMLEscapeString(string(a))), nil
+}
diff --git a/markdown/ast_paragraph_test.go b/markdown/ast_paragraph_test.go
new file mode 100644
index 0000000..4479cbc
--- /dev/null
+++ b/markdown/ast_paragraph_test.go
@@ -0,0 +1,20 @@
+package markdown
+
+import "testing"
+
+func TestParagraph(t *testing.T) {
+ content := "bonsoir"
+ lxs := lex(content)
+ tree, err := ast(lxs)
+ if err != nil {
+ t.Fatal(err)
+ }
+ c, err := tree.Eval()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if c != "<p>bonsoir</p>" {
+ t.Errorf("failed, got %s", c)
+ t.Logf("lxs: %s\ntree: %s", lxs, tree)
+ }
+}
diff --git a/markdown/ast_quote.go b/markdown/ast_quote.go
new file mode 100644
index 0000000..3dfede1
--- /dev/null
+++ b/markdown/ast_quote.go
@@ -0,0 +1,81 @@
+package markdown
+
+import (
+ "fmt"
+ "html/template"
+ "strings"
+)
+
+type astQuote struct {
+ quote []*astParagraph
+ source []*astParagraph
+}
+
+func (a *astQuote) Eval() (template.HTML, *ParseError) {
+ var quote template.HTML
+ for _, c := range a.quote {
+ ct, err := c.Eval()
+ if err != nil {
+ return "", err
+ }
+ quote += ct
+ }
+ quote = template.HTML(fmt.Sprintf("<blockquote>%s</blockquote>", trimSpace(quote)))
+ var source template.HTML
+ for _, c := range a.source {
+ ct, err := c.Eval()
+ if err != nil {
+ return "", err
+ }
+ source += ct
+ }
+ source = template.HTML(strings.TrimSpace(string(source)))
+ if len(source) > 0 {
+ return template.HTML(fmt.Sprintf(`<div class="quote">%s<p>%s</p></div>`, quote, source)), nil
+ }
+ return template.HTML(fmt.Sprintf(`<div class="quote">%s</div>`, quote)), nil
+}
+
+func quote(lxs *lexers) (*astQuote, *ParseError) {
+ tree := new(astQuote)
+ n := 0
+ quoteContinue := true
+ source := false
+ for lxs.Next() && n < 2 {
+ switch lxs.Current().Type {
+ case lexerBreak:
+ n += len(lxs.Current().Value)
+ quoteContinue = false
+ case lexerQuote:
+ n = 0
+ if source {
+ // because the code did not use it
+ lxs.Before()
+ return tree, nil
+ }
+ quoteContinue = true
+ case lexerLiteral, lexerModifier, lexerCode:
+ n = 0
+ if !quoteContinue {
+ source = true
+ }
+ p, err := paragraph(lxs, true)
+ if err != nil {
+ return nil, err
+ }
+
+ if !source {
+ tree.quote = append(tree.quote, p)
+ } else {
+ tree.source = append(tree.source, p)
+ }
+ n++
+ quoteContinue = false
+ default:
+ // because the code did not use it
+ lxs.Before()
+ return tree, nil
+ }
+ }
+ return tree, nil
+}
diff --git a/markdown/ast_quote_test.go b/markdown/ast_quote_test.go
new file mode 100644
index 0000000..e9a3202
--- /dev/null
+++ b/markdown/ast_quote_test.go
@@ -0,0 +1,23 @@
+package markdown
+
+import "testing"
+
+func TestQuote(t *testing.T) {
+ content := `
+> Bonsoir, je suis un **code**
+avec une source
+`
+ lxs := lex(content)
+ tree, err := ast(lxs)
+ if err != nil {
+ t.Fatal(err)
+ }
+ c, err := tree.Eval()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if c != `<div class="quote"><blockquote>Bonsoir, je suis un <b>code</b></blockquote><p>avec une source</p></div>` {
+ t.Errorf("failed, got %s", c)
+ t.Logf("lxs: %s\ntree: %s", lxs, tree)
+ }
+}
diff --git a/markdown/ast_test.go b/markdown/ast_test.go
new file mode 100644
index 0000000..acd6aa7
--- /dev/null
+++ b/markdown/ast_test.go
@@ -0,0 +1,62 @@
+package markdown
+
+import (
+ "strings"
+ "testing"
+)
+
+var raw = `
+# Je suis un titre
+Avec une description classique,
+sur plusieurs lignes !
+
+Et je peux mettre du texte en **gras**,
+en *italique* et les **_deux en même temps_** !
+
+> Je suis une magnifique citation
+> sur plusieurs lignes
+avec une source
+> qui recommence après !
+
+- Ceci est une liste
+- pas ordonnée
+1. et maintenant
+2. elle l'est
+- hehe
+
+![Ceci est ma pfp :3](https://cdn.anhgelus.world/pfp.jpg)
+[Ma pfp](https://now.anhgelus.world/) hehe :D
+Elle est **magnifique**, n'est-ce pas ?
+`
+
+var parsed = `
+<h1>Je suis un titre</h1>
+<p>Avec une description classique, sur plusieurs lignes !</p>
+<p>Et je peux mettre du texte en <b>gras</b>, en <em>italique</em> et les <b><em>deux en même temps</em></b> !</p>
+<div class="quote"><blockquote>Je suis une magnifique citation sur plusieurs lignes</blockquote><p>avec une source</p></div>
+<div class="quote"><blockquote>qui recommence après !</blockquote></div>
+<ul><li>Ceci est une liste</li><li>pas ordonnée</li></ul>
+<ol><li>et maintenant</li><li>elle l&#39;est</li></ol>
+<ul><li>hehe</li></ul>
+<figure>
+<img alt="Ceci est ma pfp :3" src="https://cdn.anhgelus.world/pfp.jpg">
+<figcaption><a href="https://now.anhgelus.world/">Ma pfp</a> hehe :D Elle est <b>magnifique</b>, n&#39;est-ce pas ?</figcaption>
+</figure>
+`
+
+func TestAst(t *testing.T) {
+ lxs := lex(raw)
+ tree, err := ast(lxs)
+ if err != nil {
+ t.Fatal(err)
+ }
+ res, err := tree.Eval()
+ if err != nil {
+ t.Fatal(err)
+ }
+ wanted := strings.ReplaceAll(parsed, "\n", "")
+ if string(res) != wanted {
+ t.Errorf("invalid string, got\n%s", res)
+ t.Logf("wanted\n%s", wanted)
+ }
+}
diff --git a/markdown/error.go b/markdown/error.go
new file mode 100644
index 0000000..6f88968
--- /dev/null
+++ b/markdown/error.go
@@ -0,0 +1,43 @@
+package markdown
+
+import "fmt"
+
+type ParseError struct {
+ internal error
+ lxs lexers
+}
+
+func (e *ParseError) Error() string {
+ return e.internal.Error()
+}
+
+func (e *ParseError) Pretty() string {
+ lxs := e.lxs
+ if lxs.lexers == nil {
+ return e.internal.Error()
+ }
+ current := lxs.current - 1
+ for lxs.Before() && lxs.Current().Type != lexerBreak {
+ }
+ current -= lxs.current
+ contxt := ""
+ ind := ""
+ for lxs.Next() && lxs.Current().Type != lexerBreak {
+ contxt += lxs.Current().Value
+ if lxs.current <= current {
+ ch := "~"
+ if lxs.current == current {
+ ch = "^"
+ }
+ for range len(lxs.Current().Value) {
+ ind += ch
+ }
+ }
+ }
+ if lxs.current == current {
+ runes := []rune(ind)
+ runes[len(runes)-1] = '^'
+ ind = string(runes)
+ }
+ return fmt.Sprintf("%v\n\n%s\n%s", e, contxt, ind)
+}
diff --git a/markdown/error_test.go b/markdown/error_test.go
new file mode 100644
index 0000000..6f1ba01
--- /dev/null
+++ b/markdown/error_test.go
@@ -0,0 +1,26 @@
+package markdown
+
+import "testing"
+
+func TestError(t *testing.T) {
+ v, err := Parse("**bonsoir")
+ if err == nil {
+ t.Errorf("expected error, got %s", v)
+ } else {
+ t.Log(err.Pretty())
+ }
+
+ v, err = Parse("bo*nso**ir")
+ if err == nil {
+ t.Errorf("expected error, got %s", v)
+ } else {
+ t.Log(err.Pretty())
+ }
+
+ v, err = Parse("test ``` hehe")
+ if err == nil {
+ t.Errorf("expected error, got %s", v)
+ } else {
+ t.Log(err.Pretty())
+ }
+}
diff --git a/markdown/eval.go b/markdown/eval.go
new file mode 100644
index 0000000..db9d150
--- /dev/null
+++ b/markdown/eval.go
@@ -0,0 +1,16 @@
+package markdown
+
+import "html/template"
+
+func Parse(s string) (template.HTML, *ParseError) {
+ lxs := lex(s)
+ tree, err := ast(lxs)
+ if err != nil {
+ return "", err
+ }
+ return tree.Eval()
+}
+
+func ParseBytes(b []byte) (template.HTML, *ParseError) {
+ return Parse(string(b))
+}
diff --git a/markdown/lexer.go b/markdown/lexer.go
new file mode 100644
index 0000000..a02ec54
--- /dev/null
+++ b/markdown/lexer.go
@@ -0,0 +1,132 @@
+package markdown
+
+import "fmt"
+
+type lexerType string
+
+const (
+ lexerBreak lexerType = "break"
+
+ lexerModifier lexerType = "modifier"
+
+ lexerCode lexerType = "code"
+
+ lexerHeader lexerType = "header"
+ lexerQuote lexerType = "quote"
+ lexerList lexerType = "list"
+
+ lexerExternal lexerType = "external"
+
+ lexerLiteral lexerType = "literal"
+)
+
+type lexer struct {
+ Type lexerType
+ Value string
+}
+
+func (l *lexer) String() string {
+ return fmt.Sprintf("%s(%s)", l.Type, l.Value)
+}
+
+type lexers struct {
+ current int
+ lexers []lexer
+}
+
+func (l *lexers) Next() bool {
+ l.current++
+ return !l.Finished()
+}
+
+func (l *lexers) Current() lexer {
+ return l.lexers[l.current]
+}
+
+func (l *lexers) Finished() bool {
+ return l.current >= len(l.lexers)
+}
+
+func (l *lexers) Before() bool {
+ l.current--
+ return l.current >= 0 && !l.Finished()
+}
+
+func (l *lexers) String() string {
+ s := "Lexers["
+ for _, l := range l.lexers {
+ s += l.String() + " "
+ }
+ return s + "]"
+}
+
+func lex(s string) *lexers {
+ lxs := &lexers{current: -1}
+ var lexs []lexer
+ var currentType lexerType
+ var previous string
+ fn := func(c rune, t lexerType) {
+ if currentType != t && len(previous) > 0 {
+ lexs = append(lexs, lexer{Type: currentType, Value: previous})
+ previous = ""
+ }
+ currentType = t
+ previous += string(c)
+ }
+ newLine := true
+ literalNext := false
+ runes := []rune(s)
+ for i, c := range runes {
+ if literalNext {
+ fn(c, lexerLiteral)
+ literalNext = false
+ continue
+ }
+ if c == '\\' {
+ literalNext = true
+ continue
+ }
+ switch c {
+ case '*', '_':
+ if c == '*' && newLine && i < len(runes)-1 && runes[i+1] == ' ' {
+ fn(c, lexerList)
+ } else {
+ if (currentType != lexerModifier && len(previous) > 0) ||
+ (len(previous) > 0 && []rune(previous)[0] != c) ||
+ len(previous) >= 3 {
+ lexs = append(lexs, lexer{Type: currentType, Value: previous})
+ previous = ""
+ }
+ currentType = lexerModifier
+ previous += string(c)
+ }
+ newLine = false
+ case '`':
+ newLine = false
+ fn(c, lexerCode)
+ case '\n':
+ newLine = true
+ fn(c, lexerBreak)
+ case '#':
+ newLine = false
+ fn(c, lexerHeader)
+ case '>':
+ newLine = false
+ fn(c, lexerQuote)
+ case '[', ']', '(', ')', '!':
+ newLine = false
+ fn(c, lexerExternal)
+ case '-', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.':
+ newLine = false
+ fn(c, lexerList)
+ default:
+ newLine = false
+ fn(c, lexerLiteral)
+ }
+ }
+ if len(previous) > 0 {
+ lexs = append(lexs, lexer{Type: currentType, Value: previous})
+ }
+ lxs.lexers = lexs
+ return lxs
+}
diff --git a/markdown/lexer_test.go b/markdown/lexer_test.go
new file mode 100644
index 0000000..c670753
--- /dev/null
+++ b/markdown/lexer_test.go
@@ -0,0 +1,30 @@
+package markdown
+
+import "testing"
+
+func TestLex(t *testing.T) {
+ lxs := lex("bonjour les gens")
+ if lxs.String() != "Lexers[literal(bonjour les gens) ]" {
+ t.Errorf("invalid lex, got %s", lxs)
+ }
+ lxs = lex("# bonjour les gens")
+ if lxs.String() != "Lexers[header(#) literal( bonjour les gens) ]" {
+ t.Errorf("invalid lex, got %s", lxs)
+ }
+ lxs = lex("# bonjour les gens\nComment ça va ?")
+ if lxs.String() != "Lexers[header(#) literal( bonjour les gens) break(\n) literal(Comment ça va ?) ]" {
+ t.Errorf("invalid lex, got %s", lxs)
+ }
+ lxs = lex("***hey***, what's up?")
+ if lxs.String() != "Lexers[modifier(***) literal(hey) modifier(***) literal(, what's up?) ]" {
+ t.Errorf("invalid lex, got %s", lxs)
+ }
+ lxs = lex(`Xxx\_DarkEmperor\_xxX`)
+ if lxs.String() != `Lexers[literal(Xxx_DarkEmperor_xxX) ]` {
+ t.Errorf("invalid lex, got %s", lxs)
+ }
+ lxs = lex(`* list`)
+ if lxs.String() != `Lexers[list(*) literal( list) ]` {
+ t.Errorf("invalid lex, got %s", lxs)
+ }
+}