View raw

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