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>
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.highlightElementfor the active language. - CodeJar
onUpdate(code)writes back totextarea.valueand dispatches aninputevent on the textarea. - Floating autocomplete
<ul>positioned at the caret viagetSelection().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
descpresent.
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 stockbashgrammar. No project-owned code.auto— sentinel resolved on mount from the filename:.cfg → srccfg,.sh → bash, otherwiseplain. 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 tosrccfgorbashre-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:
- Seed from the existing cvar reference doc.
docs/l4d2-server-cvar-reference.mdalready 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. - Optionally augment with a
cvarlist/cmdlistdump 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:99 — default-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:86—g.csp_nonceaccessor for template<script>tags.l4d2web/l4d2web/static/js/files-overlay.js:450-557— JSON-fetch save path that continues to readtextarea.valueunchanged.
Verification
Manual (Chrome MCP during development):
/blueprints/<id>— textarea replaced by editor; existing content preserved.- Type
sv_che→ popup showssv_cheats; Tab accepts; line highlights. - Submit form; reload; saved value matches editor content.
- Script-type overlay → bash highlighting.
- Files-type overlay → create
test.cfg, srccfg highlighting; dropdown to bash, re-highlights; save; persisted content matches. - Disable JS, reload blueprint → textarea visible, form submits.
- View-source → no inline scripts, all assets nonce'd.
Automated:
cd l4d2web && uv run pytest tests/test_blueprints.pycd 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
cvarlistcapture. - 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.