tree-sitter-hledger 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Patrick Timoney
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,154 @@
1
+ # tree-sitter-hledger
2
+
3
+ A [tree-sitter](https://tree-sitter.github.io/) grammar for [hledger](https://hledger.org/) journal files.
4
+
5
+ ## About
6
+
7
+ This is a standalone tree-sitter parser for hledger's plain text accounting format. It provides fast, incremental syntax parsing suitable for:
8
+
9
+ - Syntax highlighting in editors (Neovim, Zed, Helix, etc.)
10
+ - Code folding and structural navigation
11
+ - Building editor integrations
12
+ - Analysis and tooling
13
+
14
+ This parser complements the existing [hledger-lsp](../hledger-lsp) language server by providing syntax-level parsing that can be used independently or integrated into editors that support tree-sitter.
15
+
16
+ ## Features
17
+
18
+ **Transactions:**
19
+ - Dates with flexible formats (`2024-01-15`, `2024.1.5`, `01/15`, `1-5`)
20
+ - Status markers (`*` cleared, `!` pending)
21
+ - Transaction codes (`(#123)`)
22
+ - Descriptions
23
+ - Inline comments with tags (`; category:food, project:home`)
24
+
25
+ **Postings:**
26
+ - Account names (including spaces)
27
+ - Amounts with various formats:
28
+ - Symbol on left: `$100`, `-$50`, `$-25`
29
+ - Symbol on right: `100 USD`, `-50 EUR`
30
+ - No symbol: `100`, `-50.00`
31
+ - Cost notation: `@ $1.50` (unit cost), `@@ $150` (total cost)
32
+ - Balance assertions: `= $1000`
33
+ - Inline comments with tags
34
+
35
+ **Commodities:**
36
+ - Currency symbols: `$`, `€`, `£`, `¥`, etc.
37
+ - Alphabetic codes: `USD`, `EUR`, `BTC`
38
+ - Quoted symbols: `"AAPL US Equity"`
39
+
40
+ **Directives:**
41
+ - `account` - Account declarations
42
+ - `commodity` - Commodity declarations
43
+ - `include` - File includes
44
+ - `payee` - Payee declarations
45
+ - `tag` - Tag declarations
46
+
47
+ **Comments:**
48
+ - Line comments: `;` or `#`
49
+ - Block comments: `comment` ... `end comment`
50
+ - Inline comments with tag parsing
51
+
52
+ **Structure:**
53
+ - Indentation-based posting blocks
54
+ - Code folding support
55
+ - Syntax highlighting queries
56
+
57
+ ## Future Enhancements
58
+ - Automated transaction rules (`=`)
59
+ - Periodic transactions (`~`)
60
+ - Additional directives (alias, apply account, etc.)
61
+ - Multi-line commodity format blocks
62
+
63
+ ## Usage
64
+
65
+ ### Parse a Journal File
66
+
67
+ ```bash
68
+ npx tree-sitter parse examples/simple.journal
69
+ ```
70
+
71
+ ### Test the Grammar
72
+
73
+ ```bash
74
+ npm test
75
+ ```
76
+
77
+ ### Development
78
+
79
+ ```bash
80
+ # Generate parser from grammar
81
+ npm run build
82
+
83
+ # Test parser
84
+ npm test
85
+ ```
86
+
87
+ ## Grammar Overview
88
+
89
+ **Transactions:**
90
+ ```hledger
91
+ 2024-01-05 * Groceries ; category:food
92
+ Expenses:Food:Groceries $50.00
93
+ Assets:Checking -$50.00 ; cleared:yes
94
+ ```
95
+
96
+ **Costs and Assertions:**
97
+ ```hledger
98
+ 2024-01-10 * Currency Exchange
99
+ Assets:EUR 100 EUR @ $1.10
100
+ Assets:USD -$110.00 = $500.00
101
+ ```
102
+
103
+ **Directives:**
104
+ ```hledger
105
+ account Assets:Checking
106
+ commodity $1000.00
107
+ payee Whole Foods
108
+ include expenses.journal
109
+ ```
110
+
111
+ **Comments:**
112
+ ```hledger
113
+ ; Line comment
114
+ # Another style
115
+
116
+ comment
117
+ This is a block comment.
118
+ Multiple lines are supported.
119
+ end comment
120
+ ```
121
+
122
+ ## Syntax Highlighting
123
+
124
+ The parser includes syntax highlighting queries in `queries/highlights.scm`:
125
+
126
+ | Element | Highlight Group |
127
+ |---------|----------------|
128
+ | Dates | `@keyword.date` |
129
+ | Status markers (`*`, `!`) | `@keyword.modifier` |
130
+ | Account names | `@type` |
131
+ | Quantities | `@number` |
132
+ | Commodities | `@constant` |
133
+ | Directives | `@keyword.directive` |
134
+ | Comments | `@comment` |
135
+ | Tags | `@tag` / `@property` |
136
+ | Operators (`@`, `=`) | `@operator` |
137
+
138
+ ## Related Projects
139
+
140
+ - [hledger-lsp](../hledger-lsp) - Language Server Protocol implementation for hledger
141
+ - [hledger-vscode](../hledger-vscode) - VS Code extension
142
+ - [hledger-nvim](../hledger-nvim) - Neovim plugin
143
+
144
+ ## Development
145
+
146
+ See [CLAUDE.md](CLAUDE.md) for detailed development guidance.
147
+
148
+ ## License
149
+
150
+ MIT
151
+
152
+ ## Contributing
153
+
154
+ Contributions are welcome! Please feel free to submit issues or pull requests.
package/binding.gyp ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "targets": [
3
+ {
4
+ "target_name": "tree_sitter_hledger_binding",
5
+ "include_dirs": [
6
+ "<!(node -e \"require('nan')\")",
7
+ "src"
8
+ ],
9
+ "sources": [
10
+ "bindings/node/binding.cc",
11
+ "src/parser.c",
12
+ "src/scanner.c"
13
+ ],
14
+ "cflags_c": [
15
+ "-std=c99"
16
+ ]
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,28 @@
1
+ #include "tree_sitter/parser.h"
2
+ #include <node.h>
3
+ #include "nan.h"
4
+
5
+ using namespace v8;
6
+
7
+ extern "C" TSLanguage * tree_sitter_hledger();
8
+
9
+ namespace {
10
+
11
+ NAN_METHOD(New) {}
12
+
13
+ void Init(Local<Object> exports, Local<Object> module) {
14
+ Local<FunctionTemplate> tpl = Nan::New<FunctionTemplate>(New);
15
+ tpl->SetClassName(Nan::New("Language").ToLocalChecked());
16
+ tpl->InstanceTemplate()->SetInternalFieldCount(1);
17
+
18
+ Local<Function> constructor = Nan::GetFunction(tpl).ToLocalChecked();
19
+ Local<Object> instance = constructor->NewInstance(Nan::GetCurrentContext()).ToLocalChecked();
20
+ Nan::SetInternalFieldPointer(instance, 0, tree_sitter_hledger());
21
+
22
+ Nan::Set(instance, Nan::New("name").ToLocalChecked(), Nan::New("hledger").ToLocalChecked());
23
+ Nan::Set(module, Nan::New("exports").ToLocalChecked(), instance);
24
+ }
25
+
26
+ NODE_MODULE(tree_sitter_hledger_binding, Init)
27
+
28
+ } // namespace
@@ -0,0 +1,19 @@
1
+ try {
2
+ module.exports = require("../../build/Release/tree_sitter_hledger_binding");
3
+ } catch (error1) {
4
+ if (error1.code !== 'MODULE_NOT_FOUND') {
5
+ throw error1;
6
+ }
7
+ try {
8
+ module.exports = require("../../build/Debug/tree_sitter_hledger_binding");
9
+ } catch (error2) {
10
+ if (error2.code !== 'MODULE_NOT_FOUND') {
11
+ throw error2;
12
+ }
13
+ throw error1
14
+ }
15
+ }
16
+
17
+ try {
18
+ module.exports.nodeTypeInfo = require("../../src/node-types.json");
19
+ } catch (_) {}
package/grammar.js ADDED
@@ -0,0 +1,236 @@
1
+ module.exports = grammar({
2
+ name: 'hledger',
3
+
4
+ externals: $ => [
5
+ $.indent,
6
+ $.dedent,
7
+ $._newline,
8
+ ],
9
+
10
+ extras: _ => [
11
+ /[ \t]/, // Allow spaces and tabs as whitespace, but NOT newlines
12
+ ],
13
+
14
+ rules: {
15
+ source_file: $ => repeat($._item),
16
+
17
+ _item: $ => choice(
18
+ $._blank_line,
19
+ $._comment,
20
+ $.transaction,
21
+ $._directive,
22
+ ),
23
+
24
+ // Blank line (just newline)
25
+ _blank_line: $ => $._newline,
26
+
27
+ // Comments
28
+ _comment: $ => choice(
29
+ $.line_comment,
30
+ $.block_comment,
31
+ ),
32
+
33
+ line_comment: $ => seq(
34
+ /[;#][^\n]*/,
35
+ $._newline,
36
+ ),
37
+
38
+ block_comment: $ => seq(
39
+ 'comment',
40
+ $._newline,
41
+ repeat(
42
+ seq(/[^\n]*/, $._newline)
43
+ ),
44
+ 'end comment',
45
+ $._newline,
46
+ ),
47
+
48
+ inline_comment: $ => seq(
49
+ ';',
50
+ repeat(
51
+ choice(
52
+ $.tag,
53
+ ',',
54
+ $._word, // Non-tag words
55
+ 'comment', // Allow block_comment keyword as text
56
+ 'end', // Allow 'end' keyword as text
57
+ /:[,\s\n]/, // Colon followed by separator (not part of a tag)
58
+ )
59
+ )
60
+ ),
61
+
62
+ // Word token shared between tag_name and regular comment text
63
+ _word: _ => /[^,:\s\n]+/,
64
+
65
+ tag: $ => seq(
66
+ alias($._word, $.tag_name),
67
+ token.immediate(':'),
68
+ alias(token.immediate(/[^,\s\n]+/), $.tag_value),
69
+ ),
70
+
71
+ tag_name: _ => /[^\s:,\n]+/,
72
+ tag_value: _ => /[^,\s\n]+/,
73
+
74
+ // Transaction: header followed by indented postings/comments
75
+ transaction: $ => seq(
76
+ $.transaction_header,
77
+ $.indent,
78
+ repeat1(
79
+ choice(
80
+ $.posting,
81
+ $.line_comment,
82
+ )
83
+ ),
84
+ $.dedent,
85
+ ),
86
+
87
+ transaction_header: $ => seq(
88
+ field('date', $.date),
89
+ optional(field('status', $.status)),
90
+ optional(field('code', $.code)),
91
+ field('description', $.description),
92
+ optional($.inline_comment),
93
+ $._newline,
94
+ ),
95
+
96
+ // Posting: account name with optional amount and comment
97
+ posting: $ => seq(
98
+ optional(field('status', $.status)),
99
+ field('account', $.account),
100
+ optional(field('amount', $.amount)),
101
+ optional(field('cost', $.cost)),
102
+ optional(field('assertion', $.assertion)),
103
+ optional($.inline_comment),
104
+ $._newline,
105
+ ),
106
+
107
+ cost: $ => seq(
108
+ choice(
109
+ '@',
110
+ '@@',
111
+ ),
112
+ $.amount,
113
+ ),
114
+
115
+ assertion: $ => seq(
116
+ '=',
117
+ $.amount,
118
+ ),
119
+
120
+ // Directives
121
+ _directive: $ => seq(
122
+ choice(
123
+ $.account_directive,
124
+ $.commodity_directive,
125
+ $.include_directive,
126
+ $.payee_directive,
127
+ $.tag_directive,
128
+ ),
129
+ optional($.inline_comment),
130
+ $._newline,
131
+ ),
132
+
133
+ account_directive: $ => seq(
134
+ 'account',
135
+ field('name', $.account),
136
+ ),
137
+
138
+ commodity_directive: $ => seq(
139
+ 'commodity',
140
+ choice(
141
+ seq(
142
+ field('symbol', $.commodity),
143
+ optional(/[ \t]+/),
144
+ optional(field('quantity', $.quantity)),
145
+ ),
146
+ seq(
147
+ field('symbol', $.quantity),
148
+ optional(/[ \t]+/),
149
+ field('commodity', $.commodity),
150
+ ),
151
+ ),
152
+ ),
153
+
154
+ include_directive: $ => seq(
155
+ 'include',
156
+ field('path', $.file_path),
157
+ ),
158
+
159
+ payee_directive: $ => seq(
160
+ 'payee',
161
+ field('name', $.payee_name),
162
+ ),
163
+
164
+ tag_directive: $ => seq(
165
+ 'tag',
166
+ field('name', $.tag_name),
167
+ ),
168
+
169
+ // Basic tokens
170
+ date: _ => choice(
171
+ /\d{4}[-\/.]\d{1,2}[-\/.]\d{1,2}/,
172
+ /\d{1,2}[-\/.]\d{1,2}/,
173
+ ),
174
+
175
+ status: _ => token(prec(1, choice('*', '!'))),
176
+
177
+ sign: _ => token(choice('+', '-')),
178
+
179
+ code: _ => prec(1, seq('(', /[^)]+/, ')')),
180
+
181
+ // Description: Cannot start with status (*, !), code start parenthesis '(',
182
+ // effective date equals '=', or be empty/semicolon.
183
+ // This allows the optional fields (status, code, effective_date) to match first.
184
+ description: _ => /[^*!=(;\s][^;\n]*/,
185
+
186
+
187
+ account: _ => /([^\s;#\n]+([ \t][^;\s\n#]+)*?)/,
188
+
189
+ // Amount: quantity with optional commodity
190
+ amount: $ => choice(
191
+ // Symbol on left, negative before symbol: $100, -$100, $ 100
192
+ seq(
193
+ optional(field('sign', $.sign)),
194
+ field('commodity', $.commodity),
195
+ optional(/\s+/),
196
+ field('quantity', $.quantity),
197
+ ),
198
+ // Symbol on left, negative after symbol: $-100, $+100,
199
+ seq(
200
+ field('commodity', $.commodity),
201
+ optional(field('sign', $.sign)),
202
+ optional(/\s+/),
203
+ field('quantity', $.quantity),
204
+ ),
205
+ // Symbol on right: 100 USD, -100 EUR
206
+ seq(
207
+ optional(field('sign', $.sign)),
208
+ field('quantity', $.quantity),
209
+ optional(/\s+/),
210
+ field('commodity', $.commodity),
211
+ ),
212
+ // No symbol: 100, -100.50
213
+ seq(
214
+ optional(field('sign', $.sign)),
215
+ field('quantity', $.quantity),
216
+ ),
217
+ ),
218
+
219
+ // Commodity symbols - excludes digits, whitespace, and special characters
220
+ // Also excludes +- so they can be parsed as signs
221
+ commodity: _ => choice(
222
+ /[^!"%(){}:;'~<>,.\/\\0-9\s\-+]+/,
223
+ /"[^\n]+"/,
224
+ ),
225
+
226
+
227
+ quantity: _ => /\d[\d., \t]*/, // Number with optional separators (no newlines)
228
+
229
+ // File path for include directive
230
+ file_path: _ => /[^\s;#\n]+/,
231
+
232
+ // Payee name
233
+ payee_name: _ => /[^;\n]+/,
234
+
235
+ }
236
+ });
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "tree-sitter-hledger",
3
+ "version": "0.1.0",
4
+ "description": "Tree-sitter grammar for hledger journal files",
5
+ "main": "bindings/node",
6
+ "types": "bindings/node",
7
+ "keywords": [
8
+ "tree-sitter",
9
+ "parser",
10
+ "hledger",
11
+ "accounting",
12
+ "plaintext-accounting",
13
+ "ledger",
14
+ "finance"
15
+ ],
16
+ "author": "Patrick Timoney",
17
+ "license": "MIT",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/ptimoney/tree-sitter-hledger"
21
+ },
22
+ "bugs": {
23
+ "url": "https://github.com/ptimoney/tree-sitter-hledger/issues"
24
+ },
25
+ "homepage": "https://github.com/ptimoney/tree-sitter-hledger#readme",
26
+ "files": [
27
+ "grammar.js",
28
+ "binding.gyp",
29
+ "prebuilds/**",
30
+ "bindings/node/*",
31
+ "queries/**",
32
+ "src/**",
33
+ "*.wasm"
34
+ ],
35
+ "scripts": {
36
+ "build": "tree-sitter generate && node-gyp rebuild",
37
+ "test": "tree-sitter test",
38
+ "parse": "tree-sitter parse",
39
+ "watch": "tree-sitter generate --watch"
40
+ },
41
+ "dependencies": {
42
+ "nan": "^2.17.0"
43
+ },
44
+ "devDependencies": {
45
+ "tree-sitter-cli": "^0.20.8"
46
+ },
47
+ "tree-sitter": [
48
+ {
49
+ "scope": "source.hledger",
50
+ "file-types": [
51
+ "journal",
52
+ "hledger"
53
+ ],
54
+ "highlights": "queries/highlights.scm",
55
+ "folds": "queries/folds.scm"
56
+ }
57
+ ]
58
+ }
@@ -0,0 +1,9 @@
1
+ ; Fold transactions at the posting level
2
+ (transaction) @fold
3
+
4
+ ; Fold block comments (multi-line)
5
+ (block_comment) @fold
6
+
7
+ ; Fold consecutive line comments
8
+ ((line_comment)+ @fold)
9
+
@@ -0,0 +1,48 @@
1
+ ; Transaction dates
2
+ (date) @keyword.date
3
+
4
+ ; Status markers
5
+ (status) @keyword.modifier
6
+
7
+ ; Transaction codes
8
+ (code) @string.special
9
+
10
+ ; Descriptions
11
+ (description) @string
12
+
13
+ ; Account names
14
+ (account) @type
15
+
16
+ ; Amounts - quantities and commodities
17
+ (quantity) @number
18
+ (commodity) @constant
19
+
20
+ ; Amount signs
21
+ (sign) @operator
22
+
23
+ ; Directives
24
+ "account" @keyword.directive
25
+ "commodity" @keyword.directive
26
+ "include" @keyword.import
27
+ "payee" @keyword.directive
28
+ "tag" @keyword.directive
29
+
30
+ ; Directive values
31
+ (payee_name) @string
32
+ (file_path) @string.special
33
+
34
+ ; Comments
35
+ (line_comment) @comment
36
+ (block_comment) @comment
37
+ (inline_comment) @comment
38
+
39
+ ; Operators and punctuation
40
+ "=" @operator
41
+ "@" @operator
42
+ "@@" @operator
43
+ "(" @punctuation.bracket
44
+ ")" @punctuation.bracket
45
+
46
+ ; Tags
47
+ (tag_name) @tag
48
+ (tag_value) @property