From ce480a18b865acc21eb6c36dea8fc27b08061603 Mon Sep 17 00:00:00 2001 From: Anhgelus Morhtuuzh Date: Wed, 1 Oct 2025 22:32:41 +0200 Subject: feat(markdown): support list --- mardown/ast.go | 6 ++++ mardown/ast_list.go | 77 ++++++++++++++++++++++++++++++++++++++++++++++++ mardown/ast_list_test.go | 42 ++++++++++++++++++++++++++ mardown/ast_modifier.go | 2 +- mardown/ast_paragraph.go | 4 +-- mardown/ast_test.go | 9 ++++++ mardown/lexer.go | 14 ++++++--- 7 files changed, 147 insertions(+), 7 deletions(-) create mode 100644 mardown/ast_list.go create mode 100644 mardown/ast_list_test.go diff --git a/mardown/ast.go b/mardown/ast.go index 55fd3b9..37d670d 100644 --- a/mardown/ast.go +++ b/mardown/ast.go @@ -75,6 +75,12 @@ func getBlock(lxs *lexers, newLine bool) (block, error) { } 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, ErrInvalidCodeBlockPosition diff --git a/mardown/ast_list.go b/mardown/ast_list.go new file mode 100644 index 0000000..39be70c --- /dev/null +++ b/mardown/ast_list.go @@ -0,0 +1,77 @@ +package mardown + +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, error) { + var content template.HTML + for _, c := range a.content { + ct, err := c.Eval() + if err != nil { + return "", err + } + content += template.HTML(fmt.Sprintf("
  • %s
  • ", trimSpace(ct))) + } + return template.HTML(fmt.Sprintf("<%s>%s", a.tag, content, a.tag)), nil +} + +func list(lxs *lexers) (block, error) { + tree := new(astList) + current := lxs.Current().Value + tree.tag = detectListType(current) + if len(tree.tag) == 0 { + return paragraph(lxs, false) + } + n := 0 + for lxs.Next() && n < 2 { + switch lxs.Current().Type { + case lexerBreak: + n++ + 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 + } + tree.content = append(tree.content, c) + } + } + return tree, nil +} + +func detectListType(val string) listType { + if []rune(val)[0] == '-' { + if len(val) > 1 { + return "" + } + return listUnordered + } + if !regexOrdered.MatchString(val) { + return "" + } + return listOrdered +} diff --git a/mardown/ast_list_test.go b/mardown/ast_list_test.go new file mode 100644 index 0000000..895e439 --- /dev/null +++ b/mardown/ast_list_test.go @@ -0,0 +1,42 @@ +package mardown + +import ( + "strings" + "testing" +) + +var rw = ` +- item A +- item B + +1. item 1 +2. item 2 +` + +var expected = ` + +
      +
    1. item 1
    2. +
    3. item 2
    4. +
    +` + +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/mardown/ast_modifier.go b/mardown/ast_modifier.go index 944cb4a..0107605 100644 --- a/mardown/ast_modifier.go +++ b/mardown/ast_modifier.go @@ -63,7 +63,7 @@ func modifier(lxs *lexers) (*astModifier, error) { var s string for lxs.Next() { switch lxs.Current().Type { - case lexerLiteral, lexerHeader: + case lexerLiteral, lexerHeader, lexerList: s += lxs.Current().Value case lexerModifier: if lxs.Current().Value == mod.symbols { diff --git a/mardown/ast_paragraph.go b/mardown/ast_paragraph.go index 3e8d027..2e8895d 100644 --- a/mardown/ast_paragraph.go +++ b/mardown/ast_paragraph.go @@ -43,9 +43,9 @@ func paragraph(lxs *lexers, oneLine bool) (*astParagraph, error) { switch lxs.Current().Type { case lexerBreak: n = len(lxs.Current().Value) - case lexerQuote: + case lexerQuote, lexerList: if n > 0 { - lxs.Before() + lxs.Before() // because we did not use it return tree, nil } tree.content = append(tree.content, astLiteral(lxs.Current().Value)) diff --git a/mardown/ast_test.go b/mardown/ast_test.go index 811d222..573349e 100644 --- a/mardown/ast_test.go +++ b/mardown/ast_test.go @@ -17,6 +17,12 @@ en *italique* et les **_deux en même temps_** ! > 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 ` var parsed = ` @@ -25,6 +31,9 @@ var parsed = `

    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 !
    + +
    1. et maintenant
    2. elle l'est
    + ` func TestAst(t *testing.T) { diff --git a/mardown/lexer.go b/mardown/lexer.go index 8450032..c056460 100644 --- a/mardown/lexer.go +++ b/mardown/lexer.go @@ -5,13 +5,17 @@ import "fmt" type lexerType string const ( - lexerBreak lexerType = "break" + lexerBreak lexerType = "break" + lexerEscape lexerType = "escape" lexerModifier lexerType = "modifier" - lexerCode lexerType = "code" - lexerHeader lexerType = "header" - lexerQuote lexerType = "quote" + + lexerCode lexerType = "code" + + lexerHeader lexerType = "header" + lexerQuote lexerType = "quote" + lexerList lexerType = "list" lexerExternal lexerType = "external" @@ -94,6 +98,8 @@ func lex(s string) *lexers { fn(c, lexerExternal) case '\\': fn(c, lexerEscape) + case '-', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.': + fn(c, lexerList) default: fn(c, lexerLiteral) } -- cgit v1.2.3