Files
MyFastHtml/docs/DataGrid Formatting System.md

61 KiB
Raw Blame History

DataGrid Formatting System

Introduction

The DataGrid Formatting System provides a comprehensive solution for applying conditional formatting and data transformation to DataGrid cells. It combines a powerful formatting engine with a user-friendly Domain Specific Language (DSL) and intelligent autocompletion.

Key features:

  • Conditional formatting based on cell values and cross-column comparisons
  • Rich styling with DaisyUI 5 theme presets
  • Multiple data formatters (currency, dates, percentages, text transformations, enums)
  • Text-based DSL for defining formatting rules
  • Context-aware autocompletion with CodeMirror integration
  • Five scope levels for precise targeting (cell, row, column, table, global)

Common use cases:

  • Highlight negative amounts in red
  • Format currency values with proper separators
  • Apply different styles based on status columns
  • Compare actual vs budget values
  • Display dates in localized formats
  • Transform enum values to readable labels

Architecture Overview

System Layers

The formatting system is built in three layers, each with a specific responsibility:

┌─────────────────────────────────────────────────────────┐
│                  User Interface                         │
│            DataGridFormattingEditor (DSL)               │
└─────────────────────────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────┐
│               Layer 3: Autocompletion                   │
│         FormattingCompletionEngine + Provider           │
│    (context detection, dynamic suggestions)             │
└─────────────────────────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────┐
│                 Layer 2: DSL Parser                     │
│         Lark Grammar → Transformer → ScopedRule         │
│    (text → structured format rules)                     │
└─────────────────────────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────┐
│            Layer 1: Formatting Engine                   │
│    FormattingEngine (ConditionEvaluator +               │
│       StyleResolver + FormatterResolver)                │
│    (apply rules → CSS + formatted value)                │
└─────────────────────────────────────────────────────────┘
Layer Module Responsibility
1. Formatting Engine core/formatting/ Apply format rules to cell values, resolve conflicts
2. DSL Parser core/formatting/dsl/ Parse text DSL into structured rules
3. Autocompletion core/formatting/dsl/completion/ Provide intelligent suggestions while editing
Infrastructure core/dsl/ Generic DSL framework (reusable)

End-to-End Flow

User writes DSL in editor
         │
         ▼
┌────────────────────┐
│  Lark Parser       │  Parse text with indentation support
└────────────────────┘
         │
         ▼
┌────────────────────┐
│  DSLTransformer    │  Convert AST → ScopedRule objects
└────────────────────┘
         │
         ▼
┌────────────────────┐
│  DataGrid          │  Dispatch rules to column/row/cell formats
│  FormattingEditor  │
└────────────────────┘
         │
         ▼
┌────────────────────┐
│  FormattingEngine  │  Apply rules to each cell
└────────────────────┘
         │
         ▼
    CSS + Formatted Value → Rendered Cell

Module Responsibilities

Module Location Purpose
FormattingEngine core/formatting/engine.py Main facade for applying format rules
ConditionEvaluator core/formatting/condition_evaluator.py Evaluate conditions (==, <, contains, etc.)
StyleResolver core/formatting/style_resolver.py Resolve styles to CSS strings
FormatterResolver core/formatting/formatter_resolver.py Format values for display
parse_dsl() core/formatting/dsl/__init__.py Public API for parsing DSL
DSLTransformer core/formatting/dsl/transformer.py Convert Lark AST to dataclasses
FormattingCompletionEngine core/formatting/dsl/completion/ Autocompletion for DSL
DataGridFormattingEditor controls/DataGridFormattingEditor.py DSL editor integrated in DataGrid

Component Interaction Diagrams

Rendering Pipeline

┌─────────────────────────────────────────────────────────────────────────────────┐
│                              RENDERING PIPELINE                                  │
└─────────────────────────────────────────────────────────────────────────────────┘

DataGrid._render_cell_content()
    │
    ├─ _get_format_rules(col_pos, row_idx, col_def)
    │       Priority: cell > row > column > table > tables
    │       └─ dgm.get_state().all_tables_formats   (DataGridsManager)
    │
    └─ FormattingEngine.apply_format(rules, cell_value, row_data)
            │
            ├─ _get_rule_presets()  ◄── lambda: self._formatting_provider.rule_presets
            │
            ├─ _expand_rule_presets(rules)
            │       style("traffic_light") or format("traffic_light")
            │       → replaced by RulePreset.rules
            │
            ├─ _get_matching_rules()
            │       └─ ConditionEvaluator.evaluate(condition, value, row_data)
            │
            ├─ _resolve_style(matching_rules)
            │       └─ StyleResolver.to_style_container(style)
            │               └─ DEFAULT_STYLE_PRESETS["primary"]
            │                       → {"__class__": "mf-formatting-primary"}
            │               → StyleContainer(cls="mf-formatting-primary", css="")
            │
            └─ _resolve_formatter(matching_rules)
                    └─ FormatterResolver.resolve(formatter, value)
                            └─ NumberFormatterResolver / DateFormatterResolver / ...

DSL Editing Pipeline

┌─────────────────────────────────────────────────────────────────────────────────┐
│                              DSL EDITING PIPELINE                                │
└─────────────────────────────────────────────────────────────────────────────────┘

DataGridFormattingEditor.on_content_changed()    (subclass of DslEditor)
    │
    ├─ parse_dsl(text)
    │       ├─ DSLParser.parse(text)           → lark.Tree
    │       └─ DSLTransformer.transform(tree)  → list[ScopedRule]
    │               ScopedRule = (scope, FormatRule)
    │               scopes: ColumnScope | RowScope | CellScope | TableScope | TablesScope
    │
    └─ Dispatch by scope into DataGrid state:
            ColumnScope  → col_def.format
            RowScope     → DataGridRowUiState.format
            CellScope    → state.cell_formats[cell_id]
            TableScope   → state.table_format
            TablesScope  → DataGridsManager.all_tables_formats

