# 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 `