go-org-orgwiki/org/org_writer.go
Niklas Fasching b61e49eb85 Preserve whitespace (indentation) inside paragraphs
We want original whitespace to be rendered in some cases (e.g. verse
blocks). This requires information about the original whitespace to be
preserved during paragraph parsing. As html ignores (collapses) whitespace by
default we don't have to adapt the html writer and can just selectively enable
rendering of the preseverved whitespace wherever we want it using
css (white-space: pre).

To differentiate meaningful whitespace from document structure based
indentation (i.e. list item base indentation) we need to introduce
document.baseLvl. A paragraph by itself does not have enough information to
differentiate both kinds of whitespace and needs this information as context
[0].

As we're already touching list indentation i went along and improved (fixed?)
descriptive list item indentation rendering in the org writer (it should match
emacs tab behavior - i.e. indent subsequent lines up to the `:: `).

[0] e.g. list items can contain blank lines - a paragraph starting with a blank
line would not know that it is part of a list item / has a base indentation -
the blank line would suggest a baseLvl of 0.
2019-12-22 14:17:14 +01:00

328 lines
8 KiB
Go

package org
import (
"fmt"
"strings"
"unicode"
"unicode/utf8"
)
// OrgWriter export an org document into pretty printed org document.
type OrgWriter struct {
ExtendingWriter Writer
TagsColumn int
strings.Builder
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) WriterWithExtensions() Writer {
if w.ExtendingWriter != nil {
return w.ExtendingWriter
}
return w
}
func (w *OrgWriter) Before(d *Document) {}
func (w *OrgWriter) After(d *Document) {}
func (w *OrgWriter) WriteNodesAsString(nodes ...Node) string {
builder := w.Builder
w.Builder = strings.Builder{}
WriteNodes(w, nodes...)
out := w.String()
w.Builder = builder
return out
}
func (w *OrgWriter) WriteHeadline(h Headline) {
start := w.Len()
w.WriteString(strings.Repeat("*", h.Lvl))
if h.Status != "" {
w.WriteString(" " + h.Status)
}
if h.Priority != "" {
w.WriteString(" [#" + h.Priority + "]")
}
w.WriteString(" ")
WriteNodes(w, h.Title...)
if len(h.Tags) != 0 {
tString := ":" + strings.Join(h.Tags, ":") + ":"
if n := w.TagsColumn - len(tString) - (w.Len() - start); n > 0 {
w.WriteString(strings.Repeat(" ", n) + tString)
} else {
w.WriteString(" " + tString)
}
}
w.WriteString("\n")
if len(h.Children) != 0 {
w.WriteString(w.indent)
}
if h.Properties != nil {
WriteNodes(w, *h.Properties)
}
WriteNodes(w, h.Children...)
}
func (w *OrgWriter) WriteBlock(b Block) {
w.WriteString(w.indent + "#+BEGIN_" + b.Name)
if len(b.Parameters) != 0 {
w.WriteString(" " + strings.Join(b.Parameters, " "))
}
w.WriteString("\n")
if isRawTextBlock(b.Name) {
w.WriteString(w.indent)
}
WriteNodes(w, b.Children...)
if !isRawTextBlock(b.Name) {
w.WriteString(w.indent)
}
w.WriteString("#+END_" + b.Name + "\n")
}
func (w *OrgWriter) WriteDrawer(d Drawer) {
w.WriteString(w.indent + ":" + d.Name + ":\n")
WriteNodes(w, d.Children...)
w.WriteString(w.indent + ":END:\n")
}
func (w *OrgWriter) WritePropertyDrawer(d PropertyDrawer) {
w.WriteString(":PROPERTIES:\n")
for _, kvPair := range d.Properties {
k, v := kvPair[0], kvPair[1]
if v != "" {
v = " " + v
}
w.WriteString(fmt.Sprintf(":%s:%s\n", k, v))
}
w.WriteString(":END:\n")
}
func (w *OrgWriter) WriteFootnoteDefinition(f FootnoteDefinition) {
w.WriteString(fmt.Sprintf("[fn:%s]", f.Name))
content := w.WriteNodesAsString(f.Children...)
if content != "" && !unicode.IsSpace(rune(content[0])) {
w.WriteString(" ")
}
w.WriteString(content)
}
func (w *OrgWriter) WriteParagraph(p Paragraph) {
content := w.WriteNodesAsString(p.Children...)
if len(content) > 0 && content[0] != '\n' {
w.WriteString(w.indent)
}
w.WriteString(content + "\n")
}
func (w *OrgWriter) WriteExample(e Example) {
for _, n := range e.Children {
w.WriteString(w.indent + ":")
if content := w.WriteNodesAsString(n); content != "" {
w.WriteString(" " + content)
}
w.WriteString("\n")
}
}
func (w *OrgWriter) WriteKeyword(k Keyword) {
w.WriteString(w.indent + "#+" + k.Key + ":")
if k.Value != "" {
w.WriteString(" " + k.Value)
}
w.WriteString("\n")
}
func (w *OrgWriter) WriteInclude(i Include) {
w.WriteKeyword(i.Keyword)
}
func (w *OrgWriter) WriteNodeWithMeta(n NodeWithMeta) {
for _, ns := range n.Meta.Caption {
w.WriteString("#+CAPTION: ")
WriteNodes(w, ns...)
w.WriteString("\n")
}
for _, attributes := range n.Meta.HTMLAttributes {
w.WriteString("#+ATTR_HTML: ")
w.WriteString(strings.Join(attributes, " ") + "\n")
}
WriteNodes(w, n.Node)
}
func (w *OrgWriter) WriteNodeWithName(n NodeWithName) {
w.WriteString(fmt.Sprintf("#+NAME: %s\n", n.Name))
WriteNodes(w, n.Node)
}
func (w *OrgWriter) WriteComment(c Comment) {
w.WriteString(w.indent + "#" + c.Content + "\n")
}
func (w *OrgWriter) WriteList(l List) { WriteNodes(w, l.Items...) }
func (w *OrgWriter) WriteListItem(li ListItem) {
originalBuilder, originalIndent := w.Builder, w.indent
w.Builder, w.indent = strings.Builder{}, w.indent+strings.Repeat(" ", len(li.Bullet)+1)
WriteNodes(w, li.Children...)
content := strings.TrimPrefix(w.String(), w.indent)
w.Builder, w.indent = originalBuilder, originalIndent
w.WriteString(w.indent + li.Bullet)
if li.Status != "" {
w.WriteString(fmt.Sprintf(" [%s]", li.Status))
}
if len(content) > 0 && content[0] == '\n' {
w.WriteString(content)
} else {
w.WriteString(" " + content)
}
}
func (w *OrgWriter) WriteDescriptiveListItem(di DescriptiveListItem) {
indent := w.indent + strings.Repeat(" ", len(di.Bullet)+1)
w.WriteString(w.indent + di.Bullet)
if di.Status != "" {
w.WriteString(fmt.Sprintf(" [%s]", di.Status))
indent = indent + strings.Repeat(" ", len(di.Status)+3)
}
if len(di.Term) != 0 {
term := w.WriteNodesAsString(di.Term...)
w.WriteString(" " + term + " ::")
indent = indent + strings.Repeat(" ", len(term)+4)
}
originalBuilder, originalIndent := w.Builder, w.indent
w.Builder, w.indent = strings.Builder{}, indent
WriteNodes(w, di.Details...)
details := strings.TrimPrefix(w.String(), w.indent)
w.Builder, w.indent = originalBuilder, originalIndent
if len(details) > 0 && details[0] == '\n' {
w.WriteString(details)
} else {
w.WriteString(" " + details)
}
}
func (w *OrgWriter) WriteTable(t Table) {
for _, row := range t.Rows {
w.WriteString(w.indent)
if len(row.Columns) == 0 {
w.WriteString(`|`)
for i := 0; i < len(t.ColumnInfos); i++ {
w.WriteString(strings.Repeat("-", t.ColumnInfos[i].Len+2))
if i < len(t.ColumnInfos)-1 {
w.WriteString("+")
}
}
w.WriteString(`|`)
} else {
w.WriteString(`|`)
for _, column := range row.Columns {
w.WriteString(` `)
content := w.WriteNodesAsString(column.Children...)
if content == "" {
content = " "
}
n := column.Len - utf8.RuneCountInString(content)
if n < 0 {
n = 0
}
if column.Align == "center" {
if n%2 != 0 {
w.WriteString(" ")
}
w.WriteString(strings.Repeat(" ", n/2) + content + strings.Repeat(" ", n/2))
} else if column.Align == "right" {
w.WriteString(strings.Repeat(" ", n) + content)
} else {
w.WriteString(content + strings.Repeat(" ", n))
}
w.WriteString(` |`)
}
}
w.WriteString("\n")
}
}
func (w *OrgWriter) WriteHorizontalRule(hr HorizontalRule) {
w.WriteString(w.indent + "-----\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])
WriteNodes(w, e.Content...)
w.WriteString(borders[1])
}
func (w *OrgWriter) WriteLatexFragment(l LatexFragment) {
w.WriteString(l.OpeningPair)
WriteNodes(w, l.Content...)
w.WriteString(l.ClosingPair)
}
func (w *OrgWriter) WriteStatisticToken(s StatisticToken) {
w.WriteString(fmt.Sprintf("[%s]", s.Content))
}
func (w *OrgWriter) WriteLineBreak(l LineBreak) {
w.WriteString(strings.Repeat("\n"+w.indent, l.Count))
}
func (w *OrgWriter) WriteExplicitLineBreak(l ExplicitLineBreak) {
w.WriteString(`\\` + "\n" + w.indent)
}
func (w *OrgWriter) WriteTimestamp(t Timestamp) {
w.WriteString("<")
if t.IsDate {
w.WriteString(t.Time.Format(datestampFormat))
} else {
w.WriteString(t.Time.Format(timestampFormat))
}
if t.Interval != "" {
w.WriteString(" " + t.Interval)
}
w.WriteString(">")
}
func (w *OrgWriter) WriteFootnoteLink(l FootnoteLink) {
w.WriteString("[fn:" + l.Name)
if l.Definition != nil {
w.WriteString(":")
WriteNodes(w, l.Definition.Children[0].(Paragraph).Children...)
}
w.WriteString("]")
}
func (w *OrgWriter) WriteRegularLink(l RegularLink) {
if l.AutoLink {
w.WriteString(l.URL)
} else if l.Description == nil {
w.WriteString(fmt.Sprintf("[[%s]]", l.URL))
} else {
w.WriteString(fmt.Sprintf("[[%s][%s]]", l.URL, w.WriteNodesAsString(l.Description...)))
}
}