chronml 0.1.0
CHRON: Configurable Human-Readable Object Notation
License: MIT
Requires Python: >=3.10
Keywords: chronml, markdown, object-notation, serialization, template

# CHRON

CHRON stands for **Configurable Human-Readable Object Notation**.

CHRON is a Python library for round-tripping structured objects through
review-friendly Markdown text with explicit marker metadata.

## Why CHRON

- Human-readable output for review and diff.
- Machine-loadable with deterministic reconstruction.
- Typed records supported through `pydantic` model metadata.
- Configurable marker namespace prefix.
- No regex parsing in core codec paths.
- Marker-level grammar parsed via `lark`.
- JSON path resolution and matching via `jsonpath_ng`.

## Install

```bash
pip install chronml
```

## Quick Start

```python
from chronml import Chron

chronml = Chron()

records = [
    {"title": "hello", "body": "line1\nline2"}
]

md = chronml.dumps(records, template="# {$.title}\n\n{$.body:block}")
print(md)

loaded = chronml.loads(md)
assert loaded[0]["title"] == "hello"
assert loaded[0]["body"] == "line1\nline2"
```

## Markdown Output Examples

### 1) Inline + Block

Python:

```python
from chronml import Chron

chronml = Chron()
md = chronml.dumps(
    [{"title": "hello", "body": "line1\nline2"}],
    template="# {$.title}\n\n{$.body:block}",
)
print(md)
```

Output:

```markdown
<!-- chronml:begin type=json skeleton={} -->

# <!-- chronml:value-begin json-path=$.title --> hello <!-- chronml:value-end -->

<!-- chronml:multi-line-value-begin json-path=$.body -->
line1
line2
<!-- chronml:multi-line-value-end -->
<!-- chronml:end -->
```

### 2) Code Block

Python:

```python
from chronml import Chron

chronml = Chron()
md = chronml.dumps(
    [{"code": "print(1)\nprint(2)"}],
    template="{$.code:code-block-python}",
)
print(md)
```

Output:

````markdown
<!-- chronml:begin type=json skeleton={} -->

<!-- chronml:code-block-begin lang=python json-path=$.code -->
```python
print(1)
print(2)
```
<!-- chronml:code-block-end -->
<!-- chronml:end -->
````

### 3) Wildcard Multi-Match (`[*]`)

Python:

```python
from chronml import Chron

chronml = Chron()
md = chronml.dumps(
    [{"items": [{"name": "A"}, {"name": "B"}]}],
    template="items: {$.items[*].name}",
)
print(md)
```

Output (flattened with concrete paths):

```markdown
<!-- chronml:begin type=json skeleton={"items": [{}, {}]} -->

items: <!-- chronml:value-begin json-path=$.items[0].name --> A <!-- chronml:value-end --> <!-- chronml:value-begin json-path=$.items[1].name --> B <!-- chronml:value-end -->
<!-- chronml:end -->
```

### 4) Nested Model Template Expansion

Python:

```python
from pydantic import BaseModel
from chronml import Chron

class Child(BaseModel):
    x: int
    y: str
    __chronml_template__ = "X={$.x}\nY={$.y}"

class Parent(BaseModel):
    name: str
    child: Child
    __chronml_template__ = "N={$.name}\n{$.child}"

chronml = Chron()
md = chronml.dumps([Parent(name="p", child=Child(x=5, y="yy"))], template="")
print(md)
```

Output (nested paths rewritten to global paths):

```markdown
<!-- chronml:begin type=pydantic:__main__.Parent skeleton={"child": {}} -->

N=<!-- chronml:value-begin json-path=$.name --> p <!-- chronml:value-end -->
X=<!-- chronml:value-begin type=int json-path=$.child.x --> 5 <!-- chronml:value-end -->
Y=<!-- chronml:value-begin json-path=$.child.y --> yy <!-- chronml:value-end -->
<!-- chronml:end -->
```

## Core API

- `Chron.loads(text) -> list[record]`
- `Chron.load(path) -> list[record]`
- `Chron.dumps(records, template=..., template_func=...) -> str`
- `Chron.dump(records, path, template=..., template_func=...) -> None`

Module-level helpers with the same names are also exported:
`loads`, `load`, `dumps`, `dump`.

## Template Syntax

Placeholder forms:

- `{$.path}` inline value
- `{$.path:inline}` inline value
- `{$.path:block}` multi-line value block
- `{$.path:code-block}` fenced code block
- `{$.path:code-block-python}` fenced code block with language
- `{$.items[*].name}` multi-match flattening

For wildcard and other multi-match expressions, CHRON expands all matches in
result order. Emitted marker paths are concrete (`$.items[0].name`, ...).

## Marker Prefix

Default prefix is `chronml:`.

```python
chronml = Chron(marker_prefix="custom:")
```

Then markers are emitted as:

- `<!-- custom:begin ... -->`
- `<!-- custom:value-begin ... -->`
- `<!-- custom:end -->`

## Pydantic Support

If a record is a `pydantic.BaseModel`, CHRON stores record type metadata as
`type=pydantic:<module>.<ClassName>` and reconstructs model instances on load.

Template resolution order per record:

1. `obj.__chronml_template__` (string only)
2. `template_func(obj)`
3. `template` argument

Nested model templates are expanded in place for inline/default placeholders,
and nested paths are rewritten to global paths.

## Escaping and Round-Trip Guarantees

CHRON escapes marker-like payload text, including existing escaped marker
literals, with reversible encoding. This preserves:

- leading/trailing spaces in inline text
- multi-line content and trailing spaces in block mode
- marker-like text inside payload content

## Protocol and AST

- Marker AST spec: `docs/ast-spec.md`
- Release checklist: `docs/release.md`

## Development

Run tests:

```bash
uv run --with pytest pytest tests -q
```

Build artifacts:

```bash
uv run --with build python -m build
```