diff options
| author | Anhgelus Morhtuuzh <william@herges.fr> | 2025-10-02 19:52:38 +0200 |
|---|---|---|
| committer | Anhgelus Morhtuuzh <william@herges.fr> | 2025-10-02 19:52:38 +0200 |
| commit | 94dceb4f7c1740de9215b36ec183f93ca4337ee7 (patch) | |
| tree | 5ad184efb0b74dd3aa4da7585f88a3e3f6cb4ecc /markdown | |
| parent | 8b249c9ce8bd1c351daf80c6c9b095fb1bccafe2 (diff) | |
style(markdown): fix typo in package name
Diffstat (limited to 'markdown')
| -rw-r--r-- | markdown/ast.go | 107 | ||||
| -rw-r--r-- | markdown/ast_code.go | 66 | ||||
| -rw-r--r-- | markdown/ast_external.go | 179 | ||||
| -rw-r--r-- | markdown/ast_external_test.go | 49 | ||||
| -rw-r--r-- | markdown/ast_header.go | 39 | ||||
| -rw-r--r-- | markdown/ast_list.go | 79 | ||||
| -rw-r--r-- | markdown/ast_list_test.go | 44 | ||||
| -rw-r--r-- | markdown/ast_modifier.go | 150 | ||||
| -rw-r--r-- | markdown/ast_modifier_test.go | 22 | ||||
| -rw-r--r-- | markdown/ast_paragraph.go | 101 | ||||
| -rw-r--r-- | markdown/ast_paragraph_test.go | 20 | ||||
| -rw-r--r-- | markdown/ast_quote.go | 81 | ||||
| -rw-r--r-- | markdown/ast_quote_test.go | 23 | ||||
| -rw-r--r-- | markdown/ast_test.go | 62 | ||||
| -rw-r--r-- | markdown/error.go | 43 | ||||
| -rw-r--r-- | markdown/error_test.go | 26 | ||||
| -rw-r--r-- | markdown/eval.go | 16 | ||||
| -rw-r--r-- | markdown/lexer.go | 132 | ||||
| -rw-r--r-- | markdown/lexer_test.go | 30 |
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("") + 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(` + +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 + + +[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'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'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) + } +} |
