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
3 changes: 2 additions & 1 deletion .gitkeep
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
# .gitkeep file auto-generated at 2026-05-10T19:22:27.543Z for PR creation at branch issue-35-03946ff48852 for issue https://github.com/link-foundation/lino-objects-codec/issues/35
# .gitkeep file auto-generated at 2026-05-10T19:22:27.543Z for PR creation at branch issue-35-03946ff48852 for issue https://github.com/link-foundation/lino-objects-codec/issues/35
# Updated: 2026-08-20T05:25:16.696Z
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ All implementations share the same design philosophy and provide feature parity.
- **Circular References**: Automatically detect and preserve circular references
- **Object Identity**: Maintain object identity for shared references
- **UTF-8 Support**: Full Unicode string support using base64 encoding
- **Readable by Default (Rust)**: `encode()` writes indented, plain-text Links Notation; the previous single-line base64 form stays available as `encode_compact()`
- **Simple API**: Easy-to-use `encode()` and `decode()` functions
- **JSON/Lino Conversion**: Convert between JSON and Links Notation (JavaScript)
- **Reference Escaping**: Properly escape strings for Links Notation format (JavaScript)
Expand Down Expand Up @@ -98,11 +99,25 @@ let data = LinoValue::object([
("age", LinoValue::Int(30)),
("active", LinoValue::Bool(true)),
]);
// `encode` produces readable, indented Links Notation
let encoded = encode(&data);
assert_eq!(encoded, "(\n name \"Alice\"\n age 30\n active true\n)");