Autocompletion Pipeline

┌─────────────────────────────────────────────────────────────────────────────────┐
│                           AUTOCOMPLETION PIPELINE                                │
└─────────────────────────────────────────────────────────────────────────────────┘

CodeMirror (JS) → DslEditor (update)
    │
    └─ FormattingCompletionEngine.get_completions(text, cursor)
            │
            ├─ detect_scope(text, line)
            │       → ColumnScope / TablesScope / ...
            │
            ├─ detect_context(text, cursor, scope)
            │       → inside style() call? format() call? kwarg? ...
            │
            ├─ get_suggestions(context, scope, prefix)
            │       │
            │       └─ DatagridMetadataProvider  ◄────── SHARED (UniqueInstance / session)
            │               │
            │               ├─ list_style_presets()           → DEFAULT_STYLE_PRESETS keys
            │               ├─ list_rule_presets_for_style()  → RulePresets where has_style() == True
            │               ├─ list_rule_presets_for_format() → RulePresets where has_formatter() == True
            │               ├─ list_columns(table_name)       → DataServicesManager
            │               └─ list_column_values(...)        → DataServicesManager
            │
            └─ _extract_used_params() → deduplicate already-written kwargs

Preset Management and Shared Provider

┌─────────────────────────────────────────────────────────────────────────────────┐
│                     PRESET MANAGEMENT AND SHARED PROVIDER                        │
└─────────────────────────────────────────────────────────────────────────────────┘

DataGridFormattingManager
    │
    ├─ _editor: DslEditor  (preset DSL editor)
    │
    ├─ handle_save_preset()
    │       └─ _parse_dsl_to_rules(dsl)   ← parse + discard scope
    │               → preset.rules = [FormatRule, ...]
    │
    └─ _sync_provider()
            └─ DatagridMetadataProvider.rule_presets = {builtin + user presets}
                    ▲
                    │  UniqueInstance → same instance for all components in the session
                    │
            ┌───────┴────────────────────────────┐
            │                                    │
 DataGridFormattingManager               DataGrid
 DatagridMetadataProvider(self)    DatagridMetadataProvider(self._parent)
            │                                    │
            └───────────────┬────────────────────┘
                            │
                    SAME INSTANCE (per session)
                            │
                  ┌─────────┴──────────┐
                  │                    │
        completion engine         FormattingEngine
        (autocompletion)          (cell rendering)
                              rule_presets_provider=lambda: provider.rule_presets

Data Structures

┌─────────────────────────────────────────────────────────────────────────────────┐
│                                 DATA STRUCTURES                                  │
└─────────────────────────────────────────────────────────────────────────────────┘

RulePreset(name, description, dsl, rules: list[FormatRule])
    │
    └─ FormatRule(condition?, style?, formatter?)
            ├─ Condition(operator, value, negate, case_sensitive)
            ├─ Style(preset?, color?, font_weight?, ...)
            └─ Formatter (base)
                    ├─ NumberFormatter(precision, prefix, suffix, ...)
                    ├─ DateFormatter(format)
                    ├─ BooleanFormatter(true_value, false_value)
                    ├─ TextFormatter(transform, max_length)
                    ├─ EnumFormatter(source, default)
                    └─ ConstantFormatter(value)

Fundamental Concepts

FormatRule

A FormatRule is the core data structure combining three optional components:

@dataclass
class FormatRule:
  condition: Condition = None  # When to apply (optional)
  style: Style = None  # Visual formatting (optional)
  formatter: Formatter = None  # Value transformation (optional)

Rules:

  • At least one of style or formatter must be present
  • condition cannot appear alone
  • If condition is present, the rule applies only when the condition is met
  • Multiple rules can be defined; they are evaluated in order

Conflict resolution:

When multiple rules match the same cell:

  1. Specificity = 1 if rule has condition, 0 otherwise
  2. Higher specificity wins
  3. At equal specificity, last rule wins entirely (no property merging)

Example:

# Rule 1: Unconditional (specificity = 0)
FormatRule(style=Style(color="gray"))

# Rule 2: Conditional (specificity = 1)
FormatRule(
  condition=Condition(operator="<", value=0),
  style=Style(color="red")
)

# Rule 3: Conditional (specificity = 1)
FormatRule(
  condition=Condition(operator="==", value=-5),
  style=Style(color="black")
)

# For value = -5: Rule 3 wins (same specificity as Rule 2, but last)

Scopes

Scopes define where a rule applies. Five scope levels provide hierarchical targeting:

┌─────────────────────────────────────────────────────────┐
│                       TablesScope                       │
│                 (all cells in all tables)               │
│  ┌───────────────────────────────────────────────────┐  │
│  │                  TableScope                       │  │
│  │            (all cells in one table)               │  │
│  │  ┌─────────────────────────────────────────────┐  │  │
│  │  │            ColumnScope                      │  │  │
│  │  │       (all cells in column)                 │  │  │
│  │  │  ┌───────────────────────────────────────┐  │  │  │
│  │  │  │        RowScope                       │  │  │  │
│  │  │  │   (all cells in row)                  │  │  │  │
│  │  │  │  ┌─────────────────────────────────┐  │  │  │  │
│  │  │  │  │      CellScope                  │  │  │  │  │
│  │  │  │  │   (single cell)                 │  │  │  │  │
│  │  │  │  └─────────────────────────────────┘  │  │  │  │
│  │  │  └───────────────────────────────────────┘  │  │  │
│  │  └─────────────────────────────────────────────┘  │  │
│  └───────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────┘
Scope Targets Specificity DSL Syntax
Cell 1 specific cell Highest (1) cell (column, row): or cell tcell_id:
Row All cells in row High (2) row index:
Column All cells in column Medium (3) column name:
Table All cells in specific table Low (4) table "name":
Tables All cells in all tables Lowest (5) tables:

