1 files changed,
24 insertions(+),
65 deletions(-)
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed at:
2026-09-10 16:55:48 +0300
Authored at:
2026-09-10 14:45:46 +0300
Change ID:
vmsmwxvkquvtsktzrqyzszlkzlsmwmzy
Parent:
d3fcad3
M
journal/parser/parser.go
··· 208 208 p.skipWhitespace() 209 209 210 210 pt := &ast.PeriodicTransaction{} 211 - 212 211 pt.Period = p.parsePeriod() 213 - 214 212 if desc, dspan := p.parseOptPeriodicDescription(); desc != "" { 215 213 pt.Description = desc 216 214 pt.DescriptionSpan = dspan 217 215 } 218 216 219 - comment := p.parseOptInlineComment() 217 + pt.Comment = p.parseOptInlineComment() 220 218 p.expectNewline() 221 219 222 220 pt.HeaderComments, pt.Postings = p.parseHeaderCommentsAndPostings() 223 - 224 221 pt.Span = p.span(s) 225 - pt.Comment = comment 226 222 return pt 227 223 } 228 224 ··· 234 230 at := &ast.AutomatedTransaction{} 235 231 236 232 // expression 237 - sd := p.cur.Span 233 + expSpan := p.cur.Span 238 234 expr := p.parseDirectiveExpr() 239 235 at.Expr = expr 240 - at.ExprSpan = p.span(sd) 236 + at.ExprSpan = p.span(expSpan) 241 237 at.Comment = p.parseOptInlineComment() 242 238 p.expectNewline() 243 239 ··· 267 263 s := p.cur.Span 268 264 269 265 var periodBuf strings.Builder 270 - 271 266 for !p.got(token.NEWLINE) && !p.got(token.EOF) && 272 267 !p.got(token.SEMICOLON) && !p.got(token.HASH) && !p.got(token.PERCENT) && !p.got(token.STAR) { 273 268 ··· 289 284 290 285 str := periodBuf.String() 291 286 period := ast.Period{Raw: str, Span: p.span(s)} 292 - 293 287 if _, after, ok := strings.Cut(str, " from "); ok { 294 288 end := strings.Index(after, " ") 295 289 dateStr := after ··· 318 312 return period 319 313 } 320 314 321 -// periodDateSpan returns the source span of dateStr, which occurs in the 322 -// period text at or after searchFrom. The period span and text cover the same 323 -// bytes, so offsets line up 1:1. 324 315 func periodDateSpan(period ast.Period, text, dateStr string, searchFrom int) token.Span { 325 316 off := strings.Index(text[searchFrom:], dateStr) 326 317 abs := period.Span.Start.Offset + searchFrom + off ··· 482 473 p.expectNewline() 483 474 blockComments = append(blockComments, c) 484 475 case p.got(token.TEXT) && isCommentMarker(p.cur.Literal): 485 - // '#', '%' and '*' lex as TEXT in directive mode; treat them as comment lines 486 476 blockComments = append(blockComments, p.parseTextComment()) 487 477 case p.got(token.TEXT): 488 478 p.errorf("unknown subdirective %q", p.cur.Literal) ··· 493 483 } 494 484 } 495 485 496 - cd := &ast.CommodityDirective{ 486 + return &ast.CommodityDirective{ 497 487 Commodity: commodity, 498 488 CommoditySpan: commoditySpan, 499 489 FormatSub: format, ··· 501 491 Comment: comment, 502 492 Span: p.span(s), 503 493 } 504 - return cd 505 494 } 506 495 507 496 func (p *Parser) parseIncludeDirective() *ast.IncludeDirective { ··· 510 499 p.skipWhitespace() 511 500 512 501 id := &ast.IncludeDirective{} 513 - 514 502 if p.got(token.TEXT) { 515 503 id.Path = p.cur.Literal 516 504 p.advance() 517 505 } else { 518 506 p.errorf("expected file path, got %s", p.cur.Type) 519 507 } 520 - 521 508 id.Comment = p.parseOptInlineComment() 522 509 p.expectNewline() 523 510 id.Span = p.span(s) ··· 545 532 p.expect(token.PAYEE) 546 533 p.skipWhitespace() 547 534 548 - name, nameSpan := "", token.Span{} 535 + pd := &ast.PayeeDirective{} 549 536 if p.got(token.TEXT) || p.got(token.STRING) || p.got(token.COMMODITYMARK) { 550 - name, nameSpan = p.parsePayee() 537 + pd.Name, pd.NameSpan = p.parsePayee() 551 538 } 552 - 553 - comment := p.parseOptInlineComment() 539 + pd.Comment = p.parseOptInlineComment() 554 540 p.expectNewline() 555 - 556 - return &ast.PayeeDirective{ 557 - Name: name, 558 - NameSpan: nameSpan, 559 - Comment: comment, 560 - Span: p.span(s), 561 - } 541 + pd.Span = p.span(s) 542 + return pd 562 543 } 563 544 564 545 func (p *Parser) parseTagDirective() *ast.TagDirective { ··· 566 547 p.expect(token.TAG) 567 548 p.skipWhitespace() 568 549 569 - name := "" 550 + td := &ast.TagDirective{} 570 551 if p.got(token.TEXT) || p.got(token.COMMODITYMARK) || p.got(token.STRING) { 571 - name = unquote(p.cur.Literal) 552 + td.Name = unquote(p.cur.Literal) 572 553 p.advance() 573 554 } 574 - 575 - comment := p.parseOptInlineComment() 555 + td.Comment = p.parseOptInlineComment() 576 556 p.expectNewline() 577 - 578 - return &ast.TagDirective{ 579 - Name: name, 580 - Comment: comment, 581 - Span: p.span(s), 582 - } 557 + td.Span = p.span(s) 558 + return td 583 559 } 584 560 585 561 func (p *Parser) parseYearDirective() *ast.YearDirective { ··· 1098 1074 s := p.cur.Span 1099 1075 acc := ast.Account{Name: make([]ast.SubAccount, 0, 6)} 1100 1076 1101 - sub, ok := p.readAccountSegment() 1077 + seg, ok := p.readAccountSegment() 1102 1078 if !ok { 1103 1079 p.errorf("expected account, got %s", p.cur.Type) 1104 1080 return ast.Account{} 1105 1081 } 1106 - acc.Name = append(acc.Name, sub) 1082 + acc.Name = append(acc.Name, seg) 1107 1083 1108 1084 for p.got(token.COLON) { 1109 1085 p.advance() 1110 - sub, ok := p.readAccountSegment() 1086 + seg, ok := p.readAccountSegment() 1111 1087 if !ok { 1112 1088 break 1113 1089 } 1114 - acc.Name = append(acc.Name, sub) 1090 + acc.Name = append(acc.Name, seg) 1115 1091 } 1116 1092 1117 1093 acc.Span = p.span(s) ··· 1145 1121 return p.parseCommentRest(p.cur.Span) 1146 1122 } 1147 1123 1148 -// parseCommentRest consumes a comment marker at p.cur, then optional text; 1149 -// s anchors the span at the marker's start. 1150 1124 func (p *Parser) parseCommentRest(s token.Span) *ast.Comment { 1151 - marker := p.cur.Literal[0] 1125 + c := &ast.Comment{} 1126 + c.Marker = p.cur.Literal[0] 1152 1127 p.advance() 1153 1128 p.skipWhitespace() 1154 - 1155 - var tags []ast.Tag 1156 - text := "" 1157 1129 if p.got(token.TEXT) { 1158 - text = p.cur.Literal 1159 - tags = parseCommentTags(text, p.cur.Span) 1130 + c.Text = p.cur.Literal 1131 + c.Tags = parseCommentTags(c.Text, p.cur.Span) 1160 1132 p.advance() 1161 1133 } 1162 - 1163 - return &ast.Comment{ 1164 - Marker: marker, 1165 - Tags: tags, 1166 - Text: text, 1167 - Span: p.span(s), 1168 - } 1134 + c.Span = p.span(s) 1135 + return c 1169 1136 } 1170 1137 1171 1138 func (p *Parser) parseOptPeriodicDescription() (string, token.Span) { ··· 1174 1141 } 1175 1142 1176 1143 p.skipWhitespace() 1177 - 1178 1144 if p.cur.Type != token.TEXT { 1179 1145 return "", token.Span{} 1180 1146 } ··· 1211 1177 lit := p.cur.Literal 1212 1178 p.advance() 1213 1179 1214 - // detect format metadata before normalizing 1215 1180 amt.QuantityFmt = detectFormat(lit) 1216 - 1217 - // normalize for decimal.NewFromString 1218 - // remove thousands separators, replace decimal mark with '.' 1219 1181 normalized := normalizeLiteral(lit, amt.QuantityFmt.Thousands, amt.QuantityFmt.Decimal) 1220 - 1221 1182 q, err := decimal.FromString(normalized) 1222 1183 if err != nil { 1223 1184 p.errorf("invalid quantity %q: %v", lit, err) ··· 1255 1216 1256 1217 func (p *Parser) got(kind token.Type) bool { return p.cur.Type == kind } 1257 1218 func (p *Parser) willGet(kind token.Type) bool { return p.peek.Type == kind } 1258 - 1259 1219 func (p *Parser) expect(kind token.Type) (token.Token, bool) { 1260 1220 if p.got(kind) { 1261 1221 return p.advance(), true ··· 1271 1231 }) 1272 1232 } 1273 1233 1274 -// errorfAt records a parse error pointing at the start of span. 1275 1234 func (p *Parser) errorfAt(span token.Span, format string, args ...any) { 1276 1235 p.errors = append(p.errors, &ast.ParseError{ 1277 1236 Span: token.Span{File: span.File, Start: span.Start, End: span.Start},