Working on Formating DSL completion
This commit is contained in:
69
src/myfasthtml/core/formatting/dsl/__init__.py
Normal file
69
src/myfasthtml/core/formatting/dsl/__init__.py
Normal 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",
|
||||
]
|
||||
323
src/myfasthtml/core/formatting/dsl/completion/contexts.py
Normal file
323
src/myfasthtml/core/formatting/dsl/completion/contexts.py
Normal 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
|
||||
109
src/myfasthtml/core/formatting/dsl/completion/engine.py
Normal file
109
src/myfasthtml/core/formatting/dsl/completion/engine.py
Normal 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)
|
||||
245
src/myfasthtml/core/formatting/dsl/completion/presets.py
Normal file
245
src/myfasthtml/core/formatting/dsl/completion/presets.py
Normal 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"),
|
||||
]
|
||||
94
src/myfasthtml/core/formatting/dsl/completion/provider.py
Normal file
94
src/myfasthtml/core/formatting/dsl/completion/provider.py
Normal 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
|
||||
"""
|
||||
...
|
||||
311
src/myfasthtml/core/formatting/dsl/completion/suggestions.py
Normal file
311
src/myfasthtml/core/formatting/dsl/completion/suggestions.py
Normal 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 []
|
||||
23
src/myfasthtml/core/formatting/dsl/definition.py
Normal file
23
src/myfasthtml/core/formatting/dsl/definition.py
Normal 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
|
||||
55
src/myfasthtml/core/formatting/dsl/exceptions.py
Normal file
55
src/myfasthtml/core/formatting/dsl/exceptions.py
Normal 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 ""))
|
||||
159
src/myfasthtml/core/formatting/dsl/grammar.py
Normal file
159
src/myfasthtml/core/formatting/dsl/grammar.py
Normal 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
|
||||
"""
|
||||
111
src/myfasthtml/core/formatting/dsl/parser.py
Normal file
111
src/myfasthtml/core/formatting/dsl/parser.py
Normal 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
|
||||
47
src/myfasthtml/core/formatting/dsl/scopes.py
Normal file
47
src/myfasthtml/core/formatting/dsl/scopes.py
Normal 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
|
||||
430
src/myfasthtml/core/formatting/dsl/transformer.py
Normal file
430
src/myfasthtml/core/formatting/dsl/transformer.py
Normal 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
|
||||
Reference in New Issue
Block a user