package printer import ( "fmt" "io" "strings" "olexsmir.xyz/clerk/journal/ast" ) // AlignStyle controls how postings are aligned type AlignStyle int const ( AlignTwoSpaces AlignStyle = iota // " Account $10.00" AlignRight // amounts at fixed col: " Account $10.00" AlignTab // elastic tabstops ) // CommodityPos controls where the commodity marker is placed type CommodityPos int const ( CommodityAfter CommodityPos = iota // "10.00 EUR" CommodityBefore // "$10.00" ) type Config struct { TabIndent bool // true = tabs, false = spaces IndentWidth int // spaces per indent level (default: 2) AlignStyle AlignStyle // default AlignTwoSpaces AlignColumn int // fixed column for AlignRight CommentWidth int // wrap standalone comments at this width CommodityPos CommodityPos // where to place commodity } var defaultConfig = &Config{ TabIndent: false, IndentWidth: 2, AlignStyle: AlignTwoSpaces, AlignColumn: 70, CommentWidth: 90, CommodityPos: CommodityAfter, } func (c *Config) indent() string { if c.TabIndent { return "\t" } n := 2 if c.IndentWidth > 0 { n = c.IndentWidth } return spaces(n) } // printer holds formatting state for a single Fprint call. type printer struct { buf strings.Builder cfg *Config indent string } // Fprint formats using the default config. func Fprint(w io.Writer, j *ast.Journal) error { return defaultConfig.Fprint(w, j) } // Fprint formats a parsed journal. func (c *Config) Fprint(w io.Writer, j *ast.Journal) error { if c == nil { c = defaultConfig } p := printer{cfg: c, indent: c.indent()} for _, e := range j.Entries { p.formatEntry(e) } // allow exactly one trailing newline out := strings.TrimRight(p.buf.String(), "\n") + "\n" _, err := io.WriteString(w, out) return err } func (p *printer) formatEntry(e ast.Entry) { switch e := e.(type) { case *ast.Transaction: p.writeTransaction(e) return case *ast.PeriodicTransaction: p.writePeriodicTransaction(e) return case *ast.AutomatedTransaction: p.writeAutomatedTransaction(e) return case *ast.BlankLine: p.buf.WriteByte('\n') return case *ast.Comment: p.writeComment(e) case *ast.AccountDirective: p.writeAccountDirective(e) case *ast.CommodityDirective: p.writeCommodityDirective(e) case *ast.IncludeDirective: p.writeIncludeDirective(e) case *ast.AliasDirective: p.writeAliasDirective(e) case *ast.PayeeDirective: p.writePayeeDirective(e) case *ast.TagDirective: p.writeTagDirective(e) case *ast.YearDirective: p.writeYearDirective(e) case *ast.DecimalMarkDirective: p.writeDecimalMarkDirective(e) case *ast.MarketPriceDirective: p.writeMarketPriceDirective(e) case *ast.ConversionDirective: p.writeConversionDirective(e) case *ast.DefaultCommodityDirective: p.writeDefaultCommodityDirective(e) case *ast.ApplyDirective: p.writeApplyDirective(e) case *ast.EndDirective: p.writeEndDirective(e) case *ast.CommentBlockDirective: p.writeCommentBlockDirective(e) case *ast.IgnoredDirective: return // TODO: default: fmt.Fprintf(&p.buf, "; unknown entry %T", e) } p.buf.WriteByte('\n') } func (p *printer) writeSpaces(n int) { for range n { p.buf.WriteByte(' ') } } func (p *printer) writeComment(c *ast.Comment) { if c != nil && c.Text != "" { p.buf.WriteByte(c.Marker) // TODO: support other markers p.buf.WriteByte(' ') p.buf.WriteString(c.Text) } } func (p *printer) writeInlineComment(c *ast.Comment) { if c != nil && c.Text != "" { p.buf.WriteByte(' ') p.buf.WriteByte(c.Marker) // TODO: support other markers p.buf.WriteByte(' ') p.buf.WriteString(c.Text) } } func spaces(n int) string { b := make([]byte, n) for i := range b { b[i] = ' ' } return string(b) }