Operator verdict on the separate /asks page: the question belongs with the artifact it is about. A four-voice audition wants each voice's radio group under that voice's audio, and one submit for the lot. - booth/inline.py: data-booth-ask="stem" | "stem:key" | data-booth-ask-submit, plus <!-- booth:ask ... --> comments; unknown stem left alone, not blanked - _ask_inline.html: self-contained fragments (own scoped styles, no JS), per-question groups bound to one form via the HTML5 form= attribute so a scattered multi-question ask still POSTs once - unplaced questions and a missing submit block are appended, so a partially marked-up page can never produce an unsubmittable 400 - chip becomes a jump link to the first open ask; /asks page kept as a fallback - 6 tests (one caught the partial-placement drop); v0.1.14
115 lines
4.7 KiB
Python
115 lines
4.7 KiB
Python
"""Inline ask placement inside a booth's VERBATIM index.html.
|
|
|
|
A booth that ships its own `index.html` is served untouched, so the auto-gallery
|
|
template's asks panel never renders there. The first fix was a chip linking to a
|
|
separate `/asks` page; the operator's verdict on that (2026-09-09) was that the
|
|
question belongs WITH the artifacts it is about — a four-voice audition wants the
|
|
radio group for each voice under that voice's audio, not on another page.
|
|
|
|
So the report author marks where each piece goes, with a placeholder element:
|
|
|
|
<div data-booth-ask="anchors"></div> the whole ask: every question + submit
|
|
<div data-booth-ask="anchors:lawson"></div> just that question's radios
|
|
<div data-booth-ask-submit="anchors"></div> the notes field + submit button
|
|
|
|
Per-question fragments bind to ONE form via the HTML5 `form=` attribute, so four
|
|
groups scattered down a page still submit as a single POST — which is what a
|
|
multi-question ask requires (every question or 400). No JavaScript.
|
|
|
|
An `<!-- booth:ask anchors -->` comment works the same way, for authors who would
|
|
rather not put an empty div in their markup.
|
|
|
|
Placement is OPTIONAL. A page with no placeholders gets the whole ask appended at
|
|
the end of its body, so an ask is never invisible — that guarantee is the point,
|
|
and marking it up only moves it somewhere better.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
|
|
# <div data-booth-ask="stem"></div> / <span data-booth-ask="stem:key"></span>
|
|
_EL_RE = re.compile(
|
|
r"<(?P<tag>[A-Za-z][\w-]*)\b[^>]*?\bdata-booth-ask=\"(?P<spec>[^\"]+)\"[^>]*?>"
|
|
r"(?:\s*</(?P=tag)\s*>)?",
|
|
re.IGNORECASE,
|
|
)
|
|
_SUBMIT_EL_RE = re.compile(
|
|
r"<(?P<tag>[A-Za-z][\w-]*)\b[^>]*?\bdata-booth-ask-submit=\"(?P<spec>[^\"]+)\"[^>]*?>"
|
|
r"(?:\s*</(?P=tag)\s*>)?",
|
|
re.IGNORECASE,
|
|
)
|
|
# <!-- booth:ask stem --> / <!-- booth:ask stem:key --> / <!-- booth:ask-submit stem -->
|
|
_COMMENT_RE = re.compile(r"<!--\s*booth:ask\s+(?P<spec>[^\s>-][^\s>]*)\s*-->", re.IGNORECASE)
|
|
_COMMENT_SUBMIT_RE = re.compile(r"<!--\s*booth:ask-submit\s+(?P<spec>[^\s>]+)\s*-->", re.IGNORECASE)
|
|
|
|
|
|
def split_spec(spec: str) -> tuple[str, str | None]:
|
|
"""`"anchors:lawson"` -> `("anchors", "lawson")`; `"anchors"` -> `("anchors", None)`."""
|
|
stem, sep, key = spec.strip().partition(":")
|
|
return stem.strip(), (key.strip() or None) if sep else None
|
|
|
|
|
|
def has_placeholders(html: str) -> bool:
|
|
return bool(
|
|
_EL_RE.search(html) or _SUBMIT_EL_RE.search(html)
|
|
or _COMMENT_RE.search(html) or _COMMENT_SUBMIT_RE.search(html)
|
|
)
|
|
|
|
|
|
def form_id(stem: str) -> str:
|
|
return f"bk-ask-form-{re.sub(r'[^A-Za-z0-9_-]', '-', stem)}"
|
|
|
|
|
|
def place(html: str, asks: list[dict], render) -> tuple[str, dict[str, set], set[str]]:
|
|
"""Substitute every placeholder with rendered ask HTML.
|
|
|
|
`render(kind, ask, key)` returns the fragment for kind in
|
|
{"whole", "question", "submit"}. Returns the new html; a map of stem ->
|
|
the set of question keys placed inline (with `None` in the set meaning the
|
|
WHOLE ask was placed); and the set of stems whose submit block was placed
|
|
explicitly.
|
|
|
|
The caller needs the per-key detail, not just "this stem appeared
|
|
somewhere": a multi-question ask requires EVERY question on submit, so a
|
|
page that marks up two of four questions must still be handed the other two
|
|
or the form is unsubmittable — a 400 the operator would meet only after
|
|
filling it in.
|
|
|
|
A placeholder naming an ask this booth does not have is left ALONE, not
|
|
blanked: silently eating the author's markup would hide a typo'd stem, and
|
|
an untouched empty div is invisible anyway.
|
|
"""
|
|
by_stem = {a["stem"]: a for a in asks}
|
|
placed: dict[str, set] = {}
|
|
submitted: set[str] = set()
|
|
|
|
def sub_main(m: re.Match) -> str:
|
|
stem, key = split_spec(m.group("spec"))
|
|
ask = by_stem.get(stem)
|
|
if ask is None:
|
|
return m.group(0)
|
|
if key is None:
|
|
placed.setdefault(stem, set()).add(None)
|
|
submitted.add(stem)
|
|
return render("whole", ask, None)
|
|
q = next((q for q in ask.get("questions", []) if q.get("key") == key), None)
|
|
if q is None:
|
|
return m.group(0)
|
|
placed.setdefault(stem, set()).add(key)
|
|
return render("question", ask, key)
|
|
|
|
def sub_submit(m: re.Match) -> str:
|
|
stem, _ = split_spec(m.group("spec"))
|
|
ask = by_stem.get(stem)
|
|
if ask is None:
|
|
return m.group(0)
|
|
placed.setdefault(stem, set())
|
|
submitted.add(stem)
|
|
return render("submit", ask, None)
|
|
|
|
for pat, fn in ((_EL_RE, sub_main), (_COMMENT_RE, sub_main),
|
|
(_SUBMIT_EL_RE, sub_submit), (_COMMENT_SUBMIT_RE, sub_submit)):
|
|
html = pat.sub(fn, html)
|
|
return html, placed, submitted
|