diff options
| -rwxr-xr-x | fanfictioner | 51 |
1 files changed, 42 insertions, 9 deletions
diff --git a/fanfictioner b/fanfictioner index 8f5764b..fc5b761 100755 --- a/fanfictioner +++ b/fanfictioner @@ -95,6 +95,8 @@ def parse_plan(text): section = cur = None for line in text.splitlines(): s = line.strip() + if s.startswith("```") or s == "---": + continue if s.startswith("# ") and not plan["title"]: plan["title"] = s[2:].strip() elif s.startswith("## "): @@ -106,12 +108,12 @@ def parse_plan(text): elif section == "pages": m = re.match(r"(\d+)\.\s*(.*)", head) cur = {"n": int(m[1]) if m else 0, "title": m[2] if m else head, "text": [], - "characters": [], "pose": "", "framing": "", "model": ""} + "heading": "" if m else head, "characters": None, "pose": "", "framing": "", "model": ""} plan["pages"].append(cur) - elif section is None and (m := re.match(r"(Series|Book|Style):\s*(.*)", s)): + elif section is None and (m := re.match(r"[-*\s]*\**(Series|Book|Style)\**:\**\s*(.*)", s)): plan[m[1].lower()] = m[2].strip() elif section == "pages" and cur is not None and \ - (m := re.match(r"(Characters|Pose|Framing|Model):\s*(.*)", s)): + (m := re.match(r"[-*\s]*\**(Characters|Pose|Framing|Model)\**:\**\s*(.*)", s)): key, val = m[1].lower(), m[2].strip() if key == "characters": val = [] if val.lower() == "none" else [c.strip() for c in val.split(",") if c.strip()] @@ -132,13 +134,23 @@ def validate_plan(plan): errs.append("no characters") if not plan["pages"]: errs.append("no pages") + errs += [f'bad {k} name "{plan[k]}"' for k in ("series", "book") + if plan[k] and (("/" in plan[k]) or plan[k] in (".", ".."))] + errs += [f'bad character name "{c}"' for c in plan["characters"] if "/" in c or c in (".", "..")] ns = [p["n"] for p in plan["pages"]] - if len(set(ns)) != len(ns) or 0 in ns: + if sorted(ns) != list(range(1, len(ns) + 1)): errs.append("page numbers must be unique and start at 1") for p in plan["pages"]: - errs += [f"page {p['n']}: unknown character {c}" for c in p["characters"] + if p["heading"]: + errs.append(f'page heading "{p["heading"]}" must look like "### N. title"') + if p["characters"] is None: + errs.append(f"page {p['n']}: Characters missing") + chars = p["characters"] or [] + errs += [f"page {p['n']}: unknown character {c}" for c in chars if c not in plan["characters"]] - if p["characters"] and not p["pose"]: + if len(set(chars)) != len(chars): + errs.append(f"page {p['n']}: duplicate character") + if chars and not p["pose"]: errs.append(f"page {p['n']}: Pose missing") if p["model"] and p["model"] not in ("zimage", "krea2"): errs.append(f"page {p['n']}: unknown model {p['model']}") @@ -185,7 +197,7 @@ def edit_groups(chars): def edit_hint(plan): return "\n".join(f"Page {p['n']}: " + (" then ".join("[" + ", ".join(g) + "]" - for g in edit_groups(p["characters"])) + for g in edit_groups(p["characters"] or [])) or "no edits") for p in plan["pages"]) @@ -194,7 +206,7 @@ def check_json(pj, plan): errs = [] if [c["name"] for c in pj["characters"]] != list(plan["characters"]): errs.append("characters differ from plan.md") - want = {p["n"]: p["characters"] for p in plan["pages"]} + want = {p["n"]: p["characters"] or [] for p in plan["pages"]} if sorted(p["n"] for p in pj["pages"]) != sorted(want): errs.append("pages differ from plan.md") for p in pj["pages"]: @@ -243,6 +255,26 @@ def selftest(): assert validate_plan(parse_plan("")) == ["missing title", "missing series", "missing book", "no characters", "no pages"] + md = parse_plan(SAMPLE_PLAN.replace("Characters: Mara, Jon, Kit", "**Characters:** Mara, Jon, Kit") + .replace("Pose:", "- **Pose**:").replace("Framing:", "* Framing:") + .replace("Series:", "**Series:**").replace("Style:", "- Style:")) + assert md["series"] == "Test Series" and md["style"] == "soft watercolor, muted colors", md + assert md["pages"][0]["characters"] == ["Mara", "Jon", "Kit"], md + assert md["pages"][0]["pose"].startswith("Mara left") and md["pages"][0]["framing"].startswith("wide"), md + assert validate_plan(md) == [] + assert "page 2: Characters missing" in validate_plan(parse_plan(SAMPLE_PLAN.replace("Characters: none\n", ""))) + assert edit_hint(parse_plan(SAMPLE_PLAN.replace("Characters: none\n", ""))).endswith("Page 2: no edits") + assert 'bad series name "../x"' in validate_plan(parse_plan(SAMPLE_PLAN.replace("Test Series", "../x"))) + assert 'bad book name ".."' in validate_plan(parse_plan(SAMPLE_PLAN.replace("Book One", ".."))) + assert 'bad character name "Jon/Kit"' in validate_plan(parse_plan(SAMPLE_PLAN.replace("### Jon", "### Jon/Kit"))) + assert "page numbers must be unique and start at 1" in validate_plan( + parse_plan(SAMPLE_PLAN.replace("### 1.", "### 2.").replace("### 2. Empty", "### 3. Empty"))) + assert 'page heading "Page 2: Empty roof" must look like "### N. title"' in validate_plan( + parse_plan(SAMPLE_PLAN.replace("### 2. Empty", "### Page 2: Empty"))) + assert "page 1: duplicate character" in validate_plan( + parse_plan(SAMPLE_PLAN.replace("Characters: Mara, Jon, Kit", "Characters: Mara, Mara"))) + fenced = parse_plan(SAMPLE_PLAN.replace("They reach the roof at dusk.", "```\nThey reach the roof at dusk.\n---\n```")) + assert fenced["pages"][0]["text"] == "They reach the roof at dusk.", fenced assert edit_groups([]) == [] assert edit_groups(["Mara", "Jon", "Kit"]) == [["Mara", "Jon"], ["Kit"]] assert edit_groups(list("abcd")) == [["a", "b"], ["c", "d"]] @@ -299,7 +331,8 @@ def selftest(): assert [p["n"] for p in pages] == [2], pages keep(t / "refs" / "Mara.png", t / "deep" / "x.png") - assert (t / "deep" / "x.png").exists() + assert (t / "deep" / "x.png").read_bytes() == (t / "refs" / "Mara.png").read_bytes() + assert not list((t / "deep").glob("*.tmp")) print("selftest ok") |
