Fixed FormattingRules not being applied
This commit is contained in:
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
## Introduction
|
## 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:**
|
**Key features:**
|
||||||
|
|
||||||
@@ -60,7 +62,7 @@ The formatting system is built in three layers, each with a specific responsibil
|
|||||||
```
|
```
|
||||||
|
|
||||||
| Layer | Module | Responsibility |
|
| Layer | Module | Responsibility |
|
||||||
|-------|--------|----------------|
|
|--------------------------|-----------------------------------|------------------------------------------------------|
|
||||||
| **1. Formatting Engine** | `core/formatting/` | Apply format rules to cell values, resolve conflicts |
|
| **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 |
|
| **2. DSL Parser** | `core/formatting/dsl/` | Parse text DSL into structured rules |
|
||||||
| **3. Autocompletion** | `core/formatting/dsl/completion/` | Provide intelligent suggestions while editing |
|
| **3. Autocompletion** | `core/formatting/dsl/completion/` | Provide intelligent suggestions while editing |
|
||||||
@@ -99,7 +101,7 @@ User writes DSL in editor
|
|||||||
### Module Responsibilities
|
### Module Responsibilities
|
||||||
|
|
||||||
| Module | Location | Purpose |
|
| Module | Location | Purpose |
|
||||||
|--------|----------|---------|
|
|------------------------------|------------------------------------------|---------------------------------------------|
|
||||||
| `FormattingEngine` | `core/formatting/engine.py` | Main facade for applying format rules |
|
| `FormattingEngine` | `core/formatting/engine.py` | Main facade for applying format rules |
|
||||||
| `ConditionEvaluator` | `core/formatting/condition_evaluator.py` | Evaluate conditions (==, <, contains, etc.) |
|
| `ConditionEvaluator` | `core/formatting/condition_evaluator.py` | Evaluate conditions (==, <, contains, etc.) |
|
||||||
| `StyleResolver` | `core/formatting/style_resolver.py` | Resolve styles to CSS strings |
|
| `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 |
|
| `FormattingCompletionEngine` | `core/formatting/dsl/completion/` | Autocompletion for DSL |
|
||||||
| `DataGridFormattingEditor` | `controls/DataGridFormattingEditor.py` | DSL editor integrated in DataGrid |
|
| `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
|
## Fundamental Concepts
|
||||||
@@ -189,7 +338,7 @@ Scopes define **where** a rule applies. Five scope levels provide hierarchical t
|
|||||||
```
|
```
|
||||||
|
|
||||||
| Scope | Targets | Specificity | DSL Syntax |
|
| Scope | Targets | Specificity | DSL Syntax |
|
||||||
|-------|---------|-------------|------------|
|
|------------|-----------------------------|-------------|-------------------------------------------|
|
||||||
| **Cell** | 1 specific cell | Highest (1) | `cell (column, row):` or `cell tcell_id:` |
|
| **Cell** | 1 specific cell | Highest (1) | `cell (column, row):` or `cell tcell_id:` |
|
||||||
| **Row** | All cells in row | High (2) | `row index:` |
|
| **Row** | All cells in row | High (2) | `row index:` |
|
||||||
| **Column** | All cells in column | Medium (3) | `column name:` |
|
| **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):**
|
**Style Presets (DaisyUI 5):**
|
||||||
|
|
||||||
| Preset | Background | Text | Use Case |
|
| Preset | Background | Text | Use Case |
|
||||||
|--------|------------|------|----------|
|
|-------------|--------------------------|----------------------------------|-------------------|
|
||||||
| `primary` | `var(--color-primary)` | `var(--color-primary-content)` | Important values |
|
| `primary` | `var(--color-primary)` | `var(--color-primary-content)` | Important values |
|
||||||
| `secondary` | `var(--color-secondary)` | `var(--color-secondary-content)` | Secondary info |
|
| `secondary` | `var(--color-secondary)` | `var(--color-secondary-content)` | Secondary info |
|
||||||
| `accent` | `var(--color-accent)` | `var(--color-accent-content)` | Highlights |
|
| `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:**
|
**Formatter Presets:**
|
||||||
|
|
||||||
| Preset | Type | Output Example | Use Case |
|
| Preset | Type | Output Example | Use Case |
|
||||||
|--------|------|----------------|----------|
|
|--------------|---------|----------------|--------------------|
|
||||||
| `EUR` | number | `1 234,56 €` | Euro currency |
|
| `EUR` | number | `1 234,56 €` | Euro currency |
|
||||||
| `USD` | number | `$1,234.56` | US Dollar |
|
| `USD` | number | `$1,234.56` | US Dollar |
|
||||||
| `percentage` | number | `75.0%` | Percentages (×100) |
|
| `percentage` | number | `75.0%` | Percentages (×100) |
|
||||||
@@ -263,6 +412,7 @@ manager.add_style_preset("badge", {
|
|||||||
```
|
```
|
||||||
|
|
||||||
When a preset with `__class__` is applied:
|
When a preset with `__class__` is applied:
|
||||||
|
|
||||||
- The CSS classes are added to the element's `class` attribute
|
- The CSS classes are added to the element's `class` attribute
|
||||||
- The CSS properties are applied as inline styles
|
- The CSS properties are applied as inline styles
|
||||||
- This allows combining DaisyUI component classes with custom styling
|
- This allows combining DaisyUI component classes with custom styling
|
||||||
@@ -281,16 +431,18 @@ manager.add_style_preset("status_approved", {
|
|||||||
})
|
})
|
||||||
|
|
||||||
# Use in DSL
|
# Use in DSL
|
||||||
column status:
|
column
|
||||||
style("status_draft") if value == "draft"
|
status:
|
||||||
style("status_approved") if value == "approved"
|
style("status_draft") if value == "draft"
|
||||||
|
style("status_approved") if value == "approved"
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Layer 1: Formatting Engine
|
## 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
|
### Dataclasses
|
||||||
|
|
||||||
@@ -310,10 +462,11 @@ class Condition:
|
|||||||
**Supported operators:**
|
**Supported operators:**
|
||||||
|
|
||||||
| Operator | Description | Value Type | Example |
|
| Operator | Description | Value Type | Example |
|
||||||
|----------|-------------|------------|---------|
|
|--------------|--------------------|--------------|--------------------------|
|
||||||
| `==` | Equal | scalar | `value == 0` |
|
| `==` | Equal | scalar | `value == 0` |
|
||||||
| `!=` | Not equal | scalar | `value != ""` |
|
| `!=` | Not equal | scalar | `value != ""` |
|
||||||
| `<` | Less than | number | `value < 0` |
|
| `<` | Less than | number | `value < 0` |
|
||||||
|
| | | | |
|
||||||
| `<=` | Less or equal | number | `value <= 100` |
|
| `<=` | Less or equal | number | `value <= 100` |
|
||||||
| `>` | Greater than | number | `value > 1000` |
|
| `>` | Greater than | number | `value > 1000` |
|
||||||
| `>=` | Greater or equal | number | `value >= 0` |
|
| `>=` | 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
|
## 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
|
### DSL Syntax Overview
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Column scope
|
# Column scope
|
||||||
column amount:
|
column
|
||||||
format("EUR")
|
amount:
|
||||||
style("error") if value < 0
|
format("EUR")
|
||||||
style("success") if value > col.budget
|
style("error") if value < 0
|
||||||
|
style("success") if value > col.budget
|
||||||
|
|
||||||
# Row scope
|
# Row scope
|
||||||
row 0:
|
row
|
||||||
style("neutral", bold=True)
|
0:
|
||||||
|
style("neutral", bold=True)
|
||||||
|
|
||||||
# Cell scope
|
# Cell scope
|
||||||
cell (amount, 10):
|
cell(amount, 10):
|
||||||
style("accent", bold=True)
|
style("accent", bold=True)
|
||||||
|
|
||||||
# Table scope
|
# Table scope
|
||||||
table "orders":
|
table
|
||||||
style(font_size="14px")
|
"orders":
|
||||||
|
style(font_size="14px")
|
||||||
|
|
||||||
# Global scope
|
# Global scope
|
||||||
tables:
|
tables:
|
||||||
style(color="#333")
|
style(color="#333")
|
||||||
```
|
```
|
||||||
|
|
||||||
**Structure:**
|
**Structure:**
|
||||||
@@ -671,12 +828,14 @@ Target all cells in a column by name (matches `col_id` first, then `title`):
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
# Simple name
|
# Simple name
|
||||||
column amount:
|
column
|
||||||
style("error") if value < 0
|
amount:
|
||||||
|
style("error") if value < 0
|
||||||
|
|
||||||
# Name with spaces (quoted)
|
# Name with spaces (quoted)
|
||||||
column "total amount":
|
column
|
||||||
format("EUR")
|
"total amount":
|
||||||
|
format("EUR")
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Row Scope
|
#### Row Scope
|
||||||
@@ -685,12 +844,14 @@ Target all cells in a row by index (0-based):
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
# Header row
|
# Header row
|
||||||
row 0:
|
row
|
||||||
style("neutral", bold=True)
|
0:
|
||||||
|
style("neutral", bold=True)
|
||||||
|
|
||||||
# Specific row
|
# Specific row
|
||||||
row 5:
|
row
|
||||||
style("highlight")
|
5:
|
||||||
|
style("highlight")
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Cell Scope
|
#### Cell Scope
|
||||||
@@ -699,15 +860,16 @@ Target a specific cell by coordinates or ID:
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
# By coordinates (column, row)
|
# By coordinates (column, row)
|
||||||
cell (amount, 3):
|
cell(amount, 3):
|
||||||
style("highlight")
|
style("highlight")
|
||||||
|
|
||||||
cell ("total amount", 0):
|
cell("total amount", 0):
|
||||||
style("neutral", bold=True)
|
style("neutral", bold=True)
|
||||||
|
|
||||||
# By cell ID
|
# By cell ID
|
||||||
cell tcell_grid1-3-2:
|
cell
|
||||||
style(background_color="yellow")
|
tcell_grid1 - 3 - 2:
|
||||||
|
style(background_color="yellow")
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Table Scope
|
#### Table Scope
|
||||||
@@ -715,9 +877,10 @@ cell tcell_grid1-3-2:
|
|||||||
Target all cells in a specific table (must match DataGrid `_settings.name`):
|
Target all cells in a specific table (must match DataGrid `_settings.name`):
|
||||||
|
|
||||||
```python
|
```python
|
||||||
table "products":
|
table
|
||||||
style("neutral")
|
"products":
|
||||||
format.number(precision=2)
|
style("neutral")
|
||||||
|
format.number(precision=2)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Tables Scope
|
#### Tables Scope
|
||||||
@@ -726,7 +889,7 @@ Global rules for all tables:
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
tables:
|
tables:
|
||||||
style(font_size="14px", color="#333")
|
style(font_size="14px", color="#333")
|
||||||
```
|
```
|
||||||
|
|
||||||
### Rules Syntax
|
### Rules Syntax
|
||||||
@@ -757,7 +920,7 @@ style(background_color="#ffeeee", color="#cc0000")
|
|||||||
**Parameters:**
|
**Parameters:**
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
| Parameter | Type | Description |
|
||||||
|-----------|------|-------------|
|
|--------------------|---------------------|-----------------------------|
|
||||||
| `preset` | string (positional) | Preset name |
|
| `preset` | string (positional) | Preset name |
|
||||||
| `background_color` | string | Background color |
|
| `background_color` | string | Background color |
|
||||||
| `color` | string | Text color |
|
| `color` | string | Text color |
|
||||||
@@ -794,7 +957,7 @@ format.constant(value="N/A")
|
|||||||
**Type-specific parameters:**
|
**Type-specific parameters:**
|
||||||
|
|
||||||
| Type | Parameters |
|
| Type | Parameters |
|
||||||
|------|------------|
|
|------------|-------------------------------------------------------------------------------|
|
||||||
| `number` | `prefix`, `suffix`, `thousands_sep`, `decimal_sep`, `precision`, `multiplier` |
|
| `number` | `prefix`, `suffix`, `thousands_sep`, `decimal_sep`, `precision`, `multiplier` |
|
||||||
| `date` | `format` (strftime pattern) |
|
| `date` | `format` (strftime pattern) |
|
||||||
| `boolean` | `true_value`, `false_value`, `null_value` |
|
| `boolean` | `true_value`, `false_value`, `null_value` |
|
||||||
@@ -805,14 +968,14 @@ format.constant(value="N/A")
|
|||||||
#### Condition Expression
|
#### Condition Expression
|
||||||
|
|
||||||
```python
|
```python
|
||||||
if <left> <operator> <right>
|
if < left > < operator > < right >
|
||||||
if <operand> <unary_operator>
|
if < operand > < unary_operator >
|
||||||
```
|
```
|
||||||
|
|
||||||
**Operators:**
|
**Operators:**
|
||||||
|
|
||||||
| Operator | Example |
|
| Operator | Example |
|
||||||
|----------|---------|
|
|-------------------------|----------------------------|
|
||||||
| `==`, `!=` | `value == 0` |
|
| `==`, `!=` | `value == 0` |
|
||||||
| `<`, `<=`, `>`, `>=` | `value < 0` |
|
| `<`, `<=`, `>`, `>=` | `value < 0` |
|
||||||
| `contains` | `value contains "error"` |
|
| `contains` | `value contains "error"` |
|
||||||
@@ -827,7 +990,9 @@ if <operand> <unary_operator>
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
style("error") if not value in ["valid", "approved"]
|
style("error") if not value in ["valid", "approved"]
|
||||||
style("warning") if not value contains "OK"
|
style("warning") if not value
|
||||||
|
contains
|
||||||
|
"OK"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Case sensitivity:**
|
**Case sensitivity:**
|
||||||
@@ -835,14 +1000,16 @@ style("warning") if not value contains "OK"
|
|||||||
String comparisons are case-insensitive by default. Use `(case)` modifier:
|
String comparisons are case-insensitive by default. Use `(case)` modifier:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
style("error") if value == "Error" (case)
|
style("error") if value == "Error"(case)
|
||||||
style("warning") if value contains "WARN" (case)
|
style("warning") if value
|
||||||
|
contains
|
||||||
|
"WARN"(case)
|
||||||
```
|
```
|
||||||
|
|
||||||
**References:**
|
**References:**
|
||||||
|
|
||||||
| Reference | Description | Example |
|
| Reference | Description | Example |
|
||||||
|-----------|-------------|---------|
|
|----------------|---------------------------|----------------------------|
|
||||||
| `value` | Current cell value | `value < 0` |
|
| `value` | Current cell value | `value < 0` |
|
||||||
| `col.<name>` | Another column (same row) | `value > col.budget` |
|
| `col.<name>` | Another column (same row) | `value > col.budget` |
|
||||||
| `col."<name>"` | Column with spaces | `value > col."max amount"` |
|
| `col."<name>"` | Column with spaces | `value > col."max amount"` |
|
||||||
@@ -852,29 +1019,33 @@ style("warning") if value contains "WARN" (case)
|
|||||||
**Basic formatting:**
|
**Basic formatting:**
|
||||||
|
|
||||||
```python
|
```python
|
||||||
column amount:
|
column
|
||||||
format("EUR")
|
amount:
|
||||||
style("error") if value < 0
|
format("EUR")
|
||||||
|
style("error") if value < 0
|
||||||
```
|
```
|
||||||
|
|
||||||
**Cross-column comparison:**
|
**Cross-column comparison:**
|
||||||
|
|
||||||
```python
|
```python
|
||||||
column actual:
|
column
|
||||||
format("EUR")
|
actual:
|
||||||
style("error") if value > col.budget
|
format("EUR")
|
||||||
style("warning") if value > col.budget * 0.8
|
style("error") if value > col.budget
|
||||||
style("success") if value <= col.budget * 0.8
|
style("warning") if value > col.budget * 0.8
|
||||||
|
style("success") if value <= col.budget * 0.8
|
||||||
```
|
```
|
||||||
|
|
||||||
**Multiple conditions:**
|
**Multiple conditions:**
|
||||||
|
|
||||||
```python
|
```python
|
||||||
column status:
|
column
|
||||||
style("success") if value == "approved"
|
status:
|
||||||
style("warning") if value == "pending"
|
style("success") if value == "approved"
|
||||||
style("error") if value == "rejected"
|
style("warning") if value == "pending"
|
||||||
style("neutral") if value isempty
|
style("error") if value == "rejected"
|
||||||
|
style("neutral") if value
|
||||||
|
isempty
|
||||||
```
|
```
|
||||||
|
|
||||||
**Complex example:**
|
**Complex example:**
|
||||||
@@ -882,39 +1053,46 @@ column status:
|
|||||||
```python
|
```python
|
||||||
# Global defaults
|
# Global defaults
|
||||||
tables:
|
tables:
|
||||||
style(font_size="14px", color="#333")
|
style(font_size="14px", color="#333")
|
||||||
|
|
||||||
# Table-specific
|
# Table-specific
|
||||||
table "financial_report":
|
table
|
||||||
format.number(precision=2)
|
"financial_report":
|
||||||
|
format.number(precision=2)
|
||||||
|
|
||||||
# Header row
|
# Header row
|
||||||
row 0:
|
row
|
||||||
style("neutral", bold=True)
|
0:
|
||||||
|
style("neutral", bold=True)
|
||||||
|
|
||||||
# Amount column
|
# Amount column
|
||||||
column amount:
|
column
|
||||||
format.number(precision=2, suffix=" €", thousands_sep=" ")
|
amount:
|
||||||
style("error") if value < 0
|
format.number(precision=2, suffix=" €", thousands_sep=" ")
|
||||||
style("success") if value > col.target
|
style("error") if value < 0
|
||||||
|
style("success") if value > col.target
|
||||||
|
|
||||||
# Percentage column
|
# Percentage column
|
||||||
column progress:
|
column
|
||||||
format("percentage")
|
progress:
|
||||||
style("error") if value < 0.5
|
format("percentage")
|
||||||
style("warning") if value between 0.5 and 0.8
|
style("error") if value < 0.5
|
||||||
style("success") if value > 0.8
|
style("warning") if value
|
||||||
|
between
|
||||||
|
0.5 and 0.8
|
||||||
|
style("success") if value > 0.8
|
||||||
|
|
||||||
# Status column
|
# Status column
|
||||||
column status:
|
column
|
||||||
format.enum(source={"draft": "Draft", "review": "In Review", "approved": "Approved"})
|
status:
|
||||||
style("neutral") if value == "draft"
|
format.enum(source={"draft": "Draft", "review": "In Review", "approved": "Approved"})
|
||||||
style("info") if value == "review"
|
style("neutral") if value == "draft"
|
||||||
style("success") if value == "approved"
|
style("info") if value == "review"
|
||||||
|
style("success") if value == "approved"
|
||||||
|
|
||||||
# Highlight specific cell
|
# Highlight specific cell
|
||||||
cell (amount, 10):
|
cell(amount, 10):
|
||||||
style("accent", bold=True)
|
style("accent", bold=True)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Parser and Transformer
|
### Parser and Transformer
|
||||||
@@ -957,6 +1135,7 @@ class ScopedRule:
|
|||||||
```
|
```
|
||||||
|
|
||||||
Each `ScopedRule` contains:
|
Each `ScopedRule` contains:
|
||||||
|
|
||||||
- **scope**: Where the rule applies
|
- **scope**: Where the rule applies
|
||||||
- **rule**: What formatting to apply (condition + style + formatter)
|
- **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:
|
Before suggesting completions, detect the current scope by scanning backwards:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
column amount: # ← Scope: ColumnScope("amount")
|
column
|
||||||
style("error") if | # ← Cursor here
|
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.
|
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:
|
Based on context, generate appropriate suggestions:
|
||||||
|
|
||||||
| Context | Suggestions |
|
| Context | Suggestions |
|
||||||
|---------|-------------|
|
|------------------|---------------------------------------------------------|
|
||||||
| `SCOPE_KEYWORD` | `column`, `row`, `cell`, `table`, `tables` |
|
| `SCOPE_KEYWORD` | `column`, `row`, `cell`, `table`, `tables` |
|
||||||
| `COLUMN_NAME` | Column names from DataGrid |
|
| `COLUMN_NAME` | Column names from DataGrid |
|
||||||
| `STYLE_PRESET` | Style presets ("error", "success", etc.) |
|
| `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:
|
The engine recognizes ~30 distinct contexts:
|
||||||
|
|
||||||
| Category | Contexts |
|
| Category | Contexts |
|
||||||
|----------|----------|
|
|---------------|------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| **Scope** | `SCOPE_KEYWORD`, `COLUMN_NAME`, `ROW_INDEX`, `CELL_START`, `CELL_COLUMN`, `CELL_ROW`, `TABLE_NAME`, `TABLES_SCOPE` |
|
| **Scope** | `SCOPE_KEYWORD`, `COLUMN_NAME`, `ROW_INDEX`, `CELL_START`, `CELL_COLUMN`, `CELL_ROW`, `TABLE_NAME`, `TABLES_SCOPE` |
|
||||||
| **Rule** | `RULE_START`, `AFTER_STYLE_OR_FORMAT` |
|
| **Rule** | `RULE_START`, `AFTER_STYLE_OR_FORMAT` |
|
||||||
| **Style** | `STYLE_ARGS`, `STYLE_PRESET`, `STYLE_PARAM` |
|
| **Style** | `STYLE_ARGS`, `STYLE_PRESET`, `STYLE_PARAM` |
|
||||||
@@ -1102,7 +1282,8 @@ The engine recognizes ~30 distinct contexts:
|
|||||||
|
|
||||||
## Generic DSL Infrastructure
|
## 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
|
### Purpose
|
||||||
|
|
||||||
@@ -1118,6 +1299,7 @@ Abstract base class for defining a DSL:
|
|||||||
```python
|
```python
|
||||||
from myfasthtml.core.dsl.base import DSLDefinition
|
from myfasthtml.core.dsl.base import DSLDefinition
|
||||||
|
|
||||||
|
|
||||||
class MyDSL(DSLDefinition):
|
class MyDSL(DSLDefinition):
|
||||||
name: str = "My Custom DSL"
|
name: str = "My Custom DSL"
|
||||||
|
|
||||||
@@ -1143,6 +1325,7 @@ Abstract base class for completion engines:
|
|||||||
```python
|
```python
|
||||||
from myfasthtml.core.dsl.base_completion import BaseCompletionEngine
|
from myfasthtml.core.dsl.base_completion import BaseCompletionEngine
|
||||||
|
|
||||||
|
|
||||||
class MyCompletionEngine(BaseCompletionEngine):
|
class MyCompletionEngine(BaseCompletionEngine):
|
||||||
def detect_scope(self, text: str, current_line: int):
|
def detect_scope(self, text: str, current_line: int):
|
||||||
"""Detect current scope from previous lines."""
|
"""Detect current scope from previous lines."""
|
||||||
@@ -1173,6 +1356,7 @@ Protocol for providing domain-specific metadata:
|
|||||||
```python
|
```python
|
||||||
from myfasthtml.core.dsl.base_provider import BaseMetadataProvider
|
from myfasthtml.core.dsl.base_provider import BaseMetadataProvider
|
||||||
|
|
||||||
|
|
||||||
class MyProvider(BaseMetadataProvider):
|
class MyProvider(BaseMetadataProvider):
|
||||||
# Implement methods to provide metadata for suggestions
|
# Implement methods to provide metadata for suggestions
|
||||||
...
|
...
|
||||||
@@ -1234,18 +1418,20 @@ The editor matches column names by `col_id` first, then `title`:
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
# DSL
|
# DSL
|
||||||
column amount: # Matches col_id="amount" or title="amount"
|
column
|
||||||
...
|
amount: # Matches col_id="amount" or title="amount"
|
||||||
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
**Cell ID resolution:**
|
**Cell ID resolution:**
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# By coordinates
|
# By coordinates
|
||||||
cell (amount, 3): # Resolved to tcell_grid1-3-0
|
cell(amount, 3): # Resolved to tcell_grid1-3-0
|
||||||
|
|
||||||
# By ID
|
# By ID
|
||||||
cell tcell_grid1-3-0: # Used directly
|
cell
|
||||||
|
tcell_grid1 - 3 - 0: # Used directly
|
||||||
```
|
```
|
||||||
|
|
||||||
### FormattingDSL
|
### FormattingDSL
|
||||||
@@ -1342,9 +1528,10 @@ manager.add_style_preset("highlighted", {
|
|||||||
**Usage in DSL:**
|
**Usage in DSL:**
|
||||||
|
|
||||||
```python
|
```python
|
||||||
column status:
|
column
|
||||||
style("badge_primary") if value == "active"
|
status:
|
||||||
style("highlighted") if value == "important"
|
style("badge_primary") if value == "active"
|
||||||
|
style("highlighted") if value == "important"
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Add Custom Formatter Presets
|
#### 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}
|
# Build mapping {value: display}
|
||||||
return mapping
|
return mapping
|
||||||
|
|
||||||
|
|
||||||
engine = FormattingEngine(lookup_resolver=my_lookup_resolver)
|
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.dataclasses import Formatter
|
||||||
from myfasthtml.core.formatting.formatter_resolver import BaseFormatterResolver
|
from myfasthtml.core.formatting.formatter_resolver import BaseFormatterResolver
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class CustomFormatter(Formatter):
|
class CustomFormatter(Formatter):
|
||||||
custom_param: str = None
|
custom_param: str = None
|
||||||
|
|
||||||
|
|
||||||
class CustomFormatterResolver(BaseFormatterResolver):
|
class CustomFormatterResolver(BaseFormatterResolver):
|
||||||
def resolve(self, formatter: CustomFormatter, value: Any) -> str:
|
def resolve(self, formatter: CustomFormatter, value: Any) -> str:
|
||||||
# Custom formatting logic
|
# Custom formatting logic
|
||||||
@@ -1397,6 +1587,7 @@ class CustomFormatterResolver(BaseFormatterResolver):
|
|||||||
# Preset application logic
|
# Preset application logic
|
||||||
return formatter
|
return formatter
|
||||||
|
|
||||||
|
|
||||||
# Register in FormatterResolver
|
# Register in FormatterResolver
|
||||||
FormatterResolver._resolvers[CustomFormatter] = CustomFormatterResolver()
|
FormatterResolver._resolvers[CustomFormatter] = CustomFormatterResolver()
|
||||||
```
|
```
|
||||||
@@ -1407,6 +1598,7 @@ FormatterResolver._resolvers[CustomFormatter] = CustomFormatterResolver()
|
|||||||
from myfasthtml.core.dsl.base import DSLDefinition
|
from myfasthtml.core.dsl.base import DSLDefinition
|
||||||
from myfasthtml.core.dsl.base_completion import BaseCompletionEngine
|
from myfasthtml.core.dsl.base_completion import BaseCompletionEngine
|
||||||
|
|
||||||
|
|
||||||
class MyDSL(DSLDefinition):
|
class MyDSL(DSLDefinition):
|
||||||
name = "My DSL"
|
name = "My DSL"
|
||||||
|
|
||||||
@@ -1417,6 +1609,7 @@ class MyDSL(DSLDefinition):
|
|||||||
...
|
...
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class MyCompletionEngine(BaseCompletionEngine):
|
class MyCompletionEngine(BaseCompletionEngine):
|
||||||
def detect_scope(self, text, line):
|
def detect_scope(self, text, line):
|
||||||
# Scope detection logic
|
# Scope detection logic
|
||||||
@@ -1477,7 +1670,7 @@ class MyCompletionEngine(BaseCompletionEngine):
|
|||||||
### Public APIs by Module
|
### Public APIs by Module
|
||||||
|
|
||||||
| Module | Public API | Description |
|
| Module | Public API | Description |
|
||||||
|--------|------------|-------------|
|
|-----------------------------------|---------------------------------------------------------------------|-------------------------------------|
|
||||||
| `core/formatting/` | `FormattingEngine` | Main engine for applying rules |
|
| `core/formatting/` | `FormattingEngine` | Main engine for applying rules |
|
||||||
| `core/formatting/dataclasses.py` | `Condition`, `Style`, `Formatter`, `FormatRule` | Core data structures |
|
| `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/presets.py` | `DEFAULT_STYLE_PRESETS`, `DEFAULT_FORMATTER_PRESETS` | Built-in presets |
|
||||||
@@ -1492,7 +1685,7 @@ class MyCompletionEngine(BaseCompletionEngine):
|
|||||||
### Key Classes and Responsibilities
|
### Key Classes and Responsibilities
|
||||||
|
|
||||||
| Class | Location | Responsibility |
|
| Class | Location | Responsibility |
|
||||||
|-------|----------|----------------|
|
|------------------------------|----------------------------------------------------------------|--------------------------------------|
|
||||||
| `FormattingEngine` | `core/formatting/engine.py` | Apply format rules to cell values |
|
| `FormattingEngine` | `core/formatting/engine.py` | Apply format rules to cell values |
|
||||||
| `ConditionEvaluator` | `core/formatting/condition_evaluator.py` | Evaluate 13 operators + references |
|
| `ConditionEvaluator` | `core/formatting/condition_evaluator.py` | Evaluate 13 operators + references |
|
||||||
| `StyleResolver` | `core/formatting/style_resolver.py` | Resolve styles to CSS |
|
| `StyleResolver` | `core/formatting/style_resolver.py` | Resolve styles to CSS |
|
||||||
@@ -1506,7 +1699,7 @@ class MyCompletionEngine(BaseCompletionEngine):
|
|||||||
### Known Limitations
|
### Known Limitations
|
||||||
|
|
||||||
| Limitation | Status | Impact |
|
| Limitation | Status | Impact |
|
||||||
|------------|--------|--------|
|
|---------------------------------------------|--------------------------|-------------------------------------------------------|
|
||||||
| `row` parameter for column-level conditions | Not implemented | Cannot reference specific row in column scope |
|
| `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` |
|
| AND/OR operators | Not implemented | Use multiple rules or `in`/`between` |
|
||||||
| Format chaining | Not implemented | Only one formatter per rule |
|
| 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
|
4. FormattingEngine applies rules during rendering
|
||||||
5. Cells display with CSS styles + formatted values
|
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__")
|
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._settings = DatagridSettings(self, save_state=save_state, name=name, namespace=namespace)
|
||||||
self._state = DatagridState(self, save_state=self._settings.save_state)
|
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._columns = None
|
||||||
self.commands = Commands(self)
|
self.commands = Commands(self)
|
||||||
|
|
||||||
@@ -268,8 +271,7 @@ class DataGrid(MultipleInstance):
|
|||||||
# self._columns_manager.bind_command("SaveColumnDetails", self.commands.on_column_changed())
|
# self._columns_manager.bind_command("SaveColumnDetails", self.commands.on_column_changed())
|
||||||
|
|
||||||
if self._settings.enable_formatting:
|
if self._settings.enable_formatting:
|
||||||
provider = DatagridMetadataProvider(self._parent)
|
completion_engine = FormattingCompletionEngine(self._formatting_provider, self.get_table_name())
|
||||||
completion_engine = FormattingCompletionEngine(provider, self.get_table_name())
|
|
||||||
editor_conf = DslEditorConf(engine_id=completion_engine.get_id())
|
editor_conf = DslEditorConf(engine_id=completion_engine.get_id())
|
||||||
dsl = FormattingDSL()
|
dsl = FormattingDSL()
|
||||||
self._formatting_editor = DataGridFormattingEditor(self,
|
self._formatting_editor = DataGridFormattingEditor(self,
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ class HierarchicalCanvasGraph(MultipleInstance):
|
|||||||
self._query.bind_command("CancelQuery", self.commands.apply_filter())
|
self._query.bind_command("CancelQuery", self.commands.apply_filter())
|
||||||
|
|
||||||
# Add Menu
|
# 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}, "
|
logger.debug(f"HierarchicalCanvasGraph created with id={self._id}, "
|
||||||
f"nodes={len(conf.nodes)}, edges={len(conf.edges)}")
|
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 (
|
from myfasthtml.core.formatting.presets import (
|
||||||
DEFAULT_FORMATTER_PRESETS, DEFAULT_STYLE_PRESETS, DEFAULT_RULE_PRESETS,
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class DatagridMetadataProvider(SingleInstance, BaseMetadataProvider):
|
class DatagridMetadataProvider(UniqueInstance, BaseMetadataProvider):
|
||||||
"""Concrete session-scoped metadata provider for DataGrid DSL engines.
|
"""Concrete session-scoped metadata provider for DataGrid DSL engines.
|
||||||
|
|
||||||
Implements BaseMetadataProvider by delegating live data queries to
|
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.
|
all_tables_formats: Global format rules applied to all tables.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, parent=None, session: Optional[dict] = None,
|
def __init__(self, parent=None, session: Optional[dict] = None, _id: Optional[str] = None):
|
||||||
_id: Optional[str] = None):
|
|
||||||
super().__init__(parent, session, _id)
|
super().__init__(parent, session, _id)
|
||||||
self.style_presets: dict = DEFAULT_STYLE_PRESETS.copy()
|
self.style_presets: dict = DEFAULT_STYLE_PRESETS.copy()
|
||||||
self.formatter_presets: dict = DEFAULT_FORMATTER_PRESETS.copy()
|
self.formatter_presets: dict = DEFAULT_FORMATTER_PRESETS.copy()
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ class FormattingEngine:
|
|||||||
style_presets: dict = None,
|
style_presets: dict = None,
|
||||||
formatter_presets: dict = None,
|
formatter_presets: dict = None,
|
||||||
rule_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.
|
Initialize the FormattingEngine.
|
||||||
@@ -41,11 +42,20 @@ class FormattingEngine:
|
|||||||
formatter_presets: Custom formatter presets. If None, uses defaults.
|
formatter_presets: Custom formatter presets. If None, uses defaults.
|
||||||
rule_presets: Named rule presets (list of FormatRule dicts). 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.
|
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._condition_evaluator = ConditionEvaluator()
|
||||||
self._style_resolver = StyleResolver(style_presets)
|
self._style_resolver = StyleResolver(style_presets)
|
||||||
self._formatter_resolver = FormatterResolver(formatter_presets, lookup_resolver)
|
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 = 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(
|
def apply_format(
|
||||||
self,
|
self,
|
||||||
@@ -99,8 +109,8 @@ class FormattingEngine:
|
|||||||
"""
|
"""
|
||||||
Replace any FormatRule that references a rule preset with the preset's rules.
|
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
|
A rule is a rule preset reference when its formatter or style has a preset name
|
||||||
that exists in rule_presets (and not in formatter_presets).
|
that exists in rule_presets.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
rules: Original list of FormatRule
|
rules: Original list of FormatRule
|
||||||
@@ -108,21 +118,25 @@ class FormattingEngine:
|
|||||||
Returns:
|
Returns:
|
||||||
Expanded list with preset references replaced by their FormatRules
|
Expanded list with preset references replaced by their FormatRules
|
||||||
"""
|
"""
|
||||||
|
rule_presets = self._get_rule_presets()
|
||||||
expanded = []
|
expanded = []
|
||||||
for rule in rules:
|
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:
|
if preset_name:
|
||||||
expanded.extend(self._rule_presets[preset_name].rules)
|
expanded.extend(rule_presets[preset_name].rules)
|
||||||
else:
|
else:
|
||||||
expanded.append(rule)
|
expanded.append(rule)
|
||||||
return expanded
|
return expanded
|
||||||
|
|
||||||
def _get_rule_preset_name(self, rule: FormatRule) -> str | None:
|
def _get_rule_preset_name(self, rule: FormatRule, rule_presets: dict) -> str | None:
|
||||||
"""Return the preset name if the rule's formatter references a rule preset, else None."""
|
"""Return the preset name if the rule references a rule preset via format() or style(), else None."""
|
||||||
if rule.formatter is None:
|
if rule.formatter is not None:
|
||||||
return None
|
|
||||||
preset = getattr(rule.formatter, "preset", 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 preset
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from typing import Optional, Literal
|
|||||||
from myfasthtml.controls.helpers import Ids
|
from myfasthtml.controls.helpers import Ids
|
||||||
from myfasthtml.core.commands import BoundCommand, Command
|
from myfasthtml.core.commands import BoundCommand, Command
|
||||||
from myfasthtml.core.constants import NO_DEFAULT_VALUE
|
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
|
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)
|
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)
|
_id = args[2] if len(args) > 2 and isinstance(args[2], str) else kwargs.get("_id", None)
|
||||||
if VERBOSE_VERBOSE:
|
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
|
# Compute _id
|
||||||
_id = cls.compute_id(_id, parent)
|
_id = cls.compute_id(_id, parent)
|
||||||
@@ -163,7 +169,7 @@ class BaseInstance:
|
|||||||
def compute_id(cls, _id: Optional[str], parent: Optional['BaseInstance']):
|
def compute_id(cls, _id: Optional[str], parent: Optional['BaseInstance']):
|
||||||
if _id is None:
|
if _id is None:
|
||||||
prefix = cls.compute_prefix()
|
prefix = cls.compute_prefix()
|
||||||
if issubclass(cls, SingleInstance):
|
if issubclass(cls, (SingleInstance, UniqueInstance)):
|
||||||
_id = prefix
|
_id = prefix
|
||||||
else:
|
else:
|
||||||
_id = f"{prefix}-{str(uuid.uuid4())}"
|
_id = f"{prefix}-{str(uuid.uuid4())}"
|
||||||
@@ -174,6 +180,17 @@ class BaseInstance:
|
|||||||
|
|
||||||
return _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):
|
class SingleInstance(BaseInstance):
|
||||||
"""
|
"""
|
||||||
@@ -200,7 +217,7 @@ class UniqueInstance(BaseInstance):
|
|||||||
_id: Optional[str] = None,
|
_id: Optional[str] = None,
|
||||||
auto_register: bool = True,
|
auto_register: bool = True,
|
||||||
on_init=None):
|
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:
|
if on_init is not None:
|
||||||
on_init()
|
on_init()
|
||||||
|
|
||||||
|
|||||||
@@ -350,3 +350,32 @@ class TestPresets:
|
|||||||
|
|
||||||
assert "background-color: purple" in css.css
|
assert "background-color: purple" in css.css
|
||||||
assert "color: yellow" 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