Added "table" and "tables" in the DSL

This commit is contained in:
2026-02-07 22:48:51 +01:00
parent 08c8c00e28
commit 6160e91665
18 changed files with 717 additions and 54 deletions

View File

@@ -48,12 +48,14 @@ Rules are indented (Python-style) under their scope.
Scopes define which cells a rule applies to: Scopes define which cells a rule applies to:
| Scope | Syntax | Applies To | | Scope | Syntax | Applies To | Specificity |
|-------|--------|------------| |-------|--------|------------|-------------|
| **Column** | `column <name>:` | All cells in the column | | **Cell (coordinates)** | `cell (<col>, <row>):` | Single cell by position | Highest (1) |
| **Row** | `row <index>:` | All cells in the row | | **Cell (ID)** | `cell <cell_id>:` | Single cell by ID | Highest (1) |
| **Cell (coordinates)** | `cell (<col>, <row>):` | Single cell by position | | **Row** | `row <index>:` | All cells in the row | High (2) |
| **Cell (ID)** | `cell <cell_id>:` | Single cell by ID | | **Column** | `column <name>:` | All cells in the column | Medium (3) |
| **Table** | `table "<name>":` | All cells in a specific table | Low (4) |
| **Tables** | `tables:` | All cells in all tables (global) | Lowest (5) |
**Column scope:** **Column scope:**
@@ -97,6 +99,24 @@ cell tcell_grid1-3-2:
style(background_color="yellow") style(background_color="yellow")
``` ```
**Table scope:**
```python
# Table by name (must match DataGrid _settings.name)
table "products":
style("neutral")
format.number(precision=2)
```
**Tables scope (global):**
```python
# All tables in the application
tables:
style(color="#333")
format.date(format="%Y-%m-%d")
```
### Rules ### Rules
A rule consists of optional **style**, optional **format**, and optional **condition**: A rule consists of optional **style**, optional **format**, and optional **condition**:
@@ -355,10 +375,12 @@ program : scope+
// Scopes // Scopes
scope : scope_header NEWLINE INDENT rule+ DEDENT scope : scope_header NEWLINE INDENT rule+ DEDENT
scope_header : column_scope | row_scope | cell_scope scope_header : column_scope | row_scope | cell_scope | table_scope | tables_scope
column_scope : "column" column_name ":" column_scope : "column" column_name ":"
row_scope : "row" INTEGER ":" row_scope : "row" INTEGER ":"
cell_scope : "cell" cell_ref ":" cell_scope : "cell" cell_ref ":"
table_scope : "table" QUOTED_STRING ":"
tables_scope : "tables" ":"
column_name : NAME | QUOTED_STRING column_name : NAME | QUOTED_STRING
cell_ref : "(" column_name "," INTEGER ")" | CELL_ID cell_ref : "(" column_name "," INTEGER ")" | CELL_ID
@@ -452,6 +474,21 @@ row 0:
style("neutral", bold=True) style("neutral", bold=True)
``` ```
**Apply default style to all cells in a table:**
```python
table "products":
style("neutral")
format.number(precision=2)
```
**Apply global styles to all tables:**
```python
tables:
style(font_size="14px")
```
### Advanced Examples ### Advanced Examples
**Compare with another column:** **Compare with another column:**
@@ -496,9 +533,17 @@ column status:
format.enum(source={"draft": "Brouillon", "pending": "En attente", "approved": "Approuvé"}, default="Inconnu") format.enum(source={"draft": "Brouillon", "pending": "En attente", "approved": "Approuvé"}, default="Inconnu")
``` ```
**Complete example - Financial report:** **Complete example - Financial report with hierarchy:**
```python ```python
# Global styling for all tables
tables:
style(font_size="14px", color="#333")
# Table-specific defaults
table "financial_report":
format.number(precision=2)
# Header styling # Header styling
row 0: row 0:
style("neutral", bold=True) style("neutral", bold=True)
@@ -533,6 +578,13 @@ cell (amount, 10):
style("accent", bold=True) style("accent", bold=True)
``` ```
**Note on hierarchy:** In the example above, for the cell `(amount, 10)`, the styles are applied in this order:
1. Cell-specific rule wins (accent, bold)
2. If no cell rule, column rules apply (amount formatting + conditional styles)
3. If no column rule, row rules apply (header bold)
4. If no row rule, table rules apply (financial_report precision)
5. If no table rule, global rules apply (tables font size and color)
--- ---
## Autocompletion ## Autocompletion
@@ -579,8 +631,9 @@ The DSL editor provides context-aware autocompletion to help users write rules e
| Context | Trigger | Suggestions | | Context | Trigger | Suggestions |
|---------|---------|-------------| |---------|---------|-------------|
| **Scope keyword** | Start of line | `column`, `row`, `cell` | | **Scope keyword** | Start of line | `column`, `row`, `cell`, `table`, `tables` |
| **Column name** | After `column ` | Column names from DataGrid | | **Column name** | After `column ` | Column names from DataGrid |
| **Table name** | After `table ` | Table name from current DataGrid (_settings.name) |
| **Style preset** | Inside `style("` | Style presets | | **Style preset** | Inside `style("` | Style presets |
| **Style parameter** | Inside `style(... , ` | `bold`, `italic`, `color`, etc. | | **Style parameter** | Inside `style(... , ` | `bold`, `italic`, `color`, etc. |
| **Format preset** | Inside `format("` | Format presets | | **Format preset** | Inside `format("` | Format presets |
@@ -609,6 +662,13 @@ column amount: │ (new line, indent)
style("error", bold=True) if │ value, col., row., not style("error", bold=True) if │ value, col., row., not
style("error", bold=True) if value │ ==, !=, <, >, in, ... style("error", bold=True) if value │ ==, !=, <, >, in, ...
style("error", bold=True) if value < │ [number input] style("error", bold=True) if value < │ [number input]
tab │ table, tables
table │ [table name from current grid]
table "products": │ (new line, indent)
tables │ tables
tables: │ (new line, indent)
``` ```
--- ---
@@ -1166,12 +1226,14 @@ class DatagridMetadataProvider(Protocol):
| Context | Trigger | Suggestions | | Context | Trigger | Suggestions |
|---------|---------|-------------| |---------|---------|-------------|
| `SCOPE_KEYWORD` | Start of non-indented line | `column`, `row`, `cell` | | `SCOPE_KEYWORD` | Start of non-indented line | `column`, `row`, `cell`, `table`, `tables` |
| `COLUMN_NAME` | After `column ` | Column names from DataGrid | | `COLUMN_NAME` | After `column ` | Column names from DataGrid |
| `ROW_INDEX` | After `row ` | First 10 indices + last index | | `ROW_INDEX` | After `row ` | First 10 indices + last index |
| `CELL_START` | After `cell ` | `(` | | `CELL_START` | After `cell ` | `(` |
| `CELL_COLUMN` | After `cell (` | Column names | | `CELL_COLUMN` | After `cell (` | Column names |
| `CELL_ROW` | After `cell (col, ` | First 10 indices + last index | | `CELL_ROW` | After `cell (col, ` | First 10 indices + last index |
| `TABLE_NAME` | After `table ` | Table name from current DataGrid (_settings.name) |
| `TABLES_SCOPE` | After `tables` | `:` |
| `RULE_START` | Start of indented line (after scope) | `style(`, `format(`, `format.` | | `RULE_START` | Start of indented line (after scope) | `style(`, `format(`, `format.` |
| `STYLE_ARGS` | After `style(` (no quote) | Presets with quotes + named params (`bold=`, `color=`, ...) | | `STYLE_ARGS` | After `style(` (no quote) | Presets with quotes + named params (`bold=`, `color=`, ...) |
| `STYLE_PRESET` | Inside `style("` | Style presets | | `STYLE_PRESET` | Inside `style("` | Style presets |
@@ -1313,16 +1375,18 @@ The following features are excluded from autocompletion for simplicity:
| DSL Grammar (lark) | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/grammar.py` | | DSL Grammar (lark) | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/grammar.py` |
| DSL Parser | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/parser.py` | | DSL Parser | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/parser.py` |
| DSL Transformer | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/transformer.py` | | DSL Transformer | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/transformer.py` |
| Scope Dataclasses | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/scopes.py` | | Scope Dataclasses (Column, Row, Cell) | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/scopes.py` |
| Scope Dataclasses (Table, Tables) | :o: To implement | `src/myfasthtml/core/formatting/dsl/scopes.py` |
| Exceptions | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/exceptions.py` | | Exceptions | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/exceptions.py` |
| Public API (`parse_dsl()`) | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/__init__.py` | | Public API (`parse_dsl()`) | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/__init__.py` |
| Unit Tests (Parser) | :white_check_mark: ~35 tests | `tests/core/formatting/dsl/test_dsl_parser.py` | | Unit Tests (Parser) | :white_check_mark: ~35 tests | `tests/core/formatting/dsl/test_dsl_parser.py` |
| **Autocompletion** | | | | **Autocompletion** | | |
| DatagridMetadataProvider | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/completion/provider.py` | | DatagridMetadataProvider | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/completion/provider.py` |
| Scope Detector | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/completion/contexts.py` | | Scope Detector (Column, Row, Cell) | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/completion/contexts.py` |
| Context Detector | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/completion/contexts.py` | | Scope Detector (Table, Tables) | :o: To implement | `src/myfasthtml/core/formatting/dsl/completion/contexts.py` |
| Suggestions Generator | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/completion/suggestions.py` | | Context Detector | :white_check_mark: Implemented | `FormattingCompletionEngine.py` |
| Completion Engine | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/completion/engine.py` | | Suggestions Generator | :white_check_mark: Implemented | `FormattingCompletionEngine.py` |
| Completion Engine | :white_check_mark: Implemented | `FormattingCompletionEngine.py` |
| Presets | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/completion/presets.py` | | Presets | :white_check_mark: Implemented | `src/myfasthtml/core/formatting/dsl/completion/presets.py` |
| Unit Tests (Completion) | :white_check_mark: ~50 tests | `tests/core/formatting/dsl/test_completion.py` | | Unit Tests (Completion) | :white_check_mark: ~50 tests | `tests/core/formatting/dsl/test_completion.py` |
| REST Endpoint | :white_check_mark: Implemented | `src/myfasthtml/core/utils.py``/myfasthtml/completions` | | REST Endpoint | :white_check_mark: Implemented | `src/myfasthtml/core/utils.py``/myfasthtml/completions` |

