all repos

clerk @ d12c079

missing tooling for ledger/hledger

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
printer: add commodity-space option, 13 days 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
func (a AlignStyle) String() string {
21
	switch a {
22
	case AlignTwoSpaces:
23
		return "two-spaces"
24
	case AlignRight:
25
		return "right"
26
	case AlignTab:
27
		return "tab"
28
	default:
29
		panic("invalid align style value")
30
	}
31
}
32
33
// CommodityPos controls where the commodity marker is placed
34
type CommodityPos int
35
36
const (
37
	CommodityPosAfter  CommodityPos = iota // "10.00 EUR"
38
	CommodityPosBefore                     // "$10.00"
39
)
40
41
func (c CommodityPos) String() string {
42
	switch c {
43
	case CommodityPosAfter:
44
		return "after"
45
	case CommodityPosBefore:
46
		return "before"
47
	default:
48
		panic("invalid commodity pos value")
49
	}
50
}
51
52
// CommoditySpace controls the space between quantity and commodity
53
type CommoditySpace int
54
55
const (
56
	CommoditySpacePreserve CommoditySpace = iota // "$10" / "$ 10"
57
	CommoditySpaceAlways                         // "$ 10"
58
	CommoditySpaceNever                          // "$10"
59
)
60
61
type Config struct {
62
	TabIndent          bool           // true = tabs, false = spaces
63
	IndentWidth        int            // spaces per indent level (default: 2)
64
	PreserveBlankLines bool           // preserve consecutive blank lines as-is
65
	AlignStyle         AlignStyle     // (default AlignTwoSpaces)
66
	AlignColumn        int            // fixed column for AlignRight
67
	CommodityPos       CommodityPos   // where to place commodity
68
	CommoditySpace     CommoditySpace // space between quantity and commodity
69
}
70
71
var DefaultConfig = Config{
72
	TabIndent:          false,
73
	IndentWidth:        2,
74
	PreserveBlankLines: false,
75
	AlignStyle:         AlignTwoSpaces,
76
	AlignColumn:        70,
77
	CommodityPos:       CommodityPosAfter,
78
	CommoditySpace:     CommoditySpacePreserve,
79
}
80
81
func (c *Config) indent() string {
82
	if c.TabIndent {
83
		return "\t"
84
	}
85
	n := 2
86
	if c.IndentWidth > 0 {
87
		n = c.IndentWidth
88
	}
89
	return spaces(n)
90
}
91
92
// printer holds formatting state for a single Fprint call.
93
type printer struct {
94
	buf          strings.Builder
95
	cfg          *Config
96
	indent       string
97
	prevWasBlank bool
98
}
99
100
// Fprint formats a parsed journal.
101
func (c *Config) Fprint(w io.Writer, j *ast.Journal) error {
102
	p := printer{cfg: c, indent: c.indent()}
103
104
	for _, e := range j.Entries {
105
		p.formatEntry(e)
106
	}
107
108
	// allow exactly one trailing newline
109
	out := strings.TrimRight(p.buf.String(), "\n") + "\n"
110
	_, err := io.WriteString(w, out)
111
	return err
112
}
113
114
// FprintEntry formats a single journal entry.
115
func (c *Config) FprintEntry(w io.Writer, e ast.Entry) error {
116
	p := printer{cfg: c, indent: c.indent()}
117
	p.formatEntry(e)
118
	_, err := io.WriteString(w, p.buf.String())
119
	return err
120
}
121
122
func (c *Config) FprintEntries(w io.Writer, e []ast.Entry) error {
123
	p := printer{cfg: c, indent: c.indent()}
124
	for _, ee := range e {
125
		p.formatEntry(ee)
126
	}
127
	_, err := io.WriteString(w, p.buf.String())
128
	return err
129
}
130
131
func (p *printer) formatEntry(e ast.Entry) {
132
	switch e := e.(type) {
133
	case *ast.BlankLine:
134
		if !p.prevWasBlank || p.cfg.PreserveBlankLines {
135
			p.buf.WriteByte('\n')
136
		}
137
		p.prevWasBlank = true
138
		return
139
	case *ast.Transaction:
140
		p.prevWasBlank = false
141
		p.writeTransaction(e)
142
		return
143
	case *ast.PeriodicTransaction:
144
		p.prevWasBlank = false
145
		p.writePeriodicTransaction(e)
146
		return
147
	case *ast.AutomatedTransaction:
148
		p.prevWasBlank = false
149
		p.writeAutomatedTransaction(e)
150
		return
151
152
	case *ast.IgnoredDirective:
153
		p.writeIgnoredDirective(e)
154
		return
155
156
	case *ast.Comment:
157
		p.writeComment(e)
158
	case *ast.AccountDirective:
159
		p.writeAccountDirective(e)
160
	case *ast.CommodityDirective:
161
		p.writeCommodityDirective(e)
162
	case *ast.IncludeDirective:
163
		p.writeIncludeDirective(e)
164
	case *ast.AliasDirective:
165
		p.writeAliasDirective(e)
166
	case *ast.PayeeDirective:
167
		p.writePayeeDirective(e)
168
	case *ast.TagDirective:
169
		p.writeTagDirective(e)
170
	case *ast.YearDirective:
171
		p.writeYearDirective(e)
172
	case *ast.DecimalMarkDirective:
173
		p.writeDecimalMarkDirective(e)
174
	case *ast.MarketPriceDirective:
175
		p.writeMarketPriceDirective(e)
176
	case *ast.ConversionDirective:
177
		p.writeConversionDirective(e)
178
	case *ast.DefaultCommodityDirective:
179
		p.writeDefaultCommodityDirective(e)
180
	case *ast.ApplyDirective:
181
		p.writeApplyDirective(e)
182
	case *ast.EndDirective:
183
		p.writeEndDirective(e)
184
	case *ast.CommentBlockDirective:
185
		p.writeCommentBlockDirective(e)
186
	default:
187
		fmt.Fprintf(&p.buf, "; unknown entry %T", e)
188
	}
189
	p.prevWasBlank = false
190
	p.buf.WriteByte('\n')
191
}
192
193
func (p *printer) writeSpaces(n int) {
194
	for range n {
195
		p.buf.WriteByte(' ')
196
	}
197
}
198
199
func isCommentChar(b byte) bool {
200
	return b == ';' || b == '#' || b == '%' || b == '*'
201
}
202
203
func (p *printer) writeComment(c *ast.Comment) {
204
	if c != nil && c.Text != "" {
205
		p.buf.WriteByte(c.Marker)
206
		if !isCommentChar(c.Text[0]) {
207
			p.buf.WriteByte(' ')
208
		}
209
		p.buf.WriteString(c.Text)
210
	}
211
}
212
213
func (p *printer) writeInlineComment(c *ast.Comment) {
214
	if c != nil && c.Text != "" {
215
		p.buf.WriteByte(' ')
216
		p.buf.WriteByte(' ')
217
		p.buf.WriteByte(c.Marker)
218
		if !isCommentChar(c.Text[0]) {
219
			p.buf.WriteByte(' ')
220
		}
221
		p.buf.WriteString(c.Text)
222
	}
223
}
224
225
func spaces(n int) string {
226
	b := make([]byte, n)
227
	for i := range b {
228
		b[i] = ' '
229
	}
230
	return string(b)
231
}