Added Rules preset (on top of format and style presets)
This commit is contained in:
@@ -6,164 +6,195 @@ from typing import Any
|
||||
|
||||
@dataclass
|
||||
class Condition:
|
||||
"""
|
||||
Represents a condition for conditional formatting.
|
||||
"""
|
||||
Represents a condition for conditional formatting.
|
||||
|
||||
Attributes:
|
||||
operator: Comparison operator ("==", "!=", "<", "<=", ">", ">=",
|
||||
"contains", "startswith", "endswith", "in", "between",
|
||||
"isempty", "isnotempty")
|
||||
value: Value to compare against (literal, list, or {"col": "..."} for reference)
|
||||
negate: If True, inverts the condition result
|
||||
case_sensitive: If True, string comparisons are case-sensitive (default False)
|
||||
col: Column ID for row-level conditions (evaluate this column instead of current cell)
|
||||
row: Row index for column-level conditions (evaluate this row instead of current cell)
|
||||
"""
|
||||
operator: str
|
||||
value: Any = None
|
||||
negate: bool = False
|
||||
case_sensitive: bool = False
|
||||
col: str = None
|
||||
row: int = None
|
||||
Attributes:
|
||||
operator: Comparison operator ("==", "!=", "<", "<=", ">", ">=",
|
||||
"contains", "startswith", "endswith", "in", "between",
|
||||
"isempty", "isnotempty")
|
||||
value: Value to compare against (literal, list, or {"col": "..."} for reference)
|
||||
negate: If True, inverts the condition result
|
||||
case_sensitive: If True, string comparisons are case-sensitive (default False)
|
||||
col: Column ID for row-level conditions (evaluate this column instead of current cell)
|
||||
row: Row index for column-level conditions (evaluate this row instead of current cell)
|
||||
"""
|
||||
operator: str
|
||||
value: Any = None
|
||||
negate: bool = False
|
||||
case_sensitive: bool = False
|
||||
col: str = None
|
||||
row: int = None
|
||||
|
||||
|
||||
# === Style ===
|
||||
|
||||
@dataclass
|
||||
class Style:
|
||||
"""
|
||||
Represents style properties for cell formatting.
|
||||
"""
|
||||
Represents style properties for cell formatting.
|
||||
|
||||
Attributes:
|
||||
preset: Name of a style preset ("primary", "success", "error", etc.)
|
||||
background_color: Background color (hex, CSS name, or CSS variable)
|
||||
color: Text color
|
||||
font_weight: "normal" or "bold"
|
||||
font_style: "normal" or "italic"
|
||||
font_size: Font size ("12px", "0.9em")
|
||||
text_decoration: "none", "underline", or "line-through"
|
||||
"""
|
||||
preset: str = None
|
||||
background_color: str = None
|
||||
color: str = None
|
||||
font_weight: str = None
|
||||
font_style: str = None
|
||||
font_size: str = None
|
||||
text_decoration: str = None
|
||||
Attributes:
|
||||
preset: Name of a style preset ("primary", "success", "error", etc.)
|
||||
background_color: Background color (hex, CSS name, or CSS variable)
|
||||
color: Text color
|
||||
font_weight: "normal" or "bold"
|
||||
font_style: "normal" or "italic"
|
||||
font_size: Font size ("12px", "0.9em")
|
||||
text_decoration: "none", "underline", or "line-through"
|
||||
"""
|
||||
preset: str = None
|
||||
background_color: str = None
|
||||
color: str = None
|
||||
font_weight: str = None
|
||||
font_style: str = None
|
||||
font_size: str = None
|
||||
text_decoration: str = None
|
||||
|
||||
|
||||
# === Formatters ===
|
||||
|
||||
@dataclass
|
||||
class Formatter:
|
||||
"""Base class for all formatters."""
|
||||
preset: str = None
|
||||
"""Base class for all formatters."""
|
||||
preset: str = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class NumberFormatter(Formatter):
|
||||
"""
|
||||
Formatter for numbers, currencies, and percentages.
|
||||
"""
|
||||
Formatter for numbers, currencies, and percentages.
|
||||
|
||||
Attributes:
|
||||
prefix: Text before value (e.g., "$")
|
||||
suffix: Text after value (e.g., " EUR")
|
||||
thousands_sep: Thousands separator (e.g., ",", " ")
|
||||
decimal_sep: Decimal separator (e.g., ".", ",")
|
||||
precision: Number of decimal places
|
||||
multiplier: Multiply value before display (e.g., 100 for percentage)
|
||||
"""
|
||||
prefix: str = ""
|
||||
suffix: str = ""
|
||||
thousands_sep: str = ""
|
||||
decimal_sep: str = "."
|
||||
precision: int = 0
|
||||
multiplier: float = 1.0
|
||||
absolute: bool = False
|
||||
Attributes:
|
||||
prefix: Text before value (e.g., "$")
|
||||
suffix: Text after value (e.g., " EUR")
|
||||
thousands_sep: Thousands separator (e.g., ",", " ")
|
||||
decimal_sep: Decimal separator (e.g., ".", ",")
|
||||
precision: Number of decimal places
|
||||
multiplier: Multiply value before display (e.g., 100 for percentage)
|
||||
"""
|
||||
prefix: str = ""
|
||||
suffix: str = ""
|
||||
thousands_sep: str = ""
|
||||
decimal_sep: str = "."
|
||||
precision: int = 0
|
||||
multiplier: float = 1.0
|
||||
absolute: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class DateFormatter(Formatter):
|
||||
"""
|
||||
Formatter for dates and datetimes.
|
||||
"""
|
||||
Formatter for dates and datetimes.
|
||||
|
||||
Attributes:
|
||||
format: strftime format pattern (default: "%Y-%m-%d")
|
||||
"""
|
||||
format: str = "%Y-%m-%d"
|
||||
Attributes:
|
||||
format: strftime format pattern (default: "%Y-%m-%d")
|
||||
"""
|
||||
format: str = "%Y-%m-%d"
|
||||
|
||||
|
||||
@dataclass
|
||||
class BooleanFormatter(Formatter):
|
||||
"""
|
||||
Formatter for boolean values.
|
||||
"""
|
||||
Formatter for boolean values.
|
||||
|
||||
Attributes:
|
||||
true_value: Display string for True
|
||||
false_value: Display string for False
|
||||
null_value: Display string for None/null
|
||||
"""
|
||||
true_value: str = "true"
|
||||
false_value: str = "false"
|
||||
null_value: str = ""
|
||||
Attributes:
|
||||
true_value: Display string for True
|
||||
false_value: Display string for False
|
||||
null_value: Display string for None/null
|
||||
"""
|
||||
true_value: str = "true"
|
||||
false_value: str = "false"
|
||||
null_value: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class TextFormatter(Formatter):
|
||||
"""
|
||||
Formatter for text transformations.
|
||||
"""
|
||||
Formatter for text transformations.
|
||||
|
||||
Attributes:
|
||||
transform: Text transformation ("uppercase", "lowercase", "capitalize")
|
||||
max_length: Maximum length before truncation
|
||||
ellipsis: Suffix when truncated (default: "...")
|
||||
"""
|
||||
transform: str = None
|
||||
max_length: int = None
|
||||
ellipsis: str = "..."
|
||||
|
||||
Attributes:
|
||||
transform: Text transformation ("uppercase", "lowercase", "capitalize")
|
||||
max_length: Maximum length before truncation
|
||||
ellipsis: Suffix when truncated (default: "...")
|
||||
"""
|
||||
transform: str = None
|
||||
max_length: int = None
|
||||
ellipsis: str = "..."
|
||||
|
||||
@dataclass
|
||||
class ConstantFormatter(Formatter):
|
||||
value: str = None
|
||||
value: str = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class EnumFormatter(Formatter):
|
||||
"""
|
||||
Formatter for mapping values to display labels.
|
||||
"""
|
||||
Formatter for mapping values to display labels.
|
||||
|
||||
Attributes:
|
||||
source: Data source dict with "type" and "value" keys
|
||||
- {"type": "mapping", "value": {"key": "label", ...}}
|
||||
- {"type": "datagrid", "value": "grid_id", "value_column": "id", "display_column": "name"}
|
||||
default: Label for unknown values
|
||||
allow_empty: Show empty option in Select dropdowns
|
||||
empty_label: Label for empty option
|
||||
order_by: Sort order ("source", "display", "value")
|
||||
"""
|
||||
source: dict = field(default_factory=dict)
|
||||
default: str = ""
|
||||
allow_empty: bool = True
|
||||
empty_label: str = "-- Select --"
|
||||
order_by: str = "source"
|
||||
Attributes:
|
||||
source: Data source dict with "type" and "value" keys
|
||||
- {"type": "mapping", "value": {"key": "label", ...}}
|
||||
- {"type": "datagrid", "value": "grid_id", "value_column": "id", "display_column": "name"}
|
||||
default: Label for unknown values
|
||||
allow_empty: Show empty option in Select dropdowns
|
||||
empty_label: Label for empty option
|
||||
order_by: Sort order ("source", "display", "value")
|
||||
"""
|
||||
source: dict = field(default_factory=dict)
|
||||
default: str = ""
|
||||
allow_empty: bool = True
|
||||
empty_label: str = "-- Select --"
|
||||
order_by: str = "source"
|
||||
|
||||
|
||||
# === Format Rule ===
|
||||
|
||||
@dataclass
|
||||
class FormatRule:
|
||||
"""
|
||||
A formatting rule combining condition, style, and formatter.
|
||||
"""
|
||||
A formatting rule combining condition, style, and formatter.
|
||||
|
||||
Rules:
|
||||
- style and formatter can appear alone (unconditional formatting)
|
||||
- condition cannot appear alone - must be paired with style and/or formatter
|
||||
- If condition is present, style/formatter is applied only if condition is met
|
||||
Rules:
|
||||
- style and formatter can appear alone (unconditional formatting)
|
||||
- condition cannot appear alone - must be paired with style and/or formatter
|
||||
- If condition is present, style/formatter is applied only if condition is met
|
||||
|
||||
Attributes:
|
||||
condition: Optional condition for conditional formatting
|
||||
style: Optional style to apply
|
||||
formatter: Optional formatter to apply
|
||||
"""
|
||||
condition: Condition = None
|
||||
style: Style = None
|
||||
formatter: Formatter = None
|
||||
Attributes:
|
||||
condition: Optional condition for conditional formatting
|
||||
style: Optional style to apply
|
||||
formatter: Optional formatter to apply
|
||||
"""
|
||||
condition: Condition = None
|
||||
style: Style = None
|
||||
formatter: Formatter = None
|
||||
|
||||
|
||||
# === Rule Preset ===
|
||||
|
||||
@dataclass
|
||||
class RulePreset:
|
||||
"""
|
||||
A named, reusable list of FormatRules.
|
||||
|
||||
Referenced in DSL as format("name") or style("name").
|
||||
Appears in format() suggestions if at least one rule has a formatter.
|
||||
Appears in style() suggestions if at least one rule has a style.
|
||||
|
||||
Attributes:
|
||||
name: Unique identifier used in DSL (e.g. "accounting")
|
||||
description: Human-readable description shown in the UI
|
||||
rules: Ordered list of FormatRule to apply
|
||||
"""
|
||||
name: str
|
||||
description: str
|
||||
rules: list[FormatRule] = field(default_factory=list)
|
||||
|
||||
def has_formatter(self) -> bool:
|
||||
"""Returns True if at least one rule defines a formatter."""
|
||||
return any(r.formatter is not None for r in self.rules)
|
||||
|
||||
def has_style(self) -> bool:
|
||||
"""Returns True if at least one rule defines a style."""
|
||||
return any(r.style is not None for r in self.rules)
|
||||
|
||||
@@ -322,28 +322,41 @@ class FormattingCompletionEngine(BaseCompletionEngine):
|
||||
def _get_style_preset_suggestions(self) -> list[Suggestion]:
|
||||
"""Get style preset suggestions (without quotes)."""
|
||||
suggestions = []
|
||||
|
||||
# Add provider presets if available
|
||||
|
||||
# Add rule presets that have at least one style rule
|
||||
try:
|
||||
for rule_preset in self.provider.list_rule_presets_for_style():
|
||||
suggestions.append(Suggestion(rule_preset.name, rule_preset.description, "rule_preset"))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Add provider custom style presets
|
||||
try:
|
||||
custom_presets = self.provider.list_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)
|
||||
|
||||
# Add default style presets (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(self) -> list[Suggestion]:
|
||||
"""Get style preset suggestions with quotes."""
|
||||
suggestions = []
|
||||
|
||||
# Add provider presets if available
|
||||
|
||||
# Add rule presets that have at least one style rule
|
||||
try:
|
||||
for rule_preset in self.provider.list_rule_presets_for_style():
|
||||
suggestions.append(Suggestion(f'"{rule_preset.name}"', rule_preset.description, "rule_preset"))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Add provider custom style presets
|
||||
try:
|
||||
custom_presets = self.provider.list_style_presets()
|
||||
for preset in custom_presets:
|
||||
@@ -351,18 +364,25 @@ class FormattingCompletionEngine(BaseCompletionEngine):
|
||||
suggestions.append(Suggestion(f'"{preset}"', "Custom preset", "preset"))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Add default presets with quotes
|
||||
|
||||
# Add default style 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(self) -> list[Suggestion]:
|
||||
"""Get format preset suggestions (without quotes)."""
|
||||
suggestions = []
|
||||
|
||||
# Add provider presets if available
|
||||
|
||||
# Add rule presets that have at least one formatter rule
|
||||
try:
|
||||
for rule_preset in self.provider.list_rule_presets_for_format():
|
||||
suggestions.append(Suggestion(rule_preset.name, rule_preset.description, "rule_preset"))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Add provider custom formatter presets
|
||||
try:
|
||||
custom_presets = self.provider.list_format_presets()
|
||||
for preset in custom_presets:
|
||||
@@ -370,11 +390,11 @@ class FormattingCompletionEngine(BaseCompletionEngine):
|
||||
suggestions.append(Suggestion(preset, "Custom preset", "preset"))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Add default presets
|
||||
|
||||
# Add default formatter presets
|
||||
for preset in presets.FORMAT_PRESETS:
|
||||
suggestions.append(Suggestion(preset.label, preset.detail, preset.kind))
|
||||
|
||||
|
||||
return suggestions
|
||||
|
||||
def _get_row_index_suggestions(self) -> list[Suggestion]:
|
||||
|
||||
@@ -11,7 +11,10 @@ from typing import Any, Optional
|
||||
|
||||
from myfasthtml.core.data.DataServicesManager import DataServicesManager
|
||||
from myfasthtml.core.dsl.base_provider import BaseMetadataProvider
|
||||
from myfasthtml.core.formatting.presets import DEFAULT_FORMATTER_PRESETS, DEFAULT_STYLE_PRESETS
|
||||
from myfasthtml.core.formatting.dataclasses import RulePreset
|
||||
from myfasthtml.core.formatting.presets import (
|
||||
DEFAULT_FORMATTER_PRESETS, DEFAULT_STYLE_PRESETS, DEFAULT_RULE_PRESETS,
|
||||
)
|
||||
from myfasthtml.core.instances import SingleInstance, InstancesManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -38,6 +41,7 @@ class DatagridMetadataProvider(SingleInstance, BaseMetadataProvider):
|
||||
super().__init__(parent, session, _id)
|
||||
self.style_presets: dict = DEFAULT_STYLE_PRESETS.copy()
|
||||
self.formatter_presets: dict = DEFAULT_FORMATTER_PRESETS.copy()
|
||||
self.rule_presets: dict[str, RulePreset] = DEFAULT_RULE_PRESETS.copy()
|
||||
self.all_tables_formats: list = []
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
@@ -132,6 +136,14 @@ class DatagridMetadataProvider(SingleInstance, BaseMetadataProvider):
|
||||
"""Return the names of all registered formatter presets."""
|
||||
return list(self.formatter_presets.keys())
|
||||
|
||||
def list_rule_presets_for_format(self) -> list[RulePreset]:
|
||||
"""Return rule presets that have at least one rule with a formatter."""
|
||||
return [p for p in self.rule_presets.values() if p.has_formatter()]
|
||||
|
||||
def list_rule_presets_for_style(self) -> list[RulePreset]:
|
||||
"""Return rule presets that have at least one rule with a style."""
|
||||
return [p for p in self.rule_presets.values() if p.has_style()]
|
||||
|
||||
def get_style_presets(self) -> dict:
|
||||
"""Return the full style presets dict."""
|
||||
return self.style_presets
|
||||
|
||||
@@ -3,6 +3,7 @@ from typing import Any, Callable
|
||||
from myfasthtml.core.formatting.condition_evaluator import ConditionEvaluator
|
||||
from myfasthtml.core.formatting.dataclasses import FormatRule
|
||||
from myfasthtml.core.formatting.formatter_resolver import FormatterResolver
|
||||
from myfasthtml.core.formatting.presets import DEFAULT_RULE_PRESETS
|
||||
from myfasthtml.core.formatting.style_resolver import StyleResolver, StyleContainer
|
||||
|
||||
|
||||
@@ -29,6 +30,7 @@ class FormattingEngine:
|
||||
self,
|
||||
style_presets: dict = None,
|
||||
formatter_presets: dict = None,
|
||||
rule_presets: dict = None,
|
||||
lookup_resolver: Callable[[str, str, str], dict] = None
|
||||
):
|
||||
"""
|
||||
@@ -37,11 +39,13 @@ class FormattingEngine:
|
||||
Args:
|
||||
style_presets: Custom style presets. If None, uses defaults.
|
||||
formatter_presets: Custom formatter presets. If None, uses defaults.
|
||||
rule_presets: Named rule presets (list of FormatRule dicts). If None, uses defaults.
|
||||
lookup_resolver: Function for resolving enum datagrid sources.
|
||||
"""
|
||||
self._condition_evaluator = ConditionEvaluator()
|
||||
self._style_resolver = StyleResolver(style_presets)
|
||||
self._formatter_resolver = FormatterResolver(formatter_presets, lookup_resolver)
|
||||
self._rule_presets = rule_presets if rule_presets is not None else DEFAULT_RULE_PRESETS
|
||||
|
||||
def apply_format(
|
||||
self,
|
||||
@@ -65,6 +69,9 @@ class FormattingEngine:
|
||||
if not rules:
|
||||
return None, None
|
||||
|
||||
# Expand rule preset references before evaluating
|
||||
rules = self._expand_rule_presets(rules)
|
||||
|
||||
# Find all matching rules
|
||||
matching_rules = self._get_matching_rules(rules, cell_value, row_data)
|
||||
|
||||
@@ -88,6 +95,37 @@ class FormattingEngine:
|
||||
|
||||
return style, formatted_value
|
||||
|
||||
def _expand_rule_presets(self, rules: list[FormatRule]) -> list[FormatRule]:
|
||||
"""
|
||||
Replace any FormatRule that references a rule preset with the preset's rules.
|
||||
|
||||
A rule is a rule preset reference when its formatter has a preset name
|
||||
that exists in rule_presets (and not in formatter_presets).
|
||||
|
||||
Args:
|
||||
rules: Original list of FormatRule
|
||||
|
||||
Returns:
|
||||
Expanded list with preset references replaced by their FormatRules
|
||||
"""
|
||||
expanded = []
|
||||
for rule in rules:
|
||||
preset_name = self._get_rule_preset_name(rule)
|
||||
if preset_name:
|
||||
expanded.extend(self._rule_presets[preset_name].rules)
|
||||
else:
|
||||
expanded.append(rule)
|
||||
return expanded
|
||||
|
||||
def _get_rule_preset_name(self, rule: FormatRule) -> str | None:
|
||||
"""Return the preset name if the rule's formatter references a rule preset, else None."""
|
||||
if rule.formatter is None:
|
||||
return None
|
||||
preset = getattr(rule.formatter, "preset", None)
|
||||
if preset and preset in self._rule_presets:
|
||||
return preset
|
||||
return None
|
||||
|
||||
def _get_matching_rules(
|
||||
self,
|
||||
rules: list[FormatRule],
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
from myfasthtml.core.formatting.dataclasses import (
|
||||
Condition, Style, FormatRule, RulePreset,
|
||||
NumberFormatter, )
|
||||
|
||||
# === Style Presets (DaisyUI 5) ===
|
||||
# Keys use CSS property names (with hyphens)
|
||||
|
||||
@@ -22,6 +26,56 @@ DEFAULT_STYLE_PRESETS = {
|
||||
"white": {"__class__": "mf-formatting-white", },
|
||||
}
|
||||
|
||||
# === Rule Presets ===
|
||||
# Each RulePreset = name + description + list of FormatRule objects.
|
||||
# Referenced in DSL as: format("name") or style("name")
|
||||
|
||||
DEFAULT_RULE_PRESETS: dict[str, RulePreset] = {
|
||||
"accounting": RulePreset(
|
||||
name="accounting",
|
||||
description="Negatives in parentheses, positives plain",
|
||||
rules=[
|
||||
FormatRule(
|
||||
condition=Condition(operator="<", value=0),
|
||||
formatter=NumberFormatter(precision=0, prefix="(", suffix=")",
|
||||
absolute=True, thousands_sep=" "),
|
||||
),
|
||||
FormatRule(
|
||||
condition=Condition(operator=">", value=0),
|
||||
formatter=NumberFormatter(precision=0, thousands_sep=" "),
|
||||
),
|
||||
],
|
||||
),
|
||||
"traffic_light": RulePreset(
|
||||
name="traffic_light",
|
||||
description="Red / yellow / green style based on sign",
|
||||
rules=[
|
||||
FormatRule(condition=Condition(operator="<", value=0), style=Style(preset="error")),
|
||||
FormatRule(condition=Condition(operator="==", value=0), style=Style(preset="warning")),
|
||||
FormatRule(condition=Condition(operator=">", value=0), style=Style(preset="success")),
|
||||
],
|
||||
),
|
||||
"budget_variance": RulePreset(
|
||||
name="budget_variance",
|
||||
description="% variance: negative=error, over 10%=warning, else plain",
|
||||
rules=[
|
||||
FormatRule(
|
||||
condition=Condition(operator="<", value=0),
|
||||
formatter=NumberFormatter(precision=1, suffix="%", multiplier=100),
|
||||
style=Style(preset="error"),
|
||||
),
|
||||
FormatRule(
|
||||
condition=Condition(operator=">", value=0.1),
|
||||
formatter=NumberFormatter(precision=1, suffix="%", multiplier=100),
|
||||
style=Style(preset="warning"),
|
||||
),
|
||||
FormatRule(
|
||||
formatter=NumberFormatter(precision=1, suffix="%", multiplier=100),
|
||||
),
|
||||
],
|
||||
),
|
||||
}
|
||||
|
||||
# === Formatter Presets ===
|
||||
|
||||
DEFAULT_FORMATTER_PRESETS = {
|
||||
|
||||
Reference in New Issue
Block a user