View File

@@ -16,6 +16,12 @@
| `col` parameter (row-level conditions) | :white_check_mark: Implemented | | | `col` parameter (row-level conditions) | :white_check_mark: Implemented | |
| `row` parameter (column-level conditions) | :x: Not implemented | | | `row` parameter (column-level conditions) | :x: Not implemented | |
| Column reference in value `{"col": "..."}` | :white_check_mark: Implemented | | | Column reference in value `{"col": "..."}` | :white_check_mark: Implemented | |
| **Scope Levels** | | |
| Cell scope | :white_check_mark: Implemented | |
| Row scope | :white_check_mark: Implemented | |
| Column scope | :white_check_mark: Implemented | |
| Table scope | :o: To implement | |
| Tables scope (global) | :o: To implement | |
| **DataGrid Integration** | | | | **DataGrid Integration** | | |
| Integration in `mk_body_cell_content()` | :white_check_mark: Implemented | `DataGrid.py` | | Integration in `mk_body_cell_content()` | :white_check_mark: Implemented | `DataGrid.py` |
| DataGridFormattingEditor | :white_check_mark: Implemented | `DataGridFormattingEditor.py` | | DataGridFormattingEditor | :white_check_mark: Implemented | `DataGridFormattingEditor.py` |
@@ -32,13 +38,15 @@
This document describes the formatting capabilities for the DataGrid component. This document describes the formatting capabilities for the DataGrid component.
**Formatting applies at three levels:** **Formatting applies at five levels:**
| Level | Cells Targeted | Condition Evaluated On | | Level | Cells Targeted | Condition Evaluated On | Specificity |
|------------|-------------------------|----------------------------------------------| |------------|-----------------------------------|----------------------------------------------|-------------|
| **Cell** | 1 specific cell | The cell value | | **Cell** | 1 specific cell | The cell value | Highest (1) |
| **Row** | All cells in the row | Each cell value (or fixed column with `col`) | | **Row** | All cells in the row | Each cell value (or fixed column with `col`) | High (2) |
| **Column** | All cells in the column | Each cell value (or fixed row with `row`) | | **Column** | All cells in the column | Each cell value (or fixed row with `row`) | Medium (3) |
| **Table** | All cells in a specific table | Each cell value | Low (4) |
| **Tables** | All cells in all tables (global) | Each cell value | Lowest (5) |
--- ---
@@ -467,11 +475,13 @@ formatter_presets = {
### Format Storage Location ### Format Storage Location
| Level | Storage | Key | Status | | Level | Storage | Key | Status |
|------------|------------------------------|---------|--------| |------------|--------------------------------|---------|--------|
| **Column** | `DataGridColumnState.format` | - | Structure exists | | **Cell** | `DatagridState.cell_formats` | Cell ID | Structure exists |
| **Row** | `DataGridRowState.format` | - | Structure exists | | **Row** | `DataGridRowState.format` | - | Structure exists |
| **Cell** | `DatagridState.cell_formats` | Cell ID | Structure exists | | **Column** | `DataGridColumnState.format` | - | Structure exists |
| **Table** | `DatagridState.table_format` | - | :o: To implement |
| **Tables** | `DataGridsManager.all_tables_formats` | - | :o: To implement |
### Cell ID Format ### Cell ID Format

View File

@@ -78,6 +78,7 @@ class DatagridState(DbObject):
self.edition: DatagridEditionState = DatagridEditionState() self.edition: DatagridEditionState = DatagridEditionState()
self.selection: DatagridSelectionState = DatagridSelectionState() self.selection: DatagridSelectionState = DatagridSelectionState()
self.cell_formats: dict = {} self.cell_formats: dict = {}
self.table_format: list = []
self.ne_df = None self.ne_df = None
self.ns_fast_access = None self.ns_fast_access = None
@@ -274,7 +275,7 @@ class DataGrid(MultipleInstance):
def _get_filtered_df(self): def _get_filtered_df(self):
if self._df is None: if self._df is None:
return DataFrame() return None
df = self._df.copy() df = self._df.copy()
df = self._apply_sort(df) # need to keep the real type to sort df = self._apply_sort(df) # need to keep the real type to sort
@@ -396,6 +397,8 @@ class DataGrid(MultipleInstance):
1. Cell-level: self._state.cell_formats[cell_id] 1. Cell-level: self._state.cell_formats[cell_id]
2. Row-level: row_state.format (if row has specific state) 2. Row-level: row_state.format (if row has specific state)
3. Column-level: col_def.format 3. Column-level: col_def.format
4. Table-level: self._state.table_format
5. Tables-level (global): manager.all_tables_formats
Args: Args:
col_pos: Column position index col_pos: Column position index
@@ -419,7 +422,11 @@ class DataGrid(MultipleInstance):
if col_def.format: if col_def.format:
return col_def.format return col_def.format
return None if self._state.table_format:
return self._state.table_format
# Get global tables formatting from manager
return self._parent.all_tables_formats
def set_column_width(self, col_id: str, width: str): def set_column_width(self, col_id: str, width: str):
"""Update column width after resize. Called via Command from JS.""" """Update column width after resize. Called via Command from JS."""
@@ -629,6 +636,9 @@ class DataGrid(MultipleInstance):
OPTIMIZED: Uses OptimizedDiv for rows instead of Div for faster rendering. OPTIMIZED: Uses OptimizedDiv for rows instead of Div for faster rendering.
""" """
df = self._get_filtered_df() df = self._get_filtered_df()
if df is None:
return []
start = page_index * DATAGRID_PAGE_SIZE start = page_index * DATAGRID_PAGE_SIZE
end = start + DATAGRID_PAGE_SIZE end = start + DATAGRID_PAGE_SIZE
if self._state.ns_total_rows > end: if self._state.ns_total_rows > end:

View File

@@ -2,7 +2,8 @@ import logging
from collections import defaultdict from collections import defaultdict
from myfasthtml.controls.DslEditor import DslEditor from myfasthtml.controls.DslEditor import DslEditor
from myfasthtml.core.formatting.dsl import parse_dsl, DSLSyntaxError, ColumnScope, RowScope, CellScope from myfasthtml.core.formatting.dsl import parse_dsl, DSLSyntaxError, ColumnScope, RowScope, CellScope, TableScope, TablesScope
from myfasthtml.core.instances import InstancesManager
logger = logging.getLogger("DataGridFormattingEditor") logger = logging.getLogger("DataGridFormattingEditor")
@@ -62,11 +63,13 @@ class DataGridFormattingEditor(DslEditor):
columns_rules = defaultdict(list) # key = column name columns_rules = defaultdict(list) # key = column name
rows_rules = defaultdict(list) # key = row index rows_rules = defaultdict(list) # key = row index
cells_rules = defaultdict(list) # key = cell_id cells_rules = defaultdict(list) # key = cell_id
table_rules = [] # rules for this table
tables_rules = [] # global rules for all tables
for scoped_rule in scoped_rules: for scoped_rule in scoped_rules:
scope = scoped_rule.scope scope = scoped_rule.scope
rule = scoped_rule.rule rule = scoped_rule.rule
if isinstance(scope, ColumnScope): if isinstance(scope, ColumnScope):
columns_rules[scope.column].append(rule) columns_rules[scope.column].append(rule)
elif isinstance(scope, RowScope): elif isinstance(scope, RowScope):
@@ -75,6 +78,14 @@ class DataGridFormattingEditor(DslEditor):
cell_id = self._get_cell_id(scope) cell_id = self._get_cell_id(scope)
if cell_id: if cell_id:
cells_rules[cell_id].append(rule) cells_rules[cell_id].append(rule)
elif isinstance(scope, TableScope):
# Validate table name matches current grid
if scope.table == self._parent._settings.name:
table_rules.append(rule)
else:
logger.warning(f"Table name '{scope.table}' does not match grid name '{self._parent._settings.name}', skipping rules")
elif isinstance(scope, TablesScope):
tables_rules.append(rule)
# Step 3: Copy state for atomic update # Step 3: Copy state for atomic update
state = self._parent.get_state().copy() state = self._parent.get_state().copy()
@@ -85,6 +96,7 @@ class DataGridFormattingEditor(DslEditor):
for row in state.rows: for row in state.rows:
row.format = None row.format = None
state.cell_formats.clear() state.cell_formats.clear()
state.table_format = []
# Step 5: Apply grouped rules on the copy # Step 5: Apply grouped rules on the copy
for column_name, rules in columns_rules.items(): for column_name, rules in columns_rules.items():
@@ -103,10 +115,21 @@ class DataGridFormattingEditor(DslEditor):
for cell_id, rules in cells_rules.items(): for cell_id, rules in cells_rules.items():
state.cell_formats[cell_id] = rules state.cell_formats[cell_id] = rules
# Apply table-level rules
if table_rules:
state.table_format = table_rules
# Apply global tables-level rules to manager
if tables_rules:
from myfasthtml.controls.DataGridsManager import DataGridsManager
manager = InstancesManager.get_by_type(self._session, DataGridsManager)
if manager:
manager.all_tables_formats = tables_rules
# Step 6: Update state atomically # Step 6: Update state atomically
self._parent.get_state().update(state) self._parent.get_state().update(state)
# Step 7: Refresh the DataGrid # Step 7: Refresh the DataGrid
logger.debug(f"Formatting applied: {len(columns_rules)} columns, {len(rows_rules)} rows, {len(cells_rules)} cells") logger.debug(f"Formatting applied: {len(columns_rules)} columns, {len(rows_rules)} rows, {len(cells_rules)} cells, table: {len(table_rules)}, tables: {len(tables_rules)}")
return self._parent.render_partial("body") return self._parent.render_partial("body")

View File

@@ -84,12 +84,13 @@ class DataGridsManager(SingleInstance, DatagridMetadataProvider):
self._state = DataGridsState(self) self._state = DataGridsState(self)
self._tree = self._mk_tree() self._tree = self._mk_tree()
self._tree.bind_command("SelectNode", self.commands.show_document()) self._tree.bind_command("SelectNode", self.commands.show_document())
self._tabs_manager = InstancesManager.get_by_type(self._session, TabsManager) self._tabs_manager = InstancesManager.get_by_type(self._session, TabsManager, None)
self._registry = DataGridsRegistry(parent) self._registry = DataGridsRegistry(parent)
# Global presets shared across all DataGrids # Global presets shared across all DataGrids
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()
self.all_tables_formats: list = []
def upload_from_source(self): def upload_from_source(self):
file_upload = FileUpload(self) file_upload = FileUpload(self)

View File

@@ -99,7 +99,7 @@ class DslEditor(MultipleInstance):
self._dsl = dsl self._dsl = dsl
self.conf = conf or DslEditorConf() self.conf = conf or DslEditorConf()
self._state = DslEditorState(self, name=conf.name, save_state=save_state) self._state = DslEditorState(self, name=self.conf.name, save_state=save_state)
self.commands = Commands(self) self.commands = Commands(self)
logger.debug(f"DslEditor created with id={self._id}, dsl={dsl.name}") logger.debug(f"DslEditor created with id={self._id}, dsl={dsl.name}")

View File

@@ -23,7 +23,7 @@ Example:
""" """
from .parser import get_parser from .parser import get_parser
from .transformer import DSLTransformer from .transformer import DSLTransformer
from .scopes import ColumnScope, RowScope, CellScope, ScopedRule from .scopes import ColumnScope, RowScope, CellScope, TableScope, TablesScope, ScopedRule
from .exceptions import DSLError, DSLSyntaxError, DSLValidationError from .exceptions import DSLError, DSLSyntaxError, DSLValidationError
@@ -61,6 +61,8 @@ __all__ = [
"ColumnScope", "ColumnScope",
"RowScope", "RowScope",
"CellScope", "CellScope",
"TableScope",
"TablesScope",
"ScopedRule", "ScopedRule",
# Exceptions # Exceptions
"DSLError", "DSLError",

View File

@@ -105,7 +105,13 @@ class FormattingCompletionEngine(BaseCompletionEngine):
case Context.CELL_ROW: case Context.CELL_ROW:
return self._get_row_index_suggestions() return self._get_row_index_suggestions()
case Context.TABLE_NAME:
return self._get_table_name_suggestion()
case Context.TABLES_SCOPE:
return [Suggestion(":", "Define global rules for all tables", "syntax")]
# ================================================================= # =================================================================
# Rule-level contexts # Rule-level contexts
# ================================================================= # =================================================================
@@ -230,7 +236,13 @@ class FormattingCompletionEngine(BaseCompletionEngine):
except Exception: except Exception:
pass pass
return [] return []
def _get_table_name_suggestion(self) -> list[Suggestion]:
"""Get table name suggestion (current table only)."""
if self.table_name:
return [Suggestion(f'"{self.table_name}"', f"Current table: {self.table_name}", "table")]
return []
def _get_style_preset_suggestions(self) -> list[Suggestion]: def _get_style_preset_suggestions(self) -> list[Suggestion]:
"""Get style preset suggestions (without quotes).""" """Get style preset suggestions (without quotes)."""
suggestions = [] suggestions = []

View File

@@ -25,12 +25,14 @@ class Context(Enum):
NONE = auto() NONE = auto()
# Scope-level contexts # Scope-level contexts
SCOPE_KEYWORD = auto() # Start of non-indented line: column, row, cell SCOPE_KEYWORD = auto() # Start of non-indented line: column, row, cell, table, tables
COLUMN_NAME = auto() # After "column ": column names COLUMN_NAME = auto() # After "column ": column names
ROW_INDEX = auto() # After "row ": row indices ROW_INDEX = auto() # After "row ": row indices
CELL_START = auto() # After "cell ": ( CELL_START = auto() # After "cell ": (
CELL_COLUMN = auto() # After "cell (": column names CELL_COLUMN = auto() # After "cell (": column names
CELL_ROW = auto() # After "cell (col, ": row indices CELL_ROW = auto() # After "cell (col, ": row indices
TABLE_NAME = auto() # After "table ": table name
TABLES_SCOPE = auto() # After "tables": colon
# Rule-level contexts # Rule-level contexts
RULE_START = auto() # Start of indented line: style(, format(, format. RULE_START = auto() # Start of indented line: style(, format(, format.
@@ -76,10 +78,10 @@ class DetectedScope:
Represents the detected scope from scanning previous lines. Represents the detected scope from scanning previous lines.
Attributes: Attributes:
scope_type: "column", "row", "cell", or None scope_type: "column", "row", "cell", "table", "tables", or None
column_name: Column name (for column and cell scopes) column_name: Column name (for column and cell scopes)
row_index: Row index (for row and cell scopes) row_index: Row index (for row and cell scopes)
table_name: DataGrid name (if determinable) table_name: Table name (for table scope) or DataGrid name
""" """
scope_type: str | None = None scope_type: str | None = None
@@ -92,7 +94,7 @@ def detect_scope(text: str, current_line: int) -> DetectedScope:
""" """
Detect the current scope by scanning backwards from the cursor line. Detect the current scope by scanning backwards from the cursor line.
Looks for the most recent scope declaration (column/row/cell) Looks for the most recent scope declaration (column/row/cell/table/tables)
that is not indented. that is not indented.
Args: Args:
@@ -138,6 +140,17 @@ def detect_scope(text: str, current_line: int) -> DetectedScope:
return DetectedScope( return DetectedScope(
scope_type="cell", column_name=column_name, row_index=row_index scope_type="cell", column_name=column_name, row_index=row_index
) )
# Check for table scope
match = re.match(r'^table\s+"([^"]+)"\s*:', line)
if match:
table_name = match.group(1)
return DetectedScope(scope_type="table", table_name=table_name)
# Check for tables scope
match = re.match(r"^tables\s*:", line)
if match:
return DetectedScope(scope_type="tables")
return DetectedScope() return DetectedScope()
@@ -192,6 +205,14 @@ def detect_context(text: str, cursor: Position, scope: DetectedScope) -> Context
if re.match(r'^cell\s+\(\s*(?:"[^"]*"|[a-zA-Z_][a-zA-Z0-9_]*)\s*,\s*$', line_to_cursor): if re.match(r'^cell\s+\(\s*(?:"[^"]*"|[a-zA-Z_][a-zA-Z0-9_]*)\s*,\s*$', line_to_cursor):
return Context.CELL_ROW return Context.CELL_ROW
# After "table "
if re.match(r"^table\s+", line_to_cursor) and not line_to_cursor.rstrip().endswith(":"):
return Context.TABLE_NAME
# After "tables"
if re.match(r"^tables\s*$", line_to_cursor):
return Context.TABLES_SCOPE
# Start of line or partial keyword # Start of line or partial keyword
return Context.SCOPE_KEYWORD return Context.SCOPE_KEYWORD

View File

@@ -120,6 +120,8 @@ SCOPE_KEYWORDS: list[Suggestion] = [
Suggestion("column", "Define column scope", "keyword"), Suggestion("column", "Define column scope", "keyword"),
Suggestion("row", "Define row scope", "keyword"), Suggestion("row", "Define row scope", "keyword"),
Suggestion("cell", "Define cell scope", "keyword"), Suggestion("cell", "Define cell scope", "keyword"),
Suggestion("table", "Define table scope", "keyword"),
Suggestion("tables", "Define global scope for all tables", "keyword"),
] ]
# ============================================================================= # =============================================================================

View File

@@ -16,10 +16,14 @@ GRAMMAR = r"""
scope_header: column_scope scope_header: column_scope
| row_scope | row_scope
| cell_scope | cell_scope
| table_scope
| tables_scope
column_scope: "column" column_name column_scope: "column" column_name
row_scope: "row" INTEGER row_scope: "row" INTEGER
cell_scope: "cell" cell_ref cell_scope: "cell" cell_ref
table_scope: "table" QUOTED_STRING
tables_scope: "tables"
column_name: NAME -> name column_name: NAME -> name
| QUOTED_STRING -> quoted_name | QUOTED_STRING -> quoted_name

View File

@@ -64,12 +64,18 @@ class DSLParser:
lines = text.split("\n") lines = text.split("\n")
lines = ["" if line.strip().startswith("#") else line for line in lines] lines = ["" if line.strip().startswith("#") else line for line in lines]
text = "\n".join(lines) text = "\n".join(lines)
# Strip leading whitespace/newlines and ensure text ends with newline # Strip leading whitespace/newlines and ensure text ends with newline
text = text.strip() text = text.strip()
if text and not text.endswith("\n"):
# Handle empty text (return empty tree that will transform to empty list)
if not text:
from lark import Tree
return Tree('start', [])
if not text.endswith("\n"):
text += "\n" text += "\n"
try: try:
return self._parser.parse(text) return self._parser.parse(text)
except UnexpectedInput as e: except UnexpectedInput as e:

View File

@@ -32,6 +32,18 @@ class CellScope:
cell_id: str = None cell_id: str = None
@dataclass
class TableScope:
"""Scope targeting a specific table by name."""
table: str
@dataclass
class TablesScope:
"""Scope targeting all tables (global)."""
pass
@dataclass @dataclass
class ScopedRule: class ScopedRule:
""" """
@@ -40,8 +52,8 @@ class ScopedRule:
The DSL parser returns a list of ScopedRule objects. The DSL parser returns a list of ScopedRule objects.
Attributes: Attributes:
scope: Where the rule applies (ColumnScope, RowScope, or CellScope) scope: Where the rule applies (ColumnScope, RowScope, CellScope, TableScope, or TablesScope)
rule: The FormatRule (condition + style + formatter) rule: The FormatRule (condition + style + formatter)
""" """
scope: ColumnScope | RowScope | CellScope scope: ColumnScope | RowScope | CellScope | TableScope | TablesScope
rule: FormatRule rule: FormatRule

View File

@@ -6,7 +6,7 @@ Converts lark AST into FormatRule and ScopedRule objects.
from lark import Transformer from lark import Transformer
from .exceptions import DSLValidationError from .exceptions import DSLValidationError
from .scopes import ColumnScope, RowScope, CellScope, ScopedRule from .scopes import ColumnScope, RowScope, CellScope, TableScope, TablesScope, ScopedRule
from ..dataclasses import ( from ..dataclasses import (
Condition, Condition,
Style, Style,
@@ -67,7 +67,14 @@ class DSLTransformer(Transformer):
def cell_id(self, items): def cell_id(self, items):
cell_id = str(items[0]) cell_id = str(items[0])
return CellScope(cell_id=cell_id) return CellScope(cell_id=cell_id)
def table_scope(self, items):
table_name = self._unquote(items[0])
return TableScope(table=table_name)
def tables_scope(self, items):
return TablesScope()
def name(self, items): def name(self, items):
return str(items[0]) return str(items[0])

View File

@@ -202,12 +202,17 @@ class InstancesManager:
return default return default
@staticmethod @staticmethod
def get_by_type(session: dict, cls: type): def get_by_type(session: dict, cls: type, default=NO_DEFAULT_VALUE):
session_id = InstancesManager.get_session_id(session) session_id = InstancesManager.get_session_id(session)
res = [i for s, i in InstancesManager.instances.items() if s[0] == session_id and isinstance(i, cls)] res = [i for s, i in InstancesManager.instances.items() if s[0] == session_id and isinstance(i, cls)]
assert len(res) <= 1, f"Multiple instances of type {cls.__name__} found" assert len(res) <= 1, f"Multiple instances of type {cls.__name__} found"
assert len(res) > 0, f"No instance of type {cls.__name__} found" try:
return res[0] assert len(res) > 0, f"No instance of type {cls.__name__} found"
return res[0]
except AssertionError:
if default is NO_DEFAULT_VALUE:
raise
return default
@staticmethod @staticmethod
def dynamic_get(session, component_parent: tuple, component: tuple): def dynamic_get(session, component_parent: tuple, component: tuple):

View File

@@ -0,0 +1,279 @@
"""
Tests for DataGrid formatting integration with table/tables scopes.
Tests the complete formatting flow: DSL → Storage → Application.
"""
import pytest
from myfasthtml.controls.DataGrid import DataGrid
from myfasthtml.controls.DataGridFormattingEditor import DataGridFormattingEditor
from myfasthtml.controls.DataGridsManager import DataGridsManager
from myfasthtml.controls.datagrid_objects import DataGridColumnState, DataGridRowState
from myfasthtml.core.constants import ColumnType
from myfasthtml.core.formatting.dataclasses import FormatRule, Style
from myfasthtml.core.formatting.dsl.definition import FormattingDSL
from myfasthtml.core.instances import InstancesManager
@pytest.fixture
def manager(root_instance):
"""Create a DataGridsManager instance."""
mgr = DataGridsManager(root_instance, _id="test-manager")
yield mgr
InstancesManager.reset()
@pytest.fixture
def datagrid(manager):
"""Create a DataGrid instance."""
from myfasthtml.controls.DataGrid import DatagridConf
conf = DatagridConf(namespace="app", name="products", id="test-grid")
grid = DataGrid(manager, conf=conf, save_state=False, _id="test-datagrid")
# Add some columns
grid._state.columns = [
DataGridColumnState(col_id="amount", col_index=0, title="Amount", type=ColumnType.Number, visible=True),
DataGridColumnState(col_id="status", col_index=1, title="Status", type=ColumnType.Text, visible=True),
]
# Add some rows
grid._state.rows = [
DataGridRowState(0),
DataGridRowState(1),
]
yield grid
InstancesManager.reset()
@pytest.fixture
def editor(datagrid):
return DataGridFormattingEditor(datagrid, FormattingDSL())
# =============================================================================
# _get_format_rules() Hierarchy Tests
# =============================================================================
class TestFormatRulesHierarchy:
"""Tests for format rules hierarchy (cell > row > column > table > tables)."""
def test_i_can_get_cell_level_rules(self, datagrid):
"""Test that cell-level rules have highest priority."""
# Setup rules at different levels
cell_rules = [FormatRule(style=Style(preset="error"))]
column_rules = [FormatRule(style=Style(preset="success"))]
table_rules = [FormatRule(style=Style(preset="info"))]
datagrid._state.cell_formats["tcell_test-datagrid-0-0"] = cell_rules
datagrid._state.columns[0].format = column_rules
datagrid._state.table_format = table_rules
# Get rules for cell (0, 0)
rules = datagrid._get_format_rules(0, 0, datagrid._state.columns[0])
# Should return cell rules (highest priority)
assert rules == cell_rules
def test_i_can_get_row_level_rules(self, datagrid):
"""Test that row-level rules have second priority."""
# Setup rules at different levels
row_rules = [FormatRule(style=Style(preset="warning"))]
column_rules = [FormatRule(style=Style(preset="success"))]
table_rules = [FormatRule(style=Style(preset="info"))]
datagrid._state.rows[0].format = row_rules
datagrid._state.columns[0].format = column_rules
datagrid._state.table_format = table_rules
# Get rules for row 0 (no cell-level rules)
rules = datagrid._get_format_rules(0, 0, datagrid._state.columns[0])
# Should return row rules
assert rules == row_rules
def test_i_can_get_column_level_rules(self, datagrid):
"""Test that column-level rules have third priority."""
# Setup rules at different levels
column_rules = [FormatRule(style=Style(preset="success"))]
table_rules = [FormatRule(style=Style(preset="info"))]
datagrid._state.columns[0].format = column_rules
datagrid._state.table_format = table_rules
# Get rules for column (no cell or row rules)
rules = datagrid._get_format_rules(0, 0, datagrid._state.columns[0])
# Should return column rules
assert rules == column_rules
def test_i_can_get_table_level_rules(self, datagrid, manager):
"""Test that table-level rules have fourth priority."""
# Setup rules at different levels
table_rules = [FormatRule(style=Style(preset="info"))]
tables_rules = [FormatRule(style=Style(preset="neutral"))]
datagrid._state.table_format = table_rules
manager.all_tables_formats = tables_rules
# Get rules for cell (no higher level rules)
rules = datagrid._get_format_rules(0, 0, datagrid._state.columns[0])
# Should return table rules
assert rules == table_rules
def test_i_can_get_tables_level_rules(self, datagrid, manager):
"""Test that tables-level rules have lowest priority."""
# Setup global rules
tables_rules = [FormatRule(style=Style(preset="neutral"))]
manager.all_tables_formats = tables_rules
# Get rules for cell (no other rules)
rules = datagrid._get_format_rules(0, 0, datagrid._state.columns[0])
# Should return global tables rules
assert rules == tables_rules
def test_i_get_none_when_no_rules(self, datagrid):
"""Test that None is returned when no rules are defined."""
rules = datagrid._get_format_rules(0, 0, datagrid._state.columns[0])
assert rules == []
@pytest.mark.parametrize("level,setup_func,expected_preset", [
("cell", lambda dg: dg._state.cell_formats.__setitem__("tcell_test-datagrid-0-0",
[FormatRule(style=Style(preset="error"))]), "error"),
("row", lambda dg: setattr(dg._state.rows[0], "format",
[FormatRule(style=Style(preset="warning"))]), "warning"),
("column", lambda dg: setattr(dg._state.columns[0], "format",
[FormatRule(style=Style(preset="success"))]), "success"),
("table", lambda dg: setattr(dg._state, "table_format",
[FormatRule(style=Style(preset="info"))]), "info"),
])
def test_hierarchy_priority(self, datagrid, level, setup_func, expected_preset):
"""Test that each level has correct priority in the hierarchy."""
setup_func(datagrid)
rules = datagrid._get_format_rules(0, 0, datagrid._state.columns[0])
assert rules is not None
assert len(rules) == 1
assert rules[0].style.preset == expected_preset
# =============================================================================
# DataGridFormattingEditor Integration Tests
# =============================================================================
class TestFormattingEditorIntegration:
"""Tests for DataGridFormattingEditor with table/tables scopes."""
def test_i_can_dispatch_table_rules(self, datagrid, editor):
"""Test that table rules are dispatched to DatagridState.table_format."""
dsl = '''
table "products":
style("info")
'''
editor.set_content(dsl)
editor.on_content_changed()
# Check that table_format is populated
assert len(datagrid._state.table_format) == 1
assert datagrid._state.table_format[0].style.preset == "info"
def test_i_cannot_use_wrong_table_name(self, datagrid, editor):
"""Test that wrong table name is rejected."""
dsl = '''
table "wrong_name":
style("error")
'''
editor.set_content(dsl)
editor.on_content_changed()
# Rules should not be applied (wrong table name)
assert len(datagrid._state.table_format) == 0
def test_i_can_dispatch_tables_rules(self, manager, datagrid, editor):
"""Test that tables rules are dispatched to DataGridsManager."""
dsl = '''
tables:
style("neutral")
format.number(precision=2)
'''
editor.set_content(dsl)
editor.on_content_changed()
# Check that manager.all_tables_formats is populated
assert len(manager.all_tables_formats) == 2
assert manager.all_tables_formats[0].style.preset == "neutral"
assert manager.all_tables_formats[1].formatter.precision == 2
def test_i_can_combine_all_scope_types(self, manager, datagrid, editor):
"""Test that all 5 scope types can be used together."""
dsl = '''
tables:
style(font_size="14px")
table "products":
format.number(precision=2)
column amount:
style("success") if value > 0
row 0:
style("neutral", bold=True)
cell (amount, 1):
style("error")
'''
editor.set_content(dsl)
editor.on_content_changed()
# Check all levels are populated
assert len(manager.all_tables_formats) == 1
assert len(datagrid._state.table_format) == 1
assert len(datagrid._state.columns[0].format) == 1
assert len(datagrid._state.rows[0].format) == 1
assert len(datagrid._state.cell_formats) == 1
def test_i_can_clear_table_format(self, datagrid, editor):
"""Test that table_format is cleared when DSL changes."""
# First set table rules
dsl = '''
table "products":
style("info")
'''
editor.set_content(dsl)
editor.on_content_changed()
assert len(datagrid._state.table_format) == 1
# Then remove them
editor.set_content("")
editor.on_content_changed()
assert len(datagrid._state.table_format) == 0
@pytest.mark.parametrize("table_name,should_apply", [
("products", True), # Correct name
("PRODUCTS", False), # Case sensitive
("product", False), # Partial match not allowed
("app.products", False), # Namespace not included
("other", False), # Completely wrong
])
def test_table_name_validation(self, datagrid, editor, table_name, should_apply):
"""Test that table name validation is case-sensitive and exact."""
dsl = f'''
table "{table_name}":
style("info")
'''
editor.set_content(dsl)
editor.on_content_changed()
if should_apply:
assert len(datagrid._state.table_format) == 1
else:
assert len(datagrid._state.table_format) == 0

View File

@@ -142,6 +142,49 @@ def test_i_can_detect_scope_with_multiple_declarations():
assert scope.column_name == "amount" assert scope.column_name == "amount"
def test_i_can_detect_table_scope():
"""Test detection of table scope."""
text = 'table "products":\n style()'
scope = detect_scope(text, current_line=1)
assert scope.scope_type == "table"
assert scope.table_name == "products"
def test_i_can_detect_table_scope_with_spaces():
"""Test detection of table scope with spaces in name."""
text = 'table "financial report":\n format()'
scope = detect_scope(text, current_line=1)
assert scope.scope_type == "table"
assert scope.table_name == "financial report"
def test_i_can_detect_tables_scope():
"""Test detection of global tables scope."""
text = "tables:\n style()"
scope = detect_scope(text, current_line=1)
assert scope.scope_type == "tables"
assert scope.table_name is None
@pytest.mark.parametrize("scope_def,expected_type,expected_attrs", [
('column amount:\n style()', "column", {"column_name": "amount"}),
('row 5:\n style()', "row", {"row_index": 5}),
('cell (amount, 3):\n style()', "cell", {"column_name": "amount", "row_index": 3}),
('table "products":\n style()', "table", {"table_name": "products"}),
('tables:\n style()', "tables", {}),
])
def test_i_can_detect_all_scope_types(scope_def, expected_type, expected_attrs):
"""Test detection of all 5 scope types."""
scope = detect_scope(scope_def, current_line=1)
assert scope.scope_type == expected_type
for attr, value in expected_attrs.items():
assert getattr(scope, attr) == value
# ============================================================================= # =============================================================================
# Context Detection - Scope Contexts # Context Detection - Scope Contexts
# ============================================================================= # =============================================================================
@@ -227,6 +270,42 @@ def test_context_cell_row_after_comma_quoted():
assert context == Context.CELL_ROW assert context == Context.CELL_ROW
def test_context_table_name_after_table():
"""Test TABLE_NAME context after 'table '."""
text = "table "
cursor = Position(line=0, ch=6)
scope = DetectedScope()
context = detect_context(text, cursor, scope)
assert context == Context.TABLE_NAME
def test_context_tables_scope_after_tables():
"""Test TABLES_SCOPE context after 'tables'."""
text = "tables"
cursor = Position(line=0, ch=6)
scope = DetectedScope()
context = detect_context(text, cursor, scope)
assert context == Context.TABLES_SCOPE
@pytest.mark.parametrize("text,cursor_ch,expected_context", [
("column ", 7, Context.COLUMN_NAME),
("row ", 4, Context.ROW_INDEX),
("cell ", 5, Context.CELL_START),
("table ", 6, Context.TABLE_NAME),
("tables", 6, Context.TABLES_SCOPE),
])
def test_i_can_detect_all_scope_contexts(text, cursor_ch, expected_context):
"""Test detection of all scope-related contexts."""
cursor = Position(line=0, ch=cursor_ch)
scope = DetectedScope()
context = detect_context(text, cursor, scope)
assert context == expected_context
# ============================================================================= # =============================================================================
# Context Detection - Rule Contexts # Context Detection - Rule Contexts
# ============================================================================= # =============================================================================
@@ -553,6 +632,32 @@ def test_suggestions_scope_keyword(provider):
assert "column" in labels assert "column" in labels
assert "row" in labels assert "row" in labels
assert "cell" in labels assert "cell" in labels
assert "table" in labels
assert "tables" in labels
def test_suggestions_table_name(provider):
"""Test suggestions for TABLE_NAME context."""
engine = FormattingCompletionEngine(provider, "app.orders")
scope = DetectedScope()
suggestions = engine.get_suggestions(Context.TABLE_NAME, scope, "")
labels = [s.label for s in suggestions]
# Should suggest the current table name in quotes
assert '"app.orders"' in labels
def test_suggestions_tables_scope(provider):
"""Test suggestions for TABLES_SCOPE context."""
engine = FormattingCompletionEngine(provider, "app.orders")
scope = DetectedScope()
suggestions = engine.get_suggestions(Context.TABLES_SCOPE, scope, "")
labels = [s.label for s in suggestions]
# Should suggest colon to complete the scope
assert ":" in labels
def test_suggestions_style_preset(provider): def test_suggestions_style_preset(provider):

View File

@@ -17,6 +17,8 @@ from myfasthtml.core.formatting.dsl import (
ColumnScope, ColumnScope,
RowScope, RowScope,
CellScope, CellScope,
TableScope,
TablesScope,
DSLSyntaxError, DSLSyntaxError,
) )
@@ -125,6 +127,62 @@ cell tcell_grid1-3-2:
assert rules[0].scope.row is None assert rules[0].scope.row is None
class TestTableScope:
"""Tests for table scope parsing."""
def test_i_can_parse_table_scope(self):
"""Test parsing a table scope."""
dsl = """
table "products":
style("neutral")
"""
rules = parse_dsl(dsl)
assert len(rules) == 1
assert isinstance(rules[0].scope, TableScope)
assert rules[0].scope.table == "products"
def test_i_can_parse_table_scope_with_spaces(self):
"""Test parsing a table scope with spaces in name."""
dsl = """
table "financial report":
style("info")
"""
rules = parse_dsl(dsl)
assert len(rules) == 1
assert isinstance(rules[0].scope, TableScope)
assert rules[0].scope.table == "financial report"
class TestTablesScope:
"""Tests for tables scope (global) parsing."""
def test_i_can_parse_tables_scope(self):
"""Test parsing the global tables scope."""
dsl = """
tables:
style("neutral")
"""
rules = parse_dsl(dsl)
assert len(rules) == 1
assert isinstance(rules[0].scope, TablesScope)
def test_i_can_parse_tables_scope_with_multiple_rules(self):
"""Test parsing tables scope with multiple rules."""
dsl = """
tables:
style("neutral")
format.number(precision=2)
"""
rules = parse_dsl(dsl)
assert len(rules) == 2
assert isinstance(rules[0].scope, TablesScope)
assert isinstance(rules[1].scope, TablesScope)
# ============================================================================= # =============================================================================
# Style Tests # Style Tests
# ============================================================================= # =============================================================================
@@ -496,7 +554,49 @@ row 0:
# Third rule: row 0 # Third rule: row 0
assert isinstance(rules[2].scope, RowScope) assert isinstance(rules[2].scope, RowScope)
assert rules[2].scope.row == 0 assert rules[2].scope.row == 0
def test_i_can_parse_all_scope_types(self):
"""Test parsing all 5 scope types together."""
dsl = """
tables:
style(font_size="14px")
table "products":
format.number(precision=2)
column amount:
format("EUR")
row 0:
style("neutral", bold=True)
cell (amount, 5):
style("highlight")
"""
rules = parse_dsl(dsl)
assert len(rules) == 5
# First rule: tables (global)
assert isinstance(rules[0].scope, TablesScope)
# Second rule: table "products"
assert isinstance(rules[1].scope, TableScope)
assert rules[1].scope.table == "products"
# Third rule: column amount
assert isinstance(rules[2].scope, ColumnScope)
assert rules[2].scope.column == "amount"
# Fourth rule: row 0
assert isinstance(rules[3].scope, RowScope)
assert rules[3].scope.row == 0
# Fifth rule: cell (amount, 5)
assert isinstance(rules[4].scope, CellScope)
assert rules[4].scope.column == "amount"
assert rules[4].scope.row == 5
def test_i_can_parse_style_and_format_combined(self): def test_i_can_parse_style_and_format_combined(self):
"""Test parsing style and format on same line.""" """Test parsing style and format on same line."""
dsl = """ dsl = """