all repos

clerk @ bfe61d3

missing tooling for ledger/hledger

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
feat: config (#6), 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 (p *printer) formatEntry(e ast.Entry) {
112
	switch e := e.(type) {
113
	case *ast.BlankLine:
114
		if !p.prevWasBlank || p.cfg.PreserveBlankLines {
115
			p.buf.WriteByte('\n')
116
		}
117
		p.prevWasBlank = true
118
		return
119
	case *ast.Transaction:
120
		p.prevWasBlank = false
121
		p.writeTransaction(e)
122
		return
123
	case *ast.PeriodicTransaction:
124
		p.prevWasBlank = false
125
		p.writePeriodicTransaction(e)
126
		return
127
	case *ast.AutomatedTransaction:
128
		p.prevWasBlank = false
129
		p.writeAutomatedTransaction(e)
130
		return
131
132
	case *ast.IgnoredDirective:
133
		p.writeIgnoredDirective(e)
134
		return
135
136
	case *ast.Comment:
137
		p.writeComment(e)
138
	case *ast.AccountDirective:
139
		p.writeAccountDirective(e)
140
	case *ast.CommodityDirective:
141
		p.writeCommodityDirective(e)
142
	case *ast.IncludeDirective:
143
		p.writeIncludeDirective(e)
144
	case *ast.AliasDirective:
145
		p.writeAliasDirective(e)
146
	case *ast.PayeeDirective:
147
		p.writePayeeDirective(e)
148
	case *ast.TagDirective:
149
		p.writeTagDirective(e)
150
	case *ast.YearDirective:
151
		p.writeYearDirective(e)
152
	case *ast.DecimalMarkDirective:
153
		p.writeDecimalMarkDirective(e)
154
	case *ast.MarketPriceDirective:
155
		p.writeMarketPriceDirective(e)
156
	case *ast.ConversionDirective:
157
		p.writeConversionDirective(e)
158
	case *ast.DefaultCommodityDirective:
159
		p.writeDefaultCommodityDirective(e)
160
	case *ast.ApplyDirective:
161
		p.writeApplyDirective(e)
162
	case *ast.EndDirective:
163
		p.writeEndDirective(e)
164
	case *ast.CommentBlockDirective:
165
		p.writeCommentBlockDirective(e)
166
	default:
167
		fmt.Fprintf(&p.buf, "; unknown entry %T", e)
168
	}
169
	p.prevWasBlank = false
170
	p.buf.WriteByte('\n')
171
}
172
173
func (p *printer) writeSpaces(n int) {
174
	for range n {
175
		p.buf.WriteByte(' ')
176
	}
177
}
178
179
func isCommentChar(b byte) bool {
180
	return b == ';' || b == '#' || b == '%' || b == '*'
181
}
182
183
func (p *printer) writeComment(c *ast.Comment) {
184
	if c != nil && c.Text != "" {
185
		p.buf.WriteByte(c.Marker)
186
		if !isCommentChar(c.Text[0]) {
187
			p.buf.WriteByte(' ')
188
		}
189
		p.buf.WriteString(c.Text)
190
	}
191
}
192
193
func (p *printer) writeInlineComment(c *ast.Comment) {
194
	if c != nil && c.Text != "" {
195
		p.buf.WriteByte(' ')
196
		p.buf.WriteByte(' ')
197
		p.buf.WriteByte(c.Marker)
198
		if !isCommentChar(c.Text[0]) {
199
			p.buf.WriteByte(' ')
200
		}
201
		p.buf.WriteString(c.Text)
202
	}
203
}
204
205
func spaces(n int) string {
206
	b := make([]byte, n)
207
	for i := range b {
208
		b[i] = ' '
209
	}
210
	return string(b)
211
}