Working on Formating DSL completion

This commit is contained in:
2026-01-31 19:09:14 +01:00
parent 778e5ac69d
commit d7ec99c3d9
77 changed files with 7563 additions and 63 deletions

View File

@@ -0,0 +1,69 @@
"""
DataGrid Formatting DSL Module.
This module provides a Domain Specific Language (DSL) for defining
formatting rules in the DataGrid component.
Example:
from myfasthtml.core.formatting.dsl import parse_dsl
rules = parse_dsl('''
column amount:
style("error") if value < 0
format("EUR")
column status:
style("success") if value == "approved"
style("warning") if value == "pending"
''')
for scoped_rule in rules:
print(f"Scope: {scoped_rule.scope}")
print(f"Rule: {scoped_rule.rule}")
"""
from .parser import get_parser
from .transformer import DSLTransformer
from .scopes import ColumnScope, RowScope, CellScope, ScopedRule
from .exceptions import DSLError, DSLSyntaxError, DSLValidationError
def parse_dsl(text: str) -> list[ScopedRule]:
"""
Parse DSL text into a list of ScopedRule objects.
Args:
text: The DSL text to parse
Returns:
List of ScopedRule objects, each containing a scope and a FormatRule
Raises:
DSLSyntaxError: If the text has syntax errors
DSLValidationError: If the text is syntactically correct but semantically invalid
Example:
rules = parse_dsl('''
column price:
style("error") if value < 0
format("EUR", precision=2)
''')
"""
parser = get_parser()
tree = parser.parse(text)
transformer = DSLTransformer()
return transformer.transform(tree)
__all__ = [
# Main API
"parse_dsl",
# Scope classes
"ColumnScope",
"RowScope",
"CellScope",
"ScopedRule",
# Exceptions
"DSLError",
"DSLSyntaxError",
"DSLValidationError",
]

View File

