bfd9b2a7ea
gates / gates (push) Successful in 6m38s
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
166 lines
5.6 KiB
Python
166 lines
5.6 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""apply_guide.py — write one draft guide into templates/<app>/.felhom.yml, by TEXT, and prove the parse.
|
|
|
|
python3 apply_guide.py <catalog-root> <draft.json> [--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))
|