[OCaml] Mobile-friendly clone of cgit.
1
(** Generic site building blocks.
2
3
This module is the only place in the view layer that names HTML elements. It
4
knows nothing about the application domain — no Git, repositories, or ogit
5
routes — so the same vocabulary would serve any static-first web
6
application: every function takes plain strings and already-built nodes.
7
8
{2 Conventions}
9
10
- {b Semantic first.} Each block picks the most meaningful element available
11
([nav], [header], [time], [dl], [details]) rather than a [div] with a
12
class. Callers choose blocks by meaning, not by appearance.
13
- {b Classes are a contract.} Blocks emit a fixed vocabulary of class names
14
— [tree-dir], [tree-toggle], [line-anchor], [pagination-btn] and so on —
15
which a stylesheet is expected to implement. Those names are structural,
16
never domain-specific: a "tree" here is any hierarchical list, not a file
17
tree in particular. Optional [?class_] arguments add a modifier
18
{i alongside} the base class rather than replacing it.
19
- {b No scripting.} Interactive blocks ({!val-disclosure},
20
{!val-css_toggle}) rely on native HTML and CSS, so pages stay usable with
21
JavaScript disabled.
22
- {b Accessibility is not optional.} Where a block can only be used
23
correctly with an accessible name, that name is a required argument rather
24
than an optional one — see {!val-navigation} and {!val-dismissible}.
25
26
Nothing here performs I/O. The attribute plumbing that assembles these
27
blocks is deliberately not exported: callers compose blocks, they do not
28
assemble attributes. *)
29
30
type node = Dream_html.node
31
(** A rendered fragment. Exposed so callers can annotate lists of children
32
without opening [Dream_html] themselves. *)
33
34
val classes : string list -> string
35
(** Join class names, dropping empty ones. Lets callers pass a modifier without
36
having to manage separators or risk a stray leading space. *)
37
38
(** {1 Text and grouping} *)
39
40
val nothing : node
41
(** Renders no markup. Use for absent optional content. *)
42
43
val text : string -> node
44
(** Escaped text. *)
45
46
val group : node list -> node
47
(** Several nodes where one is expected, without introducing a wrapper element.
48
*)
49
50
(** {1 Inline} *)
51
52
val inline : ?class_:string -> ?decorative:bool -> node list -> node
53
(** An inline run of text or nodes.
54
55
@param decorative
56
hides the span from assistive technology, for glyphs that repeat
57
information already available as text. *)
58
59
val inline_text : ?class_:string -> ?decorative:bool -> string -> node
60
(** {!val-inline} around a single string. *)
61
62
(** {1 Links} *)
63
64
val link :
65
?id:string ->
66
?class_:string ->
67
?label:string ->
68
href:string ->
69
node list ->
70
node
71
(** A hyperlink.
72
73
@param label
74
an accessible name, for links whose visible text is not descriptive on its
75
own. *)
76
77
val text_link :
78
?id:string -> ?class_:string -> ?label:string -> href:string -> string -> node
79
(** {!link} around a single string. *)
80
81
(** {1 Images} *)
82
83
val image : ?class_:string -> ?alt:string -> src:string -> unit -> node
84
(** @param alt
85
omit for decorative images; the block then marks itself presentational so
86
screen readers skip it. *)
87
88
(** {1 Blocks} *)
89
90
val block : ?id:string -> ?class_:string -> node list -> node
91
(** A generic grouping box. Reach for {!region} or one of the page landmarks
92
first; this is for layout wrappers that carry no meaning of their own. *)
93
94
val region : ?id:string -> ?class_:string -> node list -> node
95
(** A self-contained part of a page. *)
96
97
val paragraph : ?class_:string -> node list -> node
98
val paragraph_text : ?class_:string -> string -> node
99
100
val heading : ?id:string -> ?level:int -> ?class_:string -> node list -> node
101
(** A heading. [level] follows the document outline: 1 for the page's subject, 2
102
and 3 for nested sections. Skipping levels breaks screen-reader navigation,
103
so pass the level that matches the structure rather than the one that looks
104
right. Levels outside 1–6 clamp to 6. [id] provides a local fragment target.
105
*)
106
107
val code_block : ?class_:string -> node list -> node
108
(** A preformatted code block without line numbers. The children can be escaped
109
text or server-rendered syntax-highlight spans. *)
110
111
(** {1 Lists} *)
112
113
val items : ?id:string -> ?class_:string -> node list -> node
114
(** An unordered list wrapping already-built {!item} nodes. *)
115
116
val ordered_items : ?id:string -> ?class_:string -> node list -> node
117
(** An ordered list wrapping already-built {!item} nodes. *)
118
119
val item : ?class_:string -> ?current:bool -> node list -> node
120
(** A list entry.
121
122
@param current
123
marks the entry as the one matching the current page, for navigation
124
lists. *)
125
126
val items_of : ?id:string -> ?class_:string -> ('a -> node) -> 'a list -> node
127
(** A list built from values, saving callers a [List.map]. *)
128
129
val code_inline : ?class_:string -> string -> node
130
(** An inline code fragment. *)
131
132
(** {1 Badges} *)
133
134
val badge :
135
?base_class:string -> ?variant:string -> ?href:string -> string -> node
136
(** A small rounded label. [variant] is appended to the base class as
137
[<base> <base>-<variant>] so a stylesheet can colour each kind. [href] turns
138
the label's text into a link while leaving the badge itself inert. *)
139
140
(** {1 Time} *)
141
142
val timestamp : machine:string -> string -> node
143
(** A machine-readable timestamp: [machine] fills the [datetime] attribute, the
144
positional argument is the visible text. *)
145
146
(** {1 Definition lists} *)
147
148
val definitions : ?class_:string -> (string * node list) list -> node
149
(** Term/description pairs, for metadata panels. *)
150
151
(** {1 Disclosure} *)
152
153
val chevron : ?class_:string -> unit -> node
154
(** Decorative open/close indicator, rotated by CSS from the enclosing
155
[details]. It carries no textual meaning, so it is hidden from assistive
156
technology. *)
157
158
val disclosure :
159
?id:string ->
160
?class_:string ->
161
?expanded:bool ->
162
?summary_class:string ->
163
summary:node list ->
164
node list ->
165
node
166
(** A native disclosure widget: [details] wrapping a clickable [summary] and its
167
panel. No JavaScript involved.
168
169
@param expanded renders the panel open on load.
170
@param summary the always-visible header contents. *)
171
172
val css_toggle :
173
id:string ->
174
toggle_class:string ->
175
control_class:string ->
176
label:string ->
177
glyph:string ->
178
unit ->
179
node
180
(** A CSS-only toggle: a visually hidden checkbox paired with a [label] acting
181
as its control. Lets stylesheets reveal and collapse content without
182
scripting.
183
184
@param label the accessible name of the control, whose [glyph] has none. *)
185
186
(** {1 Table of contents} *)
187
188
type toc_entry
189
(** One entry in a table of contents. Build with {!val-toc_entry}. Entries may
190
contain nested children to represent subheading hierarchy. *)
191
192
val toc_entry : ?children:toc_entry list -> href:string -> string -> toc_entry
193
(** A TOC entry linking to a fragment.
194
195
@param children
196
nested sub-entries displayed as an indented list beneath this entry. *)
197
198
val toc : ?class_:string -> title:string -> toc_entry list -> node
199
(** A collapsible table of contents with support for nested sub-entries. Renders
200
as a disclosure widget with classes [toc], [toc-summary], and [toc-list].
201
Nested children produce nested [toc-list] elements for semantic indentation.
202
Returns {!nothing} when the list has fewer than two entries.
203
204
@param class_ an additional modifier alongside the base [toc] class. *)
205
206
(** {1 Trees}
207
208
A "tree" is any hierarchical list: rows that either stand alone or expand to
209
reveal nested rows. Compose these into {!items}. *)
210
211
val tree_leaf : ?modifier:string -> href:string -> string -> node
212
(** A leaf row.
213
214
@param modifier a class added alongside the base [tree-file] class. *)
215
216
val tree_branch :
217
?modifier:string ->
218
?expanded:bool ->
219
href:string ->
220
string ->
221
node list ->
222
node
223
(** A branch row.
224
225
Renders a disclosure inside the list item: the summary holds a chevron and a
226
link, the panel holds the nested list. Clicking the summary padding or the
227
chevron toggles; clicking the link navigates. Every nested collection on a
228
site therefore gets the same keyboard and pointer behaviour.
229
230
@param modifier a class added alongside the base [tree-dir] class.
231
@param expanded renders the nested list open on load. *)
232
233
val tree_overflow : ?class_:string -> href:string -> string -> node
234
(** A row standing in for entries omitted from a truncated list. *)
235
236
(** {1 Breadcrumbs} *)
237
238
type crumb
239
(** One step in a trail. Build with {!val-crumb}. *)
240
241
val crumb : ?href:string -> string -> crumb
242
(** A trail step. Without an href it renders as plain text, which is how the
243
current location should be shown. *)
244
245
val breadcrumb :
246
?id:string ->
247
?class_:string ->
248
?link_class:string ->
249
?separator_class:string ->
250
?separator_decorative:bool ->
251
separator:string ->
252
crumb list ->
253
node
254
(** A trail of links joined by a separator.
255
256
@param separator_decorative
257
hides the separators from assistive technology. Appropriate when the trail
258
already reads as a list of links; leave it off when the separator carries
259
meaning, such as a path delimiter worth reading aloud. *)
260
261
(** {1 Navigation} *)
262
263
type nav_link
264
(** A destination in a navigation list. Build with {!val-nav_link}. *)
265
266
val nav_link : ?current:bool -> href:string -> string -> nav_link
267
268
val navigation :
269
?id:string -> ?class_:string -> label:string -> node list -> node
270
(** A navigation landmark. [label] is required because it names the landmark for
271
assistive technology, which matters as soon as a page has more than one. *)
272
273
val nav_links :
274
?id:string -> ?class_:string -> ?item_class:string -> nav_link list -> node
275
(** A list of navigation links; the current page's item carries
276
[aria-current="page"]. *)
277
278
(** {1 Toolbars} *)
279
280
val toolbar : ?id:string -> ?class_:string -> ?label:string -> node list -> node
281
(** A bar of controls acting on the current page. An empty toolbar renders
282
nothing, so layout offsets that depend on its presence stay consistent. *)
283
284
val button_link :
285
?class_:string -> ?label:string -> href:string -> string -> node
286
(** A link styled as a toolbar button. Still a link, not a [button], because it
287
navigates rather than acting on the current page — which keeps middle-click
288
and "open in new tab" working. *)
289
290
val dismissible :
291
?class_:string ->
292
?dismiss_class:string ->
293
value_class:string ->
294
dismiss_href:string ->
295
dismiss_label:string ->
296
string ->
297
node
298
(** An active filter together with a control that removes it.
299
300
@param value_class styles the displayed value.
301
@param dismiss_label
302
accessible name of the remove control, required because its glyph conveys
303
nothing on its own. *)
304
305
(** {1 Pagination} *)
306
307
val pagination :
308
?label:string ->
309
?previous_text:string ->
310
?next_text:string ->
311
?previous_label:string ->
312
?next_label:string ->
313
?previous_href:string ->
314
?next_href:string ->
315
int ->
316
node
317
(** Previous/next controls around a page number.
318
319
A missing neighbour — an absent [previous_href] or [next_href] — renders as
320
an inert, aria-hidden placeholder rather than disappearing, so the controls
321
keep their position between pages.
322
323
The glyphs are decorative; [previous_label] and [next_label] carry the
324
accessible names. *)
325
326
(** {1 Code} *)
327
328
val code_listing :
329
?id:string -> ?class_:string -> ?anchor_prefix:string -> string -> node
330
(** A line-numbered listing of source text.
331
332
Every line gets a stable anchor so single lines can be linked and
333
highlighted. [anchor_prefix] namespaces those anchors, which is required
334
when one page shows more than one listing.
335
336
Content is emitted verbatim as plain text with no syntax colouring. For
337
highlighted output, use {!highlighted_code_listing} instead. *)
338
339
val highlighted_code_listing :
340
?id:string ->
341
?class_:string ->
342
?anchor_prefix:string ->
343
node list list ->
344
node
345
(** A line-numbered listing of pre-highlighted source code.
346
347
Like {!code_listing} but accepts lines already tokenized into styled spans
348
(e.g. from {!Highlight.highlight}). Each inner list represents one line's
349
worth of nodes; the function adds line numbers and anchors in the same grid
350
layout as [code_listing]. *)
351
352
(** {1 Diffs} *)
353
354
(** A viewer for line-oriented change sets. The data types are deliberately
355
plain so any producer of diffs can feed them. *)
356
module Diff : sig
357
type change = Unchanged | Added | Removed
358
359
type line = {
360
before : string; (** line number in the old revision, or [""] *)
361
after : string; (** line number in the new revision, or [""] *)
362
change : change;
363
content : string;
364
}
365
366
type section = { section_heading : string; lines : line list }
367
368
type file = {
369
path : string;
370
detail : string; (** provenance line, e.g. revision identifiers *)
371
sections : section list;
372
note : string option; (** shown instead of sections, e.g. binary files *)
373
}
374
375
val view : empty_message:string -> file list -> node list
376
(** Render a change set, or a single paragraph carrying [empty_message] when
377
there is nothing to show. *)
378
end
379
380
(** {1 Document scaffolding}
381
382
Two families, kept apart by prefix. The [document_] functions build the
383
envelope a browser reads — [html], [head], [body] — and the [page_]
384
functions build the landmarks a reader sees inside it. [document_head] and
385
[page_banner] are the pair most easily confused: the first is metadata, the
386
second is the visible masthead. *)
387
388
val meta_viewport : node
389
(** Opts the page into responsive layout. Without it mobile browsers assume a
390
desktop-width viewport and scale the page down. *)
391
392
val stylesheet : string -> node
393
val icon : ?media_type:string -> string -> node
394
395
val deferred_script : string -> node
396
(** An external script that does not block rendering. *)
397
398
val inline_script : string -> node
399
(** Inline behaviour. Reserved for progressive enhancement: pages must stay
400
usable when it does not run. *)
401
402
val skip_link : href:string -> string -> node
403
(** A link that jumps past repeated navigation, revealed on focus. Expected on
404
every page for keyboard users. *)
405
406
(** {2 The document envelope} *)
407
408
val document_head : title:string -> node list -> node
409
(** The [head] element: title, metadata and asset links. [title] comes first so
410
it cannot be forgotten. *)
411
412
val document_body : ?class_:string -> node list -> node
413
(** The [body] element. *)
414
415
val document : ?lang:string -> head:node -> body:node -> unit -> node
416
(** A complete document. [lang] defaults to ["en"]; set it so screen readers
417
pick the right pronunciation. *)
418
419
(** {2 Landmarks within the page} *)
420
421
val page_banner : ?id:string -> ?class_:string -> node list -> node
422
(** The [header] element introducing the page — its masthead. Named for the ARIA
423
landmark it maps to, and to keep it distinct from {!val-document_head}. *)
424
425
val page_content : ?id:string -> ?class_:string -> node list -> node
426
(** The [main] element: the content unique to this page, which the skip link
427
targets. At most one per document. *)
428
429
val page_footer : ?class_:string -> node list -> node
430
(** The [footer] element closing the page. *)
431
432
(** {1 Responses} *)
433
434
val respond : ?status:[< Dream.status ] -> node -> Dream.response Dream.promise
435
(** Send a rendered document as an HTTP response. *)
436