Skip to content

Commit 799003a

Browse files
sabinedbuenzli
authored andcommitted
experimental --as-json option for the HTML renderer
This adds an experimental flag `--as-json` to `odoc html` to emit HTML fragments (preamble, content) together with JSON metadata (whether the page uses katex, breadcrumbs, table of contents). `--as-json` replaces the `--omit-toc`, `--omit-breadcrumbs`, and `--content-only` options of `odoc html`. Purpose: Make it simpler and more robust to embed the HTML output of odoc in websites, such as ocaml.org. Notes: - In order to abstract breadcrumb rendering, we treat breadcrumbs the same as the table of contents: there is now an intermediate data type `breadcrumb` used by the HTML renderer, from which either HTML or a JSON representation is rendererd. - In order to avoid introducing a JSON encoder dependency to odoc, @dbuenzli contributed the JSON encoder contained in this PR. All remaining additions and changes are to be blamed on @sabine. Co-authored-by: Daniel Bünzli <[email protected]>
1 parent 8f83c49 commit 799003a

File tree

16 files changed

+281
-152
lines changed

16 files changed

+281
-152
lines changed

CHANGES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
Additions
2+
- New (experimental!) option `--as-json` for the HTML renderer that emits HTML
3+
fragments (preamble, content) together with metadata (table of contents,
4+
breadcrumbs, whether katex is used) in JSON format.
5+
16
2.1.0
27
-----
38

src/html/config.ml

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,12 @@ type t = {
77
indent : bool;
88
flat : bool;
99
open_details : bool;
10-
omit_breadcrumbs : bool;
11-
omit_toc : bool;
12-
content_only : bool;
10+
as_json : bool;
1311
}
1412

1513
let v ?theme_uri ?support_uri ~semantic_uris ~indent ~flat ~open_details
16-
~omit_breadcrumbs ~omit_toc ~content_only () =
17-
{
18-
theme_uri;
19-
support_uri;
20-
semantic_uris;
21-
indent;
22-
flat;
23-
open_details;
24-
omit_breadcrumbs;
25-
omit_toc;
26-
content_only;
27-
}
14+
~as_json () =
15+
{ semantic_uris; indent; flat; open_details; theme_uri; support_uri; as_json }
2816

2917
let theme_uri config =
3018
match config.theme_uri with None -> Types.Relative None | Some uri -> uri
@@ -40,8 +28,4 @@ let flat config = config.flat
4028

4129
let open_details config = config.open_details
4230

43-
let omit_breadcrumbs config = config.omit_breadcrumbs
44-
45-
let omit_toc config = config.omit_toc
46-
47-
let content_only config = config.content_only
31+
let as_json config = config.as_json

src/html/config.mli

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@ val v :
99
indent:bool ->
1010
flat:bool ->
1111
open_details:bool ->
12-
omit_breadcrumbs:bool ->
13-
omit_toc:bool ->
14-
content_only:bool ->
12+
as_json:bool ->
1513
unit ->
1614
t
1715

@@ -27,8 +25,4 @@ val flat : t -> bool
2725

2826
val open_details : t -> bool
2927

30-
val omit_breadcrumbs : t -> bool
31-
32-
val omit_toc : t -> bool
33-
34-
val content_only : t -> bool
28+
val as_json : t -> bool

src/html/generator.ml

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,29 @@ module Toc = struct
369369
List.map section toc
370370
end
371371