Usage:

  • Cell: Override specific cell (e.g., total cell)
  • Row: Style header row or specific data row
  • Column: Format all values in a column (e.g., currency)
  • Table: Apply defaults to all cells in one DataGrid
  • Tables: Global defaults for the entire application

Presets

Presets are named configurations that can be referenced by name instead of specifying all properties.

Style Presets (DaisyUI 5):

Preset Background Text Use Case
primary var(--color-primary) var(--color-primary-content) Important values
secondary var(--color-secondary) var(--color-secondary-content) Secondary info
accent var(--color-accent) var(--color-accent-content) Highlights
neutral var(--color-neutral) var(--color-neutral-content) Headers, labels
info var(--color-info) var(--color-info-content) Information
success var(--color-success) var(--color-success-content) Positive values
warning var(--color-warning) var(--color-warning-content) Warnings
error var(--color-error) var(--color-error-content) Errors, negatives

Formatter Presets:

Preset Type Output Example Use Case
EUR number 1 234,56 € Euro currency
USD number $1,234.56 US Dollar
percentage number 75.0% Percentages (×100)
short_date date 29/01/2026 European dates
iso_date date 2026-01-29 ISO dates
yes_no boolean Yes / No Boolean values

Customization:

Presets can be customized globally via DataGridsManager:

manager.add_style_preset("highlight", {
    "background-color": "yellow",
    "color": "black"
})

manager.add_formatter_preset("CHF", {
    "type": "number",
    "prefix": "CHF ",
    "precision": 2,
    "thousands_sep": "'"
})

CSS Classes in Style Presets:

Style presets can include a special __class__ key to apply CSS classes (DaisyUI, Tailwind, or custom):

manager.add_style_preset("badge", {
    "__class__": "badge badge-primary",
    "background-color": "blue",
    "color": "white"
})

When a preset with __class__ is applied:

  • The CSS classes are added to the element's class attribute
  • The CSS properties are applied as inline styles
  • This allows combining DaisyUI component classes with custom styling

Example with DaisyUI badges:

# Define badge presets
manager.add_style_preset("status_draft", {
    "__class__": "badge badge-neutral"
})

manager.add_style_preset("status_approved", {
    "__class__": "badge badge-success",
    "font-weight": "bold"
})

# Use in DSL
column
status:
style("status_draft") if value == "draft"
style("status_approved") if value == "approved"

Layer 1: Formatting Engine

The formatting engine (core/formatting/) is the foundation that applies formatting rules to cell values. It is * independent of the DSL* and can be used programmatically.

Dataclasses

Condition

@dataclass
class Condition:
  operator: str  # Comparison operator
  value: Any = None  # Value or reference ({"col": "..."})
  negate: bool = False  # Invert result
  case_sensitive: bool = False  # Case-sensitive strings
  col: str = None  # Column for row-level conditions
  row: int = None  # Row for column-level conditions (not implemented)

Supported operators:

Operator Description Value Type Example
== Equal scalar value == 0
!= Not equal scalar value != ""
< Less than number value < 0
<= Less or equal number value <= 100
> Greater than number value > 1000
>= Greater or equal number value >= 0
contains String contains string value contains "error"
startswith String starts with string value startswith "VIP"
endswith String ends with string value endswith ".pdf"
in Value in list list value in ["A", "B"]
between Value in range [min, max] value between [0, 100]
isempty Is null/empty - value isempty
isnotempty Not null/empty - value isnotempty
isnan Is NaN - value isnan

Column references:

Compare with another column in the same row:

Condition(
  operator=">",
  value={"col": "budget"}
)
# Evaluates: cell_value > row_data["budget"]

Row-level conditions:

Evaluate a different column for all cells in a row:

Condition(
  col="status",
  operator="==",
  value="error"
)
# For each cell in row: row_data["status"] == "error"

Style

@dataclass
class Style:
  preset: str = None  # Preset name
  background_color: str = None  # Background (hex, CSS, var())
  color: str = None  # Text color
  font_weight: str = None  # "normal" or "bold"
  font_style: str = None  # "normal" or "italic"
  font_size: str = None  # "12px", "0.9em"
  text_decoration: str = None  # "none", "underline", "line-through"

Resolution logic:

  1. If preset is specified, apply all preset properties
  2. Override with any explicit properties

Example:

Style(preset="error", font_weight="bold")
# Result: error background + error text + bold

Formatter

Base class with 6 specialized subclasses:

NumberFormatter:

@dataclass
class NumberFormatter(Formatter):
  prefix: str = ""  # Text before value ("$")
  suffix: str = ""  # Text after value (" €")
  thousands_sep: str = ""  # Thousands separator (",", " ")
  decimal_sep: str = "."  # Decimal separator
  precision: int = 0  # Decimal places
  multiplier: float = 1.0  # Multiply before display (100 for %)

DateFormatter:

@dataclass
class DateFormatter(Formatter):
  format: str = "%Y-%m-%d"  # strftime pattern

BooleanFormatter:

@dataclass
class BooleanFormatter(Formatter):
  true_value: str = "true"
  false_value: str = "false"
  null_value: str = ""

TextFormatter:

