Restructure directory layout: org subpackage
This commit is contained in:
parent
6c683dfbdb
commit
fc982125c9
17 changed files with 11 additions and 7 deletions
47
org/block.go
Normal file
47
org/block.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
package org
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type Block struct {
|
||||
Name string
|
||||
Parameters []string
|
||||
Children []Node
|
||||
}
|
||||
|
||||
var beginBlockRegexp = regexp.MustCompile(`(?i)^(\s*)#\+BEGIN_(\w+)(.*)`)
|
||||
var endBlockRegexp = regexp.MustCompile(`(?i)^(\s*)#\+END_(\w+)`)
|
||||
|
||||
func lexBlock(line string) (token, bool) {
|
||||
if m := beginBlockRegexp.FindStringSubmatch(line); m != nil {
|
||||
return token{"beginBlock", len(m[1]), strings.ToUpper(m[2]), m}, true
|
||||
} else if m := endBlockRegexp.FindStringSubmatch(line); m != nil {
|
||||
return token{"endBlock", len(m[1]), strings.ToUpper(m[2]), m}, true
|
||||
}
|
||||
return nilToken, false
|
||||
}
|
||||
|
||||
func (d *Document) parseBlock(i int, parentStop stopFn) (int, Node) {
|
||||
t, start, nodes := d.tokens[i], i, []Node{}
|
||||
name, parameters := t.content, strings.Fields(t.matches[3])
|
||||
trim := trimIndentUpTo(d.tokens[i].lvl)
|
||||
for i++; !(d.tokens[i].kind == "endBlock" && d.tokens[i].content == name); i++ {
|
||||
if parentStop(d, i) {
|
||||
return 0, nil
|
||||
}
|
||||
nodes = append(nodes, Line{[]Node{Text{trim(d.tokens[i].matches[0])}}})
|
||||
}
|
||||
return i + 1 - start, Block{name, parameters, nodes}
|
||||
}
|
||||
|
||||
func trimIndentUpTo(max int) func(string) string {
|
||||
return func(line string) string {
|
||||
i := 0
|
||||
for ; i < len(line) && i < max && unicode.IsSpace(rune(line[i])); i++ {
|
||||
}
|
||||
return line[i:]
|
||||
}
|
||||
}
|
150
org/document.go
Normal file
150
org/document.go
Normal file
|
@ -0,0 +1,150 @@
|
|||
package org
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
)
|
||||
|
||||
type Document struct {
|
||||
tokens []token
|
||||
Nodes []Node
|
||||
Footnotes Footnotes
|
||||
StatusKeywords []string
|
||||
MaxEmphasisNewLines int
|
||||
BufferSettings map[string]string
|
||||
DefaultSettings map[string]string
|
||||
}
|
||||
|
||||
type Writer interface {
|
||||
before(*Document)
|
||||
after(*Document)
|
||||
writeNodes(...Node)
|
||||
String() string
|
||||
}
|
||||
|
||||
type Node interface{}
|
||||
|
||||
type lexFn = func(line string) (t token, ok bool)
|
||||
type parseFn = func(*Document, int, stopFn) (int, Node)
|
||||
type stopFn = func(*Document, int) bool
|
||||
|
||||
type token struct {
|
||||
kind string
|
||||
lvl int
|
||||
content string
|
||||
matches []string
|
||||
}
|
||||
|
||||
var lexFns = []lexFn{
|
||||
lexHeadline,
|
||||
lexBlock,
|
||||
lexList,
|
||||
lexTable,
|
||||
lexHorizontalRule,
|
||||
lexKeywordOrComment,
|
||||
lexFootnoteDefinition,
|
||||
lexText,
|
||||
}
|
||||
|
||||
var nilToken = token{"nil", -1, "", nil}
|
||||
|
||||
func NewDocument() *Document {
|
||||
return &Document{
|
||||
Footnotes: Footnotes{
|
||||
ExcludeHeading: true,
|
||||
Title: "Footnotes",
|
||||
Definitions: map[string]FootnoteDefinition{},
|
||||
},
|
||||
MaxEmphasisNewLines: 1,
|
||||
BufferSettings: map[string]string{},
|
||||
DefaultSettings: map[string]string{
|
||||
"TODO": "TODO | DONE",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Document) Write(w Writer) Writer {
|
||||
if d.Nodes == nil {
|
||||
panic("cannot Write() empty document: you must call Parse() first")
|
||||
}
|
||||
w.before(d)
|
||||
w.writeNodes(d.Nodes...)
|
||||
w.after(d)
|
||||
return w
|
||||
}
|
||||
|
||||
func (d *Document) Parse(input io.Reader) *Document {
|
||||
d.tokens = []token{}
|
||||
scanner := bufio.NewScanner(input)
|
||||
for scanner.Scan() {
|
||||
d.tokens = append(d.tokens, tokenize(scanner.Text()))
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
_, nodes := d.parseMany(0, func(d *Document, i int) bool { return !(i < len(d.tokens)) })
|
||||
d.Nodes = nodes
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *Document) Get(key string) string {
|
||||
if v, ok := d.BufferSettings[key]; ok {
|
||||
return v
|
||||
}
|
||||
if v, ok := d.DefaultSettings[key]; ok {
|
||||
return v
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (d *Document) parseOne(i int, stop stopFn) (consumed int, node Node) {
|
||||
switch d.tokens[i].kind {
|
||||
case "unorderedList", "orderedList":
|
||||
consumed, node = d.parseList(i, stop)
|
||||
case "tableRow", "tableSeparator":
|
||||
consumed, node = d.parseTable(i, stop)
|
||||
case "beginBlock":
|
||||
consumed, node = d.parseBlock(i, stop)
|
||||
case "text":
|
||||
consumed, node = d.parseParagraph(i, stop)
|
||||
case "horizontalRule":
|
||||
consumed, node = d.parseHorizontalRule(i, stop)
|
||||
case "comment":
|
||||
consumed, node = d.parseComment(i, stop)
|
||||
case "keyword":
|
||||
consumed, node = d.parseKeyword(i, stop)
|
||||
case "headline":
|
||||
consumed, node = d.parseHeadline(i, stop)
|
||||
case "footnoteDefinition":
|
||||
consumed, node = d.parseFootnoteDefinition(i, stop)
|
||||
}
|
||||
|
||||
if consumed != 0 {
|
||||
return consumed, node
|
||||
}
|
||||
log.Printf("Could not parse token %#v: Falling back to treating it as plain text.", d.tokens[i])
|
||||
m := plainTextRegexp.FindStringSubmatch(d.tokens[i].matches[0])
|
||||
d.tokens[i] = token{"text", len(m[1]), m[2], m}
|
||||
return d.parseOne(i, stop)
|
||||
}
|
||||
|
||||
func (d *Document) parseMany(i int, stop stopFn) (int, []Node) {
|
||||
start, nodes := i, []Node{}
|
||||
for i < len(d.tokens) {
|
||||
consumed, node := d.parseOne(i, stop)
|
||||
i += consumed
|
||||
nodes = append(nodes, node)
|
||||
}
|
||||
return i - start, nodes
|
||||
}
|
||||
|
||||
func tokenize(line string) token {
|
||||
for _, lexFn := range lexFns {
|
||||
if token, ok := lexFn(line); ok {
|
||||
return token
|
||||
}
|
||||
}
|
||||
panic(fmt.Sprintf("could not lex line: %s", line))
|
||||
}
|
38
org/footnote.go
Normal file
38
org/footnote.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
package org
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
)
|
||||
|
||||
type Footnotes struct {
|
||||
ExcludeHeading bool
|
||||
Title string
|
||||
Definitions map[string]FootnoteDefinition
|
||||
Order []string
|
||||
}
|
||||
|
||||
type FootnoteDefinition struct {
|
||||
Name string
|
||||
Children []Node
|
||||
}
|
||||
|
||||
var footnoteDefinitionRegexp = regexp.MustCompile(`^\[fn:([\w-]+)\]\s+(.+)`)
|
||||
|
||||
func lexFootnoteDefinition(line string) (token, bool) {
|
||||
if m := footnoteDefinitionRegexp.FindStringSubmatch(line); m != nil {
|
||||
return token{"footnoteDefinition", 0, m[1], m}, true
|
||||
}
|
||||
return nilToken, false
|
||||
}
|
||||
|
||||
func (d *Document) parseFootnoteDefinition(i int, parentStop stopFn) (int, Node) {
|
||||
name := d.tokens[i].content
|
||||
d.tokens[i] = tokenize(d.tokens[i].matches[2])
|
||||
stop := func(d *Document, i int) bool {
|
||||
return parentStop(d, i) || isSecondBlankLine(d, i) ||
|
||||
d.tokens[i].kind == "headline" || d.tokens[i].kind == "footnoteDefinition"
|
||||
}
|
||||
consumed, nodes := d.parseMany(i, stop)
|
||||
d.Footnotes.Definitions[name] = FootnoteDefinition{name, nodes}
|
||||
return consumed, nil
|
||||
}
|
69
org/headline.go
Normal file
69
org/headline.go
Normal file
|
@ -0,0 +1,69 @@
|
|||
package org
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type Headline struct {
|
||||
Lvl int
|
||||
Status string
|
||||
Priority string
|
||||
Title []Node
|
||||
Tags []string
|
||||
Children []Node
|
||||
}
|
||||
|
||||
var headlineRegexp = regexp.MustCompile(`^([*]+)\s+(.*)`)
|
||||
var tagRegexp = regexp.MustCompile(`(.*?)\s*(:[A-Za-z0-9@#%:]+:\s*$)`)
|
||||
|
||||
func lexHeadline(line string) (token, bool) {
|
||||
if m := headlineRegexp.FindStringSubmatch(line); m != nil {
|
||||
return token{"headline", 0, m[2], m}, true
|
||||
}
|
||||
return nilToken, false
|
||||
}
|
||||
|
||||
func (d *Document) todoKeywords() []string {
|
||||
return strings.FieldsFunc(d.Get("TODO"), func(r rune) bool {
|
||||
return unicode.IsSpace(r) || r == '|'
|
||||
})
|
||||
}
|
||||
|
||||
func (d *Document) parseHeadline(i int, parentStop stopFn) (int, Node) {
|
||||
t, headline := d.tokens[i], Headline{}
|
||||
headline.Lvl = len(t.matches[1])
|
||||
text := t.content
|
||||
|
||||
for _, k := range d.todoKeywords() {
|
||||
if strings.HasPrefix(text, k) && len(text) > len(k) && unicode.IsSpace(rune(text[len(k)])) {
|
||||
headline.Status = k
|
||||
text = text[len(k)+1:]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(text) >= 3 && text[0:2] == "[#" && strings.Contains("ABC", text[2:3]) && text[3] == ']' {
|
||||
headline.Priority = text[2:3]
|
||||
text = strings.TrimSpace(text[4:])
|
||||
}
|
||||
|
||||
if m := tagRegexp.FindStringSubmatch(text); m != nil {
|
||||
text = m[1]
|
||||
headline.Tags = strings.FieldsFunc(m[2], func(r rune) bool { return r == ':' })
|
||||
}
|
||||
|
||||
headline.Title = d.parseInline(text)
|
||||
|
||||
stop := func(d *Document, i int) bool {
|
||||
return parentStop(d, i) || d.tokens[i].kind == "headline" && d.tokens[i].lvl <= headline.Lvl
|
||||
}
|
||||
consumed, nodes := d.parseMany(i+1, stop)
|
||||
headline.Children = nodes
|
||||
|
||||
if headline.Lvl == 1 && text == d.Footnotes.Title && d.Footnotes.ExcludeHeading {
|
||||
return consumed + 1, nil
|
||||
}
|
||||
return consumed + 1, headline
|
||||
}
|
262
org/html.go
Normal file
262
org/html.go
Normal file
|
@ -0,0 +1,262 @@
|
|||
package org
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type HTMLWriter struct {
|
||||
stringBuilder
|
||||
HighlightCodeBlock func(source, lang string) string
|
||||
}
|
||||
|
||||
var emphasisTags = map[string][]string{
|
||||
"/": []string{"<em>", "</em>"},
|
||||
"*": []string{"<strong>", "</strong>"},
|
||||
"+": []string{"<del>", "</del>"},
|
||||
"~": []string{"<code>", "</code>"},
|
||||
"=": []string{`<code class="verbatim">`, "</code>"},
|
||||
"_": []string{`<span style="text-decoration: underline;">`, "</span>"},
|
||||
"_{}": []string{"<sub>", "</sub>"},
|
||||
"^{}": []string{"<super>", "</super>"},
|
||||
}
|
||||
|
||||
var listTags = map[string][]string{
|
||||
"+": []string{"<ul>", "</ul>"},
|
||||
"-": []string{"<ul>", "</ul>"},
|
||||
"*": []string{"<ul>", "</ul>"},
|
||||
"number": []string{"<ol>", "</ol>"},
|
||||
"letter": []string{"<ol>", "</ol>"},
|
||||
}
|
||||
|
||||
func NewHTMLWriter() *HTMLWriter {
|
||||
return &HTMLWriter{
|
||||
HighlightCodeBlock: func(source, lang string) string { return html.EscapeString(source) },
|
||||
}
|
||||
}
|
||||
|
||||
func (w *HTMLWriter) emptyClone() *HTMLWriter {
|
||||
wcopy := *w
|
||||
wcopy.stringBuilder = stringBuilder{}
|
||||
return &wcopy
|
||||
}
|
||||
|
||||
func (w *HTMLWriter) before(d *Document) {}
|
||||
|
||||
func (w *HTMLWriter) after(d *Document) {
|
||||
fs := d.Footnotes
|
||||
if len(fs.Definitions) == 0 {
|
||||
return
|
||||
}
|
||||
w.WriteString(`<div id="footnotes">` + "\n")
|
||||
w.WriteString(`<h1 class="footnotes-title">` + fs.Title + `</h1>` + "\n")
|
||||
w.WriteString(`<div class="footnote-definitions">` + "\n")
|
||||
for _, name := range fs.Order {
|
||||
w.writeNodes(fs.Definitions[name])
|
||||
}
|
||||
w.WriteString("</div>\n</div>\n")
|
||||
}
|
||||
|
||||
func (w *HTMLWriter) writeNodes(ns ...Node) {
|
||||
for _, n := range ns {
|
||||
switch n := n.(type) {
|
||||
case Keyword, Comment:
|
||||
continue
|
||||
case Headline:
|
||||
w.writeHeadline(n)
|
||||
case Block:
|
||||
w.writeBlock(n)
|
||||
|
||||
case FootnoteDefinition:
|
||||
w.writeFootnoteDefinition(n)
|
||||
|
||||
case List:
|
||||
w.writeList(n)
|
||||
case ListItem:
|
||||
w.writeListItem(n)
|
||||
|
||||
case Table:
|
||||
w.writeTable(n)
|
||||
case TableHeader:
|
||||
w.writeTableHeader(n)
|
||||
case TableRow:
|
||||
w.writeTableRow(n)
|
||||
case TableSeparator:
|
||||
w.writeTableSeparator(n)
|
||||
|
||||
case Paragraph:
|
||||
w.writeParagraph(n)
|
||||
case HorizontalRule:
|
||||
w.writeHorizontalRule(n)
|
||||
case Line:
|
||||
w.writeLine(n)
|
||||
|
||||
case Text:
|
||||
w.writeText(n)
|
||||
case Emphasis:
|
||||
w.writeEmphasis(n)
|
||||
case Linebreak:
|
||||
w.writeLinebreak(n)
|
||||
case RegularLink:
|
||||
w.writeRegularLink(n)
|
||||
case FootnoteLink:
|
||||
w.writeFootnoteLink(n)
|
||||
default:
|
||||
if n != nil {
|
||||
panic(fmt.Sprintf("bad node %#v", n))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *HTMLWriter) writeLines(lines []Node) {
|
||||
for i, line := range lines {
|
||||
w.writeNodes(line)
|
||||
if i != len(lines)-1 && line.(Line).Children != nil {
|
||||
w.WriteString(" ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *HTMLWriter) writeBlock(b Block) {
|
||||
w.WriteString("<code>")
|
||||
lang := ""
|
||||
if len(b.Parameters) >= 1 {
|
||||
lang = b.Parameters[0]
|
||||
}
|
||||
lines := []string{}
|
||||
for _, n := range b.Children {
|
||||
lines = append(lines, n.(Line).Children[0].(Text).Content)
|
||||
}
|
||||
w.WriteString(w.HighlightCodeBlock(strings.Join(lines, "\n"), lang))
|
||||
w.WriteString("</code>\n")
|
||||
}
|
||||
|
||||
func (w *HTMLWriter) writeFootnoteDefinition(f FootnoteDefinition) {
|
||||
w.WriteString(`<div class="footnote-definition">` + "\n")
|
||||
w.WriteString(fmt.Sprintf(`<sup id="footnote-%s">%s</sup>`, f.Name, f.Name) + "\n")
|
||||
w.writeNodes(f.Children...)
|
||||
w.WriteString("</div>\n")
|
||||
}
|
||||
|
||||
func (w *HTMLWriter) writeHeadline(h Headline) {
|
||||
w.WriteString(fmt.Sprintf("<h%d>", h.Lvl))
|
||||
w.writeNodes(h.Title...)
|
||||
w.WriteString(fmt.Sprintf("</h%d>\n", h.Lvl))
|
||||
w.writeNodes(h.Children...)
|
||||
}
|
||||
|
||||
func (w *HTMLWriter) writeText(t Text) {
|
||||
w.WriteString(html.EscapeString(t.Content))
|
||||
}
|
||||
|
||||
func (w *HTMLWriter) writeEmphasis(e Emphasis) {
|
||||
tags, ok := emphasisTags[e.Kind]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("bad emphasis %#v", e))
|
||||
}
|
||||
w.WriteString(tags[0])
|
||||
w.writeNodes(e.Content...)
|
||||
w.WriteString(tags[1])
|
||||
}
|
||||
|
||||
func (w *HTMLWriter) writeLinebreak(l Linebreak) {
|
||||
w.WriteString("<br>\n")
|
||||
}
|
||||
|
||||
func (w *HTMLWriter) writeFootnoteLink(l FootnoteLink) {
|
||||
name := html.EscapeString(l.Name)
|
||||
w.WriteString(fmt.Sprintf(`<sup class="footnote-reference"><a href="#footnote-%s">%s</a></sup>`, name, name))
|
||||
|
||||
}
|
||||
|
||||
func (w *HTMLWriter) writeRegularLink(l RegularLink) {
|
||||
url := html.EscapeString(l.URL)
|
||||
descriptionWriter := w.emptyClone()
|
||||
descriptionWriter.writeNodes(l.Description...)
|
||||
description := descriptionWriter.String()
|
||||
switch l.Protocol {
|
||||
case "file": // TODO
|
||||
url = url[len("file:"):]
|
||||
if strings.Contains(".png.jpg.jpeg.gif", path.Ext(l.URL)) {
|
||||
w.WriteString(fmt.Sprintf(`<img src="%s" alt="%s" title="%s" />`, url, description, description))
|
||||
} else {
|
||||
w.WriteString(fmt.Sprintf(`<a href="%s">%s</a>`, url, description))
|
||||
}
|
||||
default:
|
||||
w.WriteString(fmt.Sprintf(`<a href="%s">%s</a>`, url, description))
|
||||
}
|
||||
}
|
||||
|
||||
func (w *HTMLWriter) writeList(l List) {
|
||||
tags, ok := listTags[l.Kind]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("bad list kind %#v", l))
|
||||
}
|
||||
w.WriteString(tags[0] + "\n")
|
||||
w.writeNodes(l.Items...)
|
||||
w.WriteString(tags[1] + "\n")
|
||||
}
|
||||
|
||||
func (w *HTMLWriter) writeListItem(li ListItem) {
|
||||
w.WriteString("<li>")
|
||||
if len(li.Children) == 1 {
|
||||
if p, ok := li.Children[0].(Paragraph); ok {
|
||||
w.writeLines(p.Children)
|
||||
}
|
||||
} else {
|
||||
w.writeNodes(li.Children...)
|
||||
}
|
||||
w.WriteString("</li>\n")
|
||||
}
|
||||
|
||||
func (w *HTMLWriter) writeLine(l Line) {
|
||||
w.writeNodes(l.Children...)
|
||||
}
|
||||
|
||||
func (w *HTMLWriter) writeParagraph(p Paragraph) {
|
||||
if len(p.Children) == 1 && p.Children[0].(Line).Children == nil {
|
||||
return
|
||||
}
|
||||
w.WriteString("<p>")
|
||||
w.writeLines(p.Children)
|
||||
w.WriteString("</p>\n")
|
||||
}
|
||||
|
||||
func (w *HTMLWriter) writeHorizontalRule(h HorizontalRule) {
|
||||
w.WriteString("<hr>\n")
|
||||
}
|
||||
|
||||
func (w *HTMLWriter) writeTable(t Table) {
|
||||
w.WriteString("<table>")
|
||||
w.writeNodes(t.Header)
|
||||
w.WriteString("<tbody>")
|
||||
w.writeNodes(t.Rows...)
|
||||
w.WriteString("</tbody>\n</table>\n")
|
||||
}
|
||||
|
||||
func (w *HTMLWriter) writeTableRow(t TableRow) {
|
||||
w.WriteString("\n<tr>\n")
|
||||
for _, column := range t.Columns {
|
||||
w.WriteString("<td>")
|
||||
w.writeNodes(column...)
|
||||
w.WriteString("</td>")
|
||||
}
|
||||
w.WriteString("\n</tr>\n")
|
||||
}
|
||||
|
||||
func (w *HTMLWriter) writeTableHeader(t TableHeader) {
|
||||
w.WriteString("\n<thead>\n")
|
||||
for _, column := range t.Columns {
|
||||
w.WriteString("<th>")
|
||||
w.writeNodes(column...)
|
||||
w.WriteString("</th>")
|
||||
}
|
||||
w.WriteString("\n</thead>\n")
|
||||
}
|
||||
|
||||
func (w *HTMLWriter) writeTableSeparator(t TableSeparator) {
|
||||
w.WriteString("\n<tr></tr>\n")
|
||||
}
|
20
org/html_test.go
Normal file
20
org/html_test.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
package org
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHTMLWriter(t *testing.T) {
|
||||
for _, path := range orgTestFiles() {
|
||||
reader, writer := strings.NewReader(fileString(path)), NewHTMLWriter()
|
||||
actual := NewDocument().Parse(reader).Write(writer).String()
|
||||
expected := fileString(path[:len(path)-len(".org")] + ".html")
|
||||
|
||||
if expected != actual {
|
||||
t.Errorf("%s:\n%s'", path, diff(actual, expected))
|
||||
} else {
|
||||
t.Logf("%s: passed!", path)
|
||||
}
|
||||
}
|
||||
}
|
184
org/inline.go
Normal file
184
org/inline.go
Normal file
|
@ -0,0 +1,184 @@
|
|||
package org
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type Text struct{ Content string }
|
||||
|
||||
type Linebreak struct{}
|
||||
|
||||
type Emphasis struct {
|
||||
Kind string
|
||||
Content []Node
|
||||
}
|
||||
|
||||
type FootnoteLink struct{ Name string }
|
||||
|
||||
type RegularLink struct {
|
||||
Protocol string
|
||||
Description []Node
|
||||
URL string
|
||||
}
|
||||
|
||||
var redundantSpaces = regexp.MustCompile("[ \t]+")
|
||||
var subScriptSuperScriptRegexp = regexp.MustCompile(`([_^])\{(.*?)\}`)
|
||||
var footnoteRegexp = regexp.MustCompile(`\[fn:([\w-]+?)(:(.*?))?\]`)
|
||||
|
||||
func (d *Document) parseInline(input string) (nodes []Node) {
|
||||
previous, current := 0, 0
|
||||
for current < len(input) {
|
||||
consumed, node := 0, (Node)(nil)
|
||||
switch input[current] {
|
||||
case '^':
|
||||
consumed, node = d.parseSubOrSuperScript(input, current)
|
||||
case '_':
|
||||
consumed, node = d.parseSubScriptOrEmphasis(input, current)
|
||||
case '*', '/', '=', '~', '+':
|
||||
consumed, node = d.parseEmphasis(input, current)
|
||||
case '[':
|
||||
consumed, node = d.parseRegularLinkOrFootnoteReference(input, current)
|
||||
case '\\':
|
||||
consumed, node = d.parseExplicitLineBreak(input, current)
|
||||
}
|
||||
if consumed != 0 {
|
||||
if current > previous {
|
||||
nodes = append(nodes, Text{input[previous:current]})
|
||||
}
|
||||
if node != nil {
|
||||
nodes = append(nodes, node)
|
||||
}
|
||||
current += consumed
|
||||
previous = current
|
||||
} else {
|
||||
current++
|
||||
}
|
||||
}
|
||||
|
||||
if previous < len(input) {
|
||||
nodes = append(nodes, Text{input[previous:]})
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
func (d *Document) parseExplicitLineBreak(input string, start int) (int, Node) {
|
||||
if start == 0 || input[start-1] == '\n' || start+1 >= len(input) || input[start+1] != '\\' {
|
||||
return 0, nil
|
||||
}
|
||||
for i := start + 1; ; i++ {
|
||||
if i == len(input)-1 || input[i] == '\n' {
|
||||
return i + 1 - start, Linebreak{}
|
||||
}
|
||||
if !unicode.IsSpace(rune(input[i])) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (d *Document) parseSubOrSuperScript(input string, start int) (int, Node) {
|
||||
if m := subScriptSuperScriptRegexp.FindStringSubmatch(input[start:]); m != nil {
|
||||
return len(m[2]) + 3, Emphasis{m[1] + "{}", []Node{Text{m[2]}}}
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (d *Document) parseSubScriptOrEmphasis(input string, start int) (int, Node) {
|
||||
if consumed, node := d.parseSubOrSuperScript(input, start); consumed != 0 {
|
||||
return consumed, node
|
||||
}
|
||||
return d.parseEmphasis(input, start)
|
||||
}
|
||||
|
||||
func (d *Document) parseRegularLinkOrFootnoteReference(input string, start int) (int, Node) {
|
||||
if len(input[start:]) >= 2 && input[start] == '[' && input[start+1] == '[' {
|
||||
return d.parseRegularLink(input, start)
|
||||
} else if len(input[start:]) >= 1 && input[start] == '[' {
|
||||
return d.parseFootnoteReference(input, start)
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (d *Document) parseFootnoteReference(input string, start int) (int, Node) {
|
||||
if m := footnoteRegexp.FindStringSubmatch(input[start:]); m != nil {
|
||||
name, definition := m[1], m[3]
|
||||
seen := false
|
||||
for _, otherName := range d.Footnotes.Order {
|
||||
if name == otherName {
|
||||
seen = true
|
||||
}
|
||||
}
|
||||
if !seen {
|
||||
d.Footnotes.Order = append(d.Footnotes.Order, name)
|
||||
}
|
||||
if definition != "" {
|
||||
d.Footnotes.Definitions[name] = FootnoteDefinition{name, d.parseInline(definition)}
|
||||
}
|
||||
return len(m[0]), FootnoteLink{name}
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (d *Document) parseRegularLink(input string, start int) (int, Node) {
|
||||
if len(input[start:]) == 0 || input[start+1] != '[' {
|
||||
return 0, nil
|
||||
}
|
||||
input = input[start:]
|
||||
end := strings.Index(input, "]]")
|
||||
if end == -1 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
rawLink := input[2:end]
|
||||
link, description, parts := "", []Node{}, strings.Split(rawLink, "][")
|
||||
if len(parts) == 2 {
|
||||
link, description = parts[0], d.parseInline(parts[1])
|
||||
} else {
|
||||
link, description = rawLink, []Node{Text{rawLink}}
|
||||
}
|
||||
consumed := end + 2
|
||||
protocol, parts := "", strings.SplitN(link, ":", 2)
|
||||
if len(parts) == 2 {
|
||||
protocol = parts[0]
|
||||
}
|
||||
return consumed, RegularLink{protocol, description, link}
|
||||
}
|
||||
|
||||
func (d *Document) parseEmphasis(input string, start int) (int, Node) {
|
||||
marker, i := input[start], start
|
||||
if !hasValidPreAndBorderChars(input, i) {
|
||||
return 0, nil
|
||||
}
|
||||
for i, consumedNewLines := i+1, 0; i < len(input) && consumedNewLines <= d.MaxEmphasisNewLines; i++ {
|
||||
if input[i] == '\n' {
|
||||
consumedNewLines++
|
||||
}
|
||||
|
||||
if input[i] == marker && i != start+1 && hasValidPostAndBorderChars(input, i) {
|
||||
return i + 1 - start, Emphasis{input[start : start+1], d.parseInline(input[start+1 : i])}
|
||||
}
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// see org-emphasis-regexp-components (emacs elisp variable)
|
||||
|
||||
func hasValidPreAndBorderChars(input string, i int) bool {
|
||||
return (i+1 >= len(input) || isValidBorderChar(rune(input[i+1]))) && (i == 0 || isValidPreChar(rune(input[i-1])))
|
||||
}
|
||||
|
||||
func hasValidPostAndBorderChars(input string, i int) bool {
|
||||
return (i == 0 || isValidBorderChar(rune(input[i-1]))) && (i+1 >= len(input) || isValidPostChar(rune(input[i+1])))
|
||||
}
|
||||
|
||||
func isValidPreChar(r rune) bool {
|
||||
return unicode.IsSpace(r) || strings.ContainsRune(`-({'"`, r)
|
||||
}
|
||||
|
||||
func isValidPostChar(r rune) bool {
|
||||
return unicode.IsSpace(r) || strings.ContainsRune(`-.,:!?;'")}[`, r)
|
||||
}
|
||||
|
||||
func isValidBorderChar(r rune) bool { return !unicode.IsSpace(r) }
|
36
org/keyword.go
Normal file
36
org/keyword.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package org
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Keyword struct {
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
type Comment struct{ Content string }
|
||||
|
||||
var keywordRegexp = regexp.MustCompile(`^(\s*)#\+([^:]+):\s(.*)`)
|
||||
var commentRegexp = regexp.MustCompile(`^(\s*)#(.*)`)
|
||||
|
||||
func lexKeywordOrComment(line string) (token, bool) {
|
||||
if m := keywordRegexp.FindStringSubmatch(line); m != nil {
|
||||
return token{"keyword", len(m[1]), m[2], m}, true
|
||||
} else if m := commentRegexp.FindStringSubmatch(line); m != nil {
|
||||
return token{"comment", len(m[1]), m[2], m}, true
|
||||
}
|
||||
return nilToken, false
|
||||
}
|
||||
|
||||
func (d *Document) parseKeyword(i int, stop stopFn) (int, Node) {
|
||||
t := d.tokens[i]
|
||||
k, v := t.matches[2], t.matches[3]
|
||||
d.BufferSettings[k] = strings.Join([]string{d.BufferSettings[k], v}, "\n")
|
||||
return 1, Keyword{k, v}
|
||||
}
|
||||
|
||||
func (d *Document) parseComment(i int, stop stopFn) (int, Node) {
|
||||
return 1, Comment{d.tokens[i].content}
|
||||
}
|
82
org/list.go
Normal file
82
org/list.go
Normal file
|
@ -0,0 +1,82 @@
|
|||
package org
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type List struct {
|
||||
Kind string
|
||||
Items []Node
|
||||
}
|
||||
|
||||
type ListItem struct {
|
||||
Bullet string
|
||||
Children []Node
|
||||
}
|
||||
|
||||
var unorderedListRegexp = regexp.MustCompile(`^(\s*)([-]|[+]|[*])\s(.*)`)
|
||||
var orderedListRegexp = regexp.MustCompile(`^(\s*)(([0-9]+|[a-zA-Z])[.)])\s+(.*)`)
|
||||
|
||||
func lexList(line string) (token, bool) {
|
||||
if m := unorderedListRegexp.FindStringSubmatch(line); m != nil {
|
||||
return token{"unorderedList", len(m[1]), m[3], m}, true
|
||||
} else if m := orderedListRegexp.FindStringSubmatch(line); m != nil {
|
||||
return token{"orderedList", len(m[1]), m[4], m}, true
|
||||
}
|
||||
return nilToken, false
|
||||
}
|
||||
|
||||
func isListToken(t token) bool {
|
||||
return t.kind == "unorderedList" || t.kind == "orderedList"
|
||||
}
|
||||
|
||||
func stopIndentBelow(t token, minIndent int) bool {
|
||||
return t.lvl < minIndent && !(t.kind == "text" && t.content == "")
|
||||
}
|
||||
|
||||
func listKind(t token) string {
|
||||
switch bullet := t.matches[2]; {
|
||||
case bullet == "*" || bullet == "+" || bullet == "-":
|
||||
return bullet
|
||||
case unicode.IsLetter(rune(bullet[0])):
|
||||
return "letter"
|
||||
case unicode.IsDigit(rune(bullet[0])):
|
||||
return "number"
|
||||
default:
|
||||
panic(fmt.Sprintf("bad list bullet '%s': %#v", bullet, t))
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Document) parseList(i int, parentStop stopFn) (int, Node) {
|
||||
start, lvl := i, d.tokens[i].lvl
|
||||
|
||||
list := List{Kind: listKind(d.tokens[i])}
|
||||
for !parentStop(d, i) && d.tokens[i].lvl == lvl && isListToken(d.tokens[i]) {
|
||||
consumed, node := d.parseListItem(i, parentStop)
|
||||
i += consumed
|
||||
list.Items = append(list.Items, node)
|
||||
}
|
||||
return i - start, list
|
||||
}
|
||||
|
||||
func (d *Document) parseListItem(i int, parentStop stopFn) (int, Node) {
|
||||
start, nodes, bullet := i, []Node{}, d.tokens[i].matches[2]
|
||||
minIndent := d.tokens[i].lvl + len(bullet)
|
||||
d.tokens[i] = tokenize(strings.Repeat(" ", minIndent) + d.tokens[i].content)
|
||||
stop := func(d *Document, i int) bool {
|
||||
if parentStop(d, i) {
|
||||
return true
|
||||
}
|
||||
t := d.tokens[i]
|
||||
return t.lvl < minIndent && !(t.kind == "text" && t.content == "")
|
||||
}
|
||||
for !stop(d, i) && !isSecondBlankLine(d, i) {
|
||||
consumed, node := d.parseOne(i, stop)
|
||||
i += consumed
|
||||
nodes = append(nodes, node)
|
||||
}
|
||||
return i - start, ListItem{bullet, nodes}
|
||||
}
|
243
org/org.go
Normal file
243
org/org.go
Normal file
|
@ -0,0 +1,243 @@
|
|||
package org
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type stringBuilder = strings.Builder
|
||||
|
||||
type OrgWriter struct {
|
||||
TagsColumn int // see org-tags-column
|
||||
stringBuilder
|
||||
indent string
|
||||
}
|
||||
|
||||
var emphasisOrgBorders = map[string][]string{
|
||||
"_": []string{"_", "_"},
|
||||
"*": []string{"*", "*"},
|
||||
"/": []string{"/", "/"},
|
||||
"+": []string{"+", "+"},
|
||||
"~": []string{"~", "~"},
|
||||
"=": []string{"=", "="},
|
||||
"_{}": []string{"_{", "}"},
|
||||
"^{}": []string{"^{", "}"},
|
||||
}
|
||||
|
||||
func NewOrgWriter() *OrgWriter {
|
||||
return &OrgWriter{
|
||||
TagsColumn: 77,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *OrgWriter) before(d *Document) {}
|
||||
func (w *OrgWriter) after(d *Document) {
|
||||
fs := d.Footnotes
|
||||
if len(fs.Definitions) == 0 {
|
||||
return
|
||||
}
|
||||
w.WriteString("* " + fs.Title + "\n")
|
||||
for _, name := range fs.Order {
|
||||
w.writeNodes(fs.Definitions[name])
|
||||
}
|
||||
}
|
||||
|
||||
func (w *OrgWriter) emptyClone() *OrgWriter {
|
||||
wcopy := *w
|
||||
wcopy.stringBuilder = strings.Builder{}
|
||||
return &wcopy
|
||||
}
|
||||
|
||||
func (w *OrgWriter) writeNodes(ns ...Node) {
|
||||
for _, n := range ns {
|
||||
switch n := n.(type) {
|
||||
case Comment:
|
||||
w.writeComment(n)
|
||||
case Keyword:
|
||||
w.writeKeyword(n)
|
||||
case Headline:
|
||||
w.writeHeadline(n)
|
||||
case Block:
|
||||
w.writeBlock(n)
|
||||
|
||||
case FootnoteDefinition:
|
||||
w.writeFootnoteDefinition(n)
|
||||
|
||||
case List:
|
||||
w.writeList(n)
|
||||
case ListItem:
|
||||
w.writeListItem(n)
|
||||
|
||||
case Table:
|
||||
w.writeTable(n)
|
||||
case TableHeader:
|
||||
w.writeTableHeader(n)
|
||||
case TableRow:
|
||||
w.writeTableRow(n)
|
||||
case TableSeparator:
|
||||
w.writeTableSeparator(n)
|
||||
|
||||
case Paragraph:
|
||||
w.writeParagraph(n)
|
||||
case HorizontalRule:
|
||||
w.writeHorizontalRule(n)
|
||||
case Line:
|
||||
w.writeLine(n)
|
||||
|
||||
case Text:
|
||||
w.writeText(n)
|
||||
case Emphasis:
|
||||
w.writeEmphasis(n)
|
||||
case Linebreak:
|
||||
w.writeLinebreak(n)
|
||||
case RegularLink:
|
||||
w.writeRegularLink(n)
|
||||
case FootnoteLink:
|
||||
w.writeFootnoteLink(n)
|
||||
default:
|
||||
if n != nil {
|
||||
panic(fmt.Sprintf("bad node %#v", n))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var eolWhiteSpaceRegexp = regexp.MustCompile("[\t ]*\n")
|
||||
|
||||
func (w *OrgWriter) String() string {
|
||||
s := w.stringBuilder.String()
|
||||
return eolWhiteSpaceRegexp.ReplaceAllString(s, "\n")
|
||||
}
|
||||
|
||||
func (w *OrgWriter) writeHeadline(h Headline) {
|
||||
tmp := w.emptyClone()
|
||||
tmp.WriteString(strings.Repeat("*", h.Lvl))
|
||||
if h.Status != "" {
|
||||
tmp.WriteString(" " + h.Status)
|
||||
}
|
||||
if h.Priority != "" {
|
||||
tmp.WriteString(" [#" + h.Priority + "]")
|
||||
}
|
||||
tmp.WriteString(" ")
|
||||
tmp.writeNodes(h.Title...)
|
||||
hString := tmp.String()
|
||||
if len(h.Tags) != 0 {
|
||||
hString += " "
|
||||
tString := ":" + strings.Join(h.Tags, ":") + ":"
|
||||
if n := w.TagsColumn - len(tString) - len(hString); n > 0 {
|
||||
w.WriteString(hString + strings.Repeat(" ", n) + tString)
|
||||
} else {
|
||||
w.WriteString(hString + tString)
|
||||
}
|
||||
} else {
|
||||
w.WriteString(hString)
|
||||
}
|
||||
w.WriteString("\n")
|
||||
if len(h.Children) != 0 {
|
||||
w.WriteString(w.indent)
|
||||
}
|
||||
w.writeNodes(h.Children...)
|
||||
}
|
||||
|
||||
func (w *OrgWriter) writeBlock(b Block) {
|
||||
w.WriteString(fmt.Sprintf("%s#+BEGIN_%s %s\n", w.indent, b.Name, strings.Join(b.Parameters, " ")))
|
||||
w.writeNodes(b.Children...)
|
||||
w.WriteString(w.indent + "#+END_" + b.Name + "\n")
|
||||
}
|
||||
|
||||
func (w *OrgWriter) writeFootnoteDefinition(f FootnoteDefinition) {
|
||||
w.WriteString(fmt.Sprintf("[fn:%s] ", f.Name))
|
||||
w.writeNodes(f.Children...)
|
||||
}
|
||||
|
||||
func (w *OrgWriter) writeParagraph(p Paragraph) {
|
||||
w.writeNodes(p.Children...)
|
||||
}
|
||||
|
||||
func (w *OrgWriter) writeKeyword(k Keyword) {
|
||||
w.WriteString(w.indent + fmt.Sprintf("#+%s: %s\n", k.Key, k.Value))
|
||||
}
|
||||
|
||||
func (w *OrgWriter) writeComment(c Comment) {
|
||||
w.WriteString(w.indent + "#" + c.Content)
|
||||
}
|
||||
|
||||
func (w *OrgWriter) writeList(l List) { w.writeNodes(l.Items...) }
|
||||
|
||||
func (w *OrgWriter) writeListItem(li ListItem) {
|
||||
w.WriteString(w.indent + li.Bullet + " ")
|
||||
liWriter := w.emptyClone()
|
||||
liWriter.indent = w.indent + strings.Repeat(" ", len(li.Bullet)+1)
|
||||
liWriter.writeNodes(li.Children...)
|
||||
w.WriteString(strings.TrimPrefix(liWriter.String(), liWriter.indent))
|
||||
}
|
||||
|
||||
func (w *OrgWriter) writeTable(t Table) {
|
||||
// TODO: pretty print tables
|
||||
w.writeNodes(t.Header)
|
||||
w.writeNodes(t.Rows...)
|
||||
}
|
||||
|
||||
func (w *OrgWriter) writeTableHeader(th TableHeader) {
|
||||
w.writeTableColumns(th.Columns)
|
||||
w.writeNodes(th.Separator)
|
||||
}
|
||||
|
||||
func (w *OrgWriter) writeTableRow(tr TableRow) {
|
||||
w.writeTableColumns(tr.Columns)
|
||||
}
|
||||
|
||||
func (w *OrgWriter) writeTableSeparator(ts TableSeparator) {
|
||||
w.WriteString(w.indent + ts.Content + "\n")
|
||||
}
|
||||
|
||||
func (w *OrgWriter) writeTableColumns(columns [][]Node) {
|
||||
w.WriteString(w.indent + "| ")
|
||||
for _, columnNodes := range columns {
|
||||
w.writeNodes(columnNodes...)
|
||||
w.WriteString(" | ")
|
||||
}
|
||||
w.WriteString("\n")
|
||||
}
|
||||
|
||||
func (w *OrgWriter) writeHorizontalRule(hr HorizontalRule) {
|
||||
w.WriteString(w.indent + "-----\n")
|
||||
}
|
||||
|
||||
func (w *OrgWriter) writeLine(l Line) {
|
||||
w.WriteString(w.indent)
|
||||
w.writeNodes(l.Children...)
|
||||
w.WriteString("\n")
|
||||
}
|
||||
|
||||
func (w *OrgWriter) writeText(t Text) { w.WriteString(t.Content) }
|
||||
|
||||
func (w *OrgWriter) writeEmphasis(e Emphasis) {
|
||||
borders, ok := emphasisOrgBorders[e.Kind]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("bad emphasis %#v", e))
|
||||
}
|
||||
w.WriteString(borders[0])
|
||||
w.writeNodes(e.Content...)
|
||||
w.WriteString(borders[1])
|
||||
}
|
||||
|
||||
func (w *OrgWriter) writeLinebreak(l Linebreak) {
|
||||
w.WriteString(`\\`)
|
||||
}
|
||||
|
||||
func (w *OrgWriter) writeFootnoteLink(l FootnoteLink) {
|
||||
w.WriteString("[fn:" + l.Name + "]")
|
||||
}
|
||||
|
||||
func (w *OrgWriter) writeRegularLink(l RegularLink) {
|
||||
descriptionWriter := w.emptyClone()
|
||||
descriptionWriter.writeNodes(l.Description...)
|
||||
description := descriptionWriter.String()
|
||||
if l.URL != description {
|
||||
w.WriteString(fmt.Sprintf("[[%s][%s]]", l.URL, description))
|
||||
} else {
|
||||
w.WriteString(fmt.Sprintf("[[%s]]", l.URL))
|
||||
}
|
||||
}
|
61
org/org_test.go
Normal file
61
org/org_test.go
Normal file
|
@ -0,0 +1,61 @@
|
|||
package org
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/pmezard/go-difflib/difflib"
|
||||
)
|
||||
|
||||
func TestOrgWriter(t *testing.T) {
|
||||
for _, path := range orgTestFiles() {
|
||||
expected := fileString(path)
|
||||
reader, writer := strings.NewReader(expected), NewOrgWriter()
|
||||
actual := NewDocument().Parse(reader).Write(writer).String()
|
||||
if actual != expected {
|
||||
t.Errorf("%s:\n%s'", path, diff(actual, expected))
|
||||
} else {
|
||||
t.Logf("%s: passed!", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func orgTestFiles() []string {
|
||||
dir := "./testdata"
|
||||
files, err := ioutil.ReadDir(dir)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Could not read directory: %s", err))
|
||||
}
|
||||
orgFiles := []string{}
|
||||
for _, f := range files {
|
||||
name := f.Name()
|
||||
if filepath.Ext(name) != ".org" {
|
||||
continue
|
||||
}
|
||||
orgFiles = append(orgFiles, filepath.Join(dir, name))
|
||||
}
|
||||
return orgFiles
|
||||
}
|
||||
|
||||
func fileString(path string) string {
|
||||
bs, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Could not read file %s: %s", path, err))
|
||||
}
|
||||
return string(bs)
|
||||
}
|
||||
|
||||
func diff(actual, expected string) string {
|
||||
diff := difflib.UnifiedDiff{
|
||||
A: difflib.SplitLines(actual),
|
||||
B: difflib.SplitLines(expected),
|
||||
FromFile: "Actual",
|
||||
ToFile: "Expected",
|
||||
Context: 3,
|
||||
}
|
||||
text, _ := difflib.GetUnifiedDiffString(diff)
|
||||
return text
|
||||
}
|
57
org/paragraph.go
Normal file
57
org/paragraph.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
package org
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
)
|
||||
|
||||
type Line struct{ Children []Node }
|
||||
type Paragraph struct{ Children []Node }
|
||||
type HorizontalRule struct{}
|
||||
|
||||
var horizontalRuleRegexp = regexp.MustCompile(`^(\s*)-{5,}\s*$`)
|
||||
var plainTextRegexp = regexp.MustCompile(`^(\s*)(.*)`)
|
||||
|
||||
func lexText(line string) (token, bool) {
|
||||
if m := plainTextRegexp.FindStringSubmatch(line); m != nil {
|
||||
return token{"text", len(m[1]), m[2], m}, true
|
||||
}
|
||||
return nilToken, false
|
||||
}
|
||||
|
||||
func lexHorizontalRule(line string) (token, bool) {
|
||||
if m := horizontalRuleRegexp.FindStringSubmatch(line); m != nil {
|
||||
return token{"horizontalRule", len(m[1]), "", m}, true
|
||||
}
|
||||
return nilToken, false
|
||||
}
|
||||
|
||||
func isSecondBlankLine(d *Document, i int) bool {
|
||||
if i-1 <= 0 {
|
||||
return false
|
||||
}
|
||||
t1, t2 := d.tokens[i-1], d.tokens[i]
|
||||
if t1.kind == "text" && t2.kind == "text" && t1.content == "" && t2.content == "" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (d *Document) parseParagraph(i int, parentStop stopFn) (int, Node) {
|
||||
lines, start := []Node{Line{d.parseInline(d.tokens[i].content)}}, i
|
||||
i++
|
||||
stop := func(d *Document, i int) bool { return parentStop(d, i) || d.tokens[i].kind != "text" }
|
||||
for ; !stop(d, i) && !isSecondBlankLine(d, i); i++ {
|
||||
if isSecondBlankLine(d, i) {
|
||||
lines = lines[:len(lines)-1]
|
||||
i++
|
||||
break
|
||||
}
|
||||
lines = append(lines, Line{d.parseInline(d.tokens[i].content)})
|
||||
}
|
||||
consumed := i - start
|
||||
return consumed, Paragraph{lines}
|
||||
}
|
||||
|
||||
func (d *Document) parseHorizontalRule(i int, parentStop stopFn) (int, Node) {
|
||||
return 1, HorizontalRule{}
|
||||
}
|
63
org/table.go
Normal file
63
org/table.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
package org
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Table struct {
|
||||
Header Node
|
||||
Rows []Node
|
||||
}
|
||||
|
||||
type TableSeparator struct{ Content string }
|
||||
|
||||
type TableHeader struct {
|
||||
Columns [][]Node
|
||||
Separator TableSeparator
|
||||
}
|
||||
|
||||
type TableRow struct{ Columns [][]Node }
|
||||
|
||||
var tableSeparatorRegexp = regexp.MustCompile(`^(\s*)(\|[+-|]*)\s*$`)
|
||||
var tableRowRegexp = regexp.MustCompile(`^(\s*)(\|.*)`)
|
||||
|
||||
func lexTable(line string) (token, bool) {
|
||||
if m := tableSeparatorRegexp.FindStringSubmatch(line); m != nil {
|
||||
return token{"tableSeparator", len(m[1]), m[2], m}, true
|
||||
} else if m := tableRowRegexp.FindStringSubmatch(line); m != nil {
|
||||
return token{"tableRow", len(m[1]), m[2], m}, true
|
||||
}
|
||||
return nilToken, false
|
||||
}
|
||||
|
||||
func (d *Document) parseTable(i int, parentStop stopFn) (int, Node) {
|
||||
rows, start := []Node{}, i
|
||||
for !parentStop(d, i) && (d.tokens[i].kind == "tableRow" || d.tokens[i].kind == "tableSeparator") {
|
||||
consumed, row := d.parseTableRowOrSeparator(i, parentStop)
|
||||
i += consumed
|
||||
rows = append(rows, row)
|
||||
}
|
||||
|
||||
consumed := i - start
|
||||
if len(rows) >= 2 {
|
||||
if row, ok := rows[0].(TableRow); ok {
|
||||
if separator, ok := rows[1].(TableSeparator); ok {
|
||||
return consumed, Table{TableHeader{row.Columns, separator}, rows[2:]}
|
||||
}
|
||||
}
|
||||
}
|
||||
return consumed, Table{nil, rows}
|
||||
}
|
||||
|
||||
func (d *Document) parseTableRowOrSeparator(i int, _ stopFn) (int, Node) {
|
||||
if d.tokens[i].kind == "tableSeparator" {
|
||||
return 1, TableSeparator{d.tokens[i].content}
|
||||
}
|
||||
fields := strings.FieldsFunc(d.tokens[i].content, func(r rune) bool { return r == '|' })
|
||||
row := TableRow{}
|
||||
for _, field := range fields {
|
||||
row.Columns = append(row.Columns, d.parseInline(strings.TrimSpace(field)))
|
||||
}
|
||||
return 1, row
|
||||
}
|
78
org/testdata/example.html
vendored
Normal file
78
org/testdata/example.html
vendored
Normal file
|
@ -0,0 +1,78 @@
|
|||
<h1>Motivation</h1>
|
||||
<p>To validate the parser we'll try printing the AST back to org-mode source - if that works we can be kind of sure that the parsing worked. At least I hope so - I would like to get around writing tests for the individual parsing functions... </p>
|
||||
<h2>Headlines with TODO status, priority & tags</h2>
|
||||
<h3>Headline with todo status & priority</h3>
|
||||
<h3>Headline with TODO status</h3>
|
||||
<h3>Headline with tags & priority</h3>
|
||||
<p>this one is cheating a little as tags are ALWAYS printed right aligned to a given column number...</p>
|
||||
<h2>Lists</h2>
|
||||
<ul>
|
||||
<li>unordered list item 1</li>
|
||||
<li><p>unordered list item 2 - with <code>inline</code> <em>markup</em></p>
|
||||
<ol>
|
||||
<li><p>ordered sublist item 1</p>
|
||||
<ol>
|
||||
<li>ordered sublist item 1</li>
|
||||
<li>ordered sublist item 2</li>
|
||||
<li>ordered sublist item 3</li>
|
||||
</ol>
|
||||
</li>
|
||||
<li>ordered sublist item 2</li>
|
||||
</ol>
|
||||
</li>
|
||||
<li><p>unordered list item 3 - and a <a href="https://example.com">link</a> and some lines of text</p>
|
||||
<ol>
|
||||
<li><p>and another subitem</p>
|
||||
<code>echo with a block</code>
|
||||
</li>
|
||||
<li><p>and another one with a table</p>
|
||||
<table>
|
||||
<thead>
|
||||
<th>a</th><th>b</th><th>c</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>1</td><td>2</td><td>3</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>and text with an empty line in between as well!</p>
|
||||
</li>
|
||||
</ol>
|
||||
</li>
|
||||
<li>unordered list item 4 </li>
|
||||
</ul>
|
||||
<h2>Inline</h2>
|
||||
<ul>
|
||||
<li><em>emphasis</em> and a hard line break <br>
|
||||
see?</li>
|
||||
<li><em>.emphasis with dot border chars.</em></li>
|
||||
<li><em>emphasis with a slash/inside</em></li>
|
||||
<li><em>emphasis</em> followed by raw text with slash /</li>
|
||||
<li>->/not an emphasis/<-</li>
|
||||
<li>links with slashes do not become <em>emphasis</em>: <a href="https://somelinkshouldntrenderaccidentalemphasis.com">https://somelinkshouldntrenderaccidentalemphasis.com</a>/ <em>emphasis</em></li>
|
||||
<li><span style="text-decoration: underline;">underlined</span> <strong>bold</strong> <code class="verbatim">verbatim</code> <code>code</code> <del>strikethrough</del></li>
|
||||
<li><strong>bold string with an *asterisk inside</strong></li>
|
||||
<li><p>links</p>
|
||||
<ol>
|
||||
<li>regular link <a href="https://example.com">https://example.com</a> link without description</li>
|
||||
<li>regular link <a href="https://example.com">example.com</a> link with description</li>
|
||||
<li>regular link to a file (image) <img src="my-img.png" alt="file:my-img.png" title="file:my-img.png" /></li>
|
||||
</ol>
|
||||
</li>
|
||||
</ul>
|
||||
<h2>Footnotes</h2>
|
||||
<ul>
|
||||
<li>normal footnote reference <sup class="footnote-reference"><a href="#footnote-1">1</a></sup></li>
|
||||
<li>further references to the same footnote should not <sup class="footnote-reference"><a href="#footnote-1">1</a></sup> render duplicates in the footnote list</li>
|
||||
<li>also inline footnotes are supported via <code class="verbatim">fn:2:inline definition</code>. But we won't test that because it would cause the output to look different from the input </li>
|
||||
</ul>
|
||||
<div id="footnotes">
|
||||
<h1 class="footnotes-title">Footnotes</h1>
|
||||
<div class="footnote-definitions">
|
||||
<div class="footnote-definition">
|
||||
<sup id="footnote-1">1</sup>
|
||||
<p>Foobar</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
60
org/testdata/example.org
vendored
Normal file
60
org/testdata/example.org
vendored
Normal file
|
@ -0,0 +1,60 @@
|
|||
#+TITLE: Example org mode file
|
||||
#+AUTHOR: Niklas Fasching
|
||||
#+DESCRIPTION: just some random elements with little explanation
|
||||
|
||||
* Motivation
|
||||
|
||||
To validate the parser we'll try printing the AST back to org-mode source - if that
|
||||
works we can be kind of sure that the parsing worked.
|
||||
At least I hope so - I would like to get around writing tests for the individual parsing
|
||||
functions...
|
||||
|
||||
** Headlines with TODO status, priority & tags
|
||||
*** TODO [#B] Headline with todo status & priority
|
||||
*** DONE Headline with TODO status
|
||||
*** [#A] Headline with tags & priority :foo:bar:
|
||||
this one is cheating a little as tags are ALWAYS printed right aligned to a given column number...
|
||||
** Lists
|
||||
- unordered list item 1
|
||||
- unordered list item 2 - with ~inline~ /markup/
|
||||
1. ordered sublist item 1
|
||||
a) ordered sublist item 1
|
||||
b) ordered sublist item 2
|
||||
c) ordered sublist item 3
|
||||
2. ordered sublist item 2
|
||||
- unordered list item 3 - and a [[https://example.com][link]]
|
||||
and some lines of text
|
||||
1. and another subitem
|
||||
#+BEGIN_SRC sh
|
||||
echo with a block
|
||||
#+END_SRC
|
||||
2. and another one with a table
|
||||
| a | b | c |
|
||||
|---+---+---|
|
||||
| 1 | 2 | 3 |
|
||||
|
||||
and text with an empty line in between as well!
|
||||
- unordered list item 4
|
||||
|
||||
** Inline
|
||||
- /emphasis/ and a hard line break \\
|
||||
see?
|
||||
- /.emphasis with dot border chars./
|
||||
- /emphasis with a slash/inside/
|
||||
- /emphasis/ followed by raw text with slash /
|
||||
- ->/not an emphasis/<-
|
||||
- links with slashes do not become /emphasis/: [[https://somelinkshouldntrenderaccidentalemphasis.com]]/ /emphasis/
|
||||
- _underlined_ *bold* =verbatim= ~code~ +strikethrough+
|
||||
- *bold string with an *asterisk inside*
|
||||
- links
|
||||
1. regular link [[https://example.com]] link without description
|
||||
2. regular link [[https://example.com][example.com]] link with description
|
||||
3. regular link to a file (image) [[file:my-img.png]]
|
||||
** Footnotes
|
||||
- normal footnote reference [fn:1]
|
||||
- further references to the same footnote should not [fn:1] render duplicates in the footnote list
|
||||
- also inline footnotes are supported via =fn:2:inline definition=. But we won't test that because it would
|
||||
cause the output to look different from the input
|
||||
|
||||
* Footnotes
|
||||
[fn:1] Foobar
|
Loading…
Add table
Add a link
Reference in a new issue