123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715openCoreopenInt.Replace_polymorphic_comparetypedims={width:int;height:int}letsexp_of_dims{width;height}=sexp_of_string(sprintf"w%dh%d"widthheight)letdims_invariant{width;height}=assert(width>=0);assert(height>=0);;typevalign=[`Top|`Bottom|`Center][@@derivingsexp_of]typehalign=[`Left|`Right|`Center][@@derivingsexp_of]typet=|TextofUtf8_text.t|FillofUchar.t*dims|Hcatoft*t*dims|Vcatoft*t*dims|Ansiofstringoption*t*stringoption*dims[@@derivingsexp_of]letheight=function|Text_->1|Fill(_,d)|Hcat(_,_,d)|Vcat(_,_,d)|Ansi(_,_,_,d)->d.height;;letwidth=function|Texts->Utf8_text.widths|Fill(_,d)|Hcat(_,_,d)|Vcat(_,_,d)|Ansi(_,_,_,d)->d.width;;letuchar_newline=Uchar.of_char'\n'letrecinvariantt=matchtwith|Texts->assert(not(Utf8_text.memsuchar_newline))|Fill(_,dims)->dims_invariantdims|Hcat(t1,t2,dims)->dims_invariantdims;invariantt1;invariantt2;[%test_result:int](heightt1)~expect:dims.height;[%test_result:int](heightt2)~expect:dims.height;[%test_result:int](widtht1+widtht2)~expect:dims.width|Vcat(t1,t2,dims)->dims_invariantdims;invariantt1;invariantt2;[%test_result:int](widtht1)~expect:dims.width;[%test_result:int](widtht2)~expect:dims.width;[%test_result:int](heightt1+heightt2)~expect:dims.height|Ansi(_,t,_,dims)->dims_invariantdims;invariantt;[%test_result:int](widtht)~expect:dims.width;[%test_result:int](heightt)~expect:dims.height;;letfill_genericch~width~height=assert(width>=0);assert(height>=0);Fill(ch,{width;height});;letfill_ucharch~width~height=fill_genericch~width~heightletfillch~width~height=fill_generic(Uchar.of_charch)~width~heightletspace~width~height=fill' '~width~heightletnil=space~width:0~height:0lethstrutwidth=space~width~height:0letvstrutheight=space~height~width:0letdimst={width=widtht;height=heightt}lethalven=letfst=n/2inletsnd=fst+(nmod2)in(* fst + snd = n. snd = either fst or fst + 1 *)fst,snd;;letansi_escape?prefix?suffixt=Ansi(prefix,t,suffix,dimst)lethpad_split~aligndelta=letvaluepad=ifpad=0thenNoneelseSomepadinletkabovebelow=valueabove,valuebelowinifdelta=0thenk00else(matchalignwith|`Left->k0delta|`Right->kdelta0|`Center->leta,b=halvedeltainkab);;lethpadt~(align:halign)delta=assert(delta>=0);letpad_left,pad_right=hpad_split~aligndeltainletheight=heighttinlett=Option.foldpad_left~init:t~f:(funtdelta->Hcat(space~height~width:delta,t,{height;width=widtht+delta}))inlett=Option.foldpad_right~init:t~f:(funtdelta->Hcat(t,space~height~width:delta,{height;width=widtht+delta}))int;;letvpad_split~aligndelta=letvaluepad=ifpad=0thenNoneelseSomepadinletkabovebelow=valueabove,valuebelowinifdelta=0thenk00else(matchalignwith|`Top->k0delta|`Bottom->kdelta0|`Center->leta,b=halvedeltainkab);;letvpadt~aligndelta=assert(delta>=0);letpad_above,pad_below=vpad_split~aligndeltainletwidth=widthtinlett=Option.foldpad_above~init:t~f:(funtdelta->Vcat(space~width~height:delta,t,{width;height=heightt+delta}))inlett=Option.foldpad_below~init:t~f:(funtdelta->Vcat(t,space~width~height:delta,{width;height=heightt+delta}))int;;letmax_heightts=List.foldts~init:0~f:(funacct->Int.maxacc(heightt))letmax_widthts=List.foldts~init:0~f:(funacct->Int.maxacc(widtht))letvalignalignts=leth=max_heighttsinList.mapts~f:(funt->vpad~alignt(h-heightt));;lethalignalignts=letw=max_widthtsinList.mapts~f:(funt->hpad~alignt(w-widtht));;lethcat?(align=`Top)?septs=letts=Option.foldsep~init:ts~f:(funtssep->List.interspersets~sep)inletts=valignaligntsinmatchtswith|[]->nil|t::ts->List.fold~init:tts~f:(funacct->assert(heightacc=heightt);Hcat(acc,t,{height=heightacc;width=widthacc+widtht}));;letvcat?(align=`Left)?septs=letts=Option.foldsep~init:ts~f:(funtssep->List.interspersets~sep)inletts=halignaligntsinmatchtswith|[]->nil|t::ts->List.fold~init:tts~f:(funacct->assert(widthacc=widtht);Vcat(acc,t,{width=widthacc;height=heightacc+heightt}));;lettext_of_lineslines~align=matchlineswith|[line]->Textline|_->lines|>List.map~f:(funline->Textline)|>vcat~align;;letutf8_space=Utf8_text.of_string" "letword_wrapline~max_width=Utf8_text.splitline~on:' '|>List.filter~f:(Fn.nonUtf8_text.is_empty)|>List.fold~init:(Fqueue.empty,Fqueue.empty,0)~f:(fun(lines,line,len)word->letn=Utf8_text.widthwordinletn'=len+1+ninifn'>max_widththenFqueue.enqueuelinesline,Fqueue.singletonword,nelselines,Fqueue.enqueuelineword,n')|>(fun(lines,line,_)->Fqueue.enqueuelinesline)|>Fqueue.map~f:(funline->Fqueue.to_listline|>Utf8_text.concat~sep:utf8_space)|>Fqueue.to_list;;lettext?(align=`Left)?max_widthstr=lettxt=Utf8_text.of_stringstrinletlines=ifUtf8_text.memtxtuchar_newlinethenUtf8_text.split~on:'\n'txtelse[txt]inletlines=matchmax_widthwith|None->lines|Somemax_width->List.concat_maplines~f:(word_wrap~max_width)intext_of_lineslines~align;;(* an abstract renderer, instantiated once to compute line lengths and then again to
actually produce a string.
[line_length] is a number of bytes rather than a number of visible characters. The two
may differ in case of proper unicode [Text] or [Ansi] escape sequences. *)letrender_abstractt~write_direct~line_length=forj=0toheightt-1dowrite_directuchar_newline(line_lengthj)j~num_bytes:1done;letnext_i=Array.init(heightt)~f:(fun_->0)inletadd_charcj=leti=next_i.(j)inletnum_bytes=Uchar.utf8_byte_lengthcinnext_i.(j)<-i+num_bytes;write_directcij~num_bytesinletwrite_stringtxtj=Utf8_text.itertxt~f:(funuchar->add_charucharj)inletrecauxtj_offset=matchtwith|Texts->write_stringsj_offset|Fill(ch,d)->for_i=0tod.width-1doforj=0tod.height-1doadd_charch(j+j_offset)donedone|Vcat(t1,t2,_)->auxt1j_offset;auxt2(j_offset+heightt1)|Hcat(t1,t2,_)->auxt1j_offset;auxt2j_offset|Ansi(prefix,t,suffix,_)->letvcopys=Option.iters~f:(funs->forj=0toheightt-1dowrite_strings(j+j_offset)done)invcopy(Option.map~f:Utf8_text.of_stringprefix);auxtj_offset;vcopy(Option.map~f:Utf8_text.of_stringsuffix)inauxt0;;letline_lengthst=letheight=heighttinletr=Array.create~len:height0inletwrite_directcij~num_bytes=letis_whitespace=matchUchar.to_charcwith|None->false|Somec->Char.is_whitespacecinifnotis_whitespacethenr.(j)<-Int.maxr.(j)(i+num_bytes)inletline_length_=-1(* doesn't matter *)inrender_abstractt~write_direct~line_length;r;;letpoke_uchar:bytes->Uchar.t->pos:int->unit=letbuffer=Caml.Buffer.create4infunbytesc~pos->Uutf.Buffer.add_utf_8bufferc;fork=0toBuffer.lengthbuffer-1doBytes.setbytes(k+pos)(Buffer.nthbufferk)done;Buffer.clearbuffer;;letrendert=letheight=heighttinifheight=0then""else(letline_lengths=line_lengthstinletline_offsets,buflen=letr=Array.create~len:height0inletline_offsetj=r.(j-1)+line_lengths.(j-1)+1inforj=1toheight-1dor.(j)<-line_offsetjdone;r,line_offsetheightinletbuf=Bytes.makebuflen' 'inletwrite_directcij~num_bytes:_=ifUchar.equalcuchar_newline||i<line_lengths.(j)thenpoke_ucharbufc~pos:(i+line_offsets.(j))inletline_lengthj=line_lengths.(j)inrender_abstractt~write_direct~line_length;Bytes.unsafe_to_string~no_mutation_while_string_reachable:buf);;(* header compression *)letrecconsx=function|[]->[x]|y::zs->ifheightx<heightythenx::y::zselsecons(hcat~align:`Bottom[x;y])zs;;letrows_of_colscols~sep_width=cols|>List.transpose_exn|>List.map~f:(funrow->hcatrow~sep:(hstrutsep_width));;letcompress_table_header?(sep_width=2)(`Colscols)=letcols=List.mapcols~f:(fun(header,data,align)->header,Int.max1(max_widthdata),halignaligndata)inletheader=hcat~align:`Bottom(List.fold_rightcols~init:[]~f:(fun(header,max_width,_)stairs->letrecloopstairsacc=letstop()=cons(vcat~align:`Left[header;acc])stairsinmatchstairswith|[]->stop()|x::rest->ifwidthheader+sep_width<=widthaccthenstop()elselooprest(hcat[vcat~align:`Left[fill'|'~width:1~height:(heightx-heightacc);acc];x])inloopstairs(vcat~align:`Left[text"|";hstrut(max_width+sep_width)])))inletrows=List.mapcols~f:(fun(_,_,data)->data)|>rows_of_cols~sep_widthin`Headerheader,`Rowsrows;;lettable?(sep_width=2)(`Colscols)=letcols=List.mapcols~f:(fun(data,align)->halignaligndata)inletrows=rows_of_colscols~sep_widthin`Rowsrows;;(* Produces one of a family of unicode characters that look like
,--U--.
| U | with U filled in if [up] is passed,
| U | D filled in if [down] is passed,
LLLoRRR L filled in if [left] is passed,
| D | R filled in if [right] is passed, and
| D | o filled in if any of the above are passed.
`--D--'
*)letbox_char?up?down?left?right()=letboolify=function|None->false|Some()->trueinletup=boolifyupinletdown=boolifydowninletleft=boolifyleftinletright=boolifyrightinmatchup,down,left,rightwith|false,false,true,true->Uchar.of_scalar_exn0x2500|true,true,false,false->Uchar.of_scalar_exn0x2502|false,true,false,true->Uchar.of_scalar_exn0x250c|false,true,true,false->Uchar.of_scalar_exn0x2510|true,false,false,true->Uchar.of_scalar_exn0x2514|true,false,true,false->Uchar.of_scalar_exn0x2518|true,true,false,true->Uchar.of_scalar_exn0x251c|true,true,true,false->Uchar.of_scalar_exn0x2524|false,true,true,true->Uchar.of_scalar_exn0x252c|true,false,true,true->Uchar.of_scalar_exn0x2534|true,true,true,true->Uchar.of_scalar_exn0x253c|false,false,true,false->Uchar.of_scalar_exn0x2574|true,false,false,false->Uchar.of_scalar_exn0x2575|false,false,false,true->Uchar.of_scalar_exn0x2576|false,true,false,false->Uchar.of_scalar_exn0x2577|false,false,false,false->Uchar.of_char' ';;moduleBoxed=struct(* The representation of a boxed text block is a generalization of [box_char] where
there may be more than one place where it "pokes out" on each side. The four
directional int lists give all such positions.
It isn't until we call [wrap] at the very end that the final border goes around the
whole thing.
*)typenonrect={contents:t(* what goes inside the box *);ups:intlist(* list of column positions: 0-indexed *);downs:intlist(* list of column positions: 0-indexed *);lefts:intlist(* list of row positions: 0-indexed *);rights:intlist(* list of row positions: 0-indexed *)}[@@derivingsexp_of]letcell?(hpadding=1)?(vpadding=0)contents=(* add any horizontal padding *)letcontents=ifhpadding>0thenvcat~align:`Center[contents;hstrut(widthcontents+(2*hpadding))]elsecontentsin(* add any vertical padding *)letcontents=ifvpadding>0thenhcat~align:`Center[contents;vstrut(heightcontents+(2*vpadding))]elsecontentsin{contents;ups=[];downs=[];lefts=[];rights=[]};;letbox_char?(height=1)?(width=1)?up?down?left?right()=fill_uchar~height~width(box_char?up?down?left?right());;letulcorner=box_char()~down:()~right:()leturcorner=box_char()~down:()~left:()letllcorner=box_char()~up:()~right:()letlrcorner=box_char()~up:()~left:()lethline?(ups=[])?(downs=[])~width()=letups=Int.Set.of_listupsinletdowns=Int.Set.of_listdownsinhcat(List.initwidth~f:(funi->box_char()~left:()~right:()?up:(Option.some_if(Set.memupsi)())?down:(Option.some_if(Set.memdownsi)())));;letvline?(lefts=[])?(rights=[])~height()=letlefts=Int.Set.of_listleftsinletrights=Int.Set.of_listrightsinvcat(List.initheight~f:(funi->box_char()~up:()~down:()?left:(Option.some_if(Set.memleftsi)())?right:(Option.some_if(Set.memrightsi)())));;(* put a border around the whole thing *)letwrap{contents;ups;downs;lefts;rights}=letwidth=widthcontentsinletheight=heightcontentsin(* all directions are opposite from the border's perspective *)vcat[hcat[ulcorner;hline~downs:ups~width();urcorner];hcat[vline~rights:lefts~height();contents;vline~lefts:rights~height()];hcat[llcorner;hline~ups:downs~width();lrcorner]];;(* a helper common to vcat/hcat below to concatenate lists of "poke out" positions along
the same dimension as that of the concatenation. *)letconcat_frillsprojectwidth_or_height~ts~n=List.init((2*n)-1)~f:Fn.id|>List.fold~init:(0,[])~f:(fun(sum,vals)i->letsum,new_vals=ifi%2=0then(lett=ts.(i/2)inletvals=List.map(projectt)~f:(funj->j+sum)insum+width_or_heightt.contents,vals)else(letvals=[sum]insum+1,vals)insum,List.rev_appendnew_valsvals)|>snd|>List.rev;;lethpadt~aligndelta=letpad_left,pad_right=hpad_split~aligndeltainletacc=t.contentsinletheight=heightaccinletrecpaddingi~frills~width=letprepend_space~heightacc=ifheight=0thenaccelsespace~width~height::accinmatchfrillswith|[]->prepend_space~height:(height-i)[]|hd::tl->prepend_space~height:(hd-i)(box_char~width~left:()~right:()()::padding(hd+1)~frills:tl~width)inletpadding~frills~width=vcat(padding0~frills~width)inletacc=Option.foldpad_left~init:acc~f:(funaccdelta->Hcat(padding~frills:t.lefts~width:delta,acc,{height;width=widthacc+delta}))inletacc=Option.foldpad_right~init:acc~f:(funaccdelta->Hcat(acc,padding~frills:t.rights~width:delta,{height;width=widthacc+delta}))inacc;;letvcat?(align=`Left)ts=ifList.is_emptytsthencellnilelse(letmax_width=List.foldts~init:0~f:(funacct->Int.maxacc(widtht.contents))inletts=List.mapts~f:(funt->letcontents=t.contentsinletpadding=max_width-widthcontentsinletcontents=hpad~aligntpaddinginletshift=letoffset=matchalignwith|`Left->0|`Center->fst(halvepadding)|`Right->paddinginList.map~f:(funn->offset+n)in{twithcontents;ups=shiftt.ups;downs=shiftt.downs})inletts=Array.of_listtsinletn=Array.lengthtsinletcontents=vcat(List.init((2*n)-1)~f:(funi->ifi%2=0thents.(i/2).contentselse(letprev_t=ts.((i-1)/2)inletnext_t=ts.((i+1)/2)in(* directions flipped for the same reason as in [wrap] *)hline~ups:prev_t.downs~downs:next_t.ups~width:max_width())))inletlefts_or_rightsproject=concat_frillsprojectheight~ts~nin{contents;ups=ts.(0).ups;downs=ts.(n-1).downs;lefts=lefts_or_rights(funt->t.lefts);rights=lefts_or_rights(funt->t.rights)});;letvpadt~aligndelta=letpad_above,pad_below=vpad_split~aligndeltainletacc=t.contentsinletwidth=widthaccinletrecpaddingi~frills~height=letprepend_space~widthacc=ifwidth=0thenaccelsespace~width~height::accinmatchfrillswith|[]->prepend_space~width:(width-i)[]|hd::tl->prepend_space~width:(hd-i)(box_char~height~up:()~down:()()::padding(hd+1)~frills:tl~height)inletpadding~frills~height=hcat(padding0~frills~height)inletacc=Option.foldpad_above~init:acc~f:(funaccdelta->Vcat(padding~frills:t.ups~height:delta,acc,{width;height=heightacc+delta}))inletacc=Option.foldpad_below~init:acc~f:(funaccdelta->Vcat(acc,padding~frills:t.downs~height:delta,{width;height=heightacc+delta}))inacc;;lethcat?(align=`Top)ts=ifList.is_emptytsthencellnilelse(letmax_height=List.foldts~init:0~f:(funacct->Int.maxacc(heightt.contents))inletts=List.mapts~f:(funt->letcontents=t.contentsinletpadding=max_height-heightcontentsinletcontents=vpad~aligntpaddinginletshift=letoffset=matchalignwith|`Top->0|`Center->fst(halvepadding)|`Bottom->paddinginList.map~f:(funn->offset+n)in{twithcontents;lefts=shiftt.lefts;rights=shiftt.rights})inletts=Array.of_listtsinletn=Array.lengthtsinletcontents=hcat(List.init((2*n)-1)~f:(funi->ifi%2=0thents.(i/2).contentselse(letprev_t=ts.((i-1)/2)inletnext_t=ts.((i+1)/2)in(* directions flipped for the same reason as in [wrap] *)vline~lefts:prev_t.rights~rights:next_t.lefts~height:max_height())))inletups_or_downsproject=concat_frillsprojectwidth~ts~nin{contents;lefts=ts.(0).lefts;rights=ts.(n-1).rights;ups=ups_or_downs(funt->t.ups);downs=ups_or_downs(funt->t.downs)});;endletboxed=Boxed.wrap(* convenience definitions *)letvsep=vstrut1lethsep=hstrut1letindent?(n=2)t=hcat[hstrutn;t]letsexpsexp_of_aa=sexp_of_aa|>Sexp.to_string_hum|>textlettextf?align?max_widthfmt=ksprintf(text?align?max_width)fmtmoduleList_with_static_lengths=structtype('a,'shape)t=|[]:(_,[`nil])t|(::):'a*('a,'shape)t->('a,[`consof'shape])tletrecto_list:typeashape.(a,shape)t->alist=function|[]->[]|hd::tl->hd::to_listtl;;letrecof_same_length_list_exn:typeashape.(a,shape)t->alist->(a,shape)t=funtlist->matchtwith|[]->ifnot(List.is_emptylist)thenfailwith"list is too long";[]|_::t_tl->(matchlistwith|[]->failwith"list is too short"|list_hd::list_tl->list_hd::of_same_length_list_exnt_tllist_tl);;endmoduleWith_static_lengths=structletmakealignalignmentstatic_length_list=List_with_static_lengths.of_same_length_list_exnstatic_length_list(alignalignment(List_with_static_lengths.to_liststatic_length_list));;lethalignh=makehalignhletvalignv=makevalignvmoduleList=List_with_static_lengthsend