Skip to content

calternal_money::codec

Lossless Money Markdown codec (#404 format, DESIGN §48 F1-F3).

This is a port of spikes/money-markdown/money_markdown.py (the reviewed oracle). The rules it keeps:

  • A Document is a list of source lines. Each line keeps its raw bytes, including its own line ending, so Document::serialize returns the input byte for byte. Nothing is ever re-emitted from a model.
  • YAML frontmatter, fenced code and HTML comments are opaque: they are never projected, renamed or edited.
  • Each other line gets a shallow view: heading level and title, trailing ^block-id, key:: value property, or a transaction row.
  • A transaction row is - date, amount CUR, Account, Category, payee and memo ^id. Fields split on the first four , separators outside link labels; the rest is the payee and memo.
  • A row that starts like a dated transaction but is malformed is an error, never a silently ignored line.

Imported Notes and source metadata use JSON properties. Their delimiters are escaped so source text cannot open an opaque HTML comment (#1130).

Deliberate differences from the oracle (reported to the break-the-numbers review, #462):

  • Lines split only on \n, \r\n and \r. Python’s splitlines also splits on Unicode separators such as U+2028; those are not Markdown line endings, and splitting there made a payee with U+2028 unreadable.
  • Dates and months must use ASCII digits. The oracle’s \d also accepted other Unicode digits in a month, which then never matched a month file.
  • write_transaction refuses an empty payee: the oracle wrote a row that it could not read back.

Source: crates/calternal-money/src/codec.rs

pub struct Document

A lossless Money Markdown file. See the module documentation.

Fields

  • pub lines: Vec<Line>: Source lines in order.

Implements: Clone, Debug, Default

pub fn parse(source: &str) -> Result<Self>

Scan source into lines. Fails only on a malformed dated row, because such a row must never disappear from totals (#404 review).

pub fn parse_with_check(source: &str, mut check: impl FnMut() -> Result<()>) -> Result<Self>

Scan one file with a bounded checkpoint for cancellable large imports (#462, DESIGN §48). Existing file reads use parse and retain the same line sequence and parse rules.

pub fn serialize(&self) -> String

Concatenate the raw lines: the exact original bytes for an untouched document, and only the edited spans changed after an edit.

pub fn line_ending(&self) -> &'static str

The line ending the file already uses (\r\n if any line has one).

pub fn assert_unique_ids(&self) -> Result<()>

Reject duplicate block IDs outside opaque regions (#404 rule 1).

pub fn assert_unique_ids_with_check(
&self,
mut check: impl FnMut() -> Result<()>,
) -> Result<()>

Check block identities with bounded cancellation checkpoints (#462, DESIGN §48). The ordinary reader keeps the original API and behavior.

pub fn reparse(&self) -> Result<Self>

Re-scan the document after raw edits so every view matches its bytes.

Source: crates/calternal-money/src/codec.rs:196

pub struct Heading

A heading’s level (1-6) and its title without the trailing block ID.

Fields

  • pub level: usize: Number of # marks.
  • pub title: String: Title text.

Implements: Clone, Debug, Eq, PartialEq

Source: crates/calternal-money/src/codec.rs:136

pub struct Line

One source line and its shallow projection.

Fields

  • pub raw: String: The exact source bytes of this line, including its line ending.
  • pub text: String: raw without the line ending.
  • pub number: usize: 1-based line number in the source.
  • pub heading: Option<Heading>: Set for a Markdown heading.
  • pub block_id: Option<String>: Trailing ^block-id (for a transaction row, its ID).
  • pub property: Option<(String, String)>: key:: value property: key trimmed, value as written after ::.
  • pub transaction: Option<TransactionRow>: Set for a transaction row.
  • pub opaque: bool: Frontmatter, fenced code or HTML comment: never projected or edited.

Implements: Clone, Debug

pub fn heading_level(&self) -> Option<usize>

The heading level, if this line is a heading.

pub fn property_key(&self) -> Option<String>

The lowercase property key, if this line is a property.

pub fn property_value(&self) -> Option<&str>

The raw property value, if this line is a property.

Source: crates/calternal-money/src/codec.rs:145

pub struct Reference

A display label paired with a stable block ID in one Money file.

Identity is (file, id); name is display text that a rename changes.

Fields

  • pub name: String: Display label (link text, unescaped).
  • pub file: RefFile: Target file.
  • pub id: String: Target block ID.

Implements: Clone, Debug, Eq, Hash, PartialEq, Serialize

pub fn new(name: impl Into<String>, file: RefFile, id: impl Into<String>) -> Self

Build a reference; see write_reference for the validation.

Source: crates/calternal-money/src/codec.rs:95

pub struct TransactionRow

One transaction row. Amount and currency keep their exact source text.

Fields

  • pub id: String: Stable block ID (^id at the end of the row).
  • pub date: String: Calendar date YYYY-MM-DD (never an instant; never shifted by a zone).
  • pub amount: String: Exact decimal text as written, for example -45.00 or +0.5.
  • pub currency: String: Three-letter currency code as written.
  • pub account: Reference: Account link.
  • pub category: Reference: Category link.
  • pub payee: String: Payee and memo: everything after the fourth separator, before the ID.

Implements: Clone, Debug, Eq, PartialEq, Serialize

Source: crates/calternal-money/src/codec.rs:117

pub enum RefFile

The two Money files a reference may point at (#404: links to ^ids).

Variants

  • Accounts: Accounts.md: accounts.
  • Budget: Budget.md: categories and subscriptions.

Implements: Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize

pub const fn as_str(self) -> &'static str

The file name used in link targets.

Source: crates/calternal-money/src/codec.rs:72

pub fn amount_currency(value: &str) -> Result<(String, String)>

Read amount CUR with the plain decimal grammar; no numeric coercion.

Source: crates/calternal-money/src/codec.rs:560

pub fn frontmatter(document: &Document) -> Result<std::collections::BTreeMap<String, String>>

Read the Money YAML frontmatter keys (format, budget-id, currency, month). Other keys and all bytes stay untouched in the document.

Returns an empty map when the file has no frontmatter. Errors: a duplicate or empty known key, or frontmatter that is never closed.

Source: crates/calternal-money/src/codec.rs:782

pub fn is_stable_id(id: &str) -> bool

True for a stable block ID: [A-Za-z0-9_-]+.

Source: crates/calternal-money/src/codec.rs:717

pub fn json_property(value: &impl serde::Serialize) -> Result<String>

Encode an inert single-line JSON property value. Escaping angle brackets prevents source notes from starting a Markdown HTML comment and hiding later postings. JSON decode restores the exact original text (#1130).

Source: crates/calternal-money/src/codec.rs:41

pub fn lift_id(text: &str) -> (&str, Option<&str>)

Split a trailing ^block-id from text: (text before the whitespace, id).

Source: crates/calternal-money/src/codec.rs:423

pub fn looks_like_dated_row(text: &str) -> bool

True when text starts like a dated row (- 2026-09-01, ). Month files reject such a line when it did not parse as a transaction.

Source: crates/calternal-money/src/codec.rs:507

pub fn parse_reference(value: &str, expected: RefFile) -> Result<Reference>

Read a whole-value link [Label](File.md#^id) that targets expected.

Source: crates/calternal-money/src/codec.rs:629

pub fn parse_transaction(text: &str) -> Result<Option<TransactionRow>>

Read a transaction row, or None when the line is not a dated row.

A line that has four separators and a date-shaped first field is a transaction row: any later defect (bad date, amount, link or missing ID) is an error, so it cannot vanish from totals.

Source: crates/calternal-money/src/codec.rs:470

pub fn properties_after(
lines: &[Line],
index: usize,
level: usize,
) -> std::collections::HashMap<String, Vec<String>>

Collect key:: value properties after the heading at index, until a heading of the same or a higher level. Keys are lowercase; values are trimmed. Deeper headings do not stop the scan (oracle behaviour).

Source: crates/calternal-money/src/codec.rs:833

pub fn rename_reference(
document: &Document,
file: RefFile,
id: &str,
name: &str,
) -> Result<Document>

Change every link label that targets file#^id; destinations, other labels and opaque regions keep every byte.

Source: crates/calternal-money/src/codec.rs:918

pub fn replace_line(document: &Document, index: usize, raw: String) -> Result<Document>

Replace the raw bytes of one line and re-scan the document.

Source: crates/calternal-money/src/codec.rs:910

pub fn set_property(
document: &Document,
block_id: &str,
key: &str,
value: &str,
) -> Result<Document>

Edit one existing property value under the heading with block_id.

Only the value bytes change; the key, separator spacing and line ending stay. Errors when the heading or the property is missing or repeated.

Source: crates/calternal-money/src/codec.rs:858

pub fn split_fields(value: &str, count: usize) -> Option<Vec<String>>

Split on the first count , separators outside link labels; the rest is one last field. Backslash escapes skip the next character. Returns None when there are fewer separators or a label bracket stays open.

Source: crates/calternal-money/src/codec.rs:521

pub fn strict_date(text: &str) -> Option<chrono::NaiveDate>

Parse a strict ASCII YYYY-MM-DD calendar date.

Source: crates/calternal-money/src/codec.rs:450

pub fn validate_reference(reference: &Reference, expected: RefFile) -> Result<()>

Validate a reference before writing it: known file, stable ID, a non-empty one-line label.

Source: crates/calternal-money/src/codec.rs:690

pub fn without_eol(raw: &str) -> &str

Remove one line ending; callers keep the original ending bytes elsewhere.

Source: crates/calternal-money/src/codec.rs:358

pub fn write_reference(reference: &Reference) -> Result<String>

Write [Label](File.md#^id) with an escaped label.

Source: crates/calternal-money/src/codec.rs:706

pub fn write_transaction(row: &TransactionRow) -> Result<String>

Write one transaction row (without a line ending).

Checks the date, the plain decimal grammar and the currency code, but not the currency scale: the codec keeps exact text; arithmetic checks scale.

Source: crates/calternal-money/src/codec.rs:725