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
Documentis a list of source lines. Each line keeps its raw bytes, including its own line ending, soDocument::serializereturns 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:: valueproperty, 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\nand\r. Python’ssplitlinesalso 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
\dalso accepted other Unicode digits in a month, which then never matched a month file. write_transactionrefuses an empty payee: the oracle wrote a row that it could not read back.
Source: crates/calternal-money/src/codec.rs
Structs
Section titled “Structs”Document
Section titled “Document”pub struct DocumentA lossless Money Markdown file. See the module documentation.
Fields
pub lines: Vec<Line>: Source lines in order.
Implements: Clone, Debug, Default
Document::parse
Section titled “Document::parse”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).
Document::parse_with_check
Section titled “Document::parse_with_check”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.
Document::serialize
Section titled “Document::serialize”pub fn serialize(&self) -> StringConcatenate the raw lines: the exact original bytes for an untouched document, and only the edited spans changed after an edit.
Document::line_ending
Section titled “Document::line_ending”pub fn line_ending(&self) -> &'static strThe line ending the file already uses (\r\n if any line has one).
Document::assert_unique_ids
Section titled “Document::assert_unique_ids”pub fn assert_unique_ids(&self) -> Result<()>Reject duplicate block IDs outside opaque regions (#404 rule 1).
Document::assert_unique_ids_with_check
Section titled “Document::assert_unique_ids_with_check”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.
Document::reparse
Section titled “Document::reparse”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
Heading
Section titled “Heading”pub struct HeadingA 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 LineOne source line and its shallow projection.
Fields
pub raw: String: The exact source bytes of this line, including its line ending.pub text: String:rawwithout 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:: valueproperty: 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
Line::heading_level
Section titled “Line::heading_level”pub fn heading_level(&self) -> Option<usize>The heading level, if this line is a heading.
Line::property_key
Section titled “Line::property_key”pub fn property_key(&self) -> Option<String>The lowercase property key, if this line is a property.
Line::property_value
Section titled “Line::property_value”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
Reference
Section titled “Reference”pub struct ReferenceA 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
Reference::new
Section titled “Reference::new”pub fn new(name: impl Into<String>, file: RefFile, id: impl Into<String>) -> SelfBuild a reference; see write_reference for the validation.
Source: crates/calternal-money/src/codec.rs:95
TransactionRow
Section titled “TransactionRow”pub struct TransactionRowOne transaction row. Amount and currency keep their exact source text.
Fields
pub id: String: Stable block ID (^idat the end of the row).pub date: String: Calendar dateYYYY-MM-DD(never an instant; never shifted by a zone).pub amount: String: Exact decimal text as written, for example-45.00or+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
RefFile
Section titled “RefFile”pub enum RefFileThe 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
RefFile::as_str
Section titled “RefFile::as_str”pub const fn as_str(self) -> &'static strThe file name used in link targets.
Source: crates/calternal-money/src/codec.rs:72
Functions
Section titled “Functions”amount_currency
Section titled “amount_currency”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
frontmatter
Section titled “frontmatter”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
is_stable_id
Section titled “is_stable_id”pub fn is_stable_id(id: &str) -> boolTrue for a stable block ID: [A-Za-z0-9_-]+.
Source: crates/calternal-money/src/codec.rs:717
json_property
Section titled “json_property”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
lift_id
Section titled “lift_id”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
looks_like_dated_row
Section titled “looks_like_dated_row”pub fn looks_like_dated_row(text: &str) -> boolTrue 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
parse_reference
Section titled “parse_reference”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
parse_transaction
Section titled “parse_transaction”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
properties_after
Section titled “properties_after”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
rename_reference
Section titled “rename_reference”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
replace_line
Section titled “replace_line”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
set_property
Section titled “set_property”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
split_fields
Section titled “split_fields”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
strict_date
Section titled “strict_date”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
validate_reference
Section titled “validate_reference”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
without_eol
Section titled “without_eol”pub fn without_eol(raw: &str) -> &strRemove one line ending; callers keep the original ending bytes elsewhere.
Source: crates/calternal-money/src/codec.rs:358
write_reference
Section titled “write_reference”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
write_transaction
Section titled “write_transaction”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.