hledger-lsp 0.1.1 → 0.1.5
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 +11 -8
- package/out/features/codeActions.d.ts +20 -0
- package/out/features/codeActions.d.ts.map +1 -1
- package/out/features/codeActions.js +145 -0
- package/out/features/codeActions.js.map +1 -1
- package/out/features/completion.d.ts +16 -4
- package/out/features/completion.d.ts.map +1 -1
- package/out/features/completion.js +4 -4
- package/out/features/completion.js.map +1 -1
- package/out/features/definition.js +4 -4
- package/out/features/definition.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/hover.js +4 -4
- package/out/features/hover.js.map +1 -1
- package/out/features/inlayHints.d.ts +7 -2
- package/out/features/inlayHints.d.ts.map +1 -1
- package/out/features/inlayHints.js +65 -17
- 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.js +4 -4
- package/out/features/symbols.js.map +1 -1
- package/out/features/validator.d.ts +5 -0
- package/out/features/validator.d.ts.map +1 -1
- package/out/features/validator.js +45 -13
- 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 +547 -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.map +1 -1
- package/out/parser/index.js +39 -7
- package/out/parser/index.js.map +1 -1
- package/out/server/settings.d.ts +5 -0
- package/out/server/settings.d.ts.map +1 -1
- package/out/server/settings.js +5 -3
- package/out/server/settings.js.map +1 -1
- package/out/server.js +4 -4
- package/out/server.js.map +1 -1
- package/out/types.d.ts +16 -12
- package/out/types.d.ts.map +1 -1
- package/out/types.js +1 -0
- package/out/types.js.map +1 -1
- 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,493 @@ 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
|
+
thousandsSeparator = potentialSeparators[0];
|
|
345
|
+
}
|
|
346
|
+
else if (potentialSeparators.length > 1) {
|
|
347
|
+
let max = 0;
|
|
348
|
+
let pick = null;
|
|
349
|
+
for (const k of potentialSeparators) {
|
|
350
|
+
if (sepCounts[k] > max) {
|
|
351
|
+
max = sepCounts[k];
|
|
352
|
+
pick = k;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
thousandsSeparator = pick;
|
|
356
|
+
}
|
|
357
|
+
return { decimalMark, thousandsSeparator };
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Helper to parse number string given a decimal mark
|
|
361
|
+
*/
|
|
362
|
+
function parseNumberWithFormat(numStr, mark) {
|
|
363
|
+
if (!mark) {
|
|
364
|
+
// If no mark provided/detected, assume standard float parsing (remove all non-digits/dots/minus)
|
|
365
|
+
// But wait, if no mark, we need to decide what to do.
|
|
366
|
+
// If ambiguous (e.g. 1.000), hledger assumes decimal mark.
|
|
367
|
+
// So we should treat the last separator as decimal if it exists.
|
|
368
|
+
const lastDot = numStr.lastIndexOf('.');
|
|
369
|
+
const lastComma = numStr.lastIndexOf(',');
|
|
370
|
+
if (lastDot > -1 && lastComma > -1) {
|
|
371
|
+
mark = lastDot > lastComma ? '.' : ',';
|
|
372
|
+
}
|
|
373
|
+
else if (lastDot > -1) {
|
|
374
|
+
mark = '.';
|
|
375
|
+
}
|
|
376
|
+
else if (lastComma > -1) {
|
|
377
|
+
mark = ',';
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
let cleanStr = numStr;
|
|
381
|
+
if (mark) {
|
|
382
|
+
// Remove everything that is NOT the decimal mark, digit, or minus
|
|
383
|
+
// This effectively removes thousands separators
|
|
384
|
+
const regex = new RegExp(`[^0-9${mark === '.' ? '\\.' : ','}-]`, 'g');
|
|
385
|
+
cleanStr = numStr.replace(regex, '');
|
|
386
|
+
// Replace decimal mark with dot for JS parseFloat
|
|
387
|
+
if (mark === ',') {
|
|
388
|
+
cleanStr = cleanStr.replace(',', '.');
|
|
249
389
|
}
|
|
250
390
|
}
|
|
251
391
|
else {
|
|
252
|
-
// No
|
|
253
|
-
|
|
254
|
-
if (amount)
|
|
255
|
-
posting.amount = amount;
|
|
392
|
+
// No separators found, just parse
|
|
393
|
+
cleanStr = numStr.replace(/[^0-9.-]/g, '');
|
|
256
394
|
}
|
|
257
|
-
return
|
|
395
|
+
return parseFloat(cleanStr);
|
|
258
396
|
}
|
|
259
|
-
function parseAmount(amountStr) {
|
|
397
|
+
function parseAmount(amountStr, decimalMark) {
|
|
260
398
|
const trimmed = amountStr.trim();
|
|
261
399
|
if (!trimmed)
|
|
262
400
|
return null;
|
|
401
|
+
// Regex to split commodity and amount
|
|
402
|
+
// We need to be more permissive with the amount part to capture various formats
|
|
403
|
+
// Amount part can contain digits, commas, dots, spaces (maybe)
|
|
404
|
+
// But spaces are tricky. For now let's assume space separates commodity if symbol is on left/right
|
|
263
405
|
const patterns = [
|
|
264
|
-
{
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
406
|
+
{
|
|
407
|
+
// Symbol on left, negative
|
|
408
|
+
pattern: /^-([^\d\s-]+)\s*([-]?\d[\d.,\s]*)$/,
|
|
409
|
+
handler: (m) => {
|
|
410
|
+
const rawAmount = m[2];
|
|
411
|
+
const { decimalMark: mark } = detectNumberFormat(rawAmount, decimalMark);
|
|
412
|
+
return { quantity: -Math.abs(parseNumberWithFormat(rawAmount, mark || undefined)), commodity: m[1], rawAmount };
|
|
413
|
+
},
|
|
414
|
+
cleaner: (m, s) => s.replace(/^-/, '')
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
// Symbol on left
|
|
418
|
+
pattern: /^([^\d\s-]+)\s*([-]?\d[\d.,\s]*)$/,
|
|
419
|
+
handler: (m) => {
|
|
420
|
+
const rawAmount = m[2];
|
|
421
|
+
const { decimalMark: mark } = detectNumberFormat(rawAmount, decimalMark);
|
|
422
|
+
return { quantity: parseNumberWithFormat(rawAmount, mark || undefined), commodity: m[1], rawAmount };
|
|
423
|
+
},
|
|
424
|
+
cleaner: (m, s) => s.replace(m[2], m[2].replace('-', ''))
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
// Symbol on right
|
|
428
|
+
pattern: /^([-]?\d[\d.,\s]*)\s*([^\d\s]+)$/,
|
|
429
|
+
handler: (m) => {
|
|
430
|
+
const rawAmount = m[1];
|
|
431
|
+
const { decimalMark: mark } = detectNumberFormat(rawAmount, decimalMark);
|
|
432
|
+
return { quantity: parseNumberWithFormat(rawAmount, mark || undefined), commodity: m[2], rawAmount };
|
|
433
|
+
},
|
|
434
|
+
cleaner: (m, s) => s.replace(m[1], m[1].replace('-', ''))
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
// No symbol
|
|
438
|
+
pattern: /^([-]?\d[\d.,\s]*)$/,
|
|
439
|
+
handler: (m) => {
|
|
440
|
+
const rawAmount = m[1];
|
|
441
|
+
const { decimalMark: mark } = detectNumberFormat(rawAmount, decimalMark);
|
|
442
|
+
return { quantity: parseNumberWithFormat(rawAmount, mark || undefined), commodity: '', rawAmount };
|
|
443
|
+
},
|
|
444
|
+
cleaner: (m, s) => s.replace(m[1], m[1].replace('-', ''))
|
|
445
|
+
}
|
|
268
446
|
];
|
|
269
|
-
for (const { pattern, handler } of patterns) {
|
|
447
|
+
for (const { pattern, handler, cleaner } of patterns) {
|
|
270
448
|
const match = trimmed.match(pattern);
|
|
271
449
|
if (match) {
|
|
450
|
+
// Verify that the amount part looks valid (e.g. not just a dot)
|
|
451
|
+
// and doesn't contain internal spaces that look like commodity separators
|
|
452
|
+
// This is a heuristic.
|
|
272
453
|
const res = handler(match);
|
|
273
454
|
if (isNaN(res.quantity))
|
|
274
|
-
|
|
275
|
-
|
|
455
|
+
continue;
|
|
456
|
+
const amount = { quantity: res.quantity, commodity: res.commodity };
|
|
457
|
+
let sampleForFormat = trimmed;
|
|
458
|
+
if (res.quantity < 0) {
|
|
459
|
+
sampleForFormat = cleaner(match, trimmed);
|
|
460
|
+
}
|
|
461
|
+
const parsedFormat = parseFormat(sampleForFormat);
|
|
462
|
+
if (parsedFormat && parsedFormat.format) {
|
|
463
|
+
amount.format = parsedFormat.format;
|
|
464
|
+
}
|
|
465
|
+
return amount;
|
|
276
466
|
}
|
|
277
467
|
}
|
|
278
468
|
return null;
|
|
279
469
|
}
|
|
280
|
-
function
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
470
|
+
function parseFormat(sample) {
|
|
471
|
+
if (!sample)
|
|
472
|
+
return null;
|
|
473
|
+
const s = sample.trim();
|
|
474
|
+
const stripQuotes = (s) => { const t = s.trim(); if (t.length >= 2 && t.startsWith('"') && t.endsWith('"'))
|
|
475
|
+
return t.substring(1, t.length - 1); return t; };
|
|
476
|
+
const firstDigit = s.search(/\d/);
|
|
477
|
+
if (firstDigit === -1) {
|
|
478
|
+
return { name: stripQuotes(s) };
|
|
479
|
+
}
|
|
480
|
+
const allowed = /[0-9.,\u00A0\s]/;
|
|
481
|
+
let start = firstDigit;
|
|
482
|
+
let end = start;
|
|
483
|
+
while (end < s.length && allowed.test(s[end]))
|
|
484
|
+
end++;
|
|
485
|
+
const leftRaw = s.substring(0, start).trim();
|
|
486
|
+
const numberRaw = s.substring(start, end).trim();
|
|
487
|
+
const rightRaw = s.substring(end).trim();
|
|
488
|
+
if (!numberRaw)
|
|
489
|
+
return null;
|
|
490
|
+
const { decimalMark, thousandsSeparator } = detectNumberFormat(numberRaw);
|
|
491
|
+
let decimalIndex = -1;
|
|
492
|
+
if (decimalMark) {
|
|
493
|
+
decimalIndex = numberRaw.lastIndexOf(decimalMark);
|
|
494
|
+
}
|
|
495
|
+
const fractionalPart = decimalIndex >= 0 ? numberRaw.substring(decimalIndex + 1) : '';
|
|
496
|
+
const precision = decimalIndex >= 0 ? (fractionalPart.length > 0 ? fractionalPart.length : 0) : null;
|
|
497
|
+
let rawSymbol = leftRaw || rightRaw || '';
|
|
498
|
+
rawSymbol = stripQuotes(rawSymbol);
|
|
499
|
+
const symbolOnLeft = Boolean(leftRaw);
|
|
500
|
+
let spaceBetween = false;
|
|
501
|
+
if (symbolOnLeft) {
|
|
502
|
+
const between = s.substring(0, firstDigit);
|
|
503
|
+
spaceBetween = /\s/.test(between.replace(stripQuotes(leftRaw), '')) || /\s/.test(leftRaw.slice(-1));
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
const after = s.substring(end);
|
|
507
|
+
spaceBetween = /\s/.test(after.replace(stripQuotes(rightRaw), '')) || /\s/.test(s[end - 1]);
|
|
508
|
+
}
|
|
509
|
+
const format = { symbol: rawSymbol, symbolOnLeft, spaceBetween, decimalMark: decimalMark, thousandsSeparator: thousandsSeparator || null, precision };
|
|
510
|
+
let name = rawSymbol;
|
|
511
|
+
if (rightRaw) {
|
|
512
|
+
const candidate = stripQuotes(rightRaw);
|
|
513
|
+
if (/^[A-Za-z][A-Za-z0-9 _-]*$/.test(candidate) || (rightRaw.trim().startsWith('"') && rightRaw.trim().endsWith('"')))
|
|
514
|
+
name = candidate;
|
|
515
|
+
}
|
|
516
|
+
if (rawSymbol === '""')
|
|
517
|
+
name = '';
|
|
518
|
+
return { name, format };
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Helper functions used by processCommodityDirective
|
|
522
|
+
*/
|
|
523
|
+
const stripQuotes = (s) => { const t = s.trim(); if (t.length >= 2 && t.startsWith('"') && t.endsWith('"'))
|
|
524
|
+
return t.substring(1, t.length - 1); return t; };
|
|
525
|
+
function parseCommodityDirective(line) {
|
|
526
|
+
const directive = line.trim().substring(10).split(';')[0].trim();
|
|
527
|
+
if (!directive)
|
|
528
|
+
return null;
|
|
529
|
+
let parsed = null;
|
|
530
|
+
if (/\d/.test(directive))
|
|
531
|
+
parsed = parseFormat(directive);
|
|
532
|
+
if (parsed) {
|
|
533
|
+
return { name: parsed.name, format: parsed.format };
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
return { name: stripQuotes(directive), format: undefined };
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
function parseFormatSubDirective(line) {
|
|
540
|
+
const trimmedNext = line.trim();
|
|
541
|
+
if (!trimmedNext.startsWith('format '))
|
|
542
|
+
return null;
|
|
543
|
+
const rest = trimmedNext.substring(7).trim();
|
|
544
|
+
const m = rest.match(/^(".*?"|\S+)\s+(.*)$/);
|
|
545
|
+
if (m) {
|
|
546
|
+
const formatSymbolRaw = m[1];
|
|
547
|
+
const samplePart = m[2];
|
|
548
|
+
const parsedFormat = parseFormat(samplePart) || parseFormat(`${samplePart} ${formatSymbolRaw}`);
|
|
549
|
+
if (parsedFormat && parsedFormat.format) {
|
|
550
|
+
const fs = stripQuotes(formatSymbolRaw);
|
|
551
|
+
return { name: fs, format: parsedFormat.format };
|
|
296
552
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
553
|
+
}
|
|
554
|
+
// If no match, try parsing rest directly as a format sample (e.g., "format $1,000.00")
|
|
555
|
+
const parsedDirect = parseFormat(rest);
|
|
556
|
+
if (parsedDirect && parsedDirect.format) {
|
|
557
|
+
return { name: parsedDirect.name, format: parsedDirect.format };
|
|
558
|
+
}
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Helper functions for incremental parsing - adding items to Maps
|
|
563
|
+
*/
|
|
564
|
+
/**
|
|
565
|
+
* Add or update an account in the accounts map
|
|
566
|
+
* If account exists, mark as declared if it wasn't already
|
|
567
|
+
*/
|
|
568
|
+
function addAccount(accountMap, name, declared, sourceUri, line) {
|
|
569
|
+
const existing = accountMap.get(name);
|
|
570
|
+
if (existing) {
|
|
571
|
+
// If we're adding a declared version, update the existing entry
|
|
572
|
+
if (declared && !existing.declared) {
|
|
573
|
+
existing.declared = true;
|
|
574
|
+
if (sourceUri !== undefined)
|
|
575
|
+
existing.sourceUri = sourceUri;
|
|
576
|
+
if (line !== undefined)
|
|
577
|
+
existing.line = line;
|
|
307
578
|
}
|
|
308
579
|
}
|
|
309
|
-
|
|
580
|
+
else {
|
|
581
|
+
const acc = { name, declared };
|
|
582
|
+
if (sourceUri !== undefined) {
|
|
583
|
+
acc.sourceUri = sourceUri;
|
|
584
|
+
acc.line = line;
|
|
585
|
+
}
|
|
586
|
+
accountMap.set(name, acc);
|
|
587
|
+
}
|
|
310
588
|
}
|
|
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
|
-
}
|
|
589
|
+
/**
|
|
590
|
+
* Add or update a payee in the payees map
|
|
591
|
+
*/
|
|
592
|
+
function addPayee(payeeMap, name, declared, sourceUri, line) {
|
|
593
|
+
const existing = payeeMap.get(name);
|
|
594
|
+
if (existing) {
|
|
595
|
+
if (declared && !existing.declared) {
|
|
596
|
+
existing.declared = true;
|
|
597
|
+
if (sourceUri !== undefined)
|
|
598
|
+
existing.sourceUri = sourceUri;
|
|
599
|
+
if (line !== undefined)
|
|
600
|
+
existing.line = line;
|
|
327
601
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
p.sourceUri = sourceUri;
|
|
335
|
-
p.line = i;
|
|
336
|
-
}
|
|
337
|
-
payeeMap.set(header.payee, p);
|
|
338
|
-
}
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
const p = { name, declared };
|
|
605
|
+
if (sourceUri !== undefined) {
|
|
606
|
+
p.sourceUri = sourceUri;
|
|
607
|
+
p.line = line;
|
|
339
608
|
}
|
|
609
|
+
payeeMap.set(name, p);
|
|
340
610
|
}
|
|
341
|
-
return Array.from(payeeMap.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
342
611
|
}
|
|
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;
|
|
612
|
+
/**
|
|
613
|
+
* Add or update a commodity in the commodities map
|
|
614
|
+
* When merging formats, prefer the one with higher precision or more detail
|
|
615
|
+
*/
|
|
616
|
+
function addCommodity(commodityMap, name, declared, format, sourceUri, line) {
|
|
617
|
+
const existing = commodityMap.get(name);
|
|
618
|
+
if (existing) {
|
|
619
|
+
if (declared) {
|
|
620
|
+
existing.declared = true;
|
|
621
|
+
if (format)
|
|
622
|
+
existing.format = format;
|
|
623
|
+
if (sourceUri !== undefined)
|
|
624
|
+
existing.sourceUri = sourceUri;
|
|
625
|
+
if (line !== undefined)
|
|
626
|
+
existing.line = line;
|
|
375
627
|
}
|
|
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
|
-
}
|
|
628
|
+
else if (format && !existing.declared) {
|
|
629
|
+
// For undeclared commodities, keep the format with better precision
|
|
630
|
+
const newPrecision = format.precision ?? null;
|
|
631
|
+
const existingPrecision = existing.format?.precision ?? null;
|
|
632
|
+
if (!existing.format || (newPrecision !== null && (existingPrecision === null || newPrecision > existingPrecision))) {
|
|
633
|
+
existing.format = format;
|
|
401
634
|
}
|
|
402
|
-
thousandsSeparator = pick;
|
|
403
635
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
spaceBetween = /\s/.test(between.replace(stripQuotes(leftRaw), '')) || /\s/.test(leftRaw.slice(-1));
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
const c = { name, declared, format };
|
|
639
|
+
if (sourceUri !== undefined) {
|
|
640
|
+
c.sourceUri = sourceUri;
|
|
641
|
+
c.line = line;
|
|
411
642
|
}
|
|
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
|
-
}
|
|
643
|
+
commodityMap.set(name, c);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Add or update a tag in the tags map
|
|
648
|
+
*/
|
|
649
|
+
function addTag(tagMap, name, declared, sourceUri, line) {
|
|
650
|
+
const existing = tagMap.get(name);
|
|
651
|
+
if (existing) {
|
|
652
|
+
if (declared && !existing.declared) {
|
|
653
|
+
existing.declared = true;
|
|
654
|
+
if (sourceUri !== undefined)
|
|
655
|
+
existing.sourceUri = sourceUri;
|
|
656
|
+
if (line !== undefined)
|
|
657
|
+
existing.line = line;
|
|
491
658
|
}
|
|
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
|
-
}
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
const t = { name, declared };
|
|
662
|
+
if (sourceUri !== undefined) {
|
|
663
|
+
t.sourceUri = sourceUri;
|
|
664
|
+
t.line = line;
|
|
515
665
|
}
|
|
666
|
+
tagMap.set(name, t);
|
|
516
667
|
}
|
|
517
|
-
return Array.from(commodityMap.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
518
668
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
669
|
+
/**
|
|
670
|
+
* Process an account directive and add it to the accounts map
|
|
671
|
+
*/
|
|
672
|
+
function processAccountDirective(line, accountMap, sourceUri, lineNumber) {
|
|
673
|
+
const accountName = line.trim().substring(8).split(';')[0].trim();
|
|
674
|
+
if (accountName) {
|
|
675
|
+
addAccount(accountMap, accountName, true, sourceUri, lineNumber);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Process a payee directive and add it to the payees map
|
|
680
|
+
*/
|
|
681
|
+
function processPayeeDirective(line, payeeMap, sourceUri, lineNumber) {
|
|
682
|
+
const payeeName = line.trim().substring(6).split(';')[0].trim();
|
|
683
|
+
if (payeeName) {
|
|
684
|
+
addPayee(payeeMap, payeeName, true, sourceUri, lineNumber);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Process a commodity directive and add it to the commodities map
|
|
689
|
+
* Handles multi-line commodity directives with format subdirectives
|
|
690
|
+
*/
|
|
691
|
+
function processCommodityDirective(lines, startLine, commodityMap, sourceUri) {
|
|
692
|
+
const line = lines[startLine];
|
|
693
|
+
const parsed = parseCommodityDirective(line);
|
|
694
|
+
if (!parsed)
|
|
695
|
+
return startLine;
|
|
696
|
+
let commodityName = parsed.name;
|
|
697
|
+
let format = parsed.format;
|
|
698
|
+
// Check for format subdirective on following lines
|
|
699
|
+
let look = startLine + 1;
|
|
700
|
+
while (look < lines.length) {
|
|
701
|
+
const next = lines[look];
|
|
702
|
+
if (!next.trim()) {
|
|
703
|
+
look++;
|
|
704
|
+
continue;
|
|
535
705
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
t.sourceUri = sourceUri;
|
|
545
|
-
t.line = i;
|
|
546
|
-
}
|
|
547
|
-
tagMap.set(k, t);
|
|
548
|
-
}
|
|
706
|
+
if (!/^\s+/.test(next))
|
|
707
|
+
break;
|
|
708
|
+
const subParsed = parseFormatSubDirective(next);
|
|
709
|
+
if (subParsed) {
|
|
710
|
+
if (subParsed.format)
|
|
711
|
+
format = subParsed.format;
|
|
712
|
+
if (subParsed.name && subParsed.name !== '' && (!commodityName || commodityName === '' || commodityName === subParsed.name)) {
|
|
713
|
+
commodityName = subParsed.name;
|
|
549
714
|
}
|
|
550
715
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
716
|
+
look++;
|
|
717
|
+
}
|
|
718
|
+
addCommodity(commodityMap, commodityName, true, format, sourceUri, startLine);
|
|
719
|
+
// Return the last line we processed
|
|
720
|
+
return look - 1;
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Process a tag directive and add it to the tags map
|
|
724
|
+
*/
|
|
725
|
+
function processTagDirective(line, tagMap, sourceUri, lineNumber) {
|
|
726
|
+
const tagName = line.trim().substring(4).split(';')[0].trim();
|
|
727
|
+
if (tagName) {
|
|
728
|
+
addTag(tagMap, tagName, true, sourceUri, lineNumber);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Extract accounts, commodities, tags from a transaction and add them to the maps
|
|
733
|
+
*/
|
|
734
|
+
function processTransaction(transaction, accountMap, commodityMap, tagMap, sourceUri) {
|
|
735
|
+
// Extract payee is handled separately since it's in the transaction header
|
|
736
|
+
// Extract accounts and commodities from postings
|
|
737
|
+
for (const posting of transaction.postings) {
|
|
738
|
+
// Add account
|
|
739
|
+
if (posting.account) {
|
|
740
|
+
addAccount(accountMap, posting.account, false, sourceUri);
|
|
565
741
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
742
|
+
// Add commodity from amount
|
|
743
|
+
if (posting.amount?.commodity && posting.amount.commodity !== '') {
|
|
744
|
+
addCommodity(commodityMap, posting.amount.commodity, false, undefined, sourceUri);
|
|
745
|
+
}
|
|
746
|
+
// Add commodity from cost
|
|
747
|
+
if (posting.cost?.amount?.commodity && posting.cost.amount.commodity !== '') {
|
|
748
|
+
addCommodity(commodityMap, posting.cost.amount.commodity, false, undefined, sourceUri);
|
|
749
|
+
}
|
|
750
|
+
// Add commodity from balance assertion
|
|
751
|
+
if (posting.assertion?.commodity && posting.assertion.commodity !== '') {
|
|
752
|
+
addCommodity(commodityMap, posting.assertion.commodity, false, undefined, sourceUri);
|
|
753
|
+
}
|
|
754
|
+
// Extract tags from posting comments
|
|
755
|
+
if (posting.tags) {
|
|
756
|
+
for (const tagName of Object.keys(posting.tags)) {
|
|
757
|
+
addTag(tagMap, tagName, false, sourceUri);
|
|
579
758
|
}
|
|
580
759
|
}
|
|
581
760
|
}
|
|
582
|
-
|
|
761
|
+
// Extract tags from transaction-level tags
|
|
762
|
+
if (transaction.tags) {
|
|
763
|
+
for (const tagName of Object.keys(transaction.tags)) {
|
|
764
|
+
addTag(tagMap, tagName, false, sourceUri);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
583
767
|
}
|
|
584
768
|
function parseDirective(line) {
|
|
585
769
|
const trimmed = line.trim();
|