From 9c4855f3d005775e006c2412f835a380f29efbbb Mon Sep 17 00:00:00 2001 From: eyedeekay Date: Fri, 1 Nov 2024 20:59:53 -0400 Subject: [PATCH] Basic implementation --- .gitignore | 1 + example/doc.rst | 13 ++ example/translations.po | 17 +++ go.mod | 5 + go.sum | 25 ++++ main.go | 81 ++++++++++++ output.html | 16 +++ pkg/nodes/nodes.go | 248 +++++++++++++++++++++++++++++++++++ pkg/nodes/types.go | 65 +++++++++ pkg/parser/context.go | 25 ++++ pkg/parser/lexer.go | 115 ++++++++++++++++ pkg/parser/parser.go | 196 +++++++++++++++++++++++++++ pkg/parser/patterns.go | 23 ++++ pkg/renderer/html.go | 150 +++++++++++++++++++++ pkg/translator/translator.go | 52 ++++++++ 15 files changed, 1032 insertions(+) create mode 100644 .gitignore create mode 100644 example/doc.rst create mode 100644 example/translations.po create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 output.html create mode 100644 pkg/nodes/nodes.go create mode 100644 pkg/nodes/types.go create mode 100644 pkg/parser/context.go create mode 100644 pkg/parser/lexer.go create mode 100644 pkg/parser/parser.go create mode 100644 pkg/parser/patterns.go create mode 100644 pkg/renderer/html.go create mode 100644 pkg/translator/translator.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..facd883 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +go-rst \ No newline at end of file diff --git a/example/doc.rst b/example/doc.rst new file mode 100644 index 0000000..9657881 --- /dev/null +++ b/example/doc.rst @@ -0,0 +1,13 @@ +Welcome to My Documentation +========================== + +This is a sample RST document that demonstrates translations. + +Translations +----------- + +{% trans %}This text will be translated{% endtrans %} + +Some regular text here. + +{% trans %}Another translatable section{% endtrans %} \ No newline at end of file diff --git a/example/translations.po b/example/translations.po new file mode 100644 index 0000000..96aa747 --- /dev/null +++ b/example/translations.po @@ -0,0 +1,17 @@ +msgid "" +msgstr "" +"Project-Id-Version: Test\n" +"POT-Creation-Date: 2024-01-20 12:00-0500\n" +"PO-Revision-Date: 2024-01-20 12:00-0500\n" +"Last-Translator: Alex\n" +"Language-Team: Spanish\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgid "This text will be translated" +msgstr "Este texto será traducido" + +msgid "Another translatable section" +msgstr "Otra sección traducible" \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..caa2a99 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module i2pgit.org/idk/go-rst + +go 1.23.1 + +require github.com/leonelquinteros/gotext v1.7.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c3d6c05 --- /dev/null +++ b/go.sum @@ -0,0 +1,25 @@ +github.com/leonelquinteros/gotext v1.7.0 h1:jcJmF4AXqyamP7vuw2MMIKs+O3jAEmvrc5JQiI8Ht/8= +github.com/leonelquinteros/gotext v1.7.0/go.mod h1:qJdoQuERPpccw7L70uoU+K/BvTfRBHYsisCQyFLXyvw= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/main.go b/main.go new file mode 100644 index 0000000..63bfe24 --- /dev/null +++ b/main.go @@ -0,0 +1,81 @@ +package main + +import ( + "flag" + "fmt" + "io/ioutil" + "log" + + "i2pgit.org/idk/go-rst/pkg/parser" + "i2pgit.org/idk/go-rst/pkg/renderer" + "i2pgit.org/idk/go-rst/pkg/translator" +) + +func main() { + // CLI flags + rstFile := flag.String("rst", "", "Input RST file path") + poFile := flag.String("po", "", "Input PO file path for translations") + outFile := flag.String("out", "", "Output HTML file path") + debug := flag.Bool("debug", false, "Enable debug logging") + flag.Parse() + + if *debug { + log.SetFlags(log.Lshortfile | log.LstdFlags) + } + + // Validate input flags + if *rstFile == "" { + log.Fatal("Please provide an input RST file using -rst flag") + } + if *outFile == "" { + log.Fatal("Please provide an output HTML file using -out flag") + } + + // Read RST content + content, err := ioutil.ReadFile(*rstFile) + if err != nil { + log.Fatalf("Failed to read RST file: %v", err) + } + + if *debug { + log.Printf("Loaded RST file: %s", *rstFile) + } + + // Initialize translator + trans, err := translator.NewPOTranslator(*poFile) + if err != nil { + log.Fatalf("Failed to initialize translator: %v", err) + } + + if *debug && *poFile != "" { + log.Printf("Loaded PO file: %s", *poFile) + // Test translation + testStr := "This text will be translated" + translated := trans.Translate(testStr) + log.Printf("Translation test: '%s' -> '%s'", testStr, translated) + } + + // Initialize parser with translator + p := parser.NewParser(trans) + + // Parse RST content + nodes := p.Parse(string(content)) + + if *debug { + log.Printf("Parsed %d nodes", len(nodes)) + } + + // Initialize HTML renderer + r := renderer.NewHTMLRenderer() + + // Render HTML + html := r.Render(nodes) + + // Write output + err = ioutil.WriteFile(*outFile, []byte(html), 0644) + if err != nil { + log.Fatalf("Failed to write HTML file: %v", err) + } + + fmt.Printf("Successfully converted %s to %s\n", *rstFile, *outFile) +} \ No newline at end of file diff --git a/output.html b/output.html new file mode 100644 index 0000000..0b52b9a --- /dev/null +++ b/output.html @@ -0,0 +1,16 @@ + + + + + + +

Welcome to My Documentation

+

Translations

+

Welcome to My Documentation +This is a sample RST document that demonstrates translations. +Translations

+

Este texto será traducido +Some regular text here.

+

Otra sección traducible

+ + \ No newline at end of file diff --git a/pkg/nodes/nodes.go b/pkg/nodes/nodes.go new file mode 100644 index 0000000..fa22367 --- /dev/null +++ b/pkg/nodes/nodes.go @@ -0,0 +1,248 @@ +// pkg/nodes/nodes.go + +package nodes + +import ( + "fmt" + "strings" +) + +// HeadingNode represents a section heading in RST +type HeadingNode struct { + *BaseNode +} + +func NewHeadingNode(content string, level int) *HeadingNode { + node := &HeadingNode{ + BaseNode: NewBaseNode(NodeHeading), + } + node.SetContent(content) + node.SetLevel(level) + return node +} + +// ParagraphNode represents a text paragraph +type ParagraphNode struct { + *BaseNode +} + +func NewParagraphNode(content string) *ParagraphNode { + node := &ParagraphNode{ + BaseNode: NewBaseNode(NodeParagraph), + } + node.SetContent(content) + return node +} + +// ListNode represents an ordered or unordered list +type ListNode struct { + *BaseNode + ordered bool +} + +func NewListNode(ordered bool) *ListNode { + node := &ListNode{ + BaseNode: NewBaseNode(NodeList), + ordered: ordered, + } + return node +} + +func (n *ListNode) IsOrdered() bool { + return n.ordered +} + +// ListItemNode represents an individual list item +type ListItemNode struct { + *BaseNode +} + +func NewListItemNode(content string) *ListItemNode { + node := &ListItemNode{ + BaseNode: NewBaseNode(NodeListItem), + } + node.SetContent(content) + return node +} + +// LinkNode represents a hyperlink +type LinkNode struct { + *BaseNode + url string + title string +} + +func NewLinkNode(text, url, title string) *LinkNode { + node := &LinkNode{ + BaseNode: NewBaseNode(NodeLink), + url: url, + title: title, + } + node.SetContent(text) + return node +} + +func (n *LinkNode) URL() string { return n.url } +func (n *LinkNode) Title() string { return n.title } + +// EmphasisNode represents emphasized text (italic) +type EmphasisNode struct { + *BaseNode +} + +func NewEmphasisNode(content string) *EmphasisNode { + node := &EmphasisNode{ + BaseNode: NewBaseNode(NodeEmphasis), + } + node.SetContent(content) + return node +} + +// StrongNode represents strong text (bold) +type StrongNode struct { + *BaseNode +} + +func NewStrongNode(content string) *StrongNode { + node := &StrongNode{ + BaseNode: NewBaseNode(NodeStrong), + } + node.SetContent(content) + return node +} + +// MetaNode represents metadata information +type MetaNode struct { + *BaseNode + key string +} + +func NewMetaNode(key, value string) *MetaNode { + node := &MetaNode{ + BaseNode: NewBaseNode(NodeMeta), + key: key, + } + node.SetContent(value) + return node +} + +func (n *MetaNode) Key() string { return n.key } + +// DirectiveNode represents an RST directive +type DirectiveNode struct { + *BaseNode + name string + arguments []string + rawContent string +} + +func NewDirectiveNode(name string, args []string) *DirectiveNode { + node := &DirectiveNode{ + BaseNode: NewBaseNode(NodeDirective), + name: name, + arguments: args, + rawContent: "", + } + return node +} + +func (n *DirectiveNode) Name() string { return n.name } +func (n *DirectiveNode) Arguments() []string { return n.arguments } +func (n *DirectiveNode) RawContent() string { return n.rawContent } +func (n *DirectiveNode) SetRawContent(content string) { + n.rawContent = content +} + +// CodeNode represents a code block +type CodeNode struct { + *BaseNode + language string + lineNumbers bool +} + +func NewCodeNode(language string, content string, lineNumbers bool) *CodeNode { + node := &CodeNode{ + BaseNode: NewBaseNode(NodeCode), + language: language, + lineNumbers: lineNumbers, + } + node.SetContent(content) + return node +} + +func (n *CodeNode) Language() string { return n.language } +func (n *CodeNode) LineNumbers() bool { return n.lineNumbers } + +// TableNode represents a table structure +type TableNode struct { + *BaseNode + headers []string + rows [][]string +} + +func NewTableNode() *TableNode { + return &TableNode{ + BaseNode: NewBaseNode(NodeTable), + headers: make([]string, 0), + rows: make([][]string, 0), + } +} + +func (n *TableNode) SetHeaders(headers []string) { + n.headers = headers +} + +func (n *TableNode) AddRow(row []string) { + n.rows = append(n.rows, row) +} + +func (n *TableNode) Headers() []string { return n.headers } +func (n *TableNode) Rows() [][]string { return n.rows } + +// Utility function to get node content with proper indentation +func GetIndentedContent(node Node) string { + content := node.Content() + if node.Level() > 0 { + indent := strings.Repeat(" ", node.Level()) + lines := strings.Split(content, "\n") + for i, line := range lines { + lines[i] = indent + line + } + content = strings.Join(lines, "\n") + } + return content +} + +// String representations for debugging +func (n *HeadingNode) String() string { + return fmt.Sprintf("Heading[%d]: %s", n.Level(), n.Content()) +} + +func (n *ParagraphNode) String() string { + return fmt.Sprintf("Paragraph: %s", n.Content()) +} + +func (n *ListNode) String() string { + listType := "Unordered" + if n.ordered { + listType = "Ordered" + } + return fmt.Sprintf("%s List with %d items", listType, len(n.Children())) +} + +func (n *LinkNode) String() string { + return fmt.Sprintf("Link[%s](%s)", n.Content(), n.url) +} + +func (n *DirectiveNode) String() string { + return fmt.Sprintf("Directive[%s]: %s", n.name, n.Content()) +} + +func (n *CodeNode) String() string { + return fmt.Sprintf("Code[%s]: %d bytes", n.language, len(n.Content())) +} + +func (n *TableNode) String() string { + return fmt.Sprintf("Table: %d columns x %d rows", len(n.headers), len(n.rows)) +} + diff --git a/pkg/nodes/types.go b/pkg/nodes/types.go new file mode 100644 index 0000000..68c041e --- /dev/null +++ b/pkg/nodes/types.go @@ -0,0 +1,65 @@ +// pkg/nodes/base.go + +package nodes + +type NodeType int + +const ( + NodeHeading NodeType = iota + NodeParagraph + NodeList + NodeListItem + NodeLink + NodeEmphasis + NodeStrong + NodeMeta + NodeDirective + NodeCode + NodeTable +) + +type Node interface { + Type() NodeType + Content() string + SetContent(string) + Level() int + SetLevel(int) + Children() []Node + AddChild(Node) +} + +type BaseNode struct { + nodeType NodeType + content string + level int + children []Node +} + +func NewBaseNode(nodeType NodeType) *BaseNode { + return &BaseNode{ + nodeType: nodeType, + children: make([]Node, 0), + } +} + +func (n *BaseNode) Type() NodeType { return n.nodeType } + +func (n *BaseNode) Content() string { return n.content } + +func (n *BaseNode) SetContent(content string) { + n.content = content +} + +func (n *BaseNode) Level() int { return n.level } + +func (n *BaseNode) SetLevel(level int) { + n.level = level +} + +func (n *BaseNode) Children() []Node { + return n.children +} + +func (n *BaseNode) AddChild(child Node) { + n.children = append(n.children, child) +} \ No newline at end of file diff --git a/pkg/parser/context.go b/pkg/parser/context.go new file mode 100644 index 0000000..75590da --- /dev/null +++ b/pkg/parser/context.go @@ -0,0 +1,25 @@ +package parser + +type ParserContext struct { + inMeta bool + inDirective bool + currentDirective string + inCodeBlock bool + codeBlockIndent int + buffer []string +} + +func NewParserContext() *ParserContext { + return &ParserContext{ + buffer: make([]string, 0), + } +} + +func (c *ParserContext) Reset() { + c.inMeta = false + c.inDirective = false + c.currentDirective = "" + c.inCodeBlock = false + c.codeBlockIndent = 0 + c.buffer = c.buffer[:0] +} \ No newline at end of file diff --git a/pkg/parser/lexer.go b/pkg/parser/lexer.go new file mode 100644 index 0000000..b1c7db3 --- /dev/null +++ b/pkg/parser/lexer.go @@ -0,0 +1,115 @@ +// pkg/parser/lexer.go + +package parser + +import ( + "strings" +) + +type TokenType int + +const ( + TokenText TokenType = iota + TokenHeadingUnderline + TokenTransBlock + TokenMeta + TokenDirective + TokenCodeBlock + TokenBlankLine + TokenIndent +) + +type Token struct { + Type TokenType + Content string + Args []string +} + +type Lexer struct { + patterns *Patterns +} + +func NewLexer() *Lexer { + return &Lexer{ + patterns: NewPatterns(), + } +} + +func (l *Lexer) Tokenize(line string) Token { + // Handle blank lines + if strings.TrimSpace(line) == "" { + return Token{Type: TokenBlankLine} + } + + // Calculate indentation + indent := 0 + for _, r := range line { + if r == ' ' { + indent++ + } else if r == '\t' { + indent += 4 + } else { + break + } + } + + line = strings.TrimLeft(line, " \t") + + // Check for heading underline + if l.patterns.headingUnderline.MatchString(line) { + return Token{ + Type: TokenHeadingUnderline, + Content: line, + } + } + + // Check for translation blocks + if matches := l.patterns.transBlock.FindStringSubmatch(line); len(matches) > 1 { + return Token{ + Type: TokenTransBlock, + Content: matches[1], + } + } + + // Check for meta directive + if l.patterns.meta.MatchString(line) { + return Token{ + Type: TokenMeta, + } + } + + // Check for code block + if l.patterns.codeBlock.MatchString(line) { + args := parseDirectiveArgs(line) + return Token{ + Type: TokenCodeBlock, + Args: args, + } + } + + // Check for other directives + if matches := l.patterns.directive.FindStringSubmatch(line); len(matches) > 1 { + args := parseDirectiveArgs(line) + return Token{ + Type: TokenDirective, + Content: matches[1], + Args: args, + } + } + + // Regular text + return Token{ + Type: TokenText, + Content: line, + } +} + +func parseDirectiveArgs(line string) []string { + parts := strings.SplitN(line, "::", 2) + if len(parts) != 2 { + return nil + } + + args := strings.Fields(strings.TrimSpace(parts[1])) + return args +} \ No newline at end of file diff --git a/pkg/parser/parser.go b/pkg/parser/parser.go new file mode 100644 index 0000000..2225b2c --- /dev/null +++ b/pkg/parser/parser.go @@ -0,0 +1,196 @@ +// pkg/parser/parser.go + +package parser + +import ( + "bufio" + "strings" + + "i2pgit.org/idk/go-rst/pkg/nodes" + "i2pgit.org/idk/go-rst/pkg/translator" +) + +type Parser struct { + nodes []nodes.Node + translator translator.Translator + context *ParserContext + patterns *Patterns + lexer *Lexer +} + +func NewParser(trans translator.Translator) *Parser { + return &Parser{ + nodes: make([]nodes.Node, 0), + translator: trans, + context: NewParserContext(), + patterns: NewPatterns(), + lexer: NewLexer(), + } +} + +func (p *Parser) Parse(content string) []nodes.Node { + scanner := bufio.NewScanner(strings.NewReader(content)) + var currentNode nodes.Node + var prevToken Token + p.nodes = make([]nodes.Node, 0) // Clear existing nodes + + for scanner.Scan() { + line := scanner.Text() + token := p.lexer.Tokenize(line) + + if newNode := p.processToken(token, prevToken, currentNode); newNode != nil { + // Only append if we actually have a new node + if currentNode != nil && currentNode != newNode { + p.nodes = append(p.nodes, currentNode) + } + currentNode = newNode + } + prevToken = token + } + + // Add final node if exists and not already added + if currentNode != nil && (len(p.nodes) == 0 || p.nodes[len(p.nodes)-1] != currentNode) { + p.nodes = append(p.nodes, currentNode) + } + + return p.nodes +} + +func (p *Parser) processToken(token, prevToken Token, currentNode nodes.Node) nodes.Node { + //translatedContent := p.translator.Translate(token.Content) + //token.Content = translatedContent + switch token.Type { + case TokenTransBlock: + // Always create a new node for translation blocks + translatedContent := p.translator.Translate(strings.TrimSpace(token.Content)) + return nodes.NewParagraphNode(translatedContent) + case TokenHeadingUnderline: + if prevToken.Type == TokenText { + return p.processHeading(prevToken.Content, token.Content) + } + + case TokenMeta: + p.context.inMeta = true + return nodes.NewMetaNode("", "") + + case TokenCodeBlock: + p.context.inCodeBlock = true + p.context.codeBlockIndent = 4 + language := "" + if len(token.Args) > 0 { + language = token.Args[0] + } + return nodes.NewCodeNode(language, "", false) + + case TokenDirective: + p.context.inDirective = true + p.context.currentDirective = token.Content + return nodes.NewDirectiveNode(token.Content, token.Args) + + case TokenText: + if p.context.inCodeBlock { + return p.processCodeBlock(token.Content, currentNode) + } + if p.context.inMeta { + return p.processMetaContent(token.Content, currentNode) + } + if p.context.inDirective { + return p.processDirectiveContent(token.Content, currentNode) + } + return p.processParagraph(token.Content, currentNode) + } + + return currentNode +} + +func (p *Parser) processHeading(content, underline string) nodes.Node { + level := 1 + switch underline[0] { + case '-': + level = 2 + case '~': + level = 3 + } + + node := nodes.NewHeadingNode(strings.TrimSpace(content), level) + p.nodes = append(p.nodes, node) + return nil +} + +func (p *Parser) processMetaContent(line string, currentNode nodes.Node) nodes.Node { + if currentNode == nil || currentNode.Type() != nodes.NodeMeta { + return currentNode + } + + parts := strings.SplitN(line, ":", 2) + if len(parts) != 2 { + return currentNode + } + + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + + node := nodes.NewMetaNode(key, value) + p.nodes = append(p.nodes, node) + p.context.inMeta = false + return nil +} + +func (p *Parser) processCodeBlock(line string, currentNode nodes.Node) nodes.Node { + if currentNode == nil || currentNode.Type() != nodes.NodeCode { + return currentNode + } + + p.context.buffer = append(p.context.buffer, line) + + if strings.TrimSpace(line) == "" { + codeNode := currentNode.(*nodes.CodeNode) + content := strings.Join(p.context.buffer, "\n") + codeNode.SetContent(content) + p.nodes = append(p.nodes, codeNode) + p.context.Reset() + return nil + } + + return currentNode +} + +func (p *Parser) processDirectiveContent(line string, currentNode nodes.Node) nodes.Node { + if currentNode == nil || currentNode.Type() != nodes.NodeDirective { + return currentNode + } + + directiveNode := currentNode.(*nodes.DirectiveNode) + p.context.buffer = append(p.context.buffer, line) + + if strings.TrimSpace(line) == "" { + content := strings.Join(p.context.buffer, "\n") + directiveNode.SetRawContent(content) + p.nodes = append(p.nodes, directiveNode) + p.context.Reset() + return nil + } + + return currentNode +} + +func (p *Parser) processParagraph(line string, currentNode nodes.Node) nodes.Node { + if strings.TrimSpace(line) == "" { + if currentNode != nil { + p.nodes = append(p.nodes, currentNode) + } + return nil + } + + if currentNode == nil { + return nodes.NewParagraphNode(line) + } + + if currentNode.Type() == nodes.NodeParagraph { + currentContent := currentNode.Content() + currentNode.SetContent(currentContent + "\n" + line) + return currentNode + } + + return nodes.NewParagraphNode(line) +} \ No newline at end of file diff --git a/pkg/parser/patterns.go b/pkg/parser/patterns.go new file mode 100644 index 0000000..f571ac5 --- /dev/null +++ b/pkg/parser/patterns.go @@ -0,0 +1,23 @@ +// pkg/parser/patterns.go + +package parser + +import "regexp" + +type Patterns struct { + headingUnderline *regexp.Regexp + transBlock *regexp.Regexp + meta *regexp.Regexp + directive *regexp.Regexp + codeBlock *regexp.Regexp +} + +func NewPatterns() *Patterns { + return &Patterns{ + headingUnderline: regexp.MustCompile(`^[=\-~]+$`), + transBlock: regexp.MustCompile(`{%\s*trans\s*%}(.*?){%\s*endtrans\s*%}`), + meta: regexp.MustCompile(`^\.\.\s+meta::`), + directive: regexp.MustCompile(`^\.\.\s+(\w+)::`), + codeBlock: regexp.MustCompile(`^\.\.\s+code::`), + } +} \ No newline at end of file diff --git a/pkg/renderer/html.go b/pkg/renderer/html.go new file mode 100644 index 0000000..af1cb52 --- /dev/null +++ b/pkg/renderer/html.go @@ -0,0 +1,150 @@ +// pkg/renderer/html.go + +package renderer + +import ( + "bytes" + "fmt" + "html" + "strings" + + "i2pgit.org/idk/go-rst/pkg/nodes" +) + +type HTMLRenderer struct { + buffer bytes.Buffer +} + +func NewHTMLRenderer() *HTMLRenderer { + return &HTMLRenderer{} +} + +func (r *HTMLRenderer) Render(nodes []nodes.Node) string { + r.buffer.Reset() + + r.buffer.WriteString("\n\n\n") + r.renderMeta(nodes) + r.buffer.WriteString("\n\n") + + for _, node := range nodes { + r.renderNode(node) + } + + r.buffer.WriteString("\n") + return r.buffer.String() +} + +func (r *HTMLRenderer) renderMeta(nodelist []nodes.Node) { + r.buffer.WriteString("\n") + + for _, node := range nodelist { + switch n := node.(type) { + case *nodes.MetaNode: + r.buffer.WriteString(fmt.Sprintf("\n", + html.EscapeString(n.Key()), + html.EscapeString(n.Content()))) + } + } +} + +func (r *HTMLRenderer) renderNode(node nodes.Node) { + switch n := node.(type) { + case *nodes.HeadingNode: + r.buffer.WriteString(fmt.Sprintf("%s\n", + n.Level(), + html.EscapeString(n.Content()), + n.Level())) + + case *nodes.ParagraphNode: + r.buffer.WriteString(fmt.Sprintf("

