123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693openCoreopenPolymoduleColor=structtypet=Console.Ansi.colorendmoduleAttr=structtypet=Console.Ansi.attrendletlist_sum~flst=List.foldlst~init:0~f:(funab->a+fb)letlist_max~flst=List.foldlst~init:0~f:(funab->maxa(fb))moduleEl=struct(* One element in the table. *)typet=Attr.tlist*stringlisttyperow=tlisttypegrid=rowlistletcreateattrstr=attr,String.split_linesstrletwidth(_,lines)=list_max~f:String.lengthlinesletheightwidth(_,lines)~display_empty_rows=letheight=list_sumlines~f:(funs->max((String.lengths+(width-1))/maxwidth1)1)inifdisplay_empty_rowsthenmaxheight1elseheight;;letrecsliceswidthlines=matchlineswith|[]->[]|line::lines->slices_splitwidthlinesline(String.lengthline)0andslices_splitwidthlineslineline_lenpos=letchunk_len=minwidth(line_len-pos)inletcompletely_fits=Int.(=)chunk_len(line_len-pos)inletchunk=String.subline~pos~len:chunk_leninifcompletely_fitsthenchunk::sliceswidthlineselsechunk::slices_splitwidthlineslineline_len(pos+width);;endmoduleAlign=structtypet=|Left|Right|Centerendtypeshow=[`Yes|`No|`If_not_empty]moduleColumn=structtype'at={max_text_width:int;header:string;col_func:'a->El.t;align:Align.t;min_width:intoption;show:show}letheadert=t.headerletto_datat=t.col_functypeconstraints={total_width:int;min_widths:(string*int)list}[@@derivingsexp]exceptionImpossible_table_constraintsofconstraints[@@derivingsexp]letcreate_attr?(align=Align.Left)?min_width?(max_width=90)?(show=`Yes)strparse_func={max_text_width=max_width+1;header=str;col_func=(funx->matchparse_funcxwith|a,b->El.createab);align;(* We add one for the '|' on the left. *)min_width=Option.mapmin_width~f:((+)1);show};;letcreate?(align=Align.Left)?min_width?(max_width=90)?showstrparse_func=create_attr?min_width~align~max_width?showstr(funx->[],parse_funcx);;letheader_to_elalstt=El.createalstt.headerletmakecol_valt=t.col_funccol_valletdesired_width~spacingdatat=letcolumn_data=List.mapdata~f:t.col_funcinletheader_width=String.splitt.header~on:'\n'|>list_max~f:String.lengthin(* We need to account for the '|' to the left, so we add 1 plus the spacing
on either side. *)1+(2*spacing)+min(t.max_text_width-(1+(2*spacing)))(maxheader_width(list_maxcolumn_data~f:El.width));;letlayout~spacingtable_widthtsdata=letdesired_widths=List.mapts~f:(desired_width~spacingdata)inletall_min_width=List.filter_mapts~f:(funt->t.min_width)in(* [generic_min_chars] = minimum number of characters for a column that doesn't have
an [min_width] value. *)lettable_constraints_are_impossible,generic_min_chars=letcolumns_with_no_min_width=List.lengthts-List.lengthall_min_widthinifInt.(<>)0columns_with_no_min_width(* need to avoid a divide-by-zero *)then(letwidth=table_width-list_sumall_min_width~f:Fn.idinletgeneric_min_chars=width/columns_with_no_min_widthinletimpossible=generic_min_chars<1+(1+(spacing*2))inimpossible,generic_min_chars)else(letmin_total=List.fold~init:0all_min_width~f:Int.(+)inletextra_per_col=1+1+(spacing*2)inletimpossible=table_width<min_total+(List.lengthts*extra_per_col)in(* the zero is a nonsense value, but we only generate it when every column has a
min width and therefore this zero will never be used. *)impossible,0)iniftable_constraints_are_impossiblethenraise(Impossible_table_constraints{total_width=table_width+1;min_widths=List.filter_mapts~f:(funt->Option.mapt.min_width~f:(funnum_chars->t.header,num_chars))});letleft=ref(list_sum~f:Fn.iddesired_widths-table_width)inletstop=reffalsein(* This layout algorithm looks unbearably inefficient, but it's
simple and works reasonably well in the common case. *)letrecdecide_widthsdesired_widths=if!stopthendesired_widthselse(stop:=true;assert(List.lengthts=List.lengthdesired_widths);decide_widths(List.map2_exntsdesired_widths~f:(funtcolumn_width->letmin_chars=matcht.min_widthwith|Somex->x|None->generic_min_charsinifcolumn_width<=min_chars||!left<=0thencolumn_widthelse(left:=!left-1;stop:=false;column_width-1))))in(* The widths used in [loop] include the '|' to the left of each element,
which isn't important after layout, so we subtract off 1 and the spacing
on either side. *)List.map~f:(funx->x-(1+(spacing*2)))(decide_widthsdesired_widths);;endmoduleTable_char=structtypet={ascii:char;utf8:string}letconnect?top?bottom?left?right()=lettop,bottom,left,right=is_sometop,is_somebottom,is_someleft,is_somerightinletascii,utf8=matchtop,bottom,left,rightwith|false,false,true,true->'-',"\226\148\128"|true,true,false,false->'|',"\226\148\130"|false,true,false,true->'|',"\226\148\140"|false,true,true,false->'|',"\226\148\144"|true,false,false,true->'|',"\226\148\148"|true,false,true,false->'|',"\226\148\152"|true,true,false,true->'|',"\226\148\156"|true,true,true,false->'|',"\226\148\164"|false,true,true,true->'-',"\226\148\172"|true,false,true,true->'-',"\226\148\180"|true,true,true,true->'+',"\226\148\188"|false,false,true,false->'-',"\226\149\180"|true,false,false,false->'|',"\226\149\181"|false,false,false,true->'-',"\226\149\182"|false,true,false,false->'|',"\226\149\183"|false,false,false,false->' '," "in{ascii;utf8};;endmoduleDraw=structtypepoint=|Line|Blank|CharofAttr.tlist*chartypescreen={data:pointarrayarray;rows:int;cols:int}letcreate_screen~rows~cols=letdata=Array.make_matrix~dimx:rows~dimy:cols(Char([],' '))in{data;rows;cols};;letset_screen_point~screen~row~col~point=letc=screen.data.(row).(col)inletnew_point=matchc,pointwith|Blank,_->Blank|_,Blank->Blank|_,point->pointinscreen.data.(row).(col)<-new_point;;lethline~screen~row~col1~col2?(point=Line)()=forcol=col1tocol2doset_screen_point~screen~row~col~pointdone;;letvline~screen~col~row1~row2?(point=Line)()=forrow=row1torow2doset_screen_point~screen~row~col~pointdone;;letchar~screen~row~col~char~attr=screen.data.(row).(col)<-Char(attr,char)letstring~screen~row~col~string~attr=fori=0toString.lengthstring-1dochar~screen~row~col:(col+i)~char:string.[i]~attrdone;;letaligned~screen~row~col~string:str~attr~width~align=letcol=matchalignwith|Align.Left->col|Align.Right->col+width-String.lengthstr|Align.Center->col+(max0(width-String.lengthstr)/2)instring~screen~row~col~string:str~attr;;letget_symbol~screen~row~col=lettop=row>0&&screen.data.(row-1).(col)=Lineinletbottom=row<screen.rows-1&&screen.data.(row+1).(col)=Lineinletleft=col>0&&screen.data.(row).(col-1)=Lineinletright=col<screen.cols-1&&screen.data.(row).(col+1)=LineinTable_char.connect?top:(Option.some_iftop())?bottom:(Option.some_ifbottom())?left:(Option.some_ifleft())?right:(Option.some_ifright())();;letrender~screen~bars~output~close=letbuf=Buffer.create1024inletcurrent_attr=ref[]inletupdate_attrattr=letattr=List.sort~compare:Poly.compareattrinifattr<>!current_attrthen(ifBuffer.lengthbuf>0thenoutput!current_attrbuf;current_attr:=attr)inforrow=0toscreen.rows-1doforcol=0toscreen.cols-1domatchscreen.data.(row).(col)with|Char(attr,ch)->update_attrattr;Buffer.add_charbufch|Blank->Buffer.add_charbuf' '|Line->update_attr[];let{Table_char.ascii;utf8}=get_symbol~screen~row~colin(matchbarswith|`Ascii->Buffer.add_charbufascii|`Unicode->Buffer.add_stringbufutf8)done;update_attr[];Buffer.add_charbuf'\n'done;output!current_attrbuf;closebuf;;letoutput~oc~screen~bars=render~screen~bars~close:ignore~output:(funattrbuf->Console.Ansi.output_stringattroc(Buffer.contentsbuf);Buffer.clearbuf);;letto_string~screen~bars=letbuf=Buffer.create1024inrender~screen~bars~output:(funattrbuf'->Buffer.add_stringbuf(Console.Ansi.string_with_attrattr(Buffer.contentsbuf'));Buffer.clearbuf')~close:(fun_->Buffer.contentsbuf);;letto_string_noattr~screen~bars=render~screen~bars~output:(fun__->())~close:Buffer.contents;;endmoduleDisplay=structtypet=|Short_box|Tall_box|Line|Blank|Column_titlesletshort_box=Short_boxlettall_box=Tall_boxletline=Lineletblank=Blankletcolumn_titles=Column_titlesendmoduleGrid=structtypet={data:El.grid;heights:intlist;widths:intlist;aligns:Align.tlist}letcreate~spacing~displaymax_widthh_attrcolsraw_data~display_empty_rows=letbody=List.mapraw_data~f:(funx->List.mapcols~f:(Column.makex))inletempty=List.foldbody~init:(List.mapcols~f:(fun_->true))~f:(List.map2_exn~f:(funis_empty(_attr,lines)->is_empty&&List.for_alllines~f:(String.equal"")))inletkeep=List.map2_exncolsempty~f:(fun{Column.show;_}is_empty->matchshowwith|`Yes->true|`No->false|`If_not_empty->notis_empty)inletfilterl=List.filter_opt(List.map2_exnkeepl~f:Option.some_if)inletcols=filtercolsinletbody=List.mapbody~f:filterin(* We subtract 1 from max_width because later we're going to add a line of
'|'s to form the right wall of the table. *)letwidths=Column.layout~spacing(max_width-1)colsraw_datainletgrid_data=List.mapcols~f:(Column.header_to_elh_attr)::bodyinletheights=ifdisplay=Display.LinethenList.mapgrid_data~f:(fun_->1)elseList.mapgrid_data~f:(funrow->assert(List.lengthwidths=List.lengthrow);list_max~f:Fn.id(List.map2_exnwidthsrow~f:(El.height~display_empty_rows)))inletaligns=List.mapcols~f:(func->c.Column.align)in{data=grid_data;heights;widths;aligns};;letdraw~spacing~displayt=assert(List.lengtht.data=List.lengtht.heights);letmid_row=ifdisplay=Display.Tall_boxthen1else0in(* The total width of the table includes the '|'s to the left of elements, so we add 1
and the spacing on either side when summing. *)letcols=list_sumt.widths~f:((+)(1+(spacing*2)))+1inletrows=list_sumt.heights~f:((+)mid_row)+3-(2*mid_row)inletscreen=Draw.create_screen~rows~colsinletpoint=ifdisplay=Display.Column_titlesthenDraw.BlankelseDraw.LineinDraw.hline~screen~row:0~col1:0~col2:(cols-1)~point();Draw.hline~screen~row:(rows-1)~col1:0~col2:(cols-1)~point();ifdisplay<>Display.Blankthen(Draw.vline~screen~col:0~row1:0~row2:(rows-1)~point();ignore(List.foldt.widths~init:0~f:(funcolwidth->letcol=col+1+width+(spacing*2)inDraw.vline~screen~col~row1:0~row2:(rows-1)~point();col):int));ignore(List.fold2_exnt.datat.heights~init:1~f:(funrowrow_elementsheight->letheader_row=row=1inignore(List.fold2_exnrow_elements(List.zip_exnt.widthst.aligns)~init:(1+spacing)~f:(funcol(attr,lines)(width,align)->letstrings=El.sliceswidthlinesinifdisplay=Display.Linethen(matchstringswith|[]->()|[string]->Draw.aligned~screen~row~col~attr~string~align~width|string::_->Draw.aligned~screen~row~col~attr~string~align~width;forcol=col+max0(width-3)tocol+width-1doDraw.char~screen~row~col~char:'.'~attr:[]done)elseignore(List.foldstrings~init:row~f:(funrowstring->Draw.aligned~screen~row~col~attr~string~align~width;row+1):int);col+1+(spacing*2)+width):int);letrow=row+heightinifdisplay=Display.Tall_box||header_rowthen(ifdisplay<>Display.BlankthenDraw.hline~screen~row~col1:0~col2:(cols-1)();row+1)elserow):int);screen;;endtype('a,'rest)renderer=?display:Display.t(* Default: short_box *)->?spacing:int(* Default: 1 *)->?limit_width_to:int(* defaults to 90 characters *)->?header_attr:Attr.tlist->?bars:[`Ascii|`Unicode]->?display_empty_rows:bool(* Default: false *)->'aColumn.tlist->'alist->'restletoutput?(display=Display.short_box)?(spacing=1)?(limit_width_to=90)?(header_attr=[])?(bars=`Unicode)?(display_empty_rows=false)colsdata~oc=ifcols=[]then()else(letscreen=Grid.create~spacing~displaylimit_width_toheader_attrcolsdata~display_empty_rows|>Grid.draw~spacing~displayinDraw.output~oc~screen~bars);;letto_string_gen?(display=Display.short_box)?(spacing=1)?(limit_width_to=90)?(header_attr=[])?(bars=`Unicode)?(display_empty_rows=false)colsdata~use_attr=ifcols=[]then""else(letscreen=Grid.create~spacing~displaylimit_width_toheader_attrcolsdata~display_empty_rows|>Grid.draw~spacing~displayinifuse_attrthenDraw.to_string~screen~barselseDraw.to_string_noattr~screen~bars);;letto_string_noattr=to_string_gen~use_attr:falseletto_string=to_string_gen~use_attr:trueletsimple_list_table?(index=false)?(limit_width_to=160)?(oc=stdout)?(display=Display.line)colsdata=letcols,data=ifindexthen"#"::cols,List.mapidata~f:(funirow->Int.to_string(i+1)::row)elsecols,datainletcols=List.mapicols~f:(funicol->letcol,align=matchString.chop_prefixcol~prefix:"-"with|None->col,Align.Right|Somecol->col,Align.LeftinColumn.createcol(funls->List.nth_exnlsi)~align)inoutput~oc~display~limit_width_tocolsdata;;let%test_module_=(modulestructletcol1=Column.create"a"(fun(x,_,_)->x)letcol2=Column.create"b"(fun(_,x,_)->x)letcol3=Column.create"c"(fun(_,_,x)->x)let%expect_test_=letstringifydisplay=to_string~bars:`Ascii~display[col1;col2;col3]["a1","b1","c1";"a2","b2","c2"]inprintf"%s"(stringifyDisplay.short_box);[%expect{|
|--------------|
| a | b | c |
|----+----+----|
| a1 | b1 | c1 |
| a2 | b2 | c2 |
|--------------| |}];printf"%s"(stringifyDisplay.blank);[%expect{|
----------------
a b c
a1 b1 c1
a2 b2 c2
---------------- |}];printf"%s"(stringifyDisplay.column_titles);[%expect{|
a b c
---- ---- ----
a1 b1 c1
a2 b2 c2 |}];;let%expect_test"we keep empty lines if any"=letstringifydisplay=to_string~bars:`Ascii~display[col1;col2;col3]["a1","b_line1\nb_line2\nb_line3","c_line1\n\nc_line3";"a2","b2","c2"]inprintf"%s"(stringifyDisplay.short_box);[%expect{|
|------------------------|
| a | b | c |
|----+---------+---------|
| a1 | b_line1 | c_line1 |
| | b_line2 | |
| | b_line3 | c_line3 |
| a2 | b2 | c2 |
|------------------------| |}];;let%expect_test"trailing newline does not result in an empty line"=letstringifydisplay=to_string~bars:`Ascii~display[col1;col2;col3]["a\n","b\n","c\n"]inprintf"%s"(stringifyDisplay.short_box);[%expect{|
|-----------|
| a | b | c |
|---+---+---|
| a | b | c |
|-----------| |}];;let%expect_test_=letstringifydisplay=to_string~bars:`Ascii~display[col1;col2;col3]["a","b","c\n\n\n\n\n\n\nc"]inprintf"%s"(stringifyDisplay.short_box);[%expect{|
|-----------|
| a | b | c |
|---+---+---|
| a | b | c |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | c |
|-----------| |}];;let%expect_test_=List.iter[true;false]~f:(fundisplay_empty_rows->printf"display empty rows = %b\n"display_empty_rows;to_string~bars:`Ascii[col1;col2;col3]~display_empty_rows["a","b","c";"","","";"d","e","f"]|>printf"%s\n");[%expect{|
display empty rows = true
|-----------|
| a | b | c |
|---+---+---|
| a | b | c |
| | | |
| d | e | f |
|-----------|
display empty rows = false
|-----------|
| a | b | c |
|---+---+---|
| a | b | c |
| d | e | f |
|-----------| |}];;(* test for bug where specifying minimum widths on all columns causes a
Division_by_zero error while calculating generic_min_chars in Column.layout *)let%test_=consttrue(to_string[Column.create~min_width:9"foo"Fn.id]["bar"]);;end);;