Skip to content

Commit 0aa896f

Browse files
committed
fix(build-pages): Add proper nested list support with checkboxes
- Replace separate ordered/unordered list functions with unified convert_lists() - Support proper nesting of lists at any depth - Add checkbox parsing for task lists (- [ ] and - [x]) - Fix wrap_paragraphs to not wrap closing HTML tags - Regenerate assignment and syllabus HTML pages Also adds note for pending syllabus table column fixes.
1 parent 8a9929b commit 0aa896f

File tree

9 files changed

+806
-662
lines changed

9 files changed

+806
-662
lines changed

assignments/assignment-1/index.html

Lines changed: 91 additions & 89 deletions
Large diffs are not rendered by default.

assignments/assignment-2/index.html

Lines changed: 96 additions & 91 deletions
Large diffs are not rendered by default.

assignments/assignment-3/index.html

Lines changed: 211 additions & 204 deletions
Large diffs are not rendered by default.

assignments/assignment-4/index.html

Lines changed: 83 additions & 88 deletions
Large diffs are not rendered by default.

assignments/assignment-5/index.html

Lines changed: 115 additions & 90 deletions
Large diffs are not rendered by default.

assignments/final-project/index.html

Lines changed: 41 additions & 69 deletions
Large diffs are not rendered by default.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Syllabus Table Fixes
2+
3+
**Date:** 2026-01-04
4+
**Status:** Pending
5+
**Priority:** Medium
6+
7+
## Issue
8+
9+
The detailed schedule table in the syllabus has mismatched columns and inconsistent lecture numbering.
10+
11+
## Required Fixes
12+
13+
### 1. Column Structure
14+
The table columns should be:
15+
| Column | Description |
16+
|--------|-------------|
17+
| Day | Day of the week (Mon, Tue, Wed, etc.) |
18+
| Lecture | Lecture number |
19+
| Link to slides | URL to lecture slides |
20+
| Materials | Links to readings + assignments |
21+
22+
### 2. Lecture Numbering
23+
X-hour lectures should be numbered sequentially like every other lecture:
24+
- Week 1 should have lectures 1, 2, 3, and 4
25+
- Week 2 should continue with 5, 6, 7, 8
26+
- And so on...
27+
28+
Currently x-hour lectures may be unnumbered or treated differently.
29+
30+
## Files to Modify
31+
- `admin/syllabus.md` - Source markdown
32+
- `syllabus/index.html` - Generated output (via build-pages.py)
33+
34+
## Notes
35+
- The `scripts/build-pages.py` handles markdown-to-HTML conversion
36+
- Table parsing was recently updated to support proper HTML tables
37+
- After fixing syllabus.md, regenerate with: `python scripts/build-pages.py`

scripts/build-pages.py

Lines changed: 127 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -152,42 +152,117 @@ def convert_table_block(lines):
152152
return "\n".join(html)
153153

154154

