123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565openOdoc_documentopenTypesopenDoctree(*
Manpages relies on the (g|t|n)roff document language.
This language has a fairly long history
(see https://en.wikipedia.org/wiki/Groff_(software)).
Unfortunately, this language is very old and quite clunky.
Most manpages relies on a set of high-level macros
(http://man7.org/linux/man-pages/man7/groff_man.7.html)
that attempts to represent the semantic of common constructs in man pages. These
macros are too constraining for the rich ocamldoc markup and
their semantics are quite brittle, making them hard to use in a machine-output
context.
For these reason, we hit the low level commands directly:
- http://man7.org/linux/man-pages/man7/groff.7.html
- http://mandoc.bsd.lv/man/roff.7.html
The downside of these commands is their poor translation to HTML, which we
don't care about.
In the roff language:
1) newlines are not distinguished from other whitespace
2) Successive whitespaces are ignored, except to trigger
"end of sentence detection" for 2 or more successive whitespaces.
3) Commands must start at the beginning of a line.
4) Whitespaces separated by a macro are not treated as a single whitespace.
For all these reasons, We use a concatenative API that will gobble up adjacent
extra whitespaces and never output successive whitespaces at all.
This makes the output much more consistent.
*)moduleRoff=structtypet=|Concatoftlist|Fontofstring*t|Macroofstring*string|Space|Break|Stringofstring|Vspace|Indentofint*t|Align_lineofstring|Table_celloftletnoop=Concat[]letsp=Spaceletbreak=Breakletvspace=Vspaceletappendt1t2=match(t1,t2)with|Concatl1,Concatl2->Concat(l1@l2)|Concatl1,e2->Concat(l1@[e2])|e1,Concatl2->Concat(e1::l2)|e1,e2->Concat[e1;e2]let(++)=appendletconcat=List.fold_left(++)(Concat[])letrecintersperse~sep=function|[]->[]|[h]->[h]|h1::(_::_ast)->h1::sep::intersperse~septletlist?(sep=Concat[])l=concat@@intersperse~seplletindenticontent=Indent(i,content)letmacroidfmt=Format.ksprintf(funs->Macro(id,s))fmt(* copied from cmdliner *)letescapes=(* escapes [s] from doc language. *)letmarkup_text_need_esc=function'.'|'\\'->true|_->falseinletmax_i=String.lengths-1inletrecescaped_lenil=ifi>max_ithenlelseifmarkup_text_need_escs.[i]thenescaped_len(i+1)(l+2)elseescaped_len(i+1)(l+1)inletescaped_len=escaped_len00inifescaped_len=String.lengthsthenselseletb=Bytes.createescaped_leninletrecloopik=ifi>max_ithenBytes.unsafe_to_stringbelseletc=String.unsafe_getsiinifnot(markup_text_need_escc)then(Bytes.unsafe_setbkc;loop(i+1)(k+1))else(Bytes.unsafe_setbk'\\';Bytes.unsafe_setb(k+1)c;loop(i+1)(k+2))inloop00letstrfmt=Format.ksprintf(funs->String(escapes))fmtletescapedfmt=Format.ksprintf(funs->Strings)fmtletenvocargcontent=macroo"%s"arg++content++macroc""letfontscontent=Font(s,content)letfont_stack=Stack.create()letpp_fontppfsfmt=letcommand_fppfs=ifString.lengths=1thenFormat.fprintfppf{|\f%s|}selseFormat.fprintfppf{|\f[%s]|}sinStack.pushsfont_stack;command_fppfs;Format.kfprintf(funppf->ignore@@Stack.popfont_stack;lets=ifStack.is_emptyfont_stackthen"R"elseStack.topfont_stackincommand_fppfs)ppffmtletcollapsex=letskip_spacesl=let_,_,rest=Take.untill~classify:(functionSpace->Skip|_->Stop_and_keep)inrestandskip_spaces_and_breakl=let_,_,rest=Take.untill~classify:(function|Space|Break->Skip|_->Stop_and_keep)inrestandskip_spaces_and_break_and_vspacel=let_,_,rest=Take.untill~classify:(function|Space|Break|Vspace->Skip|_->Stop_and_keep)inrestinletrecloopaccl=matchlwith(* | (Space | Break) :: (Macro _ :: _ as t) ->
* loop acc t *)|Vspace::_->letrest=skip_spaces_and_break_and_vspacelinloop(Vspace::acc)rest|Break::_->letrest=skip_spaces_and_breaklinloop(Break::acc)rest|Space::_->letrest=skip_spaceslinloop(Space::acc)rest|Concatl::rest->loopacc(l@rest)|(Macro_ash)::rest->letrest=skip_spacesrestinloop(h::acc)rest|[]->acc|h::t->loop(h::acc)tinList.rev@@loop[][x]letrecnext_is_macro=function|(Vspace|Break|Macro_)::_->true|Concatl::_->next_is_macrol|Font(_,content)::_|Indent(_,content)::_->next_is_macro[content]|_->falseletpp_macroppfsfmt=Format.fprintfppf("@\n.%s "^^fmt)sletpp_indentppfindent=ifindent=0then()elsepp_macroppf"ti""+%d"indentletnewline_ifppfb=ifbthenFormat.pp_force_newlineppf()else()letppppft=letrecmany~indentppfl=matchlwith|[]->()|h::t->letis_macro=next_is_macrotin(matchhwith|Concatl->many~indentppfl|Strings->Format.pp_print_stringppfs|Font(s,t)->pp_fontppfs"%a"(one~indent)t|Space->Format.fprintfppf" "|Break->pp_macroppf"br""";pp_indentppfindent;newline_ifppf(notis_macro)|Vspace->pp_macroppf"sp""";pp_indentppfindent;newline_ifppf(notis_macro)|Macro(s,args)->pp_macroppfs"%s"args;newline_ifppf(notis_macro)|Align_lines->Format.pp_print_stringppf(s^".");newline_ifppf(notis_macro)|Table_cellc->Format.pp_print_textppf"T{\n";one~indentppfc;Format.pp_print_textppf"\nT}"|Indent(i,content)->letindent=indent+iinone~indentppfcontent);many~indentppftandone~indentppfx=many~indentppf@@collapsexinFormat.pp_set_marginppfmax_int;one~indent:0ppftendopenRoffletstyle(style:style)content=matchstylewith|`Bold->font"B"content|`Italic->font"I"content(* We ignore those *)|`Emphasis|`Superscript|`Subscript->content(* Striped content should be rendered in one line, without styling *)letstripl=letrecloopacc=function|[]->acc|h::t->(matchh.Inline.descwith|Text_|Entity_|Raw_markup_|Math_->loop(h::acc)t|Linebreak->loopacct|Styled(sty,content)->leth={hwithdesc=Styled(sty,List.rev@@loop[]content)}inloop(h::acc)t|Link(_,content)|InternalLink{content;_}->letacc=loopacccontentinloopacct|Sourcecode->letacc=loop_sourceacccodeinloopacct)andloop_sourceacc=function|[]->acc|Source.Eltcontent::t->loop_source(List.rev_appendcontentacc)t|Source.Tag(_,content)::t->letacc=loop_sourceacccontentinloop_sourceacctinList.rev@@loop[]l(* Partial support for now *)letentitye=matchewith"#45"->escaped"\\-"|"gt"->str">"|s->str"&%s;"s(* Should hopefully make people notice and report *)letraw_markup(t:Raw_markup.t)=lettarget,content=tinmatchAstring.String.Ascii.lowercasetargetwith|"manpage"|"troff"|"roff"->Stringcontent|_->noopletmath(s:Types.Math.t)=Stringsletrecsource_code(s:Source.t)=matchswith|[]->noop|h::t->(matchhwith|Source.Elti->inline(stripi)++source_codet|Tag(None,s)->source_codes++source_codet|Tag(Some_,s)->font"CB"(source_codes)++source_codet)andinline(l:Inline.t)=matchlwith|[]->noop|i::rest->(matchi.descwith|Text""->inlinerest|Text_->letl,_,rest=Doctree.Take.untill~classify:(function|{Inline.desc=Texts;_}->Accum[s]|_->Stop_and_keep)instr{|%s|}(String.concat""l)++inlinerest|Entitye->letx=entityeinx++inlinerest|Linebreak->break++inlinerest|Styled(sty,content)->stylesty(inlinecontent)++inlinerest|Link(href,content)->env"UR""UE"href(inline@@stripcontent)++inlinerest|InternalLink{content;_}->font"CI"(inline@@stripcontent)++inlinerest|Sourcecontent->source_codecontent++inlinerest|Maths->maths++inlinerest|Raw_markupt->raw_markupt++inlinerest)lettablepp{Table.data;align}=letsep='\t'inletalignment=letalignment=matchalignwith|align->List.map(function(* Since we are enclosing cells in text blocks, the alignment has
no effect on the content of a sufficiently big cell, for some
reason... (see the markup test in generators)
One solution would be to use the [m] column specifier to apply
a macro to the text blocks of the columns. Those macros would
be [lj], [ce] or [rj], which define alignment. However, this
breaks both the alignment for small table cells, and the
largeness of columns. For the records, it woulb be:
{[
| Some `Left -> "lmlj"
| Some `Center -> "cmce"
| Some `Right -> "rmrj"
| None -> "l"
]} *)|Table.Left->"l"|Center->"c"|Right->"r"|Default->"l")aligninAlign_line(String.concat""alignment)inenv"TS""TE"""(str"allbox;"++alignment++List.fold_left(funaccrow->acc++vspace++matchrowwith|[]->noop|(h,_)::t->List.fold_left(funacc(x,_)->acc++str"%c"sep++Table_cell(ppx))(Table_cell(pph))t)noopdata)letrecblock(l:Block.t)=matchlwith|[]->noop|b::rest->(letcontinuer=ifr=[]thennoopelsevspace++blockrinmatchb.descwith|Inlinei->inlinei++continuerest|Paragraphi->inlinei++continuerest|List(list_typ,l)->letfnb=letbullet=matchlist_typwith|Unordered->escaped{|\(bu|}|Ordered->str"%d)"(n+1)inindent2(bullet++sp++blockb)inlist~sep:break(List.mapifl)++continuerest|Tablet->tableblockt++continuerest|Description_->letdescrs,_,rest=Take.untill~classify:(function|{Block.desc=Descriptionl;_}->Accuml|_->Stop_and_keep)inletfi=letkey=inlinei.Description.keyinletdef=blocki.Description.definitioninindent2(str"@"++key++str":"++sp++def)inlist~sep:break(List.mapfdescrs)++continuerest|Source(_,content)->env"EX""EE"""(source_codecontent)++continuerest|Maths->maths++continuerest|Verbatimcontent->env"EX""EE"""(str"%s"content)++continuerest|Raw_markupt->raw_markupt++continuerest)letnext_heading,reset_heading=letheading_stack=ref[]inletrecsucc_headingil=match(i,l)with|1,[]->[1]|_,[]->1::succ_heading(i-1)[]|1,n::_->[n+1]|i,n::t->n::succ_heading(i-1)tinletprint_headingl=String.concat"."@@List.mapstring_of_intlinletnextlevel=letnew_heading=succ_headinglevel!heading_stackinheading_stack:=new_heading;print_headingnew_headingandreset()=heading_stack:=[]in(next,reset)letheading~nested{Heading.label=_;level;title;source_anchor=_}=letprefix=iflevel=0thennoopelseiflevel<=3thenstr"%s "(next_headinglevel)elsenoopinifnotnestedthenmacro"in""%d"(level+2)++font"B"(prefix++inline(striptitle))++macro"in"""elsefont"B"(prefix++inline(striptitle))letexpansion_not_inlinedurl=not(Link.should_inlineurl)lettake_codel=letc,_,rest=Take.untill~classify:(function|DocumentedSrc.Codec->Accumc|DocumentedSrc.Alternative(Expansione)whenexpansion_not_inlinede.url->Accume.summary|_->Stop_and_keep)in(c,rest)letinline_subpage=function|`Inline|`Open|`Default->true|`Closed->falseletrecdocumentedSrc(l:DocumentedSrc.t)=matchlwith|[]->noop|line::rest->(letbreak_if_nonemptyr=ifr=[]thennoopelsebreakinletcontinuer=documentedSrcrinmatchlinewith|Code_->letc,rest=take_codelinsource_codec++continuerest|Alternativealt->(matchaltwith|Expansion{expansion;url;_}->ifexpansion_not_inlinedurlthenletc,rest=take_codelinsource_codec++continuerestelsedocumentedSrcexpansion)|Subpagep->subpagep.content++continuerest|Documented_|Nested_->letlines,_,rest=Take.untill~classify:(function|DocumentedSrc.Documented{code;doc;_}->Accum[(`Dcode,doc)]|DocumentedSrc.Nested{code;doc;_}->Accum[(`Ncode,doc)]|_->Stop_and_keep)inletf(content,doc)=letdoc=matchdocwith|[]->noop|doc->indent2(break++str"(*"++sp++blockdoc++sp++str"*)")inletcontent=matchcontentwith|`Dcode->inlinecode|`Nl->indent2(documentedSrcl)incontent++docinletl=list~sep:break(List.mapflines)inindent2(break++l)++break_if_nonemptyrest++continuerest)andsubpage{preamble=_;items;url=_;_}=letcontent=itemsinletsurroundbody=ifcontent=[]thenspelseindent2(break++body)++breakinsurround@@item~nested:truecontentanditem~nested(l:Item.tlist)=matchlwith|[]->noop|i::rest->(letcontinuer=ifr=[]thennoopelsevspace++item~nestedrinmatchiwith|Textb->letd=env"fi""nf"""(blockb)ind++continuerest|Headingh->leth=heading~nestedhinvspace++h++vspace++item~nestedrest|Declaration{attr=_;anchor=_;source_anchor=_;content;doc}->letdecl=documentedSrccontentinletdoc=matchdocwith|[]->noop|doc->env"fi""nf"""(indent2(break++blockdoc))indecl++doc++continuerest|Include{attr=_;anchor=_;source_anchor=_;content={summary;status;content};doc;}->letd=ifinline_subpagestatusthenitem~nestedcontentelselets=source_codesummaryinmatchdocwith|[]->s|doc->s++indent2(break++blockdoc)ind++continuerest)leton_subsubp=matchsubpwith|`Pagep->ifLink.should_inlinep.Subpage.content.urlthenSome1elseNone|`Includeincl->ifinline_subpageincl.Include.statusthenSome0elseNoneletpagep=reset_heading();letheader=Doctree.PageTitle.render_titlep@Shift.compute~on_subp.preambleinleti=Shift.compute~on_subp.itemsinmacro"TH"{|%s 3 "" "Odoc" "OCaml Library"|}p.url.name++macro"SH""Name"++str"%s"(String.concat"."@@Link.for_printingp.url)++macro"SH""Synopsis"++vspace++item~nested:falseheader++macro"SH""Documentation"++vspace++macro"nf"""++item~nested:falseiletrecsubpagesubp=letp=subp.Subpage.contentinifLink.should_inlinep.urlthen[]else[render_pagep]andrender_page(p:Page.t)=letp=Doctree.Labels.disambiguate_page~enter_subpages:truepandchildren=Utils.flatmap~f:subpage@@Subpages.computepinletcontentppf=Format.fprintfppf"%a@."Roff.pp(pagep)inletfilename=Link.as_filenamep.urlin{Renderer.filename;content;children}letrender=function|Document.Pagepage->[render_pagepage]|Source_page_|Asset_->[]