1#![forbid(unsafe_code)]
34#![warn(missing_docs)]
35#![allow(clippy::missing_const_for_fn)]
37
38mod convert;
40mod editor;
41mod helpers;
42mod utils;
43
44pub mod types;
46
47mod api;
49mod parsed_ledger;
50
51pub use api::{balances, format, parse, query, validate_source, version};
53
54#[cfg(feature = "completions")]
55pub use api::bql_completions;
56
57#[cfg(feature = "plugins")]
58pub use api::{list_plugins, run_plugin};
59
60pub use api::expand_pads;
61pub use parsed_ledger::ParsedLedger;
62
63use wasm_bindgen::prelude::*;
64
65#[wasm_bindgen(typescript_custom_section)]
70const TS_TYPES: &'static str = r#"
71/** Error severity level. */
72export type Severity = 'error' | 'warning';
73
74/** Error with source location information. */
75export interface BeancountError {
76 message: string;
77 line?: number;
78 column?: number;
79 severity: Severity;
80}
81
82/** Amount with number and currency. */
83export interface Amount {
84 number: string;
85 currency: string;
86}
87
88/** Posting cost specification. */
89export interface PostingCost {
90 number_per?: string;
91 currency?: string;
92 date?: string;
93 label?: string;
94}
95
96/** A posting within a transaction. */
97export interface Posting {
98 account: string;
99 units?: Amount;
100 cost?: PostingCost;
101 price?: Amount;
102}
103
104/** Base directive with date. */
105interface BaseDirective {
106 date: string;
107}
108
109/** Transaction directive. */
110export interface TransactionDirective extends BaseDirective {
111 type: 'transaction';
112 flag: string;
113 payee?: string;
114 narration?: string;
115 tags: string[];
116 links: string[];
117 postings: Posting[];
118}
119
120/** Balance assertion directive. */
121export interface BalanceDirective extends BaseDirective {
122 type: 'balance';
123 account: string;
124 amount: Amount;
125}
126
127/** Open account directive. */
128export interface OpenDirective extends BaseDirective {
129 type: 'open';
130 account: string;
131 currencies: string[];
132 booking?: string;
133}
134
135/** Close account directive. */
136export interface CloseDirective extends BaseDirective {
137 type: 'close';
138 account: string;
139}
140
141/** All directive types. */
142export type Directive =
143 | TransactionDirective
144 | BalanceDirective
145 | OpenDirective
146 | CloseDirective
147 | { type: 'commodity'; date: string; currency: string }
148 | { type: 'pad'; date: string; account: string; source_account: string }
149 | { type: 'event'; date: string; event_type: string; value: string }
150 | { type: 'note'; date: string; account: string; comment: string }
151 | { type: 'document'; date: string; account: string; path: string }
152 | { type: 'price'; date: string; currency: string; amount: Amount }
153 | { type: 'query'; date: string; name: string; query_string: string }
154 | { type: 'custom'; date: string; custom_type: string };
155
156/** Ledger options. */
157export interface LedgerOptions {
158 operating_currencies: string[];
159 title?: string;
160}
161
162/** Parsed ledger. */
163export interface Ledger {
164 directives: Directive[];
165 options: LedgerOptions;
166}
167
168/** Result of parsing a Beancount file. */
169export interface ParseResult {
170 ledger?: Ledger;
171 errors: BeancountError[];
172}
173
174/** Result of validation. */
175export interface ValidationResult {
176 valid: boolean;
177 errors: BeancountError[];
178}
179
180/** Cell value in query results. */
181export type CellValue =
182 | null
183 | string
184 | number
185 | boolean
186 | Amount
187 | { units: Amount; cost?: { number: string; currency: string; date?: string; label?: string } }
188 | { positions: Array<{ units: Amount }> }
189 | string[];
190
191/** Result of a BQL query. */
192export interface QueryResult {
193 columns: string[];
194 rows: CellValue[][];
195 errors: BeancountError[];
196}
197
198/** Result of formatting. */
199export interface FormatResult {
200 formatted?: string;
201 errors: BeancountError[];
202}
203
204/** Result of pad expansion. */
205export interface PadResult {
206 directives: Directive[];
207 padding_transactions: Directive[];
208 errors: BeancountError[];
209}
210
211/** Result of running a plugin. */
212export interface PluginResult {
213 directives: Directive[];
214 errors: BeancountError[];
215}
216
217/** Plugin information. */
218export interface PluginInfo {
219 name: string;
220 description: string;
221}
222
223/** BQL completion suggestion. */
224export interface Completion {
225 text: string;
226 category: string;
227 description?: string;
228}
229
230/** Result of BQL completion request. */
231export interface CompletionResult {
232 completions: Completion[];
233 context: string;
234}
235
236// =============================================================================
237// Editor Integration Types (LSP-like features)
238// =============================================================================
239
240/** The kind of a completion item. */
241export type EditorCompletionKind = 'keyword' | 'account' | 'accountsegment' | 'currency' | 'payee' | 'date' | 'text';
242
243/** A completion item for Beancount source editing. */
244export interface EditorCompletion {
245 label: string;
246 kind: EditorCompletionKind;
247 detail?: string;
248 insertText?: string;
249}
250
251/** Result of an editor completion request. */
252export interface EditorCompletionResult {
253 completions: EditorCompletion[];
254 context: string;
255}
256
257/** A range in the document. */
258export interface EditorRange {
259 start_line: number;
260 start_character: number;
261 end_line: number;
262 end_character: number;
263}
264
265/** Hover information for a symbol. */
266export interface EditorHoverInfo {
267 contents: string;
268 range?: EditorRange;
269}
270
271/** A location in the document. */
272export interface EditorLocation {
273 line: number;
274 character: number;
275}
276
277/** The kind of a symbol. */
278export type SymbolKind = 'transaction' | 'account' | 'balance' | 'commodity' | 'posting' | 'pad' | 'event' | 'note' | 'document' | 'price' | 'query' | 'custom';
279
280/** A document symbol for the outline view. */
281export interface EditorDocumentSymbol {
282 name: string;
283 detail?: string;
284 kind: SymbolKind;
285 range: EditorRange;
286 children?: EditorDocumentSymbol[];
287 deprecated?: boolean;
288}
289
290/** The kind of reference. */
291export type ReferenceKind = 'account' | 'currency' | 'payee';
292
293/** A reference to a symbol in the document. */
294export interface EditorReference {
295 range: EditorRange;
296 kind: ReferenceKind;
297 is_definition: boolean;
298 context?: string;
299}
300
301/** Result of a find-references request. */
302export interface EditorReferencesResult {
303 symbol: string;
304 kind: ReferenceKind;
305 references: EditorReference[];
306}
307
308/**
309 * A parsed and validated ledger that caches the parse result.
310 * Use this class when you need to perform multiple operations on the same
311 * source without re-parsing each time.
312 */
313export class ParsedLedger {
314 constructor(source: string);
315 free(): void;
316
317 /** Check if the ledger is valid (no parse or validation errors). */
318 isValid(): boolean;
319
320 /** Get all errors (parse + validation). */
321 getErrors(): BeancountError[];
322
323 /** Get parse errors only. */
324 getParseErrors(): BeancountError[];
325
326 /** Get validation errors only. */
327 getValidationErrors(): BeancountError[];
328
329 /** Get the parsed directives. */
330 getDirectives(): Directive[];
331
332 /** Get the ledger options. */
333 getOptions(): LedgerOptions;
334
335 /** Get the number of directives. */
336 directiveCount(): number;
337
338 /** Run a BQL query on this ledger. */
339 query(queryStr: string): QueryResult;
340
341 /** Get account balances (shorthand for query("BALANCES")). */
342 balances(): QueryResult;
343
344 /** Format the ledger source. */
345 format(): FormatResult;
346
347 /** Expand pad directives. */
348 expandPads(): PadResult;
349
350 /** Run a native plugin on this ledger. */
351 runPlugin(pluginName: string): PluginResult;
352
353 // =========================================================================
354 // Editor Integration (LSP-like features)
355 // =========================================================================
356
357 /** Get completions at the given position. */
358 getCompletions(line: number, character: number): EditorCompletionResult;
359
360 /** Get hover information at the given position. */
361 getHoverInfo(line: number, character: number): EditorHoverInfo | null;
362
363 /** Get the definition location for the symbol at the given position. */
364 getDefinition(line: number, character: number): EditorLocation | null;
365
366 /** Get all document symbols for the outline view. */
367 getDocumentSymbols(): EditorDocumentSymbol[];
368
369 /** Find all references to the symbol at the given position. */
370 getReferences(line: number, character: number): EditorReferencesResult | null;
371}
372"#;
373
374#[wasm_bindgen(start)]
383pub fn init() {
384 console_error_panic_hook::set_once();
386}
387
388#[cfg(test)]
393mod tests {
394 use super::*;
395 use rustledger_parser::parse as parse_beancount;
396 use rustledger_validate::validate as validate_ledger;
397
398 #[test]
399 fn test_parse_simple() {
400 let source = r#"
4012024-01-01 open Assets:Bank USD
402
4032024-01-15 * "Coffee Shop" "Morning coffee"
404 Expenses:Food:Coffee 5.00 USD
405 Assets:Bank -5.00 USD
406"#;
407
408 let result = parse_beancount(source);
409 assert!(result.errors.is_empty());
410 assert_eq!(result.directives.len(), 2);
411 }
412
413 #[test]
414 fn test_version() {
415 let v = version();
416 assert!(!v.is_empty());
417 }
418
419 #[test]
420 fn test_load_and_interpolate() {
421 use helpers::load_and_interpolate;
422
423 let source = r#"
4252024-01-01 open Assets:Bank USD
4262024-01-01 open Expenses:Food USD
427
4282024-01-15 * "Coffee"
429 Expenses:Food 5.00 USD
430 Assets:Bank -5.00 USD
431"#;
432 let load = load_and_interpolate(source);
433 assert!(load.errors.is_empty());
434 assert_eq!(load.directives.len(), 3);
435
436 let source = r#"
4382024-01-01 open Assets:Bank USD
439
4402024-01-15 * "Coffee"
441 Expenses:Food 5.00 USD
442 Assets:Bank -5.00 USD
443"#;
444 let load = load_and_interpolate(source);
445 assert!(load.errors.is_empty()); let validation_errors = validate_ledger(&load.directives);
447 assert!(
448 !validation_errors.is_empty(),
449 "should detect Expenses:Food not opened"
450 );
451 }
452}