beancount 0.0.31 → 0.2.0

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.
Files changed (71) hide show
  1. package/README.md +51 -22
  2. package/build/src/benchmark.mjs +1 -1
  3. package/build/src/classes/DatedNode.d.mts +40 -0
  4. package/build/src/classes/DatedNode.mjs +61 -0
  5. package/build/src/classes/Node.d.mts +92 -0
  6. package/build/src/classes/Node.mjs +107 -0
  7. package/build/src/classes/ParseResult.d.mts +75 -75
  8. package/build/src/classes/ParseResult.mjs +96 -98
  9. package/build/src/classes/nodes/Balance.d.mts +32 -0
  10. package/build/src/classes/nodes/Balance.mjs +50 -0
  11. package/build/src/classes/nodes/Blankline.d.mts +23 -0
  12. package/build/src/classes/nodes/Blankline.mjs +37 -0
  13. package/build/src/classes/nodes/Close.d.mts +20 -0
  14. package/build/src/classes/nodes/Close.mjs +31 -0
  15. package/build/src/classes/nodes/Comment.d.mts +25 -0
  16. package/build/src/classes/nodes/Comment.mjs +42 -0
  17. package/build/src/classes/nodes/Commodity.d.mts +20 -0
  18. package/build/src/classes/nodes/Commodity.mjs +31 -0
  19. package/build/src/classes/nodes/Custom.d.mts +23 -0
  20. package/build/src/classes/nodes/Custom.mjs +38 -0
  21. package/build/src/classes/nodes/Document.d.mts +22 -0
  22. package/build/src/classes/nodes/Document.mjs +34 -0
  23. package/build/src/classes/nodes/Event.d.mts +23 -0
  24. package/build/src/classes/nodes/Event.mjs +34 -0
  25. package/build/src/classes/nodes/Include.d.mts +20 -0
  26. package/build/src/classes/nodes/Include.mjs +31 -0
  27. package/build/src/classes/nodes/Note.d.mts +22 -0
  28. package/build/src/classes/nodes/Note.mjs +34 -0
  29. package/build/src/classes/nodes/Open.d.mts +27 -0
  30. package/build/src/classes/nodes/Open.mjs +66 -0
  31. package/build/src/classes/nodes/Option.d.mts +23 -0
  32. package/build/src/classes/nodes/Option.mjs +32 -0
  33. package/build/src/classes/nodes/Pad.d.mts +22 -0
  34. package/build/src/classes/nodes/Pad.mjs +33 -0
  35. package/build/src/classes/nodes/Plugin.d.mts +22 -0
  36. package/build/src/classes/nodes/Plugin.mjs +36 -0
  37. package/build/src/classes/nodes/Poptag.d.mts +21 -0
  38. package/build/src/classes/nodes/Poptag.mjs +34 -0
  39. package/build/src/classes/nodes/Price.d.mts +32 -0
  40. package/build/src/classes/nodes/Price.mjs +57 -0
  41. package/build/src/classes/nodes/Pushtag.d.mts +21 -0
  42. package/build/src/classes/nodes/Pushtag.mjs +34 -0
  43. package/build/src/classes/nodes/Query.d.mts +22 -0
  44. package/build/src/classes/nodes/Query.mjs +34 -0
  45. package/build/src/classes/nodes/Transaction/Posting.d.mts +59 -0
  46. package/build/src/classes/nodes/Transaction/Posting.mjs +97 -0
  47. package/build/src/classes/nodes/Transaction/Tag.d.mts +28 -0
  48. package/build/src/classes/nodes/Transaction/Tag.mjs +28 -0
  49. package/build/src/classes/nodes/Transaction/index.d.mts +70 -0
  50. package/build/src/classes/nodes/Transaction/index.mjs +193 -0
  51. package/build/src/classes/nodes/index.d.mts +19 -0
  52. package/build/src/classes/nodes/index.mjs +19 -0
  53. package/build/src/cli.mjs +4 -4
  54. package/build/src/deserialize.d.mts +54 -54
  55. package/build/src/deserialize.mjs +89 -89
  56. package/build/src/directiveTypes.d.mts +10 -0
  57. package/build/src/directiveTypes.mjs +29 -0
  58. package/build/src/genericParse.d.mts +27 -20
  59. package/build/src/genericParse.mjs +30 -30
  60. package/build/src/main.d.mts +30 -31
  61. package/build/src/main.mjs +30 -30
  62. package/build/src/nodeTypeToClass.d.mts +81 -0
  63. package/build/src/nodeTypeToClass.mjs +37 -0
  64. package/build/src/parse.d.mts +16 -16
  65. package/build/src/parse.mjs +37 -37
  66. package/build/src/parseFile.d.mts +2 -2
  67. package/build/src/parseFile.mjs +11 -11
  68. package/build/src/utils/splitStringIntoSourceFragments.d.ts +14 -0
  69. package/build/src/utils/splitStringIntoSourceFragments.js +48 -0
  70. package/build/tsconfig.build.tsbuildinfo +1 -1
  71. package/package.json +7 -7
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # beancount
2
2
 