@dataclass
class TextFormatter(Formatter):
  transform: str = None  # "uppercase", "lowercase", "capitalize"
  max_length: int = None  # Truncate if exceeded
  ellipsis: str = "..."  # Suffix when truncated

EnumFormatter:

@dataclass
class EnumFormatter(Formatter):
  source: dict = field(default_factory=dict)  # Data source
  default: str = ""  # Label for unknown values
  allow_empty: bool = True
  empty_label: str = "-- Select --"
  order_by: str = "source"  # "source", "display", "value"

Source types:

  • Mapping: {"type": "mapping", "value": {"draft": "Draft", "approved": "Approved"}}
  • DataGrid: {"type": "datagrid", "value": "grid_id", "value_column": "id", "display_column": "name"}

ConstantFormatter:

@dataclass
class ConstantFormatter(Formatter):
  value: str = None  # Fixed string to display

FormattingEngine

Main facade that combines condition evaluation, style resolution, and formatting.

class FormattingEngine:
  def __init__(
      self,
      style_presets: dict = None,
      formatter_presets: dict = None,
      lookup_resolver: Callable[[str, str, str], dict] = None
  ):
    """
    Initialize the engine.

    Args:
        style_presets: Custom style presets (uses defaults if None)
        formatter_presets: Custom formatter presets (uses defaults if None)
        lookup_resolver: Function for resolving enum datagrid sources
    """

Main method:

def apply_format(
    self,
    rules: list[FormatRule],
    cell_value: Any,
    row_data: dict = None
) -> tuple[StyleContainer | None, str | None]:
  """
  Apply format rules to a cell value.

  Args:
      rules: List of FormatRule to evaluate
      cell_value: The cell value to format
      row_data: Dict of {col_id: value} for column references

  Returns:
      Tuple of (style_container, formatted_value):
      - style_container: StyleContainer with cls and css attributes, or None
      - formatted_value: Formatted string, or None
  """

Programmatic Usage

from myfasthtml.core.formatting.engine import FormattingEngine
from myfasthtml.core.formatting.dataclasses import FormatRule, Condition, Style, NumberFormatter

# Create engine
engine = FormattingEngine()

# Define rules
rules = [
    FormatRule(
      formatter=NumberFormatter(suffix=" €", precision=2, thousands_sep=" ")
    ),
    FormatRule(
      condition=Condition(operator="<", value=0),
      style=Style(preset="error")
    ),
]

# Apply to cell
style, formatted = engine.apply_format(rules, -1234.56)
# style = StyleContainer(
#     cls=None,
#     css="background-color: var(--color-error); color: var(--color-error-content);"
# )
# formatted = "-1 234,56 €"

# Access CSS string
if style:
  css_string = style.css
  css_classes = style.cls

Sub-components

ConditionEvaluator

Evaluates conditions against cell values with support for:

  • Column references: {"col": "column_name"}
  • Row-level conditions: condition.col = "status"
  • Case-sensitive/insensitive string comparisons
  • Type-safe numeric comparisons (allows int/float mixing)
  • Null-safe evaluation (returns False for None values)
evaluator = ConditionEvaluator()

condition = Condition(operator=">", value={"col": "budget"})
result = evaluator.evaluate(condition, cell_value=1000, row_data={"budget": 800})
# result = True

StyleResolver

Converts Style objects to CSS strings:

resolver = StyleResolver()

style = Style(preset="error", font_weight="bold")

# Get CSS properties dict
css_dict = resolver.resolve(style)
# {"background-color": "var(--color-error)", "color": "var(--color-error-content)", "font-weight": "bold"}

# Get CSS inline string
css_string = resolver.to_css_string(style)
# "background-color: var(--color-error); color: var(--color-error-content); font-weight: bold;"

# Get StyleContainer with classes and CSS
container = resolver.to_style_container(style)
# StyleContainer(cls=None, css="background-color: var(--color-error); ...")

StyleContainer:

The to_style_container() method returns a StyleContainer object that separates CSS classes from inline styles:

@dataclass
class StyleContainer:
  cls: str | None = None  # CSS class names
  css: str = None  # Inline CSS string

This is useful when presets include the __class__ key:

# Preset with CSS classes
custom_presets = {
    "badge": {
        "__class__": "badge badge-primary",
        "background-color": "blue"
    }
}
resolver = StyleResolver(style_presets=custom_presets)
style = Style(preset="badge")

container = resolver.to_style_container(style)
# container.cls = "badge badge-primary"
# container.css = "background-color: blue;"

FormatterResolver

Dispatches formatting to specialized resolvers based on formatter type:

resolver = FormatterResolver()

formatter = NumberFormatter(prefix="$", precision=2, thousands_sep=",")
formatted = resolver.resolve(formatter, 1234.56)
# "$1,234.56"

Error handling:

If formatting fails (e.g., non-numeric value for NumberFormatter), returns "⚠".


Layer 2: DSL Parser

The DSL provides a human-readable text format for defining formatting rules. It is parsed into structured ScopedRule objects that can be applied by the formatting engine.

DSL Syntax Overview

# Column scope
column
amount:
format("EUR")
style("error") if value < 0
style("success") if value > col.budget

# Row scope
row
0:
style("neutral", bold=True)

# Cell scope
cell(amount, 10):
style("accent", bold=True)

# Table scope
table
"orders":
style(font_size="14px")

# Global scope
tables:
style(color="#333")

Structure:

scope_header:
    rule
    rule
    ...

scope_header:
    rule
    ...
  • Rules are indented (Python-style)
  • Comments start with #
  • String values use double or single quotes

Scopes in Detail

Column Scope