155-
def convert_ordered_lists(html):
156-
"""Convert markdown numbered lists to HTML ordered lists."""
155+
def convert_lists(html):
156+
"""Convert markdown lists to HTML with proper nesting support."""
157157
lines = html.split("\n")
158158
result = []
159-
in_list = False
160-
161-
for line in lines:
162-
# Match lines starting with number and period (1. 2. etc)
163-
match = re.match(r"^(\d+)\.\s+(.+)$", line.strip())
164-
165-
if match:
166-
if not in_list:
167-
result.append("<ol>")
168-
in_list = True
169-
result.append(f"<li>{match.group(2)}</li>")
159+
stack = [] # [(indent, list_type, has_open_li)]
160+
161+
def get_indent(line):
162+
return len(line) - len(line.lstrip())
163+
164+
def parse_list_item(line):
165+
stripped = line.lstrip()
166+
indent = get_indent(line)
167+
168+
checkbox_match = re.match(r"^- \[([ xX])\]\s+(.+)$", stripped)
169+
if checkbox_match:
170+
checked = checkbox_match.group(1).lower() == "x"
171+
content = checkbox_match.group(2)
172+
checkbox_html = (
173+
f'<input type="checkbox" disabled{" checked" if checked else ""}> '
174+
)
175+
return indent, "ul", checkbox_html + content
176+
177+
ordered_match = re.match(r"^(\d+)\.\s+(.+)$", stripped)
178+
if ordered_match:
179+
return indent, "ol", ordered_match.group(2)
180+
181+
unordered_match = re.match(r"^[-*]\s+(.+)$", stripped)
182+
if unordered_match:
183+
return indent, "ul", unordered_match.group(1)
184+
185+
return -1, "", ""
186+
187+
def close_to_indent(target_indent):
188+
while stack and stack[-1][0] >= target_indent:
189+
_, list_type, has_open_li = stack.pop()
190+
if has_open_li:
191+
result.append("</li>")
192+
result.append(f"</{list_type}>")
193+
194+
def peek_next_list_item(start_idx):
195+
for j in range(start_idx, len(lines)):
196+
if lines[j].strip():
197+
return parse_list_item(lines[j])
198+
return -1, "", ""
199+
200+
i = 0
201+
while i < len(lines):
202+
line = lines[i]
203+
indent, list_type, content = parse_list_item(line)
204+
205+
if list_type:
206+
# Check if next line is a nested list
207+
next_indent, next_type, _ = peek_next_list_item(i + 1)
208+
has_nested = next_type and next_indent > indent
209+
210+
# Close lists at same or higher indent with different type
211+
while stack and stack[-1][0] >= indent:
212+
if stack[-1][0] == indent and stack[-1][1] == list_type:
213+
# Same level, same type - just close the previous li
214+
if stack[-1][2]:
215+
result.append("</li>")
216+
stack[-1] = (stack[-1][0], stack[-1][1], False)
217+
break
218+
# Different type or higher indent - close completely
219+
_, old_type, old_has_li = stack.pop()
220+
if old_has_li:
221+
result.append("</li>")
222+
result.append(f"</{old_type}>")
223+
224+
# Open new list if needed
225+
if not stack or stack[-1][0] < indent:
226+
result.append(f"<{list_type}>")
227+
stack.append((indent, list_type, False))
228+
229+
# Write the li (leave open if nested content follows)
230+
if has_nested:
231+
result.append(f"<li>{content}")
232+
stack[-1] = (stack[-1][0], stack[-1][1], True)
233+
else:
234+
result.append(f"<li>{content}</li>")
235+
236+
i += 1
170237
else:
171-
if in_list:
172-
result.append("</ol>")
173-
in_list = False
238+
# Not a list item
239+
if line.strip():
240+
close_to_indent(0)
174241
result.append(line)
242+
i += 1
175243

176-
if in_list:
177-
result.append("</ol>")
178-
244+
close_to_indent(0)
179245
return "\n".join(result)
180246

181247

248+
def convert_ordered_lists(html):
249+
"""Legacy function - now handled by convert_lists."""
250+
return html
251+
252+
182253
def convert_unordered_lists(html):
183-
"""Convert markdown unordered lists to HTML."""
254+
"""Legacy function - now handled by convert_lists."""
184255
lines = html.split("\n")
185256
in_list = False
186257
result = []
187258

188259
for line in lines:
189260
stripped = line.strip()
190-
if stripped.startswith("- "):
261+
# Skip if already processed (contains <li>)
262+
if "<li>" in line or "<ul>" in line or "<ol>" in line:
263+
result.append(line)
264+
continue
265+
if stripped.startswith("- ") and not stripped.startswith("- ["):
191266
if not in_list:
192267
result.append("<ul>")
193268
in_list = True
@@ -219,18 +294,43 @@ def wrap_paragraphs(html):
219294
if not p:
220295
continue
221296

