Skip to content

Commit 7c0976d

Browse files
committed
feat: support for inline and subdirective comments for price directives
This part of the price directive syntax was previously overlooked. Journals containing price directive comments can now be parsed without producing errors. closes #25
1 parent 6cfbf9a commit 7c0976d

File tree

9 files changed

+353
-7
lines changed

9 files changed

+353
-7
lines changed

diagram.html

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,53 @@
366366
"name": "amount",
367367
"idx": 0
368368
},
369+
{
370+
"type": "Option",
371+
"idx": 0,
372+
"definition": [
373+
{
374+
"type": "NonTerminal",
375+
"name": "inlineComment",
376+
"idx": 0
377+
}
378+
]
379+
},
380+
{
381+
"type": "Terminal",
382+
"name": "NEWLINE",
383+
"label": "NEWLINE",
384+
"idx": 0,
385+
"pattern": "(\\r\\n|\\r|\\n)"
386+
},
387+
{
388+
"type": "Repetition",
389+
"idx": 0,
390+
"definition": [
391+
{
392+
"type": "NonTerminal",
393+
"name": "priceDirectiveContentLine",
394+
"idx": 0
395+
}
396+
]
397+
}
398+
]
399+
},
400+
{
401+
"type": "Rule",
402+
"name": "priceDirectiveContentLine",
403+
"orgText": "",
404+
"definition": [
405+
{
406+
"type": "Terminal",
407+
"name": "INDENT",
408+
"label": "INDENT",
409+
"idx": 0
410+
},
411+
{
412+
"type": "NonTerminal",
413+
"name": "inlineComment",
414+
"idx": 0
415+
},
369416
{
370417
"type": "Terminal",
371418
"name": "NEWLINE",

src/__tests__/cst_to_raw_visitor/price_directive.test.ts

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,65 @@ test('returns a price directive object', (t) => {
2929
commodity: 'CAD',
3030
value: '10CAD',
3131
sign: undefined
32-
}
32+
},
33+
comments: undefined,
34+
contentLines: []
3335
},
3436
'should correctly return all price directive fields'
3537
);
3638
});
39+
40+
test('returns a price directive object with inline comment', (t) => {
41+
const cstResult = parseLedgerToCST(`P 1900/01/01 $ 10CAD ; comment\n`);
42+
43+
assertNoLexingOrParsingErrors(t, cstResult);
44+
45+
const result = CstToRawVisitor.journal(cstResult.cstJournal.children);
46+
47+
t.is(result.length, 1, 'should modify a price directive');
48+
t.is(result[0].type, 'priceDirective', 'should be a priceDirective object');
49+
50+
const priceDirective = result[0] as Raw.PriceDirective;
51+
52+
t.truthy(
53+
priceDirective.value.comments,
54+
'price directive should contain an inline comment'
55+
);
56+
t.is(
57+
priceDirective.value.comments?.value[0],
58+
'comment',
59+
'price directive should contain the correct inline comment text'
60+
);
61+
});
62+
63+
test('returns a price directive object with subdirective comments', (t) => {
64+
const cstResult = parseLedgerToCST(`P 1900/01/01 $ 10CAD
65+
; subdirective comment
66+
; more subdirective comments
67+
`);
68+
69+
assertNoLexingOrParsingErrors(t, cstResult);
70+
71+
const result = CstToRawVisitor.journal(cstResult.cstJournal.children);
72+
73+
t.is(result.length, 1, 'should modify a price directive');
74+
t.is(result[0].type, 'priceDirective', 'should be a priceDirective object');
75+
76+
const priceDirective = result[0] as Raw.PriceDirective;
77+
78+
t.is(
79+
priceDirective.value.contentLines.length,
80+
2,
81+
'price directive should contain two subdirective comments'
82+
);
83+
t.is(
84+
priceDirective.value.contentLines[0].value.inlineComment.value[0],
85+
'subdirective comment',
86+
'price directive should contain the correct subdirective comment text'
87+
);
88+
t.is(
89+
priceDirective.value.contentLines[1].value.inlineComment.value[0],
90+
'more subdirective comments',
91+
'price directive should contain the correct subdirective comment text'
92+
);
93+
});

src/__tests__/lexer/price_directives.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,20 @@ test(
140140
'p 1900/01/01 USD -$1',
141141
[]
142142
);
143+
144+
test(
145+
'recognizes an inline comment',
146+
macro,
147+
'P 2024.01.03 USD $1.37 ; comment\n',
148+
[
149+
'PDirective',
150+
'SimpleDate',
151+
{ PDirectiveCommodityText: 'USD' },
152+
{ CommodityText: '$' },
153+
'Number',
154+
'AMOUNT_WS',
155+
'SemicolonComment',
156+
'InlineCommentText',
157+
'NEWLINE'
158+
]
159+
);

