Basic implementation
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
go-rst
|
13
example/doc.rst
Normal file
13
example/doc.rst
Normal 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
17
example/translations.po
Normal 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
5
go.mod
Normal 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
25
go.sum
Normal 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
81
main.go
Normal 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
16
output.html
Normal 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
248
pkg/nodes/nodes.go
Normal 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
65
pkg/nodes/types.go
Normal 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
25
pkg/parser/context.go
Normal 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
115
pkg/parser/lexer.go
Normal 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
196
pkg/parser/parser.go
Normal 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
23
pkg/parser/patterns.go
Normal 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
150
pkg/renderer/html.go
Normal 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())))
|
||||
}
|
||||
}
|
52
pkg/translator/translator.go
Normal file
52
pkg/translator/translator.go
Normal 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
|
||||
}
|
Reference in New Issue
Block a user