hledger-lsp 0.1.2 → 0.1.6
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/README.md +91 -173
- package/out/features/codeActions.d.ts +16 -0
- package/out/features/codeActions.d.ts.map +1 -1
- package/out/features/codeActions.js +129 -0
- package/out/features/codeActions.js.map +1 -1
- package/out/features/codeLens.d.ts +29 -0
- package/out/features/codeLens.d.ts.map +1 -0
- package/out/features/codeLens.js +102 -0
- package/out/features/codeLens.js.map +1 -0
- package/out/features/completion.d.ts +16 -4
- package/out/features/completion.d.ts.map +1 -1
- package/out/features/completion.js +23 -10
- package/out/features/completion.js.map +1 -1
- package/out/features/definition.d.ts.map +1 -1
- package/out/features/definition.js +4 -4
- package/out/features/definition.js.map +1 -1
- package/out/features/foldingRanges.d.ts.map +1 -1
- package/out/features/foldingRanges.js +6 -2
- package/out/features/foldingRanges.js.map +1 -1
- package/out/features/formatter.d.ts +4 -20
- package/out/features/formatter.d.ts.map +1 -1
- package/out/features/formatter.js +271 -223
- package/out/features/formatter.js.map +1 -1
- package/out/features/formattingUtils.d.ts +38 -0
- package/out/features/formattingUtils.d.ts.map +1 -0
- package/out/features/formattingUtils.js +107 -0
- package/out/features/formattingUtils.js.map +1 -0
- package/out/features/hover.d.ts +0 -4
- package/out/features/hover.d.ts.map +1 -1
- package/out/features/hover.js +11 -20
- package/out/features/hover.js.map +1 -1
- package/out/features/inlayHints.d.ts +12 -6
- package/out/features/inlayHints.d.ts.map +1 -1
- package/out/features/inlayHints.js +202 -84
- package/out/features/inlayHints.js.map +1 -1
- package/out/features/semanticTokens.d.ts.map +1 -1
- package/out/features/semanticTokens.js +17 -14
- package/out/features/semanticTokens.js.map +1 -1
- package/out/features/symbols.d.ts.map +1 -1
- package/out/features/symbols.js +8 -5
- package/out/features/symbols.js.map +1 -1
- package/out/features/validator.d.ts +5 -4
- package/out/features/validator.d.ts.map +1 -1
- package/out/features/validator.js +145 -105
- package/out/features/validator.js.map +1 -1
- package/out/parser/ast.d.ts +48 -7
- package/out/parser/ast.d.ts.map +1 -1
- package/out/parser/ast.js +552 -363
- package/out/parser/ast.js.map +1 -1
- package/out/parser/document.d.ts +7 -0
- package/out/parser/document.d.ts.map +1 -0
- package/out/parser/document.js +70 -0
- package/out/parser/document.js.map +1 -0
- package/out/parser/includes.d.ts.map +1 -1
- package/out/parser/includes.js +21 -33
- package/out/parser/includes.js.map +1 -1
- package/out/parser/index.d.ts +8 -5
- package/out/parser/index.d.ts.map +1 -1
- package/out/parser/index.js +39 -14
- package/out/parser/index.js.map +1 -1
- package/out/server/configFile.d.ts +104 -0
- package/out/server/configFile.d.ts.map +1 -0
- package/out/server/configFile.js +231 -0
- package/out/server/configFile.js.map +1 -0
- package/out/server/settings.d.ts +13 -0
- package/out/server/settings.d.ts.map +1 -1
- package/out/server/settings.js +13 -3
- package/out/server/settings.js.map +1 -1
- package/out/server/workspace.d.ts +126 -0
- package/out/server/workspace.d.ts.map +1 -0
- package/out/server/workspace.js +599 -0
- package/out/server/workspace.js.map +1 -0
- package/out/server.js +435 -27
- package/out/server.js.map +1 -1
- package/out/types.d.ts +27 -12
- package/out/types.d.ts.map +1 -1
- package/out/types.js +12 -0
- package/out/types.js.map +1 -1
- package/out/utils/amountFormatter.d.ts +14 -0
- package/out/utils/amountFormatter.d.ts.map +1 -0
- package/out/utils/amountFormatter.js +55 -0
- package/out/utils/amountFormatter.js.map +1 -0
- package/out/utils/balanceCalculator.d.ts +32 -0
- package/out/utils/balanceCalculator.d.ts.map +1 -0
- package/out/utils/balanceCalculator.js +93 -0
- package/out/utils/balanceCalculator.js.map +1 -0
- package/out/utils/index.js +1 -1
- package/out/utils/index.js.map +1 -1
- package/package.json +1 -1
package/out/parser/ast.js
CHANGED
|
@@ -5,10 +5,16 @@ exports.inferCosts = inferCosts;
|
|
|
5
5
|
exports.parseTransactionHeader = parseTransactionHeader;
|
|
6
6
|
exports.parsePosting = parsePosting;
|
|
7
7
|
exports.parseAmount = parseAmount;
|
|
8
|
-
exports.
|
|
9
|
-
exports.
|
|
10
|
-
exports.
|
|
11
|
-
exports.
|
|
8
|
+
exports.parseFormat = parseFormat;
|
|
9
|
+
exports.addAccount = addAccount;
|
|
10
|
+
exports.addPayee = addPayee;
|
|
11
|
+
exports.addCommodity = addCommodity;
|
|
12
|
+
exports.addTag = addTag;
|
|
13
|
+
exports.processAccountDirective = processAccountDirective;
|
|
14
|
+
exports.processPayeeDirective = processPayeeDirective;
|
|
15
|
+
exports.processCommodityDirective = processCommodityDirective;
|
|
16
|
+
exports.processTagDirective = processTagDirective;
|
|
17
|
+
exports.processTransaction = processTransaction;
|
|
12
18
|
exports.parseDirective = parseDirective;
|
|
13
19
|
const index_1 = require("../utils/index");
|
|
14
20
|
/**
|
|
@@ -111,11 +117,13 @@ function inferCosts(transaction) {
|
|
|
111
117
|
const firstCommodity = transaction.postings[0].amount.commodity || '';
|
|
112
118
|
// Sum all amounts in the other commodity
|
|
113
119
|
let otherCommodity = '';
|
|
120
|
+
let otherCommodityFormat;
|
|
114
121
|
let otherSum = 0;
|
|
115
122
|
for (const posting of transaction.postings) {
|
|
116
123
|
const commodity = posting.amount.commodity || '';
|
|
117
124
|
if (commodity !== firstCommodity) {
|
|
118
125
|
otherCommodity = commodity;
|
|
126
|
+
otherCommodityFormat = posting.amount.format;
|
|
119
127
|
otherSum += posting.amount.quantity;
|
|
120
128
|
}
|
|
121
129
|
}
|
|
@@ -123,7 +131,8 @@ function inferCosts(transaction) {
|
|
|
123
131
|
// This makes the transaction balance when cost is used for balance calculation
|
|
124
132
|
const costAmount = {
|
|
125
133
|
quantity: -otherSum,
|
|
126
|
-
commodity: otherCommodity
|
|
134
|
+
commodity: otherCommodity,
|
|
135
|
+
format: otherCommodityFormat,
|
|
127
136
|
};
|
|
128
137
|
// Add inferred total cost to first posting
|
|
129
138
|
transaction.postings[0].cost = {
|
|
@@ -131,60 +140,107 @@ function inferCosts(transaction) {
|
|
|
131
140
|
amount: costAmount
|
|
132
141
|
};
|
|
133
142
|
}
|
|
134
|
-
function
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
if (!dateMatch)
|
|
143
|
+
function parseDate(line) {
|
|
144
|
+
const match = line.match(/^(\d{4}[-/]\d{2}[-/]\d{2})/);
|
|
145
|
+
if (!match)
|
|
138
146
|
return null;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
if (
|
|
144
|
-
effectiveDate
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
code = codeMatch[1];
|
|
160
|
-
rest = rest.substring(codeMatch[0].length).trim();
|
|
147
|
+
return { date: match[1], rest: line.substring(match[1].length).trim() };
|
|
148
|
+
}
|
|
149
|
+
function parseEffectiveDate(line) {
|
|
150
|
+
const match = line.match(/^=(\d{4}[-/]\d{2}[-/]\d{2})/);
|
|
151
|
+
if (match) {
|
|
152
|
+
return { effectiveDate: match[1], rest: line.substring(match[0].length).trim() };
|
|
153
|
+
}
|
|
154
|
+
return { effectiveDate: undefined, rest: line };
|
|
155
|
+
}
|
|
156
|
+
function parseStatus(line) {
|
|
157
|
+
if (line.startsWith('*'))
|
|
158
|
+
return { status: 'cleared', rest: line.substring(1).trim() };
|
|
159
|
+
if (line.startsWith('!'))
|
|
160
|
+
return { status: 'pending', rest: line.substring(1).trim() };
|
|
161
|
+
return { status: undefined, rest: line };
|
|
162
|
+
}
|
|
163
|
+
function parseCode(line) {
|
|
164
|
+
const match = line.match(/^\(([^)]+)\)/);
|
|
165
|
+
if (match) {
|
|
166
|
+
return { code: match[1], rest: line.substring(match[0].length).trim() };
|
|
161
167
|
}
|
|
168
|
+
return { code: undefined, rest: line };
|
|
169
|
+
}
|
|
170
|
+
function parseDescription(line) {
|
|
171
|
+
let description = line;
|
|
162
172
|
let comment;
|
|
163
173
|
let tags;
|
|
164
|
-
const
|
|
165
|
-
if (
|
|
166
|
-
|
|
167
|
-
comment =
|
|
174
|
+
const match = line.match(/^([^;]*);(.*)$/);
|
|
175
|
+
if (match) {
|
|
176
|
+
description = match[1].trim();
|
|
177
|
+
comment = match[2].trim();
|
|
168
178
|
const extracted = (0, index_1.extractTags)(comment);
|
|
169
179
|
if (Object.keys(extracted).length > 0)
|
|
170
180
|
tags = extracted;
|
|
171
181
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
182
|
+
else {
|
|
183
|
+
description = line.trim();
|
|
184
|
+
}
|
|
185
|
+
return { description, comment, tags };
|
|
186
|
+
}
|
|
187
|
+
function parsePayeeAndNote(description) {
|
|
178
188
|
const pipeIndex = description.indexOf('|');
|
|
179
189
|
if (pipeIndex !== -1) {
|
|
180
|
-
|
|
181
|
-
|
|
190
|
+
return {
|
|
191
|
+
payee: description.substring(0, pipeIndex).trim(),
|
|
192
|
+
note: description.substring(pipeIndex + 1).trim()
|
|
193
|
+
};
|
|
182
194
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
195
|
+
return { payee: description, note: description };
|
|
196
|
+
}
|
|
197
|
+
function parseTransactionHeader(line) {
|
|
198
|
+
const trimmed = line.trim();
|
|
199
|
+
const dateRes = parseDate(trimmed);
|
|
200
|
+
if (!dateRes)
|
|
201
|
+
return null;
|
|
202
|
+
let rest = dateRes.rest;
|
|
203
|
+
const effDateRes = parseEffectiveDate(rest);
|
|
204
|
+
rest = effDateRes.rest;
|
|
205
|
+
const statusRes = parseStatus(rest);
|
|
206
|
+
rest = statusRes.rest;
|
|
207
|
+
const codeRes = parseCode(rest);
|
|
208
|
+
rest = codeRes.rest;
|
|
209
|
+
const descRes = parseDescription(rest);
|
|
210
|
+
const { payee, note } = parsePayeeAndNote(descRes.description);
|
|
211
|
+
return {
|
|
212
|
+
date: dateRes.date,
|
|
213
|
+
effectiveDate: effDateRes.effectiveDate,
|
|
214
|
+
status: statusRes.status,
|
|
215
|
+
code: codeRes.code,
|
|
216
|
+
description: descRes.description,
|
|
217
|
+
payee,
|
|
218
|
+
note,
|
|
219
|
+
comment: descRes.comment,
|
|
220
|
+
tags: descRes.tags
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function parseCost(text) {
|
|
224
|
+
// Check for @@ first (total price), then @ (unit price)
|
|
225
|
+
const totalCostMatch = text.match(/@@\s*(.+)$/);
|
|
226
|
+
const unitCostMatch = !totalCostMatch ? text.match(/@\s*(.+)$/) : null;
|
|
227
|
+
if (totalCostMatch) {
|
|
228
|
+
const amountPart = text.substring(0, totalCostMatch.index ?? 0).trim();
|
|
229
|
+
const costPart = totalCostMatch[1].trim();
|
|
230
|
+
const costAmount = parseAmount(costPart);
|
|
231
|
+
if (costAmount) {
|
|
232
|
+
return { amountPart, cost: { type: 'total', amount: costAmount } };
|
|
233
|
+
}
|
|
186
234
|
}
|
|
187
|
-
|
|
235
|
+
else if (unitCostMatch) {
|
|
236
|
+
const amountPart = text.substring(0, unitCostMatch.index ?? 0).trim();
|
|
237
|
+
const costPart = unitCostMatch[1].trim();
|
|
238
|
+
const costAmount = parseAmount(costPart);
|
|
239
|
+
if (costAmount) {
|
|
240
|
+
return { amountPart, cost: { type: 'unit', amount: costAmount } };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return { amountPart: text.trim() };
|
|
188
244
|
}
|
|
189
245
|
function parsePosting(line) {
|
|
190
246
|
if (!(0, index_1.isPosting)(line))
|
|
@@ -221,365 +277,498 @@ function parsePosting(line) {
|
|
|
221
277
|
posting.assertion = assertionAmount;
|
|
222
278
|
}
|
|
223
279
|
// Now parse amount and cost from beforeAssertion
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
280
|
+
const { amountPart, cost } = parseCost(beforeAssertion);
|
|
281
|
+
if (cost)
|
|
282
|
+
posting.cost = cost;
|
|
283
|
+
const amount = parseAmount(amountPart);
|
|
284
|
+
if (amount)
|
|
285
|
+
posting.amount = amount;
|
|
286
|
+
return posting;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Helper to detect decimal mark and thousands separator from a number string.
|
|
290
|
+
*/
|
|
291
|
+
function detectNumberFormat(numStr, defaultDecimalMark) {
|
|
292
|
+
let decimalMark = defaultDecimalMark;
|
|
293
|
+
if (!decimalMark) {
|
|
294
|
+
const lastDot = numStr.lastIndexOf('.');
|
|
295
|
+
const lastComma = numStr.lastIndexOf(',');
|
|
296
|
+
if (lastDot > -1 && lastComma > -1) {
|
|
297
|
+
decimalMark = lastDot > lastComma ? '.' : ',';
|
|
298
|
+
}
|
|
299
|
+
else if (lastDot > -1) {
|
|
300
|
+
// Check if it's a thousands separator
|
|
301
|
+
const parts = numStr.split('.');
|
|
302
|
+
if (parts.length > 2) {
|
|
303
|
+
decimalMark = null; // Multiple dots -> thousands separator
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
decimalMark = '.';
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
else if (lastComma > -1) {
|
|
310
|
+
const parts = numStr.split(',');
|
|
311
|
+
if (parts.length > 2) {
|
|
312
|
+
decimalMark = null; // Multiple commas -> thousands separator
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
decimalMark = ',';
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
decimalMark = null;
|
|
237
320
|
}
|
|
238
321
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
322
|
+
// Detect thousands separator
|
|
323
|
+
// It should be the other separator if present
|
|
324
|
+
// We need to look at the integer part only
|
|
325
|
+
let integerPart = numStr;
|
|
326
|
+
if (decimalMark) {
|
|
327
|
+
const lastIndex = numStr.lastIndexOf(decimalMark);
|
|
328
|
+
if (lastIndex >= 0) {
|
|
329
|
+
integerPart = numStr.substring(0, lastIndex);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const sepCounts = {};
|
|
333
|
+
for (let i = 0; i < integerPart.length; i++) {
|
|
334
|
+
const ch = integerPart[i];
|
|
335
|
+
if (ch < '0' || ch > '9')
|
|
336
|
+
sepCounts[ch] = (sepCounts[ch] || 0) + 1;
|
|
337
|
+
}
|
|
338
|
+
let thousandsSeparator = null;
|
|
339
|
+
const separators = Object.keys(sepCounts);
|
|
340
|
+
// If we have a decimal mark, the thousands separator must be different
|
|
341
|
+
// Filter out the decimal mark from potential separators just in case
|
|
342
|
+
const potentialSeparators = separators.filter(s => s !== decimalMark);
|
|
343
|
+
if (potentialSeparators.length === 1) {
|
|
344
|
+
const candidate = potentialSeparators[0];
|
|
345
|
+
// Only accept valid thousand separators: '.', ',', ' '
|
|
346
|
+
if (candidate === '.' || candidate === ',' || candidate === ' ') {
|
|
347
|
+
thousandsSeparator = candidate;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
else if (potentialSeparators.length > 1) {
|
|
351
|
+
let max = 0;
|
|
352
|
+
let pick = null;
|
|
353
|
+
for (const k of potentialSeparators) {
|
|
354
|
+
// Only consider valid thousand separators
|
|
355
|
+
if ((k === '.' || k === ',' || k === ' ') && sepCounts[k] > max) {
|
|
356
|
+
max = sepCounts[k];
|
|
357
|
+
pick = k;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
thousandsSeparator = pick;
|
|
361
|
+
}
|
|
362
|
+
return { decimalMark, thousandsSeparator };
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Helper to parse number string given a decimal mark
|
|
366
|
+
*/
|
|
367
|
+
function parseNumberWithFormat(numStr, mark) {
|
|
368
|
+
if (!mark) {
|
|
369
|
+
// If no mark provided/detected, assume standard float parsing (remove all non-digits/dots/minus)
|
|
370
|
+
// But wait, if no mark, we need to decide what to do.
|
|
371
|
+
// If ambiguous (e.g. 1.000), hledger assumes decimal mark.
|
|
372
|
+
// So we should treat the last separator as decimal if it exists.
|
|
373
|
+
const lastDot = numStr.lastIndexOf('.');
|
|
374
|
+
const lastComma = numStr.lastIndexOf(',');
|
|
375
|
+
if (lastDot > -1 && lastComma > -1) {
|
|
376
|
+
mark = lastDot > lastComma ? '.' : ',';
|
|
377
|
+
}
|
|
378
|
+
else if (lastDot > -1) {
|
|
379
|
+
mark = '.';
|
|
380
|
+
}
|
|
381
|
+
else if (lastComma > -1) {
|
|
382
|
+
mark = ',';
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
let cleanStr = numStr;
|
|
386
|
+
if (mark) {
|
|
387
|
+
// Remove everything that is NOT the decimal mark, digit, or minus
|
|
388
|
+
// This effectively removes thousands separators
|
|
389
|
+
const regex = new RegExp(`[^0-9${mark === '.' ? '\\.' : ','}-]`, 'g');
|
|
390
|
+
cleanStr = numStr.replace(regex, '');
|
|
391
|
+
// Replace decimal mark with dot for JS parseFloat
|
|
392
|
+
if (mark === ',') {
|
|
393
|
+
cleanStr = cleanStr.replace(',', '.');
|
|
249
394
|
}
|
|
250
395
|
}
|
|
251
396
|
else {
|
|
252
|
-
// No
|
|
253
|
-
|
|
254
|
-
if (amount)
|
|
255
|
-
posting.amount = amount;
|
|
397
|
+
// No separators found, just parse
|
|
398
|
+
cleanStr = numStr.replace(/[^0-9.-]/g, '');
|
|
256
399
|
}
|
|
257
|
-
return
|
|
400
|
+
return parseFloat(cleanStr);
|
|
258
401
|
}
|
|
259
|
-
function parseAmount(amountStr) {
|
|
402
|
+
function parseAmount(amountStr, decimalMark) {
|
|
260
403
|
const trimmed = amountStr.trim();
|
|
261
404
|
if (!trimmed)
|
|
262
405
|
return null;
|
|
406
|
+
// Regex to split commodity and amount
|
|
407
|
+
// We need to be more permissive with the amount part to capture various formats
|
|
408
|
+
// Amount part can contain digits, commas, dots, spaces (maybe)
|
|
409
|
+
// But spaces are tricky. For now let's assume space separates commodity if symbol is on left/right
|
|
263
410
|
const patterns = [
|
|
264
|
-
{
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
411
|
+
{
|
|
412
|
+
// Symbol on left, negative
|
|
413
|
+
pattern: /^-([^\d\s-]+)\s*([-]?\d[\d.,\s]*)$/,
|
|
414
|
+
handler: (m) => {
|
|
415
|
+
const rawAmount = m[2];
|
|
416
|
+
const { decimalMark: mark } = detectNumberFormat(rawAmount, decimalMark);
|
|
417
|
+
return { quantity: -Math.abs(parseNumberWithFormat(rawAmount, mark || undefined)), commodity: m[1], rawAmount };
|
|
418
|
+
},
|
|
419
|
+
cleaner: (m, s) => s.replace(/^-/, '')
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
// Symbol on left
|
|
423
|
+
pattern: /^([^\d\s-]+)\s*([-]?\d[\d.,\s]*)$/,
|
|
424
|
+
handler: (m) => {
|
|
425
|
+
const rawAmount = m[2];
|
|
426
|
+
const { decimalMark: mark } = detectNumberFormat(rawAmount, decimalMark);
|
|
427
|
+
return { quantity: parseNumberWithFormat(rawAmount, mark || undefined), commodity: m[1], rawAmount };
|
|
428
|
+
},
|
|
429
|
+
cleaner: (m, s) => s.replace(m[2], m[2].replace('-', ''))
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
// Symbol on right
|
|
433
|
+
pattern: /^([-]?\d[\d.,\s]*)\s*([^\d\s]+)$/,
|
|
434
|
+
handler: (m) => {
|
|
435
|
+
const rawAmount = m[1];
|
|
436
|
+
const { decimalMark: mark } = detectNumberFormat(rawAmount, decimalMark);
|
|
437
|
+
return { quantity: parseNumberWithFormat(rawAmount, mark || undefined), commodity: m[2], rawAmount };
|
|
438
|
+
},
|
|
439
|
+
cleaner: (m, s) => s.replace(m[1], m[1].replace('-', ''))
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
// No symbol
|
|
443
|
+
pattern: /^([-]?\d[\d.,\s]*)$/,
|
|
444
|
+
handler: (m) => {
|
|
445
|
+
const rawAmount = m[1];
|
|
446
|
+
const { decimalMark: mark } = detectNumberFormat(rawAmount, decimalMark);
|
|
447
|
+
return { quantity: parseNumberWithFormat(rawAmount, mark || undefined), commodity: '', rawAmount };
|
|
448
|
+
},
|
|
449
|
+
cleaner: (m, s) => s.replace(m[1], m[1].replace('-', ''))
|
|
450
|
+
}
|
|
268
451
|
];
|
|
269
|
-
for (const { pattern, handler } of patterns) {
|
|
452
|
+
for (const { pattern, handler, cleaner } of patterns) {
|
|
270
453
|
const match = trimmed.match(pattern);
|
|
271
454
|
if (match) {
|
|
455
|
+
// Verify that the amount part looks valid (e.g. not just a dot)
|
|
456
|
+
// and doesn't contain internal spaces that look like commodity separators
|
|
457
|
+
// This is a heuristic.
|
|
272
458
|
const res = handler(match);
|
|
273
459
|
if (isNaN(res.quantity))
|
|
274
|
-
|
|
275
|
-
|
|
460
|
+
continue;
|
|
461
|
+
const amount = { quantity: res.quantity, commodity: res.commodity };
|
|
462
|
+
let sampleForFormat = trimmed;
|
|
463
|
+
if (res.quantity < 0) {
|
|
464
|
+
sampleForFormat = cleaner(match, trimmed);
|
|
465
|
+
}
|
|
466
|
+
const parsedFormat = parseFormat(sampleForFormat);
|
|
467
|
+
if (parsedFormat && parsedFormat.format) {
|
|
468
|
+
amount.format = parsedFormat.format;
|
|
469
|
+
}
|
|
470
|
+
return amount;
|
|
276
471
|
}
|
|
277
472
|
}
|
|
278
473
|
return null;
|
|
279
474
|
}
|
|
280
|
-
function
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
475
|
+
function parseFormat(sample) {
|
|
476
|
+
if (!sample)
|
|
477
|
+
return null;
|
|
478
|
+
const s = sample.trim();
|
|
479
|
+
const stripQuotes = (s) => { const t = s.trim(); if (t.length >= 2 && t.startsWith('"') && t.endsWith('"'))
|
|
480
|
+
return t.substring(1, t.length - 1); return t; };
|
|
481
|
+
const firstDigit = s.search(/\d/);
|
|
482
|
+
if (firstDigit === -1) {
|
|
483
|
+
return { name: stripQuotes(s) };
|
|
484
|
+
}
|
|
485
|
+
const allowed = /[0-9.,\u00A0\s]/;
|
|
486
|
+
let start = firstDigit;
|
|
487
|
+
let end = start;
|
|
488
|
+
while (end < s.length && allowed.test(s[end]))
|
|
489
|
+
end++;
|
|
490
|
+
const leftRaw = s.substring(0, start).trim();
|
|
491
|
+
const numberRaw = s.substring(start, end).trim();
|
|
492
|
+
const rightRaw = s.substring(end).trim();
|
|
493
|
+
if (!numberRaw)
|
|
494
|
+
return null;
|
|
495
|
+
const { decimalMark, thousandsSeparator } = detectNumberFormat(numberRaw);
|
|
496
|
+
let decimalIndex = -1;
|
|
497
|
+
if (decimalMark) {
|
|
498
|
+
decimalIndex = numberRaw.lastIndexOf(decimalMark);
|
|
499
|
+
}
|
|
500
|
+
const fractionalPart = decimalIndex >= 0 ? numberRaw.substring(decimalIndex + 1) : '';
|
|
501
|
+
const precision = decimalIndex >= 0 ? (fractionalPart.length > 0 ? fractionalPart.length : 0) : null;
|
|
502
|
+
let rawSymbol = leftRaw || rightRaw || '';
|
|
503
|
+
rawSymbol = stripQuotes(rawSymbol);
|
|
504
|
+
const symbolOnLeft = Boolean(leftRaw);
|
|
505
|
+
let spaceBetween = false;
|
|
506
|
+
if (symbolOnLeft) {
|
|
507
|
+
const between = s.substring(0, firstDigit);
|
|
508
|
+
spaceBetween = /\s/.test(between.replace(stripQuotes(leftRaw), '')) || /\s/.test(leftRaw.slice(-1));
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
const after = s.substring(end);
|
|
512
|
+
spaceBetween = /\s/.test(after.replace(stripQuotes(rightRaw), '')) || /\s/.test(s[end - 1]);
|
|
513
|
+
}
|
|
514
|
+
const format = { symbol: rawSymbol, symbolOnLeft, spaceBetween, decimalMark, thousandsSeparator, precision };
|
|
515
|
+
let name = rawSymbol;
|
|
516
|
+
if (rightRaw) {
|
|
517
|
+
const candidate = stripQuotes(rightRaw);
|
|
518
|
+
if (/^[A-Za-z][A-Za-z0-9 _-]*$/.test(candidate) || (rightRaw.trim().startsWith('"') && rightRaw.trim().endsWith('"')))
|
|
519
|
+
name = candidate;
|
|
520
|
+
}
|
|
521
|
+
if (rawSymbol === '""')
|
|
522
|
+
name = '';
|
|
523
|
+
return { name, format };
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Helper functions used by processCommodityDirective
|
|
527
|
+
*/
|
|
528
|
+
const stripQuotes = (s) => { const t = s.trim(); if (t.length >= 2 && t.startsWith('"') && t.endsWith('"'))
|
|
529
|
+
return t.substring(1, t.length - 1); return t; };
|
|
530
|
+
function parseCommodityDirective(line) {
|
|
531
|
+
const directive = line.trim().substring(10).split(';')[0].trim();
|
|
532
|
+
if (!directive)
|
|
533
|
+
return null;
|
|
534
|
+
let parsed = null;
|
|
535
|
+
if (/\d/.test(directive))
|
|
536
|
+
parsed = parseFormat(directive);
|
|
537
|
+
if (parsed) {
|
|
538
|
+
return { name: parsed.name, format: parsed.format };
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
return { name: stripQuotes(directive), format: undefined };
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
function parseFormatSubDirective(line) {
|
|
545
|
+
const trimmedNext = line.trim();
|
|
546
|
+
if (!trimmedNext.startsWith('format '))
|
|
547
|
+
return null;
|
|
548
|
+
const rest = trimmedNext.substring(7).trim();
|
|
549
|
+
const m = rest.match(/^(".*?"|\S+)\s+(.*)$/);
|
|
550
|
+
if (m) {
|
|
551
|
+
const formatSymbolRaw = m[1];
|
|
552
|
+
const samplePart = m[2];
|
|
553
|
+
const parsedFormat = parseFormat(samplePart) || parseFormat(`${samplePart} ${formatSymbolRaw}`);
|
|
554
|
+
if (parsedFormat && parsedFormat.format) {
|
|
555
|
+
const fs = stripQuotes(formatSymbolRaw);
|
|
556
|
+
return { name: fs, format: parsedFormat.format };
|
|
296
557
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
558
|
+
}
|
|
559
|
+
// If no match, try parsing rest directly as a format sample (e.g., "format $1,000.00")
|
|
560
|
+
const parsedDirect = parseFormat(rest);
|
|
561
|
+
if (parsedDirect && parsedDirect.format) {
|
|
562
|
+
return { name: parsedDirect.name, format: parsedDirect.format };
|
|
563
|
+
}
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Helper functions for incremental parsing - adding items to Maps
|
|
568
|
+
*/
|
|
569
|
+
/**
|
|
570
|
+
* Add or update an account in the accounts map
|
|
571
|
+
* If account exists, mark as declared if it wasn't already
|
|
572
|
+
*/
|
|
573
|
+
function addAccount(accountMap, name, declared, sourceUri, line) {
|
|
574
|
+
const existing = accountMap.get(name);
|
|
575
|
+
if (existing) {
|
|
576
|
+
// If we're adding a declared version, update the existing entry
|
|
577
|
+
if (declared && !existing.declared) {
|
|
578
|
+
existing.declared = true;
|
|
579
|
+
if (sourceUri !== undefined)
|
|
580
|
+
existing.sourceUri = sourceUri;
|
|
581
|
+
if (line !== undefined)
|
|
582
|
+
existing.line = line;
|
|
307
583
|
}
|
|
308
584
|
}
|
|
309
|
-
|
|
585
|
+
else {
|
|
586
|
+
const acc = { name, declared };
|
|
587
|
+
if (sourceUri !== undefined) {
|
|
588
|
+
acc.sourceUri = sourceUri;
|
|
589
|
+
acc.line = line;
|
|
590
|
+
}
|
|
591
|
+
accountMap.set(name, acc);
|
|
592
|
+
}
|
|
310
593
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
if (
|
|
318
|
-
|
|
319
|
-
if (
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
p.line = i;
|
|
324
|
-
}
|
|
325
|
-
payeeMap.set(payeeName, p);
|
|
326
|
-
}
|
|
594
|
+
/**
|
|
595
|
+
* Add or update a payee in the payees map
|
|
596
|
+
*/
|
|
597
|
+
function addPayee(payeeMap, name, declared, sourceUri, line) {
|
|
598
|
+
const existing = payeeMap.get(name);
|
|
599
|
+
if (existing) {
|
|
600
|
+
if (declared && !existing.declared) {
|
|
601
|
+
existing.declared = true;
|
|
602
|
+
if (sourceUri !== undefined)
|
|
603
|
+
existing.sourceUri = sourceUri;
|
|
604
|
+
if (line !== undefined)
|
|
605
|
+
existing.line = line;
|
|
327
606
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
p.sourceUri = sourceUri;
|
|
335
|
-
p.line = i;
|
|
336
|
-
}
|
|
337
|
-
payeeMap.set(header.payee, p);
|
|
338
|
-
}
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
const p = { name, declared };
|
|
610
|
+
if (sourceUri !== undefined) {
|
|
611
|
+
p.sourceUri = sourceUri;
|
|
612
|
+
p.line = line;
|
|
339
613
|
}
|
|
614
|
+
payeeMap.set(name, p);
|
|
340
615
|
}
|
|
341
|
-
return Array.from(payeeMap.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
342
616
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
if (
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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;
|
|
617
|
+
/**
|
|
618
|
+
* Add or update a commodity in the commodities map
|
|
619
|
+
* When merging formats, prefer the one with higher precision or more detail
|
|
620
|
+
*/
|
|
621
|
+
function addCommodity(commodityMap, name, declared, format, sourceUri, line) {
|
|
622
|
+
const existing = commodityMap.get(name);
|
|
623
|
+
if (existing) {
|
|
624
|
+
if (declared) {
|
|
625
|
+
existing.declared = true;
|
|
626
|
+
if (format)
|
|
627
|
+
existing.format = format;
|
|
628
|
+
if (sourceUri !== undefined)
|
|
629
|
+
existing.sourceUri = sourceUri;
|
|
630
|
+
if (line !== undefined)
|
|
631
|
+
existing.line = line;
|
|
375
632
|
}
|
|
376
|
-
else {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
}
|
|
633
|
+
else if (format && !existing.declared) {
|
|
634
|
+
// For undeclared commodities, keep the format with better precision
|
|
635
|
+
const newPrecision = format.precision ?? null;
|
|
636
|
+
const existingPrecision = existing.format?.precision ?? null;
|
|
637
|
+
if (!existing.format || (newPrecision !== null && (existingPrecision === null || newPrecision > existingPrecision))) {
|
|
638
|
+
existing.format = format;
|
|
401
639
|
}
|
|
402
|
-
thousandsSeparator = pick;
|
|
403
640
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
spaceBetween = /\s/.test(between.replace(stripQuotes(leftRaw), '')) || /\s/.test(leftRaw.slice(-1));
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
const c = { name, declared, format };
|
|
644
|
+
if (sourceUri !== undefined) {
|
|
645
|
+
c.sourceUri = sourceUri;
|
|
646
|
+
c.line = line;
|
|
411
647
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
}
|
|
648
|
+
commodityMap.set(name, c);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Add or update a tag in the tags map
|
|
653
|
+
*/
|
|
654
|
+
function addTag(tagMap, name, declared, sourceUri, line) {
|
|
655
|
+
const existing = tagMap.get(name);
|
|
656
|
+
if (existing) {
|
|
657
|
+
if (declared && !existing.declared) {
|
|
658
|
+
existing.declared = true;
|
|
659
|
+
if (sourceUri !== undefined)
|
|
660
|
+
existing.sourceUri = sourceUri;
|
|
661
|
+
if (line !== undefined)
|
|
662
|
+
existing.line = line;
|
|
491
663
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
}
|
|
664
|
+
}
|
|
665
|
+
else {
|
|
666
|
+
const t = { name, declared };
|
|
667
|
+
if (sourceUri !== undefined) {
|
|
668
|
+
t.sourceUri = sourceUri;
|
|
669
|
+
t.line = line;
|
|
515
670
|
}
|
|
671
|
+
tagMap.set(name, t);
|
|
516
672
|
}
|
|
517
|
-
return Array.from(commodityMap.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
518
673
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
674
|
+
/**
|
|
675
|
+
* Process an account directive and add it to the accounts map
|
|
676
|
+
*/
|
|
677
|
+
function processAccountDirective(line, accountMap, sourceUri, lineNumber) {
|
|
678
|
+
const accountName = line.trim().substring(8).split(';')[0].trim();
|
|
679
|
+
if (accountName) {
|
|
680
|
+
addAccount(accountMap, accountName, true, sourceUri, lineNumber);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Process a payee directive and add it to the payees map
|
|
685
|
+
*/
|
|
686
|
+
function processPayeeDirective(line, payeeMap, sourceUri, lineNumber) {
|
|
687
|
+
const payeeName = line.trim().substring(6).split(';')[0].trim();
|
|
688
|
+
if (payeeName) {
|
|
689
|
+
addPayee(payeeMap, payeeName, true, sourceUri, lineNumber);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Process a commodity directive and add it to the commodities map
|
|
694
|
+
* Handles multi-line commodity directives with format subdirectives
|
|
695
|
+
*/
|
|
696
|
+
function processCommodityDirective(lines, startLine, commodityMap, sourceUri) {
|
|
697
|
+
const line = lines[startLine];
|
|
698
|
+
const parsed = parseCommodityDirective(line);
|
|
699
|
+
if (!parsed)
|
|
700
|
+
return startLine;
|
|
701
|
+
let commodityName = parsed.name;
|
|
702
|
+
let format = parsed.format;
|
|
703
|
+
// Check for format subdirective on following lines
|
|
704
|
+
let look = startLine + 1;
|
|
705
|
+
while (look < lines.length) {
|
|
706
|
+
const next = lines[look];
|
|
707
|
+
if (!next.trim()) {
|
|
708
|
+
look++;
|
|
709
|
+
continue;
|
|
535
710
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
t.sourceUri = sourceUri;
|
|
545
|
-
t.line = i;
|
|
546
|
-
}
|
|
547
|
-
tagMap.set(k, t);
|
|
548
|
-
}
|
|
711
|
+
if (!/^\s+/.test(next))
|
|
712
|
+
break;
|
|
713
|
+
const subParsed = parseFormatSubDirective(next);
|
|
714
|
+
if (subParsed) {
|
|
715
|
+
if (subParsed.format)
|
|
716
|
+
format = subParsed.format;
|
|
717
|
+
if (subParsed.name && subParsed.name !== '' && (!commodityName || commodityName === '' || commodityName === subParsed.name)) {
|
|
718
|
+
commodityName = subParsed.name;
|
|
549
719
|
}
|
|
550
720
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
721
|
+
look++;
|
|
722
|
+
}
|
|
723
|
+
addCommodity(commodityMap, commodityName, true, format, sourceUri, startLine);
|
|
724
|
+
// Return the last line we processed
|
|
725
|
+
return look - 1;
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Process a tag directive and add it to the tags map
|
|
729
|
+
*/
|
|
730
|
+
function processTagDirective(line, tagMap, sourceUri, lineNumber) {
|
|
731
|
+
const tagName = line.trim().substring(4).split(';')[0].trim();
|
|
732
|
+
if (tagName) {
|
|
733
|
+
addTag(tagMap, tagName, true, sourceUri, lineNumber);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Extract accounts, commodities, tags from a transaction and add them to the maps
|
|
738
|
+
*/
|
|
739
|
+
function processTransaction(transaction, accountMap, commodityMap, tagMap, sourceUri) {
|
|
740
|
+
// Extract payee is handled separately since it's in the transaction header
|
|
741
|
+
// Extract accounts and commodities from postings
|
|
742
|
+
for (const posting of transaction.postings) {
|
|
743
|
+
// Add account
|
|
744
|
+
if (posting.account) {
|
|
745
|
+
addAccount(accountMap, posting.account, false, sourceUri);
|
|
565
746
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
747
|
+
// Add commodity from amount
|
|
748
|
+
if (posting.amount?.commodity && posting.amount.commodity !== '') {
|
|
749
|
+
addCommodity(commodityMap, posting.amount.commodity, false, undefined, sourceUri);
|
|
750
|
+
}
|
|
751
|
+
// Add commodity from cost
|
|
752
|
+
if (posting.cost?.amount?.commodity && posting.cost.amount.commodity !== '') {
|
|
753
|
+
addCommodity(commodityMap, posting.cost.amount.commodity, false, undefined, sourceUri);
|
|
754
|
+
}
|
|
755
|
+
// Add commodity from balance assertion
|
|
756
|
+
if (posting.assertion?.commodity && posting.assertion.commodity !== '') {
|
|
757
|
+
addCommodity(commodityMap, posting.assertion.commodity, false, undefined, sourceUri);
|
|
758
|
+
}
|
|
759
|
+
// Extract tags from posting comments
|
|
760
|
+
if (posting.tags) {
|
|
761
|
+
for (const tagName of Object.keys(posting.tags)) {
|
|
762
|
+
addTag(tagMap, tagName, false, sourceUri);
|
|
579
763
|
}
|
|
580
764
|
}
|
|
581
765
|
}
|
|
582
|
-
|
|
766
|
+
// Extract tags from transaction-level tags
|
|
767
|
+
if (transaction.tags) {
|
|
768
|
+
for (const tagName of Object.keys(transaction.tags)) {
|
|
769
|
+
addTag(tagMap, tagName, false, sourceUri);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
583
772
|
}
|
|
584
773
|
function parseDirective(line) {
|
|
585
774
|
const trimmed = line.trim();
|