Spaces:
Sleeping
Sleeping
File size: 5,359 Bytes
526950e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | """Parse `## References` markdown tables on syntopicon entry files into
structured reference dicts, resolved against a (session_date, row_index)
lookup map produced by the lecture chunker.
"""
from __future__ import annotations
import logging
import re
from pathlib import Path
logger = logging.getLogger(__name__)
_DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
_PROBLEM_ID_RE = re.compile(r"^P\d+$")
def parse_row_spec(rows_raw: str) -> list[int]:
"""Parse a Row(s) cell. Supports '159', '159, 232', '159-162'.
Returns sorted unique list. Whitespace tolerated. Returns empty list on
unparseable input.
"""
s = rows_raw.strip()
if not s:
return []
if "-" in s and "," not in s:
# Range form
try:
lo, hi = s.split("-", 1)
return list(range(int(lo.strip()), int(hi.strip()) + 1))
except ValueError:
return []
if "," in s:
try:
return sorted({int(p.strip()) for p in s.split(",") if p.strip()})
except ValueError:
return []
try:
return [int(s)]
except ValueError:
return []
def parse_references(
markdown_path: Path,
row_lookup: dict[tuple[str, int], tuple[str, str]],
) -> list[dict]:
"""Parse the ## References table in a syntopicon markdown file.
Returns a list of reference dicts. Each dict has session_date, speaker,
row_index, how_used, notes, problem_id (if notes match /^P\\d+$/),
lecture_turn_doc_id, and lecture_problem_doc_id.
Unresolvable rows are logged at WARNING and skipped. Malformed rows and
bad dates are also skipped, with logging. Files without ## References
return [] silently.
"""
text = markdown_path.read_text(encoding="utf-8")
lines = text.splitlines()
# Find ## References heading
in_section = False
in_table = False
skipped_separator = False
refs: list[dict] = []
for line in lines:
stripped = line.strip()
if stripped.lower().startswith("## references"):
in_section = True
continue
if not in_section:
continue
if stripped.startswith("## "):
# Different section; stop
break
if not stripped:
if in_table:
# Blank line ends table
break
continue
if not in_table:
# Skip until table header
if stripped.startswith("|"):
in_table = True
continue
if not skipped_separator:
# The line right after the header is the separator |---|---|
if "---" in stripped:
skipped_separator = True
continue
# No separator? still skip if it looks like one; otherwise treat as data
if stripped.startswith("|"):
skipped_separator = True
# fall through to parse this row as data? No — skip header again.
continue
# Parse a data row
cells = [c.strip() for c in stripped.strip("|").split("|")]
if len(cells) != 5:
logger.warning(
"syntopicon_reference_parser: malformed row in %s (got %d cells, want 5): %r",
markdown_path, len(cells), stripped,
)
continue
session_date, speaker, rows_raw, how_used, notes = cells
if not _DATE_RE.match(session_date):
logger.warning(
"syntopicon_reference_parser: bad date format %r in %s",
session_date, markdown_path,
)
continue
row_indices = parse_row_spec(rows_raw)
problem_id = notes if _PROBLEM_ID_RE.match(notes.strip()) else None
for row_index in row_indices:
key = (session_date, row_index)
if key not in row_lookup:
logger.warning(
"syntopicon_reference_parser: unresolvable row %s in %s (row=%d)",
session_date, markdown_path, row_index,
)
continue
turn_id, problem_did = row_lookup[key]
refs.append({
"session_date": session_date,
"speaker": speaker,
"row_index": row_index,
"how_used": how_used,
"notes": notes,
"problem_id": problem_id,
"lecture_turn_doc_id": turn_id,
"lecture_problem_doc_id": problem_did,
})
if in_section and not refs:
logger.warning(
"syntopicon_reference_parser: %s has '## References' heading but no resolvable rows",
markdown_path,
)
return refs
def parse_all_references(
syntopicon_dir: Path,
row_lookup: dict[tuple[str, int], tuple[str, str]],
) -> dict[str, list[dict]]:
"""Walk the syntopicon directory; for each *.md file (excluding _registry.json
and other non-term files), parse references. Returns {term_slug: refs}.
The term_slug is the markdown filename stem (e.g., 'demand' for 'demand.md').
"""
out: dict[str, list[dict]] = {}
for md_path in sorted(syntopicon_dir.glob("*.md")):
if md_path.stem.startswith("_"):
continue
out[md_path.stem] = parse_references(md_path, row_lookup)
return out
|