| @@ -43,12 +43,14 @@ func (r orgRenderer) nodes(ns []org.Node, first, rest string) { |
| 43 | 43 | } |
| 44 | 44 | } |
| 45 | 45 | |
| 46 | | // skipOrg drops nodes that carry nothing to render: keywords, property |
| 46 | // skipOrg drops nodes that carry nothing to render: keywords |
| 47 | // (#+INCLUDE included — ReadFile already refuses the read, but the |
| 48 | // keyword itself is still a line of source, not content), property |
| 47 | 49 | // drawers, comments, and the empty paragraph go-org emits for a blank |
| 48 | 50 | // line that separated two blocks or ended a list item. |
| 49 | 51 | func skipOrg(n org.Node) bool { |
| 50 | 52 | switch n := n.(type) { |
| 51 | | case org.Keyword, *org.PropertyDrawer, org.Comment: |
| 53 | case org.Keyword, org.Include, *org.PropertyDrawer, org.Comment: |
| 52 | 54 | return true |
| 53 | 55 | case org.Paragraph: |
| 54 | 56 | return len(n.Children) == 0 |
| @@ -67,9 +69,10 @@ func (r orgRenderer) block(n org.Node, first, rest string) { |
| 67 | 69 | case org.Paragraph: |
| 68 | 70 | r.w.para(r.inline(n.Children), first, rest) |
| 69 | 71 | case org.List: |
| 72 | loose := looseOrgList(n) |
| 70 | 73 | p := first |
| 71 | 74 | for i, item := range n.Items { |
| 72 | | if i > 0 && n.Kind == "descriptive" { |
| 75 | if i > 0 && loose { |
| 73 | 76 | r.w.blank() |
| 74 | 77 | } |
| 75 | 78 | switch item := item.(type) { |
| @@ -79,11 +82,11 @@ func (r orgRenderer) block(n org.Node, first, rest string) { |
| 79 | 82 | marker = item.Bullet + " " |
| 80 | 83 | } |
| 81 | 84 | hang := rest + strings.Repeat(" ", cells(marker)) |
| 82 | | r.nodes(item.Children, p+marker, hang) |
| 85 | r.itemChildren(item.Children, p+marker, hang, loose) |
| 83 | 86 | case org.DescriptiveListItem: |
| 84 | 87 | term := r.w.paint(sgrBold, r.inline(item.Term)) |
| 85 | 88 | r.w.para(term, p, rest) |
| 86 | | r.nodes(item.Details, rest+" ", rest+" ") |
| 89 | r.itemChildren(item.Details, rest+" ", rest+" ", loose) |
| 87 | 90 | default: |
| 88 | 91 | r.w.code(org.String(item), "", rest) |
| 89 | 92 | } |
| @@ -112,7 +115,73 @@ func (r orgRenderer) block(n org.Node, first, rest string) { |
| 112 | 115 | } |
| 113 | 116 | } |
| 114 | 117 | |
| 118 | // looseOrgList reports whether n's source had a blank line between |
| 119 | // any two of its items. go-org marks the item before such a blank |
| 120 | // with a trailing empty org.Paragraph — the same artifact skipOrg |
| 121 | // drops elsewhere, checked here first since skipOrg would erase it. |
| 122 | // The list's own last item gets that trailing empty paragraph too |
| 123 | // whenever a blank line follows the whole list (ending it before the |
| 124 | // next block), which says nothing about spacing inside the list, so |
| 125 | // only non-last items are checked. A single-item list is always |
| 126 | // tight by this measure: there is no gap between items to have or |
| 127 | // lack a blank line. |
| 128 | func looseOrgList(n org.List) bool { |
| 129 | for i := 0; i < len(n.Items)-1; i++ { |
| 130 | var children []org.Node |
| 131 | switch item := n.Items[i].(type) { |
| 132 | case org.ListItem: |
| 133 | children = item.Children |
| 134 | case org.DescriptiveListItem: |
| 135 | children = item.Details |
| 136 | default: |
| 137 | continue |
| 138 | } |
| 139 | if len(children) == 0 { |
| 140 | continue |
| 141 | } |
| 142 | if p, ok := children[len(children)-1].(org.Paragraph); ok && len(p.Children) == 0 { |
| 143 | return true |
| 144 | } |
| 145 | } |
| 146 | return false |
| 147 | } |
| 148 | |
| 149 | // itemChildren renders a list item's children. A loose list keeps |
| 150 | // nodes' blank line between them; a tight list runs them straight |
| 151 | // together, so a nested list sits directly under its parent item's |
| 152 | // line rather than a line below it. |
| 153 | func (r orgRenderer) itemChildren(ns []org.Node, first, rest string, loose bool) { |
| 154 | if loose { |
| 155 | r.nodes(ns, first, rest) |
| 156 | return |
| 157 | } |
| 158 | p := first |
| 159 | for _, n := range ns { |
| 160 | if skipOrg(n) { |
| 161 | continue |
| 162 | } |
| 163 | r.block(n, p, rest) |
| 164 | p = rest |
| 165 | } |
| 166 | } |
| 167 | |
| 168 | // trimOrgLineBreaks drops leading and trailing org.LineBreak nodes: a |
| 169 | // paragraph that follows a keyword across a blank line, or a blank |
| 170 | // line gap already handled elsewhere, otherwise renders that break as |
| 171 | // the leading or trailing space inline() gives it. |
| 172 | func trimOrgLineBreaks(ns []org.Node) []org.Node { |
| 173 | isBreak := func(n org.Node) bool { _, ok := n.(org.LineBreak); return ok } |
| 174 | for len(ns) > 0 && isBreak(ns[0]) { |
| 175 | ns = ns[1:] |
| 176 | } |
| 177 | for len(ns) > 0 && isBreak(ns[len(ns)-1]) { |
| 178 | ns = ns[:len(ns)-1] |
| 179 | } |
| 180 | return ns |
| 181 | } |
| 182 | |
| 115 | 183 | func (r orgRenderer) inline(ns []org.Node) string { |
| 184 | ns = trimOrgLineBreaks(ns) |
| 116 | 185 | var b strings.Builder |
| 117 | 186 | for _, n := range ns { |
| 118 | 187 | switch n := n.(type) { |