all repos

clerk @ 7b1aef3

missing tooling for ledger/hledger

clerk/journal/printer/printer.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
journal/printer: add PreserveBlankLines option, 1 month ago
1
package printer
2
3
import (
4
	"fmt"
5
	"io"
6
	"strings"
7
8
	"olexsmir.xyz/clerk/journal/ast"
9
)
10
11
// AlignStyle controls how postings are aligned
12
type AlignStyle int
13
14
const (
15
	AlignTwoSpaces AlignStyle = iota // "  Account  $10.00"
16
	AlignRight                       // amounts at fixed col: "  Account         $10.00"
17
	AlignTab                         // elastic tabstops
18
)
19
20
// CommodityPos controls where the commodity marker is placed
21
type CommodityPos int
22
23
const (
24
	CommodityAfter  CommodityPos = iota // "10.00 EUR"
25
	CommodityBefore                     // "$10.00"
26
)
27
28
type Config struct {
29
	TabIndent          bool         // true = tabs, false = spaces
30
	IndentWidth        int          // spaces per indent level (default: 2)
31
	PreserveBlankLines bool         // preserve consecutive blank lines as-is
32
	AlignStyle         AlignStyle   // (default AlignTwoSpaces)
33
	AlignColumn        int          // fixed column for AlignRight
34
	CommodityPos       CommodityPos // where to place commodity
35
}
36
37
var defaultConfig = &Config{
38
	TabIndent:          false,
39
	IndentWidth:        2,
40
	PreserveBlankLines: false,
41
	AlignStyle:         AlignTwoSpaces,
42
	AlignColumn:        70,
43
	CommodityPos:       CommodityAfter,
44
}
45
46
func (c *Config) indent() string {
47
	if c.TabIndent {
48
		return "\t"
49
	}
50
	n := 2
51
	if c.IndentWidth > 0 {
52
		n = c.IndentWidth
53
	}
54
	return spaces(n)
55
}
56
57
// printer holds formatting state for a single Fprint call.
58
type printer struct {
59
	buf          strings.Builder
60
	cfg          *Config
61
	indent       string
62
	prevWasBlank bool
63
}
64
65
// Fprint formats using the default config.
66
func Fprint(w io.Writer, j *ast.Journal) error { return defaultConfig.Fprint(w, j) }
67
68
// Fprint formats a parsed journal.
69
func (c *Config) Fprint(w io.Writer, j *ast.Journal) error {
70
	if c == nil {
71
		c = defaultConfig
72
	}
73
	p := printer{cfg: c, indent: c.indent()}
74
75
	for _, e := range j.Entries {
76
		p.formatEntry(e)
77
	}
78
79
	// allow exactly one trailing newline
80
	out := strings.TrimRight(p.buf.String(), "\n") + "\n"
81
	_, err := io.WriteString(w, out)
82
	return err
83
}
84
85
// FprintEntry formats a single ast entry using the default config.
86
func FprintEntry(w io.Writer, e ast.Entry) error { return defaultConfig.FprintEntry(w, e) }
87
88
// FprintEntry formats a single journal entry.
89
func (c *Config) FprintEntry(w io.Writer, e ast.Entry) error {
90
	if c == nil {
91
		c = defaultConfig
92
	}
93
	p := printer{cfg: c, indent: c.indent()}
94
	p.formatEntry(e)
95
	_, err := io.WriteString(w, p.buf.String())
96
	return err
97
}
98
99
func (p *printer) formatEntry(e ast.Entry) {
100
	switch e := e.(type) {
101
	case *ast.BlankLine:
102
		if !p.prevWasBlank || p.cfg.PreserveBlankLines {
103
			p.buf.WriteByte('\n')
104
		}
105
		p.prevWasBlank = true
106
		return
107
	case *ast.Transaction:
108
		p.prevWasBlank = false
109
		p.writeTransaction(e)
110
		return
111
	case *ast.PeriodicTransaction:
112
		p.prevWasBlank = false
113
		p.writePeriodicTransaction(e)
114
		return
115
	case *ast.AutomatedTransaction:
116
		p.prevWasBlank = false
117
		p.writeAutomatedTransaction(e)
118
		return
119
120
	case *ast.IgnoredDirective:
121
		return // TODO:
122
123
	case *ast.Comment:
124
		p.writeComment(e)
125
	case *ast.AccountDirective:
126
		p.writeAccountDirective(e)
127
	case *ast.CommodityDirective:
128
		p.writeCommodityDirective(e)
129
	case *ast.IncludeDirective:
130
		p.writeIncludeDirective(e)
131
	case *ast.AliasDirective:
132
		p.writeAliasDirective(e)
133
	case *ast.PayeeDirective:
134
		p.writePayeeDirective(e)
135
	case *ast.TagDirective:
136
		p.writeTagDirective(e)
137
	case *ast.YearDirective:
138
		p.writeYearDirective(e)
139
	case *ast.DecimalMarkDirective:
140
		p.writeDecimalMarkDirective(e)
141
	case *ast.MarketPriceDirective:
142
		p.writeMarketPriceDirective(e)
143
	case *ast.ConversionDirective:
144
		p.writeConversionDirective(e)
145
	case *ast.DefaultCommodityDirective:
146
		p.writeDefaultCommodityDirective(e)
147
	case *ast.ApplyDirective:
148
		p.writeApplyDirective(e)
149
	case *ast.EndDirective:
150
		p.writeEndDirective(e)
151
	case *ast.CommentBlockDirective:
152
		p.writeCommentBlockDirective(e)
153
	default:
154
		fmt.Fprintf(&p.buf, "; unknown entry %T", e)
155
	}
156
	p.prevWasBlank = false
157
	p.buf.WriteByte('\n')
158
}
159
160
func (p *printer) writeSpaces(n int) {
161
	for range n {
162
		p.buf.WriteByte(' ')
163
	}
164
}
165
166
func isCommentChar(b byte) bool {
167
	return b == ';' || b == '#' || b == '%' || b == '*'
168
}
169
170
func (p *printer) writeComment(c *ast.Comment) {
171
	if c != nil && c.Text != "" {
172
		p.buf.WriteByte(c.Marker)
173
		if !isCommentChar(c.Text[0]) {
174
			p.buf.WriteByte(' ')
175
		}
176
		p.buf.WriteString(c.Text)
177
	}
178
}
179
180
func (p *printer) writeInlineComment(c *ast.Comment) {
181
	if c != nil && c.Text != "" {
182
		p.buf.WriteByte(' ')
183
		p.buf.WriteByte(c.Marker)
184
		if !isCommentChar(c.Text[0]) {
185
			p.buf.WriteByte(' ')
186
		}
187
		p.buf.WriteString(c.Text)
188
	}
189
}
190
191
func spaces(n int) string {
192
	b := make([]byte, n)
193
	for i := range b {
194
		b[i] = ' '
195
	}
196
	return string(b)
197
}