src/__tests__/parser/price_directive.test.ts

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ import anyTest, { TestFn } from 'ava';
22

33
import {
44
CommodityText,
5+
INDENT,
6+
InlineCommentText,
57
JournalNumber,
68
NEWLINE,
79
PDirective,
810
PDirectiveCommodityText,
11+
SemicolonComment,
912
SimpleDate
1013
} from '../../lib/lexer/tokens';
1114
import HLedgerParser from '../../lib/parser';
@@ -88,3 +91,169 @@ test('does not parse a price directive line without a date', (t) => {
8891

8992
t.falsy(HLedgerParser.priceDirective(), '<priceDirective!> P € $1.50\\n');
9093
});
94+
95+
test('parses a price directive with an inline comment', (t) => {
96+
t.context.lexer
97+
.addToken(PDirective, 'P')
98+
.addToken(SimpleDate, '2024.01.01')
99+
.addToken(PDirectiveCommodityText, '€')
100+
.addToken(CommodityText, '¥')
101+
.addToken(JournalNumber, '15000')
102+
.addToken(SemicolonComment, ';')
103+
.addToken(InlineCommentText, 'a comment')
104+
.addToken(NEWLINE, '\n');
105+
HLedgerParser.input = t.context.lexer.tokenize();
106+
107+
t.deepEqual(
108+
simplifyCst(HLedgerParser.priceDirective()),
109+
{
110+
PDirective: 1,
111+
SimpleDate: 1,
112+
PDirectiveCommodityText: 1,
113+
NEWLINE: 1,
114+
amount: [
115+
{
116+
CommodityText: 1,
117+
Number: 1
118+
}
119+
],
120+
inlineComment: [
121+
{
122+
SemicolonComment: 1,
123+
inlineCommentItem: [
124+
{
125+
InlineCommentText: 1
126+
}
127+
]
128+
}
129+
]
130+
},
131+
'<priceDirective> P 2024.01.01 € ¥15000 ; a comment\\n'
132+
);
133+
});
134+
135+
test('parses a price directive with an inline comment and subdirective comment', (t) => {
136+
t.context.lexer
137+
.addToken(PDirective, 'P')
138+
.addToken(SimpleDate, '2024.01.01')
139+
.addToken(PDirectiveCommodityText, '€')
140+
.addToken(CommodityText, '¥')
141+
.addToken(JournalNumber, '15000')
142+
.addToken(SemicolonComment, ';')
143+
.addToken(InlineCommentText, 'a comment')
144+
.addToken(NEWLINE, '\n')
145+
.addToken(INDENT, ' ')
146+
.addToken(SemicolonComment, ';')
147+
.addToken(InlineCommentText, 'subdirective comment')
148+
.addToken(NEWLINE, '\n');
149+
HLedgerParser.input = t.context.lexer.tokenize();
150+
151+
t.deepEqual(
152+
simplifyCst(HLedgerParser.priceDirective()),
153+
{
154+
PDirective: 1,
155+
SimpleDate: 1,
156+
PDirectiveCommodityText: 1,
157+
NEWLINE: 1,
158+
amount: [
159+
{
160+
CommodityText: 1,
161+
Number: 1
162+
}
163+
],
164+
inlineComment: [
165+
{
166+
SemicolonComment: 1,
167+
inlineCommentItem: [
168+
{
169+
InlineCommentText: 1
170+
}
171+
]
172+
}
173+
],
174+
priceDirectiveContentLine: [
175+
{
176+
INDENT: 1,
177+
NEWLINE: 1,
178+
inlineComment: [
179+
{
180+
SemicolonComment: 1,
181+
inlineCommentItem: [
182+
{
183+
InlineCommentText: 1
184+
}
185+
]
186+
}
187+
]
188+
}
189+
]
190+
},
191+
'<priceDirective> P 2024.01.01 € ¥15000 ; a comment\\n ; subdirective comment\\n'
192+
);
193+
});
194+
195+
test('parses a price directive with several subdirective comments', (t) => {
196+
t.context.lexer
197+
.addToken(PDirective, 'P')
198+
.addToken(SimpleDate, '2024.01.01')
199+
.addToken(PDirectiveCommodityText, '€')
200+
.addToken(CommodityText, '¥')
201+
.addToken(JournalNumber, '15000')
202+
.addToken(NEWLINE, '\n')
203+
.addToken(INDENT, ' ')
204+
.addToken(SemicolonComment, ';')
205+
.addToken(InlineCommentText, 'subdirective comment')
206+
.addToken(NEWLINE, '\n')
207+
.addToken(INDENT, ' ')
208+
.addToken(SemicolonComment, ';')
209+
.addToken(InlineCommentText, 'another comment')
210+
.addToken(NEWLINE, '\n');
211+
HLedgerParser.input = t.context.lexer.tokenize();
212+
213+
t.deepEqual(
214+
simplifyCst(HLedgerParser.priceDirective()),
215+
{
216+
PDirective: 1,
217+
SimpleDate: 1,
218+
PDirectiveCommodityText: 1,
219+
NEWLINE: 1,
220+
amount: [
221+
{
222+
CommodityText: 1,
223+
Number: 1
224+
}
225+
],
226+
priceDirectiveContentLine: [
227+
{
228+
INDENT: 1,
229+
NEWLINE: 1,
230+
inlineComment: [
231+
{
232+
SemicolonComment: 1,
233+
inlineCommentItem: [
234+
{
235+
InlineCommentText: 1
236+
}
237+
]
238+
}
239+
]
240+
},
241+
{
242+
INDENT: 1,
243+
NEWLINE: 1,
244+
inlineComment: [
245+
{
246+
SemicolonComment: 1,
247+
inlineCommentItem: [
248+
{
249+
InlineCommentText: 1
250+
}
251+
]
252+
}
253+
]
254+
}
255+
]
256+
},
257+
'<priceDirective> P 2024.01.01 € ¥15000\\n ; subdirective comment\\n ; another comment\\n'
258+
);
259+
});