let decoded = decode(&encoded).unwrap();
assert_eq!(decoded, data);
```

```lino
(
name "Alice"
age 30
active true
)
```

The single-line base64 form is still available as `encode_compact()` (alias
`encode_obfuscated()`), and `decode()` accepts both forms.

### C#

```bash
Expand Down Expand Up @@ -354,6 +369,10 @@ The library uses the [links-notation](https://github.com/link-foundation/links-n

- Basic types are encoded with type markers: `(int 42)`, `(str aGVsbG8=)`, `(bool True)`
- Strings are base64-encoded to handle special characters and newlines
- **Rust exception**: `encode()` defaults to the readable indented form described in
[rust/README.md](rust/README.md), where strings are quoted rather than encoded and
only values containing control characters are marked as `(base64 "...")`; the form
above is what `encode_compact()` produces
- Collections with self-references use built-in links notation self-reference syntax:
- **Format**: `(obj_id: type content...)`
- **Python example**: `(obj_0: dict ((str c2VsZg==) obj_0))` for `{"self": obj}`
Expand Down
2 changes: 2 additions & 0 deletions experiments/issue-37/parenthesis-indentation/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
target/
Cargo.lock
8 changes: 8 additions & 0 deletions experiments/issue-37/parenthesis-indentation/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "parenthesis-indentation-probe"
version = "0.1.0"
edition = "2021"

# Switch this between "0.14.0" and "0.13.0" to compare parser behaviour.
[dependencies]
links-notation = "0.14.0"
25 changes: 25 additions & 0 deletions experiments/issue-37/parenthesis-indentation/src/bin/shapes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use links_notation::parse_lino_to_links;

fn show(name: &str, text: &str) {
println!("=== {} ===\n{}\n---", name, text);
match parse_lino_to_links(text) {
Ok(links) => {
for l in &links {
println!("{:?}", l);
}
}
Err(e) => println!("ERR: {:?}", e),
}
println!();
}

fn main() {
show("doc", "(\n type \"RouterState\"\n server (\n host \"127.0.0.1\"\n port 18878\n )\n models (\n \"claude-haiku\"\n \"claude-opus\"\n )\n)");
show("array of objects", "(\n value (\n (\n id \"1\"\n label \"one\"\n )\n (\n id \"2\"\n label \"two\"\n )\n )\n)");
show("empty link", "(\n a ()\n b ()\n)");
show("scalar root", "42");
show("string root", "(\n \"hello\"\n)");
show("quotes", "(\n a \"say \"\"hi\"\"\"\n b 'it''s'\n)");
show("newline in quotes", "(\n a \"line1\nline2\"\n)");
show("single pair obj", "(\n a 1\n)");
}
24 changes: 24 additions & 0 deletions experiments/issue-37/parenthesis-indentation/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//! Experiment for issue #37: does `( )` open a nested indentation context?
//!
//! Run with links-notation 0.13 and 0.14 to compare:
//! cargo run # 0.14 (as pinned in Cargo.toml)
//! cargo add links-notation@0.13.0 && cargo run
//!
//! 0.13 ignores indentation inside `( )` and flattens every line into one list,
//! so record boundaries and nested objects cannot be recovered. 0.14 keeps them.

use links_notation::parse_lino_to_links;

const DOCUMENT: &str = "(\n server (\n host \"127.0.0.1\"\n port 18878\n )\n)";

fn main() {
println!("input:\n{DOCUMENT}\n");
match parse_lino_to_links(DOCUMENT) {
Ok(links) => {
for link in &links {
println!("parsed: {link:?}");
}
}
Err(e) => println!("parse error: {e:?}"),
}
}
51 changes: 49 additions & 2 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ keywords = ["links-notation", "serialization", "codec", "object-graph", "circula
categories = ["encoding", "parser-implementations"]

[dependencies]
links-notation = "0.13.0"
links-notation = "0.14.0"
base64 = "0.22"

[dev-dependencies]
Expand Down
120 changes: 104 additions & 16 deletions rust/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ lino-objects-codec = "0.1"
- **Special Float Values**: Full support for NaN, Infinity, -Infinity (which are not valid JSON)
- **Circular References**: Detect and preserve circular references via object IDs
- **Object Identity**: Maintain object identity for shared references
- **UTF-8 Support**: Full Unicode string support using base64 encoding
- **Readable by Default**: `encode()` writes indented, plain-text Links Notation; keys and values stay legible and diffable
- **UTF-8 Support**: Full Unicode string support written as text; only values that cannot be written as text (control characters) are base64-encoded, and each is marked individually
- **Simple API**: Easy-to-use `encode()` and `decode()` functions

## Quick Start
Expand All @@ -47,13 +48,35 @@ let data = LinoValue::object([

// Encode to Links Notation
let encoded = encode(&data);
println!("Encoded: {}", encoded);
assert_eq!(encoded, "(\n name \"Alice\"\n age 30\n active true\n)");

// Decode back
let decoded = decode(&encoded).unwrap();
assert_eq!(decoded, data);
```

The encoded document reads as:

```lino
(
name "Alice"
age 30
active true
)
```

## Output Formats

| Function | Output |
| --- | --- |
| `encode(value)` | Readable, indented Links Notation (the default) |
| `encode_with_indent(value, "\t")` | Same, with a custom indentation string |
| `encode_compact(value)` | The previous single-line base64 form |
| `encode_obfuscated(value)` | Alias of `encode_compact` |

`decode()` accepts every one of them, so files written by older versions keep
working and are rewritten in the readable form the next time they are saved.

## API Reference

### Types
Expand Down Expand Up @@ -97,21 +120,42 @@ pub enum CodecError {

#### `encode(value: &LinoValue) -> String`

Encode a value to Links Notation format.
Encode a value to the readable, indented Links Notation format.

```rust
let value = LinoValue::Int(42);
let encoded = encode(&value);
assert_eq!(encoded, "(int 42)");
assert_eq!(encoded, "42");
```

#### `encode_with_indent(value: &LinoValue, indent: &str) -> String`

Same as `encode()`, but with a custom indentation string (the default is two spaces).

#### `encode_compact(value: &LinoValue) -> String`

Encode a value to the single-line, base64 form used before version 0.3.

```rust
let value = LinoValue::String("hello".to_string());
assert_eq!(encode_compact(&value), "(str aGVsbG8=)");
```

#### `encode_obfuscated(value: &LinoValue) -> String`

Alias of `encode_compact()`, named after what the base64 form actually does to the text.

#### `decode(notation: &str) -> Result<LinoValue, CodecError>`

Decode Links Notation format to a value.
Decode Links Notation format to a value. Both the readable and the compact form
are accepted.

```rust
let decoded = decode("(int 42)").unwrap();
let decoded = decode("42").unwrap();
assert_eq!(decoded, LinoValue::Int(42));

let legacy = decode("(int 42)").unwrap();
assert_eq!(legacy, LinoValue::Int(42));
```

### `ObjectCodec`
Expand All @@ -135,30 +179,34 @@ use lino_objects_codec::{encode, decode, LinoValue};

// Null
let null = LinoValue::Null;
assert_eq!(encode(&null), "(null)");
assert_eq!(encode(&null), "null");

// Boolean
let bool_val = LinoValue::Bool(true);
assert_eq!(encode(&bool_val), "(bool true)");
assert_eq!(encode(&bool_val), "true");

// Integer
let int_val = LinoValue::Int(42);
assert_eq!(encode(&int_val), "(int 42)");
assert_eq!(encode(&int_val), "42");

// Float
let float_val = LinoValue::Float(3.14);
assert!(encode(&float_val).starts_with("(float"));
assert_eq!(encode(&float_val), "3.14");

// Special floats
let inf = LinoValue::Float(f64::INFINITY);
assert_eq!(encode(&inf), "(float Infinity)");
assert_eq!(encode(&inf), "Infinity");

let nan = LinoValue::Float(f64::NAN);
assert_eq!(encode(&nan), "(float NaN)");
assert_eq!(encode(&nan), "NaN");

// String (base64 encoded)
// Strings are quoted, not encoded
let str_val = LinoValue::String("hello".to_string());
assert_eq!(encode(&str_val), "(str aGVsbG8=)");
assert_eq!(encode(&str_val), "\"hello\"");

// Numbers written as strings stay strings
let numeric = LinoValue::String("42".to_string());
assert_eq!(decode(&encode(&numeric)).unwrap(), numeric);
```

### Collections
Expand Down Expand Up @@ -235,18 +283,58 @@ let none_val: LinoValue = None::<i64>.into();

## How It Works

The codec encodes values using the [Links Notation](https://github.com/link-foundation/links-notation) format:
The codec encodes values using the [Links Notation](https://github.com/link-foundation/links-notation) format.

### Readable format (the default)

One `( )` construct carries both objects and arrays, at every level including
the root. Lines of the form `key value` make an object, bare-value lines make an
array:

```lino
(
type "RouterState"
server (
host "127.0.0.1"
port 18878
)
models (
"claude-haiku"
"claude-opus"
)
)
```

- Strings are double-quoted and written as text: `name "Alice"`
- Numbers, `true`, `false` and `null` are bare, so types survive a round trip
- `NaN`, `Infinity` and `-Infinity` are written as such
- An empty array is `()`; an empty object is `(` + newline + `)`
- A value that cannot be written as text (one containing control characters) is
base64-encoded on its own and marked as `(base64 "bGluZTEKbGluZTI=")`;
everything around it stays readable

Reading the format back requires `links-notation` 0.14 semantics, where a
parenthesis opens a nested indentation context.

### Compact format (`encode_compact`)

The previous single-line form, kept for compatibility and for cases where size
matters more than legibility:

- Basic types: `(int 42)`, `(str aGVsbG8=)`, `(bool true)`
- Strings are base64-encoded to handle special characters and newlines
- Arrays: `(array (int 1) (int 2) (int 3))`
- Objects: `(object ((str a2V5) (int 42)) ...)`
- Special floats: `(float NaN)`, `(float Infinity)`, `(float -Infinity)`

For structures with shared references or circular references, the codec uses object IDs:
For structures with shared references or circular references, the compact form
uses object IDs:
- Format: `(obj_0: array ...)` or `(obj_0: object ...)`
- References: `obj_0`

`decode()` detects which of the two forms it is given, so previously written
files keep decoding.

## Development

```bash
Expand Down
Loading
Loading