"""Serialize translatable blocks into compact marker templates. Marker scheme (short => cheap to reproduce while translating): opaque inline element (code / img / kbd / abbr-with-code-text) - kept verbatim .. link (): inner text is translatable, anchor markup is restored later .. .. everything else inline is transparent (unwrapped) but its text is translatable """ import json, re, sys from bs4 import BeautifulSoup, NavigableString, Comment SRC = r"C:/Users/yvmou/Downloads/limboai-readthedocs-io-en-stable/limboai-stable/index.html" OUT = r"C:/Users/yvmou/Downloads/limboai-readthedocs-io-en-stable/limboai-stable/_i18n/blocks.json" BLOCK_TAGS = {"p", "h1", "h2", "h3", "h4", "h5", "h6", "td", "th", "dt", "dd", "figcaption", "caption", "li"} OPAQUE = {"code", "kbd", "samp", "var", "tt", "img", "pre", "script", "style", "math"} TRANSPARENT = {"span", "cite", "markdown", "mark", "small", "sub", "sup", "u", "s", "q"} class Ser: def __init__(self): self.markers = {} self.n = 0 def add(self, html): self.n += 1 self.markers[str(self.n)] = html return self.n def walk(self, node, out): for ch in node.children: if isinstance(ch, Comment): continue if isinstance(ch, NavigableString): s = re.sub(r"\s+", " ", str(ch)) if s: out.append(s) continue name = ch.name cls = ch.get("class") or [] if name == "a" and "headerlink" in cls: continue if name in OPAQUE or name == "abbr": out.append("<%d/>" % self.add(str(ch))) continue if name in TRANSPARENT: self.walk(ch, out) continue if name == "a": idx = self.add(str(ch)) out.append("" % idx) self.walk(ch, out) out.append("") continue if name == "strong": out.append("") self.walk(ch, out) out.append("") continue if name in ("em", "dfn"): out.append("") self.walk(ch, out) out.append("") continue self.walk(ch, out) def run(self, tag): out = [] self.walk(tag, out) tpl = "".join(out) tpl = re.sub(r"[ \t]*\n[ \t]*", "\n", tpl) tpl = re.sub(r"\n{2,}", "\n", tpl).strip() return tpl def serialize(node, kind, feed): s = Ser() tpl = s.run(node) if not re.search(r"[A-Za-z]", re.sub(r"<[^>]*>", "", tpl)): return feed.append({"kind": kind, "tag": node.name, "tpl": tpl, "markers": s.markers, "node": node}) def collect(soup): """Walk the document exactly the way the extractor did.""" feed = [] body = soup.find("div", {"role": "main"}) for node in body.find_all(True): if node.name not in BLOCK_TAGS: continue if node.name == "li" and node.find("p", recursive=False): continue for p in node.parents: if getattr(p, "name", None) in ("pre", "code"): break else: serialize(node, "body", feed) for cap in soup.select("p.caption span.caption-text"): serialize(cap, "nav", feed) for a in soup.select("div.wy-menu a, nav a"): if a.find_parent("div", class_="wy-menu") is None: continue serialize(a, "nav", feed) return feed if __name__ == "__main__": blocks = collect(BeautifulSoup(open(SRC, encoding="utf-8").read(), "lxml")) plain = [{"kind": b["kind"], "tag": b["tag"], "path": "", "html": b["tpl"], "text": re.sub(r"<[^>]*>", "", b["tpl"]), "markers": b["markers"]} for b in blocks] json.dump(plain, open(OUT, "w", encoding="utf-8"), ensure_ascii=False, indent=1) uniq = {} for b in blocks: uniq.setdefault(b["tpl"] + "\x00" + json.dumps(b["markers"], sort_keys=True), b) print("blocks:", len(blocks), "unique:", len(uniq), "unique tpl chars:", sum(len(b["tpl"]) for b in uniq.values()))