document

package
v1.9.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 20, 2026 License: MIT Imports: 20 Imported by: 0

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:

  1. Block types (CalcBlock, TextBlock)
  2. Document structure with block management
  3. Dependency tracking between calc blocks
  4. 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

Constants

This section is empty.

Variables

This section is empty.

Functions

func ExchangeRateKey

func ExchangeRateKey(from, to string) string

ExchangeRateKey creates a normalized key for looking up exchange rates. Format: "FROM_TO" (e.g., "USD_EUR").

func ExtractStatementReferences

func ExtractStatementReferences(node ast.Node) []string

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 ParseExchangeRateKey

func ParseExchangeRateKey(key string) (from, to string, err error)

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).

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)
}

BlockNode wraps a Block with metadata for incremental updates.

type BlockType

type BlockType int

BlockType identifies the type of block.

const (
	// BlockCalculation represents consecutive calculation lines.
	BlockCalculation BlockType = iota
	// BlockText represents markdown text.
	BlockText
)

func (BlockType) String

func (bt BlockType) String() string

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

func NewCalcBlock(source []string) *CalcBlock

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 added in v1.8.0

func (cb *CalcBlock) ConvertApplied() []bool

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

func (cb *CalcBlock) Dependencies() []string

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) Error

func (cb *CalcBlock) Error() error

Error returns any evaluation error.

func (*CalcBlock) IsDirty

func (cb *CalcBlock) IsDirty() bool

func (*CalcBlock) LastValue

func (cb *CalcBlock) LastValue() types.Type

LastValue returns the value of the last statement in the block.

func (*CalcBlock) Results

func (cb *CalcBlock) Results() []types.Type

Results returns all per-statement evaluation results.

func (*CalcBlock) ScaleExempt added in v1.6.2

func (cb *CalcBlock) ScaleExempt() []bool

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 added in v1.8.0

func (cb *CalcBlock) SetConvertApplied(applied []bool)

SetConvertApplied sets the per-statement conversion flags.

func (*CalcBlock) SetDependencies

func (cb *CalcBlock) SetDependencies(deps []string)

SetDependencies sets the dependencies.

func (*CalcBlock) SetDiagnostics

func (cb *CalcBlock) SetDiagnostics(diags []Diagnostic)

SetDiagnostics sets the structured diagnostics for this block.

func (*CalcBlock) SetDirty

func (cb *CalcBlock) SetDirty(dirty bool)

func (*CalcBlock) SetError

func (cb *CalcBlock) SetError(err error)

SetError sets the evaluation error.

func (*CalcBlock) SetLastValue

func (cb *CalcBlock) SetLastValue(val types.Type)

SetLastValue sets the last evaluated value.

func (*CalcBlock) SetResults

func (cb *CalcBlock) SetResults(results []types.Type)

SetResults sets the per-statement evaluation results.

func (*CalcBlock) SetScaleExempt added in v1.6.2

func (cb *CalcBlock) SetScaleExempt(exempt []bool)

SetScaleExempt sets the per-statement scale exemption flags.

func (*CalcBlock) SetStatements

func (cb *CalcBlock) SetStatements(stmts []ast.Node)

SetStatements sets the parsed AST nodes.

func (*CalcBlock) SetVariables

func (cb *CalcBlock) SetVariables(vars []string)

SetVariables sets the variables defined in this block.

func (*CalcBlock) Source

func (cb *CalcBlock) Source() []string

func (*CalcBlock) Statements

func (cb *CalcBlock) Statements() []ast.Node

Statements returns the parsed AST nodes.

func (*CalcBlock) Type

func (cb *CalcBlock) Type() BlockType

func (*CalcBlock) Variables

func (cb *CalcBlock) Variables() []string

Variables returns variables defined in this block.

type ConvertToConfig added in v1.6.2

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.

func NewDetector

func NewDetector() *Detector

NewDetector creates a new block detector.

func (*Detector) DetectBlocks

