Basic implementation

This commit is contained in:
eyedeekay
2024-11-01 20:59:53 -04:00
commit 9c4855f3d0
15 changed files with 1032 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
go-rst

13
example/doc.rst Normal file
View File

@ -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 %}

17
example/translations.po Normal file
View File

@ -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"

5
go.mod Normal file
View File

@ -0,0 +1,5 @@
module i2pgit.org/idk/go-rst
go 1.23.1
require github.com/leonelquinteros/gotext v1.7.0

25
go.sum Normal file
View File

@ -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=

81
main.go Normal file
View File

@ -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)
}

16
output.html Normal file
View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<h1>Welcome to My Documentation</h1>
<h2>Translations</h2>
<p>Welcome to My Documentation
This is a sample RST document that demonstrates translations.
Translations</p>
<p>Este texto será traducido
Some regular text here.</p>
<p>Otra sección traducible</p>
</body>
</html>

248
pkg/nodes/nodes.go Normal file
View File

@ -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))
}

65
pkg/nodes/types.go Normal file
View File

@ -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)
}

25
pkg/parser/context.go Normal file
View File

@ -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]
}

115
pkg/parser/lexer.go Normal file
View File

@ -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
}

196
pkg/parser/parser.go Normal file
View File

@ -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)
}

23
pkg/parser/patterns.go Normal file
View File

@ -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::`),
}
}

150
pkg/renderer/html.go Normal file
View File

@ -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("<!DOCTYPE html>\n<html>\n<head>\n")
r.renderMeta(nodes)
r.buffer.WriteString("</head>\n<body>\n")
for _, node := range nodes {
r.renderNode(node)
}
r.buffer.WriteString("</body>\n</html>")
return r.buffer.String()
}
func (r *HTMLRenderer) renderMeta(nodelist []nodes.Node) {
r.buffer.WriteString("<meta charset=\"UTF-8\">\n")
for _, node := range nodelist {
switch n := node.(type) {
case *nodes.MetaNode:
r.buffer.WriteString(fmt.Sprintf("<meta name=\"%s\" content=\"%s\">\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("<h%d>%s</h%d>\n",
n.Level(),
html.EscapeString(n.Content()),
n.Level()))
case *nodes.ParagraphNode:
r.buffer.WriteString(fmt.Sprintf("<p>%s</p>\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("<li>%s</li>\n",
html.EscapeString(item.Content())))
}
}
r.buffer.WriteString(fmt.Sprintf("</%s>\n", tag))
case *nodes.LinkNode:
r.buffer.WriteString(fmt.Sprintf("<a href=\"%s\" title=\"%s\">%s</a>",
html.EscapeString(n.URL()),
html.EscapeString(n.Title()),
html.EscapeString(n.Content())))
case *nodes.EmphasisNode:
r.buffer.WriteString(fmt.Sprintf("<em>%s</em>",
html.EscapeString(n.Content())))
case *nodes.StrongNode:
r.buffer.WriteString(fmt.Sprintf("<strong>%s</strong>",
html.EscapeString(n.Content())))
case *nodes.CodeNode:
r.buffer.WriteString(fmt.Sprintf("<pre><code class=\"language-%s\">%s</code></pre>\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("<table>\n")
// Render headers
if len(table.Headers()) > 0 {
r.buffer.WriteString("<thead><tr>\n")
for _, header := range table.Headers() {
r.buffer.WriteString(fmt.Sprintf("<th>%s</th>",
html.EscapeString(header)))
}
r.buffer.WriteString("</tr></thead>\n")
}
// Render rows
r.buffer.WriteString("<tbody>\n")
for _, row := range table.Rows() {
r.buffer.WriteString("<tr>\n")
for _, cell := range row {
r.buffer.WriteString(fmt.Sprintf("<td>%s</td>",
html.EscapeString(cell)))
}
r.buffer.WriteString("</tr>\n")
}
r.buffer.WriteString("</tbody></table>\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("<img src=\"%s\" alt=\"%s\">\n",
html.EscapeString(directive.Arguments()[0]),
html.EscapeString(alt)))
}
case "note":
r.buffer.WriteString(fmt.Sprintf("<div class=\"note\">%s</div>\n",
html.EscapeString(directive.RawContent())))
case "warning":
r.buffer.WriteString(fmt.Sprintf("<div class=\"warning\">%s</div>\n",
html.EscapeString(directive.RawContent())))
}
}

View File

@ -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
}