222-
# Don't wrap if already an HTML element
223-
if (
297+
starts_with_tag = (
224298
p.startswith("<h")
299+
or p.startswith("</h")
225300
or p.startswith("<ul")
301+
or p.startswith("</ul")
226302
or p.startswith("<ol")
303+
or p.startswith("</ol")
304+
or p.startswith("<li")
305+
or p.startswith("</li")
227306
or p.startswith("<table")
307+
or p.startswith("</table")
308+
or p.startswith("<thead")
309+
or p.startswith("</thead")
310+
or p.startswith("<tbody")
311+
or p.startswith("</tbody")
312+
or p.startswith("<tr")
313+
or p.startswith("</tr")
228314
or p.startswith("<hr")
229315
or p.startswith("<div")
316+
or p.startswith("</div")
230317
or p.startswith("<p")
231-
):
232-
formatted.append(p)
233-
elif p.startswith("<li"):
318+
or p.startswith("</p")
319+
or p.startswith("<input")
320+
)
321+
322+
contains_block_html = (
323+
"<ul>" in p
324+
or "</ul>" in p
325+
or "<ol>" in p
326+
or "</ol>" in p
327+
or "<li>" in p
328+
or "</li>" in p
329+
or "<table>" in p
330+
or "</table>" in p
331+
)
332+
333+
if starts_with_tag or contains_block_html:
234334
formatted.append(p)
235335
else:
236336
formatted.append(f"<p>{p}</p>")
@@ -245,12 +345,10 @@ def parse_markdown_to_html(markdown_text):
245345
html = convert_latex_href(html)
246346
html = convert_latex_table(html)
247347

248-
# Convert markdown elements
249348
html = convert_headers(html)
250349
html = convert_inline_formatting(html)
251350
html = convert_tables(html)
252-
html = convert_ordered_lists(html)
253-
html = convert_unordered_lists(html)
351+
html = convert_lists(html)
254352
html = convert_horizontal_rules(html)
255353
html = convert_links(html)
256354
html = wrap_paragraphs(html)

syllabus/index.html

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,12 +273,15 @@ <h2>Grading</h2>
273273
<p>Grades will be based on the following components:</p>
274274
<ul>
275275
<li><strong>Problem Sets</strong> (75%): A total of 5 problem sets designed to reinforce key concepts (each is worth 15% of the final course grade).</li>
276-
<li><strong>Final Project</strong> (25%): You will carry out a larger scale (relative to the problem sets) "research" project on a topic of your choosing. This will include:</li>
276+
<li><strong>Final Project</strong> (25%): You will carry out a larger scale (relative to the problem sets) "research" project on a topic of your choosing. This will include:
277+
<ul>
277278
<li>Python code, organized as a Colaboratory notebook</li>
278279
<li>A "presentation" to the class (also submitted as a YouTube video), along with an in-class discussion of your project</li>
279280
<li>A brief (2--5 page) writeup of the main approach and key findings or takeaways</li>
280281
</ul>
281-
<p>Students may work together on all of the assignments, unless otherwise noted in class or in the assignment instructions. However, <strong>each student must submit their own problem set and indicate who they worked with</strong>. Final projects will (typically) be completed in groups of 2--3 students, with the entire group turning in the same project (and receiving the same grade for it).</p>
282+
</li>
283+
</ul>
284+
Students may work together on all of the assignments, unless otherwise noted in class or in the assignment instructions. However, <strong>each student must submit their own problem set and indicate who they worked with</strong>. Final projects will (typically) be completed in groups of 2--3 students, with the entire group turning in the same project (and receiving the same grade for it).
282285
<p>Grading Scale: A (93–100), A- (90–92), B+ (87–89), B (83–86), B- (80–82), C+ (77–79), C (73–76), C- (70–72), D (60–69), E (0–59). All grades will be rounded to the nearest integer (e.g., a 92.5 average will result in a final grade of "A", whereas a 92.4999 average will result in a final grade of "A-"). Out of fairness to all students in the course, there will be no "negotiations" about grading-- e.g., your grade will be determined solely by the numerical average of your assignment scores.</p>
283286
<h3>Late Policy</h3>
284287
<p>Problem sets will receive a 10% deduction for each week late, rounded <strong>up</strong> to the nearest whole week (e.g., from a grading standpoint submitting an assignment 1 minute late is the same as submitting it 1 day late, is the same as submitting it 6 days late).</p>

0 commit comments

Comments
 (0)