all repos

clerk @ 7b1aef3a98bc3fbf08318ac10fc181a87a0d9f6e

missing tooling for ledger/hledger
7 files changed, 108 insertions(+), 41 deletions(-)
journal/printer: add PreserveBlankLines option
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-06-08 21:10:56 +0300
Authored at: 2026-06-08 20:15:32 +0300
Change ID: spxltvqklvtrnvqnnmulzlzkwnyssvzy
Parent: 263a106
M journal/printer/printer.go
···
        26
        26
         )

      
        27
        27
         

      
        28
        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

      
        
        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

      
        34
        35
         }

      
        35
        36
         

      
        36
        37
         var defaultConfig = &Config{

      
        37
        
        -	TabIndent:    false,

      
        38
        
        -	IndentWidth:  2,

      
        39
        
        -	AlignStyle:   AlignTwoSpaces,

      
        40
        
        -	AlignColumn:  70,

      
        41
        
        -	CommodityPos: CommodityAfter,

      
        
        38
        +	TabIndent:          false,

      
        
        39
        +	IndentWidth:        2,

      
        
        40
        +	PreserveBlankLines: false,

      
        
        41
        +	AlignStyle:         AlignTwoSpaces,

      
        
        42
        +	AlignColumn:        70,

      
        
        43
        +	CommodityPos:       CommodityAfter,

      
        42
        44
         }

      
        43
        45
         

      
        44
        46
         func (c *Config) indent() string {

      ···
        54
        56
         

      
        55
        57
         // printer holds formatting state for a single Fprint call.

      
        56
        58
         type printer struct {

      
        57
        
        -	buf    strings.Builder

      
        58
        
        -	cfg    *Config

      
        59
        
        -	indent string

      
        
        59
        +	buf          strings.Builder

      
        
        60
        +	cfg          *Config

      
        
        61
        +	indent       string

      
        
        62
        +	prevWasBlank bool

      
        60
        63
         }

      
        61
        64
         

      
        62
        65
         // Fprint formats using the default config.

      ···
        80
        83
         }

      
        81
        84
         

      
        82
        85
         // 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
        +func FprintEntry(w io.Writer, e ast.Entry) error { return defaultConfig.FprintEntry(w, e) }

      
        86
        87
         

      
        87
        88
         // FprintEntry formats a single journal entry.

      
        88
        89
         func (c *Config) FprintEntry(w io.Writer, e ast.Entry) error {

      ···
        97
        98
         

      
        98
        99
         func (p *printer) formatEntry(e ast.Entry) {

      
        99
        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

      
        100
        107
         	case *ast.Transaction:

      
        
        108
        +		p.prevWasBlank = false

      
        101
        109
         		p.writeTransaction(e)

      
        102
        110
         		return

      
        103
        111
         	case *ast.PeriodicTransaction:

      
        
        112
        +		p.prevWasBlank = false

      
        104
        113
         		p.writePeriodicTransaction(e)

      
        105
        114
         		return

      
        106
        115
         	case *ast.AutomatedTransaction:

      
        
        116
        +		p.prevWasBlank = false

      
        107
        117
         		p.writeAutomatedTransaction(e)

      
        108
        118
         		return

      
        109
        
        -	case *ast.BlankLine:

      
        110
        
        -		p.buf.WriteByte('\n')

      
        111
        
        -		return

      
        
        119
        +

      
        112
        120
         	case *ast.IgnoredDirective:

      
        113
        121
         		return // TODO:

      
        114
        122
         

      ···
        145
        153
         	default:

      
        146
        154
         		fmt.Fprintf(&p.buf, "; unknown entry %T", e)

      
        147
        155
         	}

      
        
        156
        +	p.prevWasBlank = false

      
        148
        157
         	p.buf.WriteByte('\n')

      
        149
        158
         }

      
        150
        159
         

      
