Documentation
¶
Overview ¶
Package document defines the CalcMark document structure with blocks.
A CalcMark document consists of blocks: - CalcBlock: 1+ consecutive calculation lines (like Jupyter code cell) - TextBlock: markdown text (like Jupyter markdown cell)
Block boundaries: - 2 consecutive empty lines (\n\n\n) = hard boundary between blocks - 1 empty line (\n\n) = soft boundary (stays within current block)
Package document provides the CalcMark document model with blocks.
This package implements a higher-level document abstraction that manages mixed markdown and calculation content as a structured collection of blocks.
Architecture ¶
Key components:
- Block types (CalcBlock, TextBlock)
- Document structure with block management
- Dependency tracking between calc blocks
- Incremental evaluation for editors
Block Types ¶
CalcBlock: calculation code
block := &CalcBlock{}
block.AppendLine("x = 5")
block.AppendLine("y = x + 3")
TextBlock: markdown or prose
block := &TextBlock{}
block.AppendLine("# My Document")
block.AppendLine("Some text here")
Document Model ¶
Documents manage blocks in order:
doc := document.NewDocument()
calcBlock := &CalcBlock{}
calcBlock.AppendLine("total = 100")
doc.AppendBlock(calcBlock)
textBlock := &TextBlock{}
textBlock.AppendLine("# Results")
doc.AppendBlock(textBlock)
Incremental Evaluation ¶
The document model supports incremental updates for interactive editors:
// Initial evaluation
doc.Evaluate()
// User edits a block
doc.ReplaceBlockSource(blockID, []string{"total = 200"})
// Re-evaluate only affected blocks
doc.EvaluateBlock(blockID)
Dependency Tracking ¶
The document tracks dependencies between blocks to enable smart re-evaluation. When a block changes, only dependent blocks are re-evaluated.
Line Detection ¶
Automatic detection of calculation vs markdown:
detector := document.NewDetector()
blockType := detector.DetectLine("x = 5") // CalcBlock
blockType = detector.DetectLine("# Title") // TextBlock
Use Cases ¶
This package is designed for:
- Interactive markdown editors with live calculation
- Document processing pipelines
- Static site generators with embedded calculations
- Notebook-style interfaces
Performance ¶
Document operations are optimized for incremental updates. Evaluation complexity is O(affected blocks) not O(total blocks).
Package document provides document structure and parsing for CalcMark.
Package document provides document structure and parsing for CalcMark.
Package document provides document structure and parsing for CalcMark.
Index ¶
- Variables
- func ExchangeRateKey(from, to string) string
- func ExtractStatementReferences(node ast.Node) []string
- func IsRegisteredKey(name string) bool
- func NormalizeTableName(raw string) string
- func ParseExchangeRateKey(key string) (from, to string, err error)
- func RenderTextBlocksJoined(blocks []*TextBlock) (blockHTML []string, trailing string, ok bool)
- func TableDirectiveName(line string) (string, bool)
- type Block
- type BlockNode
- type BlockType
- type CalcBlock
- func (cb *CalcBlock) AddDiagnostic(diag Diagnostic)
- func (cb *CalcBlock) ClearDiagnostics()
- func (cb *CalcBlock) ConvertApplied() []bool
- func (cb *CalcBlock) Dependencies() []string
- func (cb *CalcBlock) Diagnostics() []Diagnostic
- func (cb *CalcBlock) Error() error
- func (cb *CalcBlock) IsDirty() bool
- func (cb *CalcBlock) LastValue() types.Type
- func (cb *CalcBlock) Results() []types.Type
- func (cb *CalcBlock) ScaleExempt() []bool
- func (cb *CalcBlock) SetConvertApplied(applied []bool)
- func (cb *CalcBlock) SetDependencies(deps []string)
- func (cb *CalcBlock) SetDiagnostics(diags []Diagnostic)
- func (cb *CalcBlock) SetDirty(dirty bool)
- func (cb *CalcBlock) SetError(err error)
- func (cb *CalcBlock) SetLastValue(val types.Type)
- func (cb *CalcBlock) SetResults(results []types.Type)
- func (cb *CalcBlock) SetScaleExempt(exempt []bool)
- func (cb *CalcBlock) SetStatements(stmts []ast.Node)
- func (cb *CalcBlock) SetVariables(vars []string)
- func (cb *CalcBlock) Source() []string
- func (cb *CalcBlock) Statements() []ast.Node
- func (cb *CalcBlock) Type() BlockType
- func (cb *CalcBlock) Variables() []string
- type CalendarYearOffset
- type ConvertToConfig
- type DependencyAnalyzer
- type Detector
- func (d *Detector) DetectBlocks(source string) ([]Block, error)
- func (d *Detector) IsCalculation(line string) (bool, error)
- func (d *Detector) IsCalculationWithKnownNames(line string, knownNames map[string]bool) (bool, error)
- func (d *Detector) LooksLikeCalculation(lineText string, knownNames map[string]bool) bool
- func (d *Detector) LooksLikeCalculationForCompletion(lineText string, knownNames map[string]bool) bool
- type Diagnostic
- type Document
- func (d *Document) ApplyFrontmatter(env EnvironmentWriter) error
- func (d *Document) DeleteBlock(blockID string) (*UpdateResult, error)
- func (d *Document) EnsureFrontmatter() *Frontmatter
- func (d *Document) GetBlock(id string) (*BlockNode, bool)
- func (d *Document) GetBlocks() []*BlockNode
- func (d *Document) GetBlocksInDependencyOrder(blockIDs []string) []string
- func (d *Document) GetFrontmatter() *Frontmatter
- func (d *Document) GetTransitiveDependents(changedVars []string) []string
- func (d *Document) InsertBlock(afterBlockID string, blockType BlockType, source []string) (*UpdateResult, error)
- func (d *Document) ReplaceBlockSource(blockID string, newSource []string) (*UpdateResult, error)
- func (d *Document) SetFrontmatter(fm *Frontmatter)
- type EnvironmentWriter
- type ExtraField
- type FiscalYearConfig
- type Frontmatter
- func (f *Frontmatter) ExchangeKeys() []string
- func (f *Frontmatter) GetExchangeRate(from, to string) (decimal.Decimal, bool)
- func (f *Frontmatter) GlobalKeys() []string
- func (f *Frontmatter) HasExchangeRate(key string) bool
- func (f *Frontmatter) HasGlobal(name string) bool
- func (f *Frontmatter) HasGlobals() bool
- func (f *Frontmatter) HasScale() bool
- func (f *Frontmatter) LineCount() int
- func (f *Frontmatter) Serialize() string
- func (f *Frontmatter) SetExchangeRate(key string, rate decimal.Decimal)
- func (f *Frontmatter) SetGlobal(name, valueExpr string)
- func (f *Frontmatter) SetRawSource(raw string)
- type FrontmatterKeyType
- type MeasurementFrontmatter
- type ParsedGlobals
- type RegisteredKey
- type ScaleConfig
- type TextBlock
- func (tb *TextBlock) AddDiagnostic(diag Diagnostic)
- func (tb *TextBlock) ClearDiagnostics()
- func (tb *TextBlock) ClearInterpolatedSource()
- func (tb *TextBlock) Diagnostics() []Diagnostic
- func (tb *TextBlock) HTML() string
- func (tb *TextBlock) InterpolatedHTMLSourceText() string
- func (tb *TextBlock) InterpolatedSource() []string
- func (tb *TextBlock) InterpolatedSourceText() string
- func (tb *TextBlock) IsDirty() bool
- func (tb *TextBlock) Render() string
- func (tb *TextBlock) SetDirty(dirty bool)
- func (tb *TextBlock) SetHTML(html string)
- func (tb *TextBlock) SetInterpolatedHTMLSource(lines []string)
- func (tb *TextBlock) SetInterpolatedSource(lines []string)
- func (tb *TextBlock) Source() []string
- func (tb *TextBlock) SourceText() string
- func (tb *TextBlock) Type() BlockType
- type UpdateResult
Constants ¶
This section is empty.
Variables ¶
var ErrFrontmatterUnclosed = fmt.Errorf("frontmatter not closed: missing closing '---' delimiter")
ErrFrontmatterUnclosed is returned by ParseFrontmatter when a document starts with an opening `---` fence but no matching closing fence is present. Along with this error, ParseFrontmatter returns the full source as the remaining body so interactive callers (editors, LSPs) can render in-progress content instead of erroring out.
Strict callers (like NewDocument) may continue to treat this as fatal. Lenient callers should check `errors.Is(err, ErrFrontmatterUnclosed)` and use the returned source.
var Registry = []RegisteredKey{ { Name: "exchange", Type: FrontmatterKeyMapStringDecimal, Doc: "Currency exchange rates as 'FROM_TO' -> rate (e.g., 'USD_EUR': 0.92 means 1 USD = 0.92 EUR). Used by currency conversion expressions in the document.", }, { Name: "globals", Type: FrontmatterKeyMapStringString, Doc: "User-defined variables as name -> CalcMark expression string. Each value is parsed as a CalcMark expression and made available throughout the document.", }, { Name: "scale", Type: FrontmatterKeyStruct, Doc: "Document-level scale transform. Either a positive number (factor only) or a map with 'factor' and optional 'unit_categories' to restrict which categories scale.", }, { Name: "convert_to", Type: FrontmatterKeyEnumString, Doc: "Document-level unit system conversion. Either the string 'si' or 'imperial', or a map with 'system' and optional 'unit_categories' to restrict which categories convert.", EnumValues: []string{"si", "imperial"}, }, { Name: "measurement", Type: FrontmatterKeyStruct, Doc: "Disambiguates ambiguous unit names. Map with axis keys 'volume' (us|imperial), 'mass' (standard|troy), 'ton' (short|long|metric), and optional 'strict' boolean controlling formatter annotation.", }, { Name: "fiscal_year_starts", Type: FrontmatterKeyStruct, Doc: "Anchors fiscal-period expressions (FQ1, FY26, 'this fiscal quarter') to a calendar start. String value naming a month, optionally with a day (e.g., 'July', 'October 1').", }, { Name: "calendar_year_offset", Type: FrontmatterKeyEnumString, Doc: "Selects which calendar year a fiscal-year label refers to. 'before' (default) — FY label = year FY ends in (Australian government year, US tax year, most companies). 'after' — FY label = year FY starts in (some companies). Has no effect when fiscal_year_starts is January.", EnumValues: []string{"before", "after"}, }, }
Registry lists every CalcMark-grammar frontmatter key recognized by ParseFrontmatter. Non-registered top-level keys are preserved verbatim in Frontmatter.Extra and carry no CalcMark semantics.
Complexity note: lookup helpers walk this slice linearly. With ~6 entries this is faster than a map and keeps the data trivially inspectable. If the registry grows past ~50 entries, consider an auxiliary map index.
var TableDirectivePattern = regexp.MustCompile(`^\s*<!--\s*table:\s*([^()]*?)\s*\(([^)]*)\)\s*-->\s*$`)
TableDirectivePattern captures the raw table name and the parenthesised column list of a directive line.
Functions ¶
func ExchangeRateKey ¶
ExchangeRateKey creates a normalized key for looking up exchange rates. Format: "FROM_TO" (e.g., "USD_EUR").
func ExtractStatementReferences ¶
ExtractStatementReferences returns the sorted set of variable names referenced by a single AST statement. For assignments, only RHS references are included (the defined variable is excluded by extractIdentifiers). Returns nil for a nil node. O(n) time where n is the number of AST nodes in the statement.
func IsRegisteredKey ¶
IsRegisteredKey reports whether name is a CalcMark-grammar frontmatter key. Comparison is case-sensitive. Runs in O(|Registry|).
func NormalizeTableName ¶ added in v2.3.0
NormalizeTableName lowercases a directive name and turns runs of whitespace into single underscores. Nothing else is stripped: if the result is not a valid identifier the directive is rejected downstream.
func ParseExchangeRateKey ¶
ParseExchangeRateKey splits a key like "USD_EUR" into (from, to) parts. Returns an error if the key format is invalid. Currency codes must be exactly 3 ASCII letters (ISO 4217 format, e.g., USD, EUR, GBP).
func RenderTextBlocksJoined ¶ added in v2.2.9
RenderTextBlocksJoined renders multiple text blocks as one markdown pass and returns the per-block HTML plus any document-wide trailing section (footnotes definitions, etc.) that only goldmark's whole-document pipeline can produce.
Why this exists: goldmark's footnote extension only resolves references whose definitions appear in the SAME markdown document. When CalcMark splits prose into separate TextBlocks around calc blocks, an in-text `[^note]` reference and its trailing `[^note]: definition` paragraph end up in distinct renders and the reference falls back to literal text. Issue #129. The fix is to join all blocks' sources with unique delimiter markers, render once so goldmark sees the whole document, then split the rendered HTML back into per-block pieces.
Returns:
- blockHTML[i] is the HTML for blocks[i], with `\x02`/`\x03` sentinels left in place so the caller can do its own interpolation-span post-processing.
- trailing is the document-level HTML that follows the last block in the joined render (typically the `<section class="footnotes">…</section>` from goldmark's footnote extension). Callers attach it wherever footnotes should appear — usually appended to the last text block or rendered as a doc-level footer.
`ok` reports whether the joined render succeeded. When ok is false, callers should fall back to per-block rendering — the per-block HTML values returned are still safe to use but footnotes etc. won't resolve across blocks.
Empty input returns (nil, "", true).
func TableDirectiveName ¶ added in v2.3.0
TableDirectiveName returns the normalized table name declared on a directive line, or "" and false when the line is not a directive.
Types ¶
type Block ¶
type Block interface {
// Type returns the block type.
Type() BlockType
// Source returns the raw source lines.
Source() []string
// IsDirty returns true if the block needs re-evaluation/rendering.
IsDirty() bool
// SetDirty marks the block as needing update.
SetDirty(bool)
}
Block represents a content block in a CalcMark document.
type BlockNode ¶
type BlockNode struct {
ID string // UUID (session-ephemeral)
Block Block // Underlying block (CalcBlock or TextBlock)
// HostLineOffset is the count of host-document lines that
// precede this block's first source line. A diagnostic on the
// block's source line N (1-based) maps to host-document line
// HostLineOffset + N.
//
// Default 0 — flat-CM parsing (NewDocument) leaves it unset and
// the evaluator falls back to its contiguous-block-source
// arithmetic (impl/document.blockLineOffset). Explicit-segmentation
// parsers like impl/embedded.BuildDocument set it per-block to
// account for fence delimiter lines that aren't represented in
// any block's Source(). If you set it, set it on every block
// for clarity.
HostLineOffset int
}
BlockNode wraps a Block with metadata for incremental updates.
type CalcBlock ¶
type CalcBlock struct {
// contains filtered or unexported fields
}
CalcBlock represents one or more consecutive calculation lines. Like a Jupyter code cell.
func NewCalcBlock ¶
NewCalcBlock creates a new calculation block.
func (*CalcBlock) AddDiagnostic ¶
func (cb *CalcBlock) AddDiagnostic(diag Diagnostic)
AddDiagnostic adds a single diagnostic to this block.
func (*CalcBlock) ClearDiagnostics ¶
func (cb *CalcBlock) ClearDiagnostics()
ClearDiagnostics removes all diagnostics from this block.
func (*CalcBlock) ConvertApplied ¶
ConvertApplied returns the per-statement conversion flags. A true value at index i means convert_to actually changed the unit for statement i (detected by comparing units before and after Apply).
func (*CalcBlock) Dependencies ¶
Dependencies returns variables referenced from other blocks.
func (*CalcBlock) Diagnostics ¶
func (cb *CalcBlock) Diagnostics() []Diagnostic
Diagnostics returns structured errors/warnings with position info.
func (*CalcBlock) ScaleExempt ¶
ScaleExempt returns the per-statement scale exemption flags. A true value at index i means statement i references @scale and should not be scaled (to prevent double-scaling).
func (*CalcBlock) SetConvertApplied ¶
SetConvertApplied sets the per-statement conversion flags.
func (*CalcBlock) SetDependencies ¶
SetDependencies sets the dependencies.
func (*CalcBlock) SetDiagnostics ¶
func (cb *CalcBlock) SetDiagnostics(diags []Diagnostic)
SetDiagnostics sets the structured diagnostics for this block.
func (*CalcBlock) SetLastValue ¶
SetLastValue sets the last evaluated value.
func (*CalcBlock) SetResults ¶
SetResults sets the per-statement evaluation results.
func (*CalcBlock) SetScaleExempt ¶
SetScaleExempt sets the per-statement scale exemption flags.
func (*CalcBlock) SetStatements ¶
SetStatements sets the parsed AST nodes.
func (*CalcBlock) SetVariables ¶
SetVariables sets the variables defined in this block.
func (*CalcBlock) Statements ¶
Statements returns the parsed AST nodes.
type CalendarYearOffset ¶
type CalendarYearOffset int
CalendarYearOffset enumerates the FY-label conventions a document can declare via the `calendar_year_offset` frontmatter key.
const ( // CalendarYearOffsetBefore is the default. The FY label corresponds // to the calendar year the FY ENDS in. CalendarYearOffsetBefore CalendarYearOffset = iota // CalendarYearOffsetAfter labels by the calendar year the FY // STARTS in. CalendarYearOffsetAfter )
type ConvertToConfig ¶
type ConvertToConfig struct {
System string // "si" or "imperial"
UnitCategories []string // empty = all
}
ConvertToConfig configures the document-level unit system conversion. When present, eligible quantities are converted to the target measurement system. UnitCategories restricts conversion to specific categories (empty = all).
type DependencyAnalyzer ¶
type DependencyAnalyzer struct{}
DependencyAnalyzer extracts variable dependencies from CalcBlocks.
func NewDependencyAnalyzer ¶
func NewDependencyAnalyzer() *DependencyAnalyzer
NewDependencyAnalyzer creates a new dependency analyzer.
func (*DependencyAnalyzer) AnalyzeBlock ¶
func (da *DependencyAnalyzer) AnalyzeBlock(block *CalcBlock) error
AnalyzeBlock parses a CalcBlock and extracts: - Variables defined (from assignments) - Variables referenced (from expressions)
type Detector ¶
type Detector struct {
// contains filtered or unexported fields
}
Detector analyzes source text and splits it into blocks.
`recognisedIdentLeads` is the union of:
- NL trigger keywords (registry-derived) like `compound`, `read`, …
- Registered function names that lex as IDENTIFIER (registry-derived) like `accumulate`, `transfer_time`, `convert_rate`, …
Built-in functions with dedicated lexer tokens (`avg`, `sqrt`, `sum`, `number`) are NOT in this set — they are caught earlier in `looksLikeCalculation` via `isFunctionToken` and never reach the IDENT path.
The classifier consults this set for the IDENT-leading line shapes that have markdown ambiguity: `name * other`, `name(args)`, etc. A line that lexes as `IDENT op …` or `IDENT LPAREN …` is admitted as calculation only when the leading name is in this set OR in the doc-scoped `knownNames` (assigned earlier in the same document). Anything outside both sets is prose typed by a user who happened to hit a token that the lexer recognises (e.g., `also *this` lexing as IDENT MULTIPLY IDENT).
func (*Detector) DetectBlocks ¶
DetectBlocks splits source into blocks using these rules: - 2 consecutive empty lines = block boundary - 1 empty line = part of current block - Calculations vs text determined by parsing each line
Unicode-aware: handles all line terminators (LF, CRLF, CR, U+2028, U+2029).
func (*Detector) IsCalculation ¶
IsCalculation checks if a line is a valid calculation. The approach: if a line parses successfully as a calculation, it's a calculation. If it fails to parse, it's text (markdown).
Returns (true, nil) for valid calculation lines. Returns (false, nil) for text lines (including invalid syntax - treated as markdown).
This is the public API for determining line type. Used by the TUI to decide how to render lines in the preview pane.
Has no doc context, so the classifier treats every leading user identifier as unknown. `DetectBlocks` calls the context-aware `isCalculationWithKnownNames` so prior `name = …` lines promote later `name * other` lines correctly.
func (*Detector) IsCalculationWithKnownNames ¶ added in v2.2.2
func (d *Detector) IsCalculationWithKnownNames(line string, knownNames map[string]bool) (bool, error)
IsCalculationWithKnownNames classifies a single line like IsCalculation, but admits lines whose leading identifier is in the doc-scoped `knownNames` set (e.g. previously-assigned variables) as calculations.
Use this from contexts that already know the document's defined names — notably the LSP, where suppressing unit / function completions on prose lines hinges on the same calc-vs-text decision the detector makes when splitting blocks.
A `nil` `knownNames` is equivalent to calling `IsCalculation`.
func (*Detector) LooksLikeCalculation ¶ added in v2.2.2
LooksLikeCalculation returns true when the token stream at the start of a line has the syntactic shape of a CalcMark calculation. Like IsCalculation it consults `knownNames` to admit references to document-defined identifiers, but unlike IsCalculation it does NOT require the line to parse end-to-end.
Use this from in-flight-typing contexts (notably the LSP completion provider) where the user's partial input — `accumulate(`, `compound 1000`, `revenue * ` — fails the strict parser but is unambiguously calc-shape. The token-shape check is what `DetectBlocks` uses internally as its final step; this method exposes it directly for callers that already have the line and want the detector's verdict without a full parse.
`lineText` may be a single line (no terminating newline required). Returns false for empty input. `knownNames` may be nil.
func (*Detector) LooksLikeCalculationForCompletion ¶ added in v2.2.7
func (d *Detector) LooksLikeCalculationForCompletion(lineText string, knownNames map[string]bool) bool
LooksLikeCalculationForCompletion is the LSP-completion variant of `LooksLikeCalculation`. It returns true for everything the strict classifier already admits AND admits one extra shape: a single bare IDENTIFIER whose value is a case-insensitive prefix of any registered IDENT-callable function name (or NL trigger keyword).
Why a separate method? `LooksLikeCalculation` is used by `DetectBlocks` to decide whether a line in a document becomes a calc block. A paragraph in prose containing the single word `compound` on its own line should stay a text block — the strict classifier's "single bare IDENT → prose" rule is correct there.
But the LSP completion provider has the user typing in-flight: the cursor sits at the end of `com` on a fresh line and the user expects `compound` to autosuggest. The same conservative rule that protects block detection silences autocomplete on the very first character of a function name — the regression cmw flagged on 2026-05-07. The lenient variant admits the bare-prefix case only when the prefix matches the registered function-name set, so arbitrary prose words like `alpha` or `hello` still classify as markdown.
Block-detection callers MUST keep using `LooksLikeCalculation`. LSP completion classifiers SHOULD use this variant.
type Diagnostic ¶
type Diagnostic struct {
BlockID string
Severity string // "error", "warning", "hint" (cascading/blocked), "info" (note on a successful result)
Code string
Message string
Detailed string // Detailed explanation with context and guidance (from semantic checker)
Line int // 1-indexed line number within the block (for block-internal lookups)
Column int // 1-indexed column number
DocLine int // 1-indexed document-absolute line number (0 if unknown)
EndLine int // 1-indexed end line (0 if unknown — falls back to start line)
EndColumn int // 1-indexed end column (0 if unknown — falls back to end-of-line)
}
Diagnostic represents a validation issue with source position info.
type Document ¶
type Document struct {
// contains filtered or unexported fields
}
Document represents a CalcMark document with incremental evaluation. Tracks blocks, dependencies, and provides efficient change propagation.
Note: Document is a pure data structure and does NOT contain interpreter state. The impl/document.Evaluator is responsible for evaluation with its own environment.
func NewDocument ¶
NewDocument creates a new document from CalcMark source and eagerly parses it.
func NewDocumentFromBlocks ¶ added in v2.2.0
func NewDocumentFromBlocks(fm *Frontmatter, blocks []Block) *Document
NewDocumentFromBlocks constructs a *Document from pre-built blocks plus an optional frontmatter. Unlike NewDocument (which parses a source string), this constructor takes the segmentation decision from its caller and just stitches the result into a real Document — wraps each block in a BlockNode with a fresh UUID, sets the frontmatter, and rebuilds the dependency graph so the evaluator sees cross-block variable references correctly.
Intended use: parser front-ends that have already done their own segmentation, so they can produce a Document whose evaluator behavior matches what NewDocument would produce for an equivalent source. The Embedded-mode parser at impl/embedded uses this to project fenced segments into CalcBlocks and passthrough segments into TextBlocks while keeping all blocks in one Document for whole-doc variable scoping.
Most consumers should use NewDocument(source) instead — it handles parsing AND construction. Reach for this helper only when you have a non-flat-CM source format that needs a custom segmentation strategy.
nil blocks slice and nil frontmatter both produce an empty but usable Document.
func (*Document) ApplyFrontmatter ¶
func (d *Document) ApplyFrontmatter(env EnvironmentWriter) error
ApplyFrontmatter injects frontmatter values (exchange rates, globals) into the given environment. This should be called before evaluation. The env parameter must implement EnvironmentWriter (e.g., *interpreter.Environment).
func (*Document) DeleteBlock ¶
func (d *Document) DeleteBlock(blockID string) (*UpdateResult, error)
DeleteBlock removes a block and updates dependents.
func (*Document) EnsureFrontmatter ¶
func (d *Document) EnsureFrontmatter() *Frontmatter
EnsureFrontmatter returns the frontmatter, creating an empty one if nil.
func (*Document) GetBlocksInDependencyOrder ¶
GetBlocksInDependencyOrder returns block IDs sorted so that dependencies come before dependents. This is a topological sort based on the dependency graph. Useful for evaluating blocks in the correct order after a change.
func (*Document) GetFrontmatter ¶
func (d *Document) GetFrontmatter() *Frontmatter
GetFrontmatter returns the document's frontmatter (may be nil).
func (*Document) GetTransitiveDependents ¶
GetTransitiveDependents returns all block IDs that transitively depend on any of the given variables. This follows the dependency chain: if a→b→c, changing 'a' returns blocks containing 'b' AND 'c'.
This is the key API for reactive UIs: when a variable changes, call this to get the minimal set of blocks that need re-evaluation.
func (*Document) InsertBlock ¶
func (d *Document) InsertBlock(afterBlockID string, blockType BlockType, source []string) (*UpdateResult, error)
InsertBlock inserts a new block after the specified block ID.
func (*Document) ReplaceBlockSource ¶
func (d *Document) ReplaceBlockSource(blockID string, newSource []string) (*UpdateResult, error)
ReplaceBlockSource replaces the source of a block and propagates changes. Returns only the affected block IDs for efficient UI updates.
func (*Document) SetFrontmatter ¶
func (d *Document) SetFrontmatter(fm *Frontmatter)
SetFrontmatter replaces the document's frontmatter.
type EnvironmentWriter ¶
type EnvironmentWriter interface {
Set(name string, value types.Type)
SetExchangeRate(from, to string, rate decimal.Decimal)
}
EnvironmentWriter can receive frontmatter values during evaluation. This interface is implemented by impl/interpreter.Environment. It decouples the spec/document package from the impl/interpreter package.
type ExtraField ¶
type ExtraField struct {
Key string
Value any // string, int, float64, []any, map[string]any
// RawValue is the verbatim YAML scalar text for scalar values, before
// type coercion — e.g. `version: 1.0` yields Value float64(1) but
// RawValue "1.0". Empty for non-scalar values (maps, lists). Lets
// consumers display exactly what the author typed instead of the
// coerced form (a `1.0` version string rendering as `1`).
RawValue string
}
ExtraField holds a non-CalcMark frontmatter key-value pair.
type FiscalYearConfig ¶
type FiscalYearConfig struct {
Month int // 1-12 (January-December)
Day int // 1-31 (defaults to 1 if not specified)
}
FiscalYearConfig holds the fiscal year start month and optional day.
type Frontmatter ¶
type Frontmatter struct {
// Exchange contains currency exchange rates as "FROM_TO" -> rate.
// Example: "USD_EUR" -> 0.92 means 1 USD = 0.92 EUR
Exchange map[string]decimal.Decimal
// Globals contains user-defined variables as name -> expression string.
// Values are CalcMark expressions that will be parsed and evaluated.
// Example: "base_date" -> "Jan 15 2025", "tax_rate" -> "0.32"
Globals map[string]string
// Scale is the document-level scale transform (nil if not present).
Scale *ScaleConfig
// ConvertTo is the document-level unit system conversion (nil if not present).
ConvertTo *ConvertToConfig
// Measurement configures how ambiguous unit names are interpreted (nil if not present).
Measurement *MeasurementFrontmatter
// FiscalYearStarts configures the start of the fiscal year (nil if not set).
// When set, fiscal expressions (FQ1, FY26, "this fiscal quarter") resolve relative
// to this configuration. When nil, fiscal expressions produce an error diagnostic.
FiscalYearStarts *FiscalYearConfig
// CalendarYearOffset selects which calendar year an FY label refers
// to. CalendarYearOffsetBefore (default) labels by the calendar
// year the FY ENDS in (matches the Australian government year,
// the US tax year, and most companies). CalendarYearOffsetAfter
// labels by the calendar year the FY STARTS in. Only has effect
// when FiscalYearStarts is non-nil and the start month is not
// January.
CalendarYearOffset CalendarYearOffset
// Extra contains non-CalcMark frontmatter keys (e.g., title, author)
// preserved in document order. Values are the raw YAML-parsed types
// (string, int, float64, []any, map[string]any).
Extra []ExtraField
// KeyRanges maps each top-level frontmatter key (registered or Extra) to
// the source range covering its key+value YAML block. Lines and columns are
// 1-based, matching ast.Position convention (see spec/ast/position.go).
// The range starts at the key identifier and ends at the last byte of its
// value (for mappings/sequences, the last byte of the deepest child).
//
// Absent keys are simply not present in the map. Use the
// `r, ok := fm.KeyRanges[key]` idiom; a returned zero-value ast.Range from a
// missing key is indistinguishable from one explicitly stored as zero.
//
// Lines are document-relative: line 1 is the opening "---" fence and the
// first key sits on line 2 in a typical frontmatter.
KeyRanges map[string]ast.Range
// contains filtered or unexported fields
}
Frontmatter represents structured metadata at the start of a CalcMark document. It is delimited by --- markers and contains YAML content.
Reserved keys (CalcMark grammar):
- exchange: Currency conversion rates
- scale: Document-level quantity scaling
- convert_to: Document-level unit system conversion
User-defined variables go under 'globals':
--- exchange: USD_EUR: 0.92 scale: 4 convert_to: imperial globals: base_date: Jan 15 2025 tax_rate: 0.32 ---
func ParseFrontmatter ¶
func ParseFrontmatter(source string) (*Frontmatter, string, error)
ParseFrontmatter extracts YAML frontmatter from the beginning of a document. Returns the parsed frontmatter, the remaining source (without frontmatter), and any error.
Frontmatter must:
- Start at line 1 with exactly "---"
- End with a line containing exactly "---"
- Contain valid YAML between the delimiters
- Unknown top-level keys are silently ignored; only CalcMark keys are processed (exchange, globals, scale, convert_to)
If no frontmatter is present, returns (nil, source, nil).
If the opening fence is found but the closing fence is missing, returns (nil, source, ErrFrontmatterUnclosed). The full source is returned as body so lenient callers can render in-progress content.
func (*Frontmatter) ExchangeKeys ¶
func (f *Frontmatter) ExchangeKeys() []string
ExchangeKeys returns exchange rate keys in insertion order. Falls back to sorted keys for backward compatibility when frontmatter is created via struct literals rather than ParseFrontmatter or SetExchangeRate.
func (*Frontmatter) GetExchangeRate ¶
func (f *Frontmatter) GetExchangeRate(from, to string) (decimal.Decimal, bool)
GetExchangeRate looks up the rate to convert from one currency to another. Returns the rate and true if found, or zero and false if not defined.
func (*Frontmatter) GlobalKeys ¶
func (f *Frontmatter) GlobalKeys() []string
GlobalKeys returns global variable names in insertion order. Falls back to sorted keys for backward compatibility when frontmatter is created via struct literals rather than ParseFrontmatter or SetGlobal.
func (*Frontmatter) HasExchangeRate ¶
func (f *Frontmatter) HasExchangeRate(key string) bool
HasExchangeRate returns true if the exchange rate is defined in frontmatter.
func (*Frontmatter) HasGlobal ¶
func (f *Frontmatter) HasGlobal(name string) bool
HasGlobal returns true if the global variable is defined in frontmatter.
func (*Frontmatter) HasGlobals ¶
func (f *Frontmatter) HasGlobals() bool
HasGlobals returns true if any globals are defined in frontmatter.
func (*Frontmatter) HasScale ¶
func (f *Frontmatter) HasScale() bool
HasScale returns true if a scale directive is defined in frontmatter.
func (*Frontmatter) LineCount ¶
func (f *Frontmatter) LineCount() int
LineCount returns the number of source lines occupied by the frontmatter (including both --- delimiters). Returns 0 if no frontmatter is present.
func (*Frontmatter) Serialize ¶
func (f *Frontmatter) Serialize() string
Serialize returns the frontmatter as a YAML string with --- delimiters. If the frontmatter has no content (no exchange rates, no globals), returns "". When rawSource is available (from parsing), the original text is preserved to support natural editing (Enter, whitespace, etc.) in the TUI editor.
func (*Frontmatter) SetExchangeRate ¶
func (f *Frontmatter) SetExchangeRate(key string, rate decimal.Decimal)
SetExchangeRate sets an exchange rate. The key should be in FROM_TO format.
func (*Frontmatter) SetGlobal ¶
func (f *Frontmatter) SetGlobal(name, valueExpr string)
SetGlobal sets a global variable value. The valueExpr is stored as the raw expression string for serialization.
func (*Frontmatter) SetRawSource ¶
func (f *Frontmatter) SetRawSource(raw string)
SetRawSource replaces the serialized frontmatter text (including --- delimiters and trailing newline) without re-parsing YAML. This allows the TUI editor to preserve user-typed text even when intermediate edits produce invalid YAML. Parsed data (Globals, Exchange) remains unchanged until a valid parse succeeds.
type FrontmatterKeyType ¶
type FrontmatterKeyType int
FrontmatterKeyType describes the value shape a registered CalcMark frontmatter key accepts. It exists so tooling (LSP completion, hover, semantic diagnostics) can describe and validate keys uniformly without special-casing each one.
const ( // FrontmatterKeyMapStringDecimal: map of string -> decimal.Decimal. // Used by `exchange` (e.g., "USD_EUR" -> 0.92). FrontmatterKeyMapStringDecimal FrontmatterKeyType = iota // FrontmatterKeyMapStringString: map of string -> string. // Used by `globals` (variable name -> CalcMark expression source). FrontmatterKeyMapStringString // FrontmatterKeyEnumString: a string drawn from a fixed set of values. // EnumValues lists the accepted values in canonical order. FrontmatterKeyEnumString // FrontmatterKeyStruct: a structured value (typically a YAML map with // known sub-keys). Sub-key shape is documented in the registered key's // Doc string and enforced by the key's parser. FrontmatterKeyStruct )
func (FrontmatterKeyType) String ¶
func (t FrontmatterKeyType) String() string
String returns a stable, human-readable label for the key type. Useful for diagnostics and tooling output.
type MeasurementFrontmatter ¶
type MeasurementFrontmatter struct {
units.MeasurementConfig
// Strict controls whether the formatter annotates bare ambiguous units in output.
// nil = use config default (true), true = annotate, false = pass through.
Strict *bool
}
MeasurementFrontmatter wraps units.MeasurementConfig with an optional Strict flag that controls formatter annotation of bare ambiguous units.
type ParsedGlobals ¶
ParsedGlobals contains parsed global variable values ready for injection into the interpreter environment.
func ParseGlobals ¶
func ParseGlobals(rawGlobals map[string]string) (*ParsedGlobals, error)
ParseGlobals parses the raw string values from frontmatter into typed CalcMark values. Only literal values are allowed (no expressions or calculations):
- Numbers: 42, 3.14, 1.5K, 25%
- Quantities: 10 meters, 5 kg, 100 MB
- Currencies: $100, 50 EUR
- Dates: Jan 15 2025, today
- Durations: 5 days, 2 weeks
- Booleans: true, false
- Rates: 100 MB/s, $50/hour
Returns an error if any value cannot be parsed or is not a literal.
func ParseGlobalsOrdered ¶
func ParseGlobalsOrdered(rawGlobals map[string]string, keys []string) (*ParsedGlobals, error)
ParseGlobalsOrdered parses globals in the specified key order. This ensures variables are processed in document order (frontmatter order).
type RegisteredKey ¶
type RegisteredKey struct {
// Name is the YAML key as it appears in document frontmatter.
// Comparison is case-sensitive.
Name string
// Type describes the accepted value shape.
Type FrontmatterKeyType
// Doc is a 1-2 sentence description of what the key configures.
// It is the source of truth for hover text in tooling (LSP).
Doc string
// EnumValues lists accepted values when Type == FrontmatterKeyEnumString.
// Empty for all other types. Order is canonical (preferred form first).
EnumValues []string
}
RegisteredKey describes a single CalcMark-specific frontmatter key. All CalcMark grammar keys (those that ParseFrontmatter recognizes and validates beyond YAML decoding) are listed in the Registry.
func LookupKey ¶
func LookupKey(name string) (RegisteredKey, bool)
LookupKey returns the RegisteredKey with the given name, if any. Comparison is case-sensitive. Runs in O(|Registry|).
type ScaleConfig ¶
type ScaleConfig struct {
Factor decimal.Decimal
UnitCategories []string // empty = nothing scales
}
ScaleConfig configures the document-level scale transform. When present, eligible quantities are multiplied by Factor. UnitCategories lists the categories to scale (empty = nothing scales).
type TextBlock ¶
type TextBlock struct {
// contains filtered or unexported fields
}
TextBlock represents markdown text. Like a Jupyter markdown cell.
func NewTextBlock ¶
NewTextBlock creates a new text block.
func (*TextBlock) AddDiagnostic ¶
func (tb *TextBlock) AddDiagnostic(diag Diagnostic)
AddDiagnostic adds a single diagnostic to this text block.
func (*TextBlock) ClearDiagnostics ¶
func (tb *TextBlock) ClearDiagnostics()
ClearDiagnostics removes all diagnostics from this text block.
func (*TextBlock) ClearInterpolatedSource ¶
func (tb *TextBlock) ClearInterpolatedSource()
ClearInterpolatedSource resets interpolation (e.g., when source changes).
func (*TextBlock) Diagnostics ¶
func (tb *TextBlock) Diagnostics() []Diagnostic
Diagnostics returns warnings for this text block.
func (*TextBlock) InterpolatedHTMLSourceText ¶
InterpolatedHTMLSourceText returns the sentinel-wrapped source for HTML rendering. Falls back to InterpolatedSourceText() when no HTML-specific source is set.
func (*TextBlock) InterpolatedSource ¶
InterpolatedSource returns interpolated lines if set, otherwise raw source. All output consumers should call this instead of Source() for display.
func (*TextBlock) InterpolatedSourceText ¶
InterpolatedSourceText returns interpolated source as a single string.
func (*TextBlock) Render ¶
Render converts the markdown source to HTML. This implementation is only available in native Go builds (not WASM).
func (*TextBlock) SetInterpolatedHTMLSource ¶
SetInterpolatedHTMLSource stores sentinel-wrapped lines for HTML rendering.
func (*TextBlock) SetInterpolatedSource ¶
SetInterpolatedSource stores the post-interpolation lines.
func (*TextBlock) SourceText ¶
SourceText returns source as a single string.
type UpdateResult ¶
type UpdateResult struct {
// ModifiedBlockID is the block that was directly modified
ModifiedBlockID string
// AffectedBlockIDs are all blocks that need UI updates
// (includes modified block + dependents)
AffectedBlockIDs []string
// Diagnostics from semantic validation
Diagnostics []Diagnostic
}
UpdateResult contains information about what changed in the document.