[OCaml] Mobile-friendly clone of cgit.
1
(** Ogit's vocabulary of page parts.
2
3
Where {!Ui} supplies generic building blocks, this module names the parts
4
specific to a Git browser and wires them to {!Routes}, so page modules can
5
describe a page without mentioning HTML or URL strings.
6
7
Every function returns a {!Ui.node}. Nothing here performs I/O. *)
8
9
(** {1 Page identity} *)
10
11
type page =
12
| Summary
13
| Commits
14
| Files
15
(** Which repository page is being shown. Drives the [aria-current] marker
16
in the navigation. *)
17
18
type site = { user_name : string; root_title : string; nav_logo : string }
19
(** Site-wide presentation settings, resolved once from configuration. *)
20
21
let site ~user_name ~root_title ~nav_logo = { user_name; root_title; nav_logo }
22
23
(** {1 Routes as links} *)
24
25
let url route = Routes.path_of route
26
27
(** A link to a route, with the route standing in for a hand-written URL. *)
28
let route_link ?class_ ?label route text =
29
Ui.text_link ?class_ ?label ~href:(url route) text
30
31
(** The commit list is narrowed through query parameters, which {!Routes} does
32
not model because it covers path-shaped routes only. Passing the filters
33
already in effect keeps them applied as the reader pages or switches
34
filters. *)
35
let commits_url ?filter_type ?author ?committer ?(page_number = 1) repo =
36
let params =
37
(if page_number > 1 then [ ("page", string_of_int page_number) ] else [])
38
@ (match filter_type with Some value -> [ ("type", value) ] | None -> [])
39
@ (match author with Some value -> [ ("author", value) ] | None -> [])
40
@ match committer with Some value -> [ ("committer", value) ] | None -> []
41
in
42
let base = Printf.sprintf "/%s/commits/" repo in
43
match params with
44
| [] -> base
45
| _ -> base ^ "?" ^ Dream.to_form_urlencoded params
46
47
(** Where a repository's clone URL lives. *)
48
let clone_url repo = Printf.sprintf "/%s" repo
49
50
(** {1 Navigation} *)
51
52
let page_route repo = function
53
| Summary -> Routes.Repo repo
54
| Commits -> Routes.Commits repo
55
| Files -> Routes.Files repo
56
57
let page_name = function
58
| Summary -> "Summary"
59
| Commits -> "Commits"
60
| Files -> "Files"
61
62
let page_link repo ~active page =
63
Ui.nav_link ~current:(page = active)
64
~href:(url (page_route repo page))
65
(page_name page)
66
67
(** Configured logos may be given as bare paths; make those root-relative while
68
leaving absolute and data URLs alone. *)
69
let asset_url source =
70
if
71
String.starts_with ~prefix:"/" source
72
|| String.starts_with ~prefix:"http://" source
73
|| String.starts_with ~prefix:"https://" source
74
|| String.starts_with ~prefix:"data:" source
75
then source
76
else "/" ^ source
77
78
let logo ~href ~alt source =
79
Ui.link ~id:"nav-logo" ~href
80
[ Ui.image ~class_:"site-logo" ~alt ~src:(asset_url source) () ]
81
82
(** Top navigation away from any repository: the repository list and project
83
directory pages. When [home_href] is absent the page {i is} the list, so the
84
logo points outward instead of back to itself. *)
85
let site_nav ~title ~logo:source ?home_href () =
86
let logo_href, logo_alt =
87
match home_href with
88
| None -> ("https://git-scm.com", "Git website")
89
| Some _ -> ("/", "Repository list")
90
in
91
Ui.navigation ~id:"top" ~label:"Site navigation"
92
[
93
logo ~href:logo_href ~alt:logo_alt source;
94
Ui.text_link ~id:"nav-home"
95
~href:(Option.value home_href ~default:"/")
96
title;
97
]
98
99
(** Top navigation within a repository. The link list collapses behind a
100
CSS-only control on narrow viewports. *)
101
let repo_nav ~active ~logo:source repo =
102
let project_name =
103
match String.split_on_char '/' repo |> List.rev with
104
| last :: _ -> last
105
| [] -> repo
106
in
107
Ui.navigation ~id:"top" ~label:"Repository navigation"
108
[
109
logo ~href:"/" ~alt:"Repository list" source;
110
Ui.text_link ~id:"nav-home" ~href:(url (Routes.Repo repo)) project_name;
111
Ui.css_toggle ~id:"nav-toggle" ~toggle_class:"nav-toggle"
112
~control_class:"nav-hamburger" ~label:"Menu" ~glyph:"\xe2\x8b\xae" ();
113
Ui.nav_links ~id:"nav-links"
114
(List.map (page_link repo ~active) [ Summary; Commits; Files ]);
115
]
116
117
(** The same repository destinations as {!repo_nav}, condensed and pinned to the
118
bottom of the viewport. CSS reveals it on narrow viewports, where the top
119
link list is hidden.
120
121
Named for its form rather than for a device: the viewport width decides when
122
it appears, and this module has no notion of what hardware is in use. *)
123
let compact_repo_nav ~active repo =
124
Ui.navigation ~id:"bottom-nav" ~label:"Compact navigation"
125
[
126
Ui.nav_links ~id:"bottom-nav-links" ~item_class:"bottom-nav-item"
127
(List.map (page_link repo ~active) [ Summary; Commits; Files ]);
128
]
129
130
(** {1 Toolbar} *)
131
132
let toolbar children =
133
Ui.toolbar ~id:"toolbar" ~label:"Repository toolbar" children
134
135
(** {1 Trees} *)
136
137
(** A directory row that both expands in place and links to its own page. *)
138
let directory ?modifier ~route ~name children =
139
Ui.tree_branch ?modifier ~href:(url route) (name ^ "/") children
140
141
let file_entry ?modifier ~route name =
142
Ui.tree_leaf ?modifier ~href:(url route) name
143
144
(** The row closing a truncated listing, linking to the full contents. *)
145
let overflow_row ~route count =
146
Ui.tree_overflow ~href:(url route) (Printf.sprintf "%d more items..." count)
147
148
(** {1 Sections} *)
149
150
(** A collapsible group of repositories on the root page. *)
151
let group ?expanded ~title children =
152
Ui.disclosure ?expanded ~summary_class:"section-toggle"
153
~summary:[ Ui.chevron (); Ui.heading [ Ui.text title ] ]
154
children
155
156
(** {1 Inline pieces} *)
157
158
(** A conventional-commit type, coloured per type and linking to the filtered
159
commit list. *)
160
let commit_type_badge ?href commit_type =
161
Ui.badge ~base_class:"commit-pill" ~variant:commit_type ?href commit_type
162
163
(** A README displayed as semantic documentation.
164
165
Markdown and Org mode receive dedicated rendering; other README filenames
166
use the Markdown-compatible fallback until additional formats are added. *)
167
let inline_readme ?(filename = "README.md") content =
168
Prose.Render.render ~filename content
169