#!/usr/bin/env python3 """Copies analysis/*.md into the site's studies collection, normalising front matter. The analyses are authored as standalone research documents; some carry rich front matter (subtitle, abstract, status), some carry none at all. The site's content schema needs a title, a description and a sort order on every one. Rather than maintain two divergent copies, this regenerates the collection from analysis/ on every build. Documents listed in INTERNAL are working notes or build inputs and are not published. """ import os import re import sys ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) SRC = os.path.join(ROOT, "analysis") DST = os.path.join(ROOT, "site", "src", "content", "studies") # Not published: build inputs and unedited working notes. raw-* are the per-book profiling # notes the lens reports were built from — they are kept in the repo as evidence trail but # are not written for a reader. INTERNAL = {"design-research.md", "raw-profiles-social-studies.md", "raw-religion-social-studies.md", "historical-quotations.md"} # Reading order on /studies. Lower sorts first. Anything unlisted falls to the end # alphabetically, which is the right default for material added later. ORDER = { "synthesis.md": 1, "rubric.md": 2, "edition-audit.md": 3, "article-assessment-demand.md": 4, "psychological-implications.md": 5, "thread-mother-tongue.md": "Every board names its regional poets. Only Sindh teaches one as a set text — and no " "Punjabi poet appears in any of Punjab's five Urdu literary readers.", "thread-iconoclasm.md": 6, "thread-moral-rescue.md": 7, "thread-mother-tongue.md": 8, "lens-religion.md": 11, "lens-civics.md": 12, "lens-history.md": 13, "lens-patriotism.md": 14, "lens-othering.md": 15, "lens-gender.md": 16, "track-pakistan-studies.md": 21, "track-social-studies.md": 22, "track-islamiyat.md": 23, "track-urdu.md": 24, "track-english.md": 25, "track-sciences.md": 26, } # A negative result is a result, and the guideline requires it be published at equal # prominence. Equal prominence is not the same as identical presentation: a study that # went looking and found nothing needs to say so before a reader infers a positive. NEGATIVE = { "thread-mother-tongue.md": "Every board names its regional poets. Only Sindh teaches one as a set text — and no " "Punjabi poet appears in any of Punjab's five Urdu literary readers.", "thread-iconoclasm.md": "Tested and NOT established. No board reaches the threshold for an explicit link " "between Makkah's idols and Muslim conquest in India.", "thread-moral-rescue.md": "Tested and NOT established for the Indian side. Sati does not appear in any of the " "112 books read; the Arabian-side framing is examined, the Indian-side equivalent is not.", } DESCRIPTIONS = { "synthesis.md": "The headline comparison: how the five boards score across seven axes, and why two " "defensible composite indices rank them differently.", "rubric.md": "The seven-axis instrument, fixed before any book was read, with the scoring rules " "and the evidence standards every claim on this site must meet.", "edition-audit.md": "Proof that the books analysed here are the current ones — and the finding that " "curriculum vintage, not province, is the sharper divide.", "article-assessment-demand.md": "A journal-length article arguing that what pupils are required to write in " "examinations is a stronger instrument for reading a curriculum than what its " "textbooks say.", "psychological-implications.md": "Twenty-six falsifiable predictions about what these four curricula should produce in the " "pupils formed by them — attitudes, and then personality structure — each with the " "test that would refute it.", "thread-moral-rescue.md": "Testing whether pre-Islamic Arabia and pre-Islamic India are constructed as a " "shared barbarism from which Muslim rule delivered rescue.", "thread-mother-tongue.md": "Every board names its regional poets. Only Sindh teaches one as a set text — and no " "Punjabi poet appears in any of Punjab's five Urdu literary readers.", "thread-iconoclasm.md": "Does any board link the smashing of idols at Makkah to Muslim conquest in India? " "A four-tier evidence test, and what each province actually does.", "lens-religion.md": "How much religious content appears outside Islamiyat — in science, English, " "geography and history — and how far each board separates faith from subject matter.", "lens-civics.md": "What a pupil is taught about rights, democracy, law and the citizen's relationship " "to the state — and which province teaches them to judge rather than recall.", "lens-history.md": "Whose history is told, where each province starts the story, and what is left out " "of the account of Partition, 1971 and military rule.", "lens-patriotism.md": "Patriotism is taught everywhere in roughly equal volume. What differs is the kind: " "civic belonging, religious destiny, or military sacrifice.", "lens-othering.md": "How Hindus, India, the West and religious minorities are portrayed, with the " "quoted passages and the counter-examples each board also prints.", "lens-gender.md": "Women as agents, as subjects, or as absent — counted across the corpus, with the " "distinction between what is taught and who is depicted.", "track-pakistan-studies.md": "The subject that carries the national narrative, compared board by board across " "classes 9 and 10.", "track-social-studies.md": "The boards do not even agree what this subject is: one integrated volume in some " "provinces, split History and Geography in others.", "track-islamiyat.md": "Compulsory religious instruction compared across the boards that publish it, classes 6 to 10.", "track-urdu.md": "What the national-language reader teaches beyond language: the selection of " "authors, themes and moral lessons in each province.", "track-english.md": "The English reader as a vehicle for values — what its comprehension passages are " "about, and how that differs by province.", "track-sciences.md": "Biology, chemistry, physics and general science: how much religious framing " "enters, and how differently the provinces handle evolution and inquiry.", } # Cross-references in the analyses are written as file paths, because that is what they are on # disk. A reader does not have the disk. Rewrite them into links to the published page, but only # where the target is actually published — an INTERNAL document has no page to link to. _DOC_TITLES = { "synthesis": "the synthesis", "rubric": "the rubric", "edition-audit": "the edition audit", "lens-religion": "the religion lens", "lens-civics": "the civics lens", "lens-history": "the history lens", "lens-patriotism": "the patriotism lens", "lens-othering": "the othering lens", "lens-gender": "the gender lens", "track-pakistan-studies": "the Pakistan Studies track", "track-social-studies": "the Social Studies track", "track-islamiyat": "the Islamiyat track", "track-urdu": "the Urdu track", "track-english": "the English track", "track-sciences": "the sciences track", "thread-iconoclasm": "the iconoclasm thread", "thread-moral-rescue": "the moral-rescue thread", "article-assessment-demand": "What the Pupil Must Write", "psychological-implications": "What the Treatment Predicts", } def linkify_refs(text): """`analysis/lens-civics.md` and bare `lens-civics.md` -> a link to /studies/lens-civics/. A trailing section reference (§2.4) is carried into the link text so the pointer survives.""" def repl(m): stem = m.group("stem").lower() if stem not in _DOC_TITLES: return m.group(0) sect = (m.group("sect") or "").strip() label = _DOC_TITLES[stem] + (" " + sect if sect else "") return "[%s](/studies/%s/)" % (label, stem) pattern = re.compile( r"`?(?:analysis/)?(?P[A-Za-z0-9_-]+)\.md`?(?P\s*§\s*[\d.]+)?") out, last = [], 0 # Never rewrite inside a fenced code block or an existing markdown link target. protected = [(m.start(), m.end()) for m in re.finditer(r"```.*?```|\]\([^)]*\)", text, re.S)] for m in pattern.finditer(text): if any(a <= m.start() < b for a, b in protected): continue out.append(text[last:m.start()]) out.append(repl(m)) last = m.end() out.append(text[last:]) return "".join(out) def split_front_matter(text): """Returns (dict_of_scalar_keys, body). Block scalars (>-, |) are folded to one line.""" if not text.startswith("---"): return {}, text end = text.find("\n---", 3) if end == -1: return {}, text raw = text[3:end] body = text[end + 4:].lstrip("\n") meta, key = {}, None for line in raw.splitlines(): m = re.match(r"^(\w+):\s*(.*)$", line) if m: key, val = m.group(1), m.group(2).strip() meta[key] = "" if val in (">", "|", ">-", "|-") else val.strip('"\'') elif key and line.strip(): meta[key] = (meta[key] + " " + line.strip()).strip() return meta, body def first_sentence(s, limit=240): s = " ".join(s.split()) m = re.search(r"^(.{40,%d}?\.)(\s|$)" % limit, s) out = m.group(1) if m else s[:limit].rstrip() return out def yaml_quote(s): return '"%s"' % " ".join(s.split()).replace('\\', '\\\\').replace('"', '\\"') def main(): os.makedirs(DST, exist_ok=True) for f in os.listdir(DST): if f.endswith(".md"): os.remove(os.path.join(DST, f)) written = 0 for name in sorted(os.listdir(SRC)): if not name.endswith(".md") or name.lower() in INTERNAL: continue slug = name.lower() text = open(os.path.join(SRC, name), encoding="utf-8").read() meta, body = split_front_matter(text) title = meta.get("title") if not title: m = re.search(r"^#\s+(.+)$", body, re.M) title = m.group(1).strip() if m else name[:-3].replace("-", " ").capitalize() # The H1 duplicates the title the layout already renders. Drop the first one only. body = re.sub(r"^#\s+.+\n+", "", body, count=1) desc = (DESCRIPTIONS.get(slug) or meta.get("description") or meta.get("abstract") or meta.get("subtitle")) if not desc: para = next((p for p in body.split("\n\n") if p.strip() and not p.lstrip().startswith(("#", ">", "|", "-", "*"))), "") desc = re.sub(r"[*_`\[\]]|\(([^)]*)\)", "", para) desc = first_sentence(desc) # Real revision date, taken from the source document's mtime. A reference work with no # date on it cannot be cited properly — the reader cannot say which version they read. import datetime as _dt revised = _dt.date.fromtimestamp( os.path.getmtime(os.path.join(SRC, name))).isoformat() fm = ["---", "title: %s" % yaml_quote(title), "description: %s" % yaml_quote(desc), "revised: %s" % yaml_quote(revised), # quoted: bare ISO dates parse as YAML dates, not strings "order: %d" % ORDER.get(slug, 90)] if slug in NEGATIVE: fm.append("result: %s" % yaml_quote("negative")) fm.append("resultNote: %s" % yaml_quote(NEGATIVE[slug])) for k in ("subtitle", "status"): if meta.get(k): fm.append("%s: %s" % (k, yaml_quote(meta[k]))) fm.append("---\n") body = linkify_refs(body) open(os.path.join(DST, slug), "w", encoding="utf-8").write("\n".join(fm) + "\n" + body) written += 1 print(" %-38s order %-3s %s" % (slug, ORDER.get(slug, 90), desc[:60])) print("studies synced: %d" % written) if written == 0: sys.exit(1) if __name__ == "__main__": main()