"""Build a learner-facing single-column page for a completed textbook unit.
The builder keeps internal project governance out of the page while reusing the
controlled Markdown converter and the Adobe-style local study interface.
"""
from __future__ import annotations
from html import escape
from pathlib import Path
import re
import sys
from xml.etree import ElementTree
ROOT = Path(__file__).resolve().parents[1]
MODULE_ROOT = ROOT / "04_modules"
PAGE_ROOT = ROOT / "06_web/pages"
SVG_ROOT = ROOT / "06_web/assets/svg"
sys.path.insert(0, str(ROOT / "scripts"))
from build_week1_html import between, markdown_blocks # noqa: E402
UNIT_META = {
1: ("Entering the Case", "When a creator's personal visibility begins to look like a business opportunity, what management questions appear first?"),
2: ("Managerial Roles", "What changes when the person who creates attention becomes responsible for people, money, and delivery?"),
3: ("Decision Making", "How should managers decide when evidence is incomplete, contradictory, or strategically biased?"),
4: ("External Environment and Cross-Border Markets", "Why can the same opportunity look attractive in one market and fragile in another?"),
5: ("Goals and Planning", "How can a vague ambition become a goal that guides resource allocation?"),
6: ("Strategy and Diversification", "When does diversification create synergy, and when does it create managerial overload?"),
7: ("Core Capabilities", "Which publicly described resources could become capabilities, and what evidence would test that claim?"),
8: ("Organization Design and HR", "What systems are required when an individual creator manages a cross-border performance team?"),
9: ("Midterm Progress Review", "Can the current evidence support a defensible company direction?"),
10: ("Teams and Communication", "How does a founder-led team replace informal coordination with a system that can scale?"),
11: ("Operations and Content Quality", "How can creative and performance work become reliable without becoming mechanically standardized?"),
12: ("Marketing and Customers", "Who pays, who uses, who influences, and who bears the risk?"),
13: ("Cash Management", "Why can visibility and revenue growth still create cash pressure?"),
14: ("Motivation and Leadership", "How can a founder move from emotional trust to fair and explicit leadership?"),
15: ("Ethics, Risk, and Change", "When does trust become an organizational asset, and when does it become a liability?"),
16: ("Control and Management Dashboard", "What should managers measure when visibility, revenue, people, quality, and trust all matter?"),
17: ("Final Case Analysis", "Which growth path should Judy choose, and what management system would make that path viable?"),
18: ("Company-Building Blueprint", "How can a case recommendation become a system that another manager could actually inherit?"),
}
SVG_BY_WEEK = {
1: "evidence-inference-decision.svg",
2: "managerial-role-map.svg",
3: "decision-memo-flow.svg",
4: "week-04-concept-map.svg",
5: "goals-planning-architecture.svg",
6: "strategy-diversification-architecture.svg",
}
FIGURE_RELATIONSHIPS = {
1: "Evidence moves to inference, and inference moves to a decision made under stated uncertainty. The evidence stage records source, date, and scope; the inference stage records reasoning and limits; the decision stage records the choice, trade-off, and review point.",
2: "Creator activity flows into managerial coordination, then into explicit decision rights, and finally into review and care. The four stages move from meaning, performance, and audience to people, resources, and information; then to who prepares, recommends, and decides; and finally to feedback, escalation, and learning.",
3: "The sequence is Frame the choice, Grade evidence, Compare options, Choose a boundary, and Review and learn. Each stage adds a distinct record: owner, scope, and stakes; source status and gaps; criteria and thresholds; pilot, stop, and support conditions; then the signal, date, and change rule for review.",
4: "External conditions are examined through state, effect, and response uncertainty. Foreignness, outsider position, language, and platform conditions then feed a locally validated entry hypothesis, which carries evidence and safeguards into Weeks 5 through 9.",
5: "Purpose feeds a strategic goal, an operating goal, and resource allocation. Measures and thresholds support review and adaptation, while non-goals and safeguards constrain every stage of the planning cycle.",
6: "The manager moves from portfolio scope to a value-transfer mechanism, then checks cost and risk before deciding whether to continue, revise, pause, or exit. Evidence, safeguards, and a reversible next step strengthen every link.",
7: "Resources become a capability through a repeatable routine and an organisation able to support it. Learning and renewal then sense, adapt, branch, or reconfigure the capability, while the VRIO questions test whether it remains valuable, rare, hard to copy, and organised.",
8: "Defined work determines the information that roles need. Information and decision rights support accountability, while people protections and safeguards make voice, correction, and learning possible.",
9: "The evidence audit moves from observation to interpretation, a conditional hypothesis, a designed action, and a review decision. A dependency audit checks evidence, uncertainty, consequences, and the next information request across the sequence.",
10: "Shared knowledge flows to language access, then to decision rights and safe voice. Shared knowledge also feeds meeting and asynchronous routines; decision rights and safe voice feed the correctable record. The meeting routine and record then lead to the review question: can another manager act without guessing?",
11: "A customer promise is translated into a service blueprint. Protection rules and creative latitude shape the work; capacity and handoffs organise delivery; recovery and learning feed revisions back into the operating system.",
12: "The value proposition sits at the centre of relationships with users, payers, influencers, partners, workers, platforms, and risk bearers. Each link must be tested for value, authority, contribution, access, and risk rather than assumed.",
13: "The customer promise flows to receipts, payments, and timing risk. Receipts feed working capital; payments and timing risk shape the liquidity buffer. Working capital and the buffer then inform a strategic choice that considers cash, capacity, value, and risk together.",
14: "Work design shapes motivation. Fair process and voice support psychological safety, which helps accountable authority explain and repair decisions. These conditions jointly support sustainable performance and trust.",
15: "A promise affects stakeholders and creates duties and risks. Voice and controls provide routes to question, refuse, report, and correct; remedy and review turn those responses into responsible change.",
16: "Strategy and risk determine leading signals and lagging results. The manager then asks what the measures hide, reviews exceptions and remedies, and revises the measure, process, or strategy when the control system distorts behaviour.",
17: "Evidence and criteria feed a comparison of three strategic options. Disconfirmation risks and stop signals shape a staged choice; the pilot is then reviewed before the manager revises or scales it.",
18: "Purpose and strategic choices feed roles and routines, then controls and learning, and finally an inheritable system. Review, adaptation, documentation, and testing keep the blueprint revisable rather than fixed.",
}
INSTRUCTOR_ONLY_BLOCK = re.compile(
r"<!--\s*instructor-only:start\s*-->.*?"
r"<!--\s*instructor-only:end\s*-->",
flags=re.I | re.S,
)
def learner_markdown(markdown: str) -> str:
"""Remove explicitly delimited instructor material before learner rendering."""
return INSTRUCTOR_ONLY_BLOCK.sub("", markdown)
def slug(text: str) -> str:
return re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-")
def heading_anchor(markdown: str, phrase: str, fallback: str) -> str:
"""Return the first heading anchor containing a required learner-facing phrase."""
for line in markdown.splitlines():
raw = line.strip()
if not raw.startswith("#"):
continue
title = raw.lstrip("#").strip()
plain = re.sub(r"[*_`]", "", title)
if phrase.lower() in plain.lower():
return slug(plain)
return fallback
def svg_text_alternative(svg: str, week: int) -> str:
"""Build an equivalent, scalable HTML account from the controlled SVG labels."""
root = ElementTree.fromstring(svg)
title = "Concept architecture"
description = "The diagram summarises the management relationships in this unit."
labels: list[str] = []
for node in root.iter():
tag = node.tag.rsplit("}", 1)[-1]
value = " ".join("".join(node.itertext()).split())
if tag == "title" and value:
title = value
elif tag == "desc" and value:
description = value
elif tag == "text" and value and value not in labels:
labels.append(value)
label_items = "".join(f"<li>{escape(label)}</li>" for label in labels)
relationship = FIGURE_RELATIONSHIPS.get(week, description)
return (
f'<div class="concept-figure__alternative" id="figure-{week}-description">'
f'<h3>Text version: {escape(title)}</h3>'
f'<p>{escape(description)}</p>'
f'<p><strong>How the elements connect:</strong> {escape(relationship)}</p>'
f'<h4>Labels in reading order</h4><ol class="concept-figure__labels">{label_items}</ol>'
"</div>"
)
def build(week: int) -> Path:
if week not in UNIT_META:
raise ValueError(f"Unknown week: {week}")
title, question = UNIT_META[week]
module = MODULE_ROOT / f"week-{week:02d}"
page = PAGE_ROOT / f"week-{week:02d}.html"
svg_name = SVG_BY_WEEK.get(week, f"week-{week:02d}-concept-map.svg")
svg_path = SVG_ROOT / svg_name
required = [
ROOT / "06_web/css/textbook.css",
ROOT / "06_web/js/textbook-reader-v2.js",
module / "manuscript.md",
module / "glossary.md",
module / "assessment.md",
module / "resources.md",
svg_path,
]
missing = [p for p in required if not p.is_file()]
if missing:
raise FileNotFoundError("Missing unit input: " + ", ".join(map(str, missing)))
manuscript = (module / "manuscript.md").read_text(encoding="utf-8")
body = learner_markdown(between(manuscript, "<!-- main-body:start -->", "<!-- main-body:end -->"))
glossary = learner_markdown((module / "glossary.md").read_text(encoding="utf-8"))
assessment = learner_markdown((module / "assessment.md").read_text(encoding="utf-8"))
resources = learner_markdown((module / "resources.md").read_text(encoding="utf-8"))
svg = svg_path.read_text(encoding="utf-8").strip()
body_html = markdown_blocks(body, heading_offset=0)
learning_anchor = heading_anchor(body, "learning map", "reading")
no_ai_anchor = heading_anchor(body, "no ai task", "reading")
figure_alternative = svg_text_alternative(svg, week)
glossary_html = markdown_blocks(glossary, heading_offset=1)
assessment_html = markdown_blocks(assessment, heading_offset=1)
resources_html = markdown_blocks(resources, heading_offset=1)
prev_link = f'week-{week - 1:02d}.html' if week > 1 else '../index.html'
prev_label = f'← Week {week - 1} · {UNIT_META[week - 1][0]}' if week > 1 else '← Course home'
next_link = f'week-{week + 1:02d}.html' if week < 18 else '../index.html'
next_label = f'Week {week + 1} · {UNIT_META[week + 1][0]} →' if week < 18 else 'Course home →'
html = f'''<!doctype html>
<html lang="en" data-font-size="m">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="Week {week} of CM108G Become Judy: {escape(title)}.">
<title>Week {week} · {escape(title)} | Become Judy</title>
<link rel="icon" href="data:,">
<link rel="stylesheet" href="../css/textbook.css">
<script defer src="../js/textbook-reader-v2.js"></script>
</head>
<body>
<a class="skip-link" href="#reading">Skip to main reading</a>
<header class="site-header"><div class="site-header__inner"><span class="wordmark">Become Judy</span><span class="course-label">CM108G · Week {week}</span></div></header>
<div class="page-shell">
<nav class="local-nav" aria-label="Week {week} contents">
<p class="local-nav__title">On this page</p>
<ol>
<li><a href="#reading">Reading</a></li>
<li><a href="#{learning_anchor}">Learning map</a></li>
<li><a href="#study-tools">Study tools</a></li>
<li><a href="#{no_ai_anchor}">No AI task</a></li>
<li><a href="#concept-architecture">Concept architecture</a></li>
<li><a href="#glossary-section">Glossary</a></li>
<li><a href="#assessment-section">Assessment</a></li>
<li><a href="#resources-section">References</a></li>
</ol>
<div class="font-tools"><span class="font-tools__label" id="font-label">Reading size</span><div class="font-tools__buttons" role="group" aria-labelledby="font-label"><button class="font-button" type="button" data-font-size="xs" aria-pressed="false">XS</button><button class="font-button" type="button" data-font-size="s" aria-pressed="false">S</button><button class="font-button" type="button" data-font-size="m" aria-pressed="true">M</button><button class="font-button" type="button" data-font-size="l" aria-pressed="false">L</button><button class="font-button" type="button" data-font-size="xl" aria-pressed="false">XL</button></div></div>
</nav>
<main>
<p class="kicker">Week {week} · {escape(title)}</p>
<h1>{escape(title)}</h1>
<p class="dek">{escape(question)}</p>
<div class="meta-line"><span>CM108G · Become Judy</span><span>5,000+ word reading</span></div>
<hr class="rule">
<section id="study-tools" class="study-tools" data-study-tools data-storage-key="cm108g-week-{week:02d}-notes-v1" aria-labelledby="study-tools-title">
<h2 id="study-tools-title">Study tools</h2>
<p class="study-tools__intro">Select text in the reading to highlight it, underline it, or add a personal note. Your study record stays in this browser and can be exported for your own use.</p>
<div class="study-tools__row"><button class="study-tool-button" type="button" data-study-action="toggle-notes">My notes <span data-note-count>0</span></button><button class="study-tool-button" type="button" data-study-action="print">Print</button><button class="study-tool-button" type="button" data-study-action="open-export">Export</button></div>
<p class="study-tools__status" data-study-status role="status" aria-live="polite">Select a passage in the main reading to begin.</p>
<p class="study-tools__privacy">Nothing is uploaded. Clearing browser data may remove locally saved notes, so download a copy when you need to keep your work.</p>
</section>
<div id="selection-tools" class="selection-tools" role="toolbar" aria-label="Selected text tools" hidden><span data-selection-count class="selection-count"></span><button class="study-tool-button" type="button" data-study-action="highlight">Highlight</button><button class="study-tool-button" type="button" data-study-action="underline">Underline</button><button class="study-tool-button" type="button" data-study-action="show-note">Add note</button><button class="study-tool-button study-tool-button--dismiss" type="button" data-study-action="dismiss-selection" aria-label="Close selected text tools">×</button></div>
<section id="reading" class="reading-body" data-count="main-body" aria-label="Week {week} main reading">{body_html}</section>
<section id="concept-architecture" aria-labelledby="concept-title"><h2 id="concept-title">Concept architecture</h2><figure class="concept-figure" aria-describedby="figure-{week}-description"><div class="concept-figure__graphic" tabindex="0" role="region" aria-label="Scrollable concept diagram. On a narrow screen, use horizontal scrolling to inspect the complete figure.">{svg}</div><figcaption class="figure-caption">Figure {week}. A compact map of the management relationships developed in this unit. A complete text version follows the diagram.</figcaption>{figure_alternative}</figure></section>
<section id="glossary-section" aria-label="Glossary">{glossary_html}</section>
<section id="assessment-section" aria-label="Assessment and reflection">{assessment_html}</section>
<section id="resources-section" aria-label="Resources and APA 7 references">{resources_html}</section>
<dialog id="annotation-dialog" class="annotation-dialog" aria-labelledby="annotation-dialog-title"><header><h2 id="annotation-dialog-title">Add a personal note</h2><button class="study-tool-button study-tool-button--dismiss" type="button" data-study-action="close-editor" aria-label="Close note editor">×</button></header><div class="annotation-dialog__body"><blockquote data-selected-quote></blockquote><label for="annotation-text">Your note <span>(optional)</span></label><textarea id="annotation-text" data-note-text placeholder="Write a question, judgment, or discussion point…"></textarea><fieldset class="color-options"><legend>Mark colour</legend><label><input type="radio" name="mark-color" value="yellow" checked> Yellow</label><label><input type="radio" name="mark-color" value="blue"> Blue</label><label><input type="radio" name="mark-color" value="pink"> Pink</label></fieldset></div><footer><button class="study-tool-button" type="button" data-study-action="cancel-note">Cancel</button><button class="study-tool-button study-tool-button--primary" type="button" data-study-action="save-note">Save note</button></footer></dialog>
<aside id="notes-panel" class="notes-panel" role="dialog" aria-modal="true" aria-labelledby="notes-panel-title" hidden><header><h2 id="notes-panel-title">My notes · <span data-note-count>0</span></h2><button class="study-tool-button study-tool-button--dismiss" type="button" data-study-action="close-notes" aria-label="Close my notes">×</button></header><p class="panel-help">Highlights and notes are listed here. Use “Return to text” to locate a saved item.</p><div class="note-list" data-note-list><p class="empty-state">Select text in the reading, then choose Highlight or Add note.</p></div><div class="panel-bottom"><label for="export-format">Export format</label><select id="export-format" data-export-format><option value="md">Markdown study notes</option><option value="json">JSON study record</option><option value="html">HTML reading with marks</option></select><button class="study-tool-button study-tool-button--primary" type="button" data-study-action="download-notes">Download</button><button class="study-tool-button" type="button" data-study-action="download-reading">Download reading</button><button class="study-tool-button study-tool-button--danger" type="button" data-study-action="clear-record">Clear saved record</button></div></aside>
<section class="instructor-note" aria-label="Instructor contact"><p><strong>Course note:</strong> If you find an error or need further explanation, contact the course instructor. Case statements are limited to the documented public-source boundary, and designed situations are labelled fictional-composite.</p></section>
<nav class="page-nav" aria-label="Unit navigation"><a href="{prev_link}">{escape(prev_label)}</a><a href="{next_link}">{escape(next_label)}</a></nav>
</main>
</div>
<footer class="site-footer">CM108G Become Judy · Week {week}</footer>
</body>
</html>
'''
page.parent.mkdir(parents=True, exist_ok=True)
page.write_text(html, encoding="utf-8", newline="\n")
return page
if __name__ == "__main__":
if len(sys.argv) != 2 or not sys.argv[1].isdigit():
raise SystemExit("Usage: python scripts/build_unit_html.py WEEK")
result = build(int(sys.argv[1]))
print(f"Built {result}")
1"""Build a learner-facing single-column page for a completed textbook unit.
3The builder keeps internal project governance out of the page while reusing the
4controlled Markdown converter and the Adobe-style local study interface.
5"""
6from __future__ import annotations
8from html import escape
9from pathlib import Path
10import re
11import sys
12from xml.etree import ElementTree
14ROOT = Path(__file__).resolve().parents[1]
15MODULE_ROOT = ROOT / "04_modules"
16PAGE_ROOT = ROOT / "06_web/pages"
17SVG_ROOT = ROOT / "06_web/assets/svg"
18sys.path.insert(0, str(ROOT / "scripts"))
19from build_week1_html import between, markdown_blocks # noqa: E402
21UNIT_META = {
22 1: ("Entering the Case", "When a creator's personal visibility begins to look like a business opportunity, what management questions appear first?"),
23 2: ("Managerial Roles", "What changes when the person who creates attention becomes responsible for people, money, and delivery?"),
24 3: ("Decision Making", "How should managers decide when evidence is incomplete, contradictory, or strategically biased?"),
25 4: ("External Environment and Cross-Border Markets", "Why can the same opportunity look attractive in one market and fragile in another?"),
26 5: ("Goals and Planning", "How can a vague ambition become a goal that guides resource allocation?"),
27 6: ("Strategy and Diversification", "When does diversification create synergy, and when does it create managerial overload?"),
28 7: ("Core Capabilities", "Which publicly described resources could become capabilities, and what evidence would test that claim?"),
29 8: ("Organization Design and HR", "What systems are required when an individual creator manages a cross-border performance team?"),
30 9: ("Midterm Progress Review", "Can the current evidence support a defensible company direction?"),
31 10: ("Teams and Communication", "How does a founder-led team replace informal coordination with a system that can scale?"),
32 11: ("Operations and Content Quality", "How can creative and performance work become reliable without becoming mechanically standardized?"),
33 12: ("Marketing and Customers", "Who pays, who uses, who influences, and who bears the risk?"),
34 13: ("Cash Management", "Why can visibility and revenue growth still create cash pressure?"),
35 14: ("Motivation and Leadership", "How can a founder move from emotional trust to fair and explicit leadership?"),
36 15: ("Ethics, Risk, and Change", "When does trust become an organizational asset, and when does it become a liability?"),
37 16: ("Control and Management Dashboard", "What should managers measure when visibility, revenue, people, quality, and trust all matter?"),
38 17: ("Final Case Analysis", "Which growth path should Judy choose, and what management system would make that path viable?"),
39 18: ("Company-Building Blueprint", "How can a case recommendation become a system that another manager could actually inherit?"),
40}
42SVG_BY_WEEK = {
43 1: "evidence-inference-decision.svg",
44 2: "managerial-role-map.svg",
45 3: "decision-memo-flow.svg",
46 4: "week-04-concept-map.svg",
47 5: "goals-planning-architecture.svg",
48 6: "strategy-diversification-architecture.svg",
49}
51FIGURE_RELATIONSHIPS = {
52 1: "Evidence moves to inference, and inference moves to a decision made under stated uncertainty. The evidence stage records source, date, and scope; the inference stage records reasoning and limits; the decision stage records the choice, trade-off, and review point.",
53 2: "Creator activity flows into managerial coordination, then into explicit decision rights, and finally into review and care. The four stages move from meaning, performance, and audience to people, resources, and information; then to who prepares, recommends, and decides; and finally to feedback, escalation, and learning.",
54 3: "The sequence is Frame the choice, Grade evidence, Compare options, Choose a boundary, and Review and learn. Each stage adds a distinct record: owner, scope, and stakes; source status and gaps; criteria and thresholds; pilot, stop, and support conditions; then the signal, date, and change rule for review.",
55 4: "External conditions are examined through state, effect, and response uncertainty. Foreignness, outsider position, language, and platform conditions then feed a locally validated entry hypothesis, which carries evidence and safeguards into Weeks 5 through 9.",
56 5: "Purpose feeds a strategic goal, an operating goal, and resource allocation. Measures and thresholds support review and adaptation, while non-goals and safeguards constrain every stage of the planning cycle.",
57 6: "The manager moves from portfolio scope to a value-transfer mechanism, then checks cost and risk before deciding whether to continue, revise, pause, or exit. Evidence, safeguards, and a reversible next step strengthen every link.",
58 7: "Resources become a capability through a repeatable routine and an organisation able to support it. Learning and renewal then sense, adapt, branch, or reconfigure the capability, while the VRIO questions test whether it remains valuable, rare, hard to copy, and organised.",
59 8: "Defined work determines the information that roles need. Information and decision rights support accountability, while people protections and safeguards make voice, correction, and learning possible.",
60 9: "The evidence audit moves from observation to interpretation, a conditional hypothesis, a designed action, and a review decision. A dependency audit checks evidence, uncertainty, consequences, and the next information request across the sequence.",
61 10: "Shared knowledge flows to language access, then to decision rights and safe voice. Shared knowledge also feeds meeting and asynchronous routines; decision rights and safe voice feed the correctable record. The meeting routine and record then lead to the review question: can another manager act without guessing?",
62 11: "A customer promise is translated into a service blueprint. Protection rules and creative latitude shape the work; capacity and handoffs organise delivery; recovery and learning feed revisions back into the operating system.",
63 12: "The value proposition sits at the centre of relationships with users, payers, influencers, partners, workers, platforms, and risk bearers. Each link must be tested for value, authority, contribution, access, and risk rather than assumed.",
64 13: "The customer promise flows to receipts, payments, and timing risk. Receipts feed working capital; payments and timing risk shape the liquidity buffer. Working capital and the buffer then inform a strategic choice that considers cash, capacity, value, and risk together.",
65 14: "Work design shapes motivation. Fair process and voice support psychological safety, which helps accountable authority explain and repair decisions. These conditions jointly support sustainable performance and trust.",
66 15: "A promise affects stakeholders and creates duties and risks. Voice and controls provide routes to question, refuse, report, and correct; remedy and review turn those responses into responsible change.",
67 16: "Strategy and risk determine leading signals and lagging results. The manager then asks what the measures hide, reviews exceptions and remedies, and revises the measure, process, or strategy when the control system distorts behaviour.",
68 17: "Evidence and criteria feed a comparison of three strategic options. Disconfirmation risks and stop signals shape a staged choice; the pilot is then reviewed before the manager revises or scales it.",
69 18: "Purpose and strategic choices feed roles and routines, then controls and learning, and finally an inheritable system. Review, adaptation, documentation, and testing keep the blueprint revisable rather than fixed.",
70}
72INSTRUCTOR_ONLY_BLOCK = re.compile(
73 r"<!--\s*instructor-only:start\s*-->.*?"
74 r"<!--\s*instructor-only:end\s*-->",
75 flags=re.I | re.S,
76)
79def learner_markdown(markdown: str) -> str:
80 """Remove explicitly delimited instructor material before learner rendering."""
81 return INSTRUCTOR_ONLY_BLOCK.sub("", markdown)
84def slug(text: str) -> str:
85 return re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-")
88def heading_anchor(markdown: str, phrase: str, fallback: str) -> str:
89 """Return the first heading anchor containing a required learner-facing phrase."""
90 for line in markdown.splitlines():
91 raw = line.strip()
92 if not raw.startswith("#"):
93 continue
94 title = raw.lstrip("#").strip()
95 plain = re.sub(r"[*_`]", "", title)
96 if phrase.lower() in plain.lower():
97 return slug(plain)
98 return fallback
101def svg_text_alternative(svg: str, week: int) -> str:
102 """Build an equivalent, scalable HTML account from the controlled SVG labels."""
103 root = ElementTree.fromstring(svg)
104 title = "Concept architecture"
105 description = "The diagram summarises the management relationships in this unit."
106 labels: list[str] = []
107 for node in root.iter():
108 tag = node.tag.rsplit("}", 1)[-1]
109 value = " ".join("".join(node.itertext()).split())
110 if tag == "title" and value:
111 title = value
112 elif tag == "desc" and value:
113 description = value
114 elif tag == "text" and value and value not in labels:
115 labels.append(value)
116 label_items = "".join(f"<li>{escape(label)}</li>" for label in labels)
117 relationship = FIGURE_RELATIONSHIPS.get(week, description)
118 return (
119 f'<div class="concept-figure__alternative" id="figure-{week}-description">'
120 f'<h3>Text version: {escape(title)}</h3>'
121 f'<p>{escape(description)}</p>'
122 f'<p><strong>How the elements connect:</strong> {escape(relationship)}</p>'
123 f'<h4>Labels in reading order</h4><ol class="concept-figure__labels">{label_items}</ol>'
124 "</div>"
125 )
128def build(week: int) -> Path:
129 if week not in UNIT_META:
130 raise ValueError(f"Unknown week: {week}")
131 title, question = UNIT_META[week]
132 module = MODULE_ROOT / f"week-{week:02d}"
133 page = PAGE_ROOT / f"week-{week:02d}.html"
134 svg_name = SVG_BY_WEEK.get(week, f"week-{week:02d}-concept-map.svg")
135 svg_path = SVG_ROOT / svg_name
136 required = [
137 ROOT / "06_web/css/textbook.css",
138 ROOT / "06_web/js/textbook-reader-v2.js",
139 module / "manuscript.md",
140 module / "glossary.md",
141 module / "assessment.md",
142 module / "resources.md",
143 svg_path,
144 ]
145 missing = [p for p in required if not p.is_file()]
146 if missing:
147 raise FileNotFoundError("Missing unit input: " + ", ".join(map(str, missing)))
148 manuscript = (module / "manuscript.md").read_text(encoding="utf-8")
149 body = learner_markdown(between(manuscript, "<!-- main-body:start -->", "<!-- main-body:end -->"))
150 glossary = learner_markdown((module / "glossary.md").read_text(encoding="utf-8"))
151 assessment = learner_markdown((module / "assessment.md").read_text(encoding="utf-8"))
152 resources = learner_markdown((module / "resources.md").read_text(encoding="utf-8"))
153 svg = svg_path.read_text(encoding="utf-8").strip()
154 body_html = markdown_blocks(body, heading_offset=0)
155 learning_anchor = heading_anchor(body, "learning map", "reading")
156 no_ai_anchor = heading_anchor(body, "no ai task", "reading")
157 figure_alternative = svg_text_alternative(svg, week)
158 glossary_html = markdown_blocks(glossary, heading_offset=1)
159 assessment_html = markdown_blocks(assessment, heading_offset=1)
160 resources_html = markdown_blocks(resources, heading_offset=1)
161 prev_link = f'week-{week - 1:02d}.html' if week > 1 else '../index.html'
162 prev_label = f'← Week {week - 1} · {UNIT_META[week - 1][0]}' if week > 1 else '← Course home'
163 next_link = f'week-{week + 1:02d}.html' if week < 18 else '../index.html'
164 next_label = f'Week {week + 1} · {UNIT_META[week + 1][0]} →' if week < 18 else 'Course home →'
165 html = f'''<!doctype html>
166<html lang="en" data-font-size="m">
167<head>
168 <meta charset="utf-8">
169 <meta name="viewport" content="width=device-width, initial-scale=1">
170 <meta name="description" content="Week {week} of CM108G Become Judy: {escape(title)}.">
171 <title>Week {week} · {escape(title)} | Become Judy</title>
172 <link rel="icon" href="data:,">
173 <link rel="stylesheet" href="../css/textbook.css">
174 <script defer src="../js/textbook-reader-v2.js"></script>
175</head>
176<body>
177 <a class="skip-link" href="#reading">Skip to main reading</a>
178 <header class="site-header"><div class="site-header__inner"><span class="wordmark">Become Judy</span><span class="course-label">CM108G · Week {week}</span></div></header>
179 <div class="page-shell">
180 <nav class="local-nav" aria-label="Week {week} contents">
181 <p class="local-nav__title">On this page</p>
182 <ol>
183 <li><a href="#reading">Reading</a></li>
184 <li><a href="#{learning_anchor}">Learning map</a></li>
185 <li><a href="#study-tools">Study tools</a></li>
186 <li><a href="#{no_ai_anchor}">No AI task</a></li>
187 <li><a href="#concept-architecture">Concept architecture</a></li>
188 <li><a href="#glossary-section">Glossary</a></li>
189 <li><a href="#assessment-section">Assessment</a></li>
190 <li><a href="#resources-section">References</a></li>
191 </ol>
192 <div class="font-tools"><span class="font-tools__label" id="font-label">Reading size</span><div class="font-tools__buttons" role="group" aria-labelledby="font-label"><button class="font-button" type="button" data-font-size="xs" aria-pressed="false">XS</button><button class="font-button" type="button" data-font-size="s" aria-pressed="false">S</button><button class="font-button" type="button" data-font-size="m" aria-pressed="true">M</button><button class="font-button" type="button" data-font-size="l" aria-pressed="false">L</button><button class="font-button" type="button" data-font-size="xl" aria-pressed="false">XL</button></div></div>
193 </nav>
194 <main>
195 <p class="kicker">Week {week} · {escape(title)}</p>
196 <h1>{escape(title)}</h1>
197 <p class="dek">{escape(question)}</p>
198 <div class="meta-line"><span>CM108G · Become Judy</span><span>5,000+ word reading</span></div>
199 <hr class="rule">
200 <section id="study-tools" class="study-tools" data-study-tools data-storage-key="cm108g-week-{week:02d}-notes-v1" aria-labelledby="study-tools-title">
201 <h2 id="study-tools-title">Study tools</h2>
202 <p class="study-tools__intro">Select text in the reading to highlight it, underline it, or add a personal note. Your study record stays in this browser and can be exported for your own use.</p>
203 <div class="study-tools__row"><button class="study-tool-button" type="button" data-study-action="toggle-notes">My notes <span data-note-count>0</span></button><button class="study-tool-button" type="button" data-study-action="print">Print</button><button class="study-tool-button" type="button" data-study-action="open-export">Export</button></div>
204 <p class="study-tools__status" data-study-status role="status" aria-live="polite">Select a passage in the main reading to begin.</p>
205 <p class="study-tools__privacy">Nothing is uploaded. Clearing browser data may remove locally saved notes, so download a copy when you need to keep your work.</p>
206 </section>
207 <div id="selection-tools" class="selection-tools" role="toolbar" aria-label="Selected text tools" hidden><span data-selection-count class="selection-count"></span><button class="study-tool-button" type="button" data-study-action="highlight">Highlight</button><button class="study-tool-button" type="button" data-study-action="underline">Underline</button><button class="study-tool-button" type="button" data-study-action="show-note">Add note</button><button class="study-tool-button study-tool-button--dismiss" type="button" data-study-action="dismiss-selection" aria-label="Close selected text tools">×</button></div>
208 <section id="reading" class="reading-body" data-count="main-body" aria-label="Week {week} main reading">{body_html}</section>
209 <section id="concept-architecture" aria-labelledby="concept-title"><h2 id="concept-title">Concept architecture</h2><figure class="concept-figure" aria-describedby="figure-{week}-description"><div class="concept-figure__graphic" tabindex="0" role="region" aria-label="Scrollable concept diagram. On a narrow screen, use horizontal scrolling to inspect the complete figure.">{svg}</div><figcaption class="figure-caption">Figure {week}. A compact map of the management relationships developed in this unit. A complete text version follows the diagram.</figcaption>{figure_alternative}</figure></section>
210 <section id="glossary-section" aria-label="Glossary">{glossary_html}</section>
211 <section id="assessment-section" aria-label="Assessment and reflection">{assessment_html}</section>
212 <section id="resources-section" aria-label="Resources and APA 7 references">{resources_html}</section>
213 <dialog id="annotation-dialog" class="annotation-dialog" aria-labelledby="annotation-dialog-title"><header><h2 id="annotation-dialog-title">Add a personal note</h2><button class="study-tool-button study-tool-button--dismiss" type="button" data-study-action="close-editor" aria-label="Close note editor">×</button></header><div class="annotation-dialog__body"><blockquote data-selected-quote></blockquote><label for="annotation-text">Your note <span>(optional)</span></label><textarea id="annotation-text" data-note-text placeholder="Write a question, judgment, or discussion point…"></textarea><fieldset class="color-options"><legend>Mark colour</legend><label><input type="radio" name="mark-color" value="yellow" checked> Yellow</label><label><input type="radio" name="mark-color" value="blue"> Blue</label><label><input type="radio" name="mark-color" value="pink"> Pink</label></fieldset></div><footer><button class="study-tool-button" type="button" data-study-action="cancel-note">Cancel</button><button class="study-tool-button study-tool-button--primary" type="button" data-study-action="save-note">Save note</button></footer></dialog>
214 <aside id="notes-panel" class="notes-panel" role="dialog" aria-modal="true" aria-labelledby="notes-panel-title" hidden><header><h2 id="notes-panel-title">My notes · <span data-note-count>0</span></h2><button class="study-tool-button study-tool-button--dismiss" type="button" data-study-action="close-notes" aria-label="Close my notes">×</button></header><p class="panel-help">Highlights and notes are listed here. Use “Return to text” to locate a saved item.</p><div class="note-list" data-note-list><p class="empty-state">Select text in the reading, then choose Highlight or Add note.</p></div><div class="panel-bottom"><label for="export-format">Export format</label><select id="export-format" data-export-format><option value="md">Markdown study notes</option><option value="json">JSON study record</option><option value="html">HTML reading with marks</option></select><button class="study-tool-button study-tool-button--primary" type="button" data-study-action="download-notes">Download</button><button class="study-tool-button" type="button" data-study-action="download-reading">Download reading</button><button class="study-tool-button study-tool-button--danger" type="button" data-study-action="clear-record">Clear saved record</button></div></aside>
215 <section class="instructor-note" aria-label="Instructor contact"><p><strong>Course note:</strong> If you find an error or need further explanation, contact the course instructor. Case statements are limited to the documented public-source boundary, and designed situations are labelled fictional-composite.</p></section>
216 <nav class="page-nav" aria-label="Unit navigation"><a href="{prev_link}">{escape(prev_label)}</a><a href="{next_link}">{escape(next_label)}</a></nav>
217 </main>
218 </div>
219 <footer class="site-footer">CM108G Become Judy · Week {week}</footer>
220</body>
221</html>
222'''
223 page.parent.mkdir(parents=True, exist_ok=True)
224 page.write_text(html, encoding="utf-8", newline="\n")
225 return page
228if __name__ == "__main__":
229 if len(sys.argv) != 2 or not sys.argv[1].isdigit():
230 raise SystemExit("Usage: python scripts/build_unit_html.py WEEK")
231 result = build(int(sys.argv[1]))
232 print(f"Built {result}")