@@ -0,0 +1,323 @@
"""
Completion contexts for the formatting DSL.
Defines the Context enum and detection logic to determine
what kind of autocompletion suggestions are appropriate.
"""
import re
from dataclasses import dataclass
from enum import Enum, auto
from myfasthtml.core.dsl import utils
from myfasthtml.core.dsl.types import Position
class Context(Enum):
"""
Autocompletion context identifiers.
Each context corresponds to a specific position in the DSL
where certain types of suggestions are appropriate.
"""
# No suggestions (e.g., in comment)
NONE = auto()
# Scope-level contexts
SCOPE_KEYWORD = auto() # Start of non-indented line: column, row, cell
COLUMN_NAME = auto() # After "column ": column names
ROW_INDEX = auto() # After "row ": row indices
CELL_START = auto() # After "cell ": (
CELL_COLUMN = auto() # After "cell (": column names
CELL_ROW = auto() # After "cell (col, ": row indices
# Rule-level contexts
RULE_START = auto() # Start of indented line: style(, format(, format.
# Style contexts
STYLE_ARGS = auto() # After "style(": presets + params
STYLE_PRESET = auto() # Inside style("): preset names
STYLE_PARAM = auto() # After comma in style(): params
# Format contexts
FORMAT_PRESET = auto() # Inside format("): preset names
FORMAT_TYPE = auto() # After "format.": number, date, etc.
FORMAT_PARAM_DATE = auto() # Inside format.date(): format=
FORMAT_PARAM_TEXT = auto() # Inside format.text(): transform=, etc.
# After style/format
AFTER_STYLE_OR_FORMAT = auto() # After ")": style(, format(, if
# Condition contexts
CONDITION_START = auto() # After "if ": value, col., not
CONDITION_AFTER_NOT = auto() # After "if not ": value, col.
COLUMN_REF = auto() # After "col.": column names
COLUMN_REF_QUOTED = auto() # After 'col."': column names with quote
# Operator contexts
OPERATOR = auto() # After operand: ==, <, contains, etc.
OPERATOR_VALUE = auto() # After operator: col., True, False, values
BETWEEN_AND = auto() # After "between X ": and
BETWEEN_VALUE = auto() # After "between X and ": values
IN_LIST_START = auto() # After "in ": [
IN_LIST_VALUE = auto() # Inside [ or after ,: values
# Value contexts
BOOLEAN_VALUE = auto() # After "bold=": True, False
COLOR_VALUE = auto() # After "color=": colors
DATE_FORMAT_VALUE = auto() # After "format=" in format.date: patterns
TRANSFORM_VALUE = auto() # After "transform=": uppercase, etc.
@dataclass
class DetectedScope:
"""
Represents the detected scope from scanning previous lines.
Attributes:
scope_type: "column", "row", "cell", or None
column_name: Column name (for column and cell scopes)
row_index: Row index (for row and cell scopes)
table_name: DataGrid name (if determinable)
"""
scope_type: str | None = None
column_name: str | None = None
row_index: int | None = None
table_name: str | None = None
def detect_scope(text: str, current_line: int) -> DetectedScope:
"""
Detect the current scope by scanning backwards from the cursor line.
Looks for the most recent scope declaration (column/row/cell)
that is not indented.
Args:
text: The full document text
current_line: Current line number (0-based)
Returns:
DetectedScope with scope information
"""
lines = text.split("\n")
# Scan backwards from current line
for i in range(current_line, -1, -1):
if i >= len(lines):
continue
line = lines[i]
# Skip empty lines and indented lines
if not line.strip() or utils.is_indented(line):
continue
# Check for column scope
match = re.match(r'^column\s+(?:"([^"]+)"|([a-zA-Z_][a-zA-Z0-9_]*))\s*:', line)
if match:
column_name = match.group(1) or match.group(2)
return DetectedScope(scope_type="column", column_name=column_name)
# Check for row scope
match = re.match(r"^row\s+(\d+)\s*:", line)
if match:
row_index = int(match.group(1))
return DetectedScope(scope_type="row", row_index=row_index)
# Check for cell scope
match = re.match(
r'^cell\s+\(\s*(?:"([^"]+)"|([a-zA-Z_][a-zA-Z0-9_]*))\s*,\s*(\d+)\s*\)\s*:',
line,
)
if match:
column_name = match.group(1) or match.group(2)
row_index = int(match.group(3))
return DetectedScope(
scope_type="cell", column_name=column_name, row_index=row_index
)
return DetectedScope()
def detect_context(text: str, cursor: Position, scope: DetectedScope) -> Context:
"""
Detect the completion context at the cursor position.
Analyzes the current line up to the cursor to determine
what kind of token is expected.
Args:
text: The full document text
cursor: Cursor position
scope: The detected scope
Returns:
Context enum value
"""
line = utils.get_line_at(text, cursor.line)
line_to_cursor = line[: cursor.ch]
# Check if in comment
if utils.is_in_comment(line, cursor.ch):
return Context.NONE
# Check if line is indented (inside a scope)
is_indented = utils.is_indented(line)
# =========================================================================
# Non-indented line contexts (scope definitions)
# =========================================================================
if not is_indented:
# After "column "
if re.match(r"^column\s+", line_to_cursor) and not line_to_cursor.rstrip().endswith(":"):
return Context.COLUMN_NAME
# After "row "
if re.match(r"^row\s+", line_to_cursor) and not line_to_cursor.rstrip().endswith(":"):
return Context.ROW_INDEX
# After "cell "
if re.match(r"^cell\s+$", line_to_cursor):
return Context.CELL_START
# After "cell ("
if re.match(r"^cell\s+\(\s*$", line_to_cursor):
return Context.CELL_COLUMN
# After "cell (col, " or "cell ("col", "
if re.match(r'^cell\s+\(\s*(?:"[^"]*"|[a-zA-Z_][a-zA-Z0-9_]*)\s*,\s*$', line_to_cursor):
return Context.CELL_ROW
# Start of line or partial keyword
return Context.SCOPE_KEYWORD
# =========================================================================
# Indented line contexts (rules inside a scope)
# =========================================================================
stripped = line_to_cursor.strip()
# Empty indented line - rule start
if not stripped:
return Context.RULE_START
# -------------------------------------------------------------------------
# Style contexts
# -------------------------------------------------------------------------
# Inside style(" - preset
if re.search(r'style\s*\(\s*"[^"]*$', line_to_cursor):
return Context.STYLE_PRESET
# After style( without quote - args (preset or params)
if re.search(r"style\s*\(\s*$", line_to_cursor):
return Context.STYLE_ARGS
# After comma in style() - params
if re.search(r"style\s*\([^)]*,\s*$", line_to_cursor):
return Context.STYLE_PARAM
# After param= in style - check which param
if re.search(r"style\s*\([^)]*(?:bold|italic|underline|strikethrough)\s*=\s*$", line_to_cursor):
return Context.BOOLEAN_VALUE
if re.search(r"style\s*\([^)]*(?:color|background_color)\s*=\s*$", line_to_cursor):
return Context.COLOR_VALUE
# -------------------------------------------------------------------------
# Format contexts
# -------------------------------------------------------------------------
# After "format." - type
if re.search(r"format\s*\.\s*$", line_to_cursor):
return Context.FORMAT_TYPE
# Inside format(" - preset
if re.search(r'format\s*\(\s*"[^"]*$', line_to_cursor):
return Context.FORMAT_PRESET
# Inside format.date( - params
if re.search(r"format\s*\.\s*date\s*\(\s*$", line_to_cursor):
return Context.FORMAT_PARAM_DATE
# After format= in format.date
if re.search(r"format\s*\.\s*date\s*\([^)]*format\s*=\s*$", line_to_cursor):
return Context.DATE_FORMAT_VALUE
# Inside format.text( - params
if re.search(r"format\s*\.\s*text\s*\(\s*$", line_to_cursor):
return Context.FORMAT_PARAM_TEXT
# After transform= in format.text
if re.search(r"format\s*\.\s*text\s*\([^)]*transform\s*=\s*$", line_to_cursor):
return Context.TRANSFORM_VALUE
# -------------------------------------------------------------------------
# After style/format - if or more style/format
# -------------------------------------------------------------------------
# After closing ) of style or format
if re.search(r"\)\s*$", line_to_cursor):
# Check if there's already an "if" on this line
if " if " not in line_to_cursor:
return Context.AFTER_STYLE_OR_FORMAT
# -------------------------------------------------------------------------
# Condition contexts
# -------------------------------------------------------------------------
# After "if not "
if re.search(r"\bif\s+not\s+$", line_to_cursor):
return Context.CONDITION_AFTER_NOT
# After "if "
if re.search(r"\bif\s+$", line_to_cursor):
return Context.CONDITION_START
# After "col." - column reference
if re.search(r'\bcol\s*\.\s*"$', line_to_cursor):
return Context.COLUMN_REF_QUOTED
if re.search(r"\bcol\s*\.\s*$", line_to_cursor):
return Context.COLUMN_REF
# After "between X and " - value
if re.search(r"\bbetween\s+\S+\s+and\s+$", line_to_cursor):
return Context.BETWEEN_VALUE
# After "between X " - and
if re.search(r"\bbetween\s+\S+\s+$", line_to_cursor):
return Context.BETWEEN_AND
# After "in [" or "in [...," - list value
if re.search(r"\bin\s+\[[^\]]*,\s*$", line_to_cursor):
return Context.IN_LIST_VALUE
if re.search(r"\bin\s+\[\s*$", line_to_cursor):
return Context.IN_LIST_VALUE
# After "in " - list start
if re.search(r"\bin\s+$", line_to_cursor):
return Context.IN_LIST_START
# After operator - value
if re.search(r"(?:==|!=|<=?|>=?|contains|startswith|endswith)\s+$", line_to_cursor):
return Context.OPERATOR_VALUE
# After operand (value, col.xxx, literal) - operator
if re.search(r"(?:value|col\.[a-zA-Z_][a-zA-Z0-9_]*|\d+|\"[^\"]*\"|True|False)\s+$", line_to_cursor):
return Context.OPERATOR
# -------------------------------------------------------------------------
# Fallback - rule start for partial input
# -------------------------------------------------------------------------
# If we're at the start of typing something
if re.match(r"^\s*[a-zA-Z]*$", line_to_cursor):
return Context.RULE_START
return Context.NONE

View File