func (d *Detector) DetectBlocks(source string) ([]Block, error)

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

func (d *Detector) IsCalculation(line string) (bool, error)

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.

type Diagnostic

type Diagnostic struct {
	BlockID   string
	Severity  string // "error", "warning", "hint"
	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

func NewDocument(source string) (*Document, error)

NewDocument creates a new document from CalcMark source and eagerly parses it.

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) GetBlock

func (d *Document) GetBlock(id string) (*BlockNode, bool)

GetBlock returns a block by ID.

func (*Document) GetBlocks

func (d *Document) GetBlocks() []*BlockNode

GetBlocks returns all blocks in order.

func (*Document) GetBlocksInDependencyOrder

func (d *Document) GetBlocksInDependencyOrder(blockIDs []string) []string

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

func (d *Document) GetTransitiveDependents(changedVars []string) []string

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 added in v1.8.8

type ExtraField struct {
	Key   string
	Value any // string, int, float64, []any, map[string]any
}

ExtraField holds a non-CalcMark frontmatter key-value pair.

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

	// 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
	// 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).

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 added in v1.6.2

func (f *Frontmatter) HasGlobals() bool

HasGlobals returns true if any globals are defined in frontmatter.

func (*Frontmatter) HasScale added in v1.6.2

func (f *Frontmatter) HasScale() bool

HasScale returns true if a scale directive is defined in frontmatter.

func (*Frontmatter) LineCount added in v1.8.0

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 MeasurementFrontmatter added in v1.8.4

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

type ParsedGlobals struct {
	Values map[string]types.Type
}

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 ScaleConfig added in v1.6.2

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

func NewTextBlock(source []string) *TextBlock

NewTextBlock creates a new text block.

func (*TextBlock) ClearInterpolatedSource added in v1.8.0

func (tb *TextBlock) ClearInterpolatedSource()

ClearInterpolatedSource resets interpolation (e.g., when source changes).

func (*TextBlock) HTML

func (tb *TextBlock) HTML() string

HTML returns the rendered HTML.

func (*TextBlock) InterpolatedHTMLSourceText added in v1.8.0

func (tb *TextBlock) InterpolatedHTMLSourceText() string

InterpolatedHTMLSourceText returns the sentinel-wrapped source for HTML rendering. Falls back to InterpolatedSourceText() when no HTML-specific source is set.

func (*TextBlock) InterpolatedSource added in v1.8.0

func (tb *TextBlock) InterpolatedSource() []string

InterpolatedSource returns interpolated lines if set, otherwise raw source. All output consumers should call this instead of Source() for display.

func (*TextBlock) InterpolatedSourceText added in v1.8.0

func (tb *TextBlock) InterpolatedSourceText() string

InterpolatedSourceText returns interpolated source as a single string.

func (*TextBlock) IsDirty

func (tb *TextBlock) IsDirty() bool

func (*TextBlock) Render

func (tb *TextBlock) Render() string

Render converts the markdown source to HTML. This implementation is only available in native Go builds (not WASM).

func (*TextBlock) SetDirty

func (tb *TextBlock) SetDirty(dirty bool)

func (*TextBlock) SetHTML

func (tb *TextBlock) SetHTML(html string)

SetHTML sets the rendered HTML.

func (*TextBlock) SetInterpolatedHTMLSource added in v1.8.0

func (tb *TextBlock) SetInterpolatedHTMLSource(lines []string)

SetInterpolatedHTMLSource stores sentinel-wrapped lines for HTML rendering.

func (*TextBlock) SetInterpolatedSource added in v1.8.0

func (tb *TextBlock) SetInterpolatedSource(lines []string)

SetInterpolatedSource stores the post-interpolation lines.

func (*TextBlock) Source

func (tb *TextBlock) Source() []string

func (*TextBlock) SourceText

func (tb *TextBlock) SourceText() string

SourceText returns source as a single string.

func (*TextBlock) Type

func (tb *TextBlock) Type() BlockType

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL