From 94dceb4f7c1740de9215b36ec183f93ca4337ee7 Mon Sep 17 00:00:00 2001 From: Anhgelus Morhtuuzh Date: Thu, 2 Oct 2025 19:52:38 +0200 Subject: style(markdown): fix typo in package name --- mardown/ast.go | 107 ------------------------ mardown/ast_code.go | 66 --------------- mardown/ast_external.go | 179 ----------------------------------------- mardown/ast_external_test.go | 49 ----------- mardown/ast_header.go | 39 --------- mardown/ast_list.go | 79 ------------------ mardown/ast_list_test.go | 44 ---------- mardown/ast_modifier.go | 150 ---------------------------------- mardown/ast_modifier_test.go | 22 ----- mardown/ast_paragraph.go | 101 ----------------------- mardown/ast_paragraph_test.go | 20 ----- mardown/ast_quote.go | 81 ------------------- mardown/ast_quote_test.go | 23 ------ mardown/ast_test.go | 62 -------------- mardown/error.go | 43 ---------- mardown/error_test.go | 26 ------ mardown/eval.go | 16 ---- mardown/lexer.go | 132 ------------------------------ mardown/lexer_test.go | 30 ------- markdown/ast.go | 107 ++++++++++++++++++++++++ markdown/ast_code.go | 66 +++++++++++++++ markdown/ast_external.go | 179 +++++++++++++++++++++++++++++++++++++++++ markdown/ast_external_test.go | 49 +++++++++++ markdown/ast_header.go | 39 +++++++++ markdown/ast_list.go | 79 ++++++++++++++++++ markdown/ast_list_test.go | 44 ++++++++++ markdown/ast_modifier.go | 150 ++++++++++++++++++++++++++++++++++ markdown/ast_modifier_test.go | 22 +++++ markdown/ast_paragraph.go | 101 +++++++++++++++++++++++ markdown/ast_paragraph_test.go | 20 +++++ markdown/ast_quote.go | 81 +++++++++++++++++++ markdown/ast_quote_test.go | 23 ++++++ markdown/ast_test.go | 62 ++++++++++++++ markdown/error.go | 43 ++++++++++ markdown/error_test.go | 26 ++++++ markdown/eval.go | 16 ++++ markdown/lexer.go | 132 ++++++++++++++++++++++++++++++ markdown/lexer_test.go | 30 +++++++ 38 files changed, 1269 insertions(+), 1269 deletions(-) delete mode 100644 mardown/ast.go delete mode 100644 mardown/ast_code.go delete mode 100644 mardown/ast_external.go delete mode 100644 mardown/ast_external_test.go delete mode 100644 mardown/ast_header.go delete mode 100644 mardown/ast_list.go delete mode 100644 mardown/ast_list_test.go delete mode 100644 mardown/ast_modifier.go delete mode 100644 mardown/ast_modifier_test.go delete mode 100644 mardown/ast_paragraph.go delete mode 100644 mardown/ast_paragraph_test.go delete mode 100644 mardown/ast_quote.go delete mode 100644 mardown/ast_quote_test.go delete mode 100644 mardown/ast_test.go delete mode 100644 mardown/error.go delete mode 100644 mardown/error_test.go delete mode 100644 mardown/eval.go delete mode 100644 mardown/lexer.go delete mode 100644 mardown/lexer_test.go create mode 100644 markdown/ast.go create mode 100644 markdown/ast_code.go create mode 100644 markdown/ast_external.go create mode 100644 markdown/ast_external_test.go create mode 100644 markdown/ast_header.go create mode 100644 markdown/ast_list.go create mode 100644 markdown/ast_list_test.go create mode 100644 markdown/ast_modifier.go create mode 100644 markdown/ast_modifier_test.go create mode 100644 markdown/ast_paragraph.go create mode 100644 markdown/ast_paragraph_test.go create mode 100644 markdown/ast_quote.go create mode 100644 markdown/ast_quote_test.go create mode 100644 markdown/ast_test.go create mode 100644 markdown/error.go create mode 100644 markdown/error_test.go create mode 100644 markdown/eval.go create mode 100644 markdown/lexer.go create mode 100644 markdown/lexer_test.go diff --git a/mardown/ast.go b/mardown/ast.go deleted file mode 100644 index ceeec6f..0000000 --- a/mardown/ast.go +++ /dev/null @@ -1,107 +0,0 @@ -package mardown - -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/mardown/ast_code.go b/mardown/ast_code.go deleted file mode 100644 index f029df2..0000000 --- a/mardown/ast_code.go +++ /dev/null @@ -1,66 +0,0 @@ -package mardown - -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("%s", template.HTMLEscapeString(a.content))), nil - case codeMultiLine: - return template.HTML(fmt.Sprintf("
%s
", 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/mardown/ast_external.go b/mardown/ast_external.go deleted file mode 100644 index 98a090e..0000000 --- a/mardown/ast_external.go +++ /dev/null @@ -1,179 +0,0 @@ -package mardown - -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(`%s`, 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(`
%s
`, 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(`
%s
%s
`, 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/mardown/ast_external_test.go b/mardown/ast_external_test.go deleted file mode 100644 index afae085..0000000 --- a/mardown/ast_external_test.go +++ /dev/null @@ -1,49 +0,0 @@ -package mardown - -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) != `

content

` { - 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) != `
image alt
` { - 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) != `
image alt
source 1 source 2
` { - t.Errorf("invalid value, got %s", got) - } - -} diff --git a/mardown/ast_header.go b/mardown/ast_header.go deleted file mode 100644 index 0ce8a22..0000000 --- a/mardown/ast_header.go +++ /dev/null @@ -1,39 +0,0 @@ -package mardown - -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("%s", 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/mardown/ast_list.go b/mardown/ast_list.go deleted file mode 100644 index 39f0178..0000000 --- a/mardown/ast_list.go +++ /dev/null @@ -1,79 +0,0 @@ -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, *ParseError) { - 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, *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/mardown/ast_list_test.go b/mardown/ast_list_test.go deleted file mode 100644 index 6736223..0000000 --- a/mardown/ast_list_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package mardown - -import ( - "strings" - "testing" -) - -var rw = ` -- item A -- item B -* item C - -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 deleted file mode 100644 index 36c6193..0000000 --- a/mardown/ast_modifier.go +++ /dev/null @@ -1,150 +0,0 @@ -package mardown - -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", 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/mardown/ast_modifier_test.go b/mardown/ast_modifier_test.go deleted file mode 100644 index ce0988f..0000000 --- a/mardown/ast_modifier_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package mardown - -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 != "

    bonsoir, ça va bien ?

    " { - t.Errorf("failed, got %s", c) - t.Logf("lxs: %s\ntree: %s", lxs, tree) - } -} diff --git a/mardown/ast_paragraph.go b/mardown/ast_paragraph.go deleted file mode 100644 index 21dc1ef..0000000 --- a/mardown/ast_paragraph.go +++ /dev/null @@ -1,101 +0,0 @@ -package mardown - -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("

    %s

    ", 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/mardown/ast_paragraph_test.go b/mardown/ast_paragraph_test.go deleted file mode 100644 index 8daba99..0000000 --- a/mardown/ast_paragraph_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package mardown - -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 != "

    bonsoir

    " { - t.Errorf("failed, got %s", c) - t.Logf("lxs: %s\ntree: %s", lxs, tree) - } -} diff --git a/mardown/ast_quote.go b/mardown/ast_quote.go deleted file mode 100644 index ffc8e83..0000000 --- a/mardown/ast_quote.go +++ /dev/null @@ -1,81 +0,0 @@ -package mardown - -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("
    %s
    ", 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(`
    %s

    %s

    `, quote, source)), nil - } - return template.HTML(fmt.Sprintf(`
    %s
    `, 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/mardown/ast_quote_test.go b/mardown/ast_quote_test.go deleted file mode 100644 index c3c7b3b..0000000 --- a/mardown/ast_quote_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package mardown - -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 != `
    Bonsoir, je suis un code

    avec une source

    ` { - t.Errorf("failed, got %s", c) - t.Logf("lxs: %s\ntree: %s", lxs, tree) - } -} diff --git a/mardown/ast_test.go b/mardown/ast_test.go deleted file mode 100644 index 2b47855..0000000 --- a/mardown/ast_test.go +++ /dev/null @@ -1,62 +0,0 @@ -package mardown - -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 = ` -

    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 !
    - -
    1. et maintenant
    2. elle l'est
    - -
    -Ceci est ma pfp :3 -
    Ma pfp hehe :D Elle est magnifique, n'est-ce pas ?
    -
    -` - -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/mardown/error.go b/mardown/error.go deleted file mode 100644 index ad279fe..0000000 --- a/mardown/error.go +++ /dev/null @@ -1,43 +0,0 @@ -package mardown - -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/mardown/error_test.go b/mardown/error_test.go deleted file mode 100644 index bcf6143..0000000 --- a/mardown/error_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package mardown - -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/mardown/eval.go b/mardown/eval.go deleted file mode 100644 index c085767..0000000 --- a/mardown/eval.go +++ /dev/null @@ -1,16 +0,0 @@ -package mardown - -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/mardown/lexer.go b/mardown/lexer.go deleted file mode 100644 index c4ff988..0000000 --- a/mardown/lexer.go +++ /dev/null @@ -1,132 +0,0 @@ -package mardown - -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/mardown/lexer_test.go b/mardown/lexer_test.go deleted file mode 100644 index 3ef87cc..0000000 --- a/mardown/lexer_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package mardown - -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) - } -} 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("%s", template.HTMLEscapeString(a.content))), nil + case codeMultiLine: + return template.HTML(fmt.Sprintf("
    %s
    ", 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(`%s`, 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(`
    %s
    `, 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(`
    %s
    %s
    `, 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) != `

    content

    ` { + 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) != `
    image alt
    ` { + 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) != `
    image alt
    source 1 source 2
    ` { + 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("%s", 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("
  • %s
  • ", trimSpace(ct))) + } + return template.HTML(fmt.Sprintf("<%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 = ` + +
      +
    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/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", 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 != "

    bonsoir, ça va bien ?

    " { + 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("

    %s

    ", 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 != "

    bonsoir

    " { + 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("
    %s
    ", 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(`
    %s

    %s

    `, quote, source)), nil + } + return template.HTML(fmt.Sprintf(`
    %s
    `, 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 != `
    Bonsoir, je suis un code

    avec une source

    ` { + 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 = ` +

    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 !
    + +
    1. et maintenant
    2. elle l'est
    + +
    +Ceci est ma pfp :3 +
    Ma pfp hehe :D Elle est magnifique, n'est-ce pas ?
    +
    +` + +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) + } +} -- cgit v1.2.3