@@ -0,0 +1,109 @@
"""
Completion engine for the formatting DSL.
Implements the BaseCompletionEngine for DataGrid formatting rules.
"""
from myfasthtml.core.dsl.base_completion import BaseCompletionEngine
from myfasthtml.core.dsl.types import Position, Suggestion, CompletionResult
from . import suggestions as suggestions_module
from .contexts import Context, DetectedScope, detect_scope, detect_context
from .provider import DatagridMetadataProvider
class FormattingCompletionEngine(BaseCompletionEngine):
"""
Autocompletion engine for the DataGrid Formatting DSL.
Provides context-aware suggestions for:
- Scope definitions (column, row, cell)
- Style expressions with presets and parameters
- Format expressions with presets and types
- Conditions with operators and values
"""
def __init__(self, provider: DatagridMetadataProvider):
"""
Initialize the completion engine.
Args:
provider: DataGrid metadata provider for dynamic suggestions
"""
super().__init__(provider)
self.provider: DatagridMetadataProvider = provider
def detect_scope(self, text: str, current_line: int) -> DetectedScope:
"""
Detect the current scope by scanning previous lines.
Looks for the most recent scope declaration (column/row/cell).
Args:
text: The full document text
current_line: Current line number (0-based)
Returns:
DetectedScope with scope information
"""
return detect_scope(text, current_line)
def detect_context(
self, text: str, cursor: Position, scope: DetectedScope
) -> Context:
"""
Detect the completion context at the cursor position.
Args:
text: The full document text
cursor: Cursor position
scope: The detected scope
Returns:
Context enum value
"""
return detect_context(text, cursor, scope)
def get_suggestions(
self, context: Context, scope: DetectedScope, prefix: str
) -> list[Suggestion]:
"""
Generate suggestions for the given context.
Args:
context: The detected completion context
scope: The detected scope
prefix: The current word prefix (not used here, filtering done in base)
Returns:
List of suggestions
"""
return suggestions_module.get_suggestions(context, scope, self.provider)
def get_completions(
text: str,
cursor: Position,
provider: DatagridMetadataProvider,
) -> CompletionResult:
"""
Get autocompletion suggestions for the formatting DSL.
This is the main entry point for the autocompletion API.
Args:
text: The full DSL document text
cursor: Cursor position (line and ch are 0-based)
provider: DataGrid metadata provider
Returns:
CompletionResult with suggestions and replacement range
Example:
result = get_completions(
text='column amount:\\n style("err',
cursor=Position(line=1, ch=15),
provider=my_provider
)
# result.suggestions contains ["error"] filtered by prefix "err"
"""
engine = FormattingCompletionEngine(provider)
return engine.get_completions(text, cursor)

View File

@@ -0,0 +1,245 @@
"""
Static data for formatting DSL autocompletion.
Contains predefined values for style presets, colors, date patterns, etc.
"""
from myfasthtml.core.dsl.types import Suggestion
# =============================================================================
# Style Presets (DaisyUI 5)
# =============================================================================
STYLE_PRESETS: list[Suggestion] = [
Suggestion("primary", "Primary theme color", "preset"),
Suggestion("secondary", "Secondary theme color", "preset"),
Suggestion("accent", "Accent theme color", "preset"),
Suggestion("neutral", "Neutral theme color", "preset"),
Suggestion("info", "Info (blue)", "preset"),
Suggestion("success", "Success (green)", "preset"),
Suggestion("warning", "Warning (yellow)", "preset"),
Suggestion("error", "Error (red)", "preset"),
]
# =============================================================================
# Format Presets
# =============================================================================
FORMAT_PRESETS: list[Suggestion] = [
Suggestion("EUR", "Euro currency (1 234,56 €)", "preset"),
Suggestion("USD", "US Dollar ($1,234.56)", "preset"),
Suggestion("percentage", "Percentage (×100, adds %)", "preset"),
Suggestion("short_date", "DD/MM/YYYY", "preset"),
Suggestion("iso_date", "YYYY-MM-DD", "preset"),
Suggestion("yes_no", "Yes/No", "preset"),
]
# =============================================================================
# CSS Colors
# =============================================================================
CSS_COLORS: list[Suggestion] = [
Suggestion("red", "Red", "color"),
Suggestion("blue", "Blue", "color"),
Suggestion("green", "Green", "color"),
Suggestion("yellow", "Yellow", "color"),
Suggestion("orange", "Orange", "color"),
Suggestion("purple", "Purple", "color"),
Suggestion("pink", "Pink", "color"),
Suggestion("gray", "Gray", "color"),
Suggestion("black", "Black", "color"),
Suggestion("white", "White", "color"),
]
# =============================================================================
# DaisyUI Color Variables
# =============================================================================
DAISYUI_COLORS: list[Suggestion] = [
Suggestion("var(--color-primary)", "Primary color", "variable"),
Suggestion("var(--color-primary-content)", "Primary content color", "variable"),
Suggestion("var(--color-secondary)", "Secondary color", "variable"),
Suggestion("var(--color-secondary-content)", "Secondary content color", "variable"),
Suggestion("var(--color-accent)", "Accent color", "variable"),
Suggestion("var(--color-accent-content)", "Accent content color", "variable"),
Suggestion("var(--color-neutral)", "Neutral color", "variable"),
Suggestion("var(--color-neutral-content)", "Neutral content color", "variable"),
Suggestion("var(--color-info)", "Info color", "variable"),
Suggestion("var(--color-info-content)", "Info content color", "variable"),
Suggestion("var(--color-success)", "Success color", "variable"),
Suggestion("var(--color-success-content)", "Success content color", "variable"),
Suggestion("var(--color-warning)", "Warning color", "variable"),
Suggestion("var(--color-warning-content)", "Warning content color", "variable"),
Suggestion("var(--color-error)", "Error color", "variable"),
Suggestion("var(--color-error-content)", "Error content color", "variable"),
Suggestion("var(--color-base-100)", "Base 100", "variable"),
Suggestion("var(--color-base-200)", "Base 200", "variable"),
Suggestion("var(--color-base-300)", "Base 300", "variable"),
Suggestion("var(--color-base-content)", "Base content color", "variable"),
]
# Combined color suggestions
ALL_COLORS: list[Suggestion] = CSS_COLORS + DAISYUI_COLORS
# =============================================================================
# Date Format Patterns
# =============================================================================
DATE_PATTERNS: list[Suggestion] = [
Suggestion('"%Y-%m-%d"', "ISO format (2026-01-29)", "pattern"),
Suggestion('"%d/%m/%Y"', "European (29/01/2026)", "pattern"),
Suggestion('"%m/%d/%Y"', "US format (01/29/2026)", "pattern"),
Suggestion('"%d %b %Y"', "Short month (29 Jan 2026)", "pattern"),
Suggestion('"%d %B %Y"', "Full month (29 January 2026)", "pattern"),
]
# =============================================================================
# Text Transform Values
# =============================================================================
TEXT_TRANSFORMS: list[Suggestion] = [
Suggestion('"uppercase"', "UPPERCASE", "value"),
Suggestion('"lowercase"', "lowercase", "value"),
Suggestion('"capitalize"', "Capitalize Each Word", "value"),
]
# =============================================================================
# Boolean Values
# =============================================================================
BOOLEAN_VALUES: list[Suggestion] = [
Suggestion("True", "Boolean true", "literal"),
Suggestion("False", "Boolean false", "literal"),
]
# =============================================================================
# Scope Keywords
# =============================================================================
SCOPE_KEYWORDS: list[Suggestion] = [
Suggestion("column", "Define column scope", "keyword"),
Suggestion("row", "Define row scope", "keyword"),
Suggestion("cell", "Define cell scope", "keyword"),
]
# =============================================================================
# Rule Start Keywords
# =============================================================================
RULE_START: list[Suggestion] = [
Suggestion("style(", "Apply visual styling", "function"),
Suggestion("format(", "Apply value formatting (preset)", "function"),
Suggestion("format.", "Apply value formatting (typed)", "function"),
]
# =============================================================================
# After Style/Format Keywords
# =============================================================================
AFTER_STYLE_OR_FORMAT: list[Suggestion] = [
Suggestion("style(", "Apply visual styling", "function"),
Suggestion("format(", "Apply value formatting (preset)", "function"),
Suggestion("format.", "Apply value formatting (typed)", "function"),
Suggestion("if", "Add condition", "keyword"),
]
# =============================================================================
# Style Parameters
# =============================================================================
STYLE_PARAMS: list[Suggestion] = [
Suggestion("bold=", "Bold text", "parameter"),
Suggestion("italic=", "Italic text", "parameter"),
Suggestion("underline=", "Underlined text", "parameter"),
Suggestion("strikethrough=", "Strikethrough text", "parameter"),
Suggestion("color=", "Text color", "parameter"),
Suggestion("background_color=", "Background color", "parameter"),
Suggestion("font_size=", "Font size", "parameter"),
]
# =============================================================================
# Format Types
# =============================================================================
FORMAT_TYPES: list[Suggestion] = [
Suggestion("number", "Number formatting", "type"),
Suggestion("date", "Date formatting", "type"),
Suggestion("boolean", "Boolean formatting", "type"),
Suggestion("text", "Text transformation", "type"),
Suggestion("enum", "Value mapping", "type"),
]
# =============================================================================
# Format Parameters by Type
# =============================================================================
FORMAT_PARAMS_DATE: list[Suggestion] = [
Suggestion("format=", "strftime pattern", "parameter"),
]
FORMAT_PARAMS_TEXT: list[Suggestion] = [
Suggestion("transform=", "Text transformation", "parameter"),
Suggestion("max_length=", "Maximum length", "parameter"),
Suggestion("ellipsis=", "Truncation suffix", "parameter"),
]
# =============================================================================
# Condition Keywords
# =============================================================================
CONDITION_START: list[Suggestion] = [
Suggestion("value", "Current cell value", "keyword"),
Suggestion("col.", "Reference another column", "keyword"),
Suggestion("not", "Negate condition", "keyword"),
]
CONDITION_AFTER_NOT: list[Suggestion] = [
Suggestion("value", "Current cell value", "keyword"),
Suggestion("col.", "Reference another column", "keyword"),
]
# =============================================================================
# Operators
# =============================================================================
COMPARISON_OPERATORS: list[Suggestion] = [
Suggestion("==", "Equal", "operator"),
Suggestion("!=", "Not equal", "operator"),
Suggestion("<", "Less than", "operator"),
Suggestion("<=", "Less or equal", "operator"),
Suggestion(">", "Greater than", "operator"),
Suggestion(">=", "Greater or equal", "operator"),
Suggestion("contains", "String contains", "operator"),
Suggestion("startswith", "String starts with", "operator"),
Suggestion("endswith", "String ends with", "operator"),
Suggestion("in", "Value in list", "operator"),
Suggestion("between", "Value in range", "operator"),
Suggestion("isempty", "Is null or empty", "operator"),
Suggestion("isnotempty", "Is not null or empty", "operator"),
]
# =============================================================================
# Operator Value Start
# =============================================================================
OPERATOR_VALUE_BASE: list[Suggestion] = [
Suggestion("col.", "Reference another column", "keyword"),
Suggestion("True", "Boolean true", "literal"),
Suggestion("False", "Boolean false", "literal"),
]
# =============================================================================
# Between Keyword
# =============================================================================
BETWEEN_AND: list[Suggestion] = [
Suggestion("and", "Between upper bound", "keyword"),
]
# =============================================================================
# In List Start
# =============================================================================
IN_LIST_START: list[Suggestion] = [
Suggestion("[", "Start list", "syntax"),
]

