From 85f5d872c87a0cc9b51d199ea5b19020a9e89203 Mon Sep 17 00:00:00 2001 From: Kodjo Sossouvi Date: Sun, 8 Feb 2026 11:15:02 +0100 Subject: [PATCH] Removed deprecated doc --- docs/DataGrid Formatting DSL.md | 1556 ------------------------------- docs/DataGrid Formatting.md | 551 ----------- 2 files changed, 2107 deletions(-) delete mode 100644 docs/DataGrid Formatting DSL.md delete mode 100644 docs/DataGrid Formatting.md diff --git a/docs/DataGrid Formatting DSL.md b/docs/DataGrid Formatting DSL.md deleted file mode 100644 index 629459d..0000000 --- a/docs/DataGrid Formatting DSL.md +++ /dev/null @@ -1,1556 +0,0 @@ -# DataGrid Formatting DSL - -## Introduction - -This document describes the Domain Specific Language (DSL) for defining formatting rules in the DataGrid component. - -### Purpose - -The DSL provides a concise, readable way to define conditional formatting rules for cells, rows, and columns. It allows users to: - -- Apply styles (colors, fonts, decorations) based on cell values -- Format values for display (currencies, dates, percentages) -- Reference other cells for cross-column/row comparisons - -### Two Modes - -The formatting system offers two interfaces: - -| Mode | Target Users | Description | -|------|--------------|-------------| -| **Advanced (DSL)** | Developers, power users | Text-based rules with Python-like syntax | -| **Basic (GUI)** | End users | Visual rule builder (80/20 coverage) | - -This document focuses on the **Advanced DSL mode**, which covers 100% of formatting capabilities. - ---- - -## Complete Syntax - -### Overview - -A DSL document consists of one or more **scopes**, each containing one or more **rules**: - -```python -: - - - ... - -: - - ... -``` - -Rules are indented (Python-style) under their scope. - -### Scopes - -Scopes define which cells a rule applies to: - -| Scope | Syntax | Applies To | Specificity | -|-------|--------|------------|-------------| -| **Cell (coordinates)** | `cell (, ):` | Single cell by position | Highest (1) | -| **Cell (ID)** | `cell :` | Single cell by ID | Highest (1) | -| **Row** | `row :` | All cells in the row | High (2) | -| **Column** | `column :` | All cells in the column | Medium (3) | -| **Table** | `table "":` | All cells in a specific table | Low (4) | -| **Tables** | `tables:` | All cells in all tables (global) | Lowest (5) | - -**Column scope:** - -```python -# Simple column name -column amount: - style("error") if value < 0 - -# Column name with spaces (quoted) -column "total amount": - format("EUR") - -# Both syntaxes are valid -column status: -column "status": -``` - -**Row scope:** - -```python -# Row by index (0-based) -row 0: - style("neutral", bold=True) - -row 5: - style("highlight") -``` - -**Cell scope:** - -```python -# By coordinates (column, row) -cell (amount, 3): - style("highlight") - -cell ("total amount", 0): - style("neutral", bold=True) - -# By cell ID -cell tcell_grid1-3-2: - style(background_color="yellow") -``` - -**Table scope:** - -```python -# Table by name (must match DataGrid _settings.name) -table "products": - style("neutral") - format.number(precision=2) -``` - -**Tables scope (global):** - -```python -# All tables in the application -tables: - style(color="#333") - format.date(format="%Y-%m-%d") -``` - -### Rules - -A rule consists of optional **style**, optional **format**, and optional **condition**: - -``` -[style(...)] [format(...)] [if ] -``` - -At least one of `style()` or `format()` must be present. - -**Examples:** - -```python -# Style only -style("error") - -# Format only -format("EUR") - -# Style + Format -style("error") format("EUR") - -# With condition -style("error") if value < 0 - -# All combined -style("error") format("EUR") if value < 0 -``` - -### Style - -The `style()` function applies visual formatting to cells. - -**Syntax:** - -```python -style() -style(, ) -style() -``` - -**Parameters:** - -| Parameter | Type | Description | -|-----------|------|-------------| -| `preset` | string (positional) | Preset name (optional) | -| `background_color` | string | Background color | -| `color` | string | Text color | -| `bold` | boolean | Bold text | -| `italic` | boolean | Italic text | -| `underline` | boolean | Underlined text | -| `strikethrough` | boolean | Strikethrough text | -| `font_size` | string | Font size (e.g., "12px", "0.9em") | - -**Available presets (DaisyUI 5):** - -| Preset | Background | Text | -|--------|------------|------| -| `primary` | `var(--color-primary)` | `var(--color-primary-content)` | -| `secondary` | `var(--color-secondary)` | `var(--color-secondary-content)` | -| `accent` | `var(--color-accent)` | `var(--color-accent-content)` | -| `neutral` | `var(--color-neutral)` | `var(--color-neutral-content)` | -| `info` | `var(--color-info)` | `var(--color-info-content)` | -| `success` | `var(--color-success)` | `var(--color-success-content)` | -| `warning` | `var(--color-warning)` | `var(--color-warning-content)` | -| `error` | `var(--color-error)` | `var(--color-error-content)` | - -**Examples:** - -```python -# Preset only -style("error") - -# Preset with overrides -style("error", bold=True) -style("success", italic=True, underline=True) - -# No preset, direct properties -style(color="red", bold=True) -style(background_color="#ffeeee", color="#cc0000") -``` - -### Format - -The `format()` function transforms cell values for display. - -**Syntax:** - -```python -format() -format(, ) -format.() -``` - -**Using presets:** - -```python -# Preset only -format("EUR") - -# Preset with overrides -format("EUR", precision=3) -format("percentage", precision=0) -``` - -**Available presets:** - -| Preset | Type | Description | -|--------|------|-------------| -| `EUR` | number | Euro currency (1 234,56 €) | -| `USD` | number | US Dollar ($1,234.56) | -| `percentage` | number | Percentage (×100, adds %) | -| `short_date` | date | DD/MM/YYYY | -| `iso_date` | date | YYYY-MM-DD | -| `yes_no` | boolean | Yes/No | - -**Using explicit types:** - -When not using a preset, specify the type explicitly: - -```python -format.number(precision=2, suffix=" €", thousands_sep=" ") -format.date(format="%d/%m/%Y") -format.boolean(true_value="Oui", false_value="Non") -format.text(max_length=50, ellipsis="...") -format.enum(source={"draft": "Draft", "published": "Published"}) -``` - -**Type-specific parameters:** - -**`format.number`:** - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `prefix` | string | `""` | Text before value | -| `suffix` | string | `""` | Text after value | -| `thousands_sep` | string | `""` | Thousands separator | -| `decimal_sep` | string | `"."` | Decimal separator | -| `precision` | int | `0` | Decimal places | -| `multiplier` | number | `1` | Multiply before display | - -**`format.date`:** - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `format` | string | `"%Y-%m-%d"` | strftime pattern | - -**`format.boolean`:** - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `true_value` | string | `"true"` | Display for true | -| `false_value` | string | `"false"` | Display for false | -| `null_value` | string | `""` | Display for null | - -**`format.text`:** - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `transform` | string | - | `"uppercase"`, `"lowercase"`, `"capitalize"` | -| `max_length` | int | - | Truncate if exceeded | -| `ellipsis` | string | `"..."` | Suffix when truncated | - -**`format.enum`:** - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `source` | object | - | Mapping or datagrid reference | -| `default` | string | `""` | Label for unknown values | - -### Conditions - -Conditions determine when a rule applies. - -**Syntax:** - -```python -if -if -``` - -**Operators:** - -| Operator | Description | Example | -|----------|-------------|---------| -| `==` | Equal | `value == 0` | -| `!=` | Not equal | `value != ""` | -| `<` | Less than | `value < 0` | -| `<=` | Less or equal | `value <= 100` | -| `>` | Greater than | `value > 1000` | -| `>=` | Greater or equal | `value >= 0` | -| `contains` | String contains | `value contains "error"` | -| `startswith` | String starts with | `value startswith "ERR"` | -| `endswith` | String ends with | `value endswith ".pdf"` | -| `in` | Value in list | `value in ["A", "B", "C"]` | -| `between` | Value in range | `value between 0 and 100` | -| `isempty` | Is null/empty | `value isempty` | -| `isnotempty` | Is not null/empty | `value isnotempty` | - -**Negation:** - -Use `not` to negate any condition: - -```python -style("error") if not value in ["valid", "approved"] -style("warning") if not value contains "OK" -``` - -**Case sensitivity:** - -String comparisons are case-insensitive by default. Use `(case)` modifier for case-sensitive: - -```python -style("error") if value == "Error" (case) -style("warning") if value contains "WARN" (case) -``` - -### References - -References allow comparing with values from other cells. - -**Syntax:** - -| Reference | Description | Example | -|-----------|-------------|---------| -| `value` | Current cell value | `value < 0` | -| `col.` | Value from another column (same row) | `value > col.budget` | -| `col.""` | Column with spaces | `value > col."max amount"` | -| `row.` | Value from another row (same column) | `value != row.0` | -| `cell.-` | Specific cell by coordinates | `value == cell.status-0` | - -**Examples:** - -```python -# Compare with another column -column amount: - style("error") if value > col.budget - style("warning") if value > col.budget * 0.9 - -# Compare with header row -column total: - style("highlight") if value == row.0 - -# Compare with specific cell -column status: - style("success") if value == cell.status-0 -``` - ---- - -## Formal Grammar (EBNF) - -```ebnf -// Top-level structure -program : scope+ - -// Scopes -scope : scope_header NEWLINE INDENT rule+ DEDENT -scope_header : column_scope | row_scope | cell_scope | table_scope | tables_scope -column_scope : "column" column_name ":" -row_scope : "row" INTEGER ":" -cell_scope : "cell" cell_ref ":" -table_scope : "table" QUOTED_STRING ":" -tables_scope : "tables" ":" -column_name : NAME | QUOTED_STRING -cell_ref : "(" column_name "," INTEGER ")" | CELL_ID - -// Rules -rule : (style_expr format_expr? | format_expr style_expr?) condition? NEWLINE -condition : "if" comparison - -// Comparisons -comparison : "not"? (binary_comp | unary_comp) case_modifier? -binary_comp : operand operator operand - | operand "in" list - | operand "between" operand "and" operand -unary_comp : operand ("isempty" | "isnotempty") -case_modifier : "(" "case" ")" - -// Operators -operator : "==" | "!=" | "<" | "<=" | ">" | ">=" - | "contains" | "startswith" | "endswith" - -// Operands -operand : value_ref | column_ref | row_ref | cell_ref_expr | literal | arithmetic -value_ref : "value" -column_ref : "col." (NAME | QUOTED_STRING) -row_ref : "row." INTEGER -cell_ref_expr : "cell." NAME "-" INTEGER -literal : STRING | NUMBER | BOOLEAN -arithmetic : operand ("*" | "/" | "+" | "-") operand -list : "[" (literal ("," literal)*)? "]" - -// Style expression -style_expr : "style" "(" style_args ")" -style_args : (QUOTED_STRING ("," style_kwargs)?) | style_kwargs -style_kwargs : style_kwarg ("," style_kwarg)* -style_kwarg : NAME "=" (QUOTED_STRING | BOOLEAN | NUMBER) - -// Format expression -format_expr : format_preset | format_typed -format_preset : "format" "(" QUOTED_STRING ("," format_kwargs)? ")" -format_typed : "format" "." FORMAT_TYPE "(" format_kwargs? ")" -format_kwargs : format_kwarg ("," format_kwarg)* -format_kwarg : NAME "=" (QUOTED_STRING | BOOLEAN | NUMBER | dict) -dict : "{" (dict_entry ("," dict_entry)*)? "}" -dict_entry : QUOTED_STRING ":" QUOTED_STRING - -// Tokens -FORMAT_TYPE : "number" | "date" | "boolean" | "text" | "enum" -NAME : /[a-zA-Z_][a-zA-Z0-9_]*/ -QUOTED_STRING : /"[^"]*"/ | /'[^']*'/ -INTEGER : /[0-9]+/ -NUMBER : /[0-9]+(\.[0-9]+)?/ -BOOLEAN : "True" | "False" | "true" | "false" -CELL_ID : /tcell_[a-zA-Z0-9_-]+/ -NEWLINE : /\n/ -INDENT : /^[ \t]+/ -DEDENT : // decrease in indentation -``` - ---- - -## Examples - -### Basic Examples - -**Highlight negative values:** - -```python -column amount: - style("error") if value < 0 -``` - -**Format as currency:** - -```python -column price: - format("EUR") -``` - -**Conditional formatting with multiple rules:** - -```python -column status: - style("success") if value == "approved" - style("warning") if value == "pending" - style("error") if value == "rejected" -``` - -**Style header row:** - -```python -row 0: - style("neutral", bold=True) -``` - -**Apply default style to all cells in a table:** - -```python -table "products": - style("neutral") - format.number(precision=2) -``` - -**Apply global styles to all tables:** - -```python -tables: - style(font_size="14px") -``` - -### Advanced Examples - -**Compare with another column:** - -```python -column actual: - style("error") if value > col.budget - style("warning") if value > col.budget * 0.8 - style("success") if value <= col.budget * 0.8 -``` - -**Multiple formatting on same column:** - -```python -column amount: - format("EUR") - style("error") if value < 0 - style("success", bold=True) if value > 10000 -``` - -**Complex conditions:** - -```python -column score: - style("error") if value between 0 and 30 - style("warning") if value between 31 and 70 - style("success") if value between 71 and 100 - -column category: - style("primary") if value in ["A", "B", "C"] - style("secondary") if not value in ["A", "B", "C"] - -column name: - style("info") if value startswith "VIP" - style("neutral") if value isempty -``` - -**Enum formatting with display mapping:** - -```python -column status: - format.enum(source={"draft": "Brouillon", "pending": "En attente", "approved": "Approuvé"}, default="Inconnu") -``` - -**Complete example - Financial report with hierarchy:** - -```python -# Global styling for all tables -tables: - style(font_size="14px", color="#333") - -# Table-specific defaults -table "financial_report": - format.number(precision=2) - -# Header styling -row 0: - style("neutral", bold=True) - -# Amount column -column amount: - format.number(precision=2, suffix=" €", thousands_sep=" ") - style("error") if value < 0 - style("success") if value > col.target - -# Percentage column -column progress: - format("percentage") - style("error") if value < 0.5 - style("warning") if value between 0.5 and 0.8 - style("success") if value > 0.8 - -# Status column -column status: - format.enum(source={"draft": "Draft", "review": "In Review", "approved": "Approved", "rejected": "Rejected"}) - style("neutral") if value == "draft" - style("info") if value == "review" - style("success") if value == "approved" - style("error") if value == "rejected" - -# Date column -column created_at: - format.date(format="%d %b %Y") - -# Highlight specific cell -cell (amount, 10): - style("accent", bold=True) -``` - -**Note on hierarchy:** In the example above, for the cell `(amount, 10)`, the styles are applied in this order: -1. Cell-specific rule wins (accent, bold) -2. If no cell rule, column rules apply (amount formatting + conditional styles) -3. If no column rule, row rules apply (header bold) -4. If no row rule, table rules apply (financial_report precision) -5. If no table rule, global rules apply (tables font size and color) - ---- - -## Autocompletion - -The DSL editor provides context-aware autocompletion to help users write rules efficiently. - -### How It Works - -``` -┌─────────────────────────────────────────────────────────────┐ -│ CodeMirror Editor │ -│ │ -│ User types: style("err| │ -│ ▲ │ -│ │ cursor position │ -└────────────────────────┼────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ Autocompletion Request (HTMX) │ -│ { "text": "style(\"err", "cursor": 11, "context": "..." } │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ Python Backend │ -│ │ -│ 1. Parse partial input │ -│ 2. Determine context (inside style(), first arg) │ -│ 3. Filter matching presets: ["error"] │ -│ 4. Return suggestions │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ Suggestions Dropdown │ -│ ┌─────────────────────────────────────────────────────┐ │ -│ │ error - Red background for errors │ │ -│ └─────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────┘ -``` - -### Completion Contexts - -| Context | Trigger | Suggestions | -|---------|---------|-------------| -| **Scope keyword** | Start of line | `column`, `row`, `cell`, `table`, `tables` | -| **Column name** | After `column ` | Column names from DataGrid | -| **Table name** | After `table ` | Table name from current DataGrid (_settings.name) | -| **Style preset** | Inside `style("` | Style presets | -| **Style parameter** | Inside `style(... , ` | `bold`, `italic`, `color`, etc. | -| **Format preset** | Inside `format("` | Format presets | -| **Format type** | After `format.` | `number`, `date`, `boolean`, `text`, `enum` | -| **Format parameter** | Inside `format.number(` | Type-specific params | -| **Operator** | After operand | `==`, `<`, `contains`, etc. | -| **Column reference** | After `col.` | Column names | -| **Keyword** | After condition | `if`, `not`, `and`, `or` | - -### Example Completion Flow - -``` -User types │ Suggestions -───────────────┼──────────────────────────────────── -col │ column -column │ [column names from grid] -column amount: │ (new line, indent) - st │ style - style( │ "error", "warning", "success", ... - style("e │ "error" - style("err │ "error" (filtered) - style("error", │ bold=, italic=, color=, ... - style("error", b │ bold= - style("error", bold=│ True, False - style("error", bold=True) │ format(, if - style("error", bold=True) if │ value, col., row., not - style("error", bold=True) if value │ ==, !=, <, >, in, ... - style("error", bold=True) if value < │ [number input] - -tab │ table, tables -table │ [table name from current grid] -table "products": │ (new line, indent) - -tables │ tables -tables: │ (new line, indent) -``` - ---- - -## CodeMirror Integration - -### DaisyUI Theme - -The editor uses DaisyUI CSS variables for consistent theming: - -```javascript -import { EditorView } from '@codemirror/view' -import { HighlightStyle, syntaxHighlighting } from '@codemirror/language' -import { tags } from '@lezer/highlight' - -// Editor theme (container, cursor, selection) -const daisyEditorTheme = EditorView.theme({ - '&': { - backgroundColor: 'var(--color-base-100)', - color: 'var(--color-base-content)', - fontSize: '14px', - }, - '.cm-content': { - fontFamily: 'var(--font-mono, ui-monospace, monospace)', - padding: '8px 0', - }, - '.cm-line': { - padding: '0 8px', - }, - '.cm-cursor': { - borderLeftColor: 'var(--color-primary)', - borderLeftWidth: '2px', - }, - '.cm-selectionBackground': { - backgroundColor: 'color-mix(in srgb, var(--color-primary) 20%, transparent)', - }, - '&.cm-focused .cm-selectionBackground': { - backgroundColor: 'color-mix(in srgb, var(--color-primary) 30%, transparent)', - }, - '.cm-gutters': { - backgroundColor: 'var(--color-base-200)', - color: 'var(--color-base-content)', - opacity: '0.5', - border: 'none', - }, - '.cm-activeLineGutter': { - backgroundColor: 'var(--color-base-300)', - }, - '.cm-activeLine': { - backgroundColor: 'color-mix(in srgb, var(--color-base-content) 5%, transparent)', - }, -}) - -// Syntax highlighting -const daisyHighlightStyle = HighlightStyle.define([ - // Keywords: column, row, cell, if, not, and, or - { tag: tags.keyword, color: 'var(--color-primary)', fontWeight: 'bold' }, - - // Functions: style, format - { tag: tags.function(tags.variableName), color: 'var(--color-secondary)' }, - - // Strings: "error", "EUR", "approved" - { tag: tags.string, color: 'var(--color-success)' }, - - // Numbers: 0, 100, 3.14 - { tag: tags.number, color: 'var(--color-accent)' }, - - // Operators: ==, <, >, contains, in - { tag: tags.operator, color: 'var(--color-warning)' }, - { tag: tags.compareOperator, color: 'var(--color-warning)' }, - - // Booleans: True, False - { tag: tags.bool, color: 'var(--color-info)' }, - - // Property names: bold=, precision= - { tag: tags.propertyName, color: 'var(--color-base-content)', opacity: '0.8' }, - - // Comments (if supported later) - { tag: tags.comment, color: 'var(--color-base-content)', opacity: '0.5', fontStyle: 'italic' }, - - // Invalid/errors - { tag: tags.invalid, color: 'var(--color-error)', textDecoration: 'underline wavy' }, -]) - -// Combined extension -export const daisyTheme = [ - daisyEditorTheme, - syntaxHighlighting(daisyHighlightStyle), -] -``` - -### CodeMirror Simple Mode (Generated from lark) - -The lark grammar terminals are extracted and converted to CodeMirror 5 Simple Mode format for syntax highlighting: - -```javascript -// Generated from lark grammar terminals -CodeMirror.defineSimpleMode("formatting-dsl", { - start: [ - // Comments - {regex: /#.*/, token: "comment"}, - - // Keywords - {regex: /\b(?:column|row|cell|if|not|and|or|in|between|case)\b/, token: "keyword"}, - - // Built-in functions - {regex: /\b(?:style|format)\b/, token: "builtin"}, - - // Operators - {regex: /\b(?:contains|startswith|endswith|isempty|isnotempty)\b/, token: "operator"}, - {regex: /==|!=|<=|>=|<|>/, token: "operator"}, - - // References - {regex: /\b(?:value|col)\b/, token: "variable-2"}, - - // Booleans - {regex: /\b(?:True|False|true|false)\b/, token: "atom"}, - - // Numbers - {regex: /\b\d+(?:\.\d+)?\b/, token: "number"}, - - // Strings - {regex: /"(?:[^\\]|\\.)*?"/, token: "string"}, - {regex: /'(?:[^\\]|\\.)*?'/, token: "string"}, - - // Cell IDs - {regex: /\btcell_[a-zA-Z0-9_-]+\b/, token: "variable-3"}, - ] -}); -``` - -**Token classes**: -- `comment` - Comments (`#`) -- `keyword` - Keywords (`column`, `row`, `cell`, `if`, `not`, `and`, `or`, `in`, `between`, `case`) -- `builtin` - Built-in functions (`style`, `format`) -- `operator` - Comparison/string operators (`==`, `<`, `contains`, etc.) -- `variable-2` - Special variables (`value`, `col`) -- `atom` - Literals (`True`, `False`) -- `number` - Numeric literals -- `string` - String literals -- `variable-3` - Cell IDs (`tcell_*`) - -These classes are styled via CSS using DaisyUI color variables for automatic theme support. - -### Editor Setup (CodeMirror 5) - -```javascript -function initDslEditor(config) { - const editor = CodeMirror(container, { - value: initialValue, - mode: "formatting-dsl", // Use generated Simple Mode - lineNumbers: true, - extraKeys: { - "Ctrl-Space": "autocomplete" - }, - hintOptions: { - hint: dslHint, // Server-side completions - completeSingle: false - }, - gutters: ["CodeMirror-linenumbers", "CodeMirror-lint-markers"], - lint: { - getAnnotations: dslLint, // Server-side validation - async: true - } - }); - - return editor; -} -``` - -**Note**: The Simple Mode provides instant syntax highlighting (approximative, lexer-level). Server-side validation via `/myfasthtml/validations` provides accurate error reporting. - ---- - -## CodeMirror Integration (Deprecated - CodeMirror 6) - -The following sections describe the original approach using CodeMirror 6 + Lezer, which was abandoned due to bundler requirements incompatible with FastHTML. - -### ~~Lezer Grammar (Translated from lark)~~ (Deprecated) - -~~The lark grammar is translated to Lezer format for client-side parsing:~~ - -```javascript -// DEPRECATED: CodeMirror 6 + Lezer approach (requires bundler) -// formatrules.grammar (Lezer) - -@top Program { scope+ } - -@skip { space | newline } - -scope { - scopeHeader ":" indent rule+ dedent -} - -scopeHeader { - ColumnScope | RowScope | CellScope -} - -ColumnScope { kw<"column"> columnName } -RowScope { kw<"row"> Integer } -CellScope { kw<"cell"> cellRef } - -columnName { Name | QuotedString } -cellRef { "(" columnName "," Integer ")" | CellId } - -rule { - (styleExpr formatExpr? | formatExpr styleExpr?) condition? -} - -condition { kw<"if"> comparison } - -comparison { - not? (binaryComp | unaryComp) caseModifier? -} - -not { kw<"not"> } - -binaryComp { - operand compareOp operand | - operand kw<"in"> list | - operand kw<"between"> operand kw<"and"> operand -} - -unaryComp { - operand (kw<"isempty"> | kw<"isnotempty">) -} - -caseModifier { "(" kw<"case"> ")" } - -compareOp { "==" | "!=" | "<" | "<=" | ">" | ">=" | kw<"contains"> | kw<"startswith"> | kw<"endswith"> } - -operand { - ValueRef | ColumnRef | RowRef | CellRefExpr | literal | ArithmeticExpr -} - -ValueRef { kw<"value"> } -ColumnRef { kw<"col"> "." (Name | QuotedString) } -RowRef { kw<"row"> "." Integer } -CellRefExpr { kw<"cell"> "." Name "-" Integer } - -literal { QuotedString | Number | Boolean } - -ArithmeticExpr { operand !arith arithmeticOp operand } -arithmeticOp { "*" | "/" | "+" | "-" } - -list { "[" (literal ("," literal)*)? "]" } - -styleExpr { kw<"style"> "(" styleArgs ")" } -styleArgs { (QuotedString ("," styleKwargs)?) | styleKwargs } -styleKwargs { styleKwarg ("," styleKwarg)* } -styleKwarg { Name "=" (QuotedString | Boolean | Number) } - -formatExpr { formatPreset | formatTyped } -formatPreset { kw<"format"> "(" QuotedString ("," formatKwargs)? ")" } -formatTyped { kw<"format"> "." formatType "(" formatKwargs? ")" } -formatType { @specialize[@name=FormatType] } -formatKwargs { formatKwarg ("," formatKwarg)* } -formatKwarg { Name "=" (QuotedString | Boolean | Number | Dict) } - -Dict { "{" (dictEntry ("," dictEntry)*)? "}" } -dictEntry { QuotedString ":" QuotedString } - -kw { @specialize[@name={term}] } - -@tokens { - Name { @asciiLetter (@asciiLetter | @digit | "_")* } - QuotedString { '"' (!["\\] | "\\" _)* '"' | "'" (!['\\] | "\\" _)* "'" } - Integer { @digit+ } - Number { @digit+ ("." @digit+)? } - Boolean { "True" | "False" | "true" | "false" } - CellId { "tcell_" ((@asciiLetter | @digit | "_" | "-"))+ } - - space { " " | "\t" } - newline { "\n" | "\r\n" } - - @precedence { CellId, Name } - @precedence { Number, Integer } -} - -@precedence { - arith @left -} - -@detectDelim -``` - -### ~~Editor Setup~~ (Deprecated - CodeMirror 6) - -```javascript -// DEPRECATED: CodeMirror 6 approach (requires bundler, incompatible with FastHTML) -import { EditorState } from '@codemirror/state' -import { EditorView, keymap, lineNumbers, drawSelection } from '@codemirror/view' -import { defaultKeymap, indentWithTab } from '@codemirror/commands' -import { indentOnInput, bracketMatching } from '@codemirror/language' -import { autocompletion } from '@codemirror/autocomplete' -import { linter } from '@codemirror/lint' - -import { formatRulesLanguage } from './formatrules-lang' // Generated from grammar -import { daisyTheme } from './daisy-theme' -import { formatRulesCompletion } from './completion' -import { formatRulesLinter } from './linter' - -function createFormatRulesEditor(container, initialValue, options = {}) { - const { onChange, columns = [], stylePresets = [], formatPresets = [] } = options - - const state = EditorState.create({ - doc: initialValue, - extensions: [ - lineNumbers(), - drawSelection(), - indentOnInput(), - bracketMatching(), - keymap.of([...defaultKeymap, indentWithTab]), - - // Language support - formatRulesLanguage(), - - // Theme - daisyTheme, - - // Autocompletion with context - autocompletion({ - override: [formatRulesCompletion({ columns, stylePresets, formatPresets })], - }), - - // Linting (validation) - linter(formatRulesLinter()), - - // Change callback - EditorView.updateListener.of((update) => { - if (update.docChanged && onChange) { - onChange(update.state.doc.toString()) - } - }), - ], - }) - - return new EditorView({ - state, - parent: container, - }) -} -``` - ---- - -## Technical Choices - -### Why lark (not pyparsing) - -Both `lark` and `pyparsing` are mature Python parsing libraries. We chose **lark** for the following reasons: - -| Criterion | lark | pyparsing | -|-----------|------|-----------| -| **Grammar definition** | Declarative EBNF string | Python combinators | -| **Regex extraction** | Easy to extract terminals | Embedded in Python logic | -| **Grammar readability** | Standard BNF-like notation | Embedded in Python code | -| **Maintenance** | Single grammar source | Two separate grammars to sync | - -**The key factor**: We need the same grammar for both: -1. **Server-side** (Python): Validation and execution -2. **Client-side** (JavaScript): Syntax highlighting - -With lark's declarative EBNF grammar, we can extract terminal regex patterns and translate them to CodeMirror Simple Mode for client-side syntax highlighting. With pyparsing, the grammar is embedded in Python logic, making extraction significantly harder. - -### Why CodeMirror 5 (not 6) - -For the web-based DSL editor, we chose **CodeMirror 5**: - -| Criterion | CodeMirror 5 | CodeMirror 6 | Monaco | Ace | -|-----------|--------------|--------------|--------|-----| -| **Bundle requirements** | CDN ready | Requires bundler | ~2MB | ~300KB | -| **FastHTML compatibility** | Direct `