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>
182 lines
8.8 KiB
Markdown
182 lines
8.8 KiB
Markdown
# 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`:
|
|
|
|
```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: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_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.
|