View File

@@ -0,0 +1,94 @@
"""
Metadata provider for DataGrid formatting DSL autocompletion.
Provides access to DataGrid metadata (columns, values, row counts)
for context-aware autocompletion.
"""
from typing import Protocol, Any
class DatagridMetadataProvider(Protocol):
"""
Protocol for providing DataGrid metadata to the autocompletion engine.
Implementations must provide access to:
- Available DataGrids (tables)
- Column names for each DataGrid
- Distinct values for each column
- Row count for each DataGrid
- Style and format presets
DataGrid names follow the pattern namespace.name (multi-level namespaces).
"""
def get_tables(self) -> list[str]:
"""
Return the list of available DataGrid names.
Returns:
List of DataGrid names (e.g., ["app.orders", "app.customers"])
"""
...
def get_columns(self, table_name: str) -> list[str]:
"""
Return the column names for a specific DataGrid.
Args:
table_name: The DataGrid name
Returns:
List of column names (e.g., ["id", "amount", "status"])
"""
...
def get_column_values(self, table_name, column_name: str) -> list[Any]:
"""
Return the distinct values for a column in the current DataGrid.
This is used to suggest values in conditions like `value == |`.
Args:
column_name: The column name
Returns:
List of distinct values in the column
"""
...
def get_row_count(self, table_name: str) -> int:
"""
Return the number of rows in a DataGrid.
Used to suggest row indices for row scope and cell scope.
Args:
table_name: The DataGrid name
Returns:
Number of rows
"""
...
def get_style_presets(self) -> list[str]:
"""
Return the list of available style preset names.
Includes default presets (primary, error, etc.) and custom presets.
Returns:
List of style preset names
"""
...
def get_format_presets(self) -> list[str]:
"""
Return the list of available format preset names.
Includes default presets (EUR, USD, etc.) and custom presets.
Returns:
List of format preset names
"""
...

View File

