chore: implement html render

This commit is contained in:
Steven 2023-12-14 22:21:23 +08:00
parent 3edce174d6
commit 242f64fa8e
12 changed files with 264 additions and 277 deletions

View File

@ -69,6 +69,10 @@ linters-settings:
disabled: true
- name: use-any
disabled: true
- name: exported
disabled: true
- name: unhandled-error
disabled: true
gocritic:
disabled-checks:
- ifElseChain

View File

@ -1,7 +1,5 @@
package ast
import "fmt"
type BaseBlock struct {
BaseNode
}
@ -10,14 +8,8 @@ type LineBreak struct {
BaseBlock
}
var NodeTypeLineBreak = NewNodeType("LineBreak")
func (*LineBreak) Type() NodeType {
return NodeTypeLineBreak
}
func (n *LineBreak) String() string {
return n.Type().String()
return LineBreakNode
}
type Paragraph struct {
@ -26,18 +18,8 @@ type Paragraph struct {
Children []Node
}
var NodeTypeParagraph = NewNodeType("Paragraph")
func (*Paragraph) Type() NodeType {
return NodeTypeParagraph
}
func (n *Paragraph) String() string {
str := n.Type().String()
for _, child := range n.Children {
str += " " + child.String()
}
return str
return ParagraphNode
}
type CodeBlock struct {
@ -47,14 +29,8 @@ type CodeBlock struct {
Content string
}
var NodeTypeCodeBlock = NewNodeType("CodeBlock")
func (*CodeBlock) Type() NodeType {
return NodeTypeCodeBlock
}
func (n *CodeBlock) String() string {
return n.Type().String() + " " + n.Language + " " + n.Content
return CodeBlockNode
}
type Heading struct {
@ -64,18 +40,8 @@ type Heading struct {
Children []Node
}
var NodeTypeHeading = NewNodeType("Heading")
func (*Heading) Type() NodeType {
return NodeTypeHeading
}
func (n *Heading) String() string {
str := n.Type().String() + " " + fmt.Sprintf("%d", n.Level)
for _, child := range n.Children {
str += " " + child.String()
}
return str
return HeadingNode
}
type HorizontalRule struct {
@ -85,14 +51,8 @@ type HorizontalRule struct {
Symbol string
}
var NodeTypeHorizontalRule = NewNodeType("HorizontalRule")
func (*HorizontalRule) Type() NodeType {
return NodeTypeHorizontalRule
}
func (n *HorizontalRule) String() string {
return n.Type().String()
return HorizontalRuleNode
}
type Blockquote struct {
@ -101,16 +61,6 @@ type Blockquote struct {
Children []Node
}
var NodeTypeBlockquote = NewNodeType("Blockquote")
func (*Blockquote) Type() NodeType {
return NodeTypeBlockquote
}
func (n *Blockquote) String() string {
str := n.Type().String()
for _, child := range n.Children {
str += " " + child.String()
}
return str
return BlockquoteNode
}

View File

@ -10,14 +10,8 @@ type Text struct {
Content string
}
var NodeTypeText = NewNodeType("Text")
func (*Text) Type() NodeType {
return NodeTypeText
}
func (n *Text) String() string {
return n.Type().String() + " " + n.Content
return TextNode
}
type Bold struct {
@ -28,14 +22,8 @@ type Bold struct {
Content string
}
var NodeTypeBold = NewNodeType("Bold")
func (*Bold) Type() NodeType {
return NodeTypeBold
}
func (n *Bold) String() string {
return n.Type().String() + " " + n.Symbol + " " + n.Content
return BoldNode
}
type Italic struct {
@ -46,14 +34,8 @@ type Italic struct {
Content string
}
var NodeTypeItalic = NewNodeType("Italic")
func (*Italic) Type() NodeType {
return NodeTypeItalic
}
func (n *Italic) String() string {
return n.Type().String() + " " + n.Symbol + " " + n.Content
return ItalicNode
}
type BoldItalic struct {
@ -64,14 +46,8 @@ type BoldItalic struct {
Content string
}
var NodeTypeBoldItalic = NewNodeType("BoldItalic")
func (*BoldItalic) Type() NodeType {
return NodeTypeBoldItalic
}
func (n *BoldItalic) String() string {
return n.Type().String() + " " + n.Symbol + " " + n.Content
return BoldItalicNode
}
type Code struct {
@ -80,14 +56,8 @@ type Code struct {
Content string
}
var NodeTypeCode = NewNodeType("Code")
func (*Code) Type() NodeType {
return NodeTypeCode
}
func (n *Code) String() string {
return n.Type().String() + " " + n.Content
return CodeNode
}
type Image struct {
@ -97,14 +67,8 @@ type Image struct {
URL string
}
var NodeTypeImage = NewNodeType("Image")
func (*Image) Type() NodeType {
return NodeTypeImage
}
func (n *Image) String() string {
return n.Type().String() + " " + n.AltText + " " + n.URL
return ImageNode
}
type Link struct {
@ -114,14 +78,8 @@ type Link struct {
URL string
}
var NodeTypeLink = NewNodeType("Link")
func (*Link) Type() NodeType {
return NodeTypeLink
}
func (n *Link) String() string {
return n.Type().String() + " " + n.Text + " " + n.URL
return LinkNode
}
type Tag struct {
@ -130,14 +88,8 @@ type Tag struct {
Content string
}
var NodeTypeTag = NewNodeType("Tag")
func (*Tag) Type() NodeType {
return NodeTypeTag
}
func (n *Tag) String() string {
return n.Type().String() + " " + n.Content
return TagNode
}
type Strikethrough struct {
@ -146,12 +98,6 @@ type Strikethrough struct {
Content string
}
var NodeTypeStrikethrough = NewNodeType("Strikethrough")
func (*Strikethrough) Type() NodeType {
return NodeTypeStrikethrough
}
func (n *Strikethrough) String() string {
return n.Type().String() + " " + n.Content
return StrikethroughNode
}

View File

@ -1,18 +1,37 @@
package ast
type NodeType uint32
const (
UnknownNode NodeType = iota
// Block nodes.
LineBreakNode
ParagraphNode
CodeBlockNode
HeadingNode
HorizontalRuleNode
BlockquoteNode
// Inline nodes.
TextNode
BoldNode
ItalicNode
BoldItalicNode
CodeNode
ImageNode
LinkNode
TagNode
StrikethroughNode
)
type Node interface {
// Type returns a node type.
Type() NodeType
// String returns a string representation of this node.
// This method is used for debugging.
String() string
// PrevSibling returns a previous sibling node of this node.
PrevSibling() Node
// GetPrevSibling returns a previous sibling node of this node.
GetPrevSibling() Node
// GetNextSibling returns a next sibling node of this node.
GetNextSibling() Node
// NextSibling returns a next sibling node of this node.
NextSibling() Node
// SetPrevSibling sets a previous sibling node to this node.
SetPrevSibling(Node)
@ -21,32 +40,17 @@ type Node interface {
SetNextSibling(Node)
}
type NodeType int
func (t NodeType) String() string {
return nodeTypeNames[t]
}
var nodeTypeIndex NodeType
var nodeTypeNames = []string{""}
func NewNodeType(name string) NodeType {
nodeTypeNames = append(nodeTypeNames, name)
nodeTypeIndex++
return nodeTypeIndex
}
type BaseNode struct {
prevSibling Node
nextSibling Node
}
func (n *BaseNode) GetPrevSibling() Node {
func (n *BaseNode) PrevSibling() Node {
return n.prevSibling
}
func (n *BaseNode) GetNextSibling() Node {
func (n *BaseNode) NextSibling() Node {
return n.nextSibling
}

View File

@ -88,7 +88,7 @@ func ParseInline(tokens []*tokenizer.Token) ([]ast.Node, error) {
tokens = tokens[size:]
if prevNode != nil {
// Merge text nodes if possible.
if prevNode.Type() == ast.NodeTypeText && node.Type() == ast.NodeTypeText {
if prevNode.Type() == ast.TextNode && node.Type() == ast.TextNode {
prevNode.(*ast.Text).Content += node.(*ast.Text).Content
break
}

View File

@ -127,8 +127,44 @@ func StringifyNodes(nodes []ast.Node) string {
var result string
for _, node := range nodes {
if node != nil {
result += node.String()
result += StringifyNode(node)
}
}
return result
}
func StringifyNode(node ast.Node) string {
switch n := node.(type) {
case *ast.LineBreak:
return "LineBreak()"
case *ast.CodeBlock:
return "CodeBlock(" + n.Language + ", " + n.Content + ")"
case *ast.Paragraph:
return "Paragraph(" + StringifyNodes(n.Children) + ")"
case *ast.Heading:
return "Heading(" + StringifyNodes(n.Children) + ")"
case *ast.HorizontalRule:
return "HorizontalRule(" + n.Symbol + ")"
case *ast.Blockquote:
return "Blockquote(" + StringifyNodes(n.Children) + ")"
case *ast.Text:
return "Text(" + n.Content + ")"
case *ast.Bold:
return "Bold(" + n.Symbol + n.Content + n.Symbol + ")"
case *ast.Italic:
return "Italic(" + n.Symbol + n.Content + n.Symbol + ")"
case *ast.BoldItalic:
return "BoldItalic(" + n.Symbol + n.Content + n.Symbol + ")"
case *ast.Code:
return "Code(" + n.Content + ")"
case *ast.Image:
return "Image(" + n.URL + ", " + n.AltText + ")"
case *ast.Link:
return "Link(" + n.Text + ", " + n.URL + ")"
case *ast.Tag:
return "Tag(" + n.Content + ")"
case *ast.Strikethrough:
return "Strikethrough(" + n.Content + ")"
}
return ""
}

View File

@ -68,14 +68,14 @@ func Tokenize(text string) []*Token {
case ' ':
tokens = append(tokens, NewToken(Space, " "))
default:
var lastToken *Token
var prevToken *Token
if len(tokens) > 0 {
lastToken = tokens[len(tokens)-1]
prevToken = tokens[len(tokens)-1]
}
if lastToken == nil || lastToken.Type != Text {
if prevToken == nil || prevToken.Type != Text {
tokens = append(tokens, NewToken(Text, string(c)))
} else {
lastToken.Value += string(c)
prevToken.Value += string(c)
}
}
}

View File

@ -0,0 +1,171 @@
package html
import (
"bytes"
"fmt"
"github.com/usememos/memos/plugin/gomark/ast"
)
// HTMLRender is a simple renderer that converts AST to HTML.
type HTMLRender struct {
output *bytes.Buffer
context *RendererContext
}
type RendererContext struct {
}
// NewHTMLRender creates a new HTMLRender.
func NewHTMLRender() *HTMLRender {
return &HTMLRender{
output: new(bytes.Buffer),
context: &RendererContext{},
}
}
// RenderNode renders a single AST node to HTML.
func (r *HTMLRender) RenderNode(node ast.Node) {
switch n := node.(type) {
case *ast.LineBreak:
r.renderLineBreak(n)
case *ast.Paragraph:
r.renderParagraph(n)
case *ast.CodeBlock:
r.renderCodeBlock(n)
case *ast.Heading:
r.renderHeading(n)
case *ast.HorizontalRule:
r.renderHorizontalRule(n)
case *ast.Blockquote:
r.renderBlockquote(n)
case *ast.Bold:
r.renderBold(n)
case *ast.Italic:
r.renderItalic(n)
case *ast.BoldItalic:
r.renderBoldItalic(n)
case *ast.Code:
r.renderCode(n)
case *ast.Image:
r.renderImage(n)
case *ast.Link:
r.renderLink(n)
case *ast.Tag:
r.renderTag(n)
case *ast.Strikethrough:
r.renderStrikethrough(n)
case *ast.Text:
r.renderText(n)
default:
// Handle other block types if needed.
}
}
// RenderNodes renders a slice of AST nodes to HTML.
func (r *HTMLRender) RenderNodes(nodes []ast.Node) {
for _, node := range nodes {
r.RenderNode(node)
}
}
// Render renders the AST to HTML.
func (r *HTMLRender) Render(astRoot []ast.Node) string {
r.RenderNodes(astRoot)
return r.output.String()
}
func (r *HTMLRender) renderLineBreak(_ *ast.LineBreak) {
r.output.WriteString("<br>")
}
func (r *HTMLRender) renderParagraph(node *ast.Paragraph) {
r.output.WriteString("<p>")
r.RenderNodes(node.Children)
r.output.WriteString("</p>")
}
func (r *HTMLRender) renderCodeBlock(node *ast.CodeBlock) {
r.output.WriteString("<pre><code>")
r.output.WriteString(node.Content)
r.output.WriteString("</code></pre>")
}
func (r *HTMLRender) renderHeading(node *ast.Heading) {
element := fmt.Sprintf("<h%d>", node.Level)
r.output.WriteString(fmt.Sprintf("<%s>", element))
r.RenderNodes(node.Children)
r.output.WriteString(fmt.Sprintf("</%s>", element))
}
func (r *HTMLRender) renderHorizontalRule(_ *ast.HorizontalRule) {
r.output.WriteString("<hr>")
}
func (r *HTMLRender) renderBlockquote(node *ast.Blockquote) {
prevSibling, nextSibling := node.PrevSibling(), node.NextSibling()
if prevSibling == nil || prevSibling.Type() != ast.BlockquoteNode {
r.output.WriteString("<blockquote>")
}
r.RenderNodes(node.Children)
if nextSibling == nil || nextSibling.Type() != ast.BlockquoteNode {
r.output.WriteString("</blockquote>")
}
}
func (r *HTMLRender) renderText(node *ast.Text) {
r.output.WriteString(node.Content)
}
func (r *HTMLRender) renderBold(node *ast.Bold) {
r.output.WriteString("<strong>")
r.output.WriteString(node.Content)
r.output.WriteString("</strong>")
}
func (r *HTMLRender) renderItalic(node *ast.Italic) {
r.output.WriteString("<em>")
r.output.WriteString(node.Content)
r.output.WriteString("</em>")
}
func (r *HTMLRender) renderBoldItalic(node *ast.BoldItalic) {
r.output.WriteString("<strong><em>")
r.output.WriteString(node.Content)
r.output.WriteString("</em></strong>")
}
func (r *HTMLRender) renderCode(node *ast.Code) {
r.output.WriteString("<code>")
r.output.WriteString(node.Content)
r.output.WriteString("</code>")
}
func (r *HTMLRender) renderImage(node *ast.Image) {
r.output.WriteString(`<img src="`)
r.output.WriteString(node.URL)
r.output.WriteString(`" alt="`)
r.output.WriteString(node.AltText)
r.output.WriteString(`" />`)
}
func (r *HTMLRender) renderLink(node *ast.Link) {
r.output.WriteString(`<a href="`)
r.output.WriteString(node.URL)
r.output.WriteString(`">`)
r.output.WriteString(node.Text)
r.output.WriteString("</a>")
}
func (r *HTMLRender) renderTag(node *ast.Tag) {
r.output.WriteString(`<span>`)
r.output.WriteString(`# `)
r.output.WriteString(node.Content)
r.output.WriteString(`</span>`)
}
func (r *HTMLRender) renderStrikethrough(node *ast.Strikethrough) {
r.output.WriteString(`<del>`)
r.output.WriteString(node.Content)
r.output.WriteString(`</del>`)
}

View File

@ -9,7 +9,7 @@ import (
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
)
func TestHTMLRenderer(t *testing.T) {
func TestHTMLRender(t *testing.T) {
tests := []struct {
text string
expected string
@ -36,7 +36,7 @@ func TestHTMLRenderer(t *testing.T) {
tokens := tokenizer.Tokenize(test.text)
nodes, err := parser.Parse(tokens)
require.NoError(t, err)
actual := NewHTMLRenderer().Render(nodes)
actual := NewHTMLRender().Render(nodes)
require.Equal(t, test.expected, actual)
}
}

View File

@ -0,0 +1 @@
package render

View File

@ -1,124 +0,0 @@
package html
import (
"bytes"
"fmt"
"github.com/usememos/memos/plugin/gomark/ast"
)
// HTMLRenderer is a simple renderer that converts AST to HTML.
// nolint
type HTMLRenderer struct {
output *bytes.Buffer
context *RendererContext
}
type RendererContext struct {
}
// NewHTMLRenderer creates a new HTMLRenderer.
func NewHTMLRenderer() *HTMLRenderer {
return &HTMLRenderer{
output: new(bytes.Buffer),
context: &RendererContext{},
}
}
// RenderNode renders a single AST node to HTML.
func (r *HTMLRenderer) RenderNode(node ast.Node) {
prevSibling, nextSibling := node.GetPrevSibling(), node.GetNextSibling()
switch n := node.(type) {
case *ast.LineBreak:
r.output.WriteString("<br>")
case *ast.Paragraph:
r.output.WriteString("<p>")
r.RenderNodes(n.Children)
r.output.WriteString("</p>")
case *ast.CodeBlock:
r.output.WriteString("<pre><code>")
r.output.WriteString(n.Content)
r.output.WriteString("</code></pre>")
case *ast.Heading:
r.output.WriteString(fmt.Sprintf("<h%d>", n.Level))
r.RenderNodes(n.Children)
r.output.WriteString(fmt.Sprintf("</h%d>", n.Level))
case *ast.HorizontalRule:
r.output.WriteString("<hr>")
case *ast.Blockquote:
if prevSibling == nil || prevSibling.Type() != ast.NodeTypeBlockquote {
r.output.WriteString("<blockquote>")
}
r.RenderNodes(n.Children)
if nextSibling != nil && nextSibling.Type() == ast.NodeTypeBlockquote {
r.RenderNode(nextSibling)
}
if prevSibling == nil || prevSibling.Type() != ast.NodeTypeBlockquote {
r.output.WriteString("</blockquote>")
}
case *ast.BoldItalic:
r.output.WriteString("<strong><em>")
r.output.WriteString(n.Content)
r.output.WriteString("</em></strong>")
case *ast.Bold:
r.output.WriteString("<strong>")
r.output.WriteString(n.Content)
r.output.WriteString("</strong>")
case *ast.Italic:
r.output.WriteString("<em>")
r.output.WriteString(n.Content)
r.output.WriteString("</em>")
case *ast.Code:
r.output.WriteString("<code>")
r.output.WriteString(n.Content)
r.output.WriteString("</code>")
case *ast.Link:
r.output.WriteString(`<a href="`)
r.output.WriteString(n.URL)
r.output.WriteString(`">`)
r.output.WriteString(n.Text)
r.output.WriteString("</a>")
case *ast.Image:
r.output.WriteString(`<img src="`)
r.output.WriteString(n.URL)
r.output.WriteString(`" alt="`)
r.output.WriteString(n.AltText)
r.output.WriteString(`" />`)
case *ast.Tag:
r.output.WriteString(`<span>`)
r.output.WriteString(`# `)
r.output.WriteString(n.Content)
r.output.WriteString(`</span>`)
case *ast.Strikethrough:
r.output.WriteString(`<del>`)
r.output.WriteString(n.Content)
r.output.WriteString(`</del>`)
case *ast.Text:
r.output.WriteString(n.Content)
default:
// Handle other block types if needed.
}
}
// RenderNodes renders a slice of AST nodes to HTML.
func (r *HTMLRenderer) RenderNodes(nodes []ast.Node) {
for _, node := range nodes {
prevSibling := node.GetPrevSibling()
if prevSibling != nil {
if prevSibling.Type() == node.Type() {
if node.Type() == ast.NodeTypeBlockquote {
continue
}
}
}
r.RenderNode(node)
}
}
// Render renders the AST to HTML.
func (r *HTMLRenderer) Render(astRoot []ast.Node) string {
r.RenderNodes(astRoot)
return r.output.String()
}

View File

@ -1 +0,0 @@
package renderer