left4me/docs/superpowers/specs/2026-05-16-textarea-code-editor-design.md
mwiegand bef6f0cdd9
spec(textarea-editor): syntax highlighting + autocomplete via CodeJar + Prism
Upgrade blueprint config, overlay script, and files-editor textareas with a
reusable vanilla-JS editor. Textarea stays as value carrier so form POST and
files-overlay.js fetch paths are untouched. Seed cvar vocabulary from the
existing l4d2-server-cvar-reference.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 17:33:10 +02:00

8.8 KiB

Textarea Code Editor (Highlighting + Autocomplete) Design

Goal: Upgrade three plain-text <textarea> fields in left4me into a lightweight code editor with syntax highlighting and identifier-as-you-type autocomplete. The motivating case is the blueprint config field, where users edit Source-engine server.cfg-style content but have to remember hundreds of L4D2 cvar names from memory.

Architecture: One reusable vanilla-JS widget. Each <textarea> opts in via a data-editor-language attribute. The textarea stays in the DOM as the value carrier — the widget mounts a contenteditable sibling that mirrors content back into the textarea on every input, so HTML form submission and existing JS that reads textarea.value keep working unchanged. Library stack is two single-file self-hosted dependencies (CodeJar + Prism) plus a small custom grammar — no bundler, no build step.


Call sites

Template Line Language Save path
l4d2web/l4d2web/templates/blueprint_detail.html 52 srccfg (preset) HTML form POST → blueprint_routes.update_blueprint_form
l4d2web/l4d2web/templates/overlay_detail.html 25 bash (preset) HTML form POST → overlay script update route
l4d2web/l4d2web/templates/overlay_detail.html 178 auto (filename-derived; dropdown override) fetch('/files/save', {path, content}) from files-overlay.js:450-557

Widget contract

Every textarea with data-editor-language="<lang>" gets upgraded on DOMContentLoaded:

  • Sibling <div class="editor-shell"> holding <code class="editor-code" contenteditable="true">, initialized with the textarea's value.
  • Textarea set to display: none (kept in DOM as form/value carrier).
  • CodeJar mounted on the <code>, with highlighter → Prism.highlightElement for the active language.
  • CodeJar onUpdate(code) writes back to textarea.value and dispatches an input event on the textarea.
  • Floating autocomplete <ul> positioned at the caret via getSelection().getRangeAt(0).getBoundingClientRect().
  • Exposes setLanguage(name) for the files-editor language dropdown.