@@ -0,0 +1,311 @@
"""
Suggestions generation for the formatting DSL.
Provides functions to generate appropriate suggestions
based on the detected context and scope.
"""
from myfasthtml.core.dsl.types import Suggestion
from . import presets
from .contexts import Context, DetectedScope
from .provider import DatagridMetadataProvider
def get_suggestions(
context: Context,
scope: DetectedScope,
provider: DatagridMetadataProvider,
) -> list[Suggestion]:
"""
Generate suggestions for the given context.
Args:
context: The detected completion context
scope: The detected scope
provider: Metadata provider for dynamic data
Returns:
List of suggestions
"""
match context:
# =================================================================
# Scope-level contexts
# =================================================================
case Context.NONE:
return []
case Context.SCOPE_KEYWORD:
return presets.SCOPE_KEYWORDS
case Context.COLUMN_NAME:
return _get_column_suggestions(provider)
case Context.ROW_INDEX:
return _get_row_index_suggestions(provider)
case Context.CELL_START:
return [Suggestion("(", "Start cell coordinates", "syntax")]
case Context.CELL_COLUMN:
return _get_column_suggestions(provider)
case Context.CELL_ROW:
return _get_row_index_suggestions(provider)
# =================================================================
# Rule-level contexts
# =================================================================
case Context.RULE_START:
return presets.RULE_START
# =================================================================
# Style contexts
# =================================================================
case Context.STYLE_ARGS:
# Presets (with quotes) + params
style_presets = _get_style_preset_suggestions_quoted(provider)
return style_presets + presets.STYLE_PARAMS
case Context.STYLE_PRESET:
return _get_style_preset_suggestions(provider)
case Context.STYLE_PARAM:
return presets.STYLE_PARAMS
# =================================================================
# Format contexts
# =================================================================
case Context.FORMAT_PRESET:
return _get_format_preset_suggestions(provider)
case Context.FORMAT_TYPE:
return presets.FORMAT_TYPES
case Context.FORMAT_PARAM_DATE:
return presets.FORMAT_PARAMS_DATE
case Context.FORMAT_PARAM_TEXT:
return presets.FORMAT_PARAMS_TEXT
# =================================================================
# After style/format
# =================================================================
case Context.AFTER_STYLE_OR_FORMAT:
return presets.AFTER_STYLE_OR_FORMAT
# =================================================================
# Condition contexts
# =================================================================
case Context.CONDITION_START:
return presets.CONDITION_START
case Context.CONDITION_AFTER_NOT:
return presets.CONDITION_AFTER_NOT
case Context.COLUMN_REF:
return _get_column_suggestions(provider)
case Context.COLUMN_REF_QUOTED:
return _get_column_suggestions_with_closing_quote(provider)
# =================================================================
# Operator contexts
# =================================================================
case Context.OPERATOR:
return presets.COMPARISON_OPERATORS
case Context.OPERATOR_VALUE | Context.BETWEEN_VALUE:
# col., True, False + column values
base = presets.OPERATOR_VALUE_BASE.copy()
base.extend(_get_column_value_suggestions(scope, provider))
return base
case Context.BETWEEN_AND:
return presets.BETWEEN_AND
case Context.IN_LIST_START:
return presets.IN_LIST_START
case Context.IN_LIST_VALUE:
return _get_column_value_suggestions(scope, provider)
# =================================================================
# Value contexts
# =================================================================
case Context.BOOLEAN_VALUE:
return presets.BOOLEAN_VALUES
case Context.COLOR_VALUE:
return presets.ALL_COLORS
case Context.DATE_FORMAT_VALUE:
return presets.DATE_PATTERNS
case Context.TRANSFORM_VALUE:
return presets.TEXT_TRANSFORMS
case _:
return []
def _get_column_suggestions(provider: DatagridMetadataProvider) -> list[Suggestion]:
"""Get column name suggestions from provider."""
try:
# Try to get columns from the first available table
tables = provider.get_tables()
if tables:
columns = provider.get_columns(tables[0])
return [Suggestion(col, "Column", "column") for col in columns]
except Exception:
pass
return []
def _get_column_suggestions_with_closing_quote(
provider: DatagridMetadataProvider,
) -> list[Suggestion]:
"""Get column name suggestions with closing quote."""
try:
tables = provider.get_tables()
if tables:
columns = provider.get_columns(tables[0])
return [Suggestion(f'{col}"', "Column", "column") for col in columns]
except Exception:
pass
return []
def _get_style_preset_suggestions(provider: DatagridMetadataProvider) -> list[Suggestion]:
"""Get style preset suggestions (without quotes)."""
suggestions = []
# Add provider presets if available
try:
custom_presets = provider.get_style_presets()
for preset in custom_presets:
# Check if it's already in default presets
if not any(s.label == preset for s in presets.STYLE_PRESETS):
suggestions.append(Suggestion(preset, "Custom preset", "preset"))
except Exception:
pass
# Add default presets (just the name, no quotes - we're inside quotes)
for preset in presets.STYLE_PRESETS:
suggestions.append(Suggestion(preset.label, preset.detail, preset.kind))
return suggestions
def _get_style_preset_suggestions_quoted(
provider: DatagridMetadataProvider,
) -> list[Suggestion]:
"""Get style preset suggestions with quotes."""
suggestions = []
# Add provider presets if available
try:
custom_presets = provider.get_style_presets()
for preset in custom_presets:
if not any(s.label == preset for s in presets.STYLE_PRESETS):
suggestions.append(Suggestion(f'"{preset}"', "Custom preset", "preset"))
except Exception:
pass
# Add default presets with quotes
for preset in presets.STYLE_PRESETS:
suggestions.append(Suggestion(f'"{preset.label}"', preset.detail, preset.kind))
return suggestions
def _get_format_preset_suggestions(provider: DatagridMetadataProvider) -> list[Suggestion]:
"""Get format preset suggestions (without quotes)."""
suggestions = []
# Add provider presets if available
try:
custom_presets = provider.get_format_presets()
for preset in custom_presets:
if not any(s.label == preset for s in presets.FORMAT_PRESETS):
suggestions.append(Suggestion(preset, "Custom preset", "preset"))
except Exception:
pass
# Add default presets
for preset in presets.FORMAT_PRESETS:
suggestions.append(Suggestion(preset.label, preset.detail, preset.kind))
return suggestions
def _get_row_index_suggestions(provider: DatagridMetadataProvider) -> list[Suggestion]:
"""Get row index suggestions (first 10 + last)."""
suggestions = []
try:
tables = provider.get_tables()
if tables:
row_count = provider.get_row_count(tables[0])
if row_count > 0:
# First 10 rows
for i in range(min(10, row_count)):
suggestions.append(Suggestion(str(i), f"Row {i}", "index"))
# Last row if not already included
last_index = row_count - 1
if last_index >= 10:
suggestions.append(
Suggestion(str(last_index), f"Last row ({row_count} total)", "index")
)
except Exception:
pass
# Fallback if no provider data
if not suggestions:
for i in range(10):
suggestions.append(Suggestion(str(i), f"Row {i}", "index"))
return suggestions
def _get_column_value_suggestions(
scope: DetectedScope,
provider: DatagridMetadataProvider,
) -> list[Suggestion]:
"""Get column value suggestions based on the current scope."""
if not scope.column_name:
return []
try:
values = provider.get_column_values(scope.column_name)
suggestions = []
for value in values:
if value is None:
continue
# Format value appropriately
if isinstance(value, str):
label = f'"{value}"'
detail = "Text value"
elif isinstance(value, bool):
label = str(value)
detail = "Boolean value"
elif isinstance(value, (int, float)):
label = str(value)
detail = "Numeric value"
else:
label = f'"{value}"'
detail = "Value"
suggestions.append(Suggestion(label, detail, "value"))
return suggestions
except Exception:
return []