372+
module Breadcrumbs = struct
373+
open Types
374+
375+
let gen_breadcrumbs ~config ~url =
376+
let rec get_parent_paths x =
377+
match x with
378+
| [] -> []
379+
| x :: xs -> (
380+
match Odoc_document.Url.Path.of_list (List.rev (x :: xs)) with
381+
| Some x -> x :: get_parent_paths xs
382+
| None -> get_parent_paths xs)
383+
in
384+
let to_breadcrumb path =
385+
let href =
386+
Link.href ~config ~resolve:(Current url)
387+
(Odoc_document.Url.from_path path)
388+
in
389+
{ href; name = path.name; kind = path.kind }
390+
in
391+
get_parent_paths (List.rev (Odoc_document.Url.Path.to_list url))
392+
|> List.rev |> List.map to_breadcrumb
393+
end
394+
372395
module Page = struct
373396
let on_sub = function
374397
| `Page _ -> None
@@ -393,11 +416,18 @@ module Page = struct
393416
let i = Doctree.Shift.compute ~on_sub i in
394417
let uses_katex = Doctree.Math.has_math_elements p in
395418
let toc = Toc.gen_toc ~config ~resolve ~path:url i in
419+
let breadcrumbs = Breadcrumbs.gen_breadcrumbs ~config ~url in
396420
let header =
397421
items ~config ~resolve (Doctree.PageTitle.render_title p @ preamble)
398422
in
399423
let content = (items ~config ~resolve i :> any Html.elt list) in
400-
Tree.make ~config ~header ~toc ~url ~uses_katex url.name content subpages
424+
if Config.as_json config then
425+
Html_fragment_json.make ~config
426+
~preamble:(items ~config ~resolve preamble :> any Html.elt list)
427+
~breadcrumbs ~toc ~url ~uses_katex content subpages
428+
else
429+
Html_page.make ~config ~header ~toc ~breadcrumbs ~url ~uses_katex content
430+
subpages
401431
end
402432

403433
let render ~config page = Page.page ~config page

src/html/html_fragment_json.ml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
(* Rendering of HTML fragments together with metadata. For embedding the
2+
generated documentation in existing websites.
3+
*)
4+
5+
module Html = Tyxml.Html
6+
7+
let json_of_breadcrumbs (breadcrumbs : Types.breadcrumb list) : Utils.Json.json
8+
=
9+
let breadcrumb (b : Types.breadcrumb) =
10+
`Object
11+
[
12+
("name", `String b.name);
13+
("href", `String b.href);
14+
("kind", `String (Odoc_document.Url.Path.string_of_kind b.kind));
15+
]
16+
in
17+
let json_breadcrumbs = breadcrumbs |> List.map breadcrumb in
18+
`Array json_breadcrumbs
19+
20+
let json_of_toc (toc : Types.toc list) : Utils.Json.json =
21+
let rec section (s : Types.toc) =
22+
`Object
23+
[
24+
("title", `String s.title_str);
25+
("href", `String s.href);
26+
("children", `Array (List.map section s.children));
27+
]
28+
in
29+
let toc_json_list = toc |> List.map section in
30+
`Array toc_json_list
31+
32+
let make ~config ~preamble ~url ~breadcrumbs ~toc ~uses_katex content children =
33+
let filename = Link.Path.as_filename ~is_flat:(Config.flat config) url in
34+
let filename = Fpath.add_ext ".json" filename in
35+
let htmlpp = Html.pp_elt ~indent:(Config.indent config) () in
36+
let json_to_string json = Utils.Json.to_string json in
37+
let content ppf =
38+
Format.pp_print_string ppf
39+
(json_to_string
40+
(`Object
41+
[
42+
("uses_katex", `Bool uses_katex);
43+
("breadcrumbs", json_of_breadcrumbs breadcrumbs);
44+
("toc", json_of_toc toc);
45+
( "preamble",
46+
`String
47+
(String.concat ""
48+
(List.map (Format.asprintf "%a" htmlpp) preamble)) );
49+
( "content",
50+
`String
51+
(String.concat ""
52+
(List.map (Format.asprintf "%a" htmlpp) content)) );
53+
]))
54+
in
55+
[ { Odoc_document.Renderer.filename; content; children } ]

src/html/html_fragment_json.mli

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
module Html = Tyxml.Html
2+
3+
val make :
4+
config:Config.t ->
5+
preamble:Html_types.div_content Html.elt list ->
6+
url:Odoc_document.Url.Path.t ->
7+
breadcrumbs:Types.breadcrumb list ->
8+
toc:Types.toc list ->
9+
uses_katex:bool ->
10+
Html_types.div_content Html.elt list ->
11+
Odoc_document.Renderer.page list ->
12+
Odoc_document.Renderer.page list

src/html/tree.ml renamed to src/html/html_page.ml

Lines changed: 51 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ module Html = Tyxml.Html
1818

1919
let html_of_toc toc =
2020
let open Types in
21-
let rec section section =
21+
let rec section (section : toc) =
2222
let link = Html.a ~a:[ Html.a_href section.href ] section.title in
2323
match section.children with [] -> [ link ] | cs -> [ link; sections cs ]
2424
and sections the_sections =
@@ -30,13 +30,54 @@ let html_of_toc toc =
3030
| [] -> []
3131
| _ -> [ Html.nav ~a:[ Html.a_class [ "odoc-toc" ] ] [ sections toc ] ]
3232

33-
let page_creator ~config ~url ~uses_katex name header toc content =
33+
let html_of_breadcrumbs (breadcrumbs : Types.breadcrumb list) =
34+
let make_navigation ~up_url rest =
35+
[
36+
Html.nav
37+
~a:[ Html.a_class [ "odoc-nav" ] ]
38+
([ Html.a ~a:[ Html.a_href up_url ] [ Html.txt "Up" ]; Html.txt "" ]
39+
@ rest);
40+
]
41+
in
42+
match List.rev breadcrumbs with
43+
| [] -> [] (* Can't happen - there's always the current page's breadcrumb. *)
44+
| [ _ ] -> [] (* No parents *)
45+
| [ { name = "index"; _ }; x ] ->
46+
(* Special case leaf pages called 'index' with one parent. This is for files called
47+
index.mld that would otherwise clash with their parent. In particular,
48+
dune and odig both cause this situation right now. *)
49+
let up_url = "../index.html" in
50+
let parent_name = x.name in
51+
make_navigation ~up_url [ Html.txt parent_name ]
52+
| current :: up :: bs ->
53+
let space = Html.txt " " in
54+
let sep = [ space; Html.entity "#x00BB"; space ] in
55+
let html =
56+
(* Create breadcrumbs *)
57+
Utils.list_concat_map ?sep:(Some sep)
58+
~f:(fun (breadcrumb : Types.breadcrumb) ->
59+
[
60+
[
61+
Html.a
62+
~a:[ Html.a_href breadcrumb.href ]
63+
[ Html.txt breadcrumb.name ];
64+
];
65+
])
66+
(up :: bs)
67+
|> List.flatten
68+
in
69+
make_navigation ~up_url:up.href
70+
(List.rev html @ sep @ [ Html.txt current.name ])
71+
72+
let page_creator ~config ~url ~uses_katex header breadcrumbs toc content =
3473
let theme_uri = Config.theme_uri config in
3574
let support_uri = Config.support_uri config in
3675
let path = Link.Path.for_printing url in
3776

3877
let head : Html_types.head Html.elt =
39-
let title_string = Printf.sprintf "%s (%s)" name (String.concat "." path) in
78+
let title_string =
79+
Printf.sprintf "%s (%s)" url.name (String.concat "." path)
80+
in
4081

4182
let file_uri base file =
4283
match base with
@@ -99,89 +140,20 @@ let page_creator ~config ~url ~uses_katex name header toc content =
99140
Html.head (Html.title (Html.txt title_string)) meta_elements
100141
in
101142

102-
let gen_breadcrumbs () =
103-
let rec get_parents x =
104-
match x with
105-
| [] -> []
106-
| x :: xs -> (
107-
match Odoc_document.Url.Path.of_list (List.rev (x :: xs)) with
108-
| Some x -> x :: get_parents xs
109-
| None -> get_parents xs)
110-
in
111-
let parents =
112-
get_parents (List.rev (Odoc_document.Url.Path.to_list url)) |> List.rev
113-
in
114-
let href page =
115-
Link.href ~resolve:(Current url) (Odoc_document.Url.from_path page)
116-
in
117-
let make_navigation ~up_url breadcrumbs =
118-
[
119-
Html.nav
120-
~a:[ Html.a_class [ "odoc-nav" ] ]
121-
([
122-
Html.a ~a:[ Html.a_href up_url ] [ Html.txt "Up" ]; Html.txt "";
123-
]
124-
@ breadcrumbs);
125-
]
126-
in
127-
match parents with
128-
| [] -> [] (* Can't happen - Url.Path.to_list returns a non-empty list *)
129-
| [ _ ] -> [] (* No parents *)
130-
| [ x; { name = "index"; _ } ] ->
131-
(* Special case leaf pages called 'index' with one parent. This is for files called
132-
index.mld that would otherwise clash with their parent. In particular,
133-
dune and odig both cause this situation right now. *)
134-
let up_url = "../index.html" in
135-
let parent_name = x.name in
136-
make_navigation ~up_url [ Html.txt parent_name ]
137-
| _ ->
138-
let up_url = href ~config (List.hd (List.tl (List.rev parents))) in
139-
let l =
140-
(* Create breadcrumbs *)
141-
let space = Html.txt " " in
142-
parents
143-
|> Utils.list_concat_map
144-
?sep:(Some [ space; Html.entity "#x00BB"; space ])
145-
~f:(fun url' ->
146-
[
147-
[
148-
(if url = url' then Html.txt url.name
149-
else
150-
Html.a
151-
~a:[ Html.a_href (href ~config url') ]
152-
[ Html.txt url'.name ]);
153-
];
154-
])
155-
|> List.flatten
156-
in
157-
make_navigation ~up_url l
158-
in
159-
160-
let breadcrumbs =
161-
if Config.omit_breadcrumbs config then [] else gen_breadcrumbs ()
162-
in
163-
let toc = if Config.omit_toc config then [] else html_of_toc toc in
164143
let body =
165-
breadcrumbs
144+
html_of_breadcrumbs breadcrumbs
166145
@ [ Html.header ~a:[ Html.a_class [ "odoc-preamble" ] ] header ]
167-
@ toc
146+
@ html_of_toc toc
168147
@ [ Html.div ~a:[ Html.a_class [ "odoc-content" ] ] content ]
169148
in
170-
let htmlpp_elt = Html.pp_elt ~indent:(Config.indent config) () in
171149
let htmlpp = Html.pp ~indent:(Config.indent config) () in
172-
if Config.content_only config then
173-
let content ppf =
174-
htmlpp_elt ppf (Html.div ~a:[ Html.a_class [ "odoc" ] ] body)
175-
in
176-
content
177-
else
178-
let html = Html.html head (Html.body ~a:[ Html.a_class [ "odoc" ] ] body) in
179-
let content ppf = htmlpp ppf html in
180-
content
150+
let html = Html.html head (Html.body ~a:[ Html.a_class [ "odoc" ] ] body) in
151+
let content ppf = htmlpp ppf html in
152+
content
181153

182-
let make ~config ~url ~header ~toc ~uses_katex title content children =
154+
let make ~config ~url ~header ~breadcrumbs ~toc ~uses_katex content children =
183155
let filename = Link.Path.as_filename ~is_flat:(Config.flat config) url in
184156
let content =
185-
page_creator ~config ~url ~uses_katex title header toc content
157+
page_creator ~config ~url ~uses_katex header breadcrumbs toc content
186158
in
187159
[ { Odoc_document.Renderer.filename; content; children } ]

src/html/tree.mli renamed to src/html/html_page.mli

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ val make :
2424
config:Config.t ->
2525
url:Odoc_document.Url.Path.t ->
2626
header:Html_types.flow5_without_header_footer Html.elt list ->
27+
breadcrumbs:Types.breadcrumb list ->
2728
toc:Types.toc list ->
2829
uses_katex:bool ->
29-
string ->
3030
Html_types.div_content Html.elt list ->
3131
Odoc_document.Renderer.page list ->
3232
Odoc_document.Renderer.page list

src/html/link.ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ let rec drop_shared_prefix l1 l2 =
4545
| l1 :: l1s, l2 :: l2s when l1 = l2 -> drop_shared_prefix l1s l2s
4646
| _, _ -> (l1, l2)
4747

48-
let href ~(config : Config.t) ~resolve t =
48+
let href ~config ~resolve t =
4949
let { Url.Anchor.page; anchor; _ } = t in
5050

5151
let target_loc = Path.for_linking ~is_flat:(Config.flat config) page in

src/html/odoc_html.ml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
module Types = Types
22
module Config = Config
33

4-
module Tree = Tree
5-
(** @canonical Odoc_html.Tree *)
4+
module Html_fragment_json = Html_fragment_json
5+
(** @canonical Odoc_html.Html_fragment_json *)
6+
7+
module Html_page = Html_page
8+
(** @canonical Odoc_html.Html_page *)
69

710
module Generator = Generator
811
module Link = Link

0 commit comments

Comments
 (0)