Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 205 additions & 0 deletions packages/toml-ast/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
# toml-ast

<p align="center" width="100%">
<img src="https://raw.githubusercontent.com/constructive-io/constructive/refs/heads/main/assets/outline-logo.svg" height="250">
</p>

TypeScript TOML parser and deparser. Parse TOML files into AST and regenerate configuration from AST.

## Installation

```bash
npm install toml-ast
```

## Usage

### Parse TOML Configuration

```typescript
import { parse } from 'toml-ast';

const config = `
[server]
host = "localhost"
port = 8080

[database]
enabled = true
ports = [8001, 8001, 8002]
`;

const ast = parse(config);
console.log(ast);
```

### Deparse AST to TOML

```typescript
import { parse, deparse } from 'toml-ast';

const ast = parse(config);
const output = deparse(ast);
console.log(output);
```

### Build AST Programmatically

```typescript
import { deparse } from 'toml-ast';
import type { TomlDocument } from 'toml-ast';

const ast: TomlDocument = {
type: 'TomlDocument',
body: [
{
type: 'Table',
key: { type: 'Key', parts: [{ type: 'KeyPart', value: 'server', style: 'bare' }] },
body: [
{
type: 'KeyValue',
key: { type: 'Key', parts: [{ type: 'KeyPart', value: 'host', style: 'bare' }] },
value: { type: 'StringValue', value: 'localhost', style: 'basic' },
},
{
type: 'KeyValue',
key: { type: 'Key', parts: [{ type: 'KeyPart', value: 'port', style: 'bare' }] },
value: { type: 'IntegerValue', value: 8080, raw: '8080' },
},
],
},
],
};

console.log(deparse(ast));
// [server]
// host = "localhost"
// port = 8080
```

### Round-Trip Testing

```typescript
import { parse, deparse, cleanTree } from 'toml-ast';

const ast1 = parse(config);
const output = deparse(ast1);
const ast2 = parse(output);

// Compare ASTs without location info
expect(cleanTree(ast1)).toEqual(cleanTree(ast2));
```

## AST Types

### TomlDocument (Root)

```typescript
interface TomlDocument {
type: 'TomlDocument';
body: RootItem[];
}
```

### KeyValue

```typescript
interface KeyValue {
type: 'KeyValue';
key: Key;
value: Value;
}
```

### Key

```typescript
interface Key {
type: 'Key';
parts: KeyPart[]; // dotted keys have multiple parts
}

interface KeyPart {
type: 'KeyPart';
value: string;
style: 'bare' | 'basic' | 'literal';
}
```

### Value Types

- `StringValue` - basic `"..."`, literal `'...'`, multiline `"""..."""`, `'''...'''`
- `IntegerValue` - decimal, hex (`0x`), octal (`0o`), binary (`0b`), underscored
- `FloatValue` - decimal, exponent, `inf`, `-inf`, `nan`
- `BooleanValue` - `true`, `false`
- `DateTimeValue` - offset datetime, local datetime, local date, local time
- `ArrayValue` - `[1, 2, 3]`
- `InlineTable` - `{ key = "value" }`

### Table

```typescript
interface Table {
type: 'Table';
key: Key;
body: TableItem[];
}
```

### ArrayOfTables

```typescript
interface ArrayOfTables {
type: 'ArrayOfTables';
key: Key;
body: TableItem[];
}
```

### Comment

```typescript
interface Comment {
type: 'Comment';
value: string;
}
```

## Deparse Options

```typescript
interface DeparseOptions {
indent?: string; // Default: ' ' (2 spaces)
newline?: string; // Default: '\n'
}

const output = deparse(ast, { indent: ' ' });
```

## Utilities

### cleanTree

Remove range/location information from AST for comparison:

```typescript
import { cleanTree } from 'toml-ast';

const cleaned = cleanTree(ast);
```

### astEqual

Compare two ASTs ignoring location info:

```typescript
import { astEqual } from 'toml-ast';

if (astEqual(ast1, ast2)) {
console.log('ASTs are equivalent');
}
```

## License

MIT
178 changes: 178 additions & 0 deletions packages/toml-ast/__tests__/deparser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { deparse } from '../src/deparser';
import type { TomlDocument } from '../src/types';