View File

@@ -0,0 +1,23 @@
"""
FormattingDSL definition for the DslEditor control.
Provides the Lark grammar and derived completions for the
DataGrid Formatting DSL.
"""
from myfasthtml.core.dsl.base import DSLDefinition
from myfasthtml.core.formatting.dsl.grammar import GRAMMAR
class FormattingDSL(DSLDefinition):
"""
DSL definition for DataGrid formatting rules.
Uses the existing Lark grammar from grammar.py.
"""
name: str = "Formatting DSL"
def get_grammar(self) -> str:
"""Return the Lark grammar for formatting DSL."""
return GRAMMAR

View File

@@ -0,0 +1,55 @@
"""
DSL-specific exceptions.
"""
class DSLError(Exception):
"""Base exception for DSL errors."""
pass
class DSLSyntaxError(DSLError):
"""
Raised when the DSL input has syntax errors.
Attributes:
message: Error description
line: Line number where error occurred (1-based)
column: Column number where error occurred (1-based)
context: The problematic line or snippet
"""
def __init__(self, message: str, line: int = None, column: int = None, context: str = None):
self.message = message
self.line = line
self.column = column
self.context = context
super().__init__(self._format_message())
def _format_message(self) -> str:
parts = [self.message]
if self.line is not None:
parts.append(f"at line {self.line}")
if self.column is not None:
parts[1] = f"at line {self.line}, column {self.column}"
if self.context:
parts.append(f"\n {self.context}")
if self.column is not None:
parts.append(f"\n {' ' * (self.column - 1)}^")
return " ".join(parts[:2]) + "".join(parts[2:])
class DSLValidationError(DSLError):
"""
Raised when the DSL is syntactically correct but semantically invalid.
Examples:
- Unknown preset name
- Invalid parameter for formatter type
- Missing required parameter
"""
def __init__(self, message: str, line: int = None):
self.message = message
self.line = line
super().__init__(f"{message}" + (f" at line {line}" if line else ""))

View File

@@ -0,0 +1,159 @@
"""
Lark grammar for the DataGrid Formatting DSL.
This grammar is designed to be translatable to Lezer for CodeMirror integration.
"""
GRAMMAR = r"""
// ==================== Top-level structure ====================
start: _NL* scope+
// ==================== Scopes ====================
scope: scope_header ":" _NL _INDENT rule+ _DEDENT
scope_header: column_scope
| row_scope
| cell_scope
column_scope: "column" column_name
row_scope: "row" INTEGER
cell_scope: "cell" cell_ref
column_name: NAME -> name
| QUOTED_STRING -> quoted_name
cell_ref: "(" column_name "," INTEGER ")" -> cell_coords
| CELL_ID -> cell_id
// ==================== Rules ====================
rule: rule_content _NL
rule_content: style_expr format_expr? condition?
| format_expr style_expr? condition?
condition: "if" comparison
// ==================== Comparisons ====================
comparison: negation? comparison_expr case_modifier?
negation: "not"
comparison_expr: binary_comparison
| unary_comparison
binary_comparison: operand operator operand -> binary_comp
| operand "in" list -> in_comp
| operand "between" operand "and" operand -> between_comp
unary_comparison: operand "isempty" -> isempty_comp
| operand "isnotempty" -> isnotempty_comp
case_modifier: "(" "case" ")"
// ==================== Operators ====================
operator: "==" -> op_eq
| "!=" -> op_ne
| "<=" -> op_le
| "<" -> op_lt
| ">=" -> op_ge
| ">" -> op_gt
| "contains" -> op_contains
| "startswith" -> op_startswith
| "endswith" -> op_endswith
// ==================== Operands ====================
operand: value_ref
| column_ref
| row_ref
| cell_ref_expr
| literal
| arithmetic
| "(" operand ")"
value_ref: "value"
column_ref: "col" "." (NAME | QUOTED_STRING)
row_ref: "row" "." INTEGER
cell_ref_expr: "cell" "." NAME "-" INTEGER
literal: QUOTED_STRING -> string_literal
| SIGNED_NUMBER -> number_literal
| BOOLEAN -> boolean_literal
arithmetic: operand arith_op operand
arith_op: "*" -> arith_mul
| "/" -> arith_div
| "+" -> arith_add
| "-" -> arith_sub
list: "[" [literal ("," literal)*] "]"
// ==================== Style expression ====================
style_expr: "style" "(" style_args ")"
style_args: QUOTED_STRING ("," kwargs)? -> style_with_preset
| kwargs -> style_without_preset
// ==================== Format expression ====================
format_expr: format_preset
| format_typed
format_preset: "format" "(" QUOTED_STRING ("," kwargs)? ")"
format_typed: "format" "." format_type "(" kwargs? ")"
format_type: "number" -> fmt_number
| "date" -> fmt_date
| "boolean" -> fmt_boolean
| "text" -> fmt_text
| "enum" -> fmt_enum
// ==================== Keyword arguments ====================
kwargs: kwarg ("," kwarg)*
kwarg: NAME "=" kwarg_value
kwarg_value: QUOTED_STRING -> kwarg_string
| SIGNED_NUMBER -> kwarg_number
| BOOLEAN -> kwarg_boolean
| dict -> kwarg_dict
dict: "{" [dict_entry ("," dict_entry)*] "}"
dict_entry: QUOTED_STRING ":" (QUOTED_STRING | SIGNED_NUMBER | BOOLEAN)
// ==================== Terminals ====================
NAME: /[a-zA-Z_][a-zA-Z0-9_]*/
QUOTED_STRING: /"[^"]*"/ | /'[^']*'/
INTEGER: /[0-9]+/
SIGNED_NUMBER: /[+-]?[0-9]+(\.[0-9]+)?/
BOOLEAN: "True" | "False" | "true" | "false"
CELL_ID: /tcell_[a-zA-Z0-9_-]+/
// ==================== Whitespace handling ====================
COMMENT: /#[^\n]*/
// Newline token includes following whitespace for indentation tracking
// This is required by lark's Indenter to detect indentation levels
_NL: /(\r?\n[\t ]*)+/
// Ignore inline whitespace (within a line, not at line start)
%ignore /[\t ]+/
%ignore COMMENT
%declare _INDENT _DEDENT
"""

View File

