52 lines
7.9 KiB
Python
52 lines
7.9 KiB
Python
#!/usr/bin/env python3
|
||
"""Render the maintained Markdown guide into a self-contained HTML document."""
|
||
from pathlib import Path
|
||
import re,html,json,base64
|
||
ROOT=Path(__file__).resolve().parents[1]
|
||
source=ROOT/'colorBlock-level-editor/docs/SCREENSHOT_RECOGNITION_EXPLAINED.md'
|
||
# This small renderer supports precisely the tables/headings/lists/code in this guide.
|
||
def inline(s):
|
||
s=html.escape(s);s=re.sub(r'`([^`]+)`',r'<code>\1</code>',s);return re.sub(r'\*\*([^*]+)\*\*',r'<strong>\1</strong>',s)
|
||
lines=source.read_text().splitlines();out=[];toc=[];i=0
|
||
while i<len(lines):
|
||
line=lines[i]
|
||
if not line.strip():i+=1;continue
|
||
if line.startswith('```'):
|
||
i+=1;block=[]
|
||
while i<len(lines) and not lines[i].startswith('```'):block.append(lines[i]);i+=1
|
||
out.append('<pre><code>'+html.escape('\n'.join(block))+'</code></pre>');i+=1;continue
|
||
if line.startswith('#'):
|
||
level=len(line)-len(line.lstrip('#'));title=line[level:].strip();id='section-'+str(len(toc)) if level==2 else 'h-'+str(i)
|
||
if level==2:toc.append((id,title))
|
||
if level>1:out.append(f'<h{level} id="{id}">{inline(title)}</h{level}>')
|
||
i+=1;continue
|
||
if line.startswith('|'):
|
||
rows=[]
|
||
while i<len(lines) and lines[i].startswith('|'):
|
||
cols=[c.strip() for c in lines[i].strip('|').split('|')]
|
||
if not all(re.fullmatch(r':?-+:?',c) for c in cols):rows.append(cols)
|
||
i+=1
|
||
out.append('<div class="table"><table><thead><tr>'+''.join('<th>'+inline(c)+'</th>' for c in rows[0])+'</tr></thead><tbody>'+''.join('<tr>'+''.join('<td>'+inline(c)+'</td>' for c in row)+'</tr>' for row in rows[1:])+'</tbody></table></div>');continue
|
||
if re.match(r'(- |\d+\. )',line):
|
||
tag='ul' if line.startswith('-') else 'ol';items=[]
|
||
while i<len(lines) and re.match(r'(- |\d+\. )',lines[i]):items.append(re.sub(r'^(- |\d+\. )','',lines[i]));i+=1
|
||
out.append('<'+tag+'>'+''.join('<li>'+inline(c)+'</li>' for c in items)+'</'+tag+'>');continue
|
||
paragraph=[]
|
||
while i<len(lines) and lines[i].strip() and not re.match(r'^(#|\||```|- |\d+\. )',lines[i]):paragraph.append(lines[i]);i+=1
|
||
out.append('<p>'+inline(' '.join(paragraph))+'</p>')
|
||
result=json.loads((ROOT/'outputs/editor-level19-20261015/result.json').read_text())
|
||
grid=result['inspections'][0]['grid'];old={'cols':6,'rows':10,'corners':[[152.5197,338],[646.7146,338],[661.6781,1130],[127.5033,1130]],'rowCurve':-.154803758}
|
||
def point(g,c,r):
|
||
u=c/g['cols'];t=r/g['rows'];v=t+g['rowCurve']*t*(t-1);a,b,d,e=g['corners'];return [(1-v)*((1-u)*a[j]+u*b[j])+v*((1-u)*e[j]+u*d[j]) for j in (0,1)]
|
||
def grid_svg(g):
|
||
poly=[]
|
||
for c in range(g['cols']+1):poly.append([point(g,c,r/4) for r in range(g['rows']*4+1)])
|
||
for r in range(g['rows']+1):poly.append([point(g,0,r),point(g,g['cols'],r)])
|
||
return ''.join('<polyline points="'+' '.join(f'{x:.2f},{y:.2f}' for x,y in ps)+'"/>' for ps in poly)
|
||
img=base64.b64encode(Path('/Users/ruoruo/Desktop/截屏2026-10-15 19.37.51.png').read_bytes()).decode()
|
||
figure=f'''<section class="comparison"><div><div class="eyebrow">用这关看网格边界</div><h2>门在网格外,方块在网格内。</h2><p>切换查看同一原图上的修复前后网格。旧网格把顶门算入高度,造成6×10;修复后为6×9,并保留轻微透视。</p><div class="switch"><button data-view="new" aria-pressed="true">修复后 · 6×9</button><button data-view="old" aria-pressed="false">修复前 · 6×10</button></div><p class="note">这是原始图片与程序计算网格的叠加示意;不替代浏览器操作验收。下方记录完整算法与限制。</p></div><svg viewBox="65 300 665 895" role="img" aria-label="第19关修复前后棋盘网格"><image href="data:image/png;base64,{img}" width="784" height="1208"/><g id="grid-new" fill="none" stroke="#fff" stroke-width="1.8">{grid_svg(grid)}</g><g id="grid-old" fill="none" stroke="#ffea70" stroke-width="1.8" style="display:none">{grid_svg(old)}</g></svg></section>'''
|
||
css='''*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:#f3f1ea;color:#223236;font:16px/1.85 -apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif}header{background:#133c39;color:#f6f3e6;padding:64px max(24px,calc((100vw - 1120px)/2)) 42px}header h1{font-size:38px;line-height:1.4;margin:16px 0}header p{max-width:720px;color:#d1e1d8}.eyebrow{letter-spacing:2px;font-size:12px;text-transform:uppercase;color:#95b4a6}.metrics{display:flex;gap:12px;flex-wrap:wrap;margin-top:30px}.metrics div{padding:14px 25px;border:1px solid #54736a;border-radius:8px}.metrics strong{font-size:27px;margin-right:8px}.comparison{max-width:1120px;margin:32px auto;padding:32px;background:#fffdf7;display:grid;grid-template-columns:1fr 380px;gap:36px;border-radius:12px}.comparison h2{border:0;margin-top:16px}.comparison svg{width:100%;max-height:510px;border-radius:8px}.switch{display:flex;gap:12px;flex-wrap:wrap}button{font:inherit;padding:10px 16px;border:1px solid #739488;background:transparent;color:#234740;border-radius:6px;cursor:pointer}button[aria-pressed=true]{background:#164d43;color:white}.note{font-size:13px;color:#67766d}.layout{display:grid;grid-template-columns:220px minmax(0,850px);gap:40px;max-width:1120px;margin:40px auto 100px}nav{position:sticky;top:24px;align-self:start;font-size:13px}nav a{display:block;padding:7px 0;color:#35584c;text-decoration:none}nav a:hover{text-decoration:underline}article{background:#fffdf7;border-radius:12px;padding:32px 42px;min-width:0}h2{font-size:24px;line-height:1.5;border-top:1px solid #d5ddd4;padding-top:30px;margin-top:44px;scroll-margin-top:20px}h3{font-size:18px;color:#266251;margin-top:28px}p{margin:16px 0}strong{color:#204e42}header strong{color:#fff5d8}code{font:13px/1.6 ui-monospace,Menlo,monospace;background:#eaf0e8;padding:2px 5px;border-radius:4px;overflow-wrap:anywhere}pre{overflow-x:auto;background:#1c3735;color:#e1f0e2;padding:22px;border-radius:8px}pre code{background:none;color:inherit;padding:0;overflow-wrap:normal}.table{overflow-x:auto}table{border-collapse:collapse;font-size:13px;min-width:100%}th,td{padding:11px 12px;text-align:left;border-bottom:1px solid #d9dfd5}th{background:#e9efe5;color:#264b40}li{padding:4px 0}footer{text-align:center;padding:26px;font-size:13px;color:#637269}@media(max-width:900px){.layout{display:block;margin:20px}.comparison{grid-template-columns:1fr;margin:20px;padding:24px}.comparison svg{max-height:480px}nav{position:static;margin-bottom:24px;columns:2}article{padding:20px}header{padding:30px 24px}header h1{font-size:30px}}@media print{nav,.switch{display:none}.layout{display:block}.comparison{break-inside:avoid}body,article{background:white}header{padding:25px}article{padding:0}h2{break-after:avoid}tr{break-inside:avoid}}'''
|
||
page='<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>本地截图识别 · 方法与第19关修复</title><style>'+css+'</style><header><div class="eyebrow">ColorBlock Studio / 0.19.3 / 2026.10.19</div><h1>一张截图,怎样变成可编辑关卡</h1><p>从像素、透视网格到方块与门。以第19关解释完整实现、错误原因、修复方式,以及补图真正能提供的信息。</p><div class="metrics"><div><strong>6 × 9</strong>可玩网格</div><div><strong>27</strong>方块</div><div><strong>8</strong>门</div><div><strong>10 / 10</strong>冻结块初值</div></div></header>'+figure+'<div class="layout"><nav aria-label="章节目录">'+''.join(f'<a href="#{id}">{html.escape(t)}</a>' for id,t in toc)+'</nav><article>'+''.join(out)+'</article></div><footer>本地离线文档 · 不加载外部资源 · 原图不修改 · 开发样本不等于独立准确率</footer><script>document.querySelectorAll("[data-view]").forEach(b=>b.onclick=()=>{const current=b.dataset.view;["new","old"].forEach(v=>document.getElementById("grid-"+v).style.display=v===current?"":"none");document.querySelectorAll("[data-view]").forEach(x=>x.setAttribute("aria-pressed",String(x===b)));});</script></html>'
|
||
(ROOT/'colorBlock-level-editor/docs/SCREENSHOT_RECOGNITION_EXPLAINED.html').write_text(page)
|
||
print('Wrote self-contained guide',len(page),'characters')
|