(** Generic site building blocks.
This module is the only place in the view layer that names HTML elements. It
knows nothing about the application domain — no Git, repositories, or ogit
routes — so the same vocabulary would serve any static-first web
application: every function takes plain strings and already-built nodes.
{2 Conventions}
- {b Semantic first.} Each block picks the most meaningful element available
([nav], [header], [time], [dl], [details]) rather than a [div] with a
class. Callers choose blocks by meaning, not by appearance.
- {b Classes are a contract.} Blocks emit a fixed vocabulary of class names
— [tree-dir], [tree-toggle], [line-anchor], [pagination-btn] and so on —
which a stylesheet is expected to implement. Those names are structural,
never domain-specific: a "tree" here is any hierarchical list, not a file
tree in particular. Optional [?class_] arguments add a modifier
{i alongside} the base class rather than replacing it.
- {b No scripting.} Interactive blocks ({!val-disclosure},
{!val-css_toggle}) rely on native HTML and CSS, so pages stay usable with
JavaScript disabled.
- {b Accessibility is not optional.} Where a block can only be used
correctly with an accessible name, that name is a required argument rather
than an optional one — see {!val-navigation} and {!val-dismissible}.
Nothing here performs I/O. The attribute plumbing that assembles these
blocks is deliberately not exported: callers compose blocks, they do not
assemble attributes. *)
type node = Dream_html.node
(** A rendered fragment. Exposed so callers can annotate lists of children
without opening [Dream_html] themselves. *)
val classes : string list -> string
(** Join class names, dropping empty ones. Lets callers pass a modifier without
having to manage separators or risk a stray leading space. *)
(** {1 Text and grouping} *)
val nothing : node
(** Renders no markup. Use for absent optional content. *)
val text : string -> node
(** Escaped text. *)
val group : node list -> node
(** Several nodes where one is expected, without introducing a wrapper element.
*)
(** {1 Inline} *)
val inline : ?class_:string -> ?decorative:bool -> node list -> node
(** An inline run of text or nodes.
@param decorative
hides the span from assistive technology, for glyphs that repeat
information already available as text. *)
val inline_text : ?class_:string -> ?decorative:bool -> string -> node
(** {!val-inline} around a single string. *)
(** {1 Links} *)
val link :
?id:string ->
?class_:string ->
?label:string ->
href:string ->
node list ->
node
(** A hyperlink.
@param label
an accessible name, for links whose visible text is not descriptive on its
own. *)
val text_link :
?id:string -> ?class_:string -> ?label:string -> href:string -> string -> node
(** {!link} around a single string. *)
(** {1 Images} *)
val image : ?class_:string -> ?alt:string -> src:string -> unit -> node
(** @param alt
omit for decorative images; the block then marks itself presentational so
screen readers skip it. *)
(** {1 Blocks} *)
val block : ?id:string -> ?class_:string -> node list -> node
(** A generic grouping box. Reach for {!region} or one of the page landmarks
first; this is for layout wrappers that carry no meaning of their own. *)
val region : ?id:string -> ?class_:string -> node list -> node
(** A self-contained part of a page. *)
val paragraph : ?class_:string -> node list -> node
val paragraph_text : ?class_:string -> string -> node
val heading : ?id:string -> ?level:int -> ?class_:string -> node list -> node
(** A heading. [level] follows the document outline: 1 for the page's subject, 2
and 3 for nested sections. Skipping levels breaks screen-reader navigation,
so pass the level that matches the structure rather than the one that looks
right. Levels outside 1–6 clamp to 6. [id] provides a local fragment target.
*)
val code_block : ?class_:string -> node list -> node
(** A preformatted code block without line numbers. The children can be escaped
text or server-rendered syntax-highlight spans. *)
(** {1 Lists} *)
val items : ?id:string -> ?class_:string -> node list -> node
(** An unordered list wrapping already-built {!item} nodes. *)
val ordered_items : ?id:string -> ?class_:string -> node list -> node
(** An ordered list wrapping already-built {!item} nodes. *)
val item : ?class_:string -> ?current:bool -> node list -> node
(** A list entry.
@param current
marks the entry as the one matching the current page, for navigation
lists. *)
val items_of : ?id:string -> ?class_:string -> ('a -> node) -> 'a list -> node
(** A list built from values, saving callers a [List.map]. *)
val code_inline : ?class_:string -> string -> node
(** An inline code fragment. *)
(** {1 Badges} *)
val badge :
?base_class:string -> ?variant:string -> ?href:string -> string -> node
(** A small rounded label. [variant] is appended to the base class as
[ -] so a stylesheet can colour each kind. [href] turns
the label's text into a link while leaving the badge itself inert. *)
(** {1 Time} *)
val timestamp : machine:string -> string -> node
(** A machine-readable timestamp: [machine] fills the [datetime] attribute, the
positional argument is the visible text. *)
(** {1 Definition lists} *)
val definitions : ?class_:string -> (string * node list) list -> node
(** Term/description pairs, for metadata panels. *)
(** {1 Disclosure} *)
val chevron : ?class_:string -> unit -> node
(** Decorative open/close indicator, rotated by CSS from the enclosing
[details]. It carries no textual meaning, so it is hidden from assistive
technology. *)
val disclosure :
?id:string ->
?class_:string ->
?expanded:bool ->
?summary_class:string ->
summary:node list ->
node list ->
node
(** A native disclosure widget: [details] wrapping a clickable [summary] and its
panel. No JavaScript involved.
@param expanded renders the panel open on load.
@param summary the always-visible header contents. *)
val css_toggle :
id:string ->
toggle_class:string ->
control_class:string ->
label:string ->
glyph:string ->
unit ->
node
(** A CSS-only toggle: a visually hidden checkbox paired with a [label] acting
as its control. Lets stylesheets reveal and collapse content without
scripting.
@param label the accessible name of the control, whose [glyph] has none. *)
(** {1 Table of contents} *)
type toc_entry
(** One entry in a table of contents. Build with {!val-toc_entry}. Entries may
contain nested children to represent subheading hierarchy. *)
val toc_entry : ?children:toc_entry list -> href:string -> string -> toc_entry
(** A TOC entry linking to a fragment.
@param children
nested sub-entries displayed as an indented list beneath this entry. *)
val toc : ?class_:string -> title:string -> toc_entry list -> node
(** A collapsible table of contents with support for nested sub-entries. Renders
as a disclosure widget with classes [toc], [toc-summary], and [toc-list].
Nested children produce nested [toc-list] elements for semantic indentation.
Returns {!nothing} when the list has fewer than two entries.
@param class_ an additional modifier alongside the base [toc] class. *)
(** {1 Trees}
A "tree" is any hierarchical list: rows that either stand alone or expand to
reveal nested rows. Compose these into {!items}. *)
val tree_leaf : ?modifier:string -> href:string -> string -> node
(** A leaf row.
@param modifier a class added alongside the base [tree-file] class. *)
val tree_branch :
?modifier:string ->
?expanded:bool ->
href:string ->
string ->
node list ->
node
(** A branch row.
Renders a disclosure inside the list item: the summary holds a chevron and a
link, the panel holds the nested list. Clicking the summary padding or the
chevron toggles; clicking the link navigates. Every nested collection on a
site therefore gets the same keyboard and pointer behaviour.
@param modifier a class added alongside the base [tree-dir] class.
@param expanded renders the nested list open on load. *)
val tree_overflow : ?class_:string -> href:string -> string -> node
(** A row standing in for entries omitted from a truncated list. *)
(** {1 Breadcrumbs} *)
type crumb
(** One step in a trail. Build with {!val-crumb}. *)
val crumb : ?href:string -> string -> crumb
(** A trail step. Without an href it renders as plain text, which is how the
current location should be shown. *)
val breadcrumb :
?id:string ->
?class_:string ->
?link_class:string ->
?separator_class:string ->
?separator_decorative:bool ->
separator:string ->
crumb list ->
node
(** A trail of links joined by a separator.
@param separator_decorative
hides the separators from assistive technology. Appropriate when the trail
already reads as a list of links; leave it off when the separator carries
meaning, such as a path delimiter worth reading aloud. *)
(** {1 Navigation} *)
type nav_link
(** A destination in a navigation list. Build with {!val-nav_link}. *)
val nav_link : ?current:bool -> href:string -> string -> nav_link
val navigation :
?id:string -> ?class_:string -> label:string -> node list -> node
(** A navigation landmark. [label] is required because it names the landmark for
assistive technology, which matters as soon as a page has more than one. *)
val nav_links :
?id:string -> ?class_:string -> ?item_class:string -> nav_link list -> node
(** A list of navigation links; the current page's item carries
[aria-current="page"]. *)
(** {1 Toolbars} *)
val toolbar : ?id:string -> ?class_:string -> ?label:string -> node list -> node
(** A bar of controls acting on the current page. An empty toolbar renders
nothing, so layout offsets that depend on its presence stay consistent. *)
val button_link :
?class_:string -> ?label:string -> href:string -> string -> node
(** A link styled as a toolbar button. Still a link, not a [button], because it
navigates rather than acting on the current page — which keeps middle-click
and "open in new tab" working. *)
val dismissible :
?class_:string ->
?dismiss_class:string ->
value_class:string ->
dismiss_href:string ->
dismiss_label:string ->
string ->
node
(** An active filter together with a control that removes it.
@param value_class styles the displayed value.
@param dismiss_label
accessible name of the remove control, required because its glyph conveys
nothing on its own. *)
(** {1 Pagination} *)
val pagination :
?label:string ->
?previous_text:string ->
?next_text:string ->
?previous_label:string ->
?next_label:string ->
?previous_href:string ->
?next_href:string ->
int ->
node
(** Previous/next controls around a page number.
A missing neighbour — an absent [previous_href] or [next_href] — renders as
an inert, aria-hidden placeholder rather than disappearing, so the controls
keep their position between pages.
The glyphs are decorative; [previous_label] and [next_label] carry the
accessible names. *)
(** {1 Code} *)
val code_listing :
?id:string -> ?class_:string -> ?anchor_prefix:string -> string -> node
(** A line-numbered listing of source text.
Every line gets a stable anchor so single lines can be linked and
highlighted. [anchor_prefix] namespaces those anchors, which is required
when one page shows more than one listing.
Content is emitted verbatim as plain text with no syntax colouring. For
highlighted output, use {!highlighted_code_listing} instead. *)
val highlighted_code_listing :
?id:string ->
?class_:string ->
?anchor_prefix:string ->
node list list ->
node
(** A line-numbered listing of pre-highlighted source code.
Like {!code_listing} but accepts lines already tokenized into styled spans
(e.g. from {!Highlight.highlight}). Each inner list represents one line's
worth of nodes; the function adds line numbers and anchors in the same grid
layout as [code_listing]. *)
(** {1 Diffs} *)
(** A viewer for line-oriented change sets. The data types are deliberately
plain so any producer of diffs can feed them. *)
module Diff : sig
type change = Unchanged | Added | Removed
type line = {
before : string; (** line number in the old revision, or [""] *)
after : string; (** line number in the new revision, or [""] *)
change : change;
content : string;
}
type section = { section_heading : string; lines : line list }
type file = {
path : string;
detail : string; (** provenance line, e.g. revision identifiers *)
sections : section list;
note : string option; (** shown instead of sections, e.g. binary files *)
}
val view : empty_message:string -> file list -> node list
(** Render a change set, or a single paragraph carrying [empty_message] when
there is nothing to show. *)
end
(** {1 Document scaffolding}
Two families, kept apart by prefix. The [document_] functions build the
envelope a browser reads — [html], [head], [body] — and the [page_]
functions build the landmarks a reader sees inside it. [document_head] and
[page_banner] are the pair most easily confused: the first is metadata, the
second is the visible masthead. *)
val meta_viewport : node
(** Opts the page into responsive layout. Without it mobile browsers assume a
desktop-width viewport and scale the page down. *)
val stylesheet : string -> node
val icon : ?media_type:string -> string -> node
val deferred_script : string -> node
(** An external script that does not block rendering. *)
val inline_script : string -> node
(** Inline behaviour. Reserved for progressive enhancement: pages must stay
usable when it does not run. *)
val skip_link : href:string -> string -> node
(** A link that jumps past repeated navigation, revealed on focus. Expected on
every page for keyboard users. *)
(** {2 The document envelope} *)
val document_head : title:string -> node list -> node
(** The [head] element: title, metadata and asset links. [title] comes first so
it cannot be forgotten. *)
val document_body : ?class_:string -> node list -> node
(** The [body] element. *)
val document : ?lang:string -> head:node -> body:node -> unit -> node
(** A complete document. [lang] defaults to ["en"]; set it so screen readers
pick the right pronunciation. *)
(** {2 Landmarks within the page} *)
val page_banner : ?id:string -> ?class_:string -> node list -> node
(** The [header] element introducing the page — its masthead. Named for the ARIA
landmark it maps to, and to keep it distinct from {!val-document_head}. *)
val page_content : ?id:string -> ?class_:string -> node list -> node
(** The [main] element: the content unique to this page, which the skip link
targets. At most one per document. *)
val page_footer : ?class_:string -> node list -> node
(** The [footer] element closing the page. *)
(** {1 Responses} *)
val respond : ?status:[< Dream.status ] -> node -> Dream.response Dream.promise
(** Send a rendered document as an HTTP response. *)