Fixed rule conflict management. Added User guide for formatting
This commit is contained in:
@@ -203,7 +203,7 @@ def generate_formatting_dsl_mode() -> Dict[str, Any]:
|
||||
{"regex": r"#.*", "token": "comment"},
|
||||
|
||||
# Scope keywords
|
||||
{"regex": r"\b(?:column|row|cell)\b", "token": "keyword"},
|
||||
{"regex": r"\b(?:column|row|cell|table|tables)\b", "token": "keyword"},
|
||||
|
||||
# Condition keywords
|
||||
{"regex": r"\b(?:if|not|and|or|in|between|case)\b", "token": "keyword"},
|
||||
|
||||
@@ -55,6 +55,7 @@ GRAMMAR = r"""
|
||||
|
||||
unary_comparison: operand "isempty" -> isempty_comp
|
||||
| operand "isnotempty" -> isnotempty_comp
|
||||
| operand "isnan" -> isnan_comp
|
||||
|
||||
case_modifier: "(" "case" ")"
|
||||
|
||||
@@ -122,6 +123,7 @@ GRAMMAR = r"""
|
||||
| "boolean" -> fmt_boolean
|
||||
| "text" -> fmt_text
|
||||
| "enum" -> fmt_enum
|
||||
| "constant" -> fmt_constant
|
||||
|
||||
// ==================== Keyword arguments ====================
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ from ..dataclasses import (
|
||||
BooleanFormatter,
|
||||
TextFormatter,
|
||||
EnumFormatter,
|
||||
ConstantFormatter,
|
||||
)
|
||||
|
||||
|
||||
@@ -96,7 +97,7 @@ class DSLTransformer(Transformer):
|
||||
if isinstance(item, Style):
|
||||
style_obj = item
|
||||
elif isinstance(item, (NumberFormatter, DateFormatter, BooleanFormatter,
|
||||
TextFormatter, EnumFormatter)):
|
||||
TextFormatter, EnumFormatter, ConstantFormatter)):
|
||||
formatter_obj = item
|
||||
elif isinstance(item, Condition):
|
||||
condition_obj = item
|
||||
@@ -166,9 +167,12 @@ class DSLTransformer(Transformer):
|
||||
|
||||
def isempty_comp(self, items):
|
||||
return Condition(operator="isempty")
|
||||
|
||||
|
||||
def isnotempty_comp(self, items):
|
||||
return Condition(operator="isnotempty")
|
||||
|
||||
def isnan_comp(self, items):
|
||||
return Condition(operator="isnan")
|
||||
|
||||
# ==================== Operators ====================
|
||||
|
||||
@@ -333,9 +337,12 @@ class DSLTransformer(Transformer):
|
||||
|
||||
def fmt_text(self, items):
|
||||
return "text"
|
||||
|
||||
|
||||
def fmt_enum(self, items):
|
||||
return "enum"
|
||||
|
||||
def fmt_constant(self, items):
|
||||
return "constant"
|
||||
|
||||
def _build_formatter(self, format_type: str, kwargs: dict):
|
||||
"""Build the appropriate Formatter subclass."""
|
||||
@@ -349,6 +356,8 @@ class DSLTransformer(Transformer):
|
||||
return TextFormatter(**self._filter_text_kwargs(kwargs))
|
||||
elif format_type == "enum":
|
||||
return EnumFormatter(**self._filter_enum_kwargs(kwargs))
|
||||
elif format_type == "constant":
|
||||
return ConstantFormatter(**self._filter_constant_kwargs(kwargs))
|
||||
else:
|
||||
raise DSLValidationError(f"Unknown formatter type: {format_type}")
|
||||
|
||||
@@ -376,7 +385,12 @@ class DSLTransformer(Transformer):
|
||||
"""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}
|
||||
|
||||
|
||||
def _filter_constant_kwargs(self, kwargs: dict) -> dict:
|
||||
"""Filter kwargs for ConstantFormatter."""
|
||||
valid_keys = {"value"}
|
||||
return {k: v for k, v in kwargs.items() if k in valid_keys}
|
||||
|
||||
# ==================== Keyword arguments ====================
|
||||
|
||||
def kwargs(self, items):
|
||||
|
||||
@@ -71,23 +71,22 @@ class FormattingEngine:
|
||||
if not matching_rules:
|
||||
return None, None
|
||||
|
||||
# Resolve conflicts to get the winning rule
|
||||
winning_rule = self._resolve_conflicts(matching_rules)
|
||||
|
||||
if winning_rule is None:
|
||||
return None, None
|
||||
# Resolve style and formatter independently
|
||||
# This allows combining style from one rule and formatter from another
|
||||
winning_style = self._resolve_style(matching_rules)
|
||||
winning_formatter = self._resolve_formatter(matching_rules)
|
||||
|
||||
# Apply style
|
||||
css_string = None
|
||||
if winning_rule.style:
|
||||
css_string = self._style_resolver.to_css_string(winning_rule.style)
|
||||
if winning_style:
|
||||
css_string = self._style_resolver.to_css_string(winning_style)
|
||||
if css_string == "":
|
||||
css_string = None
|
||||
|
||||
# Apply formatter
|
||||
formatted_value = None
|
||||
if winning_rule.formatter:
|
||||
formatted_value = self._formatter_resolver.resolve(winning_rule.formatter, cell_value)
|
||||
if winning_formatter:
|
||||
formatted_value = self._formatter_resolver.resolve(winning_formatter, cell_value)
|
||||
|
||||
return css_string, formatted_value
|
||||
|
||||
@@ -116,10 +115,89 @@ class FormattingEngine:
|
||||
|
||||
return matching
|
||||
|
||||
def _resolve_style(self, matching_rules: list[FormatRule]):
|
||||
"""
|
||||
Resolve style conflicts when multiple rules match.
|
||||
|
||||
Resolution logic:
|
||||
1. Filter to rules that have a style
|
||||
2. Specificity = 1 if rule has condition, 0 otherwise
|
||||
3. Higher specificity wins
|
||||
4. At equal specificity, last rule wins
|
||||
|
||||
Args:
|
||||
matching_rules: List of rules that matched
|
||||
|
||||
Returns:
|
||||
The winning Style, or None if no rules have style
|
||||
"""
|
||||
# Filter to rules with style
|
||||
style_rules = [rule for rule in matching_rules if rule.style is not None]
|
||||
|
||||
if not style_rules:
|
||||
return None
|
||||
|
||||
if len(style_rules) == 1:
|
||||
return style_rules[0].style
|
||||
|
||||
# Calculate specificity for each rule
|
||||
def get_specificity(rule: FormatRule) -> int:
|
||||
return 1 if rule.condition is not None else 0
|
||||
|
||||
# Find the maximum specificity
|
||||
max_specificity = max(get_specificity(rule) for rule in style_rules)
|
||||
|
||||
# Filter to rules with max specificity
|
||||
top_rules = [rule for rule in style_rules if get_specificity(rule) == max_specificity]
|
||||
|
||||
# Last rule wins among equal specificity
|
||||
return top_rules[-1].style
|
||||
|
||||
def _resolve_formatter(self, matching_rules: list[FormatRule]):
|
||||
"""
|
||||
Resolve formatter conflicts when multiple rules match.
|
||||
|
||||
Resolution logic:
|
||||
1. Filter to rules that have a formatter
|
||||
2. Specificity = 1 if rule has condition, 0 otherwise
|
||||
3. Higher specificity wins
|
||||
4. At equal specificity, last rule wins
|
||||
|
||||
Args:
|
||||
matching_rules: List of rules that matched
|
||||
|
||||
Returns:
|
||||
The winning Formatter, or None if no rules have formatter
|
||||
"""
|
||||
# Filter to rules with formatter
|
||||
formatter_rules = [rule for rule in matching_rules if rule.formatter is not None]
|
||||
|
||||
if not formatter_rules:
|
||||
return None
|
||||
|
||||
if len(formatter_rules) == 1:
|
||||
return formatter_rules[0].formatter
|
||||
|
||||
# Calculate specificity for each rule
|
||||
def get_specificity(rule: FormatRule) -> int:
|
||||
return 1 if rule.condition is not None else 0
|
||||
|
||||
# Find the maximum specificity
|
||||
max_specificity = max(get_specificity(rule) for rule in formatter_rules)
|
||||
|
||||
# Filter to rules with max specificity
|
||||
top_rules = [rule for rule in formatter_rules if get_specificity(rule) == max_specificity]
|
||||
|
||||
# Last rule wins among equal specificity
|
||||
return top_rules[-1].formatter
|
||||
|
||||
def _resolve_conflicts(self, matching_rules: list[FormatRule]) -> FormatRule | None:
|
||||
"""
|
||||
Resolve conflicts when multiple rules match.
|
||||
|
||||
DEPRECATED: This method is kept for backward compatibility but is no longer used.
|
||||
Use _resolve_style() and _resolve_formatter() instead.
|
||||
|
||||
Resolution logic:
|
||||
1. Specificity = 1 if rule has condition, 0 otherwise
|
||||
2. Higher specificity wins
|
||||
|
||||
Reference in New Issue
Block a user