hledger-lsp 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 +21 -0
- package/README.md +632 -0
- package/out/extension.d.ts +1 -0
- package/out/extension.d.ts.map +1 -0
- package/out/extension.js +2 -0
- package/out/extension.js.map +1 -0
- package/out/features/codeActions.d.ts +73 -0
- package/out/features/codeActions.d.ts.map +1 -0
- package/out/features/codeActions.js +417 -0
- package/out/features/codeActions.js.map +1 -0
- package/out/features/completion.d.ts +94 -0
- package/out/features/completion.d.ts.map +1 -0
- package/out/features/completion.js +323 -0
- package/out/features/completion.js.map +1 -0
- package/out/features/definition.d.ts +12 -0
- package/out/features/definition.d.ts.map +1 -0
- package/out/features/definition.js +61 -0
- package/out/features/definition.js.map +1 -0
- package/out/features/documentLinks.d.ts +17 -0
- package/out/features/documentLinks.d.ts.map +1 -0
- package/out/features/documentLinks.js +68 -0
- package/out/features/documentLinks.js.map +1 -0
- package/out/features/findReferences.d.ts +33 -0
- package/out/features/findReferences.d.ts.map +1 -0
- package/out/features/findReferences.js +79 -0
- package/out/features/findReferences.js.map +1 -0
- package/out/features/foldingRanges.d.ts +27 -0
- package/out/features/foldingRanges.d.ts.map +1 -0
- package/out/features/foldingRanges.js +111 -0
- package/out/features/foldingRanges.js.map +1 -0
- package/out/features/formatter.d.ts +70 -0
- package/out/features/formatter.d.ts.map +1 -0
- package/out/features/formatter.js +373 -0
- package/out/features/formatter.js.map +1 -0
- package/out/features/hover.d.ts +66 -0
- package/out/features/hover.d.ts.map +1 -0
- package/out/features/hover.js +387 -0
- package/out/features/hover.js.map +1 -0
- package/out/features/inlayHints.d.ts +43 -0
- package/out/features/inlayHints.d.ts.map +1 -0
- package/out/features/inlayHints.js +221 -0
- package/out/features/inlayHints.js.map +1 -0
- package/out/features/selectionRange.d.ts +47 -0
- package/out/features/selectionRange.d.ts.map +1 -0
- package/out/features/selectionRange.js +273 -0
- package/out/features/selectionRange.js.map +1 -0
- package/out/features/semanticTokens.d.ts +83 -0
- package/out/features/semanticTokens.d.ts.map +1 -0
- package/out/features/semanticTokens.js +370 -0
- package/out/features/semanticTokens.js.map +1 -0
- package/out/features/symbols.d.ts +47 -0
- package/out/features/symbols.d.ts.map +1 -0
- package/out/features/symbols.js +249 -0
- package/out/features/symbols.js.map +1 -0
- package/out/features/transactionAnalyzer.d.ts +63 -0
- package/out/features/transactionAnalyzer.d.ts.map +1 -0
- package/out/features/transactionAnalyzer.js +127 -0
- package/out/features/transactionAnalyzer.js.map +1 -0
- package/out/features/validator.d.ts +142 -0
- package/out/features/validator.d.ts.map +1 -0
- package/out/features/validator.js +633 -0
- package/out/features/validator.js.map +1 -0
- package/out/parser/ast.d.ts +37 -0
- package/out/parser/ast.d.ts.map +1 -0
- package/out/parser/ast.js +606 -0
- package/out/parser/ast.js.map +1 -0
- package/out/parser/includes.d.ts +25 -0
- package/out/parser/includes.d.ts.map +1 -0
- package/out/parser/includes.js +106 -0
- package/out/parser/includes.js.map +1 -0
- package/out/parser/index.d.ts +54 -0
- package/out/parser/index.d.ts.map +1 -0
- package/out/parser/index.js +146 -0
- package/out/parser/index.js.map +1 -0
- package/out/server/deps.d.ts +19 -0
- package/out/server/deps.d.ts.map +1 -0
- package/out/server/deps.js +77 -0
- package/out/server/deps.js.map +1 -0
- package/out/server/settings.d.ts +60 -0
- package/out/server/settings.d.ts.map +1 -0
- package/out/server/settings.js +110 -0
- package/out/server/settings.js.map +1 -0
- package/out/server.d.ts +3 -0
- package/out/server.d.ts.map +1 -0
- package/out/server.js +420 -0
- package/out/server.js.map +1 -0
- package/out/types.d.ts +84 -0
- package/out/types.d.ts.map +1 -0
- package/out/types.js +6 -0
- package/out/types.js.map +1 -0
- package/out/utils/index.d.ts +38 -0
- package/out/utils/index.d.ts.map +1 -0
- package/out/utils/index.js +89 -0
- package/out/utils/index.js.map +1 -0
- package/out/utils/uri.d.ts +32 -0
- package/out/utils/uri.d.ts.map +1 -0
- package/out/utils/uri.js +215 -0
- package/out/utils/uri.js.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Transaction, Posting, Amount, Account, Payee, Commodity, Tag, Directive } from '../types';
|
|
2
|
+
import { TextDocument } from 'vscode-languageserver-textdocument';
|
|
3
|
+
/**
|
|
4
|
+
* Parse a transaction starting at startLine within lines array.
|
|
5
|
+
* This is a pure helper extracted from HledgerParser.
|
|
6
|
+
*/
|
|
7
|
+
export declare function parseTransaction(lines: string[], startLine: number): Transaction | null;
|
|
8
|
+
/**
|
|
9
|
+
* Infer costs for two-commodity transactions without explicit cost notation.
|
|
10
|
+
*
|
|
11
|
+
* According to hledger docs, when a transaction has:
|
|
12
|
+
* - All postings with amounts specified
|
|
13
|
+
* - Exactly two commodities
|
|
14
|
+
* - No explicit cost notation
|
|
15
|
+
*
|
|
16
|
+
* Then hledger infers a total cost on the first posting to balance the transaction.
|
|
17
|
+
*/
|
|
18
|
+
export declare function inferCosts(transaction: Transaction): void;
|
|
19
|
+
export declare function parseTransactionHeader(line: string): {
|
|
20
|
+
date: string;
|
|
21
|
+
effectiveDate?: string;
|
|
22
|
+
status?: 'cleared' | 'pending' | 'unmarked';
|
|
23
|
+
code?: string;
|
|
24
|
+
description: string;
|
|
25
|
+
payee: string;
|
|
26
|
+
note: string;
|
|
27
|
+
comment?: string;
|
|
28
|
+
tags?: Record<string, string>;
|
|
29
|
+
} | null;
|
|
30
|
+
export declare function parsePosting(line: string): Posting | null;
|
|
31
|
+
export declare function parseAmount(amountStr: string): Amount | null;
|
|
32
|
+
export declare function extractAccounts(document: TextDocument, sourceUri?: string): Account[];
|
|
33
|
+
export declare function extractPayees(document: TextDocument, sourceUri?: string): Payee[];
|
|
34
|
+
export declare function extractCommodities(document: TextDocument, sourceUri?: string): Commodity[];
|
|
35
|
+
export declare function extractTagNames(document: TextDocument, sourceUri?: string): Tag[];
|
|
36
|
+
export declare function parseDirective(line: string): Directive | null;
|
|
37
|
+
//# sourceMappingURL=ast.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ast.d.ts","sourceRoot":"","sources":["../../src/parser/ast.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAQ,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACzG,OAAO,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AAGlE;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CA8DvF;AAED;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,CAkDzD;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,UAAU,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,IAAI,CAiDnQ;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAsEzD;AAED,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAqB5D;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,YAAY,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,EAAE,CAwBrF;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,YAAY,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,KAAK,EAAE,CAyBjF;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,YAAY,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,EAAE,CA0G1F;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,YAAY,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,GAAG,EAAE,CA4CjF;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAyB7D"}
|
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseTransaction = parseTransaction;
|
|
4
|
+
exports.inferCosts = inferCosts;
|
|
5
|
+
exports.parseTransactionHeader = parseTransactionHeader;
|
|
6
|
+
exports.parsePosting = parsePosting;
|
|
7
|
+
exports.parseAmount = parseAmount;
|
|
8
|
+
exports.extractAccounts = extractAccounts;
|
|
9
|
+
exports.extractPayees = extractPayees;
|
|
10
|
+
exports.extractCommodities = extractCommodities;
|
|
11
|
+
exports.extractTagNames = extractTagNames;
|
|
12
|
+
exports.parseDirective = parseDirective;
|
|
13
|
+
const index_1 = require("../utils/index");
|
|
14
|
+
/**
|
|
15
|
+
* Parse a transaction starting at startLine within lines array.
|
|
16
|
+
* This is a pure helper extracted from HledgerParser.
|
|
17
|
+
*/
|
|
18
|
+
function parseTransaction(lines, startLine) {
|
|
19
|
+
if (!lines || lines.length === 0 || startLine >= lines.length) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const headerLine = lines[startLine];
|
|
23
|
+
if (!(0, index_1.isTransactionHeader)(headerLine))
|
|
24
|
+
return null;
|
|
25
|
+
// reuse the header parser below
|
|
26
|
+
const header = parseTransactionHeader(headerLine);
|
|
27
|
+
if (!header)
|
|
28
|
+
return null;
|
|
29
|
+
const postings = [];
|
|
30
|
+
let currentLine = startLine + 1;
|
|
31
|
+
let transactionComment;
|
|
32
|
+
const transactionTags = {};
|
|
33
|
+
while (currentLine < lines.length) {
|
|
34
|
+
const line = lines[currentLine];
|
|
35
|
+
if ((0, index_1.isTransactionHeader)(line) || (!line.trim().startsWith(';') && !line.trim().startsWith('#') && line.trim() && !(0, index_1.isPosting)(line))) {
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
if ((0, index_1.isComment)(line)) {
|
|
39
|
+
const commentText = line.trim().substring(1).trim();
|
|
40
|
+
if (!transactionComment)
|
|
41
|
+
transactionComment = commentText;
|
|
42
|
+
const tags = (0, index_1.extractTags)(commentText);
|
|
43
|
+
Object.assign(transactionTags, tags);
|
|
44
|
+
currentLine++;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if ((0, index_1.isPosting)(line)) {
|
|
48
|
+
const posting = parsePosting(line);
|
|
49
|
+
if (posting)
|
|
50
|
+
postings.push(posting);
|
|
51
|
+
}
|
|
52
|
+
currentLine++;
|
|
53
|
+
if (!line.trim())
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
const transaction = {
|
|
57
|
+
date: header.date,
|
|
58
|
+
description: header.description,
|
|
59
|
+
payee: header.payee,
|
|
60
|
+
note: header.note,
|
|
61
|
+
postings,
|
|
62
|
+
line: startLine
|
|
63
|
+
};
|
|
64
|
+
if (header.effectiveDate)
|
|
65
|
+
transaction.effectiveDate = header.effectiveDate;
|
|
66
|
+
if (header.status)
|
|
67
|
+
transaction.status = header.status;
|
|
68
|
+
if (header.code)
|
|
69
|
+
transaction.code = header.code;
|
|
70
|
+
if (header.comment || transactionComment)
|
|
71
|
+
transaction.comment = header.comment || transactionComment;
|
|
72
|
+
if (Object.keys(transactionTags).length > 0 || header.tags)
|
|
73
|
+
transaction.tags = { ...transactionTags, ...header.tags };
|
|
74
|
+
// Infer costs for two-commodity transactions
|
|
75
|
+
inferCosts(transaction);
|
|
76
|
+
return transaction;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Infer costs for two-commodity transactions without explicit cost notation.
|
|
80
|
+
*
|
|
81
|
+
* According to hledger docs, when a transaction has:
|
|
82
|
+
* - All postings with amounts specified
|
|
83
|
+
* - Exactly two commodities
|
|
84
|
+
* - No explicit cost notation
|
|
85
|
+
*
|
|
86
|
+
* Then hledger infers a total cost on the first posting to balance the transaction.
|
|
87
|
+
*/
|
|
88
|
+
function inferCosts(transaction) {
|
|
89
|
+
// Requirements for cost inference
|
|
90
|
+
if (transaction.postings.length === 0)
|
|
91
|
+
return;
|
|
92
|
+
// 1. All postings must have amounts
|
|
93
|
+
const allHaveAmounts = transaction.postings.every(p => p.amount);
|
|
94
|
+
if (!allHaveAmounts)
|
|
95
|
+
return;
|
|
96
|
+
// 2. No posting should already have explicit cost notation
|
|
97
|
+
const hasExplicitCost = transaction.postings.some(p => p.cost);
|
|
98
|
+
if (hasExplicitCost)
|
|
99
|
+
return;
|
|
100
|
+
// 3. Count distinct commodities
|
|
101
|
+
const commodities = new Set();
|
|
102
|
+
for (const posting of transaction.postings) {
|
|
103
|
+
if (posting.amount) {
|
|
104
|
+
commodities.add(posting.amount.commodity || '');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Must have exactly 2 commodities
|
|
108
|
+
if (commodities.size !== 2)
|
|
109
|
+
return;
|
|
110
|
+
// Get first posting's commodity
|
|
111
|
+
const firstCommodity = transaction.postings[0].amount.commodity || '';
|
|
112
|
+
// Sum all amounts in the other commodity
|
|
113
|
+
let otherCommodity = '';
|
|
114
|
+
let otherSum = 0;
|
|
115
|
+
for (const posting of transaction.postings) {
|
|
116
|
+
const commodity = posting.amount.commodity || '';
|
|
117
|
+
if (commodity !== firstCommodity) {
|
|
118
|
+
otherCommodity = commodity;
|
|
119
|
+
otherSum += posting.amount.quantity;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Infer total cost: the negation of the sum of other commodity
|
|
123
|
+
// This makes the transaction balance when cost is used for balance calculation
|
|
124
|
+
const costAmount = {
|
|
125
|
+
quantity: -otherSum,
|
|
126
|
+
commodity: otherCommodity
|
|
127
|
+
};
|
|
128
|
+
// Add inferred total cost to first posting
|
|
129
|
+
transaction.postings[0].cost = {
|
|
130
|
+
type: 'total',
|
|
131
|
+
amount: costAmount
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function parseTransactionHeader(line) {
|
|
135
|
+
const trimmed = line.trim();
|
|
136
|
+
const dateMatch = trimmed.match(/^(\d{4}[-/]\d{2}[-/]\d{2})/);
|
|
137
|
+
if (!dateMatch)
|
|
138
|
+
return null;
|
|
139
|
+
const date = dateMatch[1];
|
|
140
|
+
let rest = trimmed.substring(date.length).trim();
|
|
141
|
+
let effectiveDate;
|
|
142
|
+
const effectiveDateMatch = rest.match(/^=(\d{4}[-/]\d{2}[-/]\d{2})/);
|
|
143
|
+
if (effectiveDateMatch) {
|
|
144
|
+
effectiveDate = effectiveDateMatch[1];
|
|
145
|
+
rest = rest.substring(effectiveDateMatch[0].length).trim();
|
|
146
|
+
}
|
|
147
|
+
let status;
|
|
148
|
+
if (rest.startsWith('*')) {
|
|
149
|
+
status = 'cleared';
|
|
150
|
+
rest = rest.substring(1).trim();
|
|
151
|
+
}
|
|
152
|
+
else if (rest.startsWith('!')) {
|
|
153
|
+
status = 'pending';
|
|
154
|
+
rest = rest.substring(1).trim();
|
|
155
|
+
}
|
|
156
|
+
let code;
|
|
157
|
+
const codeMatch = rest.match(/^\(([^)]+)\)/);
|
|
158
|
+
if (codeMatch) {
|
|
159
|
+
code = codeMatch[1];
|
|
160
|
+
rest = rest.substring(codeMatch[0].length).trim();
|
|
161
|
+
}
|
|
162
|
+
let comment;
|
|
163
|
+
let tags;
|
|
164
|
+
const commentMatch = rest.match(/^([^;]*);(.*)$/);
|
|
165
|
+
if (commentMatch) {
|
|
166
|
+
rest = commentMatch[1].trim();
|
|
167
|
+
comment = commentMatch[2].trim();
|
|
168
|
+
const extracted = (0, index_1.extractTags)(comment);
|
|
169
|
+
if (Object.keys(extracted).length > 0)
|
|
170
|
+
tags = extracted;
|
|
171
|
+
}
|
|
172
|
+
const description = rest.trim();
|
|
173
|
+
// Parse payee and note according to hledger spec:
|
|
174
|
+
// If description contains |, split into payee (left) and note (right)
|
|
175
|
+
// If no |, payee and note both equal description
|
|
176
|
+
let payee;
|
|
177
|
+
let note;
|
|
178
|
+
const pipeIndex = description.indexOf('|');
|
|
179
|
+
if (pipeIndex !== -1) {
|
|
180
|
+
payee = description.substring(0, pipeIndex).trim();
|
|
181
|
+
note = description.substring(pipeIndex + 1).trim();
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
payee = description;
|
|
185
|
+
note = description;
|
|
186
|
+
}
|
|
187
|
+
return { date, effectiveDate, status, code, description, payee, note, comment, tags };
|
|
188
|
+
}
|
|
189
|
+
function parsePosting(line) {
|
|
190
|
+
if (!(0, index_1.isPosting)(line))
|
|
191
|
+
return null;
|
|
192
|
+
const account = (0, index_1.extractAccountFromPosting)(line);
|
|
193
|
+
if (!account)
|
|
194
|
+
return null;
|
|
195
|
+
const posting = { account };
|
|
196
|
+
const commentMatch = line.match(/^([^;]*);(.*)$/);
|
|
197
|
+
let mainPart = line;
|
|
198
|
+
let commentPart = '';
|
|
199
|
+
if (commentMatch) {
|
|
200
|
+
mainPart = commentMatch[1];
|
|
201
|
+
commentPart = commentMatch[2];
|
|
202
|
+
posting.comment = commentPart.trim();
|
|
203
|
+
const tags = (0, index_1.extractTags)(commentPart);
|
|
204
|
+
if (Object.keys(tags).length > 0)
|
|
205
|
+
posting.tags = tags;
|
|
206
|
+
}
|
|
207
|
+
const trimmed = mainPart.trim();
|
|
208
|
+
const afterAccount = trimmed.substring(account.length).trim();
|
|
209
|
+
if (!afterAccount)
|
|
210
|
+
return posting;
|
|
211
|
+
// Parse order: amount [@ cost | @@ cost] [= assertion]
|
|
212
|
+
// First, split on assertion (=)
|
|
213
|
+
const assertionMatch = afterAccount.match(/=\s*(.+)$/);
|
|
214
|
+
const beforeAssertion = assertionMatch
|
|
215
|
+
? afterAccount.substring(0, assertionMatch.index ?? 0).trim()
|
|
216
|
+
: afterAccount;
|
|
217
|
+
if (assertionMatch) {
|
|
218
|
+
const assertionPart = assertionMatch[1].trim();
|
|
219
|
+
const assertionAmount = parseAmount(assertionPart);
|
|
220
|
+
if (assertionAmount)
|
|
221
|
+
posting.assertion = assertionAmount;
|
|
222
|
+
}
|
|
223
|
+
// Now parse amount and cost from beforeAssertion
|
|
224
|
+
// Check for @@ first (total price), then @ (unit price)
|
|
225
|
+
const totalCostMatch = beforeAssertion.match(/@@\s*(.+)$/);
|
|
226
|
+
const unitCostMatch = !totalCostMatch ? beforeAssertion.match(/@\s*(.+)$/) : null;
|
|
227
|
+
if (totalCostMatch) {
|
|
228
|
+
// Parse total cost: amount @@ totalCost
|
|
229
|
+
const amountPart = beforeAssertion.substring(0, totalCostMatch.index ?? 0).trim();
|
|
230
|
+
const costPart = totalCostMatch[1].trim();
|
|
231
|
+
const amount = parseAmount(amountPart);
|
|
232
|
+
if (amount)
|
|
233
|
+
posting.amount = amount;
|
|
234
|
+
const costAmount = parseAmount(costPart);
|
|
235
|
+
if (costAmount) {
|
|
236
|
+
posting.cost = { type: 'total', amount: costAmount };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
else if (unitCostMatch) {
|
|
240
|
+
// Parse unit cost: amount @ unitCost
|
|
241
|
+
const amountPart = beforeAssertion.substring(0, unitCostMatch.index ?? 0).trim();
|
|
242
|
+
const costPart = unitCostMatch[1].trim();
|
|
243
|
+
const amount = parseAmount(amountPart);
|
|
244
|
+
if (amount)
|
|
245
|
+
posting.amount = amount;
|
|
246
|
+
const costAmount = parseAmount(costPart);
|
|
247
|
+
if (costAmount) {
|
|
248
|
+
posting.cost = { type: 'unit', amount: costAmount };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
// No cost notation, just parse amount
|
|
253
|
+
const amount = parseAmount(beforeAssertion);
|
|
254
|
+
if (amount)
|
|
255
|
+
posting.amount = amount;
|
|
256
|
+
}
|
|
257
|
+
return posting;
|
|
258
|
+
}
|
|
259
|
+
function parseAmount(amountStr) {
|
|
260
|
+
const trimmed = amountStr.trim();
|
|
261
|
+
if (!trimmed)
|
|
262
|
+
return null;
|
|
263
|
+
const patterns = [
|
|
264
|
+
{ pattern: /^-([^\d\s-]+)\s*(\d+(?:[,]\d{3})*(?:\.\d+)?)$/, handler: (m) => ({ quantity: -parseFloat(m[2].replace(/,/g, '')), commodity: m[1] }) },
|
|
265
|
+
{ pattern: /^([^\d\s-]+)\s*([-]?\d+(?:[,]\d{3})*(?:\.\d+)?)$/, handler: (m) => ({ quantity: parseFloat(m[2].replace(/,/g, '')), commodity: m[1] }) },
|
|
266
|
+
{ pattern: /^([-]?\d+(?:[,]\d{3})*(?:\.\d+)?)\s*([^\d\s]+)$/, handler: (m) => ({ quantity: parseFloat(m[1].replace(/,/g, '')), commodity: m[2] }) },
|
|
267
|
+
{ pattern: /^([-]?\d+(?:[,]\d{3})*(?:\.\d+)?)$/, handler: (m) => ({ quantity: parseFloat(m[1].replace(/,/g, '')), commodity: '' }) }
|
|
268
|
+
];
|
|
269
|
+
for (const { pattern, handler } of patterns) {
|
|
270
|
+
const match = trimmed.match(pattern);
|
|
271
|
+
if (match) {
|
|
272
|
+
const res = handler(match);
|
|
273
|
+
if (isNaN(res.quantity))
|
|
274
|
+
return null;
|
|
275
|
+
return { quantity: res.quantity, commodity: res.commodity };
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
function extractAccounts(document, sourceUri) {
|
|
281
|
+
const text = document.getText();
|
|
282
|
+
const lines = text.split('\n');
|
|
283
|
+
const accountMap = new Map();
|
|
284
|
+
for (let i = 0; i < lines.length; i++) {
|
|
285
|
+
const line = lines[i];
|
|
286
|
+
if (line.trim().startsWith('account ')) {
|
|
287
|
+
const accountName = line.trim().substring(8).split(';')[0].trim();
|
|
288
|
+
if (accountName) {
|
|
289
|
+
const acc = { name: accountName, declared: true };
|
|
290
|
+
if (sourceUri !== undefined) {
|
|
291
|
+
acc.sourceUri = sourceUri;
|
|
292
|
+
acc.line = i;
|
|
293
|
+
}
|
|
294
|
+
accountMap.set(accountName, acc);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if ((0, index_1.isPosting)(line)) {
|
|
298
|
+
const account = (0, index_1.extractAccountFromPosting)(line);
|
|
299
|
+
if (account && !accountMap.has(account)) {
|
|
300
|
+
const acc = { name: account, declared: false };
|
|
301
|
+
if (sourceUri !== undefined) {
|
|
302
|
+
acc.sourceUri = sourceUri;
|
|
303
|
+
acc.line = i;
|
|
304
|
+
}
|
|
305
|
+
accountMap.set(account, acc);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return Array.from(accountMap.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
310
|
+
}
|
|
311
|
+
function extractPayees(document, sourceUri) {
|
|
312
|
+
const text = document.getText();
|
|
313
|
+
const lines = text.split('\n');
|
|
314
|
+
const payeeMap = new Map();
|
|
315
|
+
for (let i = 0; i < lines.length; i++) {
|
|
316
|
+
const line = lines[i];
|
|
317
|
+
if (line.trim().startsWith('payee ')) {
|
|
318
|
+
const payeeName = line.trim().substring(6).split(';')[0].trim();
|
|
319
|
+
if (payeeName) {
|
|
320
|
+
const p = { name: payeeName, declared: true };
|
|
321
|
+
if (sourceUri !== undefined) {
|
|
322
|
+
p.sourceUri = sourceUri;
|
|
323
|
+
p.line = i;
|
|
324
|
+
}
|
|
325
|
+
payeeMap.set(payeeName, p);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if ((0, index_1.isTransactionHeader)(line)) {
|
|
329
|
+
const header = parseTransactionHeader(line);
|
|
330
|
+
// Use payee field instead of description (handles | splitting)
|
|
331
|
+
if (header && header.payee && !payeeMap.has(header.payee)) {
|
|
332
|
+
const p = { name: header.payee, declared: false };
|
|
333
|
+
if (sourceUri !== undefined) {
|
|
334
|
+
p.sourceUri = sourceUri;
|
|
335
|
+
p.line = i;
|
|
336
|
+
}
|
|
337
|
+
payeeMap.set(header.payee, p);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return Array.from(payeeMap.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
342
|
+
}
|
|
343
|
+
function extractCommodities(document, sourceUri) {
|
|
344
|
+
const text = document.getText();
|
|
345
|
+
const lines = text.split('\n');
|
|
346
|
+
const commodityMap = new Map();
|
|
347
|
+
const stripQuotes = (s) => { const t = s.trim(); if (t.length >= 2 && t.startsWith('"') && t.endsWith('"'))
|
|
348
|
+
return t.substring(1, t.length - 1); return t; };
|
|
349
|
+
const parseCommoditySample = (sample) => {
|
|
350
|
+
if (!sample)
|
|
351
|
+
return null;
|
|
352
|
+
const s = sample.trim();
|
|
353
|
+
const firstDigit = s.search(/\d/);
|
|
354
|
+
if (firstDigit === -1)
|
|
355
|
+
return null;
|
|
356
|
+
const allowed = /[0-9.,\u00A0\s]/;
|
|
357
|
+
let start = firstDigit;
|
|
358
|
+
let end = start;
|
|
359
|
+
while (end < s.length && allowed.test(s[end]))
|
|
360
|
+
end++;
|
|
361
|
+
const leftRaw = s.substring(0, start).trim();
|
|
362
|
+
const numberRaw = s.substring(start, end).trim();
|
|
363
|
+
const rightRaw = s.substring(end).trim();
|
|
364
|
+
if (!numberRaw)
|
|
365
|
+
return null;
|
|
366
|
+
const lastDot = numberRaw.lastIndexOf('.');
|
|
367
|
+
const lastComma = numberRaw.lastIndexOf(',');
|
|
368
|
+
let decimalMark;
|
|
369
|
+
let decimalIndex = -1;
|
|
370
|
+
if (lastDot === -1 && lastComma === -1)
|
|
371
|
+
decimalMark = undefined;
|
|
372
|
+
else if (lastDot > lastComma) {
|
|
373
|
+
decimalMark = '.';
|
|
374
|
+
decimalIndex = lastDot;
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
decimalMark = ',';
|
|
378
|
+
decimalIndex = lastComma;
|
|
379
|
+
}
|
|
380
|
+
const integerPart = decimalIndex >= 0 ? numberRaw.substring(0, decimalIndex) : numberRaw;
|
|
381
|
+
const fractionalPart = decimalIndex >= 0 ? numberRaw.substring(decimalIndex + 1) : '';
|
|
382
|
+
const precision = decimalIndex >= 0 ? (fractionalPart.length > 0 ? fractionalPart.length : 0) : null;
|
|
383
|
+
const sepCounts = {};
|
|
384
|
+
for (let i = 0; i < integerPart.length; i++) {
|
|
385
|
+
const ch = integerPart[i];
|
|
386
|
+
if (ch < '0' || ch > '9')
|
|
387
|
+
sepCounts[ch] = (sepCounts[ch] || 0) + 1;
|
|
388
|
+
}
|
|
389
|
+
let thousandsSeparator = null;
|
|
390
|
+
const separators = Object.keys(sepCounts);
|
|
391
|
+
if (separators.length === 1)
|
|
392
|
+
thousandsSeparator = separators[0];
|
|
393
|
+
else if (separators.length > 1) {
|
|
394
|
+
let max = 0;
|
|
395
|
+
let pick = null;
|
|
396
|
+
for (const k of separators) {
|
|
397
|
+
if (sepCounts[k] > max) {
|
|
398
|
+
max = sepCounts[k];
|
|
399
|
+
pick = k;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
thousandsSeparator = pick;
|
|
403
|
+
}
|
|
404
|
+
let rawSymbol = leftRaw || rightRaw || '';
|
|
405
|
+
rawSymbol = stripQuotes(rawSymbol);
|
|
406
|
+
const symbolOnLeft = Boolean(leftRaw);
|
|
407
|
+
let spaceBetween = false;
|
|
408
|
+
if (symbolOnLeft) {
|
|
409
|
+
const between = s.substring(0, firstDigit);
|
|
410
|
+
spaceBetween = /\s/.test(between.replace(stripQuotes(leftRaw), '')) || /\s/.test(leftRaw.slice(-1));
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
const after = s.substring(end);
|
|
414
|
+
spaceBetween = /\s/.test(after.replace(stripQuotes(rightRaw), '')) || /\s/.test(s[end - 1]);
|
|
415
|
+
}
|
|
416
|
+
const format = { symbol: rawSymbol, symbolOnLeft, spaceBetween, decimalMark: decimalMark, thousandsSeparator: thousandsSeparator || null, precision };
|
|
417
|
+
let name = rawSymbol;
|
|
418
|
+
if (rightRaw) {
|
|
419
|
+
const candidate = stripQuotes(rightRaw);
|
|
420
|
+
if (/^[A-Za-z][A-Za-z0-9 _-]*$/.test(candidate) || (rightRaw.trim().startsWith('"') && rightRaw.trim().endsWith('"')))
|
|
421
|
+
name = candidate;
|
|
422
|
+
}
|
|
423
|
+
if (rawSymbol === '""')
|
|
424
|
+
name = '';
|
|
425
|
+
return { name, format };
|
|
426
|
+
};
|
|
427
|
+
for (let i = 0; i < lines.length; i++) {
|
|
428
|
+
const line = lines[i];
|
|
429
|
+
if (line.trim().startsWith('commodity ')) {
|
|
430
|
+
const directive = line.trim().substring(10).split(';')[0].trim();
|
|
431
|
+
if (!directive)
|
|
432
|
+
continue;
|
|
433
|
+
let parsed = null;
|
|
434
|
+
if (/\d/.test(directive))
|
|
435
|
+
parsed = parseCommoditySample(directive);
|
|
436
|
+
let commodityName;
|
|
437
|
+
let format;
|
|
438
|
+
if (parsed) {
|
|
439
|
+
commodityName = parsed.name;
|
|
440
|
+
format = parsed.format;
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
commodityName = stripQuotes(directive);
|
|
444
|
+
format = undefined;
|
|
445
|
+
}
|
|
446
|
+
let look = i + 1;
|
|
447
|
+
while (look < lines.length) {
|
|
448
|
+
const next = lines[look];
|
|
449
|
+
if (!next.trim()) {
|
|
450
|
+
look++;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if (!/^\s+/.test(next))
|
|
454
|
+
break;
|
|
455
|
+
const trimmedNext = next.trim();
|
|
456
|
+
if (trimmedNext.startsWith('format ')) {
|
|
457
|
+
const rest = trimmedNext.substring(7).trim();
|
|
458
|
+
const m = rest.match(/^(".*?"|\S+)\s+(.*)$/);
|
|
459
|
+
if (m) {
|
|
460
|
+
const formatSymbolRaw = m[1];
|
|
461
|
+
const samplePart = m[2];
|
|
462
|
+
const parsedFormat = parseCommoditySample(samplePart) || parseCommoditySample(`${samplePart} ${formatSymbolRaw}`);
|
|
463
|
+
if (parsedFormat && parsedFormat.format) {
|
|
464
|
+
format = parsedFormat.format;
|
|
465
|
+
const fs = stripQuotes(formatSymbolRaw);
|
|
466
|
+
if (fs && fs !== '' && (!commodityName || commodityName === '' || commodityName === fs))
|
|
467
|
+
commodityName = fs;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
look++;
|
|
472
|
+
}
|
|
473
|
+
const key = commodityName;
|
|
474
|
+
if (commodityMap.has(key)) {
|
|
475
|
+
const existing = commodityMap.get(key);
|
|
476
|
+
const merged = { ...existing, declared: existing.declared || true, format: existing.format || format };
|
|
477
|
+
if (sourceUri !== undefined) {
|
|
478
|
+
merged.sourceUri = existing.sourceUri || sourceUri;
|
|
479
|
+
merged.line = existing.line ?? i;
|
|
480
|
+
}
|
|
481
|
+
commodityMap.set(key, merged);
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
const c = { name: commodityName, declared: true, format };
|
|
485
|
+
if (sourceUri !== undefined) {
|
|
486
|
+
c.sourceUri = sourceUri;
|
|
487
|
+
c.line = i;
|
|
488
|
+
}
|
|
489
|
+
commodityMap.set(key, c);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if ((0, index_1.isPosting)(line)) {
|
|
493
|
+
const posting = parsePosting(line);
|
|
494
|
+
if (posting?.amount?.commodity && posting.amount.commodity !== '') {
|
|
495
|
+
const key = posting.amount.commodity;
|
|
496
|
+
if (!commodityMap.has(key)) {
|
|
497
|
+
const c = { name: key, declared: false };
|
|
498
|
+
if (sourceUri !== undefined) {
|
|
499
|
+
c.sourceUri = sourceUri;
|
|
500
|
+
}
|
|
501
|
+
commodityMap.set(key, c);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
// Also extract commodity from cost notation
|
|
505
|
+
if (posting?.cost?.amount?.commodity && posting.cost.amount.commodity !== '') {
|
|
506
|
+
const key = posting.cost.amount.commodity;
|
|
507
|
+
if (!commodityMap.has(key)) {
|
|
508
|
+
const c = { name: key, declared: false };
|
|
509
|
+
if (sourceUri !== undefined) {
|
|
510
|
+
c.sourceUri = sourceUri;
|
|
511
|
+
}
|
|
512
|
+
commodityMap.set(key, c);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return Array.from(commodityMap.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
518
|
+
}
|
|
519
|
+
function extractTagNames(document, sourceUri) {
|
|
520
|
+
const text = document.getText();
|
|
521
|
+
const lines = text.split('\n');
|
|
522
|
+
const tagMap = new Map();
|
|
523
|
+
for (let i = 0; i < lines.length; i++) {
|
|
524
|
+
const line = lines[i];
|
|
525
|
+
if (line.trim().startsWith('tag ')) {
|
|
526
|
+
const tagName = line.trim().substring(4).split(';')[0].trim();
|
|
527
|
+
if (tagName) {
|
|
528
|
+
const t = { name: tagName, declared: true };
|
|
529
|
+
if (sourceUri !== undefined) {
|
|
530
|
+
t.sourceUri = sourceUri;
|
|
531
|
+
t.line = i;
|
|
532
|
+
}
|
|
533
|
+
tagMap.set(tagName, t);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
// Standalone or indented comment lines (transaction-level comments)
|
|
537
|
+
if ((0, index_1.isComment)(line)) {
|
|
538
|
+
const commentText = line.trim().substring(1);
|
|
539
|
+
const extracted = (0, index_1.extractTags)(commentText);
|
|
540
|
+
for (const k of Object.keys(extracted)) {
|
|
541
|
+
if (!tagMap.has(k)) {
|
|
542
|
+
const t = { name: k, declared: false };
|
|
543
|
+
if (sourceUri !== undefined) {
|
|
544
|
+
t.sourceUri = sourceUri;
|
|
545
|
+
t.line = i;
|
|
546
|
+
}
|
|
547
|
+
tagMap.set(k, t);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if ((0, index_1.isPosting)(line)) {
|
|
552
|
+
// Use parsePosting to extract tags from the posting's comment part only
|
|
553
|
+
const posting = parsePosting(line);
|
|
554
|
+
if (posting?.tags) {
|
|
555
|
+
for (const k of Object.keys(posting.tags)) {
|
|
556
|
+
if (!tagMap.has(k)) {
|
|
557
|
+
const t = { name: k, declared: false };
|
|
558
|
+
if (sourceUri !== undefined) {
|
|
559
|
+
t.sourceUri = sourceUri;
|
|
560
|
+
}
|
|
561
|
+
tagMap.set(k, t);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
if ((0, index_1.isTransactionHeader)(line)) {
|
|
567
|
+
const m = line.match(/;(.+)$/);
|
|
568
|
+
if (m) {
|
|
569
|
+
const extracted = (0, index_1.extractTags)(m[1]);
|
|
570
|
+
for (const k of Object.keys(extracted)) {
|
|
571
|
+
if (!tagMap.has(k)) {
|
|
572
|
+
const t = { name: k, declared: false };
|
|
573
|
+
if (sourceUri !== undefined) {
|
|
574
|
+
t.sourceUri = sourceUri;
|
|
575
|
+
}
|
|
576
|
+
tagMap.set(k, t);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return Array.from(tagMap.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
583
|
+
}
|
|
584
|
+
function parseDirective(line) {
|
|
585
|
+
const trimmed = line.trim();
|
|
586
|
+
// Extract comment if present
|
|
587
|
+
const commentMatch = trimmed.match(/^([^;]*);(.*)$/);
|
|
588
|
+
const mainPart = commentMatch ? commentMatch[1].trim() : trimmed;
|
|
589
|
+
const comment = commentMatch ? commentMatch[2].trim() : undefined;
|
|
590
|
+
// Parse directive type and value
|
|
591
|
+
const directives = ['account', 'commodity', 'payee', 'tag', 'include', 'alias'];
|
|
592
|
+
for (const directiveType of directives) {
|
|
593
|
+
if (mainPart.startsWith(directiveType + ' ')) {
|
|
594
|
+
const value = mainPart.substring(directiveType.length + 1).trim();
|
|
595
|
+
if (value) {
|
|
596
|
+
return {
|
|
597
|
+
type: directiveType,
|
|
598
|
+
value,
|
|
599
|
+
comment
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
//# sourceMappingURL=ast.js.map
|