#!/usr/bin/env python3 # -*- coding: utf-8 -*- """apply_guide.py — write one draft guide into templates//.felhom.yml, by TEXT, and prove the parse. python3 apply_guide.py [--hidden] The draft is {"app", "checked_version", "hu": {"sections", "faq"}, "en": {"sections", "faq"}}. The Hungarian guide goes at the END of the top-level `app_info:` block (two spaces), the English one at the end of `i18n.en.app_info` (six spaces). An existing guide block is replaced, so a morning format change is one more run. Every string is written JSON-quoted (valid YAML double-quoted: Hungarian quotes, apostrophes and colons cannot break it — the „Hungarian-quote YAML kill" trap). `--hidden` writes `checked_version: ""` (a label was not found: the box does not show the guide). PROOF, after writing: the file parses; `app_info.guide` and `i18n.en.app_info.guide` equal the draft; and the WHOLE document minus those two keys equals the document before. Any difference restores the old file and exits 1. """ import copy import io import json import sys import yaml def q(s): return json.dumps(s, ensure_ascii=False) def block(g, ind, cv=None): p = " " * ind out = [p + "guide:"] if cv is not None: out.append(p + " checked_version: " + q(cv)) out.append(p + " sections:") for se in g["sections"]: out.append(p + " - title: " + q(se["title"])) if se.get("intro"): out.append(p + " intro: " + q(se["intro"])) out.append(p + " steps:") for st in se["steps"]: out.append(p + " - " + q(st)) if g.get("faq"): out.append(p + " faq:") for qa in g["faq"]: out.append(p + " - q: " + q(qa["q"])) out.append(p + " a: " + q(qa["a"])) return out def indent(line): return len(line) - len(line.lstrip(" ")) def is_content(line): s = line.strip() return s != "" and not s.startswith("#") def span(lines, start, ind): """Index just past the last CONTENT line belonging to the block whose header is lines[start] (children are indented deeper than `ind`). Trailing blank and comment lines stay outside, with whatever follows.""" end = start + 1 last = start i = start + 1 while i < len(lines): if is_content(lines[i]): if indent(lines[i]) <= ind: break last = i i += 1 return last + 1 def remove_guide(lines, parent, ind): """Remove a `guide:` child (at indent `ind`) of the block headed at lines[parent].""" end = span(lines, parent, ind - 2) for i in range(parent + 1, end): if lines[i] == " " * ind + "guide:": j = span(lines, i, ind) del lines[i:j] return True return False def find_line(lines, text, after=0, before=None): for i in range(after, before if before is not None else len(lines)): if lines[i] == text: return i return -1 def strip_guides(doc): d = copy.deepcopy(doc) (d.get("app_info") or {}).pop("guide", None) en = ((d.get("i18n") or {}).get("en") or {}) (en.get("app_info") or {}).pop("guide", None) return d def main(argv): root, draft_path = argv[1], argv[2] hidden = "--hidden" in argv d = json.load(io.open(draft_path, encoding="utf-8")) app = d["app"] path = "%s/templates/%s/.felhom.yml" % (root, app) old_text = io.open(path, encoding="utf-8").read() old_doc = yaml.safe_load(old_text) lines = old_text.split("\n") # Hungarian: top-level app_info ai = find_line(lines, "app_info:") if ai < 0: print("FAIL %s: no top-level app_info:" % app) return 1 remove_guide(lines, ai, 2) end = span(lines, ai, 0) cv = "" if hidden else d["checked_version"] lines[end:end] = block(d["hu"], 2, cv) # English: i18n -> en -> app_info i18 = find_line(lines, "i18n:") en = find_line(lines, " en:", i18 + 1) if i18 >= 0 else -1 if en < 0: print("FAIL %s: no i18n.en block — every app is translated; refusing to invent one" % app) return 1 en_end = span(lines, en, 2) eai = find_line(lines, " app_info:", en + 1, en_end) if eai < 0: lines[en_end:en_end] = [" app_info:"] eai = en_end remove_guide(lines, eai, 6) e_end = span(lines, eai, 4) lines[e_end:e_end] = block(d["en"], 6) new_text = "\n".join(lines) new_doc = yaml.safe_load(new_text) want_hu = {"checked_version": cv, "sections": d["hu"]["sections"], "faq": d["hu"]["faq"]} want_en = {"sections": d["en"]["sections"], "faq": d["en"]["faq"]} def norm(g): # the writer omits an empty intro; compare like for like g = copy.deepcopy(g) for se in g.get("sections") or []: if not se.get("intro"): se.pop("intro", None) return g problems = [] if norm(new_doc["app_info"].get("guide")) != norm(want_hu): problems.append("Hungarian guide does not read back as the draft") if norm(new_doc["i18n"]["en"]["app_info"].get("guide")) != norm(want_en): problems.append("English guide does not read back as the draft") if strip_guides(new_doc) != strip_guides(old_doc): problems.append("the rest of the document CHANGED") if problems: print("FAIL %s: %s — file left as it was" % (app, "; ".join(problems))) return 1 with io.open(path, "w", encoding="utf-8") as fh: fh.write(new_text) print("OK %s: guide written (%d sections, %d faq; checked_version %r)" % (app, len(d["hu"]["sections"]), len(d["hu"]["faq"]), cv)) return 0 if __name__ == "__main__": sys.exit(main(sys.argv))