1
+
Added:
(* -*- mode: tuareg; -*- *)
2
+
Added:
3
+
Added:
(** Reusable HTML building blocks shared across views.
4
+
Added:
5
+
Added:
This module holds presentation fragments that appear on more than one page
6
+
Added:
so their markup — and therefore their CSS contract — stays identical
7
+
Added:
everywhere. Nothing here performs I/O or touches repository state. *)
8
+
Added:
9
+
Added:
open Dream_html
10
+
Added:
11
+
Added:
(** {1 Page identity} *)
12
+
Added:
13
+
Added:
type page = Summary | Commits | Files | Branches | Tags | Readme
14
+
Added:
type site = { user_name : string; root_title : string; nav_logo : string }
15
+
Added:
16
+
Added:
let site ~user_name ~root_title ~nav_logo = { user_name; root_title; nav_logo }
17
+
Added:
18
+
Added:
let page_to_nav_item repo = function
19
+
Added:
| Summary -> (Routes.Repo repo, "Summary", Summary)
20
+
Added:
| Commits -> (Routes.Commits repo, "Commits", Commits)
21
+
Added:
| Files -> (Routes.Files repo, "Files", Files)
22
+
Added:
| Branches -> (Routes.Branches repo, "Branches", Branches)
23
+
Added:
| Tags -> (Routes.Tags repo, "Tags", Tags)
24
+
Added:
| Readme -> (Routes.Readme repo, "README", Readme)
25
+
Added:
26
+
Added:
(** {1 Disclosure widgets} *)
27
+
Added:
28
+
Added:
(** Decorative disclosure indicator. Rotated by CSS when the enclosing [details]
29
+
Added:
is open, so it carries no textual meaning and is hidden from assistive
30
+
Added:
technology. *)
31
+
Added:
let chevron () =
32
+
Added:
HTML.(span [ class_ "tree-chevron"; Aria.hidden true ] [ txt "\xe2\x80\xba" ])
33
+
Added:
34
+
Added:
(** A collapsible directory row.
35
+
Added:
36
+
Added:
Renders [li.tree-dir > details > summary.tree-toggle] where the summary
37
+
Added:
holds the chevron plus an anchor to [route]. Clicking the summary padding or
38
+
Added:
chevron toggles the nested list; clicking the anchor navigates. Both the
39
+
Added:
repository file tree and the project directory listing use this so their
40
+
Added:
interaction model is identical.
41
+
Added:
42
+
Added:
@param extra_class appended to the [li] class list (e.g. [" tree-hidden"]).
43
+
Added:
@param expanded renders the [details] initially open.
44
+
Added:
@param label
45
+
Added:
anchor text, conventionally the directory name with a trailing slash.
46
+
Added:
@param children [li] nodes for the nested list. *)
47
+
Added:
let tree_dir ?(extra_class = "") ?(expanded = false) ~route ~label children =
48
+
Added:
let details_attrs = if expanded then HTML.[ open_ ] else [] in
49
+
Added:
(* Bound outside the [HTML] scope below, where [label] would otherwise
50
+
Added:
resolve to [HTML.label]. *)
51
+
Added:
let label_text = label in
52
+
Added:
HTML.(
53
+
Added:
li
54
+
Added:
[ class_ "tree-dir%s" extra_class ]
55
+
Added:
[
56
+
Added:
details details_attrs
57
+
Added:
[
58
+
Added:
summary
59
+
Added:
[ class_ "tree-toggle" ]
60
+
Added:
[
61
+
Added:
chevron ();
62
+
Added:
Routes.link_to route
63
+
Added:
~other_attrs:[ class_ "tree-link" ]
64
+
Added:
(txt "%s" label_text);
65
+
Added:
];
66
+
Added:
ul [ class_ "tree-nested" ] children;
67
+
Added:
];
68
+
Added:
])
69
+
Added:
70
+
Added:
(** A collapsible page section headed by [h1], used for the repository-list
71
+
Added:
groupings on the root page. *)
72
+
Added:
let section_disclosure ?(expanded = false) ~title:section_title children =
73
+
Added:
let details_attrs = if expanded then HTML.[ open_ ] else [] in
74
+
Added:
HTML.(
75
+
Added:
details details_attrs
76
+
Added:
(summary
77
+
Added:
[ class_ "section-toggle" ]
78
+
Added:
[ chevron (); h1 [] [ txt "%s" section_title ] ]
79
+
Added:
:: children))
80
+
Added:
81
+
Added:
(** {1 Navigation} *)
82
+
Added:
83
+
Added:
(** Static assets may be configured as bare paths; make them root-relative
84
+
Added:
unless they are already absolute or a data URI. *)
85
+
Added:
let normalize_asset_url source =
86
+
Added:
if
87
+
Added:
String.starts_with ~prefix:"/" source
88
+
Added:
|| String.starts_with ~prefix:"http://" source
89
+
Added:
|| String.starts_with ~prefix:"https://" source
90
+
Added:
|| String.starts_with ~prefix:"data:" source
91
+
Added:
then source
92
+
Added:
else "/" ^ source
93
+
Added:
94
+
Added:
let nav_logo ~href:logo_href ~alt:alt_text logo =
95
+
Added:
HTML.(
96
+
Added:
a
97
+
Added:
[ id "nav-logo"; href "%s" logo_href ]
98
+
Added:
[
99
+
Added:
img
100
+
Added:
[
101
+
Added:
src "%s" (normalize_asset_url logo);
102
+
Added:
alt "%s" alt_text;
103
+
Added:
class_ "site-logo";
104
+
Added:
];
105
+
Added:
])
106
+
Added:
107
+
Added:
(** Breadcrumb trail for a nested repository path. Every segment but the last
108
+
Added:
links to its project directory; the last links to the repository summary. *)
109
+
Added:
let repo_breadcrumb repo =
110
+
Added:
let segments =
111
+
Added:
String.split_on_char '/' repo |> List.filter (fun segment -> segment <> "")
112
+
Added:
in
113
+
Added:
let last_index = List.length segments - 1 in
114
+
Added:
let nodes =
115
+
Added:
List.mapi
116
+
Added:
(fun index segment ->
117
+
Added:
let path = String.concat "/" (List_ext.take (index + 1) segments) in
118
+
Added:
let repo_link =
119
+
Added:
if index = last_index then
120
+
Added:
Routes.link_to (Repo repo) (txt "%s" segment)
121
+
Added:
else Routes.link_to (Project_dir path) (txt "%s" segment)
122
+
Added:
in
123
+
Added:
if index = 0 then repo_link
124
+
Added:
else
125
+
Added:
HTML.(
126
+
Added:
null
127
+
Added:
[
128
+
Added:
span [ class_ "nav-home-sep"; Aria.hidden true ] [ txt "/" ];
129
+
Added:
repo_link;
130
+
Added:
]))
131
+
Added:
segments
132
+
Added:
in
133
+
Added:
HTML.(span [ id "nav-home"; class_ "repo-hierarchy" ] nodes)
134
+
Added:
135
+
Added:
(** Top navigation for pages that are not scoped to a repository. *)
136
+
Added:
let rootnav ~title:nav_title ~nav_logo:logo ?home_href () =
137
+
Added:
let logo_href, logo_alt =
138
+
Added:
match home_href with
139
+
Added:
| None -> ("https://git-scm.com", "Git website")
140
+
Added:
| Some _ -> ("/", "Repository list")
141
+
Added:
in
142
+
Added:
let home_href = Option.value home_href ~default:"/" in
143
+
Added:
HTML.(
144
+
Added:
nav
145
+
Added:
[ id "top"; Aria.label "Site navigation" ]
146
+
Added:
[
147
+
Added:
nav_logo ~href:logo_href ~alt:logo_alt logo;
148
+
Added:
a [ id "nav-home"; href "%s" home_href ] [ txt "%s" nav_title ];
149
+
Added:
])
150
+
Added:
151
+
Added:
(** Top navigation for repository-scoped pages. *)
152
+
Added:
let topnav ?(active = Summary) ~nav_logo:logo repo =
153
+
Added:
let nav_items =
154
+
Added:
List.map (page_to_nav_item repo)
155
+
Added:
[ Summary; Commits; Files; Branches; Tags; Readme ]
156
+
Added:
in
157
+
Added:
let li_of_item (route, text, page) =
158
+
Added:
let attrs = if page = active then [ Aria.current `page ] else [] in
159
+
Added:
HTML.li attrs [ Routes.link_to route (txt "%s" text) ]
160
+
Added:
in
161
+
Added:
HTML.(
162
+
Added:
nav
163
+
Added:
[ id "top"; Aria.label "Repository navigation" ]
164
+
Added:
[
165
+
Added:
nav_logo ~href:"/" ~alt:"Repository list" logo;
166
+
Added:
repo_breadcrumb repo;
167
+
Added:
input [ type_ "checkbox"; id "nav-toggle"; class_ "nav-toggle" ];
168
+
Added:
label
169
+
Added:
[ for_ "nav-toggle"; class_ "nav-hamburger"; Aria.label "Menu" ]
170
+
Added:
[ txt "\xe2\x8b\xae" ];
171
+
Added:
ul [ id "nav-links" ] (List.map li_of_item nav_items);
172
+
Added:
])
173
+
Added:
174
+
Added:
(** Fixed bottom navigation, revealed by CSS on narrow viewports only. *)
175
+
Added:
let bottomnav ?(active = Summary) repo =
176
+
Added:
let items = [ Summary; Commits; Files; Readme ] in
177
+
Added:
let li_of_item page =
178
+
Added:
let route, nav_label, _ = page_to_nav_item repo page in
179
+
Added:
let attrs =
180
+
Added:
[ HTML.class_ "bottom-nav-item" ]
181
+
Added:
@ if page = active then [ Aria.current `page ] else []
182
+
Added:
in
183
+
Added:
HTML.(li attrs [ Routes.link_to route (txt "%s" nav_label) ])
184
+
Added:
in
185
+
Added:
HTML.(
186
+
Added:
nav
187
+
Added:
[ id "bottom-nav"; Aria.label "Mobile navigation" ]
188
+
Added:
[ ul [ id "bottom-nav-links" ] (List.map li_of_item items) ])
189
+
Added:
190
+
Added:
(** Sticky secondary bar below the top nav. Collapses to nothing when empty so
191
+
Added:
the [body.has-toolbar] sticky offsets stay consistent. *)
192
+
Added:
let repo_toolbar children =
193
+
Added:
match children with
194
+
Added:
| [] -> HTML.null []
195
+
Added:
| _ ->
196
+
Added:
HTML.(
197
+
Added:
div
198
+
Added:
[ id "toolbar"; role `toolbar; Aria.label "Repository toolbar" ]
199
+
Added:
children)