@@ -0,0 +1,111 @@
"""
DSL Parser using lark.
Handles parsing of the DSL text into an AST.
"""
from lark import Lark, UnexpectedInput
from lark.indenter import Indenter
from .exceptions import DSLSyntaxError
from .grammar import GRAMMAR
class DSLIndenter(Indenter):
"""
Custom indenter for Python-style indentation.
Handles INDENT/DEDENT tokens for scoped rules.
"""
NL_type = "_NL"
OPEN_PAREN_types = [] # No multi-line expressions in our DSL
CLOSE_PAREN_types = []
INDENT_type = "_INDENT"
DEDENT_type = "_DEDENT"
tab_len = 4
class DSLParser:
"""
Parser for the DataGrid Formatting DSL.
Uses lark with custom indentation handling.
Example:
parser = DSLParser()
tree = parser.parse('''
column amount:
style("error") if value < 0
format("EUR")
''')
"""
def __init__(self):
self._parser = Lark(
GRAMMAR,
parser="lalr",
postlex=DSLIndenter(),
propagate_positions=True,
)
def parse(self, text: str):
"""
Parse DSL text into an AST.
Args:
text: The DSL text to parse
Returns:
lark.Tree: The parsed AST
Raises:
DSLSyntaxError: If the text has syntax errors
"""
# Pre-process: replace comment lines with empty lines (preserves line numbers)
lines = text.split("\n")
lines = ["" if line.strip().startswith("#") else line for line in lines]
text = "\n".join(lines)
# Strip leading whitespace/newlines and ensure text ends with newline
text = text.strip()
if text and not text.endswith("\n"):
text += "\n"
try:
return self._parser.parse(text)
except UnexpectedInput as e:
# Extract context for error message
context = None
if hasattr(e, "get_context"):
context = e.get_context(text)
raise DSLSyntaxError(
message=self._format_error_message(e),
line=getattr(e, "line", None),
column=getattr(e, "column", None),
context=context,
) from e
def _format_error_message(self, error: UnexpectedInput) -> str:
"""Format a user-friendly error message from lark exception."""
if hasattr(error, "expected"):
expected = list(error.expected)
if len(expected) == 1:
return f"Expected {expected[0]}"
elif len(expected) <= 5:
return f"Expected one of: {', '.join(expected)}"
else:
return "Unexpected input"
return str(error)
# Singleton parser instance
_parser = None
def get_parser() -> DSLParser:
"""Get the singleton parser instance."""
global _parser
if _parser is None:
_parser = DSLParser()
return _parser

View File

@@ -0,0 +1,47 @@
"""
Scope dataclasses for DSL output.
"""
from dataclasses import dataclass
from ..dataclasses import FormatRule
@dataclass
class ColumnScope:
"""Scope targeting a column by name."""
column: str
@dataclass
class RowScope:
"""Scope targeting a row by index."""
row: int
@dataclass
class CellScope:
"""
Scope targeting a specific cell.
Can be specified either by:
- Coordinates: column + row
- Cell ID: cell_id
"""
column: str = None
row: int = None
cell_id: str = None
@dataclass
class ScopedRule:
"""
A format rule with its scope.
The DSL parser returns a list of ScopedRule objects.
Attributes:
scope: Where the rule applies (ColumnScope, RowScope, or CellScope)
rule: The FormatRule (condition + style + formatter)
"""
scope: ColumnScope | RowScope | CellScope
rule: FormatRule

View File