%s

\n", + html.EscapeString(n.Content()))) + + case *nodes.ListNode: + tag := "ul" + if n.IsOrdered() { + tag = "ol" + } + r.buffer.WriteString(fmt.Sprintf("<%s>\n", tag)) + for _, child := range n.Children() { + if item, ok := child.(*nodes.ListItemNode); ok { + r.buffer.WriteString(fmt.Sprintf("
  • %s
  • \n", + html.EscapeString(item.Content()))) + } + } + r.buffer.WriteString(fmt.Sprintf("\n", tag)) + + case *nodes.LinkNode: + r.buffer.WriteString(fmt.Sprintf("%s", + html.EscapeString(n.URL()), + html.EscapeString(n.Title()), + html.EscapeString(n.Content()))) + + case *nodes.EmphasisNode: + r.buffer.WriteString(fmt.Sprintf("%s", + html.EscapeString(n.Content()))) + + case *nodes.StrongNode: + r.buffer.WriteString(fmt.Sprintf("%s", + html.EscapeString(n.Content()))) + + case *nodes.CodeNode: + r.buffer.WriteString(fmt.Sprintf("
    %s
    \n", + html.EscapeString(n.Language()), + html.EscapeString(n.Content()))) + + case *nodes.TableNode: + r.renderTable(n) + + case *nodes.DirectiveNode: + r.renderDirective(n) + } +} + +func (r *HTMLRenderer) renderTable(table *nodes.TableNode) { + r.buffer.WriteString("\n") + + // Render headers + if len(table.Headers()) > 0 { + r.buffer.WriteString("\n") + for _, header := range table.Headers() { + r.buffer.WriteString(fmt.Sprintf("", + html.EscapeString(header))) + } + r.buffer.WriteString("\n") + } + + // Render rows + r.buffer.WriteString("\n") + for _, row := range table.Rows() { + r.buffer.WriteString("\n") + for _, cell := range row { + r.buffer.WriteString(fmt.Sprintf("", + html.EscapeString(cell))) + } + r.buffer.WriteString("\n") + } + r.buffer.WriteString("
    %s
    %s
    \n") +} + +func (r *HTMLRenderer) renderDirective(directive *nodes.DirectiveNode) { + switch directive.Name() { + case "image": + if len(directive.Arguments()) > 0 { + alt := "" + if len(directive.Arguments()) > 1 { + alt = strings.Join(directive.Arguments()[1:], " ") + } + r.buffer.WriteString(fmt.Sprintf("\"%s\"\n", + html.EscapeString(directive.Arguments()[0]), + html.EscapeString(alt))) + } + + case "note": + r.buffer.WriteString(fmt.Sprintf("
    %s
    \n", + html.EscapeString(directive.RawContent()))) + + case "warning": + r.buffer.WriteString(fmt.Sprintf("
    %s
    \n", + html.EscapeString(directive.RawContent()))) + } +} \ No newline at end of file diff --git a/pkg/translator/translator.go b/pkg/translator/translator.go new file mode 100644 index 0000000..d27f224 --- /dev/null +++ b/pkg/translator/translator.go @@ -0,0 +1,52 @@ +package translator + +import ( + "github.com/leonelquinteros/gotext" +) + +type Translator interface { + Translate(text string) string +} + +type POTranslator struct { + po *gotext.Po +} + +func NewPOTranslator(poFile string) (*POTranslator, error) { + translator := &POTranslator{ + po: gotext.NewPo(), + } + + // If no PO file is provided, return a pass-through translator + if poFile == "" { + return translator, nil + } + + // Parse PO file + translator.po.ParseFile(poFile) + + + return translator, nil +} + +func (t *POTranslator) Translate(text string) string { + if t.po == nil { + return text + } + translated := t.po.Get(text) + if translated == "" { + return text + } + return translated +} + +// NoopTranslator implements Translator interface but doesn't translate +type NoopTranslator struct{} + +func NewNoopTranslator() *NoopTranslator { + return &NoopTranslator{} +} + +func (t *NoopTranslator) Translate(text string) string { + return text +} \ No newline at end of file