import json,sys,platform
from html.parser import HTMLParser
class Table(HTMLParser):
def __init__(self):super().__init__();self.rows=[];self.cell=None;self.caption=False;self.captionText='';self.complex=False
def handle_starttag(self,t,a):
d=dict(a)
if t=='caption':self.caption=True
if t=='tr':self.rows.append([])
if t in ['td','th']:
self.cell={'tag':t,'attrs':d,'text':''};self.rows[-1].append(self.cell)
if int(d.get('colspan','1'))!=1 or int(d.get('rowspan','1'))!=1:self.complex=True
def handle_endtag(self,t):
if t=='caption':self.caption=False
if t in ['td','th']:self.cell=None
def handle_data(self,x):
if self.cell is not None:self.cell['text']+=x
if self.caption:self.captionText+=x
expected=[{'row':'자료집','column':'1분기','value':'12'},{'row':'자료집','column':'2분기','value':'15'},{'row':'체크리스트','column':'1분기','value':'8'},{'row':'체크리스트','column':'2분기','value':'11'}]
plain='
'
full='분기별 자료 수량| 상품 | 1분기 | 2분기 |
|---|
| 자료집 | 12 | 15 |
|---|
| 체크리스트 | 8 | 11 |
|---|
'
scope_only=full.replace(' headers="book q1"','').replace(' headers="book q2"','').replace(' headers="list q1"','').replace(' headers="list q2"','')
col_only=scope_only.replace('scope="row"','')
explicit=full.replace(' scope="col"','').replace(' scope="row"','')
fixtures={'plain-td':plain,'column-only':col_only,'scope-both':scope_only,'explicit-headers':explicit,'missing-header-id':full.replace('headers="book q2"','headers="book missing"'),'complex-span':scope_only.replace('id="q1"','id="q1" colspan="2"')}
def inspect(s):
p=Table();p.feed(s)
if p.complex:return {'supported':False,'reason':'spans-outside-simple-grid','caption':p.captionText,'mapping':[],'mappingMatchesExpected':None}
ids={c['attrs']['id']:c for row in p.rows for c in row if c['tag']=='th' and c['attrs'].get('id')};mapping=[];missing=[]
for ri,row in enumerate(p.rows[1:],1):
for ci,c in enumerate(row[1:],1):
names=[]
if 'headers' in c['attrs']:
for key in c['attrs']['headers'].split():
if key in ids:names.append(ids[key]['text'])
else:missing.append(key)
else:
names += [h['text'] for h in row if h['tag']=='th' and h['attrs'].get('scope')=='row']
names += [r[ci]['text'] for r in p.rows if len(r)>ci and r[ci]['tag']=='th' and r[ci]['attrs'].get('scope')=='col']
mapping.append({'value':c['text'],'headers':names})
matches=len(mapping)==len(expected) and all(set(a['headers'])=={b['row'],b['column']} and a['value']==b['value'] for a,b in zip(mapping,expected))
return {'supported':True,'caption':p.captionText,'mapping':mapping,'missingHeaderIds':missing,'mappingMatchesExpected':matches}
result={'expectedAssociations':expected,'cases':[{'id':name,'html':s,'result':inspect(s)} for name,s in fixtures.items()],'screenReaderExecuted':False,'complexSpansSupported':False}
assert [x['result']['mappingMatchesExpected'] for x in result['cases']]==[False,False,True,True,False,None]
from datetime import datetime as _runtime_datetime, timezone as _runtime_timezone
result.update({'fixtureCreatedOn':'2026-10-15','executedAt':_runtime_datetime.now(_runtime_timezone.utc).isoformat(),'runtime':platform.python_version(),'synthetic':True})
print(json.dumps(result,ensure_ascii=False,indent=2))