all repos

clerk @ e12aa47

missing tooling for ledger/hledger

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

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