Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 31 additions & 13 deletions scripts/gen-problem-cards.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
# the only one that reads them. Run it first.
PAGE_MAP = ROOT / "web" / "problem-pages.json"
INDEX = ROOT / "web" / "index.html"
TOPIC_MAP = ROOT / "web" / "problem-topics.json"
# Matched without their indentation, which the surrounding markup owns and has
# already changed once: wrapping the grid in a `<details>` moved it two columns
# right, and the six-space literals these used to be went on matching only
Expand All @@ -37,18 +38,17 @@ def parse_args() -> argparse.Namespace:


def read_json(path: Path) -> object:
return json.loads(path.read_text())
return json.loads(path.read_text(encoding="utf-8"))


def card(problem: dict[str, object], page: str, title: str) -> str:
"""One lobby card, named the way the interview will name the exercise.

Keyed by page name, with the scenario title and no topic tags: the
recommendation line reads the title off this card, and "Recommended: Coin
Change", a `coin-change` key or a "Dynamic Programming" tag tells the
candidate what they are about to be asked. The published title is not in
the page at all; the source slot is filled from the page map only when the
candidate turns that on to drill one problem by name.
Keyed by page name, with the scenario title the recommendation line reads.
A `coin-change` key, published title or "Dynamic Programming" tag tells the
candidate what they are about to be asked, so none ships in the initial
page. The published title and topic map are fetched only after the candidate
opts into the corresponding picker control.
"""
return "\n".join(
[
Expand All @@ -61,8 +61,7 @@ def card(problem: dict[str, object], page: str, title: str) -> str:
)


def generated(problems: list) -> str:
pages = read_json(PAGE_MAP)
def generated(problems: list, pages: dict) -> str:
cards = "\n".join(
card(problem, pages[problem["id"]]["page"], pages[problem["id"]]["title"])
for problem in problems
Expand All @@ -77,9 +76,17 @@ def generated(problems: list) -> str:
)


def generated_topics(problems: list, pages: dict) -> str:
"""The opt-in topic filter's page-to-topic lookup."""
mapping = {
pages[problem["id"]]["page"]: problem.get("topics", []) for problem in problems
}
return json.dumps(mapping, ensure_ascii=False, indent=2) + "\n"


def main() -> int:
args = parse_args()
html_text = INDEX.read_text()
html_text = INDEX.read_text(encoding="utf-8")
opening = re.search(
rf"^([ \t]*){re.escape(START)}[ \t]*$", html_text, flags=re.MULTILINE
)
Expand All @@ -101,20 +108,31 @@ def main() -> int:
return 1

problems = read_json(SOURCE)
block = textwrap.indent(generated(problems), indent)
pages = read_json(PAGE_MAP)
block = textwrap.indent(generated(problems, pages), indent)
updated = html_text[: opening.start()] + block + html_text[end.end() :]
topics = generated_topics(problems, pages)
current_topics = TOPIC_MAP.read_text(encoding="utf-8") if TOPIC_MAP.exists() else ""
if args.check:
stale = []
if updated != html_text:
stale.append("web/index.html problem cards")
if topics != current_topics:
stale.append("web/problem-topics.json")
if stale:
print(
"web/index.html problem cards are stale; run: python3 scripts/gen-problem-cards.py",
f"{', '.join(stale)} are stale; run: python3 scripts/gen-problem-cards.py",
file=sys.stderr,
)
return 1
return 0

if updated != html_text:
INDEX.write_text(updated)
INDEX.write_text(updated, encoding="utf-8")
print(f"updated {len(problems)} problem cards in web/index.html")
if topics != current_topics:
TOPIC_MAP.write_text(topics, encoding="utf-8")
print(f"updated {len(problems)} problem topics in web/problem-topics.json")
return 0


Expand Down
16 changes: 15 additions & 1 deletion tests/browser/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ register("./dom-hooks.js", pathToFileURL(`${import.meta.dirname}/`));
/// properties that put a string on the page without going through `textContent`.
const SPOKEN_ATTRIBUTES = ["title", "ariaLabel", "alt", "placeholder", "value"];
const MARKUP = ["innerHTML", "innerText", "outerHTML"];
const FORM_CONTROLS = new Set(["button", "input", "select", "textarea"]);

class Element {
#text = "";
Expand Down Expand Up @@ -366,7 +367,20 @@ export function installDocument(markup) {
// `#start` and `button#start` render into two different places, which is a
// page a browser cannot produce.
const node = (element) => {
if (!nodes.has(element)) nodes.set(element, new Element(element.tag));
if (!nodes.has(element)) {
const created = new Element(element.tag);
for (const [name, value] of Object.entries(element.attributes)) {
if (name.startsWith("data-")) {
const key = name
.slice(5)
.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
created.dataset[key] = value;
}
}
if (FORM_CONTROLS.has(element.tag) && "value" in element.attributes)
created.value = element.attributes.value;
nodes.set(element, created);
}
return nodes.get(element);
};
globalThis.document = {
Expand Down
1 change: 0 additions & 1 deletion tests/browser/lobby-render.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ const SIGNED_OUT = {
start: "Start interview",
"progress-summary": "Could not load progress saved on this device.",
};

test("the lobby page loads outside a browser and says what it was given", () => {
for (const [id, expected] of Object.entries(SIGNED_OUT)) {
assert.deepEqual(dom.node(id).spoken(), [expected], `#${id}`);
Expand Down
Loading