123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197open!Coreopen!ImportincludeColumn_intftype'at={max_width:int;header:Utf8_text.t;col_func:'a->Cell.t;align:Align.t;min_width:intoption;show:Show.t}[@@derivingfields,sexp_of]letliftt~f={twithcol_func=(funx->t.col_func(fx))}letheadert=Utf8_text.to_stringt.headerletto_datata=letattr,lines=Cell.to_tuple(t.col_funca)inattr,List.maplines~f:Utf8_text.to_string;;typeconstraints={total_width:int;min_widths:(Utf8_text.t*int)list}[@@derivingsexp_of]exceptionImpossible_table_constraintsofconstraints[@@derivingsexp_of]letcreate_attr?(align=Align.Left)?min_width?(max_width=90)?(show=`Yes)strparse_func={max_width;header=Utf8_text.of_stringstr;col_func=(funx->matchparse_funcxwith|a,b->Cell.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);;letto_cellt~value=t.col_funcvalueletdesired_width~spacingdatat=letcolumn_data=List.mapdata~f:t.col_funcinletheader_width=Utf8_text.splitt.header~on:'\n'|>list_max~f:Utf8_text.widthin(* 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_width-(2*spacing))(maxheader_width(list_maxcolumn_data~f:Cell.width));;letlayouttsdata~spacing~max_width:table_width=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:(funmin_width->t.header,min_width))});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);;moduleOf_field=structletfield?align?min_width?max_width?show?headerto_stringrecord_field=create?align?min_width?max_width?show(Option.valueheader~default:(Field.namerecord_field))(funrecord->to_string(Field.getrecord_fieldrecord));;letfield_attr?align?min_width?max_width?show?headerto_string_and_attrrecord_field=create_attr?align?min_width?max_width?show(Option.valueheader~default:(Field.namerecord_field))(funrecord->to_string_and_attr(Field.getrecord_fieldrecord));;letfield_opt?align?min_width?max_width?show?headerto_stringrecord_field=field?align?min_width?max_width?show?header(function|None->""|Somex->to_stringx)record_field;;letfield_opt_attr?align?min_width?max_width?show?headerto_string_and_attrrecord_field=field_attr?align?min_width?max_width?show?header(function|None->[],""|Somex->to_string_and_attrx)record_field;;endmodulePrivate=structletlayout=layoutletto_cell=to_cellend