Fixed FormattingRules not being applied
This commit is contained in:
@@ -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:**
|
||||
|
||||
@@ -60,7 +62,7 @@ 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 |
|
||||
@@ -99,7 +101,7 @@ 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 |
|
||||
@@ -109,6 +111,153 @@ User writes DSL in editor
|
||||
| `FormattingCompletionEngine` | `core/formatting/dsl/completion/` | Autocompletion for DSL |
|
||||
| `DataGridFormattingEditor` | `controls/DataGridFormattingEditor.py` | DSL editor integrated in DataGrid |
|
||||
|
||||
### Component Interaction Diagrams
|
||||
|
||||
#### Rendering Pipeline
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ RENDERING PIPELINE │
|
||||
└─────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
DataGrid._render_cell_content()
|
||||
│
|
||||
├─ _get_format_rules(col_pos, row_idx, col_def)
|
||||
│ Priority: cell > row > column > table > tables
|
||||
│ └─ dgm.get_state().all_tables_formats (DataGridsManager)
|
||||
│
|
||||
└─ FormattingEngine.apply_format(rules, cell_value, row_data)
|
||||
│
|
||||
├─ _get_rule_presets() ◄── lambda: self._formatting_provider.rule_presets
|
||||
│
|
||||
├─ _expand_rule_presets(rules)
|
||||
│ style("traffic_light") or format("traffic_light")
|
||||
│ → replaced by RulePreset.rules
|
||||
│
|
||||
├─ _get_matching_rules()
|
||||
│ └─ ConditionEvaluator.evaluate(condition, value, row_data)
|
||||
│
|
||||
├─ _resolve_style(matching_rules)
|
||||
│ └─ StyleResolver.to_style_container(style)
|
||||
│ └─ DEFAULT_STYLE_PRESETS["primary"]
|
||||
│ → {"__class__": "mf-formatting-primary"}
|
||||
│ → StyleContainer(cls="mf-formatting-primary", css="")
|
||||
│
|
||||
└─ _resolve_formatter(matching_rules)
|
||||
└─ FormatterResolver.resolve(formatter, value)
|
||||
└─ NumberFormatterResolver / DateFormatterResolver / ...
|
||||
```
|
||||
|
||||
#### DSL Editing Pipeline
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ DSL EDITING PIPELINE │
|
||||
└─────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
DataGridFormattingEditor.on_content_changed() (subclass of DslEditor)
|
||||
│
|
||||
├─ parse_dsl(text)
|
||||
│ ├─ DSLParser.parse(text) → lark.Tree
|
||||
│ └─ DSLTransformer.transform(tree) → list[ScopedRule]
|
||||
│ ScopedRule = (scope, FormatRule)
|
||||
│ scopes: ColumnScope | RowScope | CellScope | TableScope | TablesScope
|
||||
│
|
||||
└─ Dispatch by scope into DataGrid state:
|
||||
ColumnScope → col_def.format
|
||||
RowScope → DataGridRowUiState.format
|
||||
CellScope → state.cell_formats[cell_id]
|
||||
TableScope → state.table_format
|
||||
TablesScope → DataGridsManager.all_tables_formats
|
||||
```
|
||||
|
||||
#### Autocompletion Pipeline
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ AUTOCOMPLETION PIPELINE │
|
||||
└─────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
CodeMirror (JS) → DslEditor (update)
|
||||
│
|
||||
└─ FormattingCompletionEngine.get_completions(text, cursor)
|
||||
│
|
||||
├─ detect_scope(text, line)
|
||||
│ → ColumnScope / TablesScope / ...
|
||||
│
|
||||
├─ detect_context(text, cursor, scope)
|
||||
│ → inside style() call? format() call? kwarg? ...
|
||||
│
|
||||
├─ get_suggestions(context, scope, prefix)
|
||||
│ │
|
||||
│ └─ DatagridMetadataProvider ◄────── SHARED (UniqueInstance / session)
|
||||
│ │
|
||||
│ ├─ list_style_presets() → DEFAULT_STYLE_PRESETS keys
|
||||
│ ├─ list_rule_presets_for_style() → RulePresets where has_style() == True
|
||||
│ ├─ list_rule_presets_for_format() → RulePresets where has_formatter() == True
|
||||
│ ├─ list_columns(table_name) → DataServicesManager
|
||||
│ └─ list_column_values(...) → DataServicesManager
|
||||
│
|
||||
└─ _extract_used_params() → deduplicate already-written kwargs
|
||||
```
|
||||
|
||||
#### Preset Management and Shared Provider
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ PRESET MANAGEMENT AND SHARED PROVIDER │
|
||||
└─────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
DataGridFormattingManager
|
||||
│
|
||||
├─ _editor: DslEditor (preset DSL editor)
|
||||
│
|
||||
├─ handle_save_preset()
|
||||
│ └─ _parse_dsl_to_rules(dsl) ← parse + discard scope
|
||||
│ → preset.rules = [FormatRule, ...]
|
||||
│
|
||||
└─ _sync_provider()
|
||||
└─ DatagridMetadataProvider.rule_presets = {builtin + user presets}
|
||||
▲
|
||||
│ UniqueInstance → same instance for all components in the session
|
||||
│
|
||||
┌───────┴────────────────────────────┐
|
||||
│ │
|
||||
DataGridFormattingManager DataGrid
|
||||
DatagridMetadataProvider(self) DatagridMetadataProvider(self._parent)
|
||||
│ │
|
||||
└───────────────┬────────────────────┘
|
||||
│
|
||||
SAME INSTANCE (per session)
|
||||
│
|
||||
┌─────────┴──────────┐
|
||||
│ │
|
||||
completion engine FormattingEngine
|
||||
(autocompletion) (cell rendering)
|
||||
rule_presets_provider=lambda: provider.rule_presets
|
||||
```
|
||||
|
||||
#### Data Structures
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ DATA STRUCTURES │
|
||||
└─────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
RulePreset(name, description, dsl, rules: list[FormatRule])
|
||||
│
|
||||
└─ FormatRule(condition?, style?, formatter?)
|
||||
├─ Condition(operator, value, negate, case_sensitive)
|
||||
├─ Style(preset?, color?, font_weight?, ...)
|
||||
└─ Formatter (base)
|
||||
├─ NumberFormatter(precision, prefix, suffix, ...)
|
||||
├─ DateFormatter(format)
|
||||
├─ BooleanFormatter(true_value, false_value)
|
||||
├─ TextFormatter(transform, max_length)
|
||||
├─ EnumFormatter(source, default)
|
||||
└─ ConstantFormatter(value)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fundamental Concepts
|
||||
@@ -189,7 +338,7 @@ 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:` |
|
||||
@@ -211,7 +360,7 @@ 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 |
|
||||
@@ -224,7 +373,7 @@ Presets are named configurations that can be referenced by name instead of speci
|
||||
**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) |
|
||||
@@ -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
|
||||
|
||||
@@ -310,10 +462,11 @@ class Condition:
|
||||
**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` |
|
||||
@@ -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
|
||||
@@ -757,7 +920,7 @@ style(background_color="#ffeeee", color="#cc0000")
|
||||
**Parameters:**
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
|--------------------|---------------------|-----------------------------|
|
||||
| `preset` | string (positional) | Preset name |
|
||||
| `background_color` | string | Background color |
|
||||
| `color` | string | Text color |
|
||||
@@ -794,7 +957,7 @@ 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` |
|
||||
@@ -805,14 +968,14 @@ format.constant(value="N/A")
|
||||
#### Condition Expression
|
||||
|
||||
```python
|
||||
if <left> <operator> <right>
|
||||
if <operand> <unary_operator>
|
||||
if < left > < operator > < right >
|
||||
if < operand > < unary_operator >
|
||||
```
|
||||
|
||||
**Operators:**
|
||||
|
||||
| Operator | Example |
|
||||
|----------|---------|
|
||||
|-------------------------|----------------------------|
|
||||
| `==`, `!=` | `value == 0` |
|
||||
| `<`, `<=`, `>`, `>=` | `value < 0` |
|
||||
| `contains` | `value contains "error"` |
|
||||
@@ -827,7 +990,9 @@ if <operand> <unary_operator>
|
||||
|
||||
```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,14 +1000,16 @@ 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.<name>` | Another column (same row) | `value > col.budget` |
|
||||
| `col."<name>"` | Column with spaces | `value > col."max amount"` |
|
||||
@@ -852,29 +1019,33 @@ style("warning") if value contains "WARN" (case)
|
||||
**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
|
||||
@@ -957,6 +1135,7 @@ class ScopedRule:
|
||||
```
|
||||
|
||||
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.
|
||||
@@ -1033,7 +1213,7 @@ 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.) |
|
||||
@@ -1090,7 +1270,7 @@ 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` |
|
||||
@@ -1102,7 +1282,8 @@ The engine recognizes ~30 distinct contexts:
|
||||
|
||||
## 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,6 +1299,7 @@ Abstract base class for defining a DSL:
|
||||
```python
|
||||
from myfasthtml.core.dsl.base import DSLDefinition
|
||||
|
||||
|
||||
class MyDSL(DSLDefinition):
|
||||
name: str = "My Custom DSL"
|
||||
|
||||
@@ -1143,6 +1325,7 @@ 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."""
|
||||
@@ -1173,6 +1356,7 @@ 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
|
||||
...
|
||||
@@ -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
|
||||
@@ -1375,6 +1562,7 @@ def my_lookup_resolver(grid_id: str, value_col: str, display_col: str) -> dict:
|
||||
# Build mapping {value: display}
|
||||
return mapping
|
||||
|
||||
|
||||
engine = FormattingEngine(lookup_resolver=my_lookup_resolver)
|
||||
```
|
||||
|
||||
@@ -1384,10 +1572,12 @@ 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
|
||||
|
||||
|
||||
class CustomFormatterResolver(BaseFormatterResolver):
|
||||
def resolve(self, formatter: CustomFormatter, value: Any) -> str:
|
||||
# Custom formatting logic
|
||||
@@ -1397,6 +1587,7 @@ class CustomFormatterResolver(BaseFormatterResolver):
|
||||
# Preset application logic
|
||||
return formatter
|
||||
|
||||
|
||||
# Register in FormatterResolver
|
||||
FormatterResolver._resolvers[CustomFormatter] = CustomFormatterResolver()
|
||||
```
|
||||
@@ -1407,6 +1598,7 @@ 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"
|
||||
|
||||
@@ -1417,6 +1609,7 @@ class MyDSL(DSLDefinition):
|
||||
...
|
||||
"""
|
||||
|
||||
|
||||
class MyCompletionEngine(BaseCompletionEngine):
|
||||
def detect_scope(self, text, line):
|
||||
# Scope detection logic
|
||||
@@ -1477,7 +1670,7 @@ 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 |
|
||||
@@ -1492,7 +1685,7 @@ class MyCompletionEngine(BaseCompletionEngine):
|
||||
### 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 |
|
||||
@@ -1506,7 +1699,7 @@ class MyCompletionEngine(BaseCompletionEngine):
|
||||
### 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 |
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)}")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,21 +118,25 @@ 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
|
||||
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 self._rule_presets:
|
||||
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
|
||||
|
||||
|
||||
@@ -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())}"
|
||||
@@ -174,6 +180,17 @@ class BaseInstance:
|
||||
|
||||
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()
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user