src/lib/hledger_cst.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,19 @@ export type PriceDirectiveCstChildren = {
9191
SimpleDate: IToken[];
9292
PDirectiveCommodityText: IToken[];
9393
amount: AmountCstNode[];
94+
inlineComment?: InlineCommentCstNode[];
95+
NEWLINE: IToken[];
96+
priceDirectiveContentLine?: PriceDirectiveContentLineCstNode[];
97+
};
98+
99+
export interface PriceDirectiveContentLineCstNode extends CstNode {
100+
name: "priceDirectiveContentLine";
101+
children: PriceDirectiveContentLineCstChildren;
102+
}
103+
104+
export type PriceDirectiveContentLineCstChildren = {
105+
INDENT: IToken[];
106+
inlineComment: InlineCommentCstNode[];
94107
NEWLINE: IToken[];
95108
};
96109

@@ -380,6 +393,7 @@ export interface ICstNodeVisitor<IN, OUT> extends ICstVisitor<IN, OUT> {
380393
journalItem(children: JournalItemCstChildren, param?: IN): OUT;
381394
transaction(children: TransactionCstChildren, param?: IN): OUT;
382395
priceDirective(children: PriceDirectiveCstChildren, param?: IN): OUT;
396+
priceDirectiveContentLine(children: PriceDirectiveContentLineCstChildren, param?: IN): OUT;
383397
accountDirective(children: AccountDirectiveCstChildren, param?: IN): OUT;
384398
accountDirectiveContentLine(children: AccountDirectiveContentLineCstChildren, param?: IN): OUT;
385399
transactionInitLine(children: TransactionInitLineCstChildren, param?: IN): OUT;

src/lib/lexer/modes.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,8 @@ export const price_amounts_mode = [
146146
JournalNumber,
147147
CommodityText,
148148
DASH,
149-
PLUS
149+
PLUS,
150+
SemicolonComment
150151
];
151152

152153
export const memo_mode = [SemicolonComment, NEWLINE, Memo];

src/lib/parser.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,19 +110,28 @@ class HLedgerParser extends CstParser {
110110

111111
public transaction = this.RULE('transaction', () => {
112112
this.SUBRULE(this.transactionInitLine);
113-
this.MANY(() => {
114-
this.SUBRULE(this.transactionContentLine);
115-
});
113+
this.MANY(() => this.SUBRULE(this.transactionContentLine));
116114
});
117115

118116
public priceDirective = this.RULE('priceDirective', () => {
119117
this.CONSUME(PDirective);
120118
this.CONSUME(SimpleDate);
121119
this.CONSUME(PDirectiveCommodityText);
122120
this.SUBRULE(this.amount);
123-
this.CONSUME(NEWLINE); // TODO: There is support for inline comments prior to NEWLINE.
121+
this.OPTION(() => this.SUBRULE(this.inlineComment));
122+
this.CONSUME(NEWLINE);
123+
this.MANY(() => this.SUBRULE(this.priceDirectiveContentLine));
124124
});
125125

126+
public priceDirectiveContentLine = this.RULE(
127+
'priceDirectiveContentLine',
128+
() => {
129+
this.CONSUME(INDENT);
130+
this.SUBRULE(this.inlineComment);
131+
this.CONSUME(NEWLINE);
132+
}
133+
);
134+
126135
public accountDirective = this.RULE('accountDirective', () => {
127136
this.CONSUME(AccountDirective);
128137
this.CONSUME(AccountName);

0 commit comments

Comments
 (0)