156 lines
6.7 KiB
Python
156 lines
6.7 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Inject a collapsible 题目讲解 panel (题目 / 孩子代码 / 答案 / 解析) into each
|
|
student's OJ section on the two CSP04 class pages (0830 & 1400). Additive only.
|
|
|
|
Usage: python inject_prob_details.py <page1.html> <page2.html> ...
|
|
"""
|
|
import sys, os, json, html, re, copy
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent.parent.parent
|
|
TMP = ROOT / '.claude' / 'memory' / 'temp'
|
|
CODE = json.load(open(TMP / 'student_code_CSP04_L01.json', encoding='utf-8'))
|
|
META = json.load(open(TMP / 'problems_meta.json', encoding='utf-8'))
|
|
|
|
# display prefix -> numeric pid (as used in fetched codes)
|
|
PIDNUM = {v.split(' ')[0]: k for k, v in CODE['pid_map'].items()}
|
|
PROB_ORDER = ['S0401P1', 'S0401P2', 'S0401P3', 'S0401P4', 'S0401P5']
|
|
|
|
STATUS_TXT = {'AC': '✅ 已通过 AC', 'WA': '❌ 答案错误 WA', 'CE': '❌ 编译错误 CE',
|
|
'RE': '❌ 运行错误 RE', 'TLE': '❌ 超时 TLE', 'PE': '⚠️ 格式错误 PE', 'MLE': '❌ 超内存 MLE'}
|
|
|
|
CSS_BLOCK = '''
|
|
/* ===== 题目讲解(题目/孩子代码/答案/解析) ===== */
|
|
.oj-explains{margin-top:16px;}
|
|
.oj-explains-title{font-size:0.9em;font-weight:600;color:var(--ink);margin-bottom:8px;display:flex;align-items:center;gap:6px;}
|
|
.prob-detail{border:1px solid var(--border);border-radius:10px;margin-bottom:8px;overflow:hidden;background:var(--cream);}
|
|
.prob-head{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:10px 14px;cursor:pointer;font-size:0.85em;}
|
|
.prob-head:hover{background:var(--paper-warm);}
|
|
.prob-title{font-weight:600;color:var(--ink);}
|
|
.prob-sub{font-size:0.74em;color:var(--ink-soft);flex:1;text-align:right;}
|
|
.prob-caret{font-size:0.85em;color:var(--ink-soft);transition:transform 0.2s ease;}
|
|
.prob-detail.open .prob-caret{transform:rotate(90deg);}
|
|
.prob-body{display:none;padding:12px 14px;border-top:1px solid var(--border);}
|
|
.prob-detail.open .prob-body{display:block;}
|
|
.prob-sec{margin-bottom:12px;}
|
|
.prob-sec:last-child{margin-bottom:0;}
|
|
.prob-sec-h{font-size:0.78em;font-weight:700;color:var(--ink-soft);margin-bottom:5px;}
|
|
.prob-sec-c{font-size:0.83em;line-height:1.75;color:var(--ink-soft);background:var(--paper);border-radius:8px;padding:8px 11px;}
|
|
.prob-code{background:#1b2733;color:#e6eef7;border-radius:8px;padding:10px 12px;font-size:0.76em;line-height:1.6;overflow:auto;white-space:pre;max-height:240px;font-family:Consolas,'Courier New',monospace;}
|
|
/* 未提交 -> 课后作业(不展开、不做分析) */
|
|
.prob-detail.homework{background:var(--paper-warm);border-style:dashed;}
|
|
.prob-detail.homework .prob-head{cursor:default;}
|
|
.prob-detail.homework .prob-sub{color:#a8842c;font-weight:600;}
|
|
'''
|
|
|
|
JS_BLOCK = '\n function toggleProb(el){el.classList.toggle("open");var c=el.querySelector(".prob-caret");if(c)c.textContent=el.classList.contains("open")?"▼":"▶";}\n'
|
|
|
|
|
|
def esc(t):
|
|
return html.escape(str(t))
|
|
|
|
|
|
def build_panels(student):
|
|
panels = ['<div class="oj-explains">',
|
|
'<div class="oj-explains-title">📖 题目讲解(已提交的题 · 题目 / 孩子代码 / 参考答案 / 解析)</div>']
|
|
codes = CODE['students'].get(student, {})
|
|
for pid in PROB_ORDER:
|
|
meta = META.get(pid)
|
|
if not meta:
|
|
continue
|
|
entry = codes.get(PIDNUM.get(pid, ''), {})
|
|
code = entry.get('code', '')
|
|
status = entry.get('status', '')
|
|
attempts = entry.get('attempts', 0)
|
|
if code:
|
|
sub = '%s · %d次提交' % (STATUS_TXT.get(status, status), attempts)
|
|
panels.append(
|
|
'<div class="prob-detail" onclick="toggleProb(this)">'
|
|
'<div class="prob-head">'
|
|
'<span class="prob-title">%s</span>'
|
|
'<span class="prob-sub">%s</span>'
|
|
'<span class="prob-caret">▶</span>'
|
|
'</div>'
|
|
'<div class="prob-body">'
|
|
'<div class="prob-sec"><div class="prob-sec-h">📋 题目</div><div class="prob-sec-c">%s</div></div>'
|
|
'<div class="prob-sec"><div class="prob-sec-h">👦 孩子的代码</div><pre class="prob-code">%s</pre></div>'
|
|
'<div class="prob-sec"><div class="prob-sec-h">✅ 参考答案</div><pre class="prob-code">%s</pre></div>'
|
|
'<div class="prob-sec"><div class="prob-sec-h">💡 解析</div><div class="prob-sec-c">%s</div></div>'
|
|
'</div></div>' % (esc(meta['key']), sub, esc(meta['statement']), esc(code), esc(meta['ref_code']), esc(meta['key_points'])))
|
|
else:
|
|
# 未提交 -> 只标为课后作业,不做分析
|
|
panels.append(
|
|
'<div class="prob-detail homework">'
|
|
'<div class="prob-head">'
|
|
'<span class="prob-title">%s</span>'
|
|
'<span class="prob-sub">📚 未提交 · 课后作业</span>'
|
|
'</div></div>' % esc(meta['key']))
|
|
panels.append('</div>')
|
|
return '\n'.join(panels)
|
|
|
|
|
|
def find_oj_section(html, start):
|
|
"""Return end index (after the matching </div>) for the oj-section starting at `start`."""
|
|
n = len(html)
|
|
depth = 1
|
|
i = start + len('<div class="oj-section"')
|
|
while depth > 0 and i < n:
|
|
o = html.find('<div', i)
|
|
c = html.find('</div>', i)
|
|
if c == -1:
|
|
return -1
|
|
if o != -1 and o < c:
|
|
depth += 1
|
|
i = o + 4
|
|
else:
|
|
depth -= 1
|
|
i = c + 6
|
|
return i
|
|
|
|
|
|
def inject_style(html, css):
|
|
idx = html.rfind('</style>')
|
|
if idx == -1:
|
|
return html
|
|
return html[:idx] + css + html[idx:]
|
|
|
|
|
|
def inject_script(html, js):
|
|
idx = html.rfind('</script>')
|
|
if idx == -1:
|
|
return html
|
|
return html[:idx] + js + html[idx:]
|
|
|
|
|
|
def process(html):
|
|
# inject CSS/JS once
|
|
if '.prob-detail' not in html:
|
|
html = inject_style(html, CSS_BLOCK)
|
|
if 'function toggleProb' not in html:
|
|
html = inject_script(html, JS_BLOCK)
|
|
# inject panels per student oj-section (insert RIGHT-TO-LEFT so earlier
|
|
# positions stay valid after the html string changes length)
|
|
pat = re.compile(r'<div class="oj-section" data-oj-section="([^"]+)">')
|
|
matches = [(m.group(1), m.start()) for m in pat.finditer(html)]
|
|
for name, start in reversed(matches):
|
|
end = find_oj_section(html, start)
|
|
if end == -1:
|
|
continue
|
|
closing = end - 6 # position of the </div> that closes oj-section
|
|
panels = build_panels(name)
|
|
html = html[:closing] + panels + '\n' + html[closing:]
|
|
return html
|
|
|
|
|
|
def main(files):
|
|
for p in files:
|
|
orig = open(p, encoding='utf-8').read()
|
|
out = process(orig)
|
|
changed = out != orig
|
|
open(p, 'w', encoding='utf-8').write(out)
|
|
print(('CHANGED ' if changed else 'same ') + os.path.basename(p))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main(sys.argv[1:])
|