Target all cells in a column by name (matches col_id first, then title):

# Simple name
column
amount:
style("error") if value < 0

# Name with spaces (quoted)
column
"total amount":
format("EUR")

Row Scope

Target all cells in a row by index (0-based):

# Header row
row
0:
style("neutral", bold=True)

# Specific row
row
5:
style("highlight")

Cell Scope

Target a specific cell by coordinates or ID:

# By coordinates (column, row)
cell(amount, 3):
style("highlight")

cell("total amount", 0):
style("neutral", bold=True)

# By cell ID
cell
tcell_grid1 - 3 - 2:
style(background_color="yellow")

Table Scope

Target all cells in a specific table (must match DataGrid _settings.name):

table
"products":
style("neutral")
format.number(precision=2)

Tables Scope

Global rules for all tables:

tables:
style(font_size="14px", color="#333")

Rules Syntax

A rule consists of optional style, optional format, and optional condition:

[style(...)] [format(...)] [if <condition>]

At least one of style() or format() must be present.

Style Expression

# Preset only
style("error")

# Preset with overrides
style("error", bold=True)
style("success", italic=True, underline=True)

# No preset, direct properties
style(color="red", bold=True)
style(background_color="#ffeeee", color="#cc0000")

Parameters:

Parameter Type Description
preset string (positional) Preset name
background_color string Background color
color string Text color
bold boolean Bold text
italic boolean Italic text
underline boolean Underlined text
strikethrough boolean Strikethrough text
font_size string Font size ("12px", "0.9em")

Format Expression

Using presets:

# Preset only
format("EUR")

# Preset with overrides
format("EUR", precision=3)
format("percentage", precision=0)

Using explicit types:

format.number(precision=2, suffix=" €", thousands_sep=" ")
format.date(format="%d/%m/%Y")
format.boolean(true_value="Yes", false_value="No")
format.text(max_length=50, ellipsis="...")
format.enum(source={"draft": "Draft", "approved": "Approved"}, default="Unknown")
format.constant(value="N/A")

Type-specific parameters:

Type Parameters
number prefix, suffix, thousands_sep, decimal_sep, precision, multiplier
date format (strftime pattern)
boolean true_value, false_value, null_value
text transform, max_length, ellipsis
enum source, default, allow_empty, empty_label, order_by
constant value

Condition Expression

if < left > < operator > < right >
if < operand > < unary_operator >

Operators:

Operator Example
==, != value == 0
<, <=, >, >= value < 0
contains value contains "error"
startswith value startswith "VIP"
endswith value endswith ".pdf"
in value in ["A", "B", "C"]
between value between 0 and 100
isempty, isnotempty value isempty
isnan value isnan

Negation:

style("error") if not value in ["valid", "approved"]
style("warning") if not value
contains
"OK"

Case sensitivity:

String comparisons are case-insensitive by default. Use (case) modifier:

style("error") if value == "Error"(case)
style("warning") if value
contains
"WARN"(case)

References:

Reference Description Example
value Current cell value value < 0
col.<name> Another column (same row) value > col.budget
col."<name>" Column with spaces value > col."max amount"

Complete Examples

Basic formatting:

column
amount:
format("EUR")
style("error") if value < 0

Cross-column comparison:

column
actual:
format("EUR")
style("error") if value > col.budget
style("warning") if value > col.budget * 0.8
style("success") if value <= col.budget * 0.8

Multiple conditions:

column
status:
style("success") if value == "approved"
style("warning") if value == "pending"
style("error") if value == "rejected"
style("neutral") if value
isempty

Complex example:

# Global defaults
tables:
style(font_size="14px", color="#333")

# Table-specific
table
"financial_report":
format.number(precision=2)

# Header row
row
0:
style("neutral", bold=True)

# Amount column
column
amount:
format.number(precision=2, suffix=" €", thousands_sep=" ")
style("error") if value < 0
style("success") if value > col.target

# Percentage column
column
progress:
format("percentage")
style("error") if value < 0.5
style("warning") if value
between
0.5 and 0.8
style("success") if value > 0.8

# Status column
column
status:
format.enum(source={"draft": "Draft", "review": "In Review", "approved": "Approved"})
style("neutral") if value == "draft"
style("info") if value == "review"
style("success") if value == "approved"

# Highlight specific cell
cell(amount, 10):
style("accent", bold=True)

Parser and Transformer

Lark Grammar

The DSL uses a Lark grammar (EBNF) with Python-style indentation:

from myfasthtml.core.formatting.dsl.grammar import GRAMMAR
# Contains the full Lark grammar string

Key features:

  • Indentation tracking with _INDENT / _DEDENT tokens
  • Comments with # (ignored)
  • Quoted strings for names with spaces
  • Support for arithmetic expressions (for future use)

Parsing API

from myfasthtml.core.formatting.dsl import parse_dsl, DSLSyntaxError

try:
  scoped_rules = parse_dsl(dsl_text)
  # scoped_rules: list[ScopedRule]
except DSLSyntaxError as e:
  print(f"Syntax error at line {e.line}, column {e.column}: {e.message}")

Output structure:

@dataclass
class ScopedRule:
  scope: ColumnScope | RowScope | CellScope | TableScope | TablesScope
  rule: FormatRule

Each ScopedRule contains:

  • scope: Where the rule applies
  • rule: What formatting to apply (condition + style + formatter)

Layer 3: Autocompletion System

The autocompletion system provides intelligent, context-aware suggestions while editing the DSL.

Architecture

