Editor: jump-to-section dropdown (parses /* banner */ comments)
This commit is contained in:
@@ -38,7 +38,7 @@ Set `TOKEN=...` in the environment to pin your own.
|
||||
| Route | What it does |
|
||||
|-------------|-----------------------------------------------------------|
|
||||
| `GET /` | The editor page (no auth) |
|
||||
| `GET /file` | Current strings file + git dirty/clean banner |
|
||||
| `GET /file` | Current strings file + git dirty/clean banner + section list |
|
||||
| `POST /save`| Write the file, then `plutil -lint` it |
|
||||
| `POST /save-build` | Write the file and start a background build job |
|
||||
| `GET /job/<id>` | Job progress (polled by the page, 2 s) |
|
||||
|
||||
@@ -20,6 +20,7 @@ Config via env: FUELBOARD_REPO, TOKEN, PORT (default 8790).
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import secrets
|
||||
import shutil
|
||||
import subprocess
|
||||
@@ -76,6 +77,18 @@ def git_head() -> str:
|
||||
return git("log", "--oneline", "-1") or "no commits"
|
||||
|
||||
|
||||
def parse_sections(content: str) -> list:
|
||||
"""Find single-line comment banners like /* Stations tab */ so the
|
||||
editor can offer a jump-to-section dropdown. The multi-line header
|
||||
comment at the top never matches (it doesn't end with */ on one line)."""
|
||||
sections = []
|
||||
for i, line in enumerate(content.splitlines(), start=1):
|
||||
m = re.match(r"^/\* (.+) \*/$", line.strip())
|
||||
if m:
|
||||
sections.append({"name": m.group(1).strip(), "line": i})
|
||||
return sections
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Build job (runs in a background thread so the phone doesn't wait on it)
|
||||
# --------------------------------------------------------------------------
|
||||
@@ -212,7 +225,8 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self._json(404, {"error": f"missing {STRINGS_FILE}"})
|
||||
return
|
||||
self._json(200, {"content": content, "dirty": git_dirty_files(),
|
||||
"head": git_head(), "repo": str(REPO)})
|
||||
"head": git_head(), "repo": str(REPO),
|
||||
"sections": parse_sections(content)})
|
||||
return
|
||||
if self.path.startswith("/job/"):
|
||||
if not self._authorized():
|
||||
|
||||
+27
@@ -20,6 +20,11 @@
|
||||
padding: 8px 10px; border-radius: 8px; font-size: 13px; margin-bottom: 12px;
|
||||
}
|
||||
#dirty.ok { background: #12331c; border-color: #1f7a3d; color: #7ee2a0; }
|
||||
select {
|
||||
width: 100%; margin-bottom: 12px; padding: 10px 12px; font-size: 15px;
|
||||
background: #0d0f12; color: #e6e8eb; border: 1px solid #2a313a;
|
||||
border-radius: 10px; -webkit-appearance: none; appearance: none;
|
||||
}
|
||||
textarea {
|
||||
width: 100%; min-height: 46vh; resize: vertical;
|
||||
background: #0d0f12; color: #d7dce2; border: 1px solid #2a313a;
|
||||
@@ -51,6 +56,9 @@
|
||||
<h1>⛽ FuelBoard Copy Editor</h1>
|
||||
<div class="meta"><code id="repo">…</code> · <span id="head">…</span></div>
|
||||
<div id="dirty"></div>
|
||||
<select id="jump" disabled>
|
||||
<option value="">Jump to section…</option>
|
||||
</select>
|
||||
<textarea id="src" spellcheck="false" placeholder="Loading Localizable.strings…"></textarea>
|
||||
<div class="row">
|
||||
<button id="save">Save</button>
|
||||
@@ -83,6 +91,10 @@ async function load() {
|
||||
document.getElementById("src").value = data.content;
|
||||
document.getElementById("repo").textContent = data.repo;
|
||||
document.getElementById("head").textContent = data.head;
|
||||
const jump = document.getElementById("jump");
|
||||
jump.innerHTML = '<option value="">Jump to section…</option>' +
|
||||
(data.sections || []).map(s => `<option value="${s.line}">${s.name}</option>`).join("");
|
||||
jump.disabled = !(data.sections || []).length;
|
||||
const dirty = document.getElementById("dirty");
|
||||
if (data.dirty.length) {
|
||||
dirty.style.display = "block";
|
||||
@@ -155,6 +167,21 @@ function enable() {
|
||||
|
||||
document.getElementById("save").addEventListener("click", saveOnly);
|
||||
document.getElementById("saveBuild").addEventListener("click", saveAndBuild);
|
||||
document.getElementById("jump").addEventListener("change", (e) => {
|
||||
const line = parseInt(e.target.value, 10);
|
||||
e.target.value = ""; // act as a "go" control, not a persistent selection
|
||||
if (!line) return;
|
||||
const ta = document.getElementById("src");
|
||||
const ls = ta.value.split("\n");
|
||||
let offset = 0;
|
||||
for (let i = 0; i < line - 1; i++) offset += ls[i].length + 1;
|
||||
const bannerLen = ls[line - 1].length;
|
||||
const lh = parseFloat(getComputedStyle(ta).lineHeight); // e.g. 19.5
|
||||
const pad = parseFloat(getComputedStyle(ta).paddingTop); // 12
|
||||
ta.focus({ preventScroll: true });
|
||||
ta.scrollTop = Math.max(0, (line - 1) * lh - pad);
|
||||
ta.setSelectionRange(offset, offset + bannerLen); // flash-highlight the banner
|
||||
});
|
||||
load();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user