I can apply format rules
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import html
|
import html
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
from dataclasses import dataclass
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@@ -21,6 +22,8 @@ from myfasthtml.controls.helpers import mk, icons
|
|||||||
from myfasthtml.core.commands import Command
|
from myfasthtml.core.commands import Command
|
||||||
from myfasthtml.core.constants import ColumnType, ROW_INDEX_ID, FooterAggregation, DATAGRID_PAGE_SIZE, FILTER_INPUT_CID
|
from myfasthtml.core.constants import ColumnType, ROW_INDEX_ID, FooterAggregation, DATAGRID_PAGE_SIZE, FILTER_INPUT_CID
|
||||||
from myfasthtml.core.dbmanager import DbObject
|
from myfasthtml.core.dbmanager import DbObject
|
||||||
|
from myfasthtml.core.formatting.dataclasses import FormatRule, Style, Condition, ConstantFormatter
|
||||||
|
from myfasthtml.core.formatting.engine import FormattingEngine
|
||||||
from myfasthtml.core.instances import MultipleInstance
|
from myfasthtml.core.instances import MultipleInstance
|
||||||
from myfasthtml.core.optimized_ft import OptimizedDiv
|
from myfasthtml.core.optimized_ft import OptimizedDiv
|
||||||
from myfasthtml.core.utils import make_safe_id
|
from myfasthtml.core.utils import make_safe_id
|
||||||
@@ -47,6 +50,12 @@ def _mk_bool_cached(_value):
|
|||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DatagridConf:
|
||||||
|
namespace: Optional[str] = None
|
||||||
|
name: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class DatagridState(DbObject):
|
class DatagridState(DbObject):
|
||||||
def __init__(self, owner, save_state):
|
def __init__(self, owner, save_state):
|
||||||
with self.initializing():
|
with self.initializing():
|
||||||
@@ -66,14 +75,17 @@ class DatagridState(DbObject):
|
|||||||
self.ne_df = None
|
self.ne_df = None
|
||||||
|
|
||||||
self.ns_fast_access = None
|
self.ns_fast_access = None
|
||||||
|
self.ns_row_data = None
|
||||||
self.ns_total_rows = None
|
self.ns_total_rows = None
|
||||||
|
|
||||||
|
|
||||||
class DatagridSettings(DbObject):
|
class DatagridSettings(DbObject):
|
||||||
def __init__(self, owner, save_state):
|
def __init__(self, owner, save_state, name, namespace):
|
||||||
with self.initializing():
|
with self.initializing():
|
||||||
super().__init__(owner, name=f"{owner.get_full_id()}#settings", save_state=save_state)
|
super().__init__(owner, name=f"{owner.get_full_id()}#settings", save_state=save_state)
|
||||||
self.save_state = save_state is True
|
self.save_state = save_state is True
|
||||||
|
self.namespace: Optional[str] = namespace
|
||||||
|
self.name: Optional[str] = name
|
||||||
self.file_name: Optional[str] = None
|
self.file_name: Optional[str] = None
|
||||||
self.selected_sheet_name: Optional[str] = None
|
self.selected_sheet_name: Optional[str] = None
|
||||||
self.header_visible: bool = True
|
self.header_visible: bool = True
|
||||||
@@ -148,10 +160,12 @@ class Commands(BaseCommands):
|
|||||||
|
|
||||||
|
|
||||||
class DataGrid(MultipleInstance):
|
class DataGrid(MultipleInstance):
|
||||||
def __init__(self, parent, settings=None, save_state=None, _id=None):
|
def __init__(self, parent, conf=None, save_state=None, _id=None):
|
||||||
super().__init__(parent, _id=_id)
|
super().__init__(parent, _id=_id)
|
||||||
self._settings = settings or DatagridSettings(self, save_state=save_state)
|
name, namespace = (conf.name, conf.namespace) if conf else ("No name", "__default__")
|
||||||
|
self._settings = DatagridSettings(self, save_state=save_state, name=name, namespace=namespace)
|
||||||
self._state = DatagridState(self, save_state=self._settings.save_state)
|
self._state = DatagridState(self, save_state=self._settings.save_state)
|
||||||
|
self._formatting_engine = FormattingEngine()
|
||||||
self.commands = Commands(self)
|
self.commands = Commands(self)
|
||||||
self.init_from_dataframe(self._state.ne_df, init_state=False) # state comes from DatagridState
|
self.init_from_dataframe(self._state.ne_df, init_state=False) # state comes from DatagridState
|
||||||
|
|
||||||
@@ -187,6 +201,8 @@ class DataGrid(MultipleInstance):
|
|||||||
"ctrl+click": {"command": self.commands.on_click(), "hx_vals": "js:getCellId()"},
|
"ctrl+click": {"command": self.commands.on_click(), "hx_vals": "js:getCellId()"},
|
||||||
"shift+click": {"command": self.commands.on_click(), "hx_vals": "js:getCellId()"},
|
"shift+click": {"command": self.commands.on_click(), "hx_vals": "js:getCellId()"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.debug(f"DataGrid '{self._get_full_name()}' with id='{self._id}' created.")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _df(self):
|
def _df(self):
|
||||||
@@ -262,6 +278,9 @@ class DataGrid(MultipleInstance):
|
|||||||
self._state.selection.selected = pos
|
self._state.selection.selected = pos
|
||||||
self._state.save()
|
self._state.save()
|
||||||
|
|
||||||
|
def _get_full_name(self):
|
||||||
|
return f"{self._settings.namespace}.{self._settings.name}" if self._settings.namespace else self._settings.name
|
||||||
|
|
||||||
def init_from_dataframe(self, df, init_state=True):
|
def init_from_dataframe(self, df, init_state=True):
|
||||||
|
|
||||||
def _get_column_type(dtype):
|
def _get_column_type(dtype):
|
||||||
@@ -310,6 +329,24 @@ class DataGrid(MultipleInstance):
|
|||||||
res[ROW_INDEX_ID] = _df.index.to_numpy()
|
res[ROW_INDEX_ID] = _df.index.to_numpy()
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
def _init_row_data(_df):
|
||||||
|
"""
|
||||||
|
Generates a list of row data dictionaries for column references in formatting conditions.
|
||||||
|
|
||||||
|
Each dict contains {col_id: value} for a single row, used by FormattingEngine
|
||||||
|
to evaluate conditions that reference other columns (e.g., {"col": "budget"}).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
_df (DataFrame): The input pandas DataFrame.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[dict]: A list where each element is a dict of column values for that row.
|
||||||
|
"""
|
||||||
|
if _df is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
return _df.to_dict(orient='records')
|
||||||
|
|
||||||
if df is not None:
|
if df is not None:
|
||||||
self._state.ne_df = df
|
self._state.ne_df = df
|
||||||
if init_state:
|
if init_state:
|
||||||
@@ -317,10 +354,55 @@ class DataGrid(MultipleInstance):
|
|||||||
self._state.rows = [DataGridRowState(row_id) for row_id in self._df.index]
|
self._state.rows = [DataGridRowState(row_id) for row_id in self._df.index]
|
||||||
self._state.columns = _init_columns(df) # use df not self._df to keep the original title
|
self._state.columns = _init_columns(df) # use df not self._df to keep the original title
|
||||||
self._state.ns_fast_access = _init_fast_access(self._df)
|
self._state.ns_fast_access = _init_fast_access(self._df)
|
||||||
|
self._state.ns_row_data = _init_row_data(self._df)
|
||||||
self._state.ns_total_rows = len(self._df) if self._df is not None else 0
|
self._state.ns_total_rows = len(self._df) if self._df is not None else 0
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def _get_format_rules(self, col_pos, row_index, col_def):
|
||||||
|
"""
|
||||||
|
Get format rules for a cell, returning only the most specific level defined.
|
||||||
|
|
||||||
|
Priority (most specific wins):
|
||||||
|
1. Cell-level: self._state.cell_formats[cell_id]
|
||||||
|
2. Row-level: row_state.format (if row has specific state)
|
||||||
|
3. Column-level: col_def.format
|
||||||
|
|
||||||
|
Args:
|
||||||
|
col_pos: Column position index
|
||||||
|
row_index: Row index
|
||||||
|
col_def: DataGridColumnState for the column
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[FormatRule] or None if no formatting defined
|
||||||
|
"""
|
||||||
|
|
||||||
|
# hack to test
|
||||||
|
if col_def.col_id == "age":
|
||||||
|
return [
|
||||||
|
FormatRule(style=Style(color="green")),
|
||||||
|
FormatRule(condition=Condition(operator=">", value=18), style=Style(color="red")),
|
||||||
|
]
|
||||||
|
|
||||||
|
return [
|
||||||
|
FormatRule(condition=Condition(operator="isnan"), formatter=ConstantFormatter(value="-")),
|
||||||
|
]
|
||||||
|
|
||||||
|
cell_id = self._get_element_id_from_pos("cell", (row_index, col_pos))
|
||||||
|
|
||||||
|
if cell_id in self._state.cell_formats:
|
||||||
|
return self._state.cell_formats[cell_id]
|
||||||
|
|
||||||
|
if row_index < len(self._state.rows):
|
||||||
|
row_state = self._state.rows[row_index]
|
||||||
|
if row_state.format:
|
||||||
|
return row_state.format
|
||||||
|
|
||||||
|
if col_def.format:
|
||||||
|
return col_def.format
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def set_column_width(self, col_id: str, width: str):
|
def set_column_width(self, col_id: str, width: str):
|
||||||
"""Update column width after resize. Called via Command from JS."""
|
"""Update column width after resize. Called via Command from JS."""
|
||||||
logger.debug(f"set_column_width: {col_id=} {width=}")
|
logger.debug(f"set_column_width: {col_id=} {width=}")
|
||||||
@@ -431,35 +513,35 @@ class DataGrid(MultipleInstance):
|
|||||||
|
|
||||||
def mk_body_cell_content(self, col_pos, row_index, col_def: DataGridColumnState, filter_keyword_lower=None):
|
def mk_body_cell_content(self, col_pos, row_index, col_def: DataGridColumnState, filter_keyword_lower=None):
|
||||||
"""
|
"""
|
||||||
OPTIMIZED: Generate cell content with minimal object creation.
|
Generate cell content with formatting and optional search highlighting.
|
||||||
- Uses plain strings instead of Label objects when possible
|
|
||||||
- Accepts pre-computed filter_keyword_lower to avoid repeated dict lookups
|
Processing order:
|
||||||
- Avoids html.escape when not necessary
|
1. Apply formatter (transforms value for display)
|
||||||
- Uses cached boolean HTML (_mk_bool_cached)
|
2. Apply style (CSS inline style)
|
||||||
|
3. Apply search highlighting (on top of formatted value)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def mk_highlighted_text(value_str, css_class):
|
def mk_highlighted_text(value_str, css_class, style=None):
|
||||||
"""Return highlighted text as raw HTML string or tuple of Spans."""
|
"""Return highlighted text as raw HTML string or tuple of Spans."""
|
||||||
|
style_attr = f' style="{style}"' if style else ''
|
||||||
|
|
||||||
if not filter_keyword_lower:
|
if not filter_keyword_lower:
|
||||||
# OPTIMIZATION: Return plain HTML string instead of Label object
|
return NotStr(f'<span class="{css_class} truncate"{style_attr}>{value_str}</span>')
|
||||||
# Include "truncate text-sm" to match mk.label() behavior (ellipsis + font size)
|
|
||||||
return NotStr(f'<span class="{css_class} truncate">{value_str}</span>')
|
|
||||||
|
|
||||||
index = value_str.lower().find(filter_keyword_lower)
|
index = value_str.lower().find(filter_keyword_lower)
|
||||||
if index < 0:
|
if index < 0:
|
||||||
return NotStr(f'<span class="{css_class} truncate">{value_str}</span>')
|
return NotStr(f'<span class="{css_class} truncate"{style_attr}>{value_str}</span>')
|
||||||
|
|
||||||
# Has highlighting - need to use Span objects
|
# Has highlighting - need to use Span objects
|
||||||
# Add "truncate text-sm" to match mk.label() behavior
|
|
||||||
len_keyword = len(filter_keyword_lower)
|
len_keyword = len(filter_keyword_lower)
|
||||||
res = []
|
res = []
|
||||||
if index > 0:
|
if index > 0:
|
||||||
res.append(Span(value_str[:index], cls=f"{css_class}"))
|
res.append(Span(value_str[:index], cls=f"{css_class}"))
|
||||||
res.append(Span(value_str[index:index + len_keyword], cls=f"dt2-highlight-1"))
|
res.append(Span(value_str[index:index + len_keyword], cls="dt2-highlight-1"))
|
||||||
if index + len_keyword < len(value_str):
|
if index + len_keyword < len(value_str):
|
||||||
res.append(Span(value_str[index + len_keyword:], cls=f"{css_class}"))
|
res.append(Span(value_str[index + len_keyword:], cls=f"{css_class}"))
|
||||||
|
|
||||||
return Span(*res, cls=f"{css_class} truncate") if len(res) > 1 else res[0]
|
return Span(*res, cls=f"{css_class} truncate", style=style) if len(res) > 1 else res[0]
|
||||||
|
|
||||||
column_type = col_def.type
|
column_type = col_def.type
|
||||||
value = self._state.ns_fast_access[col_def.col_id][row_index]
|
value = self._state.ns_fast_access[col_def.col_id][row_index]
|
||||||
@@ -472,8 +554,16 @@ class DataGrid(MultipleInstance):
|
|||||||
if column_type == ColumnType.RowIndex:
|
if column_type == ColumnType.RowIndex:
|
||||||
return NotStr(f'<span class="dt2-cell-content-number truncate">{row_index}</span>')
|
return NotStr(f'<span class="dt2-cell-content-number truncate">{row_index}</span>')
|
||||||
|
|
||||||
# Convert value to string
|
# Get format rules and apply formatting
|
||||||
value_str = str(value)
|
css_string = None
|
||||||
|
formatted_value = None
|
||||||
|
rules = self._get_format_rules(col_pos, row_index, col_def)
|
||||||
|
if rules:
|
||||||
|
row_data = self._state.ns_row_data[row_index] if row_index < len(self._state.ns_row_data) else None
|
||||||
|
css_string, formatted_value = self._formatting_engine.apply_format(rules, value, row_data)
|
||||||
|
|
||||||
|
# Use formatted value or convert to string
|
||||||
|
value_str = formatted_value if formatted_value is not None else str(value)
|
||||||
|
|
||||||
# OPTIMIZATION: Only escape if necessary (check for HTML special chars with pre-compiled regex)
|
# OPTIMIZATION: Only escape if necessary (check for HTML special chars with pre-compiled regex)
|
||||||
if _HTML_SPECIAL_CHARS_REGEX.search(value_str):
|
if _HTML_SPECIAL_CHARS_REGEX.search(value_str):
|
||||||
@@ -481,9 +571,9 @@ class DataGrid(MultipleInstance):
|
|||||||
|
|
||||||
# Number or Text type
|
# Number or Text type
|
||||||
if column_type == ColumnType.Number:
|
if column_type == ColumnType.Number:
|
||||||
return mk_highlighted_text(value_str, "dt2-cell-content-number")
|
return mk_highlighted_text(value_str, "dt2-cell-content-number", css_string)
|
||||||
else:
|
else:
|
||||||
return mk_highlighted_text(value_str, "dt2-cell-content-text")
|
return mk_highlighted_text(value_str, "dt2-cell-content-text", css_string)
|
||||||
|
|
||||||
def mk_body_cell(self, col_pos, row_index, col_def: DataGridColumnState, filter_keyword_lower=None):
|
def mk_body_cell(self, col_pos, row_index, col_def: DataGridColumnState, filter_keyword_lower=None):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import pandas as pd
|
|||||||
from fasthtml.components import Div
|
from fasthtml.components import Div
|
||||||
|
|
||||||
from myfasthtml.controls.BaseCommands import BaseCommands
|
from myfasthtml.controls.BaseCommands import BaseCommands
|
||||||
from myfasthtml.controls.DataGrid import DataGrid
|
from myfasthtml.controls.DataGrid import DataGrid, DatagridConf
|
||||||
from myfasthtml.controls.FileUpload import FileUpload
|
from myfasthtml.controls.FileUpload import FileUpload
|
||||||
from myfasthtml.controls.TabsManager import TabsManager
|
from myfasthtml.controls.TabsManager import TabsManager
|
||||||
from myfasthtml.controls.TreeView import TreeView, TreeNode
|
from myfasthtml.controls.TreeView import TreeView, TreeNode
|
||||||
@@ -97,12 +97,15 @@ class DataGridsManager(MultipleInstance):
|
|||||||
def open_from_excel(self, tab_id, file_upload: FileUpload):
|
def open_from_excel(self, tab_id, file_upload: FileUpload):
|
||||||
excel_content = file_upload.get_content()
|
excel_content = file_upload.get_content()
|
||||||
df = pd.read_excel(BytesIO(excel_content), file_upload.get_sheet_name())
|
df = pd.read_excel(BytesIO(excel_content), file_upload.get_sheet_name())
|
||||||
dg = DataGrid(self._tabs_manager, save_state=True) # first time the Datagrid is created
|
namespace = file_upload.get_file_basename()
|
||||||
|
name = file_upload.get_sheet_name()
|
||||||
|
dg_conf = DatagridConf(namespace=namespace, name=name)
|
||||||
|
dg = DataGrid(self._tabs_manager, conf=dg_conf, save_state=True) # first time the Datagrid is created
|
||||||
dg.init_from_dataframe(df)
|
dg.init_from_dataframe(df)
|
||||||
document = DocumentDefinition(
|
document = DocumentDefinition(
|
||||||
document_id=str(uuid.uuid4()),
|
document_id=str(uuid.uuid4()),
|
||||||
namespace=file_upload.get_file_basename(),
|
namespace=namespace,
|
||||||
name=file_upload.get_sheet_name(),
|
name=name,
|
||||||
type="excel",
|
type="excel",
|
||||||
tab_id=tab_id,
|
tab_id=tab_id,
|
||||||
datagrid_id=dg.get_id()
|
datagrid_id=dg.get_id()
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from numbers import Number
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from myfasthtml.core.formatting.dataclasses import Condition
|
from myfasthtml.core.formatting.dataclasses import Condition
|
||||||
@@ -32,6 +33,10 @@ class ConditionEvaluator:
|
|||||||
if condition.operator == "isnotempty":
|
if condition.operator == "isnotempty":
|
||||||
result = not self._is_empty(cell_value)
|
result = not self._is_empty(cell_value)
|
||||||
return self._apply_negate(result, condition.negate)
|
return self._apply_negate(result, condition.negate)
|
||||||
|
|
||||||
|
if condition.operator == "isnan":
|
||||||
|
result = self._is_nan(cell_value)
|
||||||
|
return self._apply_negate(result, condition.negate)
|
||||||
|
|
||||||
# For all other operators, None cell_value returns False
|
# For all other operators, None cell_value returns False
|
||||||
if cell_value is None:
|
if cell_value is None:
|
||||||
@@ -70,6 +75,10 @@ class ConditionEvaluator:
|
|||||||
if isinstance(value, str) and value == "":
|
if isinstance(value, str) and value == "":
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _is_nan(self, value: Any) -> bool:
|
||||||
|
"""Check if a value is NaN."""
|
||||||
|
return isinstance(value, float) and value != value
|
||||||
|
|
||||||
def _apply_negate(self, result: bool, negate: bool) -> bool:
|
def _apply_negate(self, result: bool, negate: bool) -> bool:
|
||||||
"""Apply negation if needed."""
|
"""Apply negation if needed."""
|
||||||
@@ -124,7 +133,7 @@ class ConditionEvaluator:
|
|||||||
"""Check if cell_value < compare_value."""
|
"""Check if cell_value < compare_value."""
|
||||||
if type(cell_value) != type(compare_value):
|
if type(cell_value) != type(compare_value):
|
||||||
# Allow int/float comparison
|
# Allow int/float comparison
|
||||||
if isinstance(cell_value, (int, float)) and isinstance(compare_value, (int, float)):
|
if isinstance(cell_value, Number) and isinstance(compare_value, Number):
|
||||||
return cell_value < compare_value
|
return cell_value < compare_value
|
||||||
raise TypeError("Type mismatch")
|
raise TypeError("Type mismatch")
|
||||||
return cell_value < compare_value
|
return cell_value < compare_value
|
||||||
@@ -132,7 +141,7 @@ class ConditionEvaluator:
|
|||||||
def _less_than_or_equal(self, cell_value: Any, compare_value: Any) -> bool:
|
def _less_than_or_equal(self, cell_value: Any, compare_value: Any) -> bool:
|
||||||
"""Check if cell_value <= compare_value."""
|
"""Check if cell_value <= compare_value."""
|
||||||
if type(cell_value) != type(compare_value):
|
if type(cell_value) != type(compare_value):
|
||||||
if isinstance(cell_value, (int, float)) and isinstance(compare_value, (int, float)):
|
if isinstance(cell_value, Number) and isinstance(compare_value, Number):
|
||||||
return cell_value <= compare_value
|
return cell_value <= compare_value
|
||||||
raise TypeError("Type mismatch")
|
raise TypeError("Type mismatch")
|
||||||
return cell_value <= compare_value
|
return cell_value <= compare_value
|
||||||
@@ -140,7 +149,7 @@ class ConditionEvaluator:
|
|||||||
def _greater_than(self, cell_value: Any, compare_value: Any) -> bool:
|
def _greater_than(self, cell_value: Any, compare_value: Any) -> bool:
|
||||||
"""Check if cell_value > compare_value."""
|
"""Check if cell_value > compare_value."""
|
||||||
if type(cell_value) != type(compare_value):
|
if type(cell_value) != type(compare_value):
|
||||||
if isinstance(cell_value, (int, float)) and isinstance(compare_value, (int, float)):
|
if isinstance(cell_value, Number) and isinstance(compare_value, Number):
|
||||||
return cell_value > compare_value
|
return cell_value > compare_value
|
||||||
raise TypeError("Type mismatch")
|
raise TypeError("Type mismatch")
|
||||||
return cell_value > compare_value
|
return cell_value > compare_value
|
||||||
@@ -148,7 +157,7 @@ class ConditionEvaluator:
|
|||||||
def _greater_than_or_equal(self, cell_value: Any, compare_value: Any) -> bool:
|
def _greater_than_or_equal(self, cell_value: Any, compare_value: Any) -> bool:
|
||||||
"""Check if cell_value >= compare_value."""
|
"""Check if cell_value >= compare_value."""
|
||||||
if type(cell_value) != type(compare_value):
|
if type(cell_value) != type(compare_value):
|
||||||
if isinstance(cell_value, (int, float)) and isinstance(compare_value, (int, float)):
|
if isinstance(cell_value, Number) and isinstance(compare_value, Number):
|
||||||
return cell_value >= compare_value
|
return cell_value >= compare_value
|
||||||
raise TypeError("Type mismatch")
|
raise TypeError("Type mismatch")
|
||||||
return cell_value >= compare_value
|
return cell_value >= compare_value
|
||||||
|
|||||||
@@ -121,6 +121,9 @@ class TextFormatter(Formatter):
|
|||||||
max_length: int = None
|
max_length: int = None
|
||||||
ellipsis: str = "..."
|
ellipsis: str = "..."
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ConstantFormatter(Formatter):
|
||||||
|
value: str = None
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class EnumFormatter(Formatter):
|
class EnumFormatter(Formatter):
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from myfasthtml.core.formatting.dataclasses import (
|
|||||||
BooleanFormatter,
|
BooleanFormatter,
|
||||||
TextFormatter,
|
TextFormatter,
|
||||||
EnumFormatter,
|
EnumFormatter,
|
||||||
|
ConstantFormatter,
|
||||||
)
|
)
|
||||||
from myfasthtml.core.formatting.presets import DEFAULT_FORMATTER_PRESETS
|
from myfasthtml.core.formatting.presets import DEFAULT_FORMATTER_PRESETS
|
||||||
|
|
||||||
@@ -258,6 +259,15 @@ class EnumFormatterResolver(BaseFormatterResolver):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ConstantFormatterResolver(BaseFormatterResolver):
|
||||||
|
|
||||||
|
def resolve(self, formatter: ConstantFormatter, value: Any) -> str:
|
||||||
|
return formatter.value
|
||||||
|
|
||||||
|
def apply_preset(self, formatter: Formatter, presets: dict) -> Formatter:
|
||||||
|
return formatter
|
||||||
|
|
||||||
|
|
||||||
class FormatterResolver:
|
class FormatterResolver:
|
||||||
"""
|
"""
|
||||||
Main resolver that dispatches to the appropriate formatter resolver.
|
Main resolver that dispatches to the appropriate formatter resolver.
|
||||||
@@ -281,6 +291,7 @@ class FormatterResolver:
|
|||||||
BooleanFormatter: BooleanFormatterResolver(),
|
BooleanFormatter: BooleanFormatterResolver(),
|
||||||
TextFormatter: TextFormatterResolver(),
|
TextFormatter: TextFormatterResolver(),
|
||||||
EnumFormatter: EnumFormatterResolver(lookup_resolver),
|
EnumFormatter: EnumFormatterResolver(lookup_resolver),
|
||||||
|
ConstantFormatter: ConstantFormatterResolver()
|
||||||
}
|
}
|
||||||
|
|
||||||
def resolve(self, formatter: Formatter, value: Any) -> str:
|
def resolve(self, formatter: Formatter, value: Any) -> str:
|
||||||
|
|||||||
Reference in New Issue
Block a user