┌─────────────────────────────────────────────────────────┐
│                    CodeMirror Editor                    │
│  User types: style("err|                                │
└─────────────────────────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────┐
│           FormattingCompletionEngine                    │
│                                                         │
│  1. Detect scope (which column?)                       │
│  2. Detect context (inside style()?)                   │
│  3. Generate suggestions (presets + metadata)          │
│  4. Filter by prefix ("err")                           │
└─────────────────────────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────┐
│          DatagridMetadataProvider                       │
│                                                         │
│  - list_columns() → ["amount", "status", ...]          │
│  - list_column_values("status") → ["draft", "approved"]│
│  - list_style_presets() → custom presets              │
└─────────────────────────────────────────────────────────┘

How It Works

Step 1: Scope Detection

Before suggesting completions, detect the current scope by scanning backwards:

column
amount:  # ← Scope: ColumnScope("amount")
style("error") if |  # ← Cursor here

The engine knows we're in the amount column, so it can suggest column values from that column.

Step 2: Context Detection

Analyze the current line up to the cursor to determine what's expected:

User types          Context Detected
──────────────────  ─────────────────────
col                 SCOPE_KEYWORD
column a            COLUMN_NAME
column amount:
    st              RULE_START
    style(          STYLE_ARGS
    style("         STYLE_PRESET
    style("err      STYLE_PRESET (filtered)
    style("error",  STYLE_PARAM
    style("error", bold=  BOOLEAN_VALUE
    style("error") if     CONDITION_START
    style("error") if value  OPERATOR
    style("error") if value == OPERATOR_VALUE

Step 3: Suggestion Generation

Based on context, generate appropriate suggestions:

Context Suggestions
SCOPE_KEYWORD column, row, cell, table, tables
COLUMN_NAME Column names from DataGrid
STYLE_PRESET Style presets ("error", "success", etc.)
STYLE_PARAM bold=, italic=, color=, etc.
FORMAT_TYPE number, date, boolean, text, enum, constant
OPERATOR ==, <, contains, in, etc.
OPERATOR_VALUE col., True, False + column values

Step 4: Filtering

Filter suggestions by the current prefix (case-insensitive):

# User typed: style("err
# Prefix: "err"
# Suggestions: ["error"] (filtered from all style presets)

DatagridMetadataProvider

Protocol for providing DataGrid metadata:

class DatagridMetadataProvider(Protocol):
  def list_tables(self) -> list[str]:
    """List of available DataGrid names."""
    ...
  
  def list_columns(self, table_name: str) -> list[str]:
    """Column names for a specific DataGrid."""
    ...
  
  def list_column_values(self, table_name: str, column_name: str) -> list[Any]:
    """Distinct values for a column."""
    ...
  
  def get_row_count(self, table_name: str) -> int:
    """Number of rows in a DataGrid."""
    ...
  
  def list_style_presets(self) -> list[str]:
    """Available style preset names."""
    ...
  
  def list_format_presets(self) -> list[str]:
    """Available format preset names."""
    ...

Implementations (like DataGridsManager) provide this interface to enable dynamic suggestions.

Completion Contexts

The engine recognizes ~30 distinct contexts:

Category Contexts
Scope SCOPE_KEYWORD, COLUMN_NAME, ROW_INDEX, CELL_START, CELL_COLUMN, CELL_ROW, TABLE_NAME, TABLES_SCOPE
Rule RULE_START, AFTER_STYLE_OR_FORMAT
Style STYLE_ARGS, STYLE_PRESET, STYLE_PARAM
Format FORMAT_PRESET, FORMAT_TYPE, FORMAT_PARAM_DATE, FORMAT_PARAM_TEXT
Condition CONDITION_START, CONDITION_AFTER_NOT, OPERATOR, OPERATOR_VALUE, BETWEEN_AND, BETWEEN_VALUE, IN_LIST_START, IN_LIST_VALUE
Values BOOLEAN_VALUE, COLOR_VALUE, DATE_FORMAT_VALUE, TRANSFORM_VALUE, COLUMN_REF, COLUMN_REF_QUOTED

Generic DSL Infrastructure

The core/dsl/ module provides a reusable framework for creating custom DSLs with CodeMirror integration and autocompletion.

Purpose

  • Define custom DSLs for different domains
  • Automatic syntax highlighting via CodeMirror Simple Mode
  • Context-aware autocompletion
  • Integration with DslEditor control

DSLDefinition

Abstract base class for defining a DSL:

from myfasthtml.core.dsl.base import DSLDefinition


class MyDSL(DSLDefinition):
  name: str = "My Custom DSL"
  
  def get_grammar(self) -> str:
    """Return the Lark grammar string."""
    return """
        start: statement+
        statement: NAME "=" VALUE
        ...
        """

Automatic features:

  • completions: Extracted from grammar terminals
  • simple_mode_config: CodeMirror 5 Simple Mode for syntax highlighting
  • get_editor_config(): Configuration for DslEditor JavaScript

BaseCompletionEngine

Abstract base class for completion engines:

from myfasthtml.core.dsl.base_completion import BaseCompletionEngine


class MyCompletionEngine(BaseCompletionEngine):
  def detect_scope(self, text: str, current_line: int):
    """Detect current scope from previous lines."""
    ...
  
  def detect_context(self, text: str, cursor: Position, scope):
    """Detect what kind of completion is expected."""
    ...
  
  def get_suggestions(self, context, scope, prefix) -> list[Suggestion]:
    """Generate suggestions for the context."""
    ...

Main entry point:

engine = MyCompletionEngine(provider)
result = engine.get_completions(text, cursor)
# result.suggestions: list[Suggestion]
# result.from_pos / result.to_pos: replacement range

BaseMetadataProvider

Protocol for providing domain-specific metadata:

from myfasthtml.core.dsl.base_provider import BaseMetadataProvider


class MyProvider(BaseMetadataProvider):
  # Implement methods to provide metadata for suggestions
  ...

Utilities

Lark to Simple Mode conversion:

from myfasthtml.core.dsl.lark_to_simple_mode import lark_to_simple_mode

simple_mode_config = lark_to_simple_mode(grammar_string)
# Returns CodeMirror 5 Simple Mode configuration

Extract completions from grammar:

from myfasthtml.core.dsl.lark_to_simple_mode import extract_completions_from_grammar

completions = extract_completions_from_grammar(grammar_string)
# Returns: {"keywords": [...], "operators": [...], ...}

DataGrid Integration

DataGridFormattingEditor

The DataGridFormattingEditor is a specialized DslEditor that integrates the formatting DSL into DataGrid:

class DataGridFormattingEditor(DslEditor):
  def on_content_changed(self):
    """Called when DSL content changes."""
    # 1. Parse DSL
    scoped_rules = parse_dsl(self.get_content())
    
    # 2. Group by scope
    columns_rules = defaultdict(list)
    rows_rules = defaultdict(list)
    cells_rules = defaultdict(list)
    table_rules = []
    tables_rules = []
    
    # 3. Dispatch to DataGrid state
    state.columns[i].format = rules
    state.rows[i].format = rules
    state.cell_formats[cell_id] = rules
    state.table_format = rules
    # tables rules stored in DataGridsManager

Column name resolution:

The editor matches column names by col_id first, then title:

# DSL
column
amount:  # Matches col_id="amount" or title="amount"
...

Cell ID resolution:

# By coordinates
cell(amount, 3):  # Resolved to tcell_grid1-3-0

# By ID
cell
tcell_grid1 - 3 - 0:  # Used directly

FormattingDSL

Implementation of DSLDefinition for the formatting DSL:

from myfasthtml.core.formatting.dsl.definition import FormattingDSL

dsl = FormattingDSL()
dsl.name  # "Formatting DSL"
dsl.get_grammar()  # Returns the Lark grammar
dsl.simple_mode_config  # CodeMirror Simple Mode config
dsl.completions  # Static completion items

Used by DataGridFormattingEditor to configure the CodeMirror editor.

Complete Flow in DataGrid

1. User opens formatting editor
         │
         ▼
2. DslEditor loads with FormattingDSL configuration
   - Syntax highlighting enabled
   - Autocompletion registered
         │
         ▼
3. User types DSL text
   - Autocompletion triggered on keystrokes
   - FormattingCompletionEngine provides suggestions
         │
         ▼
4. User saves or content changes
   - DataGridFormattingEditor.on_content_changed()
         │
         ▼
5. Parse DSL → ScopedRule[]
         │
         ▼
6. Dispatch rules to state
   - state.columns[i].format = [rule, ...]
   - state.rows[i].format = [rule, ...]
   - state.cell_formats[cell_id] = [rule, ...]
   - state.table_format = [rule, ...]
         │
         ▼
7. DataGrid renders cells
   - mk_body_cell_content() applies formatting
   - FormattingEngine.apply_format(rules, cell_value, row_data)
   - Returns (StyleContainer, formatted_value)
         │
         ▼
8. CSS classes + inline styles + formatted value rendered in cell
   - StyleContainer.cls applied to class attribute
   - StyleContainer.css applied as inline style

Developer Reference

Extension Points

Add Custom Style Presets

from myfasthtml.controls.DataGridsManager import DataGridsManager

manager = DataGridsManager.get_instance(session)

# Style preset with CSS properties only
manager.add_style_preset("corporate", {
    "background-color": "#003366",
    "color": "#FFFFFF",
    "font-weight": "bold"
})

# Style preset with CSS classes (DaisyUI/Tailwind)
manager.add_style_preset("badge_primary", {
    "__class__": "badge badge-primary",
    "font-weight": "bold"
})

# Style preset mixing classes and inline styles
manager.add_style_preset("highlighted", {
    "__class__": "badge badge-accent",
    "background-color": "#fef08a",
    "color": "#854d0e"
})

Usage in DSL:

column
status:
style("badge_primary") if value == "active"
style("highlighted") if value == "important"

Add Custom Formatter Presets

manager.add_formatter_preset("GBP", {
    "type": "number",
    "prefix": "£",
    "thousands_sep": ",",
    "decimal_sep": ".",
    "precision": 2
})

Implement Lookup Resolver for Enum

When using EnumFormatter with source.type = "datagrid", provide a lookup resolver:

def my_lookup_resolver(grid_id: str, value_col: str, display_col: str) -> dict:
  """
  Resolve enum values from another DataGrid.

  Returns:
      Dict mapping value_col values to display_col values
  """
  # Fetch data from grid_id
  # Build mapping {value: display}
  return mapping


engine = FormattingEngine(lookup_resolver=my_lookup_resolver)

Create Custom Formatter

from myfasthtml.core.formatting.dataclasses import Formatter
from myfasthtml.core.formatting.formatter_resolver import BaseFormatterResolver


@dataclass
class CustomFormatter(Formatter):
  custom_param: str = None


class CustomFormatterResolver(BaseFormatterResolver):
  def resolve(self, formatter: CustomFormatter, value: Any) -> str:
    # Custom formatting logic
    return str(value).upper()
  
  def apply_preset(self, formatter: Formatter, presets: dict) -> CustomFormatter:
    # Preset application logic
    return formatter


# Register in FormatterResolver
FormatterResolver._resolvers[CustomFormatter] = CustomFormatterResolver()

Create a New DSL

from myfasthtml.core.dsl.base import DSLDefinition
from myfasthtml.core.dsl.base_completion import BaseCompletionEngine


class MyDSL(DSLDefinition):
  name = "My DSL"
  
  def get_grammar(self) -> str:
    return """
        start: rule+
        rule: NAME ":" VALUE
        ...
        """


class MyCompletionEngine(BaseCompletionEngine):
  def detect_scope(self, text, line):
    # Scope detection logic
    return scope
  
  def detect_context(self, text, cursor, scope):
    # Context detection logic
    return context
  
  def get_suggestions(self, context, scope, prefix):
    # Suggestion generation
    return suggestions

Module Dependency Graph

┌─────────────────────────────────────────────────────────┐
│             controls/DataGridFormattingEditor           │
└─────────────────────────────────────────────────────────┘
                         │
        ┌────────────────┼────────────────┐
        ▼                ▼                ▼
┌──────────────┐  ┌──────────────┐  ┌──────────────┐
│ formatting/  │  │ formatting/  │  │ formatting/  │
│   dsl/       │  │   dsl/       │  │   engine.py  │
│ __init__.py  │  │ definition.py│  │              │
└──────────────┘  └──────────────┘  └──────────────┘
        │                │                │
        ▼                ▼                ▼
┌──────────────┐  ┌──────────────┐  ┌──────────────┐
│ formatting/  │  │ dsl/         │  │ formatting/  │
│   dsl/       │  │ base.py      │  │ condition_   │
│ parser.py    │  │              │  │ evaluator.py │
│ transformer  │  │              │  │ style_       │
│   .py        │  │              │  │ resolver.py  │
│ scopes.py    │  │              │  │ formatter_   │
│ grammar.py   │  │              │  │ resolver.py  │
└──────────────┘  └──────────────┘  └──────────────┘
        │                                   │
        ▼                                   ▼
┌──────────────┐                  ┌──────────────┐
│ formatting/  │                  │ formatting/  │
│ dataclasses  │◄─────────────────│ presets.py   │
│   .py        │                  │              │
└──────────────┘                  └──────────────┘

Dependency layers (bottom to top):

  1. Dataclasses + Presets: Core data structures
  2. Resolvers: Condition evaluation, style, formatting
  3. Engine: Facade combining resolvers
  4. DSL Grammar + Parser + Transformer: Text → structured rules
  5. Completion: Intelligent suggestions
  6. Integration: DataGrid editor

Public APIs by Module

Module Public API Description
core/formatting/ FormattingEngine Main engine for applying rules
core/formatting/dataclasses.py Condition, Style, Formatter, FormatRule Core data structures
core/formatting/presets.py DEFAULT_STYLE_PRESETS, DEFAULT_FORMATTER_PRESETS Built-in presets
core/formatting/dsl/ parse_dsl() Parse DSL text → list[ScopedRule]
core/formatting/dsl/ ColumnScope, RowScope, CellScope, TableScope, TablesScope Scope classes
core/formatting/dsl/ DSLSyntaxError, DSLValidationError Exceptions
core/formatting/dsl/completion/ FormattingCompletionEngine Autocompletion engine
core/formatting/dsl/completion/ DatagridMetadataProvider Metadata protocol
core/dsl/ DSLDefinition Base for creating DSLs
core/dsl/ BaseCompletionEngine Base for completion engines

Key Classes and Responsibilities

Class Location Responsibility
FormattingEngine core/formatting/engine.py Apply format rules to cell values
ConditionEvaluator core/formatting/condition_evaluator.py Evaluate 13 operators + references
StyleResolver core/formatting/style_resolver.py Resolve styles to CSS
FormatterResolver core/formatting/formatter_resolver.py Dispatch to type-specific formatters
DSLParser (internal) core/formatting/dsl/parser.py Parse DSL text with Lark
DSLTransformer core/formatting/dsl/transformer.py Convert AST → dataclasses
FormattingCompletionEngine core/formatting/dsl/completion/FormattingCompletionEngine.py Context-aware suggestions
DataGridFormattingEditor controls/DataGridFormattingEditor.py Integrate DSL into DataGrid
FormattingDSL core/formatting/dsl/definition.py DSL definition for editor

Known Limitations

Limitation Status Impact
row parameter for column-level conditions Not implemented Cannot reference specific row in column scope
AND/OR operators Not implemented Use multiple rules or in/between
Format chaining Not implemented Only one formatter per rule
Cell references in conditions Partial Only column references (col.x), not arbitrary cells
Arithmetic in conditions Parsed but not evaluated Expressions like col.budget * 0.9 not supported

Summary

The DataGrid Formatting System provides a comprehensive, three-layer architecture for conditional formatting:

  1. Formatting Engine: Core logic for applying rules (condition evaluation, style resolution, formatting)
  2. DSL Parser: Human-readable text format for defining rules with scopes and conditions
  3. Autocompletion: Intelligent suggestions based on context and DataGrid metadata

Key strengths:

  • Flexible: 5 scope levels, 13 operators, 6 formatter types
  • DaisyUI integrated: 8 style presets that adapt to theme
  • Developer-friendly: Clean API, programmatic + DSL usage
  • Extensible: Custom presets, formatters, and DSLs
  • Production-ready: Robust parsing, error handling, conflict resolution

Typical workflow:

  1. Write DSL in DataGridFormattingEditor
  2. DSL parsed → ScopedRule objects
  3. Rules dispatched to column/row/cell formats
  4. FormattingEngine applies rules during rendering
  5. Cells display with CSS styles + formatted values

For most use cases, the DSL provides sufficient expressiveness without requiring programmatic interaction with the formatting engine.