all repos

clerk @ 20a67e087c8413b7ae18b5742d707519f64f2131

missing tooling for ledger/hledger
84 files changed, 1055 insertions(+), 367 deletions(-)
add support of subdirectives and it's highlights
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-08-12 16:49:20 +0300
Authored at: 2026-08-12 16:17:28 +0300
Change ID: qnkoyolslopnwkwkowmzrmlnloqluxyl
Parent: 2a7dd08
jump to
M internal/analyzer/analyzer.go
M internal/analyzer/analyzer_test.go
M internal/analyzer/build.go
M internal/analyzer/testdata/account-indexing.txtar
M internal/analyzer/testdata/account-usage.txtar
A internal/analyzer/testdata/aliases.txtar
M internal/analyzer/testdata/commodity-usage.txtar
M internal/analyzer/testdata/cost-balance.txtar
M internal/analyzer/testdata/dates.txtar
M internal/analyzer/testdata/decimal-marks.txtar
M internal/analyzer/testdata/directives.txtar
M internal/analyzer/testdata/duplicate-transactions.txtar
M internal/analyzer/testdata/empty.txtar
M internal/analyzer/testdata/include.txtar
M internal/analyzer/testdata/journal.txtar
M internal/analyzer/testdata/payees.txtar
M internal/analyzer/testdata/periodic-automated.txtar
M internal/analyzer/testdata/posting-types.txtar
M internal/analyzer/testdata/tags.txtar
M internal/analyzer/testdata/typed-entries.txtar
M internal/linter/rule_invalid_type_tag.go
M internal/linter/rule_missing_commodity.go
M internal/linter/rule_undeclared_account.go
M internal/linter/rule_unused_account.go
M internal/linter/testdata/invalid-type-tag.txtar
M internal/linter/testdata/undeclared-account.txtar
M internal/linter/testdata/unused-account.txtar
M internal/lsp/testdata/definition-journal.txtar
M internal/lsp/testdata/rename-account.txtar
M internal/lsp/testdata/semantic-directives.txtar
M internal/lsp/testdata/semantic-with-errors.txtar
M internal/lsp/textdocument_completion.go
M internal/lsp/textdocument_definition.go
M internal/lsp/textdocument_format.go
M internal/lsp/textdocument_rename.go
M internal/lsp/textdocument_semantic.go
M internal/lsp/textdocument_semantic_test.go
M internal/lsp/textdocument_sync.go
M journal/ast/directives.go
M journal/ast/dump.go
M journal/ast/entries.go
M journal/journal_test.go
M journal/lexer/benchmark_test.go
M journal/lexer/lexer.go
M journal/lexer/lexer_test.go
M journal/loader.go
M journal/parser/benchmark_test.go
M journal/parser/parser.go
M journal/parser/parser_test.go
M journal/parser/testdata/C_directive.txtar
M journal/parser/testdata/D_directive.txtar
A journal/parser/testdata/account_subdirectives_types.txtar
M journal/parser/testdata/account_with_subdirectives.txtar
M journal/parser/testdata/automated_transaction.txtar
M journal/parser/testdata/bad_between_good.txtar
M journal/parser/testdata/bad_posting_at_end.txtar
M journal/parser/testdata/bad_then_next_transaction.txtar
M journal/parser/testdata/comment_between_bad_postings.txtar
A journal/parser/testdata/commodity_format_errors.txtar
M journal/parser/testdata/commodity_quantity_after.txtar
M journal/parser/testdata/commodity_quantity_first.txtar
M journal/parser/testdata/commodity_with_subdirectives.txtar
M journal/parser/testdata/comodity_directive_no_space.txtar
M journal/parser/testdata/illegal_between_transactions.txtar
M journal/parser/testdata/multiple_bad_postings.txtar
M journal/parser/testdata/period_transaction_expressions.txtar
M journal/parser/testdata/quoted_payee_names.txtar
M journal/parser/testdata/recovery_after_bad_posting.txtar
M journal/parser/testdata/three_bad_postings.txtar
M journal/parser/testdata/transaction_in_ukrainian.txtar
M journal/parser/testdata/transaction_with_balance_assignment.txtar
M journal/parser/testdata/transaction_with_cost_and_assertion.txtar
M journal/parser/testdata/transaction_with_costs.txtar
M journal/parser/testdata/transaction_with_inclusive_balance_assertion.txtar
M journal/parser/testdata/transaction_with_scientific_amounts.txtar
M journal/parser/testdata/transaction_with_spaced_account_name.txtar
M journal/parser/testdata/transaction_with_strange_commodity_symbols.txtar
M journal/parser/testdata/transaction_with_tag_comments.txtar
M journal/parser/testdata/transaction_with_trailing_indent.txtar
M journal/parser/testdata/transaction_with_virtual_accounts.txtar
M journal/parser/testdata/virtual_postings_with_statuses.txtar
M journal/printer/directives.go
M journal/printer/testdata/directives.txtar
A journal/testdata/journals/subdirectives.journal
M internal/analyzer/analyzer.go
···
        17
        17
         	Commodities map[string]*CommodityInfo

      
        18
        18
         	Payees      map[string]*PayeeInfo

      
        19
        19
         

      
        
        20
        +	// AccountAliases maps alias names to the account they resolve to, from

      
        
        21
        +	// account "alias" subdirectives and top-level "alias A = B" directives.

      
        
        22
        +	AccountAliases map[string]string

      
        
        23
        +

      
        20
        24
         	AccountNames     []string            // sorted for binary search

      
        21
        25
         	PayeeNames       []string            // sorted, all payee names from directives + usage

      
        22
        26
         	AccountsByPrefix map[string][]string // "expenses:" -> ["expenses:food", "expenses:taxi"]

      
M internal/analyzer/analyzer_test.go
···
        31
        31
         		"tags",

      
        32
        32
         		"decimal-marks",

      
        33
        33
         		"duplicate-transactions",

      
        
        34
        +		"aliases",

      
        34
        35
         	}

      
        35
        36
         

      
        36
        37
         	for _, tname := range tests {

      ···
        109
        110
         		if info.LastUsed.Year != 0 {

      
        110
        111
         			fmt.Fprintf(w, "    last-used: %d-%02d-%02d\n", info.LastUsed.Year, info.LastUsed.Month, info.LastUsed.Day)

      
        111
        112
         		}

      
        
        113
        +	}

      
        
        114
        +

      
        
        115
        +	// account aliases

      
        
        116
        +	anames = anames[:0]

      
        
        117
        +	for name := range a.AccountAliases {

      
        
        118
        +		anames = append(anames, name)

      
        
        119
        +	}

      
        
        120
        +	sort.Strings(anames)

      
        
        121
        +	fmt.Fprintf(w, "\naliases (%d):\n", len(anames))

      
        
        122
        +	for _, name := range anames {

      
        
        123
        +		fmt.Fprintf(w, "  %s -> %s\n", name, a.AccountAliases[name])

      
        112
        124
         	}

      
        113
        125
         

      
        114
        126
         	// payees

      
