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='
상품1분기2분기
자료집1215
체크리스트811
' full='
분기별 자료 수량
상품1분기2분기
자료집1215
체크리스트811
' 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))