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[1]}`,
);
continue;
}
// Comments
if (trimmed.startsWith("//")) {
- result.push(``);
+ result.push(``);
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;