M internal/analyzer/build.go
···
        19
        19
         	a := &Analysis{

      
        20
        20
         		Files:                 rj.Occurrences,

      
        21
        21
         		Accounts:              make(map[string]*AccountInfo),

      
        
        22
        +		AccountAliases:        make(map[string]string),

      
        22
        23
         		Commodities:           make(map[string]*CommodityInfo),

      
        23
        24
         		Payees:                make(map[string]*PayeeInfo),

      
        24
        25
         		Tags:                  make(map[string]*TagInfo),

      ···
        62
        63
         		a.addAccountDirective(e)

      
        63
        64
         	case *ast.CommodityDirective:

      
        64
        65
         		a.addCommodityDirective(e)

      
        
        66
        +	case *ast.AliasDirective:

      
        
        67
        +		a.addAliasDirective(e)

      
        65
        68
         	case *ast.PayeeDirective:

      
        66
        69
         		a.addPayeeDirective(e)

      
        67
        70
         	case *ast.TagDirective:

      ···
        119
        122
         		a.Accounts[aname] = info

      
        120
        123
         	}

      
        121
        124
         	info.Directives = append(info.Directives, ad)

      
        
        125
        +	for _, sd := range ad.Subdirectives {

      
        
        126
        +		if sd.Name == "alias" {

      
        
        127
        +			a.AccountAliases[sd.Value] = aname

      
        
        128
        +		}

      
        
        129
        +	}

      
        
        130
        +}

      
        
        131
        +

      
        
        132
        +// addAliasDirective records a top-level "alias A = B" directive; the alias

      
        
        133
        +// source name resolves to the target account.

      
        
        134
        +func (a *Analysis) addAliasDirective(ad *ast.AliasDirective) {

      
        
        135
        +	a.AccountAliases[ad.From.String()] = ad.To.String()

      
        122
        136
         }

      
        123
        137
         

      
        124
        138
         func (a *Analysis) addPayeeDirective(pd *ast.PayeeDirective) {

      
M internal/analyzer/testdata/account-indexing.txtar
···
        51
        51
               file 0: expenses:transport:fuel

      
        52
        52
             last-used: 2024-01-02

      
        53
        53
         

      
        
        54
        +aliases (0):

      
        
        55
        +

      
        54
        56
         payees (2):

      
        55
        57
           groceries

      
        56
        58
             directives: 0

      ···
        106
        108
         

      
        107
        109
         tag values (0):

      
        108
        110
         

      
        109
        
        -commodity decimal marks (1):

      
        110
        
        -  $: .

      
        
        111
        +commodity decimal marks (0):

      
M internal/analyzer/testdata/account-usage.txtar
···
        43
        43
               file 0: expenses:food

      
        44
        44
             last-used: 2024-01-15

      
        45
        45
         

      
        
        46
        +aliases (0):

      
        
        47
        +

      
        46
        48
         payees (3):

      
        47
        49
           dinner

      
        48
        50
             directives: 0

      ···
        99
        101
         

      
        100
        102
         tag values (0):

      
        101
        103
         

      
        102
        
        -commodity decimal marks (1):

      
        103
        
        -  $: .

      
        
        104
        +commodity decimal marks (0):

      
A internal/analyzer/testdata/aliases.txtar
···
        
        1
        +-- in.journal --

      
        
        2
        +account expenses:food

      
        
        3
        +  alias food

      
        
        4
        +  alias groceries

      
        
        5
        +

      
        
        6
        +alias checking = assets:bank:checking

      
        
        7
        +alias food = expenses:food

      
        
        8
        +

      
        
        9
        +2024-01-01 groceries

      
        
        10
        +    food  $10

      
        
        11
        +    checking  $5

      
        
        12
        +

      
        
        13
        +-- expect --

      
        
        14
        +files (1):

      
        
        15
        +  0: in.journal

      
        
        16
        +

      
        
        17
        +commodities (1):

      
        
        18
        +  $

      
        
        19
        +    directives: 0

      
        
        20
        +    used: 2

      
        
        21
        +    usages: 2

      
        
        22
        +      file 0: $ ("10")

      
        
        23
        +      file 0: $ ("5")

      
        
        24
        +    last-used: 2024-01-01

      
        
        25
        +

      
        
        26
        +accounts (3):

      
        
        27
        +  checking

      
        
        28
        +    directives: 0

      
        
        29
        +    used: 1

      
        
        30
        +    usages: 1

      
        
        31
        +      file 0: checking

      
        
        32
        +    last-used: 2024-01-01

      
        
        33
        +  expenses:food

      
        
        34
        +    directives: 1

      
        
        35
        +    used: 0

      
        
        36
        +      account expenses:food

      
        
        37
        +    usages: 0

      
        
        38
        +  food

      
        
        39
        +    directives: 0

      
        
        40
        +    used: 1

      
        
        41
        +    usages: 1

      
        
        42
        +      file 0: food

      
        
        43
        +    last-used: 2024-01-01

      
        
        44
        +

      
        
        45
        +aliases (3):

      
        
        46
        +  checking -> assets:bank:checking

      
        
        47
        +  food -> expenses:food

      
        
        48
        +  groceries -> expenses:food

      
        
        49
        +

      
        
        50
        +payees (1):

      
        
        51
        +  groceries

      
        
        52
        +    directives: 0

      
        
        53
        +    used: 1

      
        
        54
        +    usages: 1

      
        
        55
        +      file 0: groceries

      
        
        56
        +

      
        
        57
        +prefixes (1):

      
        
        58
        +  expenses:

      
        
        59
        +    expenses:food

      
        
        60
        +

      
        
        61
        +transactions: 1

      
        
        62
        +periodic transactions: 0

      
        
        63
        +automated transactions: 0

      
        
        64
        +

      
        
        65
        +directives (3):

      
        
        66
        +  account expenses:food

      
        
        67
        +  alias checking = assets:bank:checking

      
        
        68
        +  alias food = expenses:food

      
        
        69
        +

      
        
        70
        +transaction keys (1):

      
        
        71
        +  2024-01-01|groceries|food,checking,

      
        
        72
        +

      
        
        73
        +payee templates (1):

      
        
        74
        +  groceries

      
        
        75
        +    food  $10

      
        
        76
        +    checking  $5

      
        
        77
        +

      
        
        78
        +dates (1):

      
        
        79
        +  2024-01-01

      
        
        80
        +

      
        
        81
        +tags (0):

      
        
        82
        +

      
        
        83
        +tag names (0):

      
        
        84
        +

      
        
        85
        +tag values (0):

      
        
        86
        +

      
        
        87
        +commodity decimal marks (0):

      
M internal/analyzer/testdata/commodity-usage.txtar
···
        53
        53
               file 0: expenses:food

      
        54
        54
             last-used: 2024-01-03

      
        55
        55
         

      
        
        56
        +aliases (0):

      
        
        57
        +

      
        56
        58
         payees (3):

      
        57
        59
           exchange

      
        58
        60
             directives: 0

      ···
        109
        111
         

      
        110
        112
         tag values (0):

      
        111
        113
         

      
        112
        
        -commodity decimal marks (3):

      
        113
        
        -  $: .

      
        114
        
        -  AAPL: .

      
        115
        
        -  EUR: .

      
        
        114
        +commodity decimal marks (0):

      
M internal/analyzer/testdata/cost-balance.txtar
···
        39
        39
               file 0: assets:stocks

      
        40
        40
             last-used: 2024-01-01

      
        41
        41
         

      
        
        42
        +aliases (0):

      
        
        43
        +

      
        42
        44
         payees (1):

      
        43
        45
           buy stock

      
        44
        46
             directives: 0

      ···
        75
        77
         

      
        76
        78
         tag values (0):

      
        77
        79
         

      
        78
        
        -commodity decimal marks (2):

      
        
        80
        +commodity decimal marks (1):

      
        79
        81
           $: .

      
        80
        
        -  AAPL: .

      
M internal/analyzer/testdata/dates.txtar
···
        50
        50
               file 0: expenses:food

      
        51
        51
             last-used: 2024-01-03

      
        52
        52
         

      
        
        53
        +aliases (0):

      
        
        54
        +

      
        53
        55
         payees (4):

      
        54
        56
           also-first

      
        55
        57
             directives: 0

      ···
        115
        117
         

      
        116
        118
         tag values (0):

      
        117
        119
         

      
        118
        
        -commodity decimal marks (1):

      
        119
        
        -  $: .

      
        
        120
        +commodity decimal marks (0):

      
M internal/analyzer/testdata/decimal-marks.txtar
···
        31
        31
               file 0: expenses:food

      
        32
        32
             last-used: 2024-01-01

      
        33
        33
         

      
        
        34
        +aliases (0):

      
        
        35
        +

      
        34
        36
         payees (1):

      
        35
        37
           groceries

      
        36
        38
             directives: 0

      
M internal/analyzer/testdata/directives.txtar
···
        33
        33
               account expenses:food

      
        34
        34
             usages: 0

      
        35
        35
         

      
        
        36
        +aliases (1):

      
        
        37
        +  expenses:food -> expenses:food:old

      
        
        38
        +

      
        36
        39
         payees (1):

      
        37
        40
           TestPayee

      
        38
        41
             directives: 1

      
M internal/analyzer/testdata/duplicate-transactions.txtar
···
        36
        36
               file 0: expenses:food

      
        37
        37
             last-used: 2024-01-01

      
        38
        38
         

      
        
        39
        +aliases (0):

      
        
        40
        +

      
        39
        41
         payees (1):

      
        40
        42
           groceries

      
        41
        43
             directives: 0

      ···
        73
        75
         

      
        74
        76
         tag values (0):

      
        75
        77
         

      
        76
        
        -commodity decimal marks (1):

      
        77
        
        -  $: .

      
        
        78
        +commodity decimal marks (0):

      
M internal/analyzer/testdata/empty.txtar
···
        8
        8
         

      
        9
        9
         accounts (0):

      
        10
        10
         

      
        
        11
        +aliases (0):

      
        
        12
        +

      
        11
        13
         payees (0):

      
        12
        14
         

      
        13
        15
         prefixes (0):

      
M internal/analyzer/testdata/include.txtar
···
        36
        36
               file 0: expenses:food

      
        37
        37
             last-used: 2024-01-01

      
        38
        38
         

      
        
        39
        +aliases (0):

      
        
        40
        +

      
        39
        41
         payees (1):

      
        40
        42
           test

      
        41
        43
             directives: 0

      ···
        73
        75
         

      
        74
        76
         tag values (0):

      
        75
        77
         

      
        76
        
        -commodity decimal marks (1):

      
        77
        
        -  $: .

      
        
        78
        +commodity decimal marks (0):

      
M internal/analyzer/testdata/journal.txtar
···
        93
        93
             usages: 1

      
        94
        94
               file 0: expenses:transport

      
        95
        95
         

      
        
        96
        +aliases (0):

      
        
        97
        +

      
        96
        98
         payees (3):

      
        97
        99
           aws

      
        98
        100
             directives: 0

      ···
        158
        160
         

      
        159
        161
         tag values (0):

      
        160
        162
         

      
        161
        
        -commodity decimal marks (2):

      
        162
        
        -  $: .

      
        
        163
        +commodity decimal marks (1):

      
        163
        164
           USD: .

      
M internal/analyzer/testdata/payees.txtar
···
        48
        48
               file 0: expenses:food

      
        49
        49
             last-used: 2024-01-03

      
        50
        50
         

      
        
        51
        +aliases (0):

      
        
        52
        +

      
        51
        53
         payees (2):

      
        52
        54
           groceries

      
        53
        55
             directives: 0

      ···
        98
        100
         

      
        99
        101
         tag values (0):

      
        100
        102
         

      
        101
        
        -commodity decimal marks (1):

      
        102
        
        -  $: .

      
        
        103
        +commodity decimal marks (0):

      
M internal/analyzer/testdata/periodic-automated.txtar
···
        33
        33
               file 0: expenses:food

      
        34
        34
               file 0: expenses:food

      
        35
        35
         

      
        
        36
        +aliases (0):

      
        
        37
        +

      
        36
        38
         payees (0):

      
        37
        39
         

      
        38
        40
         prefixes (2):

      ···
        59
        61
         

      
        60
        62
         tag values (0):

      
        61
        63
         

      
        62
        
        -commodity decimal marks (1):

      
        63
        
        -  $: .

      
        
        64
        +commodity decimal marks (0):

      
M internal/analyzer/testdata/posting-types.txtar
···
        38
        38
               file 0: virtual:unbalanced

      
        39
        39
             last-used: 2024-01-01

      
        40
        40
         

      
        
        41
        +aliases (0):

      
        
        42
        +

      
        41
        43
         payees (1):

      
        42
        44
           test

      
        43
        45
             directives: 0

      ···
        76
        78
         

      
        77
        79
         tag values (0):

      
        78
        80
         

      
        79
        
        -commodity decimal marks (1):

      
        80
        
        -  $: .

      
        
        81
        +commodity decimal marks (0):

      
M internal/analyzer/testdata/tags.txtar
···
        41
        41
               file 0: expenses:consulting

      
        42
        42
             last-used: 2024-02-03

      
        43
        43
         

      
        
        44
        +aliases (0):

      
        
        45
        +

      
        44
        46
         payees (1):

      
        45
        47
           Client work

      
        46
        48
             directives: 0

      ···
        132
        134
           extra

      
        133
        135
           project-xyz

      
        134
        136
         

      
        135
        
        -commodity decimal marks (1):

      
        136
        
        -  $: .

      
        
        137
        +commodity decimal marks (0):

      
M internal/analyzer/testdata/typed-entries.txtar
···
        58
        58
             usages: 1

      
        59
        59
               file 0: expenses:transport

      
        60
        60
         

      
        
        61
        +aliases (0):

      
        
        62
        +

      
        61
        63
         payees (1):

      
        62
        64
           groceries

      
        63
        65
             directives: 0

      ···
        98
        100
         

      
        99
        101
         tag values (0):

      
        100
        102
         

      
        101
        
        -commodity decimal marks (1):

      
        102
        
        -  $: .

      
        
        103
        +commodity decimal marks (0):

      
M internal/linter/rule_invalid_type_tag.go
···
        8
        8
         	"olexsmir.xyz/clerk/journal/ast"

      
        9
        9
         )

      
        10
        10
         

      
        11
        
        -// InvalidTypeTag flags type tag on account directive whose value is not valid account type code.

      
        
        11
        +// InvalidTypeTag flags account type declarations (the type: tag and the type

      
        
        12
        +// subdirective) whose value is not a valid account type code.

      
        12
        13
         type InvalidTypeTag struct{}

      
        13
        14
         

      
        14
        15
         func (InvalidTypeTag) ID() RuleID         { return "invalid-type-tag" }

      ···
        17
        18
         	var finds []Find

      
        18
        19
         	for _, entry := range an.Directives {

      
        19
        20
         		ad, ok := entry.(*ast.AccountDirective)

      
        20
        
        -		if !ok || ad.Comment == nil {

      
        
        21
        +		if !ok {

      
        21
        22
         			continue

      
        22
        23
         		}

      
        23
        
        -		for _, tag := range ad.Comment.Tags {

      
        24
        
        -			if tag.Key != "type" {

      
        
        24
        +		if ad.Comment != nil {

      
        
        25
        +			for _, tag := range ad.Comment.Tags {

      
        
        26
        +				if tag.Key != "type" {

      
        
        27
        +					continue

      
        
        28
        +				}

      
        
        29
        +				if err := i.parseAccountTypeCode(tag.Value); err != nil {

      
        
        30
        +					finds = append(finds, Find{

      
        
        31
        +						Code:     i.ID(),

      
        
        32
        +						Severity: i.Severity(),

      
        
        33
        +						Span:     tag.Span,

      
        
        34
        +						Message:  fmt.Sprintf("invalid type: tag value %q", tag.Value),

      
        
        35
        +					})

      
        
        36
        +				}

      
        
        37
        +			}

      
        
        38
        +		}

      
        
        39
        +		for _, sd := range ad.Subdirectives {

      
        
        40
        +			if sd.Name != "type" || sd.Value == "" {

      
        
        41
        +				// empty values are flagged by the parser

      
        25
        42
         				continue

      
        26
        43
         			}

      
        27
        
        -			if err := i.parseAccountTypeCode(tag.Value); err != nil {

      
        
        44
        +			if err := i.parseAccountTypeCode(sd.Value); err != nil {

      
        28
        45
         				finds = append(finds, Find{

      
        29
        46
         					Code:     i.ID(),

      
        30
        47
         					Severity: i.Severity(),

      
        31
        
        -					Span:     tag.Span,

      
        32
        
        -					Message:  fmt.Sprintf("invalid type: tag value %q", tag.Value),

      
        
        48
        +					Span:     sd.ValueSpan,

      
        
        49
        +					Message:  fmt.Sprintf("invalid type subdirective value %q", sd.Value),

      
        33
        50
         				})

      
        34
        51
         			}

      
        35
        52
         		}

      
M internal/linter/rule_missing_commodity.go
···
        29
        29
         			m.check(&finds, e.From)

      
        30
        30
         			m.check(&finds, e.To)

      
        31
        31
         		case *ast.CommodityDirective:

      
        32
        
        -			if e.Format.Commodity != "" {

      
        33
        
        -				m.check(&finds, e.Format)

      
        
        32
        +			if e.FormatSub != nil && e.FormatSub.Amount.Commodity != "" {

      
        
        33
        +				m.check(&finds, e.FormatSub.Amount)

      
        34
        34
         			}

      
        35
        35
         		case *ast.DefaultCommodityDirective:

      
        36
        36
         			m.check(&finds, e.Amount)

      
M internal/linter/rule_undeclared_account.go
···
        17
        17
         		if len(info.Directives) > 0 {

      
        18
        18
         			continue

      
        19
        19
         		}

      
        
        20
        +		if _, aliased := an.AccountAliases[name]; aliased {

      
        
        21
        +			continue

      
        
        22
        +		}

      
        20
        23
         		for _, usage := range info.Usages {

      
        21
        24
         			finds = append(finds, Find{

      
        22
        25
         				Code:     u.ID(),

      
M internal/linter/rule_unused_account.go
···
        20
        20
         		if len(info.Usages) > 0 {

      
        21
        21
         			continue

      
        22
        22
         		}

      
        
        23
        +		if _, aliased := an.AccountAliases[name]; aliased {

      
        
        24
        +			continue

      
        
        25
        +		}

      
        23
        26
         		for _, d := range info.Directives {

      
        24
        27
         			finds = append(finds, Find{

      
        25
        28
         				Code:     u.ID(),

      
M internal/linter/testdata/invalid-type-tag.txtar
···
        14
        14
         account typo:code      ; type:Expenc

      
        15
        15
         

      
        16
        16
         account caps:key       ; TYPE:asset

      
        
        17
        +account sub:type

      
        
        18
        +  type Z

      
        
        19
        +account sub:good

      
        
        20
        +  type expense

      
        
        21
        +account sub:bad

      
        
        22
        +  type Expenc

      
        
        23
        +account sub:empty

      
        
        24
        +  type

      
        17
        25
         2026-01-15 Shopping    ; type:whatever

      
        18
        26
           Expenses:Food   $10

      
        19
        27
           Assets:Cash

      ···
        24
        32
         in.journal:11:26: invalid-type-tag: invalid type: tag value "Z"

      
        25
        33
         in.journal:12:26: invalid-type-tag: invalid type: tag value ""

      
        26
        34
         in.journal:13:26: invalid-type-tag: invalid type: tag value "Expenc"

      
        
        35
        +in.journal:17:8: invalid-type-tag: invalid type subdirective value "Z"

      
        
        36
        +in.journal:21:8: invalid-type-tag: invalid type subdirective value "Expenc"

      
M internal/linter/testdata/undeclared-account.txtar
···
        1
        1
         -- in.journal --

      
        2
        2
         account Expenses:Food

      
        
        3
        +  alias food

      
        
        4
        +

      
        
        5
        +account Assets:Bank

      
        
        6
        +  alias bank

      
        
        7
        +

      
        
        8
        +alias cash = Assets:Cash

      
        3
        9
         

      
        4
        10
         2026-01-15 * "Lunch"

      
        5
        
        -  Expenses:Food      $10

      
        
        11
        +  food      $10

      
        6
        12
           Assets:Cash

      
        7
        13
         

      
        8
        14
         2026-01-16 * "Dinner"

      
        9
        15
           Expenses:Food      $15

      
        10
        
        -  Assets:Cash

      
        
        16
        +  cash

      
        11
        17
         

      
        12
        18
         -- expect --

      
        13
        
        -in.journal:5:3: undeclared-account: undeclared account: Assets:Cash

      
        14
        
        -in.journal:9:3: undeclared-account: undeclared account: Assets:Cash

      
        
        19
        +in.journal:11:3: undeclared-account: undeclared account: Assets:Cash

      
M internal/linter/testdata/unused-account.txtar
···
        1
        1
         -- in.journal --

      
        2
        2
         account assets:cash

      
        3
        3
         account equity:opening

      
        
        4
        +  alias opening

      
        4
        5
         account assets:cash

      
        5
        6
         

      
        6
        7
         2024-05-02 * "groceries"

      
M internal/lsp/testdata/definition-journal.txtar
···
        1
        1
         -- in.journal --

      
        2
        2
         account ex^penses:food

      
        3
        3
         account assets:checking

      
        
        4
        +account expenses:food

      
        
        5
        +  alias foo^d

      
        
        6
        +alias fo^od = expenses:food

      
        
        7
        +alias food = expenses:fo^od

      
        4
        8
         account untracked

      
        5
        9
             no^te ignore

      
        6
        10
         

      ···
        30
        34
         

      
        31
        35
         -- expect --

      
        32
        36
         0:10 in.journal 0:8-0:21

      
        33
        
        -3:6 <none>

      
        34
        
        -5:10 in.journal 5:10-5:11

      
        35
        
        -8:13 in.journal 8:6-8:19

      
        36
        
        -10:0 <none>

      
        37
        
        -10:18 in.journal 8:6-8:19

      
        38
        
        -10:29 <none>

      
        39
        
        -11:4 in.journal 0:8-0:21

      
        40
        
        -11:17 in.journal 0:8-0:21

      
        41
        
        -11:21 <none>

      
        42
        
        -12:19 in.journal 5:10-5:11

      
        43
        
        -13:23 in.journal 5:10-5:11

      
        44
        
        -14:25 in.journal 14:24-14:27

      
        45
        
        -16:18 in.journal 16:11-16:24

      
        46
        
        -18:25 in.journal 14:24-14:27

      
        47
        
        -19:27 in.journal 5:10-5:11

      
        48
        
        -25:15 in.journal 21:11-21:19

      
        49
        
        -27:13 in.journal 14:4-14:15

      
        
        37
        +3:11 in.journal 0:8-0:21

      
        
        38
        +4:8 in.journal 0:8-0:21

      
        
        39
        +5:24 in.journal 0:8-0:21

      
        
        40
        +7:6 <none>

      
        
        41
        +9:10 in.journal 9:10-9:11

      
        
        42
        +12:13 in.journal 12:6-12:19

      
        
        43
        +14:0 <none>

      
        
        44
        +14:18 in.journal 12:6-12:19

      
        
        45
        +14:29 <none>

      
        
        46
        +15:4 in.journal 0:8-0:21

      
        
        47
        +15:17 in.journal 0:8-0:21

      
        
        48
        +15:21 <none>

      
        
        49
        +16:19 in.journal 9:10-9:11

      
        
        50
        +17:23 in.journal 9:10-9:11

      
        
        51
        +18:25 in.journal 18:24-18:27

      
        
        52
        +20:18 in.journal 20:11-20:24

      
        
        53
        +22:25 in.journal 18:24-18:27

      
        
        54
        +23:27 in.journal 9:10-9:11

      
        
        55
        +29:15 in.journal 25:11-25:19

      
        
        56
        +31:13 in.journal 18:4-18:15

      
M internal/lsp/testdata/rename-account.txtar
···
        4
        4
         account ex^penses

      
        5
        5
         account expenses2

      
        6
        6
         account expenses:food

      
        
        7
        +  alias foo^d

      
        
        8
        +alias fo^od = expenses:food

      
        
        9
        +alias food = expenses:fo^od

      
        7
        10
         

      
        8
        11
         2024-01-15 Grocery Store

      
        9
        12
             expenses:food  $50

      ···
        23
        26
         -- rename --

      
        24
        27
         costs

      
        25
        28
         food

      
        
        29
        +food

      
        
        30
        +food

      
        
        31
        +costs:food

      
        26
        32
         

      
        27
        33
         -- expect --

      
        28
        34
         2:10

      
        29
        35
           prep in.journal 2:8-2:16 "expenses"

      
        30
        36
           ren in.journal 2:8-2:16 "costs"

      
        31
        37
           ren in.journal 4:8-4:21 "costs:food"

      
        32
        
        -  ren in.journal 7:4-7:17 "costs:food"

      
        33
        
        -  ren in.journal 9:5-9:18 "costs:food"

      
        34
        
        -  ren in.journal 12:4-12:17 "costs:food"

      
        
        38
        +  ren in.journal 6:13-6:26 "costs:food"

      
        
        39
        +  ren in.journal 7:13-7:26 "costs:food"

      
        
        40
        +  ren in.journal 10:4-10:17 "costs:food"

      
        
        41
        +  ren in.journal 12:5-12:18 "costs:food"

      
        
        42
        +  ren in.journal 15:4-15:17 "costs:food"

      
        35
        43
           ren sub.journal 0:8-0:21 "costs:food"

      
        36
        44
           ren sub.journal 3:4-3:17 "costs:food"

      
        37
        
        -12:9

      
        38
        
        -  prep in.journal 12:4-12:17 "expenses:food"

      
        
        45
        +5:11

      
        
        46
        +  prep in.journal 5:8-5:12 "food"

      
        
        47
        +  ren in.journal 5:8-5:12 "food"

      
        
        48
        +  ren in.journal 6:6-6:10 "food"

      
        
        49
        +  ren in.journal 7:6-7:10 "food"

      
        
        50
        +6:8

      
        
        51
        +  prep in.journal 6:6-6:10 "food"

      
        
        52
        +  ren in.journal 5:8-5:12 "food"

      
        
        53
        +  ren in.journal 6:6-6:10 "food"

      
        
        54
        +  ren in.journal 7:6-7:10 "food"

      
        
        55
        +7:24

      
        
        56
        +  prep in.journal 7:13-7:26 "expenses:food"

      
        39
        57
           ren in.journal 4:8-4:21 "food"

      
        40
        
        -  ren in.journal 7:4-7:17 "food"

      
        41
        
        -  ren in.journal 9:5-9:18 "food"

      
        42
        
        -  ren in.journal 12:4-12:17 "food"

      
        
        58
        +  ren in.journal 6:13-6:26 "food"

      
        
        59
        +  ren in.journal 7:13-7:26 "food"

      
        
        60
        +  ren in.journal 10:4-10:17 "food"

      
        
        61
        +  ren in.journal 12:5-12:18 "food"

      
        
        62
        +  ren in.journal 15:4-15:17 "food"

      
        43
        63
           ren sub.journal 0:8-0:21 "food"

      
        44
        64
           ren sub.journal 3:4-3:17 "food"

      
        
        65
        +15:9

      
        
        66
        +  prep in.journal 15:4-15:17 "expenses:food"

      
        
        67
        +  ren in.journal 4:8-4:21 "costs:food"

      
        
        68
        +  ren in.journal 6:13-6:26 "costs:food"

      
        
        69
        +  ren in.journal 7:13-7:26 "costs:food"

      
        
        70
        +  ren in.journal 10:4-10:17 "costs:food"

      
        
        71
        +  ren in.journal 12:5-12:18 "costs:food"

      
        
        72
        +  ren in.journal 15:4-15:17 "costs:food"

      
        
        73
        +  ren sub.journal 0:8-0:21 "costs:food"

      
        
        74
        +  ren sub.journal 3:4-3:17 "costs:food"

      
M internal/lsp/testdata/semantic-directives.txtar
···
        1
        1
         -- in.journal --

      
        2
        2
         account expenses:food

      
        3
        3
         account expenses:food  ; note

      
        
        4
        +account expenses:food

      
        
        5
        +  alias food

      
        
        6
        +  alias groceries  ; alias note

      
        
        7
        +account expenses:food  ; account note

      
        
        8
        +  ; block comment line

      
        
        9
        +  alias food

      
        
        10
        +account expenses:food

      
        
        11
        +  type X

      
        
        12
        +  note grocery shopping  ; note comment

      
        
        13
        +  # hash comment line

      
        4
        14
         

      
        5
        15
         alias old = new

      
        6
        16
         alias old = new  ; alias note

      ···
        75
        85
         1:0+7 keyword

      
        76
        86
         1:8+13 namespace

      
        77
        87
         1:23+6 comment

      
        78
        
        -3:0+5 keyword

      
        79
        
        -3:6+3 namespace

      
        80
        
        -3:10+1 operator

      
        81
        
        -3:12+3 namespace

      
        82
        
        -4:0+5 keyword

      
        83
        
        -4:6+3 namespace

      
        84
        
        -4:10+1 operator

      
        85
        
        -4:12+3 namespace

      
        86
        
        -4:17+12 comment

      
        87
        
        -6:0+5 keyword

      
        88
        
        -6:6+10 property

      
        89
        
        -7:0+5 keyword

      
        90
        
        -7:6+10 property

      
        91
        
        -7:18+12 comment

      
        92
        
        -9:0+3 keyword

      
        93
        
        -9:4+3 property

      
        94
        
        -10:0+3 keyword

      
        95
        
        -10:4+3 property

      
        96
        
        -10:9+10 comment

      
        97
        
        -12:0+4 keyword

      
        98
        
        -12:5+4 property

      
        99
        
        -13:0+4 keyword

      
        100
        
        -13:5+4 property

      
        101
        
        -13:11+11 comment

      
        102
        
        -15:0+1 keyword

      
        103
        
        -15:2+4 property

      
        104
        
        -16:0+1 keyword

      
        105
        
        -16:2+4 property

      
        106
        
        -16:8+8 comment

      
        107
        
        -18:0+5 keyword

      
        108
        
        -18:6+16 property

      
        
        88
        +2:0+7 keyword

      
        
        89
        +2:8+13 namespace

      
        
        90
        +3:2+5 keyword

      
        
        91
        +3:8+4 namespace

      
        
        92
        +4:2+5 keyword

      
        
        93
        +4:8+9 namespace

      
        
        94
        +4:19+12 comment

      
        
        95
        +5:0+7 keyword

      
        
        96
        +5:8+13 namespace

      
        
        97
        +5:23+14 comment

      
        
        98
        +6:2+20 comment

      
        
        99
        +7:2+5 keyword

      
        
        100
        +7:8+4 namespace

      
        
        101
        +8:0+7 keyword

      
        
        102
        +8:8+13 namespace

      
        
        103
        +9:2+4 keyword

      
        
        104
        +9:7+1 property

      
        
        105
        +10:2+4 keyword

      
        
        106
        +10:7+16 property

      
        
        107
        +10:25+14 comment

      
        
        108
        +11:2+19 comment

      
        
        109
        +13:0+5 keyword

      
        
        110
        +13:6+3 namespace

      
        
        111
        +13:10+1 operator

      
        
        112
        +13:12+3 namespace

      
        
        113
        +14:0+5 keyword

      
        
        114
        +14:6+3 namespace

      
        
        115
        +14:10+1 operator

      
        
        116
        +14:12+3 namespace

      
        
        117
        +14:17+12 comment

      
        
        118
        +16:0+5 keyword

      
        
        119
        +16:6+10 property

      
        
        120
        +17:0+5 keyword

      
        
        121
        +17:6+10 property

      
        
        122
        +17:18+12 comment

      
        109
        123
         19:0+3 keyword

      
        110
        
        -19:4+5 property

      
        111
        
        -20:0+5 keyword

      
        112
        
        -20:6+16 property

      
        113
        
        -20:24+12 comment

      
        114
        
        -21:0+3 keyword

      
        115
        
        -21:4+5 property

      
        116
        
        -21:11+10 comment

      
        117
        
        -23:0+5 keyword

      
        118
        
        -23:6+11 property

      
        119
        
        -24:0+3 keyword

      
        120
        
        -24:4+5 property

      
        121
        
        -25:0+5 keyword

      
        122
        
        -25:6+11 property

      
        123
        
        -25:19+16 comment

      
        124
        
        -26:0+3 keyword

      
        125
        
        -26:4+5 property

      
        126
        
        -26:11+10 comment

      
        
        124
        +19:4+3 property

      
        
        125
        +20:0+3 keyword

      
        
        126
        +20:4+3 property

      
        
        127
        +20:9+10 comment

      
        
        128
        +22:0+4 keyword

      
        
        129
        +22:5+4 property

      
        
        130
        +23:0+4 keyword

      
        
        131
        +23:5+4 property

      
        
        132
        +23:11+11 comment

      
        
        133
        +25:0+1 keyword

      
        
        134
        +25:2+4 property

      
        
        135
        +26:0+1 keyword

      
        
        136
        +26:2+4 property

      
        
        137
        +26:8+8 comment

      
        127
        138
         28:0+5 keyword

      
        128
        
        -28:6+15 property

      
        
        139
        +28:6+16 property

      
        129
        140
         29:0+3 keyword

      
        130
        141
         29:4+5 property

      
        131
        142
         30:0+5 keyword

      
        132
        
        -30:6+15 property

      
        133
        
        -30:23+18 comment

      
        
        143
        +30:6+16 property

      
        
        144
        +30:24+12 comment

      
        134
        145
         31:0+3 keyword

      
        135
        146
         31:4+5 property

      
        136
        147
         31:11+10 comment

      
        137
        
        -33:0+1 keyword

      
        138
        
        -33:2+7 property

      
        139
        
        -34:0+1 keyword

      
        140
        
        -34:2+7 property

      
        141
        
        -34:11+8 comment

      
        142
        
        -35:0+1 keyword

      
        143
        
        -35:2+1 property

      
        144
        
        -36:0+1 keyword

      
        145
        
        -36:2+1 property

      
        146
        
        -36:5+15 comment

      
        147
        
        -38:0+1 keyword

      
        148
        
        -38:2+1 type

      
        149
        
        -38:3+8 number

      
        150
        
        -39:0+1 keyword

      
        151
        
        -39:2+1 type

      
        152
        
        -39:3+8 number

      
        153
        
        -39:13+8 comment

      
        154
        
        -41:0+1 keyword

      
        155
        
        -41:2+10 class

      
        156
        
        -41:13+3 type

      
        157
        
        -41:17+4 number

      
        158
        
        -42:0+1 keyword

      
        159
        
        -42:2+10 class

      
        160
        
        -42:13+3 type

      
        161
        
        -42:17+4 number

      
        162
        
        -42:23+8 comment

      
        
        148
        +33:0+5 keyword

      
        
        149
        +33:6+11 property

      
        
        150
        +34:0+3 keyword

      
        
        151
        +34:4+5 property

      
        
        152
        +35:0+5 keyword

      
        
        153
        +35:6+11 property

      
        
        154
        +35:19+16 comment

      
        
        155
        +36:0+3 keyword

      
        
        156
        +36:4+5 property

      
        
        157
        +36:11+10 comment

      
        
        158
        +38:0+5 keyword

      
        
        159
        +38:6+15 property

      
        
        160
        +39:0+3 keyword

      
        
        161
        +39:4+5 property

      
        
        162
        +40:0+5 keyword

      
        
        163
        +40:6+15 property

      
        
        164
        +40:23+18 comment

      
        
        165
        +41:0+3 keyword

      
        
        166
        +41:4+5 property

      
        
        167
        +41:11+10 comment

      
        163
        168
         43:0+1 keyword

      
        164
        
        -43:2+10 class

      
        165
        
        -43:13+8 class

      
        166
        
        -43:22+3 type

      
        167
        
        -43:26+5 number

      
        
        169
        +43:2+7 property

      
        168
        170
         44:0+1 keyword

      
        169
        
        -44:2+10 class

      
        170
        
        -44:13+8 class

      
        171
        
        -44:22+3 type

      
        172
        
        -44:26+5 number

      
        173
        
        -44:33+13 comment

      
        
        171
        +44:2+7 property

      
        
        172
        +44:11+8 comment

      
        
        173
        +45:0+1 keyword

      
        
        174
        +45:2+1 property

      
        174
        175
         46:0+1 keyword

      
        175
        
        -46:2+3 number

      
        176
        
        -46:6+3 type

      
        177
        
        -46:10+1 operator

      
        178
        
        -46:12+2 number

      
        179
        
        -46:15+3 type

      
        180
        
        -47:0+1 keyword

      
        181
        
        -47:2+3 number

      
        182
        
        -47:6+3 type

      
        183
        
        -47:10+1 operator

      
        184
        
        -47:12+2 number

      
        185
        
        -47:15+3 type

      
        186
        
        -47:20+8 comment

      
        187
        
        -49:0+12 keyword

      
        188
        
        -49:13+1 property

      
        189
        
        -50:0+12 keyword

      
        190
        
        -50:13+1 property

      
        191
        
        -50:16+14 comment

      
        192
        
        -52:0+7 keyword

      
        193
        
        -52:8+11 string

      
        194
        
        -53:0+7 keyword

      
        195
        
        -53:8+11 string

      
        196
        
        -53:21+14 comment

      
        197
        
        -55:0+9 keyword

      
        198
        
        -55:10+3 type

      
        199
        
        -56:0+9 keyword

      
        200
        
        -56:10+3 type

      
        201
        
        -56:15+16 comment

      
        202
        
        -58:0+9 keyword

      
        203
        
        -59:9+3 type

      
        204
        
        -59:13+14 number

      
        205
        
        -60:0+9 keyword

      
        206
        
        -60:15+20 comment

      
        207
        
        -61:9+3 type

      
        208
        
        -61:13+14 number

      
        209
        
        -63:0+9 keyword

      
        210
        
        -63:10+8 number

      
        211
        
        -63:19+3 type

      
        212
        
        -64:0+9 keyword

      
        213
        
        -64:10+8 number

      
        214
        
        -64:19+3 type

      
        215
        
        -64:24+23 comment

      
        
        176
        +46:2+1 property

      
        
        177
        +46:5+15 comment

      
        
        178
        +48:0+1 keyword

      
        
        179
        +48:2+1 type

      
        
        180
        +48:3+8 number

      
        
        181
        +49:0+1 keyword

      
        
        182
        +49:2+1 type

      
        
        183
        +49:3+8 number

      
        
        184
        +49:13+8 comment

      
        
        185
        +51:0+1 keyword

      
        
        186
        +51:2+10 class

      
        
        187
        +51:13+3 type

      
        
        188
        +51:17+4 number

      
        
        189
        +52:0+1 keyword

      
        
        190
        +52:2+10 class

      
        
        191
        +52:13+3 type

      
        
        192
        +52:17+4 number

      
        
        193
        +52:23+8 comment

      
        
        194
        +53:0+1 keyword

      
        
        195
        +53:2+10 class

      
        
        196
        +53:13+8 class

      
        
        197
        +53:22+3 type

      
        
        198
        +53:26+5 number

      
        
        199
        +54:0+1 keyword

      
        
        200
        +54:2+10 class

      
        
        201
        +54:13+8 class

      
        
        202
        +54:22+3 type

      
        
        203
        +54:26+5 number

      
        
        204
        +54:33+13 comment

      
        
        205
        +56:0+1 keyword

      
        
        206
        +56:2+3 number

      
        
        207
        +56:6+3 type

      
        
        208
        +56:10+1 operator

      
        
        209
        +56:12+2 number

      
        
        210
        +56:15+3 type

      
        
        211
        +57:0+1 keyword

      
        
        212
        +57:2+3 number

      
        
        213
        +57:6+3 type

      
        
        214
        +57:10+1 operator

      
        
        215
        +57:12+2 number

      
        
        216
        +57:15+3 type

      
        
        217
        +57:20+8 comment

      
        
        218
        +59:0+12 keyword

      
        
        219
        +59:13+1 property

      
        
        220
        +60:0+12 keyword

      
        
        221
        +60:13+1 property

      
        
        222
        +60:16+14 comment

      
        
        223
        +62:0+7 keyword

      
        
        224
        +62:8+11 string

      
        
        225
        +63:0+7 keyword

      
        
        226
        +63:8+11 string

      
        
        227
        +63:21+14 comment

      
        216
        228
         65:0+9 keyword

      
        217
        
        -66:9+8 number

      
        218
        
        -66:18+3 type

      
        219
        
        -67:0+9 keyword

      
        220
        
        -67:15+20 comment

      
        221
        
        -68:9+8 number

      
        222
        
        -68:18+3 type

      
        
        229
        +65:10+3 type

      
        
        230
        +66:0+9 keyword

      
        
        231
        +66:10+3 type

      
        
        232
        +66:15+16 comment

      
        
        233
        +68:0+9 keyword

      
        
        234
        +69:2+6 keyword

      
        
        235
        +69:9+3 type

      
        
        236
        +69:13+14 number

      
        
        237
        +70:0+9 keyword

      
        
        238
        +70:15+20 comment

      
        
        239
        +71:2+6 keyword

      
        
        240
        +71:9+3 type

      
        
        241
        +71:13+14 number

      
        
        242
        +73:0+9 keyword

      
        
        243
        +73:10+8 number

      
        
        244
        +73:19+3 type

      
        
        245
        +74:0+9 keyword

      
        
        246
        +74:10+8 number

      
        
        247
        +74:19+3 type

      
        
        248
        +74:24+23 comment

      
        
        249
        +75:0+9 keyword

      
        
        250
        +76:2+6 keyword

      
        
        251
        +76:9+8 number

      
        
        252
        +76:18+3 type

      
        
        253
        +77:0+9 keyword

      
        
        254
        +77:15+20 comment

      
        
        255
        +78:2+6 keyword

      
        
        256
        +78:9+8 number

      
        
        257
        +78:18+3 type

      
M internal/lsp/testdata/semantic-with-errors.txtar
···
        5
        5
         

      
        6
        6
         some broken input

      
        7
        7
         

      
        
        8
        +commodity $

      
        
        9
        +  format 1.000,00

      
        
        10
        +

      
        
        11
        +account assets:bank

      
        
        12
        +  payee ^store$

      
        
        13
        +

      
        8
        14
         -- expect --

      
        9
        15
         0:0+10 class

      
        10
        16
         0:11+6 property

      ···
        14
        20
         2:2+11 namespace

      
        15
        21
         2:17+1 type

      
        16
        22
         2:18+7 number negative

      
        
        23
        +6:0+9 keyword

      
        
        24
        +6:10+1 type

      
        
        25
        +7:2+6 keyword

      
        
        26
        +7:9+8 number

      
        
        27
        +9:0+7 keyword

      
        
        28
        +9:8+11 namespace

      
M internal/lsp/textdocument_completion.go
···
        430
        430
         

      
        431
        431
         func lexLine(content string, cursor int) []token.Token {

      
        432
        432
         	lineStart, lineEnd := lineBounds(content, cursor)

      
        433
        
        -	l := lexer.New("", content[lineStart:lineEnd])

      
        
        433
        +	l := lexer.New("", []byte(content[lineStart:lineEnd]))

      
        434
        434
         	var out []token.Token

      
        435
        435
         	for {

      
        436
        436
         		t := l.Next()

      
M internal/lsp/textdocument_definition.go
···
        49
        49
         }

      
        50
        50
         

      
        51
        51
         func findAccountDefinition(an *analyzer.Analysis, name string) *protocol.Location {

      
        
        52
        +	if canon, ok := an.AccountAliases[name]; ok {

      
        
        53
        +		name = canon

      
        
        54
        +	}

      
        52
        55
         	info := an.Accounts[name]

      
        53
        56
         	if info == nil {

      
        54
        57
         		return nil

      
M internal/lsp/textdocument_format.go
···
        18
        18
         	}

      
        19
        19
         

      
        20
        20
         	text := state.text

      
        21
        
        -	lex := lexer.New(params.TextDocument.URI.Path(), text)

      
        
        21
        +	lex := lexer.New(params.TextDocument.URI.Path(), []byte(text))

      
        22
        22
         	jnrl := parser.New(lex).ParseJournal()

      
        23
        23
         	if len(jnrl.Errors) > 0 {

      
        24
        24
         		return nil, fmt.Errorf("can't format file with errors: %v", jnrl.Errors[0].Message) // TODO: report all errors

      
M internal/lsp/textdocument_rename.go
···
        131
        131
         		if spanContains(content, e.Account.Span, cursor) {

      
        132
        132
         			return &symbolRef{symbolAccount, e.Account.String(), e.Account.Span}

      
        133
        133
         		}

      
        
        134
        +		for _, sd := range e.Subdirectives {

      
        
        135
        +			if sd.Name == "alias" && spanContains(content, sd.ValueSpan, cursor) {

      
        
        136
        +				return &symbolRef{symbolAccount, sd.Value, sd.ValueSpan}

      
        
        137
        +			}

      
        
        138
        +		}

      
        
        139
        +	case *ast.AliasDirective:

      
        
        140
        +		if spanContains(content, e.From.Span, cursor) {

      
        
        141
        +			return &symbolRef{symbolAccount, e.From.String(), e.From.Span}

      
        
        142
        +		}

      
        
        143
        +		if spanContains(content, e.To.Span, cursor) {

      
        
        144
        +			return &symbolRef{symbolAccount, e.To.String(), e.To.Span}

      
        
        145
        +		}

      
        134
        146
         	case *ast.CommodityDirective:

      
        135
        147
         		if spanContains(content, e.CommoditySpan, cursor) {

      
        136
        148
         			return &symbolRef{symbolCommodity, e.Commodity, e.CommoditySpan}

      ···
        280
        292
         	case *ast.AccountDirective:

      
        281
        293
         		if text, ok := ref.renameTo(symbolAccount, e.Account.String(), newName); ok {

      
        282
        294
         			add(e.Account.Span, text)

      
        
        295
        +		}

      
        
        296
        +		for _, sd := range e.Subdirectives {

      
        
        297
        +			if sd.Name == "alias" {

      
        
        298
        +				if text, ok := ref.renameTo(symbolAccount, sd.Value, newName); ok {

      
        
        299
        +					add(sd.ValueSpan, text)

      
        
        300
        +				}

      
        
        301
        +			}

      
        
        302
        +		}

      
        
        303
        +	case *ast.AliasDirective:

      
        
        304
        +		if text, ok := ref.renameTo(symbolAccount, e.From.String(), newName); ok {

      
        
        305
        +			add(e.From.Span, text)

      
        
        306
        +		}

      
        
        307
        +		if text, ok := ref.renameTo(symbolAccount, e.To.String(), newName); ok {

      
        
        308
        +			add(e.To.Span, text)

      
        283
        309
         		}

      
        284
        310
         	case *ast.CommodityDirective:

      
        285
        311
         		if text, ok := ref.renameTo(symbolCommodity, e.Commodity, newName); ok {

      
M internal/lsp/textdocument_semantic.go
···
        203
        203
         	case *ast.AccountDirective:

      
        204
        204
         		emit(directiveKeyword(e.Span, "account"), semDirective, 0)

      
        205
        205
         		emit(e.Account.Span, semAccount, 0)

      
        
        206
        +		for _, sd := range e.Subdirectives {

      
        
        207
        +			if sd.Name == "" {

      
        
        208
        +				emitComment(sd.Comment, emit)

      
        
        209
        +				continue

      
        
        210
        +			}

      
        
        211
        +			emit(sd.NameSpan, semDirective, 0)

      
        
        212
        +			switch sd.Name {

      
        
        213
        +			case "alias":

      
        
        214
        +				emit(sd.ValueSpan, semAccount, 0)

      
        
        215
        +			case "type", "note":

      
        
        216
        +				emit(sd.ValueSpan, semProperty, 0)

      
        
        217
        +			}

      
        
        218
        +			emitComment(sd.Comment, emit)

      
        
        219
        +		}

      
        206
        220
         		emitComment(e.Comment, emit)

      
        207
        221
         	case *ast.CommodityDirective:

      
        208
        222
         		emit(directiveKeyword(e.Span, "commodity"), semDirective, 0)

      
        209
        
        -		if e.Format.Span.End.Offset > 0 {

      
        210
        
        -			semEmitAmount(content, &e.Format, emit)

      
        
        223
        +		if e.FormatSub != nil {

      
        
        224
        +			if e.FormatSub.KeywordSpan.End.Offset > 0 {

      
        
        225
        +				emit(e.FormatSub.KeywordSpan, semDirective, 0)

      
        
        226
        +			}

      
        
        227
        +			semEmitAmount(content, &e.FormatSub.Amount, emit)

      
        
        228
        +			emitComment(e.FormatSub.Comment, emit)

      
        211
        229
         		} else if e.CommoditySpan.Start.Offset > 0 && e.CommoditySpan.End.Offset > 0 {

      
        212
        230
         			emit(e.CommoditySpan, semCommodity, 0)

      
        213
        231
         		}

      
        
        232
        +		emitBlockComments(e.BlockComments, emit)

      
        214
        233
         		emitComment(e.Comment, emit)

      
        215
        234
         	case *ast.IncludeDirective:

      
        216
        235
         		emitDirective(content, e.Span, len("include"), semString, e.Comment, emit)

      ···
        464
        483
         

      
        465
        484
         type semEmitFunc func(tok token.Span, tokKind, modifier uint32)

      
        466
        485
         

      
        
        486
        +func emitBlockComments(cs []*ast.Comment, emit semEmitFunc) {

      
        
        487
        +	for _, c := range cs {

      
        
        488
        +		emitComment(c, emit)

      
        
        489
        +	}

      
        
        490
        +}

      
        
        491
        +

      
        467
        492
         func emitComment(c *ast.Comment, emit semEmitFunc) {

      
        468
        493
         	if c == nil {

      
        469
        494
         		return

      ···
        522
        547
         }

      
        523
        548
         

      
        524
        549
         func semLexerFallback(content string, base []rawSpan, emit semEmitFunc) {

      
        525
        
        -	l := lexer.New("", content)

      
        
        550
        +	l := lexer.New("", []byte(content))

      
        526
        551
         

      
        527
        552
         	var commentStart, commentEnd int // 0 = not inside a comment line

      
        528
        553
         	lineStart := true                // the next significant token starts a line

      ···
        615
        640
         	return false

      
        616
        641
         }

      
        617
        642
         

      
        
        643
        +// encodeSemTokens encodes tokens into LSP delta form. Input must be sorted by

      
        
        644
        +// line and column; [rawToSemanticTokens] produces such order.

      
        618
        645
         func encodeSemTokens(tokens []semanticToken) []uint32 {

      
        619
        646
         	if len(tokens) == 0 {

      
        620
        647
         		return nil

      
        621
        648
         	}

      
        622
        
        -	slices.SortFunc(tokens, func(a, b semanticToken) int {

      
        623
        
        -		if a.line != b.line {

      
        624
        
        -			return int(a.line) - int(b.line)

      
        625
        
        -		}

      
        626
        
        -		return int(a.col) - int(b.col)

      
        627
        
        -	})

      
        628
        649
         	data := make([]uint32, 0, len(tokens)*5)

      
        629
        650
         	var prevLine, prevCol uint32

      
        630
        651
         	for _, t := range tokens {

      
M internal/lsp/textdocument_semantic_test.go
···
        34
        34
         			0, 11, 5, semString, 0,

      
        35
        35
         			1, 4, 10, semAccount, 0,

      
        36
        36
         		}},

      
        37
        
        -		// the wire format requires non-negative deltas; unsorted input must

      
        38
        
        -		// be sorted first (commodity at col 36 comes after amount at col 32)

      
        39
        
        -		"unsorted input": {[]semanticToken{

      
        
        37
        +		// input must be sorted by line and column (rawToSemanticTokens output);

      
        
        38
        +		// deltas would underflow otherwise

      
        
        39
        +		"sorted": {[]semanticToken{

      
        40
        40
         			{line: 0, col: 0, length: 10, tokenType: semDate},

      
        41
        
        -			{line: 0, col: 36, length: 3, tokenType: semCommodity},

      
        42
        41
         			{line: 0, col: 32, length: 2, tokenType: semAmount},

      
        
        42
        +			{line: 0, col: 36, length: 3, tokenType: semCommodity},

      
        43
        43
         			{line: 1, col: 4, length: 6, tokenType: semAccount},

      
        44
        44
         		}, []uint32{

      
        45
        45
         			0, 0, 10, semDate, 0,

      
M internal/lsp/textdocument_sync.go
···
        86
        86
         }

      
        87
        87
         

      
        88
        88
         func parseJournalStr(content string) *ast.Journal {

      
        89
        
        -	l := lexer.New("", content)

      
        
        89
        +	l := lexer.New("", []byte(content))

      
        90
        90
         	p := parser.New(l)

      
        91
        91
         	return p.ParseJournal()

      
        92
        92
         }

      
M journal/ast/directives.go
···
        3
        3
         import "olexsmir.xyz/clerk/journal/token"

      
        4
        4
         

      
        5
        5
         type AccountDirective struct {

      
        6
        
        -	Account Account

      
        7
        
        -	Comment *Comment

      
        8
        
        -	Span    token.Span

      
        
        6
        +	Account       Account

      
        
        7
        +	Subdirectives []AccountSubdirective // indented block lines, in order

      
        
        8
        +	Comment       *Comment

      
        
        9
        +	Span          token.Span

      
        9
        10
         }

      
        10
        11
         

      
        11
        12
         func (AccountDirective) entryNode() {}

      
        
        13
        +

      
        
        14
        +type AccountSubdirective struct {

      
        
        15
        +	Name      string     // "alias" / "type" / "note" / "" for comment lines

      
        
        16
        +	NameSpan  token.Span // keyword span; for comment lines, the marker span

      
        
        17
        +	Value     string     // text after the keyword; "" for comment lines

      
        
        18
        +	ValueSpan token.Span // value span; zero for comment lines and empty values

      
        
        19
        +	Comment   *Comment   // inline comment after the value; for comment lines, the line itself

      
        
        20
        +}

      
        12
        21
         

      
        13
        22
         type CommodityDirective struct {

      
        14
        23
         	Commodity     string

      
        15
        
        -	CommoditySpan token.Span // span of the commodity token

      
        16
        
        -	Format        Amount     // optional format hint: "1,000.00 UAH"

      
        17
        
        -	Comment       *Comment   // optional inline comment

      
        
        24
        +	CommoditySpan token.Span          // span of the commodity token

      
        
        25
        +	FormatSub     *FormatSubDirective // display format; nil when not given

      
        
        26
        +	BlockComments []*Comment          // indented comment-only lines inside the block

      
        
        27
        +	Comment       *Comment            // optional inline comment

      
        18
        28
         	Span          token.Span

      
        19
        29
         }

      
        20
        30
         

      
        21
        31
         func (CommodityDirective) entryNode() {}

      
        
        32
        +

      
        
        33
        +// FormatSubDirective is a commodity display format, either inline on the

      
        
        34
        +// directive line ("commodity $1,000.00") or as a "format" subdirective.

      
        
        35
        +type FormatSubDirective struct {

      
        
        36
        +	Amount      Amount

      
        
        37
        +	KeywordSpan token.Span // span of the "format" keyword; zero when inline

      
        
        38
        +	Comment     *Comment   // inline comment; subdirective form only

      
        
        39
        +}

      
        22
        40
         

      
        23
        41
         type PayeeDirective struct {

      
        24
        42
         	Name    *Payee

      
M journal/ast/dump.go
···
        272
        272
         	}

      
        273
        273
         	indent(b, depth+1)

      
        274
        274
         	fmt.Fprintf(b, "Precision: %d\n", a.QuantityFmt.Precision)

      
        275
        
        -	indent(b, depth+1)

      
        276
        
        -	fmt.Fprintf(b, "Decimal: %q\n", string(a.QuantityFmt.Decimal))

      
        
        275
        +	if a.QuantityFmt.Decimal != 0 {

      
        
        276
        +		indent(b, depth+1)

      
        
        277
        +		fmt.Fprintf(b, "Decimal: %q\n", string(a.QuantityFmt.Decimal))

      
        
        278
        +	}

      
        277
        279
         	if a.QuantityFmt.Thousands != 0 {

      
        278
        280
         		indent(b, depth+1)

      
        279
        281
         		fmt.Fprintf(b, "Thousands: %q\n", string(a.QuantityFmt.Thousands))

      ···
        318
        320
         	indent(b, depth)

      
        319
        321
         	fmt.Fprintf(b, "AccountDirective %s\n", a.Span)

      
        320
        322
         	dumpAccount(b, a.Account, depth+1)

      
        
        323
        +	for _, sd := range a.Subdirectives {

      
        
        324
        +		if sd.Name == "" {

      
        
        325
        +			dumpComment(b, sd.Comment, depth+1)

      
        
        326
        +			continue

      
        
        327
        +		}

      
        
        328
        +		indent(b, depth+1)

      
        
        329
        +		fmt.Fprintf(b, "Subdirective\n")

      
        
        330
        +		indent(b, depth+2)

      
        
        331
        +		fmt.Fprintf(b, "Name: %q %s\n", sd.Name, sd.NameSpan)

      
        
        332
        +		indent(b, depth+2)

      
        
        333
        +		fmt.Fprintf(b, "Value: %q %s\n", sd.Value, sd.ValueSpan)

      
        
        334
        +		dumpOptComment(b, sd.Comment, depth+2)

      
        
        335
        +	}

      
        321
        336
         	dumpOptComment(b, a.Comment, depth+1)

      
        322
        337
         }

      
        323
        338
         

      ···
        326
        341
         	fmt.Fprintf(b, "CommodityDirective %s\n", c.Span)

      
        327
        342
         	indent(b, depth+1)

      
        328
        343
         	fmt.Fprintf(b, "Commodity: %q %s\n", c.Commodity, c.CommoditySpan)

      
        329
        
        -	if c.Format.QuantityFmt.Decimal != 0 {

      
        330
        
        -		dumpAmount(b, &c.Format, depth+1)

      
        
        344
        +	if c.FormatSub != nil {

      
        
        345
        +		indent(b, depth+1)

      
        
        346
        +		if c.FormatSub.KeywordSpan.End.Offset > 0 {

      
        
        347
        +			fmt.Fprintf(b, "Format %s\n", c.FormatSub.KeywordSpan)

      
        
        348
        +		} else {

      
        
        349
        +			fmt.Fprintf(b, "Format inline\n")

      
        
        350
        +		}

      
        
        351
        +		dumpAmount(b, &c.FormatSub.Amount, depth+2)

      
        
        352
        +		dumpOptComment(b, c.FormatSub.Comment, depth+2)

      
        
        353
        +	}

      
        
        354
        +	for _, cm := range c.BlockComments {

      
        
        355
        +		dumpComment(b, cm, depth+1)

      
        331
        356
         	}

      
        332
        357
         	dumpOptComment(b, c.Comment, depth+1)

      
        333
        358
         }

      
M journal/ast/entries.go
···
        132
        132
         )

      
        133
        133
         

      
        134
        134
         type QuantityFormat struct {

      
        135
        
        -	Decimal   byte // '.' or ','

      
        
        135
        +	Decimal   byte // '.' or ','; 0 when the literal has no explicit decimal mark

      
        136
        136
         	Thousands byte // ',' '.' ' ' or 0

      
        137
        137
         	Precision int

      
        138
        138
         }

      
M journal/journal_test.go
···
        61
        61
         	"unicode-cyrillic.journal":                  {desc: "synthetic: unicode Cyrillic in descriptions and account names"},

      
        62
        62
         	"unicode-mixed-languages.journal":           {desc: "synthetic: mixed latin/cyrillic/cjk in descriptions and account names"},

      
        63
        63
         	"virtual-posting.dat":                       {desc: "ledger: virtual/balanced postings with [brackets]"},

      
        
        64
        +	"subdirectives.journal":                     {desc: "synthetic: account alias and commodity format subdirectives with comments"},

      
        64
        65
         }

      
        65
        66
         

      
        66
        67
         func TestParserOnRealJournals(t *testing.T) {

      
M journal/lexer/benchmark_test.go
···
        25
        25
         		b.ResetTimer()

      
        26
        26
         		b.ReportAllocs()

      
        27
        27
         		for b.Loop() {

      
        28
        
        -			l := New("bench", string(src))

      
        
        28
        +			l := New("bench", src)

      
        29
        29
         			for l.Next().Type != token.EOF {

      
        30
        30
         			}

      
        31
        31
         		}

      
M journal/lexer/lexer.go
···
        41
        41
         

      
        42
        42
         type Lexer struct {

      
        43
        43
         	file  string

      
        44
        
        -	input string

      
        
        44
        +	input []byte

      
        45
        45
         	mode  mode

      
        46
        46
         

      
        47
        47
         	ch     rune // current rune (0 = EOF/sentinel)

      ···
        57
        57
         

      
        58
        58
         	includePath bool // true after [token.INCLUDE] is omitted

      
        59
        59
         

      
        60
        
        -	// subdirective is set when the current line is an account/commodity

      
        61
        
        -	// directive; the next indented line then lexes as directive content

      
        
        60
        +	// subdirective is set while inside an account/commodity directive block;

      
        
        61
        +	// every indented line then lexes as directive content until a

      
        
        62
        +	// non-indented line start clears it in lexDefault

      
        62
        63
         	subdirective bool

      
        63
        64
         }

      
        64
        65
         

      
        65
        
        -func New(file string, input string) *Lexer {

      
        
        66
        +func New(file string, input []byte) *Lexer {

      
        66
        67
         	l := &Lexer{

      
        67
        68
         		file:  file,

      
        68
        69
         		input: input,

      ···
        115
        116
         	case l.ch == ' ' || l.ch == '\t':

      
        116
        117
         		tok := l.lexIndent()

      
        117
        118
         		if l.subdirective {

      
        
        119
        +			// keep directive mode for every line of a directive block; the

      
        
        120
        +			// flag is cleared at the next non-indented line start in lexDefault

      
        118
        121
         			l.mode = modeDirective

      
        119
        
        -			l.subdirective = false

      
        120
        122
         		} else {

      
        121
        123
         			l.mode = modePosting

      
        122
        124
         		}

      ···
        690
        692
         		l.ch = 0

      
        691
        693
         		l.chSize = 0

      
        692
        694
         	} else {

      
        693
        
        -		r, size := utf8.DecodeRuneInString(l.input[l.rpos:])

      
        
        695
        +		r, size := utf8.DecodeRune(l.input[l.rpos:])

      
        694
        696
         		l.ch = r

      
        695
        697
         		l.chSize = size

      
        696
        698
         	}

      ···
        705
        707
         }

      
        706
        708
         

      
        707
        709
         func (l *Lexer) peek() rune {

      
        708
        
        -	r, _ := utf8.DecodeRuneInString(l.input[l.rpos:])

      
        
        710
        +	r, _ := utf8.DecodeRune(l.input[l.rpos:])

      
        709
        711
         	return r

      
        710
        712
         }

      
        711
        713
         

      
M journal/lexer/lexer_test.go
···
        35
        35
         	for _, tt := range tests {

      
        36
        36
         		t.Run(tt, func(t *testing.T) {

      
        37
        37
         			a := golden.Read(t, tt)

      
        38
        
        -			golden.Assert(t, a, New("j", string(a.Get("input"))).dump())

      
        
        38
        +			golden.Assert(t, a, New("j", a.Get("input")).dump())

      
        39
        39
         		})

      
        40
        40
         	}

      
        41
        41
         }

      ···
        75
        75
         

      
        76
        76
         	f.Fuzz(func(t *testing.T, data []byte) {

      
        77
        77
         		// Pass 1: lex and validate token stream

      
        78
        
        -		l := New("j", string(data))

      
        
        78
        +		l := New("j", data)

      
        79
        79
         		var tokens []token.Token

      
        80
        80
         		maxTokens := max(len(data)*2, 16)

      
        81
        81
         		prevEnd := -1

      ···
        125
        125
         		}

      
        126
        126
         

      
        127
        127
         		// Pass 2: re-lex the same input — token stream must be identical

      
        128
        
        -		l2 := New("j", string(data))

      
        
        128
        +		l2 := New("j", data)

      
        129
        129
         		for _, expected := range tokens {

      
        130
        130
         			tok := l2.Next()

      
        131
        131
         			if tok.Type != expected.Type || tok.Literal != expected.Literal {

      
M journal/loader.go
···
        180
        180
         		return

      
        181
        181
         	}

      
        182
        182
         

      
        183
        
        -	lex := lexer.New(fpath, string(src))

      
        
        183
        +	lex := lexer.New(fpath, src)

      
        184
        184
         	par := parser.NewWithYear(lex, defaultYear)

      
        185
        185
         	j := par.ParseJournal()

      
        186
        186
         

      
M journal/parser/benchmark_test.go
···
        25
        25
         		b.ResetTimer()

      
        26
        26
         		b.ReportAllocs()

      
        27
        27
         		for b.Loop() {

      
        28
        
        -			l := lexer.New("bench", string(src))

      
        
        28
        +			l := lexer.New("bench", src)

      
        29
        29
         			New(l).ParseJournal()

      
        30
        30
         		}

      
        31
        31
         	}

      
M journal/parser/parser.go
···
        342
        342
         	comment := p.parseOptInlineComment()

      
        343
        343
         	p.expectNewline()

      
        344
        344
         

      
        
        345
        +	var subs []ast.AccountSubdirective

      
        345
        346
         	for p.got(token.INDENT) {

      
        346
        347
         		p.advance()

      
        347
        
        -		for !p.got(token.NEWLINE) && !p.got(token.EOF) {

      
        
        348
        +		p.skipWhitespace()

      
        
        349
        +		if p.got(token.NEWLINE) || p.got(token.EOF) {

      
        
        350
        +			// whitespace-only line: block continues

      
        
        351
        +			p.expectNewline()

      
        
        352
        +			continue

      
        
        353
        +		}

      
        
        354
        +		switch {

      
        
        355
        +		case p.got(token.SEMICOLON):

      
        
        356
        +			// comment line: directive mode lexes only ';' as a comment marker

      
        
        357
        +			s := p.cur.Span

      
        
        358
        +			c := p.parseCommentRest(s)

      
        
        359
        +			p.expectNewline()

      
        
        360
        +			subs = append(subs, ast.AccountSubdirective{NameSpan: s, Comment: c})

      
        
        361
        +		case p.got(token.TEXT) && isCommentMarker(p.cur.Literal):

      
        
        362
        +			// '#', '%' and '*' lex as TEXT in directive mode; treat them as comment lines

      
        
        363
        +			c := p.parseTextComment()

      
        
        364
        +			subs = append(subs, ast.AccountSubdirective{NameSpan: c.Span, Comment: c})

      
        
        365
        +		case p.got(token.TEXT):

      
        
        366
        +			name := p.cur.Literal

      
        
        367
        +			if !isAccountSubdirective(name) {

      
        
        368
        +				p.errorf("unknown subdirective %q", name)

      
        
        369
        +				p.skipToNewline()

      
        
        370
        +				continue

      
        
        371
        +			}

      
        
        372
        +			kw := p.cur.Span

      
        348
        373
         			p.advance()

      
        
        374
        +			value, vspan := p.parseSubdirectiveValue()

      
        
        375
        +			if value == "" {

      
        
        376
        +				p.errorf("expected value for subdirective %q", name)

      
        
        377
        +			}

      
        
        378
        +			c := p.parseOptInlineComment()

      
        
        379
        +			p.expectNewline()

      
        
        380
        +			subs = append(subs, ast.AccountSubdirective{

      
        
        381
        +				Name:      name,

      
        
        382
        +				NameSpan:  kw,

      
        
        383
        +				Value:     value,

      
        
        384
        +				ValueSpan: vspan,

      
        
        385
        +				Comment:   c,

      
        
        386
        +			})

      
        
        387
        +		default:

      
        
        388
        +			p.errorf("expected subdirective name, got %s", p.cur.Type)

      
        
        389
        +			p.skipToNewline()

      
        349
        390
         		}

      
        350
        
        -		p.expectNewline()

      
        351
        391
         	}

      
        352
        392
         

      
        353
        393
         	return &ast.AccountDirective{

      
        354
        
        -		Account: account,

      
        355
        
        -		Comment: comment,

      
        356
        
        -		Span:    p.span(s),

      
        
        394
        +		Account:       account,

      
        
        395
        +		Subdirectives: subs,

      
        
        396
        +		Comment:       comment,

      
        
        397
        +		Span:          p.span(s),

      
        357
        398
         	}

      
        358
        399
         }

      
        359
        400
         

      ···
        364
        405
         

      
        365
        406
         	var commodity string

      
        366
        407
         	var commoditySpan token.Span

      
        367
        
        -	var format *ast.Amount

      
        
        408
        +	var format *ast.FormatSubDirective

      
        368
        409
         

      
        369
        410
         	switch p.cur.Type {

      
        370
        411
         	case token.COMMODITYMARK, token.TEXT, token.STRING:

      ···
        375
        416
         		hadSpace := p.got(token.WHITESPACE)

      
        376
        417
         		p.skipWhitespace()

      
        377
        418
         		if p.got(token.INT) || p.got(token.DECIMAL) || p.got(token.TEXT) {

      
        378
        
        -			format = p.parseAmount()

      
        379
        
        -			format.Commodity = commodity

      
        380
        
        -			format.CommoditySpan = commoditySpan

      
        381
        
        -			format.CommodityPos = ast.CommodityBefore

      
        382
        
        -			format.HasSpace = hadSpace

      
        
        419
        +			amt := p.parseAmount()

      
        
        420
        +			amt.Commodity = commodity

      
        
        421
        +			amt.CommoditySpan = commoditySpan

      
        
        422
        +			amt.CommodityPos = ast.CommodityBefore

      
        
        423
        +			amt.HasSpace = hadSpace

      
        
        424
        +			format = &ast.FormatSubDirective{Amount: *amt}

      
        383
        425
         		}

      
        384
        426
         	case token.INT, token.DECIMAL:

      
        385
        
        -		format = p.parseAmount()

      
        386
        
        -		commodity = format.Commodity

      
        387
        
        -		commoditySpan = format.CommoditySpan

      
        
        427
        +		amt := p.parseAmount()

      
        
        428
        +		commodity = amt.Commodity

      
        
        429
        +		commoditySpan = amt.CommoditySpan

      
        
        430
        +		format = &ast.FormatSubDirective{Amount: *amt}

      
        388
        431
         	default:

      
        389
        432
         		p.errorf("expected commodity name or amount, got %s", p.cur.Type)

      
        390
        433
         	}

      ···
        393
        436
         		p.errorf("expected commodity name, got %s", p.cur.Type)

      
        394
        437
         	}

      
        395
        438
         

      
        
        439
        +	// hledger parity: an inline format amount must include a decimal mark

      
        
        440
        +	if format != nil && format.Amount.QuantityFmt.Decimal == 0 {

      
        
        441
        +		p.errorfAt(format.Amount.Span.Start, "Please include a decimal point or decimal comma in commodity directives, to help us parse correctly. It may be followed by zero or more decimal digits.")

      
        
        442
        +	}

      
        
        443
        +

      
        396
        444
         	comment := p.parseOptInlineComment()

      
        397
        445
         	p.expectNewline()

      
        398
        446
         

      
        
        447
        +	var blockComments []*ast.Comment

      
        399
        448
         	for p.got(token.INDENT) {

      
        400
        449
         		p.advance()

      
        401
        450
         		p.skipWhitespace()

      
        402
        
        -		if p.got(token.TEXT) && p.cur.Literal == "format" {

      
        403
        
        -			p.advance()

      
        404
        
        -			p.skipWhitespace()

      
        405
        
        -			format = p.parseAmount()

      
        
        451
        +		if p.got(token.NEWLINE) || p.got(token.EOF) {

      
        
        452
        +			// whitespace-only line: block continues

      
        406
        453
         			p.expectNewline()

      
        407
        454
         			continue

      
        408
        455
         		}

      
        409
        
        -		for !p.got(token.NEWLINE) && !p.got(token.EOF) {

      
        
        456
        +		switch {

      
        
        457
        +		case p.got(token.TEXT) && p.cur.Literal == "format":

      
        
        458
        +			kw := p.cur.Span

      
        410
        459
         			p.advance()

      
        
        460
        +			p.skipWhitespace()

      
        
        461
        +			amt := p.parseAmount()

      
        
        462
        +			// hledger parity: the format symbol must match the declared commodity,

      
        
        463
        +			// and the amount must include a decimal mark; the node is kept either

      
        
        464
        +			// way so the printer can round-trip the input.

      
        
        465
        +			if amt.Commodity != commodity {

      
        
        466
        +				p.errorfAt(amt.Span.Start, "commodity directive symbol %q and format directive symbol %q should be the same", commodity, amt.Commodity)

      
        
        467
        +			} else if amt.QuantityFmt.Decimal == 0 {

      
        
        468
        +				p.errorfAt(amt.Span.Start, "Please include a decimal point or decimal comma in commodity directives, to help us parse correctly. It may be followed by zero or more decimal digits.")

      
        
        469
        +			}

      
        
        470
        +			c := p.parseOptInlineComment()

      
        
        471
        +			p.expectNewline()

      
        
        472
        +			format = &ast.FormatSubDirective{KeywordSpan: kw, Amount: *amt, Comment: c}

      
        
        473
        +		case p.got(token.SEMICOLON): // comment line

      
        
        474
        +			c := p.parseCommentRest(p.cur.Span)

      
        
        475
        +			p.expectNewline()

      
        
        476
        +			blockComments = append(blockComments, c)

      
        
        477
        +		case p.got(token.TEXT) && isCommentMarker(p.cur.Literal):

      
        
        478
        +			// '#', '%' and '*' lex as TEXT in directive mode; treat them as comment lines

      
        
        479
        +			blockComments = append(blockComments, p.parseTextComment())

      
        
        480
        +		case p.got(token.TEXT):

      
        
        481
        +			p.errorf("unknown subdirective %q", p.cur.Literal)

      
        
        482
        +			p.skipToNewline()

      
        
        483
        +		default:

      
        
        484
        +			p.errorf("expected subdirective name, got %s", p.cur.Type)

      
        
        485
        +			p.skipToNewline()

      
        411
        486
         		}

      
        412
        
        -		p.expectNewline()

      
        413
        487
         	}

      
        414
        488
         

      
        415
        489
         	cd := &ast.CommodityDirective{

      
        416
        490
         		Commodity:     commodity,

      
        417
        491
         		CommoditySpan: commoditySpan,

      
        
        492
        +		FormatSub:     format,

      
        
        493
        +		BlockComments: blockComments,

      
        418
        494
         		Comment:       comment,

      
        419
        495
         		Span:          p.span(s),

      
        420
        
        -	}

      
        421
        
        -	if format != nil {

      
        422
        
        -		cd.Format = *format

      
        423
        496
         	}

      
        424
        497
         	return cd

      
        425
        498
         }

      ···
        770
        843
         func (p *Parser) parseAmount() *ast.Amount {

      
        771
        844
         	s := p.cur.Span

      
        772
        845
         	amt := &ast.Amount{

      
        773
        
        -		QuantityFmt: ast.QuantityFormat{Decimal: '.'},

      
        
        846
        +		QuantityFmt: ast.QuantityFormat{},

      
        774
        847
         	}

      
        775
        848
         	defer func() {

      
        776
        849
         		// The span covers from the first token to the start of the next unconsumed token.

      ···
        851
        924
         		lit := p.cur.Literal

      
        852
        925
         		amt := &ast.Amount{

      
        853
        926
         			IsExpr:      true,

      
        854
        
        -			QuantityFmt: ast.QuantityFormat{Decimal: '.'},

      
        
        927
        +			QuantityFmt: ast.QuantityFormat{},

      
        855
        928
         		}

      
        856
        929
         		if len(lit) >= 2 && lit[0] == '(' && lit[len(lit)-1] == ')' {

      
        857
        930
         			amt.Expr = strings.Trim(lit[1:len(lit)-1], " \t")

      ···
        1193
        1266
         	})

      
        1194
        1267
         }

      
        1195
        1268
         

      
        
        1269
        +// errorfAt records a parse error pointing at a single position.

      
        
        1270
        +func (p *Parser) errorfAt(pos token.Pos, format string, args ...any) {

      
        
        1271
        +	p.errors = append(p.errors, &ast.ParseError{

      
        
        1272
        +		Span:    token.Span{Start: pos, End: pos},

      
        
        1273
        +		Message: fmt.Sprintf(format, args...),

      
        
        1274
        +	})

      
        
        1275
        +}

      
        
        1276
        +

      
        
        1277
        +func isAccountSubdirective(name string) bool {

      
        
        1278
        +	switch name {

      
        
        1279
        +	case "alias", "type", "note":

      
        
        1280
        +		return true

      
        
        1281
        +	}

      
        
        1282
        +	return false

      
        
        1283
        +}

      
        
        1284
        +

      
        
        1285
        +func isCommentMarker(s string) bool {

      
        
        1286
        +	return len(s) > 0 && (s[0] == '#' || s[0] == '%' || s[0] == '*')

      
        
        1287
        +}

      
        
        1288
        +

      
        
        1289
        +// skipToNewline consumes the rest of the current line.

      
        
        1290
        +func (p *Parser) skipToNewline() {

      
        
        1291
        +	for !p.got(token.NEWLINE) && !p.got(token.EOF) {

      
        
        1292
        +		p.advance()

      
        
        1293
        +	}

      
        
        1294
        +	p.expectNewline()

      
        
        1295
        +}

      
        
        1296
        +

      
        
        1297
        +// parseSubdirectiveValue consumes the tokens after a subdirective keyword up

      
        
        1298
        +// to an inline comment or the end of the line, returning the raw text and its

      
        
        1299
        +// span (trimmed of surrounding whitespace). Single-token values return the

      
        
        1300
        +// lexer's literal, sharing the source backing without a copy.

      
        
        1301
        +func (p *Parser) parseSubdirectiveValue() (string, token.Span) {

      
        
        1302
        +	var b strings.Builder

      
        
        1303
        +	var first, last token.Span

      
        
        1304
        +	var single, pendingWS string

      
        
        1305
        +	for !p.got(token.SEMICOLON) && !p.got(token.NEWLINE) && !p.got(token.EOF) {

      
        
        1306
        +		t := p.cur

      
        
        1307
        +		if t.Type == token.WHITESPACE || t.Type == token.INDENT {

      
        
        1308
        +			if first.Start.Offset > 0 {

      
        
        1309
        +				pendingWS = t.Literal // only the last whitespace run matters

      
        
        1310
        +			}

      
        
        1311
        +			p.advance()

      
        
        1312
        +			continue

      
        
        1313
        +		}

      
        
        1314
        +		if first.Start.Offset == 0 {

      
        
        1315
        +			first, last = t.Span, t.Span

      
        
        1316
        +			single = t.Literal

      
        
        1317
        +		} else {

      
        
        1318
        +			if single != "" {

      
        
        1319
        +				b.WriteString(single)

      
        
        1320
        +				single = ""

      
        
        1321
        +			}

      
        
        1322
        +			if pendingWS != "" {

      
        
        1323
        +				b.WriteString(pendingWS)

      
        
        1324
        +				pendingWS = ""

      
        
        1325
        +			}

      
        
        1326
        +			b.WriteString(t.Literal)

      
        
        1327
        +			last = t.Span

      
        
        1328
        +		}

      
        
        1329
        +		p.advance()

      
        
        1330
        +	}

      
        
        1331
        +	if first.Start.Offset == 0 {

      
        
        1332
        +		return "", token.Span{}

      
        
        1333
        +	}

      
        
        1334
        +	if single != "" {

      
        
        1335
        +		return single, token.Span{Start: first.Start, End: last.End}

      
        
        1336
        +	}

      
        
        1337
        +	return strings.TrimSpace(b.String()), token.Span{Start: first.Start, End: last.End}

      
        
        1338
        +}

      
        
        1339
        +

      
        
        1340
        +// parseTextComment consumes a comment line whose marker ('#', '%' or '*')

      
        
        1341
        +// lexed as TEXT inside a directive block, building the comment from token

      
        
        1342
        +// literals. Tags are not extracted; the comment is one line.

      
        
        1343
        +func (p *Parser) parseTextComment() *ast.Comment {

      
        
        1344
        +	s := p.cur.Span

      
        
        1345
        +	marker := p.cur.Literal[0]

      
        
        1346
        +	var b strings.Builder

      
        
        1347
        +	b.WriteString(p.cur.Literal)

      
        
        1348
        +	p.advance()

      
        
        1349
        +	for !p.got(token.NEWLINE) && !p.got(token.EOF) {

      
        
        1350
        +		b.WriteString(p.cur.Literal)

      
        
        1351
        +		p.advance()

      
        
        1352
        +	}

      
        
        1353
        +	text := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(b.String()), string(marker)))

      
        
        1354
        +	span := p.span(s) // marker to line end, without the newline

      
        
        1355
        +	p.expectNewline()

      
        
        1356
        +	return &ast.Comment{Marker: marker, Text: text, Span: span}

      
        
        1357
        +}

      
        
        1358
        +

      
        1196
        1359
         func isDirectiveKeyword(t token.Type) bool {

      
        1197
        1360
         	switch t {

      
        1198
        1361
         	case token.COMMENTKW, token.ACCOUNT, token.COMMODITY, token.INCLUDE,

      ···
        1263
        1426
         	}

      
        1264
        1427
         

      
        1265
        1428
         	if len(seps) == 0 {

      
        1266
        
        -		return ast.QuantityFormat{Decimal: '.', Thousands: 0, Precision: 0}

      
        
        1429
        +		return ast.QuantityFormat{}

      
        1267
        1430
         	}

      
        1268
        1431
         

      
        1269
        1432
         	last := seps[len(seps)-1]

      
        1270
        1433
         	dec := lit[last]

      
        
        1434
        +	if dec != '.' && dec != ',' {

      
        
        1435
        +		// the last separator is a thousands mark; the literal has no decimal mark

      
        
        1436
        +		dec = 0

      
        
        1437
        +	}

      
        1271
        1438
         	var thou byte

      
        1272
        1439
         	if len(seps) > 1 {

      
        1273
        1440
         		thou = lit[seps[0]]

      
        1274
        
        -	} else if dec == ' ' || dec == '_' || dec == '\'' {

      
        
        1441
        +	} else if dec == 0 {

      
        1275
        1442
         		// single space/underscore/apostrophe is always thousands

      
        1276
        
        -		thou = dec

      
        1277
        
        -		dec = '.'

      
        
        1443
        +		thou = lit[last]

      
        1278
        1444
         	}

      
        1279
        1445
         

      
        1280
        1446
         	// calculate precision when the last separator is a real decimal

      
M journal/parser/parser_test.go
···
        41
        41
         		"account directive",

      
        42
        42
         		"account directive with comment",

      
        43
        43
         		"account with subdirectives",

      
        
        44
        +		"account subdirectives types",

      
        44
        45
         		"comodity directive",

      
        45
        46
         		"comodity directive word",

      
        46
        47
         		"comodity directive no space",

      
        47
        48
         		"commodity quantity first",

      
        48
        49
         		"commodity quantity after",

      
        49
        50
         		"commodity with subdirectives",

      
        
        51
        +		"commodity format errors",

      
        50
        52
         		"payee directive",

      
        51
        53
         		"transaction",

      
        52
        54
         		"automated transaction",

      ···
        101
        103
         	for _, tt := range tests {

      
        102
        104
         		t.Run(tt, func(t *testing.T) {

      
        103
        105
         			a := golden.Read(t, tt)

      
        104
        
        -			l := lexer.New("j", string(a.Get("input")))

      
        
        106
        +			l := lexer.New("j", a.Get("input"))

      
        105
        107
         			f := New(l).ParseJournal()

      
        106
        108
         			golden.Assert(t, a, ast.Dump(f))

      
        107
        109
         		})

      ···
        144
        146
         	f.Add([]byte{0xff, 0xfe, 0x00})

      
        145
        147
         

      
        146
        148
         	f.Fuzz(func(t *testing.T, data []byte) {

      
        147
        
        -		l := lexer.New("j", string(data))

      
        
        149
        +		l := lexer.New("j", data)

      
        148
        150
         		j := New(l).ParseJournal()

      
        149
        151
         		if j == nil {

      
        150
        152
         			t.Fatalf("nil journal for input %q", string(data))

      
M journal/parser/testdata/C_directive.txtar
···
        18
        18
                 CommodityPos: After

      
        19
        19
                 HasSpace: false

      
        20
        20
                 Precision: 0

      
        21
        
        -        Decimal: "."

      
M journal/parser/testdata/D_directive.txtar
···
        18
        18
               CommodityPos: After

      
        19
        19
               HasSpace: true

      
        20
        20
               Precision: 0

      
        21
        
        -      Decimal: "."

      
A journal/parser/testdata/account_subdirectives_types.txtar
···
        
        1
        +-- input --

      
        
        2
        +account expenses:food

      
        
        3
        +  type X

      
        
        4
        +  note grocery shopping  ; what we spend

      
        
        5
        +  alias food

      
        
        6
        +

      
        
        7
        +account assets:bank

      
        
        8
        +  type asset

      
        
        9
        +  # hash comment

      
        
        10
        +  payee ^store$

      
        
        11
        +  alias bank

      
        
        12
        +  type A  ; duplicate type

      
        
        13
        +  alias

      
        
        14
        +

      
        
        15
        +-- expect --

      
        
        16
        +Journal

      
        
        17
        +  AccountDirective j:1:1-6:0

      
        
        18
        +    Account j:1:9-2:0

      
        
        19
        +      SubAccount: "expenses" j:1:9-1:17

      
        
        20
        +      SubAccount: "food" j:1:18-2:0

      
        
        21
        +    Subdirective

      
        
        22
        +      Name: "type" j:2:3-2:7

      
        
        23
        +      Value: "X" j:2:8-3:0

      
        
        24
        +    Subdirective

      
        
        25
        +      Name: "note" j:3:3-3:7

      
        
        26
        +      Value: "grocery shopping" j:3:8-3:24

      
        
        27
        +      Comment j:3:26-4:0

      
        
        28
        +        Marker: ";"

      
        
        29
        +        Text: "what we spend"

      
        
        30
        +    Subdirective

      
        
        31
        +      Name: "alias" j:4:3-4:8

      
        
        32
        +      Value: "food" j:4:9-5:0

      
        
        33
        +  BlankLine j:6:0-6:1

      
        
        34
        +  AccountDirective j:6:1-14:0

      
        
        35
        +    Account j:6:9-7:0

      
        
        36
        +      SubAccount: "assets" j:6:9-6:15

      
        
        37
        +      SubAccount: "bank" j:6:16-7:0

      
        
        38
        +    Subdirective

      
        
        39
        +      Name: "type" j:7:3-7:7

      
        
        40
        +      Value: "asset" j:7:8-8:0

      
        
        41
        +    Comment j:8:3-9:0

      
        
        42
        +      Marker: "#"

      
        
        43
        +      Text: "hash comment"

      
        
        44
        +    Subdirective

      
        
        45
        +      Name: "alias" j:10:3-10:8

      
        
        46
        +      Value: "bank" j:10:9-11:0

      
        
        47
        +    Subdirective

      
        
        48
        +      Name: "type" j:11:3-11:7

      
        
        49
        +      Value: "A" j:11:8-11:9

      
        
        50
        +      Comment j:11:11-12:0

      
        
        51
        +        Marker: ";"

      
        
        52
        +        Text: "duplicate type"

      
        
        53
        +    Subdirective

      
        
        54
        +      Name: "alias" j:12:3-13:0

      
        
        55
        +      Value: "" 0:0-0:0

      
        
        56
        +  BlankLine j:14:0-14:1

      
        
        57
        +  Errors

      
        
        58
        +    j:9:3-9:8: unknown subdirective "payee"

      
        
        59
        +    j:13:0-14:0: expected value for subdirective "alias"

      
M journal/parser/testdata/account_with_subdirectives.txtar
···
        8
        8
             Account j:1:9-2:0

      
        9
        9
               SubAccount: "expenses" j:1:9-1:17

      
        10
        10
               SubAccount: "food" j:1:18-2:0

      
        
        11
        +    Subdirective

      
        
        12
        +      Name: "note" j:2:3-2:7

      
        
        13
        +      Value: "some note" j:2:8-3:0

      
        
        14
        +    Subdirective

      
        
        15
        +      Name: "alias" j:3:3-3:8

      
        
        16
        +      Value: "food" j:3:9-3:13

      
        
        17
        +      Comment j:3:15-4:0

      
        
        18
        +        Marker: ";"

      
        
        19
        +        Text: "this gets ignored"

      
M journal/parser/testdata/automated_transaction.txtar
···
        39
        39
                 HasSpace: false

      
        40
        40
                 IsExpr: true

      
        41
        41
                 Precision: 0

      
        42
        
        -        Decimal: "."

      
        43
        42
             Posting j:6:1-8:0

      
        44
        43
               Account j:6:5-6:18

      
        45
        44
                 SubAccount: "assets" j:6:5-6:11

      ···
        51
        50
                 HasSpace: false

      
        52
        51
                 IsExpr: true

      
        53
        52
                 Precision: 0

      
        54
        
        -        Decimal: "."

      
        55
        53
           BlankLine j:8:0-8:1

      
        56
        54
           AutomatedTransaction j:8:1-10:1

      
        57
        55
             Expr: "income:salary" j:8:3-9:0

      ···
        67
        65
                 IsExpr: true

      
        68
        66
                 Expr: "amount * 0.5"

      
        69
        67
                 Precision: 0

      
        70
        
        -        Decimal: "."

      
M journal/parser/testdata/bad_between_good.txtar
···
        18
        18
                 CommodityPos: Before

      
        19
        19
                 HasSpace: false

      
        20
        20
                 Precision: 0

      
        21
        
        -        Decimal: "."

      
        22
        21
             Posting j:4:1-5:1

      
        23
        22
               Account j:4:5-4:16

      
        24
        23
                 SubAccount: "assets" j:4:5-4:11

      ···
        29
        28
                 CommodityPos: Before

      
        30
        29
                 HasSpace: false

      
        31
        30
                 Precision: 0

      
        32
        
        -        Decimal: "."

      
        33
        31
           Errors

      
        34
        32
             j:3:5-3:7: expected account name, got ATAT

      
M journal/parser/testdata/bad_posting_at_end.txtar
···
        21
        21
                 CommodityPos: Before

      
        22
        22
                 HasSpace: false

      
        23
        23
                 Precision: 0

      
        24
        
        -        Decimal: "."

      
        25
        24
           BlankLine j:5:0-5:1

      
        26
        25
           Transaction j:5:1-8:1

      
        27
        26
             Date: 2024/01/02

      ···
        36
        35
                 CommodityPos: Before

      
        37
        36
                 HasSpace: false

      
        38
        37
                 Precision: 0

      
        39
        
        -        Decimal: "."

      
        40
        38
             Posting j:7:1-8:1

      
        41
        39
               Account j:7:5-8:0

      
        42
        40
                 SubAccount: "assets" j:7:5-7:11

      
M journal/parser/testdata/bad_then_next_transaction.txtar
···
        21
        21
                 CommodityPos: Before

      
        22
        22
                 HasSpace: false

      
        23
        23
                 Precision: 0

      
        24
        
        -        Decimal: "."

      
        25
        24
           BlankLine j:5:0-5:1

      
        26
        25
           Transaction j:5:1-8:1

      
        27
        26
             Date: 2024/01/02

      ···
        36
        35
                 CommodityPos: Before

      
        37
        36
                 HasSpace: false

      
        38
        37
                 Precision: 0

      
        39
        
        -        Decimal: "."

      
        40
        38
             Posting j:7:1-8:1

      
        41
        39
               Account j:7:5-8:0

      
        42
        40
                 SubAccount: "assets" j:7:5-7:11

      
M journal/parser/testdata/comment_between_bad_postings.txtar
···
        20
        20
                 CommodityPos: Before

      
        21
        21
                 HasSpace: false

      
        22
        22
                 Precision: 0

      
        23
        
        -        Decimal: "."

      
        24
        23
             Posting j:6:1-7:1

      
        25
        24
               Account j:6:5-7:0

      
        26
        25
                 SubAccount: "assets" j:6:5-6:11

      
A journal/parser/testdata/commodity_format_errors.txtar
···
        
        1
        +-- input --

      
        
        2
        +commodity $

      
        
        3
        +  format 1.000,00

      
        
        4
        +

      
        
        5
        +commodity EUR

      
        
        6
        +  format EUR 100.00  ; ok

      
        
        7
        +

      
        
        8
        +commodity $100

      
        
        9
        +-- expect --

      
        
        10
        +Journal

      
        
        11
        +  CommodityDirective j:1:1-4:0

      
        
        12
        +    Commodity: "$" j:1:11-2:0

      
        
        13
        +    Format j:2:3-2:9

      
        
        14
        +      Amount j:2:10-3:0

      
        
        15
        +        Quantity: 1000

      
        
        16
        +        Commodity: ""

      
        
        17
        +        CommodityPos: Before

      
        
        18
        +        HasSpace: false

      
        
        19
        +        Precision: 2

      
        
        20
        +        Decimal: ","

      
        
        21
        +        Thousands: "."

      
        
        22
        +  BlankLine j:4:0-4:1

      
        
        23
        +  CommodityDirective j:4:1-7:0

      
        
        24
        +    Commodity: "EUR" j:4:11-5:0

      
        
        25
        +    Format j:5:3-5:9

      
        
        26
        +      Amount j:5:10-5:20

      
        
        27
        +        Quantity: 100

      
        
        28
        +        Commodity: "EUR" j:5:10-5:13

      
        
        29
        +        CommodityPos: Before

      
        
        30
        +        HasSpace: true

      
        
        31
        +        Precision: 2

      
        
        32
        +        Decimal: "."

      
        
        33
        +      Comment j:5:22-6:0

      
        
        34
        +        Marker: ";"

      
        
        35
        +        Text: "ok"

      
        
        36
        +  BlankLine j:7:0-7:1

      
        
        37
        +  CommodityDirective j:7:1-8:1

      
        
        38
        +    Commodity: "$" j:7:11-7:12

      
        
        39
        +    Format inline

      
        
        40
        +      Amount j:7:12-8:0

      
        
        41
        +        Quantity: 100

      
        
        42
        +        Commodity: "$" j:7:11-7:12

      
        
        43
        +        CommodityPos: Before

      
        
        44
        +        HasSpace: false

      
        
        45
        +        Precision: 0

      
        
        46
        +  Errors

      
        
        47
        +    j:2:10-2:10: commodity directive symbol "$" and format directive symbol "" should be the same

      
        
        48
        +    j:7:12-7:12: Please include a decimal point or decimal comma in commodity directives, to help us parse correctly. It may be followed by zero or more decimal digits.

      
M journal/parser/testdata/commodity_quantity_after.txtar
···
        4
        4
         Journal

      
        5
        5
           CommodityDirective j:1:1-2:1

      
        6
        6
             Commodity: "UAH" j:1:11-1:14

      
        7
        
        -    Amount j:1:15-2:0

      
        8
        
        -      Quantity: 1000

      
        9
        
        -      Commodity: "UAH" j:1:11-1:14

      
        10
        
        -      CommodityPos: Before

      
        11
        
        -      HasSpace: true

      
        12
        
        -      Precision: 2

      
        13
        
        -      Decimal: "."

      
        14
        
        -      Thousands: ","

      
        
        7
        +    Format inline

      
        
        8
        +      Amount j:1:15-2:0

      
        
        9
        +        Quantity: 1000

      
        
        10
        +        Commodity: "UAH" j:1:11-1:14

      
        
        11
        +        CommodityPos: Before

      
        
        12
        +        HasSpace: true

      
        
        13
        +        Precision: 2

      
        
        14
        +        Decimal: "."

      
        
        15
        +        Thousands: ","

      
M journal/parser/testdata/commodity_quantity_first.txtar
···
        4
        4
         Journal

      
        5
        5
           CommodityDirective j:1:1-2:1

      
        6
        6
             Commodity: "UAH" j:1:20-2:0

      
        7
        
        -    Amount j:1:11-2:0

      
        8
        
        -      Quantity: 1000

      
        9
        
        -      Commodity: "UAH" j:1:20-2:0

      
        10
        
        -      CommodityPos: After

      
        11
        
        -      HasSpace: true

      
        12
        
        -      Precision: 2

      
        13
        
        -      Decimal: "."

      
        14
        
        -      Thousands: ","

      
        
        7
        +    Format inline

      
        
        8
        +      Amount j:1:11-2:0

      
        
        9
        +        Quantity: 1000

      
        
        10
        +        Commodity: "UAH" j:1:20-2:0

      
        
        11
        +        CommodityPos: After

      
        
        12
        +        HasSpace: true

      
        
        13
        +        Precision: 2

      
        
        14
        +        Decimal: "."

      
        
        15
        +        Thousands: ","

      
M journal/parser/testdata/commodity_with_subdirectives.txtar
···
        7
        7
         Journal

      
        8
        8
           CommodityDirective j:1:1-5:0

      
        9
        9
             Commodity: "UAH" j:1:11-2:0

      
        10
        
        -    Amount j:2:10-3:0

      
        11
        
        -      Quantity: 1000

      
        12
        
        -      Commodity: "UAH" j:2:19-3:0

      
        13
        
        -      CommodityPos: After

      
        14
        
        -      HasSpace: true

      
        15
        
        -      Precision: 2

      
        16
        
        -      Decimal: "."

      
        17
        
        -      Thousands: " "

      
        
        10
        +    Format j:2:3-2:9

      
        
        11
        +      Amount j:2:10-3:0

      
        
        12
        +        Quantity: 1000

      
        
        13
        +        Commodity: "UAH" j:2:19-3:0

      
        
        14
        +        CommodityPos: After

      
        
        15
        +        HasSpace: true

      
        
        16
        +        Precision: 2

      
        
        17
        +        Decimal: "."

      
        
        18
        +        Thousands: " "

      
        18
        19
           BlankLine j:5:0-5:1

      
        
        20
        +  Errors

      
        
        21
        +    j:3:3-3:7: unknown subdirective "note"

      
M journal/parser/testdata/comodity_directive_no_space.txtar
···
        4
        4
         Journal

      
        5
        5
           CommodityDirective j:1:1-2:1

      
        6
        6
             Commodity: "$" j:1:11-1:12

      
        7
        
        -    Amount j:1:12-2:0

      
        8
        
        -      Quantity: 1000

      
        9
        
        -      Commodity: "$" j:1:11-1:12

      
        10
        
        -      CommodityPos: Before

      
        11
        
        -      HasSpace: false

      
        12
        
        -      Precision: 2

      
        13
        
        -      Decimal: "."

      
        
        7
        +    Format inline

      
        
        8
        +      Amount j:1:12-2:0

      
        
        9
        +        Quantity: 1000

      
        
        10
        +        Commodity: "$" j:1:11-1:12

      
        
        11
        +        CommodityPos: Before

      
        
        12
        +        HasSpace: false

      
        
        13
        +        Precision: 2

      
        
        14
        +        Decimal: "."

      
M journal/parser/testdata/illegal_between_transactions.txtar
···
        21
        21
                 CommodityPos: Before

      
        22
        22
                 HasSpace: false

      
        23
        23
                 Precision: 0

      
        24
        
        -        Decimal: "."

      
        25
        24
             Posting j:3:1-4:1

      
        26
        25
               Account j:3:5-4:0

      
        27
        26
                 SubAccount: "assets" j:3:5-3:11

      ···
        40
        39
                 CommodityPos: Before

      
        41
        40
                 HasSpace: false

      
        42
        41
                 Precision: 0

      
        43
        
        -        Decimal: "."

      
        44
        42
             Posting j:7:1-8:1

      
        45
        43
               Account j:7:5-8:0

      
        46
        44
                 SubAccount: "assets" j:7:5-7:11

      
M journal/parser/testdata/multiple_bad_postings.txtar
···
        19
        19
                 CommodityPos: Before

      
        20
        20
                 HasSpace: false

      
        21
        21
                 Precision: 0

      
        22
        
        -        Decimal: "."

      
        23
        22
             Posting j:5:1-6:1

      
        24
        23
               Account j:5:5-6:0

      
        25
        24
                 SubAccount: "assets" j:5:5-5:11

      
M journal/parser/testdata/period_transaction_expressions.txtar
···
        34
        34
                 CommodityPos: Before

      
        35
        35
                 HasSpace: false

      
        36
        36
                 Precision: 0

      
        37
        
        -        Decimal: "."

      
        38
        37
             Posting j:4:1-6:0

      
        39
        38
               Account j:4:5-5:0

      
        40
        39
                 SubAccount: "assets" j:4:5-4:11

      ···
        56
        55
                 CommodityPos: Before

      
        57
        56
                 HasSpace: false

      
        58
        57
                 Precision: 0

      
        59
        
        -        Decimal: "."

      
        60
        58
             Posting j:8:1-10:0

      
        61
        59
               Account j:8:5-9:0

      
        62
        60
                 SubAccount: "assets" j:8:5-8:11

      ···
        77
        75
                 CommodityPos: Before

      
        78
        76
                 HasSpace: false

      
        79
        77
                 Precision: 0

      
        80
        
        -        Decimal: "."

      
        81
        78
             Posting j:12:1-14:0

      
        82
        79
               Account j:12:5-13:0

      
        83
        80
                 SubAccount: "assets" j:12:5-12:11

      ···
        98
        95
                 CommodityPos: Before

      
        99
        96
                 HasSpace: false

      
        100
        97
                 Precision: 0

      
        101
        
        -        Decimal: "."

      
        102
        98
             Posting j:16:1-18:0

      
        103
        99
               Account j:16:5-17:0

      
        104
        100
                 SubAccount: "assets" j:16:5-16:11

      ···
        120
        116
                 CommodityPos: Before

      
        121
        117
                 HasSpace: false

      
        122
        118
                 Precision: 0

      
        123
        
        -        Decimal: "."

      
        124
        119
             Posting j:20:1-21:1

      
        125
        120
               Account j:20:5-21:0

      
        126
        121
                 SubAccount: "assets" j:20:5-20:11

      
M journal/parser/testdata/quoted_payee_names.txtar
···
        17
        17
                 CommodityPos: Before

      
        18
        18
                 HasSpace: false

      
        19
        19
                 Precision: 0

      
        20
        
        -        Decimal: "."

      
        21
        20
             Posting j:3:1-4:1

      
        22
        21
               Account j:3:3-4:0

      
        23
        22
                 SubAccount: "assets" j:3:3-3:9

      
M journal/parser/testdata/recovery_after_bad_posting.txtar
···
        30
        30
                 CommodityPos: Before

      
        31
        31
                 HasSpace: false

      
        32
        32
                 Precision: 0

      
        33
        
        -        Decimal: "."

      
        34
        33
             Posting j:7:1-8:1

      
        35
        34
               Account j:7:5-8:0

      
        36
        35
                 SubAccount: "assets" j:7:5-7:11

      
M journal/parser/testdata/three_bad_postings.txtar
···
        20
        20
                 CommodityPos: Before

      
        21
        21
                 HasSpace: false

      
        22
        22
                 Precision: 0

      
        23
        
        -        Decimal: "."

      
        24
        23
             Posting j:6:1-7:1

      
        25
        24
               Account j:6:5-7:0

      
        26
        25
                 SubAccount: "assets" j:6:5-6:11

      
M journal/parser/testdata/transaction_in_ukrainian.txtar
···
        17
        17
                 CommodityPos: After

      
        18
        18
                 HasSpace: true

      
        19
        19
                 Precision: 0

      
        20
        
        -        Decimal: "."

      
        21
        20
             Posting j:3:1-4:1

      
        22
        21
               Account j:3:3-4:0

      
        23
        22
                 SubAccount: "активи" j:3:3-3:9

      
M journal/parser/testdata/transaction_with_balance_assignment.txtar
···
        16
        16
                 CommodityPos: Before

      
        17
        17
                 HasSpace: false

      
        18
        18
                 Precision: 0

      
        19
        
        -        Decimal: "."

      
        20
        19
               BalanceAssertion j:2:12-3:0

      
        21
        20
                 IsStrict: false

      
        22
        21
                 IsInclusive: false

      ···
        27
        26
                   CommodityPos: Before

      
        28
        27
                   HasSpace: false

      
        29
        28
                   Precision: 0

      
        30
        
        -          Decimal: "."

      
        31
        29
           BlankLine j:4:0-4:1

      
M journal/parser/testdata/transaction_with_cost_and_assertion.txtar
···
        26
        26
                   CommodityPos: After

      
        27
        27
                   HasSpace: true

      
        28
        28
                   Precision: 0

      
        29
        
        -          Decimal: "."

      
        30
        29
               BalanceAssertion j:2:34-3:0

      
        31
        30
                 IsStrict: false

      
        32
        31
                 IsInclusive: false

      ···
        37
        36
                   CommodityPos: After

      
        38
        37
                   HasSpace: true

      
        39
        38
                   Precision: 0

      
        40
        
        -          Decimal: "."

      
        41
        39
             Posting j:3:1-5:0

      
        42
        40
               Account j:3:2-4:0

      
        43
        41
                 SubAccount: "assets" j:3:2-3:8

      
M journal/parser/testdata/transaction_with_costs.txtar
···
        38
        38
                   CommodityPos: After

      
        39
        39
                   HasSpace: true

      
        40
        40
                   Precision: 0

      
        41
        
        -          Decimal: "."

      
        42
        41
             Posting j:3:1-5:0

      
        43
        42
               Account j:3:2-4:0

      
        44
        43
                 SubAccount: "assets" j:3:2-3:8

      ···
        66
        65
                   CommodityPos: After

      
        67
        66
                   HasSpace: true

      
        68
        67
                   Precision: 0

      
        69
        
        -          Decimal: "."

      
        70
        68
             Posting j:7:1-9:0

      
        71
        69
               Account j:7:2-8:0

      
        72
        70
                 SubAccount: "assets" j:7:2-7:8

      ···
        94
        92
                   CommodityPos: After

      
        95
        93
                   HasSpace: true

      
        96
        94
                   Precision: 0

      
        97
        
        -          Decimal: "."

      
        98
        95
               BalanceAssertion j:10:34-11:0

      
        99
        96
                 IsStrict: false

      
        100
        97
                 IsInclusive: false

      ···
        113
        110
                     CommodityPos: After

      
        114
        111
                     HasSpace: true

      
        115
        112
                     Precision: 0

      
        116
        
        -            Decimal: "."

      
        117
        113
             Posting j:11:1-13:0

      
        118
        114
               Account j:11:2-12:0

      
        119
        115
                 SubAccount: "assets" j:11:2-11:8

      ···
        133
        129
                 CommodityPos: After

      
        134
        130
                 HasSpace: true

      
        135
        131
                 Precision: 0

      
        136
        
        -        Decimal: "."

      
        137
        132
               Cost(unit) j:14:26-15:0

      
        138
        133
                 Amount j:14:28-15:0

      
        139
        134
                   Quantity: 7.53

      
M journal/parser/testdata/transaction_with_inclusive_balance_assertion.txtar
···
        18
        18
                 CommodityPos: Before

      
        19
        19
                 HasSpace: false

      
        20
        20
                 Precision: 0

      
        21
        
        -        Decimal: "."

      
        22
        21
               BalanceAssertion j:2:11-3:0

      
        23
        22
                 IsStrict: false

      
        24
        23
                 IsInclusive: true

      ···
        29
        28
                   CommodityPos: Before

      
        30
        29
                   HasSpace: false

      
        31
        30
                   Precision: 0

      
        32
        
        -          Decimal: "."

      
        33
        31
             Posting j:3:1-4:1

      
        34
        32
               Account j:3:5-3:6

      
        35
        33
                 SubAccount: "b" j:3:5-3:6

      ···
        39
        37
                 CommodityPos: Before

      
        40
        38
                 HasSpace: false

      
        41
        39
                 Precision: 0

      
        42
        
        -        Decimal: "."

      
        43
        40
               BalanceAssertion j:3:11-4:0

      
        44
        41
                 IsStrict: false

      
        45
        42
                 IsInclusive: true

      ···
        50
        47
                   CommodityPos: Before

      
        51
        48
                   HasSpace: false

      
        52
        49
                   Precision: 0

      
        53
        
        -          Decimal: "."

      
        54
        50
             Posting j:4:1-6:0

      
        55
        51
               Account j:4:5-4:6

      
        56
        52
                 SubAccount: "c" j:4:5-4:6

      ···
        60
        56
                 CommodityPos: Before

      
        61
        57
                 HasSpace: false

      
        62
        58
                 Precision: 0

      
        63
        
        -        Decimal: "."

      
        64
        59
               BalanceAssertion j:4:11-5:0

      
        65
        60
                 IsStrict: true

      
        66
        61
                 IsInclusive: true

      ···
        71
        66
                   CommodityPos: Before

      
        72
        67
                   HasSpace: false

      
        73
        68
                   Precision: 0

      
        74
        
        -          Decimal: "."

      
        75
        69
           BlankLine j:6:0-6:1

      
M journal/parser/testdata/transaction_with_scientific_amounts.txtar
···
        18
        18
                 CommodityPos: After

      
        19
        19
                 HasSpace: true

      
        20
        20
                 Precision: 0

      
        21
        
        -        Decimal: "."

      
        22
        21
             Posting j:3:1-4:1

      
        23
        22
               Account j:3:5-3:6

      
        24
        23
                 SubAccount: "b" j:3:5-3:6

      ···
        28
        27
                 CommodityPos: Before

      
        29
        28
                 HasSpace: true

      
        30
        29
                 Precision: 0

      
        31
        
        -        Decimal: "."

      
        32
        30
             Posting j:4:1-6:0

      
        33
        31
               Account j:4:5-4:6

      
        34
        32
                 SubAccount: "c" j:4:5-4:6

      
M journal/parser/testdata/transaction_with_spaced_account_name.txtar
···
        17
        17
                 CommodityPos: Before

      
        18
        18
                 HasSpace: false

      
        19
        19
                 Precision: 0

      
        20
        
        -        Decimal: "."

      
        21
        20
               BalanceAssertion j:2:34-3:0

      
        22
        21
                 IsStrict: false

      
        23
        22
                 IsInclusive: false

      ···
        28
        27
                   CommodityPos: Before

      
        29
        28
                   HasSpace: false

      
        30
        29
                   Precision: 0

      
        31
        
        -          Decimal: "."

      
        32
        30
             Posting j:3:1-5:0

      
        33
        31
               Account j:3:5-4:0

      
        34
        32
                 SubAccount: "equity" j:3:5-3:11

      
M journal/parser/testdata/transaction_with_strange_commodity_symbols.txtar
···
        23
        23
                 CommodityPos: After

      
        24
        24
                 HasSpace: true

      
        25
        25
                 Precision: 0

      
        26
        
        -        Decimal: "."

      
        27
        26
             Posting j:4:1-6:0

      
        28
        27
               Account j:4:3-5:0

      
        29
        28
                 SubAccount: "asdf2" j:4:3-5:0

      ···
        40
        39
                 CommodityPos: After

      
        41
        40
                 HasSpace: true

      
        42
        41
                 Precision: 0

      
        43
        
        -        Decimal: "."

      
        44
        42
             Posting j:8:1-9:1

      
        45
        43
               Account j:8:3-9:0

      
        46
        44
                 SubAccount: "asdf2" j:8:3-9:0

      
M journal/parser/testdata/transaction_with_tag_comments.txtar
···
        24
        24
                 CommodityPos: Before

      
        25
        25
                 HasSpace: false

      
        26
        26
                 Precision: 0

      
        27
        
        -        Decimal: "."

      
        28
        27
               Comment j:2:28-3:0

      
        29
        28
                 Marker: ";"

      
        30
        29
                 Tags

      
M journal/parser/testdata/transaction_with_trailing_indent.txtar
···
        22
        22
                 CommodityPos: Before

      
        23
        23
                 HasSpace: false

      
        24
        24
                 Precision: 0

      
        25
        
        -        Decimal: "."

      
        26
        25
             Posting j:4:1-6:0

      
        27
        26
               Account j:4:5-4:25

      
        28
        27
                 SubAccount: "assets" j:4:5-4:11

      ···
        34
        33
                 CommodityPos: Before

      
        35
        34
                 HasSpace: false

      
        36
        35
                 Precision: 0

      
        37
        
        -        Decimal: "."

      
        38
        36
           BlankLine j:6:0-6:1

      
M journal/parser/testdata/transaction_with_virtual_accounts.txtar
···
        19
        19
                 CommodityPos: After

      
        20
        20
                 HasSpace: true

      
        21
        21
                 Precision: 0

      
        22
        
        -        Decimal: "."

      
        23
        22
             Posting j:3:1-4:1

      
        24
        23
               Type: balanced virtual

      
        25
        24
               Account j:3:3-3:17

      ···
        31
        30
                 CommodityPos: After

      
        32
        31
                 HasSpace: true

      
        33
        32
                 Precision: 0

      
        34
        
        -        Decimal: "."

      
        35
        33
             Posting j:4:1-5:1

      
        36
        34
               Account j:4:2-5:0

      
        37
        35
                 SubAccount: "something" j:4:2-4:11

      
M journal/parser/testdata/virtual_postings_with_statuses.txtar
···
        27
        27
                 CommodityPos: Before

      
        28
        28
                 HasSpace: false

      
        29
        29
                 Precision: 0

      
        30
        
        -        Decimal: "."

      
        31
        30
             Posting j:3:1-5:0

      
        32
        31
               Type: unbalanced virtual

      
        33
        32
               Account j:3:4-3:15

      ···
        39
        38
                 CommodityPos: Before

      
        40
        39
                 HasSpace: false

      
        41
        40
                 Precision: 0

      
        42
        
        -        Decimal: "."

      
        43
        41
           BlankLine j:5:0-5:1

      
        44
        42
           Transaction j:5:1-9:0

      
        45
        43
             Date: 2024/01/01

      ···
        56
        54
                 CommodityPos: Before

      
        57
        55
                 HasSpace: false

      
        58
        56
                 Precision: 0

      
        59
        
        -        Decimal: "."

      
        60
        57
             Posting j:7:1-9:0

      
        61
        58
               Type: unbalanced virtual

      
        62
        59
               Account j:7:6-7:17

      ···
        68
        65
                 CommodityPos: Before

      
        69
        66
                 HasSpace: false

      
        70
        67
                 Precision: 0

      
        71
        
        -        Decimal: "."

      
        72
        68
           BlankLine j:9:0-9:1

      
        73
        69
           Transaction j:9:1-12:1

      
        74
        70
             Date: 2024/01/01

      ···
        85
        81
                 CommodityPos: Before

      
        86
        82
                 HasSpace: false

      
        87
        83
                 Precision: 0

      
        88
        
        -        Decimal: "."

      
        89
        84
             Posting j:11:1-12:1

      
        90
        85
               Type: unbalanced virtual

      
        91
        86
               Status: "*"

      ···
        98
        93
                 CommodityPos: Before

      
        99
        94
                 HasSpace: false

      
        100
        95
                 Precision: 0

      
        101
        
        -        Decimal: "."

      
M journal/printer/directives.go
···
        17
        17
         	p.buf.WriteString("account ")

      
        18
        18
         	p.buf.WriteString(a.Account.String())

      
        19
        19
         	p.writeInlineComment(a.Comment)

      
        
        20
        +	for _, sd := range a.Subdirectives {

      
        
        21
        +		p.buf.WriteByte('\n')

      
        
        22
        +		p.buf.WriteString(p.indent)

      
        
        23
        +		if sd.Name == "" {

      
        
        24
        +			p.writeComment(sd.Comment)

      
        
        25
        +			continue

      
        
        26
        +		}

      
        
        27
        +		p.buf.WriteString(sd.Name)

      
        
        28
        +		p.buf.WriteByte(' ')

      
        
        29
        +		p.buf.WriteString(sd.Value)

      
        
        30
        +		p.writeInlineComment(sd.Comment)

      
        
        31
        +	}

      
        20
        32
         }

      
        21
        33
         

      
        22
        34
         func (p *printer) writeCommodityDirective(c *ast.CommodityDirective) {

      
        23
        35
         	p.buf.WriteString("commodity ")

      
        24
        
        -	if c.Format.Quantity.IsZero() {

      
        
        36
        +	if c.FormatSub == nil {

      
        25
        37
         		p.buf.WriteString(c.Commodity)

      
        26
        38
         		p.writeInlineComment(c.Comment)

      
        27
        39
         		return

      
        28
        40
         	}

      
        29
        
        -	prec := max(c.Format.QuantityFmt.Precision, 2)

      
        30
        
        -	switch c.Format.CommodityPos {

      
        
        41
        +	if c.FormatSub.KeywordSpan.End.Offset == 0 {

      
        
        42
        +		// inline form: "commodity $1,000.00"

      
        
        43
        +		p.writeCommodityAmount(&c.FormatSub.Amount)

      
        
        44
        +		p.writeInlineComment(c.Comment)

      
        
        45
        +		return

      
        
        46
        +	}

      
        
        47
        +	p.buf.WriteString(c.Commodity)

      
        
        48
        +	p.writeInlineComment(c.Comment)

      
        
        49
        +	p.buf.WriteByte('\n')

      
        
        50
        +	p.buf.WriteString(p.indent)

      
        
        51
        +	p.buf.WriteString("format ")

      
        
        52
        +	p.writeCommodityAmount(&c.FormatSub.Amount)

      
        
        53
        +	p.writeInlineComment(c.FormatSub.Comment)

      
        
        54
        +	p.writeBlockComments(c.BlockComments)

      
        
        55
        +}

      
        
        56
        +

      
        
        57
        +// writeCommodityAmount renders a commodity format amount, preserving the

      
        
        58
        +// original commodity placement instead of the config's CommodityPos.

      
        
        59
        +func (p *printer) writeCommodityAmount(a *ast.Amount) {

      
        
        60
        +	prec := max(a.QuantityFmt.Precision, 2)

      
        
        61
        +	if a.Commodity == "" {

      
        
        62
        +		p.writeDecimal(a.Quantity, a.QuantityFmt, prec)

      
        
        63
        +		return

      
        
        64
        +	}

      
        
        65
        +	switch a.CommodityPos {

      
        31
        66
         	case ast.CommodityBefore:

      
        32
        
        -		p.buf.WriteString(c.Format.Commodity)

      
        33
        
        -		if c.Format.HasSpace {

      
        
        67
        +		p.buf.WriteString(a.Commodity)

      
        
        68
        +		if a.HasSpace {

      
        34
        69
         			p.buf.WriteByte(' ')

      
        35
        70
         		}

      
        36
        
        -		p.writeDecimal(c.Format.Quantity, c.Format.QuantityFmt, prec)

      
        
        71
        +		p.writeDecimal(a.Quantity, a.QuantityFmt, prec)

      
        37
        72
         	case ast.CommodityAfter:

      
        38
        
        -		p.writeDecimal(c.Format.Quantity, c.Format.QuantityFmt, prec)

      
        39
        
        -		if c.Format.HasSpace {

      
        
        73
        +		p.writeDecimal(a.Quantity, a.QuantityFmt, prec)

      
        
        74
        +		if a.HasSpace {

      
        40
        75
         			p.buf.WriteByte(' ')

      
        41
        76
         		}

      
        42
        
        -		p.buf.WriteString(c.Format.Commodity)

      
        
        77
        +		p.buf.WriteString(a.Commodity)

      
        
        78
        +	}

      
        
        79
        +}

      
        
        80
        +

      
        
        81
        +// writeBlockComments writes indented comment lines inside a directive block.

      
        
        82
        +func (p *printer) writeBlockComments(cs []*ast.Comment) {

      
        
        83
        +	for _, c := range cs {

      
        
        84
        +		p.buf.WriteByte('\n')

      
        
        85
        +		p.buf.WriteString(p.indent)

      
        
        86
        +		p.writeComment(c)

      
        43
        87
         	}

      
        44
        
        -	p.writeInlineComment(c.Comment)

      
        45
        88
         }

      
        46
        89
         

      
        47
        90
         func (p *printer) writeAliasDirective(a *ast.AliasDirective) {

      
M journal/printer/testdata/directives.txtar
···
        1
        1
         -- input --

      
        2
        2
         ; Directives

      
        3
        3
         account Assets:Cash ; cash account

      
        
        4
        +account Assets:Bank

      
        
        5
        +  type A

      
        
        6
        +  note main bank account  ; bank note

      
        
        7
        +  alias bank

      
        
        8
        +  alias checking  ; checking alias

      
        
        9
        +  ; some note

      
        
        10
        +  # hash comment

      
        4
        11
         commodity $

      
        5
        12
         commodity UAH

      
        
        13
        +commodity EUR

      
        
        14
        +  format EUR 1.000,00  ; euro format

      
        
        15
        +commodity CAD

      
        
        16
        +  format 1 000.00 CAD

      
        6
        17
         commodity 1000.00 USD

      
        7
        18
         commodity $1000.00

      
        8
        19
         commodity 1,000.00 UAH

      ···
        39
        50
         -- expect --

      
        40
        51
         ; Directives

      
        41
        52
         account Assets:Cash  ; cash account

      
        
        53
        +account Assets:Bank

      
        
        54
        +  type A

      
        
        55
        +  note main bank account  ; bank note

      
        
        56
        +  alias bank

      
        
        57
        +  alias checking  ; checking alias

      
        
        58
        +  ; some note

      
        
        59
        +  # hash comment

      
        42
        60
         commodity $

      
        43
        61
         commodity UAH

      
        
        62
        +commodity EUR

      
        
        63
        +  format EUR 1.000,00  ; euro format

      
        
        64
        +commodity CAD

      
        
        65
        +  format 1 000.00 CAD

      
        44
        66
         commodity 1000.00 USD

      
        45
        67
         commodity $1000.00

      
        46
        68
         commodity 1,000.00 UAH

      
A journal/testdata/journals/subdirectives.journal
···
        
        1
        +; subdirectives coverage

      
        
        2
        +;

      
        
        3
        +; account directive blocks: alias/type/note subdirectives and comment lines;

      
        
        4
        +; commodity directive blocks: format subdirectives (inline and block forms)

      
        
        5
        +; and comment lines

      
        
        6
        +

      
        
        7
        +commodity $

      
        
        8
        +commodity $0.00

      
        
        9
        +commodity EUR

      
        
        10
        +  format EUR 1.000,00

      
        
        11
        +commodity UAH

      
        
        12
        +  format 1 000.00 UAH  ; hryvnia display format

      
        
        13
        +  ; block comment inside a commodity block

      
        
        14
        +commodity 1,000.00 USD

      
        
        15
        +

      
        
        16
        +account assets:bank

      
        
        17
        +  alias bank

      
        
        18
        +account assets:checking

      
        
        19
        +  type C

      
        
        20
        +  alias checking  ; main checking account

      
        
        21
        +  ; used for daily expenses

      
        
        22
        +  note main checking account

      
        
        23
        +account equity:opening

      
        
        24
        +account expenses:food

      
        
        25
        +  alias food

      
        
        26
        +  alias groceries  ; second alias for the same account

      
        
        27
        +

      
        
        28
        +2024-01-01 Opening balances

      
        
        29
        +    assets:bank      $1000.00

      
        
        30
        +    assets:checking  €200.00

      
        
        31
        +    equity:opening

      
        
        32
        +

      
        
        33
        +2024-01-02 Grocery store

      
        
        34
        +    expenses:food      50.00 UAH

      
        
        35
        +    assets:checking