3
+ [![CI](https://github.com/Boelensman1/node-beancount/actions/workflows/ci.yml/badge.svg)](https://github.com/Boelensman1/node-beancount/actions/workflows/ci.yml)
4
+
3
5
  A parser and editor for Beancount accounting files with full type safety.
4
6
 
5
7
  ## Installation
@@ -11,16 +13,18 @@ npm install beancount
11
13
  ## Features
12
14
 
13
15
  - **Full Beancount Support** - All directives supported: transactions, open/close, balance, pad, note, document, price, event, query, custom, commodity, include, option, plugin, pushtag/poptag
14
- - **Type-Safe** - Complete TypeScript types for all entries and components
15
- - **Rich Transaction Support** - Tags, links, metadata, costs, price annotations, and postings
16
+ - **Full Transaction Support** - Tags, links, metadata, costs, price annotations, and postings
17
+ - **Type-Safe** - Complete TypeScript types
18
+ - **Platform Agnostic** - Works in Node.js, browsers, Deno, and other JavaScript runtimes
16
19
  - **Round-Trip Parsing** - Parse to objects and serialize back to text
17
- - **Formatted Output** - Column-aligned output with `toFormattedString()` and CLI formatter
20
+ - **Recursive File Parsing** - Option to automatically follow and merge `include` directives with circular reference protection
21
+ - **Formatted Output** - Column-aligned output with `toFormattedString()`
18
22
  - **CLI Formatter** - Command-line tool `beancount-format` for formatting files with auto-aligned columns
19
23
 
20
24
  ## Quick Start
21
25
 
22
26
  ```typescript
23
- import { parse, ParseResult } from 'beancount'
27
+ import { parse } from './main.mjs'
24
28
 
25
29
  const beancountContent = `
26
30
  2024-01-01 open Assets:Checking USD
@@ -32,22 +36,21 @@ const beancountContent = `
32
36
 
33
37
  const result = parse(beancountContent)
34
38
 
35
- // Access parsed entries
36
- console.log(result.entries.length) // 2
37
-
38
- // Convert back to string
39
- console.log(result.toString())
39
+ // Access parsed directives
40
+ console.log(result.nodes.length) // 5
41
+ console.log(result.nodes)
40
42
 
41
- // Convert to JSON
42
- const resultJSON = JSON.stringify(result)
43
- console.log(resultJSON)
43
+ // Edit parsed directives
44
+ result.transactions[0].narration = 'Trip to the supermarket'
45
+ result.transactions[0].postings.forEach((p) => (p.currency = 'EUR'))
44
46
 
45
- // Convert back to parsed entries
46
- console.log(ParseResult.fromJSON(result))
47
-
48
- // Or with formatted output (aligned columns)
49
- // only partially implemented at this point
47
+ // Output formatted
50
48
  console.log(result.toFormattedString())
49
+ /* outputs:
50
+ 2024-01-15 * "Grocery Store" "Trip to the supermarket"
51
+ Assets:Checking -50.00 EUR
52
+ Expenses:Food 50.00 EUR
53
+ */
51
54
  ```
52
55
 
53
56
  ## Parsing Files
@@ -64,7 +67,37 @@ const result = await parseFile('/path/to/ledger.beancount')
64
67
  const result = await parseFile('/path/to/main.beancount', { recurse: true })
65
68
  ```
66
69
 
67
- When `recurse: true`, the parser follows all `include` directives and merges the entries from included files into the result. Circular includes are handled gracefully (each file is only parsed once).
70
+ When `recurse: true`, the parser follows all `include` directives and merges the directives from included files into the result. Circular includes are handled gracefully (each file is only parsed once).
71
+
72
+ ## Browser Usage
73
+
74
+ The library works in browsers and other non-Node.js environments. The core `parse()` function works everywhere. For `parseFile()`, provide custom file system helpers:
75
+
76
+ ```typescript
77
+ import { parseFile, type FileSystemHelpers } from 'beancount'
78
+
79
+ const browserFS: FileSystemHelpers = {
80
+ readFile: async (path) => {
81
+ const response = await fetch(path)
82
+ return response.text()
83
+ },
84
+ resolvePath: (path) => new URL(path, window.location.origin).pathname,
85
+ resolveRelative: (base, rel) => {
86
+ const baseDir = base.substring(0, base.lastIndexOf('/') + 1)
87
+ return new URL(rel, window.location.origin + baseDir).pathname
88
+ },
89
+ dirname: (path) => path.substring(0, path.lastIndexOf('/')),
90
+ }
91
+
92
+ const result = await parseFile('/api/ledger.beancount', {
93
+ recurse: true,
94
+ fs: browserFS,
95
+ })
96
+ ```
97
+
98
+ ## Documentation
99
+
100
+ Full API documentation is available at https://Boelensman1.github.io/node-beancount/
68
101
 
69
102
  ## CLI Usage
70
103
 
@@ -102,10 +135,6 @@ beancount-format -w income.beancount expenses.beancount
102
135
  beancount-format --currency-column 60 ledger.beancount
103
136
  ```
104
137
 
105
- ## Documentation
106
-
107
- Full API documentation is available at https://Boelensman1.github.io/node-beancount/
108
-
109
138
  ## Contributing
110
139
 
111
140
  This project uses Make for task orchestration. Common commands:
@@ -25,4 +25,4 @@ console.log(`Average parse time: ${avgDuration.toFixed(3)} ms`);
25
25
  console.log(`Min: ${minDuration.toFixed(3)} ms, Max: ${maxDuration.toFixed(3)} ms`);
26
26
  console.log(`Average throughput: ${(fileSizeKB / avgDurationSeconds / 1000).toFixed(2)} MB/s`);
27
27
  console.log('---');
28
- console.log(`Parsed ${lastResult.entries.length} items`);
28
+ console.log(`Parsed ${lastResult.nodes.length} items`);
@@ -0,0 +1,40 @@
1
+ import { Temporal } from '@js-temporal/polyfill';
2
+ import { Node } from './Node.mjs';
3
+ import { Value } from './Value.mjs';
4
+ import type { DatedDirectiveNodeType } from '../nodeTypeToClass.mjs';
5
+ /**
6
+ * Abstract base class for all nodes that correspond to dated Beancount directives
7
+ * like transaction and open.
8
+ *
9
+ * Child classes must implement static `fromGenericParseResult` method
10
+ */
11
+ export declare abstract class DatedNode extends Node {
12
+ /** The type of this dated node */
13
+ abstract type: DatedDirectiveNodeType;
14
+ /** The date of this node as a Temporal.PlainDate object */
15
+ date: Temporal.PlainDate;
16
+ /** Optional metadata key-value pairs associated with this node */
17
+ metadata?: Record<string, Value>;
18
+ /**
19
+ * Creates a new DatedNode instance.
20
+ * @param obj - Object containing node properties
21
+ * @param obj.date - The date string in YYYY-MM-DD format
22
+ */
23
+ constructor(obj: {
24
+ date: string | Temporal.PlainDate;
25
+ [key: string]: unknown;
26
+ });
27
+ /**
28
+ * Returns the common prefix for all DatedNode toString methods.
29
+ * Format: "YYYY-MM-DD <type>"
30
+ * @returns The formatted date and type prefix string
31
+ */
32
+ protected getDateTypePrefix(): string;
33
+ /**
34
+ * Converts metadata and comment to a formatted string.
35
+ * If metadata exists, each key-value pair is formatted on separate indented lines.
36
+ *
37
+ * @returns The formatted metadata and comment string, or empty string if neither exists
38
+ */
39
+ getMetaDataString(): string;
40
+ }
@@ -0,0 +1,61 @@
1
+ import { Temporal } from '@js-temporal/polyfill';
2
+ import { Node } from './Node.mjs';
3
+ import { Value } from './Value.mjs';
4
+ /**
5
+ * Abstract base class for all nodes that correspond to dated Beancount directives
6
+ * like transaction and open.
7
+ *
8
+ * Child classes must implement static `fromGenericParseResult` method
9
+ */
10
+ export class DatedNode extends Node {
11
+ /**
12
+ * Creates a new DatedNode instance.
13
+ * @param obj - Object containing node properties
14
+ * @param obj.date - The date string in YYYY-MM-DD format
15
+ */
16
+ constructor(obj) {
17
+ const { date, metadata, ...props } = obj;
18
+ super(props);
19
+ if (date) {
20
+ if (date instanceof Temporal.PlainDate) {
21
+ this.date = date;
22
+ }
23
+ else if (typeof date === 'string') {
24
+ this.date = Temporal.PlainDate.from(date, { overflow: 'reject' });
25
+ }
26
+ else {
27
+ throw new Error('Could not parse date, should be either string of Temporal.PlainDate');
28
+ }
29
+ }
30
+ if (metadata) {
31
+ this.metadata = Object.fromEntries(Object.entries(metadata).map(([key, val]) => [
32
+ key,
33
+ new Value(val),
34
+ ]));
35
+ }
36
+ }
37
+ /**
38
+ * Returns the common prefix for all DatedNode toString methods.
39
+ * Format: "YYYY-MM-DD <type>"
40
+ * @returns The formatted date and type prefix string
41
+ */
42
+ getDateTypePrefix() {
43
+ return `${this.date.toJSON()} ${this.type}`;
44
+ }
45
+ /**
46
+ * Converts metadata and comment to a formatted string.
47
+ * If metadata exists, each key-value pair is formatted on separate indented lines.
48
+ *
49
+ * @returns The formatted metadata and comment string, or empty string if neither exists
50
+ */
51
+ getMetaDataString() {
52
+ const comment = this.comment ? ` ; ${this.comment}` : '';
53
+ if (!this.metadata) {
54
+ return comment;
55
+ }
56
+ return (`${comment}\n` +
57
+ Object.entries(this.metadata)
58
+ .map(([key, val]) => ` ${key}: ${val.toString()}`)
59
+ .join('\n'));
60
+ }
61
+ }
@@ -0,0 +1,92 @@
1
+ import type { BeancountDirectiveNodeType } from '../nodeTypeToClass.mjs';
2
+ import { genericParse } from '../genericParse.mjs';
3
+ import { FormatOptions } from './ParseResult.mjs';
4
+ /**
5
+ * Type helper for Node class constructors that enforce the static factory methods.
6
+ * Child classes must implement static fromGenericParseResult and fromObject methods to create instances.
7
+ * Note: The constructor is protected, so only the static factory methods are part of the public API.
8
+ */
9
+ export interface NodeConstructor<T extends Node> {
10
+ /**
11
+ * Creates an Node instance from a generic parse result.
12
+ * @param parsedStart - The result from genericParse containing parsed node data
13
+ * @returns A new instance of the Node subclass
14
+ */
15
+ fromGenericParseResult(parsedStart: ReturnType<typeof genericParse>): T;
16
+ }
17
+ /**
18
+ * Abstract base class for all nodes.
19
+ *
20
+ * Child classes must implement static `fromGenericParseResult` method
21
+ */
22
+ export declare abstract class Node {
23
+ /** Optional comment text associated with this node */
24
+ comment?: string;
25
+ /** Internal metadata key-value pairs associated with this node.
26
+ * These can be anything, are not used in the output, and are meant to be used
27
+ * to allow your pipeline to keep track of an internal property */
28
+ internalMetadata: Record<string, unknown>;
29
+ /** The type of this node (e.g., 'open', 'close', 'transaction', 'comment', 'blankline') */
30
+ abstract type: BeancountDirectiveNodeType | 'comment' | 'blankline';
31
+ /**
32
+ * Creates a new Node instance.
33
+ * @param obj - Object containing node properties
34
+ */
35
+ constructor(obj: Record<string, unknown>);
36
+ /**
37
+ * Creates an Node instance from a Beancount string.
38
+ * Parses the input string and constructs the appropriate Node subclass.
39
+ *
40
+ * @param input - A single unparsed node as a string
41
+ * @returns A new instance of the Node subclass
42
+ */
43
+ static fromString<T extends Node>(this: NodeConstructor<T>, input: string): T;
44
+ /**
45
+ * Creates an Node instance from JSON data.
46
+ * Calls fromJSONData to allow subclasses to transform the data before construction.
47
+ *
48
+ * @param jsonString - JSON data representing an node
49
+ * @returns A new instance of the Node subclass
50
+ * @remarks **Warning:** No validation is performed on the JSON input. We assume the JSON is valid and well-formed.
51
+ */
52
+ static fromJSON<T extends Node>(this: new (obj: any) => T, jsonString: string): T;
53
+ /**
54
+ * Creates an Node instance from JSON data.
55
+ * Calls fromJSONData to allow subclasses to transform the data before construction.
56
+ *
57
+ * @param jsonData - object representing an node
58
+ * @returns A new instance of the Node subclass
59
+ * @remarks **Warning:** No validation is performed on the input. We assume the input is valid and well-formed.
60
+ */
61
+ static fromJSONData<T extends Node>(this: new (obj: any) => T, jsonData: Record<string, unknown>): T;
62
+ /**
63
+ * Converts this node to a formatted string with aligned columns.
64
+ * Default implementation delegates to toString(). Subclasses can override for custom formatting.
65
+ *
66
+ * @param _formatOptions - Formatting options (unused in base implementation)
67
+ * @returns The formatted string representation of this node
68
+ */
69
+ toFormattedString(_formatOptions?: FormatOptions): string;
70
+ /**
71
+ * Transforms JSON data before creating an Node instance.
72
+ * Default implementation returns the input unchanged.
73
+ * Subclasses can override this to handle custom deserialization logic
74
+ * (e.g., converting nested objects, handling dates, etc.).
75
+ *
76
+ * @param json - The JSON data to transform
77
+ * @returns The transformed data ready for the constructor
78
+ */
79
+ protected parseJSONData(json: Record<string, unknown>): Record<string, unknown>;
80
+ /**
81
+ * Converts an node to a JSON-serializable object.
82
+ * @returns A JSON-serializable representation of this node
83
+ */
84
+ toJSON(): Record<string, unknown>;
85
+ }
86
+ /**
87
+ * Type assertion helper to ensure a class conforms to NodeConstructor.
88
+ * Usage: assertNodeConstructor(MyNodeClass)
89
+ * @param _ctor - The constructor to validate (unused at runtime)
90
+ * @internal
91
+ */
92
+ export declare function assertNodeConstructor<T extends Node>(_ctor: NodeConstructor<T>): void;
@@ -0,0 +1,107 @@
1
+ import { genericParse } from '../genericParse.mjs';
2
+ import { stringAwareSplitLine } from '../utils/stringAwareSplitLine.mjs';
3
+ import { defaultFormatOptions } from './ParseResult.mjs';
4
+ /**
5
+ * Abstract base class for all nodes.
6
+ *
7
+ * Child classes must implement static `fromGenericParseResult` method
8
+ */
9
+ export class Node {
10
+ /**
11
+ * Creates a new Node instance.
12
+ * @param obj - Object containing node properties
13
+ */
14
+ constructor(obj) {
15
+ /** Internal metadata key-value pairs associated with this node.
16
+ * These can be anything, are not used in the output, and are meant to be used
17
+ * to allow your pipeline to keep track of an internal property */
18
+ this.internalMetadata = {};
19
+ Object.assign(this, obj);
20
+ }
21
+ /**
22
+ * Creates an Node instance from a Beancount string.
23
+ * Parses the input string and constructs the appropriate Node subclass.
24
+ *
25
+ * @param input - A single unparsed node as a string
26
+ * @returns A new instance of the Node subclass
27
+ */
28
+ static fromString(input) {
29
+ const sourceFragment = stringAwareSplitLine(input);
30
+ const genericParseResult = genericParse(sourceFragment);
31
+ const result = this.fromGenericParseResult(genericParseResult);
32
+ if (result.type !== genericParseResult.type) {
33
+ console.warn('Parse result type is not equal to requested object type, make sure the correct class is initiated');
34
+ }
35
+ return result;
36
+ }
37
+ /**
38
+ * Creates an Node instance from JSON data.
39
+ * Calls fromJSONData to allow subclasses to transform the data before construction.
40
+ *
41
+ * @param jsonString - JSON data representing an node
42
+ * @returns A new instance of the Node subclass
43
+ * @remarks **Warning:** No validation is performed on the JSON input. We assume the JSON is valid and well-formed.
44
+ */
45
+ static fromJSON(jsonString) {
46
+ const json = JSON.parse(jsonString);
47
+ // @ts-expect-error Not sure how to type this correctly
48
+ return this.fromJSONData(json); // eslint-disable-line
49
+ }
50
+ /**
51
+ * Creates an Node instance from JSON data.
52
+ * Calls fromJSONData to allow subclasses to transform the data before construction.
53
+ *
54
+ * @param jsonData - object representing an node
55
+ * @returns A new instance of the Node subclass
56
+ * @remarks **Warning:** No validation is performed on the input. We assume the input is valid and well-formed.
57
+ */
58
+ static fromJSONData(jsonData) {
59
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
60
+ const instance = new this({});
61
+ const transformedData = instance.parseJSONData(jsonData);
62
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
63
+ return new this(transformedData);
64
+ }
65
+ /**
66
+ * Converts this node to a formatted string with aligned columns.
67
+ * Default implementation delegates to toString(). Subclasses can override for custom formatting.
68
+ *
69
+ * @param _formatOptions - Formatting options (unused in base implementation)
70
+ * @returns The formatted string representation of this node
71
+ */
72
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
73
+ toFormattedString(_formatOptions = defaultFormatOptions) {
74
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
75
+ return this.toString();
76
+ }
77
+ /**
78
+ * Transforms JSON data before creating an Node instance.
79
+ * Default implementation returns the input unchanged.
80
+ * Subclasses can override this to handle custom deserialization logic
81
+ * (e.g., converting nested objects, handling dates, etc.).
82
+ *
83
+ * @param json - The JSON data to transform
84
+ * @returns The transformed data ready for the constructor
85
+ */
86
+ parseJSONData(json) {
87
+ return json;
88
+ }
89
+ /**
90
+ * Converts an node to a JSON-serializable object.
91
+ * @returns A JSON-serializable representation of this node
92
+ */
93
+ toJSON() {
94
+ return {
95
+ ...this,
96
+ };
97
+ }
98
+ }
99
+ /**
100
+ * Type assertion helper to ensure a class conforms to NodeConstructor.
101
+ * Usage: assertNodeConstructor(MyNodeClass)
102
+ * @param _ctor - The constructor to validate (unused at runtime)
103
+ * @internal
104
+ */
105
+ export function assertNodeConstructor(
106
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
107
+ _ctor) { }