@@ -0,0 +1,430 @@
"""
DSL Transformer.
Converts lark AST into FormatRule and ScopedRule objects.
"""
from lark import Transformer
from .exceptions import DSLValidationError
from .scopes import ColumnScope, RowScope, CellScope, ScopedRule
from ..dataclasses import (
Condition,
Style,
FormatRule,
NumberFormatter,
DateFormatter,
BooleanFormatter,
TextFormatter,
EnumFormatter,
)
class DSLTransformer(Transformer):
"""
Transforms the lark AST into ScopedRule objects.
This transformer visits each node in the AST and converts it
to the appropriate dataclass.
"""
# ==================== Top-level ====================
def start(self, items):
"""Flatten all scoped rules from all scopes."""
result = []
for scope_rules in items:
result.extend(scope_rules)
return result
# ==================== Scopes ====================
def scope(self, items):
"""Process a scope block, returning list of ScopedRules."""
scope_obj = items[0] # scope_header result
rules = items[1:] # rule results
return [ScopedRule(scope=scope_obj, rule=rule) for rule in rules]
def scope_header(self, items):
return items[0]
def column_scope(self, items):
column_name = items[0]
return ColumnScope(column=column_name)
def row_scope(self, items):
row_index = int(items[0])
return RowScope(row=row_index)
def cell_scope(self, items):
return items[0] # cell_ref result
def cell_coords(self, items):
column_name = items[0]
row_index = int(items[1])
return CellScope(column=column_name, row=row_index)
def cell_id(self, items):
cell_id = str(items[0])
return CellScope(cell_id=cell_id)
def name(self, items):
return str(items[0])
def quoted_name(self, items):
return self._unquote(items[0])
# ==================== Rules ====================
def rule(self, items):
return items[0] # rule_content result
def rule_content(self, items):
"""Build a FormatRule from style, format, and condition."""
style_obj = None
formatter_obj = None
condition_obj = None
for item in items:
if isinstance(item, Style):
style_obj = item
elif isinstance(item, (NumberFormatter, DateFormatter, BooleanFormatter,
TextFormatter, EnumFormatter)):
formatter_obj = item
elif isinstance(item, Condition):
condition_obj = item
return FormatRule(
condition=condition_obj,
style=style_obj,
formatter=formatter_obj,
)
# ==================== Conditions ====================
def condition(self, items):
return items[0] # comparison result
def comparison(self, items):
"""Process comparison with optional negation and case modifier."""
negate = False
case_sensitive = False
condition = None
for item in items:
if item == "not":
negate = True
elif item == "case":
case_sensitive = True
elif isinstance(item, Condition):
condition = item
if condition:
condition.negate = negate
condition.case_sensitive = case_sensitive
return condition
def negation(self, items):
return "not"
def case_modifier(self, items):
return "case"
def comparison_expr(self, items):
return items[0]
def binary_comparison(self, items):
return items[0]
def unary_comparison(self, items):
return items[0]
def binary_comp(self, items):
left, operator, right = items
# Handle column reference in value
if isinstance(right, dict) and "col" in right:
value = right
else:
value = right
return Condition(operator=operator, value=value)
def in_comp(self, items):
operand, values = items
return Condition(operator="in", value=values)
def between_comp(self, items):
operand, low, high = items
return Condition(operator="between", value=[low, high])
def isempty_comp(self, items):
return Condition(operator="isempty")
def isnotempty_comp(self, items):
return Condition(operator="isnotempty")
# ==================== Operators ====================
def op_eq(self, items):
return "=="
def op_ne(self, items):
return "!="
def op_lt(self, items):
return "<"
def op_le(self, items):
return "<="
def op_gt(self, items):
return ">"
def op_ge(self, items):
return ">="
def op_contains(self, items):
return "contains"
def op_startswith(self, items):
return "startswith"
def op_endswith(self, items):
return "endswith"
# ==================== Operands ====================
def operand(self, items):
return items[0]
def value_ref(self, items):
return "value" # Marker for current cell value
def column_ref(self, items):
col_name = items[0]
if isinstance(col_name, str) and col_name.startswith('"'):
col_name = self._unquote(col_name)
return {"col": col_name}
def row_ref(self, items):
row_index = int(items[0])
return {"row": row_index}
def cell_ref_expr(self, items):
col_name = str(items[0])
row_index = int(items[1])
return {"col": col_name, "row": row_index}
def literal(self, items):
return items[0]
def string_literal(self, items):
return self._unquote(items[0])
def number_literal(self, items):
value = str(items[0])
if "." in value:
return float(value)
return int(value)
def boolean_literal(self, items):
return str(items[0]).lower() == "true"
def arithmetic(self, items):
left, op, right = items
# For now, return as a dict representing the expression
# This could be evaluated later or kept as-is for complex comparisons
return {"arithmetic": {"left": left, "op": op, "right": right}}
def arith_mul(self, items):
return "*"
def arith_div(self, items):
return "/"
def arith_add(self, items):
return "+"
def arith_sub(self, items):
return "-"
def list(self, items):
return list(items)
# ==================== Style ====================
def style_expr(self, items):
return items[0] # style_args result
def style_args(self, items):
return items[0]
def style_with_preset(self, items):
preset = self._unquote(items[0])
kwargs = items[1] if len(items) > 1 else {}
return self._build_style(preset, kwargs)
def style_without_preset(self, items):
kwargs = items[0] if items else {}
return self._build_style(None, kwargs)
def _build_style(self, preset: str, kwargs: dict) -> Style:
"""Build a Style object from preset and kwargs."""
# Map DSL parameter names to Style attribute names
param_map = {
"bold": ("font_weight", lambda v: "bold" if v else "normal"),
"italic": ("font_style", lambda v: "italic" if v else "normal"),
"underline": ("text_decoration", lambda v: "underline" if v else None),
"strikethrough": ("text_decoration", lambda v: "line-through" if v else None),
"background_color": ("background_color", lambda v: v),
"color": ("color", lambda v: v),
"font_size": ("font_size", lambda v: v),
}
style_kwargs = {"preset": preset}
for key, value in kwargs.items():
if key in param_map:
attr_name, converter = param_map[key]
converted = converter(value)
if converted is not None:
style_kwargs[attr_name] = converted
else:
# Pass through unknown params (may be custom)
style_kwargs[key] = value
return Style(**{k: v for k, v in style_kwargs.items() if v is not None})
# ==================== Format ====================
def format_expr(self, items):
return items[0]
def format_preset(self, items):
preset = self._unquote(items[0])
kwargs = items[1] if len(items) > 1 else {}
# When using preset, we don't know the type yet
# Return a generic formatter with preset
return NumberFormatter(preset=preset, **self._filter_number_kwargs(kwargs))
def format_typed(self, items):
format_type = items[0]
kwargs = items[1] if len(items) > 1 else {}
return self._build_formatter(format_type, kwargs)
def format_type(self, items):
return items[0]
def fmt_number(self, items):
return "number"
def fmt_date(self, items):
return "date"
def fmt_boolean(self, items):
return "boolean"
def fmt_text(self, items):
return "text"
def fmt_enum(self, items):
return "enum"
def _build_formatter(self, format_type: str, kwargs: dict):
"""Build the appropriate Formatter subclass."""
if format_type == "number":
return NumberFormatter(**self._filter_number_kwargs(kwargs))
elif format_type == "date":
return DateFormatter(**self._filter_date_kwargs(kwargs))
elif format_type == "boolean":
return BooleanFormatter(**self._filter_boolean_kwargs(kwargs))
elif format_type == "text":
return TextFormatter(**self._filter_text_kwargs(kwargs))
elif format_type == "enum":
return EnumFormatter(**self._filter_enum_kwargs(kwargs))
else:
raise DSLValidationError(f"Unknown formatter type: {format_type}")
def _filter_number_kwargs(self, kwargs: dict) -> dict:
"""Filter kwargs for NumberFormatter."""
valid_keys = {"prefix", "suffix", "thousands_sep", "decimal_sep", "precision", "multiplier"}
return {k: v for k, v in kwargs.items() if k in valid_keys}
def _filter_date_kwargs(self, kwargs: dict) -> dict:
"""Filter kwargs for DateFormatter."""
valid_keys = {"format"}
return {k: v for k, v in kwargs.items() if k in valid_keys}
def _filter_boolean_kwargs(self, kwargs: dict) -> dict:
"""Filter kwargs for BooleanFormatter."""
valid_keys = {"true_value", "false_value", "null_value"}
return {k: v for k, v in kwargs.items() if k in valid_keys}
def _filter_text_kwargs(self, kwargs: dict) -> dict:
"""Filter kwargs for TextFormatter."""
valid_keys = {"transform", "max_length", "ellipsis"}
return {k: v for k, v in kwargs.items() if k in valid_keys}
def _filter_enum_kwargs(self, kwargs: dict) -> dict:
"""Filter kwargs for EnumFormatter."""
valid_keys = {"source", "default", "allow_empty", "empty_label", "order_by"}
return {k: v for k, v in kwargs.items() if k in valid_keys}
# ==================== Keyword arguments ====================
def kwargs(self, items):
"""Collect keyword arguments into a dict."""
result = {}
for item in items:
if isinstance(item, tuple):
key, value = item
result[key] = value
return result
def kwarg(self, items):
key = str(items[0])
value = items[1]
return (key, value)
def kwarg_value(self, items):
return items[0]
def kwarg_string(self, items):
return self._unquote(items[0])
def kwarg_number(self, items):
value = str(items[0])
if "." in value:
return float(value)
return int(value)
def kwarg_boolean(self, items):
return str(items[0]).lower() == "true"
def kwarg_dict(self, items):
return items[0]
def dict(self, items):
"""Build a dict from dict entries."""
result = {}
for item in items:
if isinstance(item, tuple):
key, value = item
result[key] = value
return result
def dict_entry(self, items):
key = self._unquote(items[0])
value = items[1]
if isinstance(value, str) and (value.startswith('"') or value.startswith("'")):
value = self._unquote(value)
return (key, value)
# ==================== Helpers ====================
def _unquote(self, s) -> str:
"""Remove quotes from a quoted string."""
s = str(s)
if (s.startswith('"') and s.endswith('"')) or (s.startswith("'") and s.endswith("'")):
return s[1:-1]
return s