48 KiB
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 |
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
styleorformattermust be present conditioncannot appear alone- If
conditionis 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:
- Specificity = 1 if rule has condition, 0 otherwise
- Higher specificity wins
- 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
classattribute - 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:
- If
presetis specified, apply all preset properties - 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/_DEDENTtokens - 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 terminalssimple_mode_config: CodeMirror 5 Simple Mode for syntax highlightingget_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):
- Dataclasses + Presets: Core data structures
- Resolvers: Condition evaluation, style, formatting
- Engine: Facade combining resolvers
- DSL Grammar + Parser + Transformer: Text → structured rules
- Completion: Intelligent suggestions
- 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:
- Formatting Engine: Core logic for applying rules (condition evaluation, style resolution, formatting)
- DSL Parser: Human-readable text format for defining rules with scopes and conditions
- 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:
- Write DSL in DataGridFormattingEditor
- DSL parsed → ScopedRule objects
- Rules dispatched to column/row/cell formats
- FormattingEngine applies rules during rendering
- Cells display with CSS styles + formatted values
For most use cases, the DSL provides sufficient expressiveness without requiring programmatic interaction with the formatting engine.