diff --git a/docs/DataGrid Formatting System.md b/docs/DataGrid Formatting System.md index 78f77b9..4c27b09 100644 --- a/docs/DataGrid Formatting System.md +++ b/docs/DataGrid Formatting System.md @@ -2,7 +2,9 @@ ## 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. +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:** @@ -59,12 +61,12 @@ The formatting system is built in three layers, each with a specific responsibil └─────────────────────────────────────────────────────────┘ ``` -| 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) | +| 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 @@ -98,16 +100,163 @@ User writes DSL in editor ### 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 | +| 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) +``` --- @@ -120,9 +269,9 @@ A `FormatRule` is the core data structure combining three optional components: ```python @dataclass class FormatRule: - condition: Condition = None # When to apply (optional) - style: Style = None # Visual formatting (optional) - formatter: Formatter = None # Value transformation (optional) + condition: Condition = None # When to apply (optional) + style: Style = None # Visual formatting (optional) + formatter: Formatter = None # Value transformation (optional) ``` **Rules:** @@ -148,14 +297,14 @@ FormatRule(style=Style(color="gray")) # Rule 2: Conditional (specificity = 1) FormatRule( - condition=Condition(operator="<", value=0), - style=Style(color="red") + condition=Condition(operator="<", value=0), + style=Style(color="red") ) # Rule 3: Conditional (specificity = 1) FormatRule( - condition=Condition(operator="==", value=-5), - style=Style(color="black") + condition=Condition(operator="==", value=-5), + style=Style(color="black") ) # For value = -5: Rule 3 wins (same specificity as Rule 2, but last) @@ -188,13 +337,13 @@ Scopes define **where** a rule applies. Five scope levels provide hierarchical t └─────────────────────────────────────────────────────────┘ ``` -| 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:` | +| 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:** @@ -210,27 +359,27 @@ Presets are named configurations that can be referenced by name instead of speci **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 | +| 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 | +| 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:** @@ -263,6 +412,7 @@ manager.add_style_preset("badge", { ``` 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 @@ -281,16 +431,18 @@ manager.add_style_preset("status_approved", { }) # Use in DSL -column status: - style("status_draft") if value == "draft" - style("status_approved") if value == "approved" +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. +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 @@ -299,32 +451,33 @@ The formatting engine (`core/formatting/`) is the foundation that applies format ```python @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) + 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` | +| 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:** @@ -332,8 +485,8 @@ Compare with another column in the same row: ```python Condition( - operator=">", - value={"col": "budget"} + operator=">", + value={"col": "budget"} ) # Evaluates: cell_value > row_data["budget"] ``` @@ -344,9 +497,9 @@ Evaluate a different column for all cells in a row: ```python Condition( - col="status", - operator="==", - value="error" + col="status", + operator="==", + value="error" ) # For each cell in row: row_data["status"] == "error" ``` @@ -356,13 +509,13 @@ Condition( ```python @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" + 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:** @@ -386,12 +539,12 @@ Base class with 6 specialized subclasses: ```python @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 %) + 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:** @@ -399,7 +552,7 @@ class NumberFormatter(Formatter): ```python @dataclass class DateFormatter(Formatter): - format: str = "%Y-%m-%d" # strftime pattern + format: str = "%Y-%m-%d" # strftime pattern ``` **BooleanFormatter:** @@ -407,9 +560,9 @@ class DateFormatter(Formatter): ```python @dataclass class BooleanFormatter(Formatter): - true_value: str = "true" - false_value: str = "false" - null_value: str = "" + true_value: str = "true" + false_value: str = "false" + null_value: str = "" ``` **TextFormatter:** @@ -417,9 +570,9 @@ class BooleanFormatter(Formatter): ```python @dataclass class TextFormatter(Formatter): - transform: str = None # "uppercase", "lowercase", "capitalize" - max_length: int = None # Truncate if exceeded - ellipsis: str = "..." # Suffix when truncated + transform: str = None # "uppercase", "lowercase", "capitalize" + max_length: int = None # Truncate if exceeded + ellipsis: str = "..." # Suffix when truncated ``` **EnumFormatter:** @@ -427,11 +580,11 @@ class TextFormatter(Formatter): ```python @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: 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:** @@ -444,7 +597,7 @@ class EnumFormatter(Formatter): ```python @dataclass class ConstantFormatter(Formatter): - value: str = None # Fixed string to display + value: str = None # Fixed string to display ``` ### FormattingEngine @@ -453,20 +606,20 @@ Main facade that combines condition evaluation, style resolution, and formatting ```python class FormattingEngine: - def __init__( - self, - style_presets: dict = None, - formatter_presets: dict = None, - lookup_resolver: Callable[[str, str, str], dict] = None - ): - """ - Initialize the engine. + 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 - """ + 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:** @@ -478,19 +631,19 @@ def apply_format( cell_value: Any, row_data: dict = None ) -> tuple[StyleContainer | None, str | None]: - """ - Apply format rules to a cell value. + """ + 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 + 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 - """ + 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 @@ -505,11 +658,11 @@ engine = FormattingEngine() # Define rules rules = [ FormatRule( - formatter=NumberFormatter(suffix=" €", precision=2, thousands_sep=" ") + formatter=NumberFormatter(suffix=" €", precision=2, thousands_sep=" ") ), FormatRule( - condition=Condition(operator="<", value=0), - style=Style(preset="error") + condition=Condition(operator="<", value=0), + style=Style(preset="error") ), ] @@ -523,8 +676,8 @@ style, formatted = engine.apply_format(rules, -1234.56) # Access CSS string if style: - css_string = style.css - css_classes = style.cls + css_string = style.css + css_classes = style.cls ``` ### Sub-components @@ -576,8 +729,8 @@ The `to_style_container()` method returns a `StyleContainer` object that separat ```python @dataclass class StyleContainer: - cls: str | None = None # CSS class names - css: str = None # Inline CSS string + cls: str | None = None # CSS class names + css: str = None # Inline CSS string ``` This is useful when presets include the `__class__` key: @@ -618,32 +771,36 @@ 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. +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 ```python # Column scope -column amount: - format("EUR") - style("error") if value < 0 - style("success") if value > col.budget +column +amount: +format("EUR") +style("error") if value < 0 +style("success") if value > col.budget # Row scope -row 0: - style("neutral", bold=True) +row +0: +style("neutral", bold=True) # Cell scope -cell (amount, 10): - style("accent", bold=True) +cell(amount, 10): +style("accent", bold=True) # Table scope -table "orders": - style(font_size="14px") +table +"orders": +style(font_size="14px") # Global scope tables: - style(color="#333") +style(color="#333") ``` **Structure:** @@ -671,12 +828,14 @@ Target all cells in a column by name (matches `col_id` first, then `title`): ```python # Simple name -column amount: - style("error") if value < 0 +column +amount: +style("error") if value < 0 # Name with spaces (quoted) -column "total amount": - format("EUR") +column +"total amount": +format("EUR") ``` #### Row Scope @@ -685,12 +844,14 @@ Target all cells in a row by index (0-based): ```python # Header row -row 0: - style("neutral", bold=True) +row +0: +style("neutral", bold=True) # Specific row -row 5: - style("highlight") +row +5: +style("highlight") ``` #### Cell Scope @@ -699,15 +860,16 @@ Target a specific cell by coordinates or ID: ```python # By coordinates (column, row) -cell (amount, 3): - style("highlight") +cell(amount, 3): +style("highlight") -cell ("total amount", 0): - style("neutral", bold=True) +cell("total amount", 0): +style("neutral", bold=True) # By cell ID -cell tcell_grid1-3-2: - style(background_color="yellow") +cell +tcell_grid1 - 3 - 2: +style(background_color="yellow") ``` #### Table Scope @@ -715,9 +877,10 @@ cell tcell_grid1-3-2: Target all cells in a specific table (must match DataGrid `_settings.name`): ```python -table "products": - style("neutral") - format.number(precision=2) +table +"products": +style("neutral") +format.number(precision=2) ``` #### Tables Scope @@ -726,7 +889,7 @@ Global rules for all tables: ```python tables: - style(font_size="14px", color="#333") +style(font_size="14px", color="#333") ``` ### Rules Syntax @@ -756,16 +919,16 @@ 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") | +| 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 @@ -793,41 +956,43 @@ 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` | +| 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 ```python -if -if +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` | +| 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:** ```python style("error") if not value in ["valid", "approved"] -style("warning") if not value contains "OK" +style("warning") if not value +contains +"OK" ``` **Case sensitivity:** @@ -835,46 +1000,52 @@ style("warning") if not value contains "OK" String comparisons are case-insensitive by default. Use `(case)` modifier: ```python -style("error") if value == "Error" (case) -style("warning") if value contains "WARN" (case) +style("error") if value == "Error"(case) +style("warning") if value +contains +"WARN"(case) ``` **References:** -| Reference | Description | Example | -|-----------|-------------|---------| -| `value` | Current cell value | `value < 0` | -| `col.` | Another column (same row) | `value > col.budget` | -| `col.""` | Column with spaces | `value > col."max amount"` | +| Reference | Description | Example | +|----------------|---------------------------|----------------------------| +| `value` | Current cell value | `value < 0` | +| `col.` | Another column (same row) | `value > col.budget` | +| `col.""` | Column with spaces | `value > col."max amount"` | ### Complete Examples **Basic formatting:** ```python -column amount: - format("EUR") - style("error") if value < 0 +column +amount: +format("EUR") +style("error") if value < 0 ``` **Cross-column comparison:** ```python -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 +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:** ```python -column status: - style("success") if value == "approved" - style("warning") if value == "pending" - style("error") if value == "rejected" - style("neutral") if value isempty +column +status: +style("success") if value == "approved" +style("warning") if value == "pending" +style("error") if value == "rejected" +style("neutral") if value +isempty ``` **Complex example:** @@ -882,39 +1053,46 @@ column status: ```python # Global defaults tables: - style(font_size="14px", color="#333") +style(font_size="14px", color="#333") # Table-specific -table "financial_report": - format.number(precision=2) +table +"financial_report": +format.number(precision=2) # Header row -row 0: - style("neutral", bold=True) +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 +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 +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" +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) +cell(amount, 10): +style("accent", bold=True) ``` ### Parser and Transformer @@ -941,10 +1119,10 @@ Key features: from myfasthtml.core.formatting.dsl import parse_dsl, DSLSyntaxError try: - scoped_rules = parse_dsl(dsl_text) - # scoped_rules: list[ScopedRule] + 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}") + print(f"Syntax error at line {e.line}, column {e.column}: {e.message}") ``` **Output structure:** @@ -952,11 +1130,12 @@ except DSLSyntaxError as e: ```python @dataclass class ScopedRule: - scope: ColumnScope | RowScope | CellScope | TableScope | TablesScope - rule: FormatRule + scope: ColumnScope | RowScope | CellScope | TableScope | TablesScope + rule: FormatRule ``` Each `ScopedRule` contains: + - **scope**: Where the rule applies - **rule**: What formatting to apply (condition + style + formatter) @@ -1001,8 +1180,9 @@ The autocompletion system provides intelligent, context-aware suggestions while Before suggesting completions, detect the current scope by scanning backwards: ```python -column amount: # ← Scope: ColumnScope("amount") - style("error") if | # ← Cursor here +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. @@ -1032,15 +1212,15 @@ column amount: 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 | +| 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** @@ -1058,29 +1238,29 @@ Protocol for providing DataGrid metadata: ```python 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.""" - ... + 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. @@ -1089,20 +1269,21 @@ Implementations (like `DataGridsManager`) provide this interface to enable dynam 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` | +| 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` | +| **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. +The `core/dsl/` module provides a **reusable framework** for creating custom DSLs with CodeMirror integration and +autocompletion. ### Purpose @@ -1118,12 +1299,13 @@ Abstract base class for defining a DSL: ```python 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 """ +class MyDSL(DSLDefinition): + name: str = "My Custom DSL" + + def get_grammar(self) -> str: + """Return the Lark grammar string.""" + return """ start: statement+ statement: NAME "=" VALUE ... @@ -1143,18 +1325,19 @@ Abstract base class for completion engines: ```python 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.""" - ... + 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:** @@ -1173,9 +1356,10 @@ Protocol for providing domain-specific metadata: ```python from myfasthtml.core.dsl.base_provider import BaseMetadataProvider + class MyProvider(BaseMetadataProvider): - # Implement methods to provide metadata for suggestions - ... + # Implement methods to provide metadata for suggestions + ... ``` ### Utilities @@ -1208,24 +1392,24 @@ The `DataGridFormattingEditor` is a specialized `DslEditor` that integrates the ```python 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 + 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:** @@ -1234,18 +1418,20 @@ The editor matches column names by `col_id` first, then `title`: ```python # DSL -column amount: # Matches col_id="amount" or title="amount" - ... +column +amount: # Matches col_id="amount" or title="amount" +... ``` **Cell ID resolution:** ```python # By coordinates -cell (amount, 3): # Resolved to tcell_grid1-3-0 +cell(amount, 3): # Resolved to tcell_grid1-3-0 # By ID -cell tcell_grid1-3-0: # Used directly +cell +tcell_grid1 - 3 - 0: # Used directly ``` ### FormattingDSL @@ -1342,9 +1528,10 @@ manager.add_style_preset("highlighted", { **Usage in DSL:** ```python -column status: - style("badge_primary") if value == "active" - style("highlighted") if value == "important" +column +status: +style("badge_primary") if value == "active" +style("highlighted") if value == "important" ``` #### Add Custom Formatter Presets @@ -1365,15 +1552,16 @@ When using `EnumFormatter` with `source.type = "datagrid"`, provide a lookup res ```python def my_lookup_resolver(grid_id: str, value_col: str, display_col: str) -> dict: - """ - Resolve enum values from another DataGrid. + """ + 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 - 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) ``` @@ -1384,18 +1572,21 @@ engine = FormattingEngine(lookup_resolver=my_lookup_resolver) from myfasthtml.core.formatting.dataclasses import Formatter from myfasthtml.core.formatting.formatter_resolver import BaseFormatterResolver + @dataclass class CustomFormatter(Formatter): - custom_param: str = None + custom_param: str = None + class CustomFormatterResolver(BaseFormatterResolver): - def resolve(self, formatter: CustomFormatter, value: Any) -> str: - # Custom formatting logic - return str(value).upper() + 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 - def apply_preset(self, formatter: Formatter, presets: dict) -> CustomFormatter: - # Preset application logic - return formatter # Register in FormatterResolver FormatterResolver._resolvers[CustomFormatter] = CustomFormatterResolver() @@ -1407,28 +1598,30 @@ FormatterResolver._resolvers[CustomFormatter] = CustomFormatterResolver() 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 """ +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 + 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 @@ -1476,42 +1669,42 @@ class MyCompletionEngine(BaseCompletionEngine): ### 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 | +| 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 | +| 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 | +| 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 | --- @@ -1539,4 +1732,5 @@ The DataGrid Formatting System provides a comprehensive, three-layer architectur 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. +For most use cases, the DSL provides sufficient expressiveness without requiring programmatic interaction with the +formatting engine. diff --git a/src/myfasthtml/controls/DataGrid.py b/src/myfasthtml/controls/DataGrid.py index 49c15b5..562c9a3 100644 --- a/src/myfasthtml/controls/DataGrid.py +++ b/src/myfasthtml/controls/DataGrid.py @@ -220,7 +220,10 @@ class DataGrid(MultipleInstance): name, namespace = (conf.name, conf.namespace) if conf else ("No name", "__default__") self._settings = DatagridSettings(self, save_state=save_state, name=name, namespace=namespace) self._state = DatagridState(self, save_state=self._settings.save_state) - self._formatting_engine = FormattingEngine() + self._formatting_provider = DatagridMetadataProvider(self._parent) + self._formatting_engine = FormattingEngine( + rule_presets_provider=lambda: self._formatting_provider.rule_presets + ) self._columns = None self.commands = Commands(self) @@ -268,8 +271,7 @@ class DataGrid(MultipleInstance): # self._columns_manager.bind_command("SaveColumnDetails", self.commands.on_column_changed()) if self._settings.enable_formatting: - provider = DatagridMetadataProvider(self._parent) - completion_engine = FormattingCompletionEngine(provider, self.get_table_name()) + completion_engine = FormattingCompletionEngine(self._formatting_provider, self.get_table_name()) editor_conf = DslEditorConf(engine_id=completion_engine.get_id()) dsl = FormattingDSL() self._formatting_editor = DataGridFormattingEditor(self, diff --git a/src/myfasthtml/controls/HierarchicalCanvasGraph.py b/src/myfasthtml/controls/HierarchicalCanvasGraph.py index 6fe8d7f..7d3d4f9 100644 --- a/src/myfasthtml/controls/HierarchicalCanvasGraph.py +++ b/src/myfasthtml/controls/HierarchicalCanvasGraph.py @@ -140,7 +140,7 @@ class HierarchicalCanvasGraph(MultipleInstance): self._query.bind_command("CancelQuery", self.commands.apply_filter()) # Add Menu - self._menu = Menu(self, conf=MenuConf(["ResetView"])) + self._menu = Menu(self, conf=MenuConf(["ResetView"]), _id="-menu") logger.debug(f"HierarchicalCanvasGraph created with id={self._id}, " f"nodes={len(conf.nodes)}, edges={len(conf.edges)}") diff --git a/src/myfasthtml/core/formatting/dsl/completion/provider.py b/src/myfasthtml/core/formatting/dsl/completion/provider.py index 9b5e5df..cb7066f 100644 --- a/src/myfasthtml/core/formatting/dsl/completion/provider.py +++ b/src/myfasthtml/core/formatting/dsl/completion/provider.py @@ -15,12 +15,12 @@ from myfasthtml.core.formatting.dataclasses import RulePreset from myfasthtml.core.formatting.presets import ( DEFAULT_FORMATTER_PRESETS, DEFAULT_STYLE_PRESETS, DEFAULT_RULE_PRESETS, ) -from myfasthtml.core.instances import SingleInstance, InstancesManager +from myfasthtml.core.instances import UniqueInstance, InstancesManager logger = logging.getLogger(__name__) -class DatagridMetadataProvider(SingleInstance, BaseMetadataProvider): +class DatagridMetadataProvider(UniqueInstance, BaseMetadataProvider): """Concrete session-scoped metadata provider for DataGrid DSL engines. Implements BaseMetadataProvider by delegating live data queries to @@ -36,8 +36,7 @@ class DatagridMetadataProvider(SingleInstance, BaseMetadataProvider): all_tables_formats: Global format rules applied to all tables. """ - def __init__(self, parent=None, session: Optional[dict] = None, - _id: Optional[str] = None): + def __init__(self, parent=None, session: Optional[dict] = None, _id: Optional[str] = None): super().__init__(parent, session, _id) self.style_presets: dict = DEFAULT_STYLE_PRESETS.copy() self.formatter_presets: dict = DEFAULT_FORMATTER_PRESETS.copy() diff --git a/src/myfasthtml/core/formatting/engine.py b/src/myfasthtml/core/formatting/engine.py index 6e7f793..b64b632 100644 --- a/src/myfasthtml/core/formatting/engine.py +++ b/src/myfasthtml/core/formatting/engine.py @@ -31,7 +31,8 @@ class FormattingEngine: style_presets: dict = None, formatter_presets: dict = None, rule_presets: dict = None, - lookup_resolver: Callable[[str, str, str], dict] = None + lookup_resolver: Callable[[str, str, str], dict] = None, + rule_presets_provider: Callable[[], dict] = None, ): """ Initialize the FormattingEngine. @@ -41,11 +42,20 @@ class FormattingEngine: formatter_presets: Custom formatter presets. If None, uses defaults. rule_presets: Named rule presets (list of FormatRule dicts). If None, uses defaults. lookup_resolver: Function for resolving enum datagrid sources. + rule_presets_provider: Callable returning the current rule_presets dict. + When provided, takes precedence over rule_presets on every apply_format call. + Use this to keep the engine in sync with a shared provider. """ self._condition_evaluator = ConditionEvaluator() self._style_resolver = StyleResolver(style_presets) self._formatter_resolver = FormatterResolver(formatter_presets, lookup_resolver) self._rule_presets = rule_presets if rule_presets is not None else DEFAULT_RULE_PRESETS + self._rule_presets_provider = rule_presets_provider + + def _get_rule_presets(self) -> dict: + if self._rule_presets_provider is not None: + return self._rule_presets_provider() + return self._rule_presets def apply_format( self, @@ -99,8 +109,8 @@ class FormattingEngine: """ Replace any FormatRule that references a rule preset with the preset's rules. - A rule is a rule preset reference when its formatter has a preset name - that exists in rule_presets (and not in formatter_presets). + A rule is a rule preset reference when its formatter or style has a preset name + that exists in rule_presets. Args: rules: Original list of FormatRule @@ -108,22 +118,26 @@ class FormattingEngine: Returns: Expanded list with preset references replaced by their FormatRules """ + rule_presets = self._get_rule_presets() expanded = [] for rule in rules: - preset_name = self._get_rule_preset_name(rule) + preset_name = self._get_rule_preset_name(rule, rule_presets) if preset_name: - expanded.extend(self._rule_presets[preset_name].rules) + expanded.extend(rule_presets[preset_name].rules) else: expanded.append(rule) return expanded - - def _get_rule_preset_name(self, rule: FormatRule) -> str | None: - """Return the preset name if the rule's formatter references a rule preset, else None.""" - if rule.formatter is None: - return None - preset = getattr(rule.formatter, "preset", None) - if preset and preset in self._rule_presets: - return preset + + def _get_rule_preset_name(self, rule: FormatRule, rule_presets: dict) -> str | None: + """Return the preset name if the rule references a rule preset via format() or style(), else None.""" + if rule.formatter is not None: + preset = getattr(rule.formatter, "preset", None) + if preset and preset in rule_presets: + return preset + if rule.style is not None: + preset = getattr(rule.style, "preset", None) + if preset and preset in rule_presets: + return preset return None def _get_matching_rules( diff --git a/src/myfasthtml/core/instances.py b/src/myfasthtml/core/instances.py index 8803208..82f6b6d 100644 --- a/src/myfasthtml/core/instances.py +++ b/src/myfasthtml/core/instances.py @@ -5,7 +5,7 @@ from typing import Optional, Literal from myfasthtml.controls.helpers import Ids from myfasthtml.core.commands import BoundCommand, Command from myfasthtml.core.constants import NO_DEFAULT_VALUE -from myfasthtml.core.utils import pascal_to_snake, get_class, snake_to_pascal +from myfasthtml.core.utils import pascal_to_snake, get_class, snake_to_pascal, debug_session VERBOSE_VERBOSE = False @@ -36,7 +36,13 @@ class BaseInstance: session = args[1] if len(args) > 1 and isinstance(args[1], dict) else kwargs.get("session", None) _id = args[2] if len(args) > 2 and isinstance(args[2], str) else kwargs.get("_id", None) if VERBOSE_VERBOSE: - logger.debug(f" parent={parent}, session={session}, _id={_id}") + logger.debug(f" parent={parent}, session={debug_session(session)}, _id={_id}") + + # for UniqueInstance, the parent is always the ultimate root parent + if issubclass(cls, UniqueInstance): + parent = BaseInstance.get_ultimate_root_parent(parent) + if VERBOSE_VERBOSE: + logger.debug(f" UniqueInstance detected. parent is set to ultimate root {parent=}") # Compute _id _id = cls.compute_id(_id, parent) @@ -163,7 +169,7 @@ class BaseInstance: def compute_id(cls, _id: Optional[str], parent: Optional['BaseInstance']): if _id is None: prefix = cls.compute_prefix() - if issubclass(cls, SingleInstance): + if issubclass(cls, (SingleInstance, UniqueInstance)): _id = prefix else: _id = f"{prefix}-{str(uuid.uuid4())}" @@ -173,6 +179,17 @@ class BaseInstance: return f"{parent.get_id()}{_id}" return _id + + @staticmethod + def get_ultimate_root_parent(instance): + if instance is None: + return None + + parent = instance + while True: + if parent.get_parent() is None: + return parent + parent = parent.get_parent() class SingleInstance(BaseInstance): @@ -200,7 +217,7 @@ class UniqueInstance(BaseInstance): _id: Optional[str] = None, auto_register: bool = True, on_init=None): - super().__init__(parent, session, _id, auto_register) + super().__init__(BaseInstance.get_ultimate_root_parent(parent), session, _id, auto_register) if on_init is not None: on_init() diff --git a/tests/core/formatting/test_engine.py b/tests/core/formatting/test_engine.py index f546abe..e26fab9 100644 --- a/tests/core/formatting/test_engine.py +++ b/tests/core/formatting/test_engine.py @@ -350,3 +350,32 @@ class TestPresets: assert "background-color: purple" in css.css assert "color: yellow" in css.css + + def test_i_can_expand_rule_preset_via_style(self): + """A rule preset referenced via style() must be expanded like via format(). + + Why: style("traffic_light") should expand the traffic_light rule preset + (which has conditional style rules) instead of looking up "traffic_light" + as a style preset name (where it doesn't exist). + """ + engine = FormattingEngine() + rules = [FormatRule(style=Style(preset="traffic_light"))] + + css, _ = engine.apply_format(rules, cell_value=-5) + + assert css is not None + assert css.cls == "mf-formatting-error" + + def test_i_can_expand_rule_preset_via_style_with_no_match(self): + """A rule preset via style() with a non-matching condition returns no style. + + Why: traffic_light has style("error") only if value < 0. + A positive value should produce no style output. + """ + engine = FormattingEngine() + rules = [FormatRule(style=Style(preset="traffic_light"))] + + css, _ = engine.apply_format(rules, cell_value=10) + + assert css is not None + assert css.cls == "mf-formatting-success"