1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275
|
// Package org is an Org mode syntax processor.
//
// It parses plain text into an AST and can export it as HTML or pretty printed Org mode syntax.
// Further export formats can be defined using the Writer interface.
//
// You probably want to start with something like this:
// input := strings.NewReader("Your Org mode input")
// html, err := org.New().Parse(input, "./").Write(org.NewHTMLWriter())
// if err != nil {
// log.Fatalf("Something went wrong: %s", err)
// }
// log.Print(html)
package org
import (
"bufio"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"strings"
"sync"
)
type Configuration struct {
MaxEmphasisNewLines int // Maximum number of newlines inside an emphasis. See org-emphasis-regexp-components newline.
AutoLink bool // Try to convert text passages that look like hyperlinks into hyperlinks.
DefaultSettings map[string]string // Default values for settings that are overriden by setting the same key in BufferSettings.
Log *log.Logger // Log is used to print warnings during parsing.
ReadFile func(filename string) ([]byte, error) // ReadFile is used to read e.g. #+INCLUDE files.
}
// Document contains the parsing results and a pointer to the Configuration.
type Document struct {
*Configuration
Path string // Path of the file containing the parse input - used to resolve relative paths during parsing (e.g. INCLUDE).
tokens []token
baseLvl int
Macros map[string]string
Links map[string]string
Nodes []Node
NamedNodes map[string]Node
Outline Outline // Outline is a Table Of Contents for the document and contains all sections (headline + content).
BufferSettings map[string]string // Settings contains all settings that were parsed from keywords.
Error error
}
// Node represents a parsed node of the document.
type Node interface {
String() string // String returns the pretty printed Org mode string for the node (see OrgWriter).
}
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,
lexDrawer,
lexBlock,
lexResult,
lexList,
lexTable,
lexHorizontalRule,
lexKeywordOrComment,
lexFootnoteDefinition,
lexExample,
lexLatexBlock,
lexText,
}
var nilToken = token{"nil", -1, "", nil}
var orgWriterMutex = sync.Mutex{}
var orgWriter = NewOrgWriter()
// New returns a new Configuration with (hopefully) sane defaults.
func New() *Configuration {
return &Configuration{
AutoLink: true,
MaxEmphasisNewLines: 1,
DefaultSettings: map[string]string{
"TODO": "TODO | DONE",
"EXCLUDE_TAGS": "noexport",
"OPTIONS": "toc:t <:t e:t f:t pri:t todo:t tags:t title:t ealb:nil",
},
Log: log.New(os.Stderr, "go-org: ", 0),
ReadFile: ioutil.ReadFile,
}
}
// String returns the pretty printed Org mode string for the given nodes (see OrgWriter).
func String(nodes ...Node) string {
orgWriterMutex.Lock()
defer orgWriterMutex.Unlock()
return orgWriter.WriteNodesAsString(nodes...)
}
// Write is called after with an instance of the Writer interface to export a parsed Document into another format.
func (d *Document) Write(w Writer) (out string, err error) {
defer func() {
if recovered := recover(); recovered != nil {
err = fmt.Errorf("could not write output: %s", recovered)
}
}()
if d.Error != nil {
return "", d.Error
} else if d.Nodes == nil {
return "", fmt.Errorf("could not write output: parse was not called")
}
w.Before(d)
WriteNodes(w, d.Nodes...)
w.After(d)
return w.String(), err
}
// Parse parses the input into an AST (and some other helpful fields like Outline).
// To allow method chaining, errors are stored in document.Error rather than being returned.
func (c *Configuration) Parse(input io.Reader, path string) (d *Document) {
outlineSection := &Section{}
d = &Document{
Configuration: c,
Outline: Outline{outlineSection, outlineSection, 0},
BufferSettings: map[string]string{},
NamedNodes: map[string]Node{},
Links: map[string]string{},
Macros: map[string]string{},
Path: path,
}
defer func() {
if recovered := recover(); recovered != nil {
d.Error = fmt.Errorf("could not parse input: %v", recovered)
}
}()
if d.tokens != nil {
d.Error = fmt.Errorf("parse was called multiple times")
}
d.tokenize(input)
_, nodes := d.parseMany(0, func(d *Document, i int) bool { return i >= len(d.tokens) })
d.Nodes = nodes
return d
}
// Silent disables all logging of warnings during parsing.
func (c *Configuration) Silent() *Configuration {
c.Log = log.New(ioutil.Discard, "", 0)
return c
}
func (d *Document) tokenize(input io.Reader) {
d.tokens = []token{}
scanner := bufio.NewScanner(input)
for scanner.Scan() {
d.tokens = append(d.tokens, tokenize(scanner.Text()))
}
if err := scanner.Err(); err != nil {
d.Error = fmt.Errorf("could not tokenize input: %s", err)
}
}
// Get returns the value for key in BufferSettings or DefaultSettings if key does not exist in the former
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 ""
}
// GetOption returns the value associated to the export option key
// Currently supported options:
// - < (export timestamps)
// - e (export org entities)
// - f (export footnotes)
// - title (export title)
// - toc (export table of content. an int limits the included org headline lvl)
// - todo (export headline todo status)
// - pri (export headline priority)
// - tags (export headline tags)
// - ealb (non-standard) (export with east asian line breaks / ignore line breaks between multi-byte characters)
// see https://orgmode.org/manual/Export-Settings.html for more information
func (d *Document) GetOption(key string) string {
get := func(settings map[string]string) string {
for _, field := range strings.Fields(settings["OPTIONS"]) {
if strings.HasPrefix(field, key+":") {
return field[len(key)+1:]
}
}
return ""
}
value := get(d.BufferSettings)
if value == "" {
value = get(d.DefaultSettings)
}
if value == "" {
value = "nil"
d.Log.Printf("Missing value for export option %s", key)
}
return value
}
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 "beginLatexBlock":
consumed, node = d.parseLatexBlock(i, stop)
case "result":
consumed, node = d.parseResult(i, stop)
case "beginDrawer":
consumed, node = d.parseDrawer(i, stop)
case "text":
consumed, node = d.parseParagraph(i, stop)
case "example":
consumed, node = d.parseExample(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
}
d.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) && !stop(d, i) {
consumed, node := d.parseOne(i, stop)
i += consumed
nodes = append(nodes, node)
}
return i - start, nodes
}
func (d *Document) addHeadline(headline *Headline) int {
current := &Section{Headline: headline}
d.Outline.last.add(current)
d.Outline.count++
d.Outline.last = current
return d.Outline.count
}
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))
}
|