diff --git a/src/voodoo-gen/generator.ml b/src/voodoo-gen/generator.ml new file mode 100644 index 00000000..dc7497b7 --- /dev/null +++ b/src/voodoo-gen/generator.ml @@ -0,0 +1,420 @@ +(* + * Copyright (c) 2016 Thomas Refis + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + *) + +open Odoc_document.Types +module Html = Tyxml.Html +module Doctree = Odoc_document.Doctree + +type any = Html_types.flow5 +type item = Html_types.flow5_without_header_footer +type flow = Html_types.flow5_without_sectioning_heading_header_footer +type phrasing = Html_types.phrasing +type non_link_phrasing = Html_types.phrasing_without_interactive + +let mk_anchor_link id = + [ Html.a ~a:[ Html.a_href ("#" ^ id); Html.a_class [ "anchor" ] ] [] ] + +let mk_anchor anchor = + match anchor with + | None -> ([], []) + | Some { Odoc_document.Url.Anchor.anchor; _ } -> + let link = mk_anchor_link anchor in + let attrib = [ Html.a_id anchor; Html.a_class [ "anchored" ] ] in + (attrib, link) + +let class_ (l : Class.t) = if l = [] then [] else [ Html.a_class l ] + +and raw_markup (t : Raw_markup.t) = + let target, content = t in + match Astring.String.Ascii.lowercase target with + | "html" -> + (* This is OK because we output *textual* HTML. + In theory, we should try to parse the HTML with lambdasoup and rebuild + the HTML tree from there. + *) + [ Html.Unsafe.data content ] + | _ -> [] + +and source k ?a (t : Source.t) = + let rec token (x : Source.token) = + match x with + | Elt i -> k i + | Tag (None, l) -> + let content = tokens l in + if content = [] then [] else [ Html.span content ] + | Tag (Some s, l) -> [ Html.span ~a:[ Html.a_class [ s ] ] (tokens l) ] + and tokens t = Utils.list_concat_map t ~f:token in + Utils.optional_elt Html.code ?a (tokens t) + +and styled style ~emph_level = + match style with + | `Emphasis -> + let a = if emph_level mod 2 = 0 then [] else [ Html.a_class [ "odd" ] ] in + (emph_level + 1, Html.em ~a) + | `Bold -> (emph_level, Html.b ~a:[]) + | `Italic -> (emph_level, Html.i ~a:[]) + | `Superscript -> (emph_level, Html.sup ~a:[]) + | `Subscript -> (emph_level, Html.sub ~a:[]) + +let rec internallink ~emph_level ~resolve ?(a = []) (t : InternalLink.t) = + match t with + | Resolved (uri, content) -> + let href = Link.href ~resolve uri in + let a = (a :> Html_types.a_attrib Html.attrib list) in + let elt = + Html.a ~a:(Html.a_href href :: a) (inline_nolink ~emph_level content) + in + let elt = (elt :> phrasing Html.elt) in + [ elt ] + | Unresolved content -> + (* let title = + * Html.a_title (Printf.sprintf "unresolved reference to %S" + * (ref_to_string ref) + * in *) + let a = Html.a_class [ "xref-unresolved" ] :: a in + let elt = Html.span ~a (inline ~emph_level ~resolve content) in + let elt = (elt :> phrasing Html.elt) in + [ elt ] + +and internallink_nolink ~emph_level + ~(a : Html_types.span_attrib Html.attrib list) (t : InternalLink.t) = + match t with + | Resolved (_, content) | Unresolved content -> + [ Html.span ~a (inline_nolink ~emph_level content) ] + +and inline ?(emph_level = 0) ~resolve (l : Inline.t) : phrasing Html.elt list = + let one (t : Inline.one) = + let a = class_ t.attr in + match t.desc with + | Text "" -> [] + | Text s -> + if a = [] then [ Html.txt s ] else [ Html.span ~a [ Html.txt s ] ] + | Entity s -> + if a = [] then [ Html.entity s ] else [ Html.span ~a [ Html.entity s ] ] + | Linebreak -> [ Html.br ~a () ] + | Styled (style, c) -> + let emph_level, app_style = styled style ~emph_level in + [ app_style @@ inline ~emph_level ~resolve c ] + | Link (href, c) -> + let a = (a :> Html_types.a_attrib Html.attrib list) in + let content = inline_nolink ~emph_level c in + [ Html.a ~a:(Html.a_href href :: a) content ] + | InternalLink c -> internallink ~emph_level ~resolve ~a c + | Source c -> source (inline ~emph_level ~resolve) ~a c + | Raw_markup r -> raw_markup r + in + Utils.list_concat_map ~f:one l + +and inline_nolink ?(emph_level = 0) (l : Inline.t) : + non_link_phrasing Html.elt list = + let one (t : Inline.one) = + let a = class_ t.attr in + match t.desc with + | Text "" -> [] + | Text s -> + if a = [] then [ Html.txt s ] else [ Html.span ~a [ Html.txt s ] ] + | Entity s -> + if a = [] then [ Html.entity s ] else [ Html.span ~a [ Html.entity s ] ] + | Linebreak -> [ Html.br ~a () ] + | Styled (style, c) -> + let emph_level, app_style = styled style ~emph_level in + [ app_style @@ inline_nolink ~emph_level c ] + | Link (_, c) -> inline_nolink ~emph_level c + | InternalLink c -> internallink_nolink ~emph_level ~a c + | Source c -> source (inline_nolink ~emph_level) ~a c + | Raw_markup r -> raw_markup r + in + Utils.list_concat_map ~f:one l + +let heading ~resolve (h : Heading.t) = + let a, anchor = + match h.label with + | Some id -> ([ Html.a_id id ], mk_anchor_link id) + | None -> ([], []) + in + let content = inline ~resolve h.title in + let mk = + match h.level with + | 0 -> Html.h1 + | 1 -> Html.h2 + | 2 -> Html.h3 + | 3 -> Html.h4 + | 4 -> Html.h5 + | _ -> Html.h6 + in + mk ~a (anchor @ content) + +let rec block ~resolve (l : Block.t) : flow Html.elt list = + let as_flow x = (x : phrasing Html.elt list :> flow Html.elt list) in + let one (t : Block.one) = + let a = class_ t.attr in + match t.desc with + | Inline i -> + if a = [] then as_flow @@ inline ~resolve i + else [ Html.span ~a (inline ~resolve i) ] + | Paragraph i -> [ Html.p ~a (inline ~resolve i) ] + | List (typ, l) -> + let mk = match typ with Ordered -> Html.ol | Unordered -> Html.ul in + [ mk ~a (List.map (fun x -> Html.li (block ~resolve x)) l) ] + | Description l -> + [ + (let item i = + let a = class_ i.Description.attr in + let term = + (inline ~resolve i.Description.key + : phrasing Html.elt list + :> flow Html.elt list) + in + let def = block ~resolve i.Description.definition in + Html.li ~a (term @ (Html.txt " " :: def)) + in + Html.ul ~a (List.map item l)); + ] + | Raw_markup r -> raw_markup r + | Verbatim s -> [ Html.pre ~a [ Html.txt s ] ] + | Source c -> [ Html.pre ~a (source (inline ~resolve) c) ] + in + Utils.list_concat_map l ~f:one + +(* This coercion is actually sound, but is not currently accepted by Tyxml. + See https://github.com/ocsigen/tyxml/pull/265 for details + Can be replaced by a simple type coercion once this is fixed +*) +let flow_to_item : flow Html.elt list -> item Html.elt list = + fun x -> Html.totl @@ Html.toeltl x + +let div : ([< Html_types.div_attrib ], [< item ], [> Html_types.div ]) Html.star + = + Html.Unsafe.node "div" + +let spec_class = function [] -> [] | attr -> class_ ("spec" :: attr) + +let spec_doc_div ~resolve = function + | [] -> [] + | docs -> + let a = [ Html.a_class [ "spec-doc" ] ] in + [ div ~a (flow_to_item @@ block ~resolve docs) ] + +let rec documentedSrc ~resolve (t : DocumentedSrc.t) : item Html.elt list = + let open DocumentedSrc in + let take_code l = + Doctree.Take.until l ~classify:(function + | Code code -> Accum code + | Alternative (Expansion { summary; _ }) -> Accum summary + | _ -> Stop_and_keep) + in + let take_descr l = + Doctree.Take.until l ~classify:(function + | Documented { attrs; anchor; code; doc; markers } -> + Accum + [ { DocumentedSrc.attrs; anchor; code = `D code; doc; markers } ] + | Nested { attrs; anchor; code; doc; markers } -> + Accum + [ { DocumentedSrc.attrs; anchor; code = `N code; doc; markers } ] + | _ -> Stop_and_keep) + in + let rec to_html t : item Html.elt list = + match t with + | [] -> [] + | (Code _ | Alternative _) :: _ -> + let code, _, rest = take_code t in + source (inline ~resolve) code @ to_html rest + | Subpage subp :: _ -> subpage ~resolve subp + | (Documented _ | Nested _) :: _ -> + let l, _, rest = take_descr t in + let one { DocumentedSrc.attrs; anchor; code; doc; markers } = + let content = + match code with + | `D code -> (inline ~resolve code :> item Html.elt list) + | `N n -> to_html n + in + let doc = + match doc with + | [] -> [] + | doc -> + let opening, closing = markers in + [ + Html.td + ~a:(class_ [ "def-doc" ]) + (Html.span + ~a:(class_ [ "comment-delim" ]) + [ Html.txt opening ] + :: block ~resolve doc + @ [ + Html.span + ~a:(class_ [ "comment-delim" ]) + [ Html.txt closing ]; + ]); + ] + in + let a, link = mk_anchor anchor in + let content = + let c = link @ content in + Html.td ~a:(class_ attrs) (c :> any Html.elt list) + in + Html.tr ~a (content :: doc) + in + Html.table (List.map one l) :: to_html rest + in + to_html t + +and subpage ~resolve (subp : Subpage.t) : item Html.elt list = + items ~resolve subp.content.items + +and items ~resolve l : item Html.elt list = + let rec walk_items acc (t : Item.t list) : item Html.elt list = + let continue_with rest elts = + (walk_items [@tailcall]) (List.rev_append elts acc) rest + in + match t with + | [] -> List.rev acc + | Text _ :: _ as t -> + let text, _, rest = + Doctree.Take.until t ~classify:(function + | Item.Text text -> Accum text + | _ -> Stop_and_keep) + in + let content = flow_to_item @@ block ~resolve text in + (continue_with [@tailcall]) rest content + | Heading h :: rest -> + (continue_with [@tailcall]) rest [ heading ~resolve h ] + | Include { attr; anchor; doc; content = { summary; status; content } } + :: rest -> + let doc = spec_doc_div ~resolve doc in + let included_html = (items content :> any Html.elt list) in + let content = + let details ~open' = + let open' = if open' then [ Html.a_open () ] else [] in + let summary = + let anchor_attrib, anchor_link = mk_anchor anchor in + let a = spec_class attr @ anchor_attrib in + Html.summary ~a @@ anchor_link @ source (inline ~resolve) summary + in + [ Html.details ~a:open' summary included_html ] + in + match status with + | `Inline -> included_html + | `Closed -> details ~open':false + | `Open -> details ~open':true + | `Default -> details ~open':true + in + let inc = + [ Html.div ~a:[ Html.a_class [ "odoc-include" ] ] (doc @ content) ] + in + (continue_with [@tailcall]) rest inc + | Declaration { Item.attr; anchor; content; doc } :: rest -> + let anchor_attrib, anchor_link = mk_anchor anchor in + let a = spec_class attr @ anchor_attrib in + let content = anchor_link @ documentedSrc ~resolve content in + let spec = + let doc = spec_doc_div ~resolve doc in + [ div ~a:[ Html.a_class [ "odoc-spec" ] ] (div ~a content :: doc) ] + in + (continue_with [@tailcall]) rest spec + and items l = walk_items [] l in + items l + +module Toc = struct + open Odoc_document.Doctree + + let render_toc ~resolve (toc : Toc.t) : string = + let rec section { Toc.url; text; children } = + let text = inline_nolink text in + let text = + (text + : non_link_phrasing Html.elt list + :> Html_types.flow5_without_interactive Html.elt list) + in + let text = + List.map (Format.asprintf "%a" (Tyxml.Html.pp_elt ())) text + |> String.concat " " + in + let href = Link.href ~resolve url in + match children with + | [] -> + `Assoc + [ + ("title", `String text); + ("href", `String href); + ("children", `List []); + ] + | _ -> + `Assoc + [ + ("title", `String text); + ("href", `String href); + ("children", sections children); + ] + and sections the_sections = + `List (List.map (fun the_section -> section the_section) the_sections) + in + match toc with + | [] -> `List [] |> Yojson.Safe.to_string + | _ -> sections toc |> Yojson.Safe.to_string + + let on_sub : Subpage.status -> bool = function + | `Closed | `Open | `Default -> false + | `Inline -> true + + let from_items ~resolve ~path i = + render_toc ~resolve @@ Toc.compute path ~on_sub i +end + +module Page = struct + let on_sub = function + | `Page _ -> None + | `Include x -> ( + match x.Include.status with + | `Closed | `Open | `Default -> None + | `Inline -> Some 0) + + let rec include_ indent { Subpage.content; _ } = [ page indent content ] + + and subpages indent i = + Utils.list_concat_map ~f:(include_ indent) @@ Doctree.Subpages.compute i + + and page indent ({ items = i; url; _ } as p) = + let resolve = Link.Current url in + let i = Doctree.Shift.compute ~on_sub i in + let subpages = subpages indent p in + let content = (items ~resolve i :> any Html.elt list) in + let page = + let filename = Link.Path.as_filename url in + let content ppf = (Html.pp_elt ~indent ()) ppf (Html.div content) in + { Odoc_document.Renderer.filename; content; children = subpages } + in + page +end + +let render_content ~indent page = Page.page indent page + +let render_toc ~indent:_ page = + let { Odoc_document.Types.Page.items = i; url; _ } = page in + let resolve = Link.Current url in + let i = Doctree.Shift.compute ~on_sub:Page.on_sub i in + let toc = Toc.from_items ~resolve ~path:url i in + let page = + let filename = + Link.Path.as_filename url |> Fpath.rem_ext |> Fpath.add_ext ".toc.json" + in + let content ppf = Format.pp_print_string ppf toc in + { Odoc_document.Renderer.filename; content; children = [] } + in + page + +let doc ~xref_base_uri b = + let resolve = Link.Base xref_base_uri in + block ~resolve b diff --git a/src/voodoo-gen/generator.mli b/src/voodoo-gen/generator.mli new file mode 100644 index 00000000..dc8f7c29 --- /dev/null +++ b/src/voodoo-gen/generator.mli @@ -0,0 +1,9 @@ +open Odoc_document + +val render_content : indent:bool -> Types.Page.t -> Renderer.page +val render_toc : indent:bool -> Types.Page.t -> Renderer.page + +val doc : + xref_base_uri:string -> + Types.Block.t -> + Html_types.flow5_without_sectioning_heading_header_footer Tyxml.Html.elt list diff --git a/src/voodoo-gen/link.ml b/src/voodoo-gen/link.ml new file mode 100644 index 00000000..e7b85152 --- /dev/null +++ b/src/voodoo-gen/link.ml @@ -0,0 +1,96 @@ +module Url = Odoc_document.Url + +let flat = ref false + +(* Translation from Url.Path *) +module Path = struct + let for_printing url = List.map snd @@ Url.Path.to_list url + + let segment_to_string (kind, name) = + match kind with + | `Module | `Page -> name + | _ -> Format.asprintf "%a-%s" Url.Path.pp_kind kind name + + let is_leaf_page url = url.Url.Path.kind = `LeafPage + + let get_dir_and_file url = + let l = Url.Path.to_list url in + let is_dir = + if !flat then function `Page -> `Always | _ -> `Never + else function `LeafPage -> `Never | `File -> `Never | _ -> `Always + in + let dir, file = Url.Path.split ~is_dir l in + let dir = List.map segment_to_string dir in + let file = + match file with + | [] -> "index.html" + | [ (`LeafPage, name) ] -> name ^ ".html" + | [ (`File, name) ] -> name + | xs -> + assert !flat; + String.concat "-" (List.map segment_to_string xs) ^ ".html" + in + (dir, file) + + let for_linking url = + let dir, file = get_dir_and_file url in + dir @ [ file ] + + let as_filename (url : Url.Path.t) = + Fpath.(v @@ String.concat Fpath.dir_sep @@ for_linking url) +end + +let semantic_uris = ref false + +type resolve = Current of Url.Path.t | Base of string + +let rec drop_shared_prefix l1 l2 = + match (l1, l2) with + | l1 :: l1s, l2 :: l2s when l1 = l2 -> drop_shared_prefix l1s l2s + | _, _ -> (l1, l2) + +let href ~resolve t = + let { Url.Anchor.page; anchor; _ } = t in + + let target_loc = Path.for_linking page in + + (* If xref_base_uri is defined, do not perform relative URI resolution. *) + match resolve with + | Base xref_base_uri -> ( + let page = xref_base_uri ^ String.concat "/" target_loc in + match anchor with "" -> page | anchor -> page ^ "#" ^ anchor) + | Current path -> ( + let current_loc = Path.for_linking path in + + let current_from_common_ancestor, target_from_common_ancestor = + drop_shared_prefix current_loc target_loc + in + + let relative_target = + match current_from_common_ancestor with + | [] -> + (* We're already on the right page *) + (* If we're already on the right page, the target from our common + ancestor can't be anything other than the empty list *) + assert (target_from_common_ancestor = []); + [] + | [ _ ] -> + (* We're already in the right dir *) + target_from_common_ancestor + | l -> + (* We need to go up some dirs *) + List.map (fun _ -> "..") (List.tl l) @ target_from_common_ancestor + in + let remove_index_html l = + match List.rev l with + | "index.html" :: rest -> List.rev ("" :: rest) + | _ -> l + in + let relative_target = + if !semantic_uris then remove_index_html relative_target + else relative_target + in + match (relative_target, anchor) with + | [], "" -> "#" + | page, "" -> String.concat "/" page + | page, anchor -> String.concat "/" page ^ "#" ^ anchor) diff --git a/src/voodoo-gen/link.mli b/src/voodoo-gen/link.mli new file mode 100644 index 00000000..6a0c98b8 --- /dev/null +++ b/src/voodoo-gen/link.mli @@ -0,0 +1,17 @@ +(** HTML-specific interpretation of {!Odoc_document.Url} *) + +module Url = Odoc_document.Url + +val semantic_uris : bool ref +(** Whether to generate pretty/semantics links or not. *) + +type resolve = Current of Url.Path.t | Base of string + +val href : resolve:resolve -> Url.t -> string + +module Path : sig + val is_leaf_page : Url.Path.t -> bool + val for_printing : Url.Path.t -> string list + val for_linking : Url.Path.t -> string list + val as_filename : Url.Path.t -> Fpath.t +end diff --git a/src/voodoo-gen/markdown.ml b/src/voodoo-gen/markdown.ml index 1a2a58e3..96dc72a1 100644 --- a/src/voodoo-gen/markdown.ml +++ b/src/voodoo-gen/markdown.ml @@ -47,10 +47,7 @@ let rec block : 'attr block -> intermediate = function | Code_block (_, _a, b) -> Bl [ - { - desc = Source ("markdown", [ Elt [ { desc = Text b; attr = [] } ] ]); - attr = []; - }; + { desc = Source [ Elt [ { desc = Text b; attr = [] } ] ]; attr = [] }; ] | Html_block _ -> Bl [] | Definition_list _ -> Bl [] @@ -71,13 +68,16 @@ let of_content content ~name ~url = let items = List.map (function It x -> x | Bl x -> Text x) intermediate in Ok (match items with - | [] -> Odoc_document.Types.Page.{ preamble = []; items = []; url } + | [] -> + Odoc_document.Types.Page.{ title = name; header = []; items = []; url } | (Heading _ as x) :: rest -> - Odoc_document.Types.Page.{ preamble = [ x ]; items = rest; url } + Odoc_document.Types.Page. + { title = name; header = [ x ]; items = rest; url } | _ -> Odoc_document.Types.Page. { - preamble = + title = name; + header = [ Heading { @@ -111,9 +111,10 @@ let read_plain f url = Ok Odoc_document.Types.Page. { + title = name; url; items = [ Text [ { desc = Verbatim content; attr = [] } ] ]; - preamble = + header = [ Heading { diff --git a/src/voodoo-gen/rendering.ml b/src/voodoo-gen/rendering.ml index b0930569..a9961e7a 100644 --- a/src/voodoo-gen/rendering.ml +++ b/src/voodoo-gen/rendering.ml @@ -23,12 +23,8 @@ let render_document ~output odoctree = Format.fprintf fmt "%t@?" content; close_out oc) in - aux - @@ Odoc_html.Generator.render - ~config: - (Odoc_html.Config.v ~semantic_uris:true ~indent:false ~flat:false - ~open_details:true ~as_json:true ()) - odoctree; + aux @@ Generator.render_content ~indent:false odoctree; + aux @@ Generator.render_toc ~indent:false odoctree; Ok () let docs_ids parent docs = @@ -36,20 +32,15 @@ let docs_ids parent docs = match root.content with | Page_content odoctree -> ( match odoctree.Odoc_model.Lang.Page.name with - | { iv = `LeafPage _; _ } -> Error (`Msg "Parent is a leaf!") - | { iv = `Page (maybe_container_page, _); _ } as parent_id -> + | `LeafPage _ -> Error (`Msg "Parent is a leaf!") + | `Page _ as parent_id -> let result = List.map (fun doc -> let id = let basename = Fpath.basename doc in - { - parent_id with - iv = - `LeafPage - ( maybe_container_page, - Odoc_model.Names.PageName.make_std basename ); - } + `LeafPage + (Some parent_id, Odoc_model.Names.PageName.make_std basename) in (id, doc)) docs @@ -62,8 +53,8 @@ let otherversions parent vs = match root.content with | Page_content odoctree -> ( match odoctree.Odoc_model.Lang.Page.name with - | { iv = `LeafPage _; _ } -> Error (`Msg "Parent is a leaf!") - | { iv = `Page (parent_id, _); _ } -> + | `LeafPage _ -> Error (`Msg "Parent is a leaf!") + | `Page (parent_id, _) -> let result = List.map (fun v -> `Page (parent_id, Odoc_model.Names.PageName.make_std v)) diff --git a/voodoo-gen.opam b/voodoo-gen.opam index 5660ca1b..ae5ce8a3 100644 --- a/voodoo-gen.opam +++ b/voodoo-gen.opam @@ -39,5 +39,4 @@ build: [ dev-repo: "git+https://github.com/jonludlam/voodoo.git" pin-depends: [ ["pandoc.dev" "git+https://github.com/tatchi/opam-pandoc-bin#5eeb415c7023323045371ad803f88365c7003b38"] - ["odoc.dev" "git+https://github.com/ocaml/odoc#503a2e895c211f555ff1a5e0ae11f2ab1697db73"] ] diff --git a/voodoo-gen.opam.template b/voodoo-gen.opam.template index a0a91d2e..21efcaea 100644 --- a/voodoo-gen.opam.template +++ b/voodoo-gen.opam.template @@ -1,4 +1,3 @@ pin-depends: [ ["pandoc.dev" "git+https://github.com/tatchi/opam-pandoc-bin#5eeb415c7023323045371ad803f88365c7003b38"] - ["odoc.dev" "git+https://github.com/ocaml/odoc#503a2e895c211f555ff1a5e0ae11f2ab1697db73"] ]