If JS fails to bootstrap, the textarea is shown (matching today's behavior) — graceful no-JS fallback for free.

Autocomplete behavior

  • Trigger: word fragment before caret matches [A-Za-z0-9_]{2,} and has ≥1 hit in the active language's vocabulary.
  • Filter: case-insensitive prefix match first, then substring match. Cap 50 candidates; render top 8 with scroll for the rest.
  • Keys (popup open): ↑/↓ navigate · Tab/Enter accept · Esc close · any other key continues editing and re-filters.
  • Mouse: click to accept.
  • Display: identifier line + muted description line if desc present.

Languages

  • srccfg — custom Prism grammar (~30 lines). Tokens: comment (//…), string ("…" with escapes), number, keyword (exec | alias | bind), identifier. Purely visual — no semantic validation.
  • bash — Prism's stock bash grammar. No project-owned code.
  • auto — sentinel resolved on mount from the filename: .cfg → srccfg, .sh → bash, otherwise plain. Re-evaluated when the filename input changes (only while the dropdown is in its initial auto state).
  • plain — no highlighter, no autocomplete. The widget still mounts so the language dropdown remains usable; setting language back to srccfg or bash re-activates highlighting.

Vocabulary

Lives at l4d2web/l4d2web/static/data/srccfg-vocab.json:

{
  "cvars":    [{"name": "sv_cheats", "desc": "Allow cheat cvars (0/1)"}, ],
  "commands": [{"name": "exec",      "desc": "Execute a .cfg file"},    ]
}

Sourcing — two-stage:

  1. Seed from the existing cvar reference doc. docs/l4d2-server-cvar-reference.md already curates the cvars users actually touch (with descriptions and recommended values). Extract identifiers from its tables and code fences; carry across short descriptions where present.
  2. Optionally augment with a cvarlist/cmdlist dump from a freshly- started L4D2 server with the project's common SourceMod plugins loaded, hand-trimmed for engine internals nobody touches. This is purely additive breadth; the curated reference is the authoritative source for descriptions.

Generated once, committed verbatim. Document the regeneration procedure in the top-of-file comment.

Loading: lazy fetch on first srccfg editor mount; cached on window.__srccfgVocab so multiple editors on the same page share the load.

Asset layout (new)

l4d2web/l4d2web/static/
  vendor/
    prism.js          # prismjs.com custom build: core + clike + bash, pinned
    prism.css
    codejar.js        # github.com/antonmedv/codejar release, pinned
    README.md         # source URLs + versions + SHA256 per file
  js/
    editor.js         # widget: mount, popup, sync, language switch
    srccfg-grammar.js # Prism.languages.srccfg
  data/
    srccfg-vocab.json # curated cvars + commands

All vendored files are self-hosted; the existing CSP (l4d2web/l4d2web/app.py:99default-src 'self'; script-src 'self' 'nonce-…') requires it. Template <script> tags use nonce="{{ g.csp_nonce }}" per the established pattern (app.py:86).

Files Touched

File Change
l4d2web/l4d2web/templates/blueprint_detail.html Add data-editor-language="srccfg" to line 52; include editor asset block
l4d2web/l4d2web/templates/overlay_detail.html Add data-editor-language="bash" to line 25; add data-editor-language="auto" + language <select> near line 178 filename field; include editor asset block
l4d2web/l4d2web/templates/_editor_assets.html New Jinja partial: the five script/link tags with nonces
l4d2web/l4d2web/static/vendor/prism.{js,css} New (vendored)
l4d2web/l4d2web/static/vendor/codejar.js New (vendored)
l4d2web/l4d2web/static/vendor/README.md New — versions + SHA256s
l4d2web/l4d2web/static/js/editor.js New — ~250 LOC widget
l4d2web/l4d2web/static/js/srccfg-grammar.js New — ~30 LOC Prism grammar
l4d2web/l4d2web/static/data/srccfg-vocab.json New — curated cvars/commands
l4d2web/l4d2web/static/css/components.css Add .editor-shell, .editor-code, .editor-popup rules; reuse tokens.css color variables; preserve textarea visual to avoid layout shift
l4d2web/pyproject.toml Add playwright dev dep
l4d2web/tests/e2e/conftest.py New — boot Flask app under test, expose URL fixture
l4d2web/tests/e2e/test_editor.py New — Playwright test: type sv_che, assert popup, accept, assert textarea value
l4d2web/tests/test_blueprints.py Extend — assert editor attrs in GET, assert form contract on POST

Untouched by design: l4d2web/l4d2web/blueprint_routes.py, l4d2web/l4d2web/static/js/files-overlay.js, l4d2web/l4d2web/app.py — the form contract and the JSON-fetch save path remain identical, which is the central reason this design uses "textarea-as-value-carrier" rather than replacing the textarea.

Reusable patterns referenced

  • l4d2web/l4d2web/static/js/blueprint-overlay-picker.js:20-26,63 — progressive enhancement via DOM-manipulated form fields. This editor follows the same pattern (no submit interception, native form serialization).
  • l4d2web/l4d2web/app.py:86g.csp_nonce accessor for template <script> tags.
  • l4d2web/l4d2web/static/js/files-overlay.js:450-557 — JSON-fetch save path that continues to read textarea.value unchanged.

Verification

Manual (Chrome MCP during development):

  1. /blueprints/<id> — textarea replaced by editor; existing content preserved.
  2. Type sv_che → popup shows sv_cheats; Tab accepts; line highlights.
  3. Submit form; reload; saved value matches editor content.
  4. Script-type overlay → bash highlighting.
  5. Files-type overlay → create test.cfg, srccfg highlighting; dropdown to bash, re-highlights; save; persisted content matches.
  6. Disable JS, reload blueprint → textarea visible, form submits.
  7. View-source → no inline scripts, all assets nonce'd.

Automated:

  • cd l4d2web && uv run pytest tests/test_blueprints.py
  • cd l4d2web && uv run pytest -m e2e (Playwright)

Open / Closed

  • Closed in v1: line numbers, search-in-editor, multi-cursor, bracket matching, theme switching, SourcePawn highlighting, bash buffer-context autocomplete, per-server live cvarlist capture.
  • Open as additive future work: line numbers (deferred per explicit scope decision; cheap to add later as a non-wrapping mode for srccfg + bash); SourcePawn grammar; per-server cvar augmentation.
  • No backend changes. The design is intentionally constrained to static assets + template attributes so the change is small to review and easy to revert by deleting the asset block.