123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495openOdoc_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*tletnoop=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)|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_->loop(h::acc)t|Linebreak->loopacct|Styled(sty,content)->leth={hwithdesc=Styled(sty,List.rev@@loop[]content)}inloop(h::acc)t|Link(_,content)|InternalLink(Resolved(_,content))|InternalLink(Unresolvedcontent)->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|_->noopletrecsource_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(Resolved(_,content)|Unresolvedcontent)->font"CI"(inline@@stripcontent)++inlinerest|Sourcecontent->source_codecontent++inlinerest|Raw_markupt->raw_markupt++inlinerest)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|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|Sourcecontent->env"EX""EE"""(source_codecontent)++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}=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{title=_;header=_;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=_;content;doc}->letdecl=documentedSrccontentinletdoc=matchdocwith|[]->noop|doc->env"fi""nf"""(indent2(break++blockdoc))indecl++doc++continuerest|Include{attr=_;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.statusthenSome0elseNoneletpage{Page.title;header;items=i;url}=reset_heading();letheader=Shift.compute~on_subheaderinleti=Shift.compute~on_subiinmacro"TH"{|%s 3 "" "Odoc" "OCaml Library"|}title++macro"SH""Name"++str"%s"(String.concat"."@@Link.for_printingurl)++macro"SH""Synopsis"++vspace++item~nested:falseheader++macro"SH""Documentation"++vspace++macro"nf"""++item~nested:falseiletrecsubpagesubp=letp=subp.Subpage.contentinifLink.should_inlinep.urlthen[]else[renderp]andrender(p:Page.t)=letp=Doctree.Labels.disambiguate_pagepandchildren=Utils.flatmap~f:subpage@@Subpages.computepinletcontentppf=Format.fprintfppf"%a@."Roff.pp(pagep)inletfilename=Link.as_filenamep.urlin{Renderer.filename;content;children}