M journal/printer/printer_test.go
···
        8
        8
         	"olexsmir.xyz/clerk/journal"

      
        9
        9
         )

      
        10
        10
         

      
        
        11
        +var tests = []string{"entries", "directives", "sample"}

      
        
        12
        +

      
        11
        13
         func TestRoundTrip(t *testing.T) {

      
        12
        
        -	tests := []string{"entries", "directives", "sample"}

      
        13
        14
         	for _, tname := range tests {

      
        14
        15
         		t.Run(tname, func(t *testing.T) {

      
        15
        16
         			inp := golden.Load(t, tname)

      ···
        29
        30
         }

      
        30
        31
         

      
        31
        32
         func BenchmarkPrinter(b *testing.B) {

      
        32
        
        -	for _, tname := range []string{"entries", "directives", "sample"} {

      
        
        33
        +	for _, tname := range tests {

      
        33
        34
         		b.Run(tname, func(b *testing.B) {

      
        34
        35
         			b.ReportAllocs()

      
        35
        36
         			inp := golden.Load(b, tname)

      ···
        48
        49
         	}

      
        49
        50
         }

      
        50
        51
         

      
        
        52
        +var testsWithConfig = map[string]*Config{

      
        
        53
        +	"align_right":      {AlignStyle: AlignRight, AlignColumn: 50},

      
        
        54
        +	"align_tab":        {AlignStyle: AlignTab, AlignColumn: 50},

      
        
        55
        +	"commodity_before": {CommodityPos: CommodityBefore},

      
        
        56
        +	"preserve_blanks":  {PreserveBlankLines: true},

      
        
        57
        +	"tab_indent":       {TabIndent: true},

      
        
        58
        +	"indent_width":     {IndentWidth: 4},

      
        
        59
        +}

      
        
        60
        +

      
        51
        61
         func TestRoundTrip_WithConfig(t *testing.T) {

      
        52
        
        -	tests := map[string]*Config{

      
        53
        
        -		"align_right":      {AlignStyle: AlignRight, AlignColumn: 50},

      
        54
        
        -		"align_tab":        {AlignStyle: AlignTab, AlignColumn: 50},

      
        55
        
        -		"commodity_before": {CommodityPos: CommodityBefore},

      
        56
        
        -		"tab_indent":       {TabIndent: true},

      
        57
        
        -		"indent_width":     {IndentWidth: 4},

      
        58
        
        -	}

      
        59
        
        -	for tname, tt := range tests {

      
        
        62
        +	for tname, tt := range testsWithConfig {

      
        60
        63
         		t.Run(tname, func(t *testing.T) {

      
        61
        64
         			inp := golden.Load(t, tname)

      
        62
        65
         			pf, err := journal.NewLoader().LoadBytes(tname+".journal", inp)

      ···
        74
        77
         }

      
        75
        78
         

      
        76
        79
         func BenchmarkPrinter_Config(b *testing.B) {

      
        77
        
        -	configs := map[string]*Config{

      
        78
        
        -		"default":          defaultConfig,

      
        79
        
        -		"align_right":      {AlignStyle: AlignRight, AlignColumn: 50},

      
        80
        
        -		"align_tab":        {AlignStyle: AlignTab, AlignColumn: 50},

      
        81
        
        -		"commodity_before": {CommodityPos: CommodityBefore},

      
        82
        
        -		"tab_indent":       {TabIndent: true},

      
        83
        
        -		"indent_width":     {IndentWidth: 4},

      
        84
        
        -	}

      
        85
        80
         	inp := golden.Load(b, "entries")

      
        86
        81
         	pf, err := journal.NewLoader().LoadBytes("entries.journal", inp)

      
        87
        82
         	if err != nil {

      
        88
        83
         		b.Fatal(err)

      
        89
        84
         	}

      
        90
        
        -	for name, cfg := range configs {

      
        
        85
        +	for name, cfg := range testsWithConfig {

      
        91
        86
         		b.Run(name, func(b *testing.B) {

      
        92
        87
         			b.ReportAllocs()

      
        93
        88
         			b.ResetTimer()

      
M journal/printer/testdata/commodity_before.golden
···
        1
        1
         2026-01-15 * Groceries

      
        2
        2
           Expenses:Food  $50.00

      
        3
        
        -  Assets:Cash  $-50.00

      
        
        3
        +  Assets:Cash

      
        4
        4
         

      
        5
        5
         2026-01-16 Salary

      
        6
        6
           Assets:Cash  $2000.00

      
M journal/printer/testdata/commodity_before.input
···
        1
        1
         2026-01-15 * Groceries

      
        2
        2
           Expenses:Food  50.00$

      
        3
        
        -  Assets:Cash  -50.00$

      
        
        3
        +  Assets:Cash

      
        4
        4
         

      
        5
        5
         2026-01-16 Salary

      
        6
        
        -  Assets:Cash  2000.00$

      
        
        6
        +  Assets:Cash  $2000.00

      
        7
        7
           Income:Salary  -2000.00$

      
A journal/printer/testdata/preserve_blanks.golden
···
        
        1
        +; accounts {{{

      
        
        2
        +account assets

      
        
        3
        +account assets:bank

      
        
        4
        +account assets:cash

      
        
        5
        +account equity

      
        
        6
        +account income

      
        
        7
        +account income:salary

      
        
        8
        +account income:invenstment

      
        
        9
        +account expenses

      
        
        10
        +account expenses:education

      
        
        11
        +; }}}

      
        
        12
        +; 01 Jan {{{

      
        
        13
        +2026-01-01 opening balances

      
        
        14
        +  assets:bank  123.00 USD

      
        
        15
        +  assets:cash  1234.12 USD

      
        
        16
        +  equity:open

      
        
        17
        +

      
        
        18
        +2026-01-04 selary

      
        
        19
        +  income:salary  543.22$

      
        
        20
        +  assets:bank

      
        
        21
        +; }}}

      
        
        22
        +; 02 Feb {{{

      
        
        23
        +2026-02-01 internet

      
        
        24
        +  expenses:utilities  350.00 UAH

      
        
        25
        +  assets:bank

      
        
        26
        +

      
        
        27
        +

      
        
        28
        +2026-02-10 * something | 2 months of *income*

      
        
        29
        +  assets:bank  1.50 USD ; jan

      
        
        30
        +  assets:bank  1.00 USD ; feb

      
        
        31
        +  income:invenstment

      
A journal/printer/testdata/preserve_blanks.input
···
        
        1
        +; accounts {{{

      
        
        2
        +account assets

      
        
        3
        +account assets:bank

      
        
        4
        +account assets:cash

      
        
        5
        +account equity

      
        
        6
        +account income

      
        
        7
        +account income:salary

      
        
        8
        +account income:invenstment

      
        
        9
        +account expenses

      
        
        10
        +account expenses:education

      
        
        11
        +; }}}

      
        
        12
        +; 01 Jan {{{

      
        
        13
        +2026-01-01 opening balances

      
        
        14
        +  assets:bank  123.00 USD

      
        
        15
        +  assets:cash  1234.12 USD

      
        
        16
        +  equity:open

      
        
        17
        +

      
        
        18
        +2026-01-04 selary

      
        
        19
        +  income:salary  $543.22

      
        
        20
        +  assets:bank

      
        
        21
        +; }}}

      
        
        22
        +; 02 Feb {{{

      
        
        23
        +2026-02-01 internet

      
        
        24
        +  expenses:utilities  350 UAH

      
        
        25
        +  assets:bank

      
        
        26
        +

      
        
        27
        +

      
        
        28
        +2026-02-10 * something | 2 months of *income*

      
        
        29
        +    assets:bank  1.50 USD ; jan

      
        
        30
        +    assets:bank  USD 1 ; feb

      
        
        31
        +    income:invenstment

      
M journal/printer/testdata/sample.input
···
        24
        24
           expenses:utilities  350 UAH

      
        25
        25
           assets:bank

      
        26
        26
         

      
        
        27
        +

      
        27
        28
         2026-02-10 * something | 2 months of *income*

      
        28
        29
             assets:bank  1.50 USD ; jan

      
        29
        30
             assets:bank  USD 1 ; feb