all repos

clerk @ 263a106703c6521b2fcef090c54f131040a34c13

missing tooling for ledger/hledger

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

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