all repos

clerk @ a7ca45c45f1c26525fb20f33ea16ae41bc4db348

missing tooling for ledger/hledger

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
journal/printer: fix inline comments (always insert 2 spaces before), 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
		p.writeIgnoredDirective(e)
122
		return
123
124
	case *ast.Comment:
125
		p.writeComment(e)
126
	case *ast.AccountDirective:
127
		p.writeAccountDirective(e)
128
	case *ast.CommodityDirective:
129
		p.writeCommodityDirective(e)
130
	case *ast.IncludeDirective:
131
		p.writeIncludeDirective(e)
132
	case *ast.AliasDirective:
133
		p.writeAliasDirective(e)
134
	case *ast.PayeeDirective:
135
		p.writePayeeDirective(e)
136
	case *ast.TagDirective:
137
		p.writeTagDirective(e)
138
	case *ast.YearDirective:
139
		p.writeYearDirective(e)
140
	case *ast.DecimalMarkDirective:
141
		p.writeDecimalMarkDirective(e)
142
	case *ast.MarketPriceDirective:
143
		p.writeMarketPriceDirective(e)
144
	case *ast.ConversionDirective:
145
		p.writeConversionDirective(e)
146
	case *ast.DefaultCommodityDirective:
147
		p.writeDefaultCommodityDirective(e)
148
	case *ast.ApplyDirective:
149
		p.writeApplyDirective(e)
150
	case *ast.EndDirective:
151
		p.writeEndDirective(e)
152
	case *ast.CommentBlockDirective:
153
		p.writeCommentBlockDirective(e)
154
	default:
155
		fmt.Fprintf(&p.buf, "; unknown entry %T", e)
156
	}
157
	p.prevWasBlank = false
158
	p.buf.WriteByte('\n')
159
}
160
161
func (p *printer) writeSpaces(n int) {
162
	for range n {
163
		p.buf.WriteByte(' ')
164
	}
165
}
166
167
func isCommentChar(b byte) bool {
168
	return b == ';' || b == '#' || b == '%' || b == '*'
169
}
170
171
func (p *printer) writeComment(c *ast.Comment) {
172
	if c != nil && c.Text != "" {
173
		p.buf.WriteByte(c.Marker)
174
		if !isCommentChar(c.Text[0]) {
175
			p.buf.WriteByte(' ')
176
		}
177
		p.buf.WriteString(c.Text)
178
	}
179
}
180
181
func (p *printer) writeInlineComment(c *ast.Comment) {
182
	if c != nil && c.Text != "" {
183
		p.buf.WriteByte(' ')
184
		p.buf.WriteByte(' ')
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 spaces(n int) string {
194
	b := make([]byte, n)
195
	for i := range b {
196
		b[i] = ' '
197
	}
198
	return string(b)
199
}