(** Ogit's vocabulary of page parts. Where {!Ui} supplies generic building blocks, this module names the parts specific to a Git browser and wires them to {!Routes}, so page modules can describe a page without mentioning HTML or URL strings. Every function returns a {!Ui.node}. Nothing here performs I/O. *) (** {1 Page identity} *) type page = | Summary | Commits | Files (** Which repository page is being shown. Drives the [aria-current] marker in the navigation. *) type site = { user_name : string; root_title : string; nav_logo : string } (** Site-wide presentation settings, resolved once from configuration. *) let site ~user_name ~root_title ~nav_logo = { user_name; root_title; nav_logo } (** {1 Routes as links} *) let url route = Routes.path_of route (** A link to a route, with the route standing in for a hand-written URL. *) let route_link ?class_ ?label route text = Ui.text_link ?class_ ?label ~href:(url route) text (** The commit list is narrowed through query parameters, which {!Routes} does not model because it covers path-shaped routes only. Passing the filters already in effect keeps them applied as the reader pages or switches filters. *) let commits_url ?filter_type ?author ?committer ?(page_number = 1) repo = let params = (if page_number > 1 then [ ("page", string_of_int page_number) ] else []) @ (match filter_type with Some value -> [ ("type", value) ] | None -> []) @ (match author with Some value -> [ ("author", value) ] | None -> []) @ match committer with Some value -> [ ("committer", value) ] | None -> [] in let base = Printf.sprintf "/%s/commits/" repo in match params with | [] -> base | _ -> base ^ "?" ^ Dream.to_form_urlencoded params (** Where a repository's clone URL lives. *) let clone_url repo = Printf.sprintf "/%s" repo (** {1 Navigation} *) let page_route repo = function | Summary -> Routes.Repo repo | Commits -> Routes.Commits repo | Files -> Routes.Files repo let page_name = function | Summary -> "Summary" | Commits -> "Commits" | Files -> "Files" let page_link repo ~active page = Ui.nav_link ~current:(page = active) ~href:(url (page_route repo page)) (page_name page) (** Configured logos may be given as bare paths; make those root-relative while leaving absolute and data URLs alone. *) let asset_url source = if String.starts_with ~prefix:"/" source || String.starts_with ~prefix:"http://" source || String.starts_with ~prefix:"https://" source || String.starts_with ~prefix:"data:" source then source else "/" ^ source let logo ~href ~alt source = Ui.link ~id:"nav-logo" ~href [ Ui.image ~class_:"site-logo" ~alt ~src:(asset_url source) () ] (** Top navigation away from any repository: the repository list and project directory pages. When [home_href] is absent the page {i is} the list, so the logo points outward instead of back to itself. *) let site_nav ~title ~logo:source ?home_href () = let logo_href, logo_alt = match home_href with | None -> ("https://git-scm.com", "Git website") | Some _ -> ("/", "Repository list") in Ui.navigation ~id:"top" ~label:"Site navigation" [ logo ~href:logo_href ~alt:logo_alt source; Ui.text_link ~id:"nav-home" ~href:(Option.value home_href ~default:"/") title; ] (** Top navigation within a repository. The link list collapses behind a CSS-only control on narrow viewports. *) let repo_nav ~active ~logo:source repo = let project_name = match String.split_on_char '/' repo |> List.rev with | last :: _ -> last | [] -> repo in Ui.navigation ~id:"top" ~label:"Repository navigation" [ logo ~href:"/" ~alt:"Repository list" source; Ui.text_link ~id:"nav-home" ~href:(url (Routes.Repo repo)) project_name; Ui.css_toggle ~id:"nav-toggle" ~toggle_class:"nav-toggle" ~control_class:"nav-hamburger" ~label:"Menu" ~glyph:"\xe2\x8b\xae" (); Ui.nav_links ~id:"nav-links" (List.map (page_link repo ~active) [ Summary; Commits; Files ]); ] (** The same repository destinations as {!repo_nav}, condensed and pinned to the bottom of the viewport. CSS reveals it on narrow viewports, where the top link list is hidden. Named for its form rather than for a device: the viewport width decides when it appears, and this module has no notion of what hardware is in use. *) let compact_repo_nav ~active repo = Ui.navigation ~id:"bottom-nav" ~label:"Compact navigation" [ Ui.nav_links ~id:"bottom-nav-links" ~item_class:"bottom-nav-item" (List.map (page_link repo ~active) [ Summary; Commits; Files ]); ] (** {1 Toolbar} *) let toolbar children = Ui.toolbar ~id:"toolbar" ~label:"Repository toolbar" children (** {1 Trees} *) (** A directory row that both expands in place and links to its own page. *) let directory ?modifier ~route ~name children = Ui.tree_branch ?modifier ~href:(url route) (name ^ "/") children let file_entry ?modifier ~route name = Ui.tree_leaf ?modifier ~href:(url route) name (** The row closing a truncated listing, linking to the full contents. *) let overflow_row ~route count = Ui.tree_overflow ~href:(url route) (Printf.sprintf "%d more items..." count) (** {1 Sections} *) (** A collapsible group of repositories on the root page. *) let group ?expanded ~title children = Ui.disclosure ?expanded ~summary_class:"section-toggle" ~summary:[ Ui.chevron (); Ui.heading [ Ui.text title ] ] children (** {1 Inline pieces} *) (** A conventional-commit type, coloured per type and linking to the filtered commit list. *) let commit_type_badge ?href commit_type = Ui.badge ~base_class:"commit-pill" ~variant:commit_type ?href commit_type (** A README displayed as semantic documentation. Markdown and Org mode receive dedicated rendering; other README filenames use the Markdown-compatible fallback until additional formats are added. *) let inline_readme ?(filename = "README.md") content = Prose.Render.render ~filename content