10 files changed,
0 insertions(+),
645 deletions(-)
drop tags generator
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed at:
2026-07-31 17:45:31 +0300
Authored at:
2026-07-31 17:44:31 +0300
Change ID:
oukpnvvylvrnmznvotkvxkwolykyyrzl
Parent:
8ff5e11
jump to
M
internal/cli/cli.go
··· 63 63 Action: c.lspAction, 64 64 }, 65 65 { 66 - Name: "tags", 67 - Usage: "generate a tags file for journal entries", 68 - Action: c.tagsAction, 69 - Flags: []cli.Flag{&cli.StringFlag{ 70 - Name: "out", 71 - Aliases: []string{"o"}, 72 - Usage: "output file, set to - for stdout", 73 - Value: "tags", 74 - }}, 75 - Arguments: []cli.Argument{&cli.StringArgs{ 76 - Name: "journals", 77 - UsageText: "(path to journal files/directories)", 78 - Min: 0, 79 - Max: -1, 80 - }}, 81 - }, 82 - { 83 66 Name: "lint", 84 67 Usage: "lint journal files for common mistakes", 85 68 Action: c.lintAction,
D
internal/cli/cmd_tags.go
··· 1 -package cli 2 - 3 -import ( 4 - "context" 5 - "fmt" 6 - "io" 7 - "os" 8 - 9 - "github.com/urfave/cli/v3" 10 - 11 - "olexsmir.xyz/clerk/journal" 12 - "olexsmir.xyz/clerk/journal/tags" 13 -) 14 - 15 -func (c *Cli) tagsAction(ctx context.Context, cmd *cli.Command) error { 16 - cwd, err := os.Getwd() 17 - if err != nil { 18 - return fmt.Errorf("getting working directory: %w", err) 19 - } 20 - 21 - journals := cmd.StringArgs("journals") 22 - if len(journals) == 0 { 23 - journals = []string{"."} 24 - } 25 - 26 - journalFiles, err := resolvePaths(journals) 27 - if err != nil { 28 - return err 29 - } 30 - 31 - if len(journalFiles) == 0 { 32 - return fmt.Errorf("no journal files found") 33 - } 34 - 35 - loader := journal.NewLoader() 36 - 37 - var rj *journal.ResolvedJournal 38 - for _, file := range journalFiles { 39 - resolved, err := loader.Resolve(file) 40 - if err != nil { 41 - fmt.Fprintf(os.Stderr, "error: %v\n", err) 42 - continue 43 - } 44 - if rj == nil { 45 - rj = resolved 46 - } else { 47 - // Merge: not supported for multi-root yet — use the last. 48 - rj = resolved 49 - } 50 - } 51 - if rj == nil { 52 - return cli.Exit("", 1) 53 - } 54 - 55 - tagger := tags.New(rj, cwd) 56 - 57 - out := cmd.String("out") 58 - var w io.Writer = os.Stdout 59 - if out != "-" { 60 - f, err := os.Create(out) 61 - if err != nil { 62 - return fmt.Errorf("creating output file: %w", err) 63 - } 64 - defer f.Close() 65 - w = f 66 - } 67 - 68 - if err := tagger.Write(w); err != nil { 69 - return fmt.Errorf("writing tags: %w", err) 70 - } 71 - 72 - return nil 73 -}
D
internal/tags/tags.go
··· 1 -package tags 2 - 3 -import ( 4 - "fmt" 5 - "io" 6 - "slices" 7 - "sort" 8 - "strings" 9 -) 10 - 11 -type Entry struct { 12 - Name string 13 - Kind byte 14 - File string // file path (relative or absolute) 15 - Line int // line number 16 - Pattern string // the full line content (without /^ $/ wrappers) 17 - Language string // language field, emitted per-entry when non-empty 18 -} 19 - 20 -type Writer struct { 21 - entries []Entry 22 - kinds map[byte]string // kind: description 23 -} 24 - 25 -func NewWriter() *Writer { 26 - return &Writer{kinds: make(map[byte]string)} 27 -} 28 - 29 -func (w *Writer) DescribeKind(kind byte, description string) { 30 - w.kinds[kind] = description 31 -} 32 - 33 -func (w *Writer) Add(entry Entry) { 34 - w.entries = append(w.entries, entry) 35 -} 36 - 37 -func (w *Writer) Write(to io.Writer) error { 38 - // Pseudo-tags 39 - _, _ = fmt.Fprintf(to, "!_TAG_FILE_FORMAT\t2\t/extended format/\n") 40 - _, _ = fmt.Fprintf(to, "!_TAG_FILE_SORTED\t1\t/1=sorted/\n") 41 - _, _ = fmt.Fprintf(to, "!_TAG_PROGRAM_NAME\tclerk\t//\n") 42 - _, _ = fmt.Fprintf(to, "!_TAG_PROGRAM_URL\thttps://olexsmir.xyz/clerk\t//\n") // TODO: 43 - _, _ = fmt.Fprintf(to, "!_TAG_PROGRAM_VERSION\t0.1.0\t//\n") 44 - 45 - // Kind descriptions 46 - var kindLetters []byte 47 - for k := range w.kinds { 48 - kindLetters = append(kindLetters, k) 49 - } 50 - 51 - slices.Sort(kindLetters) 52 - for _, k := range kindLetters { 53 - _, _ = fmt.Fprintf(to, "!_TAG_KIND_DESCRIPTION!%c\t%c,%s\t/%s/\n", k, k, w.kinds[k], w.kinds[k]) 54 - } 55 - 56 - // Tags 57 - sort.Slice(w.entries, func(i, j int) bool { 58 - if w.entries[i].Name != w.entries[j].Name { 59 - return w.entries[i].Name < w.entries[j].Name 60 - } 61 - if w.entries[i].Kind != w.entries[j].Kind { 62 - return w.entries[i].Kind < w.entries[j].Kind 63 - } 64 - if w.entries[i].File != w.entries[j].File { 65 - return w.entries[i].File < w.entries[j].File 66 - } 67 - return w.entries[i].Line < w.entries[j].Line 68 - }) 69 - 70 - for _, e := range w.entries { 71 - _, _ = fmt.Fprintf(to, "%s\t%s\t/^%s$/;\"\tkind:%c\tline:%d", e.Name, e.File, escapePattern(e.Pattern), e.Kind, e.Line) 72 - if e.Language != "" { 73 - _, _ = fmt.Fprintf(to, "\tlanguage:%s", e.Language) 74 - } 75 - _, _ = fmt.Fprintf(to, "\n") 76 - } 77 - 78 - return nil 79 -} 80 - 81 -func escapePattern(s string) string { 82 - s = strings.ReplaceAll(s, `\`, `\\`) 83 - s = strings.ReplaceAll(s, `/`, `\/`) 84 - return s 85 -}
D
internal/tags/tags_test.go
··· 1 -package tags 2 - 3 -import ( 4 - "bytes" 5 - "strings" 6 - "testing" 7 -) 8 - 9 -// ctags extended format: https://ctags.sourceforge.net/FORMAT 10 - 11 -func TestWriterRoundTrip(t *testing.T) { 12 - w := NewWriter() 13 - w.DescribeKind('a', "account") 14 - w.DescribeKind('c', "commodity") 15 - w.Add(Entry{Name: "expenses:food", Kind: 'a', File: "test.journal", Line: 3, Pattern: "account expenses:food", Language: "hledger"}) 16 - w.Add(Entry{Name: "$", Kind: 'c', File: "test.journal", Line: 9, Pattern: " expenses:food $50.00", Language: "hledger"}) 17 - w.Add(Entry{Name: "assets:bank", Kind: 'a', File: "test.journal", Line: 5, Pattern: "account assets:bank", Language: "hledger"}) 18 - 19 - var buf bytes.Buffer 20 - if err := w.Write(&buf); err != nil { 21 - t.Fatal(err) 22 - } 23 - out := buf.String() 24 - 25 - // pseudo-tags 26 - for _, tag := range []string{ 27 - "!_TAG_FILE_FORMAT\t2", 28 - "!_TAG_FILE_SORTED\t1", 29 - "!_TAG_PROGRAM_NAME\tclerk", 30 - "!_TAG_PROGRAM_URL\t", 31 - "!_TAG_PROGRAM_VERSION\t", 32 - } { 33 - if !strings.Contains(out, tag) { 34 - t.Errorf("missing: %q", tag) 35 - } 36 - } 37 - 38 - // Verify language field on each tag entry 39 - for line := range strings.SplitSeq(out, "\n") { 40 - if strings.HasPrefix(line, "!_TAG_") || line == "" { 41 - continue 42 - } 43 - if !strings.Contains(line, "\tlanguage:hledger") { 44 - t.Errorf("tag entry missing language:hledger: %q", line) 45 - } 46 - } 47 - 48 - var firstTag string 49 - for line := range strings.SplitSeq(out, "\n") { 50 - if !strings.HasPrefix(line, "!_TAG_") && line != "" { 51 - firstTag = line 52 - break 53 - } 54 - } 55 - 56 - if !strings.HasPrefix(firstTag, "$\ttest.journal\t/") { 57 - t.Errorf("unexpected first entry: %q", firstTag) 58 - } 59 - if !strings.Contains(firstTag, "kind:c") { 60 - t.Errorf("missing kind:c: %q", firstTag) 61 - } 62 - if !strings.Contains(firstTag, "line:9") { 63 - t.Errorf("missing line:9: %q", firstTag) 64 - } 65 -} 66 - 67 -func TestNoLanguageNoFieldDescription(t *testing.T) { 68 - w := NewWriter() 69 - w.DescribeKind('a', "account") 70 - w.Add(Entry{Name: "test", Kind: 'a', File: "f", Line: 1, Pattern: "account test"}) 71 - 72 - var buf bytes.Buffer 73 - if err := w.Write(&buf); err != nil { 74 - t.Fatal(err) 75 - } 76 - out := buf.String() 77 - 78 - if strings.Contains(out, "\tlanguage:") { 79 - t.Error("language: field should not appear when no entries have Language set") 80 - } 81 -}
D
journal/tags/tag.go
··· 1 -package tags 2 - 3 -import ( 4 - "io" 5 - "path/filepath" 6 - "sort" 7 - 8 - "olexsmir.xyz/clerk/internal/tags" 9 - "olexsmir.xyz/clerk/journal" 10 - "olexsmir.xyz/clerk/journal/ast" 11 - "olexsmir.xyz/clerk/journal/token" 12 -) 13 - 14 -const ( 15 - KindAccount = 'a' 16 - KindAccountDesc = "account" 17 - 18 - KindCommodity = 'c' 19 - KindCommodityDesc = "commodity" 20 - 21 - KindPayee = 'p' 22 - KindPayeeDesc = "payee" 23 - 24 - KindTag = 't' 25 - KindTagDesc = "tag" 26 -) 27 - 28 -// priority constants for source types - lower wins. 29 -const ( 30 - prioTagDirective = 0 31 - 32 - prioAccountDirective = 0 33 - prioAliasDirective = 1 34 - prioAccountPosting = 2 35 - 36 - prioCommodityDirective = 0 37 - prioMarketPrice = 1 38 - prioAmountCommodity = 2 39 - 40 - prioPayeeDirective = 0 41 - prioPayeeTransaction = 1 42 -) 43 - 44 -// candidate is a potential tag before sorting and dedup. 45 -type candidate struct { 46 - name string 47 - kind byte 48 - file string 49 - line int 50 - priority int 51 - src []byte 52 - offset int 53 -} 54 - 55 -type Tagger struct { 56 - rj *journal.ResolvedJournal 57 - relDir string 58 - 59 - candidates []candidate 60 -} 61 - 62 -// New creates a [Tagger] from a resolved journal. If relDir is not empty, 63 -// file paths in the tag output are made relative to relDir. 64 -func New(rj *journal.ResolvedJournal, relDir string) *Tagger { 65 - return &Tagger{rj: rj, relDir: relDir} 66 -} 67 - 68 -// Write generate tags file and write it in tags format. 69 -func (t *Tagger) Write(w io.Writer) error { 70 - tw := tags.NewWriter() 71 - tw.DescribeKind(KindAccount, KindAccountDesc) 72 - tw.DescribeKind(KindCommodity, KindCommodityDesc) 73 - tw.DescribeKind(KindPayee, KindPayeeDesc) 74 - tw.DescribeKind(KindTag, KindTagDesc) 75 - for _, e := range t.collect() { 76 - tw.Add(e) 77 - } 78 - return tw.Write(w) 79 -} 80 - 81 -func (t *Tagger) collect() []tags.Entry { 82 - t.candidates = nil 83 - 84 - for _, item := range t.rj.Items { 85 - if item.IsInclude { 86 - continue 87 - } 88 - pf := item.Occurrence 89 - filePath := pf.Path 90 - if t.relDir != "" { 91 - filePath = relativePath(filePath, t.relDir) 92 - } 93 - t.collectFromEntry(pf, filePath, pf.Ast.Entries[item.EntryIndex]) 94 - } 95 - 96 - // sort by priority 97 - sort.Slice(t.candidates, func(i, j int) bool { 98 - if t.candidates[i].name != t.candidates[j].name { 99 - return t.candidates[i].name < t.candidates[j].name 100 - } 101 - if t.candidates[i].kind != t.candidates[j].kind { 102 - return t.candidates[i].kind < t.candidates[j].kind 103 - } 104 - if t.candidates[i].priority != t.candidates[j].priority { 105 - return t.candidates[i].priority < t.candidates[j].priority 106 - } 107 - if t.candidates[i].file != t.candidates[j].file { 108 - return t.candidates[i].file < t.candidates[j].file 109 - } 110 - return t.candidates[i].line < t.candidates[j].line 111 - }) 112 - 113 - // deduplicate 114 - var entries []tags.Entry 115 - for i, c := range t.candidates { 116 - if i > 0 && t.candidates[i-1].name == c.name && t.candidates[i-1].kind == c.kind { 117 - continue 118 - } 119 - entries = append(entries, tags.Entry{ 120 - Name: c.name, 121 - File: c.file, 122 - Line: c.line, 123 - Pattern: extractLine(c.src, c.offset), 124 - Kind: c.kind, 125 - Language: "hledger", 126 - }) 127 - } 128 - 129 - return entries 130 -} 131 - 132 -func (t *Tagger) collectFromEntry(pf *journal.ParsedFile, fpath string, entry ast.Entry) { 133 - switch e := entry.(type) { 134 - case *ast.AccountDirective: 135 - t.addCandidate(e.Account.String(), fpath, e.Account.Span, pf.Src, KindAccount, prioAccountDirective) 136 - 137 - case *ast.CommodityDirective: 138 - t.addCandidate(e.Commodity, fpath, e.Span, pf.Src, KindCommodity, prioCommodityDirective) 139 - 140 - case *ast.PayeeDirective: 141 - t.addCandidate(e.Name, fpath, e.Span, pf.Src, KindPayee, prioPayeeDirective) 142 - 143 - case *ast.TagDirective: 144 - t.addCandidate(e.Name, fpath, e.Span, pf.Src, KindTag, prioTagDirective) 145 - 146 - case *ast.Transaction: 147 - if e.Payee != nil { 148 - t.addCandidate(e.Payee.Name, fpath, e.Payee.Span, pf.Src, KindPayee, prioPayeeTransaction) 149 - } 150 - for _, p := range e.Postings { 151 - t.collectFromPosting(p, fpath, pf.Src) 152 - } 153 - 154 - case *ast.PeriodicTransaction: 155 - for _, p := range e.Postings { 156 - t.collectFromPosting(p, fpath, pf.Src) 157 - } 158 - 159 - case *ast.AutomatedTransaction: 160 - for _, p := range e.Postings { 161 - t.collectFromPosting(p, fpath, pf.Src) 162 - } 163 - 164 - case *ast.MarketPriceDirective: 165 - t.addCandidate(e.Commodity, fpath, e.Span, pf.Src, KindCommodity, prioMarketPrice) 166 - if e.Amount.Commodity != "" { 167 - t.addCandidate(e.Amount.Commodity, fpath, e.Amount.Span, pf.Src, KindCommodity, prioMarketPrice) 168 - } 169 - 170 - case *ast.DefaultCommodityDirective: 171 - if e.Amount.Commodity != "" { 172 - t.addCandidate(e.Amount.Commodity, fpath, e.Amount.Span, pf.Src, KindCommodity, prioAmountCommodity) 173 - } 174 - 175 - case *ast.ConversionDirective: 176 - if e.From.Commodity != "" { 177 - t.addCandidate(e.From.Commodity, fpath, e.From.Span, pf.Src, KindCommodity, prioAmountCommodity) 178 - } 179 - if e.To.Commodity != "" { 180 - t.addCandidate(e.To.Commodity, fpath, e.To.Span, pf.Src, KindCommodity, prioAmountCommodity) 181 - } 182 - 183 - case *ast.AliasDirective: 184 - t.addCandidate(e.From.String(), fpath, e.Span, pf.Src, KindAccount, prioAliasDirective) 185 - t.addCandidate(e.To.String(), fpath, e.Span, pf.Src, KindAccount, prioAliasDirective) 186 - } 187 -} 188 - 189 -func (t *Tagger) collectFromPosting(p *ast.Posting, filePath string, src []byte) { 190 - t.addCandidate(p.Account.String(), filePath, p.Account.Span, src, KindAccount, prioAccountPosting) 191 - if p.Amount != nil && p.Amount.Commodity != "" { 192 - t.addCandidate(p.Amount.Commodity, filePath, p.Amount.Span, src, KindCommodity, prioAmountCommodity) 193 - } 194 - if p.Cost != nil && p.Cost.Amount.Commodity != "" { 195 - t.addCandidate(p.Cost.Amount.Commodity, filePath, p.Cost.Amount.Span, src, KindCommodity, prioAmountCommodity) 196 - } 197 - if p.Balance != nil && p.Balance.Amount.Commodity != "" { 198 - t.addCandidate(p.Balance.Amount.Commodity, filePath, p.Balance.Amount.Span, src, KindCommodity, prioAmountCommodity) 199 - } 200 -} 201 - 202 -func (t *Tagger) addCandidate(name, fpath string, span token.Span, src []byte, kind byte, priority int) { 203 - if name == "" { 204 - return 205 - } 206 - t.candidates = append(t.candidates, candidate{ 207 - name: name, 208 - kind: kind, 209 - file: fpath, 210 - line: span.Start.Line, 211 - priority: priority, 212 - src: src, 213 - offset: span.Start.Offset, 214 - }) 215 -} 216 - 217 -func relativePath(target, base string) string { 218 - rel, err := filepath.Rel(base, target) 219 - if err != nil { 220 - return target 221 - } 222 - return rel 223 -} 224 - 225 -// extractLine returns the source line containing offset. 226 -// Caller guarantees src is non-nil and 0 <= offset < len(src). 227 -func extractLine(src []byte, offset int) string { 228 - start := offset 229 - for start > 0 && src[start-1] != '\n' { 230 - start-- 231 - } 232 - end := offset 233 - for end < len(src) && src[end] != '\n' { 234 - end++ 235 - } 236 - return string(src[start:end]) 237 -}
D
journal/tags/tag_test.go
··· 1 -package tags 2 - 3 -import ( 4 - "bytes" 5 - "testing" 6 - 7 - "olexsmir.xyz/clerk/internal/testutil/golden" 8 - "olexsmir.xyz/clerk/journal" 9 -) 10 - 11 -func TestTags(t *testing.T) { 12 - tests := []string{"basic", "kinds", "duplicates", "crossfile"} 13 - for _, tt := range tests { 14 - t.Run(tt, func(t *testing.T) { 15 - t.Parallel() 16 - a := golden.Read(t, tt) 17 - fsys, err := a.FS() 18 - if err != nil { 19 - t.Fatal(err) 20 - } 21 - l := journal.NewLoader() 22 - rj, err := l.ResolveFS(fsys, "in.journal") 23 - if err != nil { 24 - t.Fatalf("loading journal: %v", err) 25 - } 26 - tagger := New(rj, "") 27 - var buf bytes.Buffer 28 - if err := tagger.Write(&buf); err != nil { 29 - t.Fatal(err) 30 - } 31 - golden.Assert(t, a, buf.String()) 32 - }) 33 - } 34 -}
D
journal/tags/testdata/basic.txtar
··· 1 --- in.journal -- 2 -; basic journal — accounts + commodities from directives and postings 3 - 4 -account expenses:food 5 -account expenses:rent 6 -account assets:bank 7 -account assets:cash 8 - 9 -2025-01-15 * "Groceries" 10 - expenses:food $50.00 11 - assets:bank 12 - 13 -2025-02-01 "Rent" 14 - expenses:rent €800.00 15 - assets:bank €-800.00 16 - 17 -P 2025-01-15 € $1.05 18 - 19 --- expect -- 20 -!_TAG_FILE_FORMAT 2 /extended format/ 21 -!_TAG_FILE_SORTED 1 /1=sorted/ 22 -!_TAG_PROGRAM_NAME clerk // 23 -!_TAG_PROGRAM_URL https://olexsmir.xyz/clerk // 24 -!_TAG_PROGRAM_VERSION 0.1.0 // 25 -!_TAG_KIND_DESCRIPTION!a a,account /account/ 26 -!_TAG_KIND_DESCRIPTION!c c,commodity /commodity/ 27 -!_TAG_KIND_DESCRIPTION!p p,payee /payee/ 28 -!_TAG_KIND_DESCRIPTION!t t,tag /tag/ 29 -$ in.journal /^P 2025-01-15 € $1.05$/;" kind:c line:16 language:hledger 30 -Groceries in.journal /^2025-01-15 * "Groceries"$/;" kind:p line:8 language:hledger 31 -Rent in.journal /^2025-02-01 "Rent"$/;" kind:p line:12 language:hledger 32 -assets:bank in.journal /^account assets:bank$/;" kind:a line:5 language:hledger 33 -assets:cash in.journal /^account assets:cash$/;" kind:a line:6 language:hledger 34 -expenses:food in.journal /^account expenses:food$/;" kind:a line:3 language:hledger 35 -expenses:rent in.journal /^account expenses:rent$/;" kind:a line:4 language:hledger 36 -€ in.journal /^P 2025-01-15 € $1.05$/;" kind:c line:16 language:hledger
D
journal/tags/testdata/crossfile.txtar
··· 1 --- in.journal -- 2 -include decls.journal 3 - 4 -2024-01-15 "Amazon" 5 - expenses:food $50.00 6 - assets:bank 7 - 8 --- decls.journal -- 9 -commodity $ 10 -account assets:bank 11 -payee Amazon 12 - 13 --- expect -- 14 -!_TAG_FILE_FORMAT 2 /extended format/ 15 -!_TAG_FILE_SORTED 1 /1=sorted/ 16 -!_TAG_PROGRAM_NAME clerk // 17 -!_TAG_PROGRAM_URL https://olexsmir.xyz/clerk // 18 -!_TAG_PROGRAM_VERSION 0.1.0 // 19 -!_TAG_KIND_DESCRIPTION!a a,account /account/ 20 -!_TAG_KIND_DESCRIPTION!c c,commodity /commodity/ 21 -!_TAG_KIND_DESCRIPTION!p p,payee /payee/ 22 -!_TAG_KIND_DESCRIPTION!t t,tag /tag/ 23 -$ decls.journal /^commodity $$/;" kind:c line:1 language:hledger 24 -Amazon decls.journal /^payee Amazon$/;" kind:p line:3 language:hledger 25 -assets:bank decls.journal /^account assets:bank$/;" kind:a line:2 language:hledger 26 -expenses:food in.journal /^ expenses:food $50.00$/;" kind:a line:4 language:hledger
D
journal/tags/testdata/duplicates.txtar
··· 1 --- in.journal -- 2 -; same account name in directive AND postings — should use directive line 3 - 4 -account test 5 - 6 -2025-01-01 "test txn" 7 - test 1 8 - other 9 - 10 -2025-02-01 "another" 11 - test 2 12 - other 13 - 14 --- expect -- 15 -!_TAG_FILE_FORMAT 2 /extended format/ 16 -!_TAG_FILE_SORTED 1 /1=sorted/ 17 -!_TAG_PROGRAM_NAME clerk // 18 -!_TAG_PROGRAM_URL https://olexsmir.xyz/clerk // 19 -!_TAG_PROGRAM_VERSION 0.1.0 // 20 -!_TAG_KIND_DESCRIPTION!a a,account /account/ 21 -!_TAG_KIND_DESCRIPTION!c c,commodity /commodity/ 22 -!_TAG_KIND_DESCRIPTION!p p,payee /payee/ 23 -!_TAG_KIND_DESCRIPTION!t t,tag /tag/ 24 -another in.journal /^2025-02-01 "another"$/;" kind:p line:9 language:hledger 25 -other in.journal /^ other$/;" kind:a line:7 language:hledger 26 -test in.journal /^account test$/;" kind:a line:3 language:hledger 27 -test txn in.journal /^2025-01-01 "test txn"$/;" kind:p line:5 language:hledger
D
journal/tags/testdata/kinds.txtar
··· 1 --- in.journal -- 2 -; all four tag kinds via directives 3 - 4 -account expenses:food 5 -commodity $ 6 -commodity EUR 7 -payee Amazon 8 -tag receipt 9 - 10 -2025-03-01 * "Amazon" 11 - expenses:food $45.00 12 - assets:bank 13 - 14 --- expect -- 15 -!_TAG_FILE_FORMAT 2 /extended format/ 16 -!_TAG_FILE_SORTED 1 /1=sorted/ 17 -!_TAG_PROGRAM_NAME clerk // 18 -!_TAG_PROGRAM_URL https://olexsmir.xyz/clerk // 19 -!_TAG_PROGRAM_VERSION 0.1.0 // 20 -!_TAG_KIND_DESCRIPTION!a a,account /account/ 21 -!_TAG_KIND_DESCRIPTION!c c,commodity /commodity/ 22 -!_TAG_KIND_DESCRIPTION!p p,payee /payee/ 23 -!_TAG_KIND_DESCRIPTION!t t,tag /tag/ 24 -$ in.journal /^commodity $$/;" kind:c line:4 language:hledger 25 -Amazon in.journal /^payee Amazon$/;" kind:p line:6 language:hledger 26 -EUR in.journal /^commodity EUR$/;" kind:c line:5 language:hledger 27 -assets:bank in.journal /^ assets:bank$/;" kind:a line:11 language:hledger 28 -expenses:food in.journal /^account expenses:food$/;" kind:a line:3 language:hledger 29 -receipt in.journal /^tag receipt$/;" kind:t line:7 language:hledger