// YAML-driven tables: load YAML, normalize to a grid, render Typst tables.// --- cell / column helpers ---#letyaml-cell(value)={iftype(value)==dictionaryandvalue.at("markup",default:false)==true{lettext=value.at("value",default:value.at("text",default:none))eval(str(iftext!=none{text}else{""}),mode:"markup")}elseiftype(value)==strandvalue.ends-with("!markup"){eval(value.slice(0,value.len()-7),mode:"markup")}else{[#value]}}#letparse-align(a)={ifa==auto{auto}elseiftype(a)==str{ifa=="left"{left}elseifa=="center"{center}elseifa=="right"{right}else{panic("unknown column align: "+a)}}else{a}}#letparse-width(w)={ifw==auto{auto}elseiftype(w)==str{eval(w)}else{w}}#letparse-column-spec(col)={iftype(col)==dictionaryand"id"incol{(id:str(col.id),label:col.at("label",default:str(col.id)),align:parse-align(col.at("align",default:auto)),width:parse-width(col.at("width",default:auto)),)}elseiftype(col)==dictionary{letkeys=col.keys()ifkeys.len()!=1{panic("column entry must have id/label or be a single-key dictionary")}letid=keys.at(0)(id:str(id),label:col.at(id),align:auto,width:auto,)}else{panic("invalid column spec: "+repr(col))}}#letrow-cells(row,col-ids)={col-ids.map(id=>{ifidnotinrow{panic("row missing column '"+id+"': "+repr(row))}yaml-cell(row.at(id))})}#letlegacy-single-key-rows(rows,col-ids)={letgrouped=()letbuf=(:)forrowinrows{iftype(row)!=dictionary{panic("expected dictionary row in legacy map format")}letkeys=row.keys()ifkeys.len()!=1{returnnone}letkey=keys.at(0)letval=row.at(key)buf.insert(str(key),val)ifbuf.len()==col-ids.len(){grouped.push(row-cells(buf,col-ids))buf=(:)}}ifbuf.len()!=0{panic("legacy row list does not divide evenly into "+str(col-ids.len())+" columns")}grouped}#letrows-are-legacy-single-key(rows)={ifrows.len()==0{returnfalse}rows.all(row=>type(row)==dictionaryandrow.len()==1)}// --- schema detection ---#letdetect-schema(raw)={iftype(raw)==dictionary{if"headers"inrawand"rows"inrawandtype(raw.headers)==array{ifraw.rows.len()>0andtype(raw.rows.at(0))==array{return"matrix"}}if"columns"inrawand"rows"inraw{return"map"}letreserved=("columns","rows","headers")letkeys=raw.keys()ifkeys.all(k=>knotinreserved){return"kv"}}iftype(raw)==arrayandraw.len()>0andtype(raw.at(0))==dictionary{return"records"}panic("could not detect YAML table schema; set schema: explicitly\n"+"data: "+repr(raw),)}// --- normalizers → grid model ---// grid: (headers, rows, col-keys, col-aligns, col-widths, header-default)#letnormalize-kv(raw)={letpairs=raw.pairs()letrows=pairs.map(p=>(yaml-cell(p.at(0)),yaml-cell(p.at(1))))(headers:none,rows:rows,col-keys:("key","value"),col-aligns:(auto,auto),col-widths:(auto,auto),header-default:false,)}#letnormalize-matrix(raw)={letheaders=raw.headers.map(h=>yaml-cell(h))letn=headers.len()letrows=raw.rows.map(r=>{ifr.len()!=n{panic("matrix row length "+str(r.len())+" != header count "+str(n))}r.map(c=>yaml-cell(c))})(headers:headers,rows:rows,col-keys:headers.map(h=>repr(h)),col-aligns:headers.map(_=>auto),col-widths:headers.map(_=>auto),header-default:true,)}#letnormalize-map(raw)={letcol-specs=raw.columns.map(parse-column-spec)letcol-ids=col-specs.map(c=>c.id)letheaders=col-specs.map(c=>yaml-cell(c.label))letcol-aligns=col-specs.map(c=>c.align)letcol-widths=col-specs.map(c=>c.width)letdata-rows=ifrows-are-legacy-single-key(raw.rows){legacy-single-key-rows(raw.rows,col-ids)}else{raw.rows.map(row=>{iftype(row)!=dictionary{panic("map row must be a dictionary")}row-cells(row,col-ids)})}ifdata-rows==none{panic("failed to normalize map rows")}(headers:headers,rows:data-rows,col-keys:col-ids,col-aligns:col-aligns,col-widths:col-widths,header-default:raw.at("header",default:true),)}#letnormalize-records(raw)={letcol-ids=ifraw.len()>0{raw.at(0).keys().map(k=>str(k))}else{()}letheaders=col-ids.map(id=>yaml-cell(id))letrows=raw.map(row=>row-cells(row,col-ids))(headers:headers,rows:rows,col-keys:col-ids,col-aligns:col-ids.map(_=>auto),col-widths:col-ids.map(_=>auto),header-default:true,)}#letnormalize(raw,schema)={letkind=ifschema==auto{detect-schema(raw)}elseiftype(schema)==str{schema}else{panic("schema must be auto or a string")}ifkind=="kv"{normalize-kv(raw)}elseifkind=="map"orkind=="grid"{normalize-map(raw)}elseifkind=="matrix"{normalize-matrix(raw)}elseifkind=="records"{normalize-records(raw)}else{panic("unknown schema: "+repr(kind))}}// --- rendering ---#letresolve-fill(fill,has-header:true)={ifhas-header{iffill==noneorfill==auto{none}elseiffill=="striped"{(x,y)=>{ify==0{cfg.table-header-bg}elseifcalc.even(y){cfg.table-striped-bg}else{none}}}else{fill}}elseiffill==noneorfill==auto{// Without table.header(), row 0 is data — override global header-row fill.(x,y)=>ify==0{white}else{none}}elseiffill=="striped"{(x,y)=>{ifcalc.even(y){cfg.table-striped-bg}else{none}}}else{letbase=fill(x,y)=>ify==0{white}else{base(x,y)}}}#letresolve-stroke(stroke,has-header:true)={ifhas-header{stroke}elseifstroke==auto{// Without table.header(), row 1 is data — use horizontal stroke, not header-b.table-stroke}elseiftype(stroke)==function{(x,y)=>{lets=table-stroke(x,y)ify==1andtype(s)==dictionary{s+(top:cfg.table-stroke-horizontal)}else{s}}}else{stroke}}#letresolve-header(header,grid)={ifheader==auto{ifgrid.header-default{grid.headers}else{none}}elseifheader==false{none}elseifheader==true{grid.headers}elseiftype(header)==array{header.map(h=>yaml-cell(h))}else{panic("header must be auto, true, false, or an array")}}#letbuild-table-args(grid,header-cells,columns,align,fill,stroke,inset,strong-header,table-args,)={letn=grid.col-keys.len()letcol-spec=ifcolumns!=auto{columns}else{letws=grid.col-widthsifws.all(w=>w==auto){// Typst defaults to a single column unless columns is set explicitly.n}else{ws}}letcol-aligns=ifalign!=auto{align}else{letgrid-aligns=grid.col-alignsifgrid-aligns.all(a=>a==auto){auto}else{grid-aligns}}lethas-header=header-cells!=noneletresolved-fill=resolve-fill(fill,has-header:has-header)letresolved-stroke=resolve-stroke(stroke,has-header:has-header)letargs=(columns:col-spec)ifcol-aligns!=auto{args.insert("align",col-aligns)}ifresolved-fill!=none{args.insert("fill",resolved-fill)}ifresolved-stroke!=auto{args.insert("stroke",resolved-stroke)}ifinset!=auto{args.insert("inset",inset)}for(k,v)intable-args.named(){args.insert(str(k),v)}letheader-row=ifheader-cells!=none{letcells=ifstrong-header{header-cells.map(c=>strong(c))}else{header-cells}(table.header(..cells),table.hline())}else{()}letflat-rows=()forrowingrid.rows{forcellinrow{flat-rows.push(cell)}}(args:args,header-row:header-row,flat-rows:flat-rows)}#letyaml-table(path,schema:auto,header:auto,columns:auto,align:auto,fill:none,stroke:auto,inset:auto,strong-header:true,as-figure:false,caption:none,kind:table,..table-args,)={letraw=yaml(path)letgrid=normalize(raw,schema)letheader-cells=resolve-header(header,grid)letbuilt=build-table-args(grid,header-cells,columns,align,fill,stroke,inset,strong-header,table-args,)lettbl=table(..built.args,..built.header-row,..built.flat-rows)ifas-figure{figure(tbl,caption:caption,kind:kind)}else{tbl}}#letyaml-table-figure(path,caption:none,..args)={yaml-table(path,as-figure:true,caption:caption,..args)}