diff --git a/CHANGELOG.md b/CHANGELOG.md index 021a551..c6a0abe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ All notable changes to Elo are documented here. The format is based on ## [Unreleased] +- The editor now renders operators as typographic glyphs in the display layer + without changing the underlying text: `*` → `×`, spaced `/` → `÷`, and the + ligatures `->` → `→`, `=>` → `⇒`, `!=` → `≠`, `<=` → `≤`, `>=` → `≥`. - New app icon: a typed "E" mark resolving to a result row, rendered across all platforms (macOS `.icns`, Linux/Windows PNGs and `.ico`, and the iOS AppIcon set). diff --git a/elo-tauri/src/main.ts b/elo-tauri/src/main.ts index 8d336f5..ab47b56 100644 --- a/elo-tauri/src/main.ts +++ b/elo-tauri/src/main.ts @@ -129,14 +129,14 @@ function highlightMarkdown(text: string): string { const headerMatch = escaped.match(/^(#{1,3}\s)(.*)/); if (headerMatch) { result.push( - `${headerMatch[1]}${headerMatch[2]}`, + `${headerMatch[1]}${applyOperators(headerMatch[2])}`, ); continue; } // Comments if (trimmed.startsWith("//")) { - result.push(`${escaped}`); + result.push(`${applyOperators(escaped)}`); continue; } @@ -165,19 +165,48 @@ function highlightMarkdown(text: string): string { } function applyInlineFormatting(escaped: string): string { - // Single-pass regex matching inline code, bold, underline, italic - // Order matters: longer delimiters first to avoid partial matches - return escaped.replace( - /(`[^`]+`)|(\*\*[^*]+\*\*)|(__[^_]+__)|(\*[^*]+\*)|(_[^_]+_)/g, - (match, code, bold, underline, italicStar, italicUnderscore) => { - if (code) return `${match}`; - if (bold) return `${match}`; - if (underline) return `${match}`; - if (italicStar) return `${match}`; - if (italicUnderscore) return `${match}`; - return match; - }, - ); + // Walk the markdown matches manually so inline code/bold/italic/underline are + // emitted verbatim, while only the plain-text gaps between them receive + // operator substitution. This protects a markdown `*` (italic/bold) from + // becoming `×` while still converting a math `*` (e.g. `2 * 3`, `(a+b)*c`), + // which lives in a gap. + // Order matters: longer delimiters first to avoid partial matches. + const md = /(`[^`]+`)|(\*\*[^*]+\*\*)|(__[^_]+__)|(\*[^*]+\*)|(_[^_]+_)/g; + let out = ""; + let last = 0; + let m: RegExpExecArray | null; + while ((m = md.exec(escaped)) !== null) { + out += applyOperators(escaped.slice(last, m.index)); + const [match, code, bold, underline, italicStar] = m; + if (code) out += `${match}`; + else if (bold) out += `${match}`; + else if (underline) out += `${match}`; + else if (italicStar) out += `${match}`; + else out += `${match}`; + last = m.index + match.length; + } + out += applyOperators(escaped.slice(last)); + return out; +} + +// Display-only operator glyphs. Runs on already-HTML-escaped text (so `<`/`>` +// are `<`/`>`). The underlying textarea value is never changed, so the +// Rust parser still receives raw ASCII. Two-char ligatures use a 2ch-wide span +// so the glyph occupies exactly the same character grid as the source chars, +// keeping the textarea caret and soft-wrapping aligned with the overlay. +function applyOperators(s: string): string { + return s + .replace(/->/g, ``) + .replace(/=>/g, ``) + .replace(/!=/g, ``) + .replace(/<=/g, ``) + .replace(/>=/g, ``) + // Division: only when surrounded by whitespace, so dates (06/28), + // rates/units (km/h), and paths stay literal. Single cell. + .replace(/(?<=\s)\/(?=\s)/g, `÷`) + // Multiplication: any remaining `*` (markdown `*` is already consumed on + // inline-formatted lines before this runs). Single cell. + .replace(/\*/g, `×`); } // Sync scroll between editor, highlight, and results diff --git a/elo-tauri/src/styles.css b/elo-tauri/src/styles.css index f0e68d3..4b43215 100644 --- a/elo-tauri/src/styles.css +++ b/elo-tauri/src/styles.css @@ -127,6 +127,7 @@ html, body { overflow: hidden; pointer-events: none; z-index: 0; + font-variant-ligatures: none; } #editor { @@ -149,6 +150,7 @@ html, body { user-select: text; z-index: 1; font-synthesis: none; + font-variant-ligatures: none; } #editor::selection { @@ -190,6 +192,16 @@ html, body { .md-code-fence { color: var(--text-muted); } .md-code-block { color: var(--text-muted); } +/* Display-only operator glyphs in the overlay. Two-char ligatures occupy + exactly two monospace cells so the caret/wrapping stay aligned with the + underlying textarea text. */ +.op-lig { + display: inline-block; + width: 2ch; + text-align: center; +} +/* op-mul / op-div are single-cell 1:1 swaps; classes kept for theming. */ + /* Results gutter */ #results { width: 260px;