describe('toml deparser', () => {
it('deparses key-value pair', () => {
const ast: TomlDocument = {
type: 'TomlDocument',
body: [{
type: 'KeyValue',
key: { type: 'Key', parts: [{ type: 'KeyPart', value: 'title', style: 'bare' }] },
value: { type: 'StringValue', value: 'TOML Example', style: 'basic' },
}],
};
expect(deparse(ast)).toBe('title = "TOML Example"');
});

it('deparses integer value', () => {
const ast: TomlDocument = {
type: 'TomlDocument',
body: [{
type: 'KeyValue',
key: { type: 'Key', parts: [{ type: 'KeyPart', value: 'port', style: 'bare' }] },
value: { type: 'IntegerValue', value: 8080, raw: '8080' },
}],
};
expect(deparse(ast)).toBe('port = 8080');
});

it('deparses boolean value', () => {
const ast: TomlDocument = {
type: 'TomlDocument',
body: [{
type: 'KeyValue',
key: { type: 'Key', parts: [{ type: 'KeyPart', value: 'enabled', style: 'bare' }] },
value: { type: 'BooleanValue', value: true },
}],
};
expect(deparse(ast)).toBe('enabled = true');
});

it('deparses table', () => {
const ast: TomlDocument = {
type: 'TomlDocument',
body: [{
type: 'Table',
key: { type: 'Key', parts: [{ type: 'KeyPart', value: 'server', style: 'bare' }] },
body: [{
type: 'KeyValue',
key: { type: 'Key', parts: [{ type: 'KeyPart', value: 'host', style: 'bare' }] },
value: { type: 'StringValue', value: 'localhost', style: 'basic' },
}],
}],
};
expect(deparse(ast)).toBe('[server]\nhost = "localhost"');
});

it('deparses array of tables', () => {
const ast: TomlDocument = {
type: 'TomlDocument',
body: [{
type: 'ArrayOfTables',
key: { type: 'Key', parts: [{ type: 'KeyPart', value: 'products', style: 'bare' }] },
body: [{
type: 'KeyValue',
key: { type: 'Key', parts: [{ type: 'KeyPart', value: 'name', style: 'bare' }] },
value: { type: 'StringValue', value: 'Hammer', style: 'basic' },
}],
}],
};
expect(deparse(ast)).toBe('[[products]]\nname = "Hammer"');
});

it('deparses inline table', () => {
const ast: TomlDocument = {
type: 'TomlDocument',
body: [{
type: 'KeyValue',
key: { type: 'Key', parts: [{ type: 'KeyPart', value: 'point', style: 'bare' }] },
value: {
type: 'InlineTable',
entries: [
{
type: 'KeyValue',
key: { type: 'Key', parts: [{ type: 'KeyPart', value: 'x', style: 'bare' }] },
value: { type: 'IntegerValue', value: 1, raw: '1' },
},
{
type: 'KeyValue',
key: { type: 'Key', parts: [{ type: 'KeyPart', value: 'y', style: 'bare' }] },
value: { type: 'IntegerValue', value: 2, raw: '2' },
},
],
},
}],
};
expect(deparse(ast)).toBe('point = { x = 1, y = 2 }');
});

it('deparses array', () => {
const ast: TomlDocument = {
type: 'TomlDocument',
body: [{
type: 'KeyValue',
key: { type: 'Key', parts: [{ type: 'KeyPart', value: 'ports', style: 'bare' }] },
value: {
type: 'ArrayValue',
elements: [
{ type: 'IntegerValue', value: 80, raw: '80' },
{ type: 'IntegerValue', value: 443, raw: '443' },
],
},
}],
};
expect(deparse(ast)).toBe('ports = [80, 443]');
});

it('deparses dotted key', () => {
const ast: TomlDocument = {
type: 'TomlDocument',
body: [{
type: 'KeyValue',
key: {
type: 'Key',
parts: [
{ type: 'KeyPart', value: 'server', style: 'bare' },
{ type: 'KeyPart', value: 'host', style: 'bare' },
],
},
value: { type: 'StringValue', value: 'localhost', style: 'basic' },
}],
};
expect(deparse(ast)).toBe('server.host = "localhost"');
});

it('deparses quoted key', () => {
const ast: TomlDocument = {
type: 'TomlDocument',
body: [{
type: 'KeyValue',
key: { type: 'Key', parts: [{ type: 'KeyPart', value: 'special key', style: 'basic' }] },
value: { type: 'IntegerValue', value: 42, raw: '42' },
}],
};
expect(deparse(ast)).toBe('"special key" = 42');
});

it('deparses comment', () => {
const ast: TomlDocument = {
type: 'TomlDocument',
body: [{ type: 'Comment', value: 'This is a comment' }],
};
expect(deparse(ast)).toBe('# This is a comment');
});

it('deparses literal string', () => {
const ast: TomlDocument = {
type: 'TomlDocument',
body: [{
type: 'KeyValue',
key: { type: 'Key', parts: [{ type: 'KeyPart', value: 'path', style: 'bare' }] },
value: { type: 'StringValue', value: 'C:\\Users\\name', style: 'literal' },
}],
};
expect(deparse(ast)).toBe("path = 'C:\\Users\\name'");
});

it('deparses escaped basic string', () => {
const ast: TomlDocument = {
type: 'TomlDocument',
body: [{
type: 'KeyValue',
key: { type: 'Key', parts: [{ type: 'KeyPart', value: 'msg', style: 'bare' }] },
value: { type: 'StringValue', value: 'line1\nline2', style: 'basic' },
}],
};
expect(deparse(ast)).toBe('msg = "line1\\nline2"');
});
});
Loading
Loading