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