(* * Copyright (c) 2016 Thomas Refis * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *) open Odoc_utils module HLink = Link open Odoc_document.Types module Html = Tyxml.Html module Doctree = Odoc_document.Doctree module Url = Odoc_document.Url module Link = HLink type any = Html_types.flow5 type item = Html_types.flow5_without_header_footer type flow = Html_types.flow5_without_sectioning_heading_header_footer type phrasing = Html_types.phrasing type non_link_phrasing = Html_types.phrasing_without_interactive let mk_anchor_link id = [ Html.a ~a:[ Html.a_href ("#" ^ id); Html.a_class [ "anchor" ] ] [] ] let mk_anchor config anchor = match anchor with | None -> ([], [], []) | _ when Config.search_result config -> (* When displaying for a search result, anchor are not added as it would make no sense to add them. *) ([], [], []) | Some { Url.Anchor.anchor; _ } -> let link = mk_anchor_link anchor in let extra_attr = [ Html.a_id anchor ] in let extra_class = [ "anchored" ] in (extra_attr, extra_class, link) let mk_link_to_source ~config ~resolve anchor = match anchor with | None -> [] | Some url -> let href = Link.href ~config ~resolve url in [ Html.a ~a:[ Html.a_href href; Html.a_class [ "source_link" ] ] [ Html.txt "Source" ]; ] let class_ (l : Class.t) = if l = [] then [] else [ Html.a_class l ] let inline_math (s : Math.t) = Html.code ~a:[ Html.a_class [ "odoc-katex-math" ] ] [ Html.txt s ] let source_text_content (t : Source.t) = let buf = Buffer.create 16 in let rec token (x : Source.token) = match x with | Elt inl -> List.iter (fun (one : Inline.one) -> match one.desc with Inline.Text s -> Buffer.add_string buf s | _ -> ()) inl | Tag (_, l) -> List.iter token l in List.iter token t; Buffer.contents buf let block_math (s : Math.t) = Html.pre ~a:[ Html.a_class [ "odoc-katex-math"; "display" ] ] [ Html.txt s ] and raw_markup (t : Raw_markup.t) = let target, content = t in match Astring.String.Ascii.lowercase target with | "html" -> (* This is OK because we output *textual* HTML. In theory, we should try to parse the HTML with lambdasoup and rebuild the HTML tree from there. *) [ Html.Unsafe.data content ] | _ -> [] and source k ?a ?mode_links (t : Source.t) = let rec token (x : Source.token) = match x with | Elt i -> k i | Tag (None, l) -> let content = tokens l in if content = [] then [] else [ Html.span content ] | Tag (Some "mode", l) -> ( match mode_links with | Some base_uri -> let name = source_text_content l in let href = base_uri ^ "#" ^ name in let content = tokens l in let link = Html.a ~a:[ Html.a_href href; Html.a_class [ "mode-link" ] ] (Html.totl (Html.toeltl content)) in Html.totl (Html.toeltl [ link ]) | None -> [ Html.span ~a:[ Html.a_class [ "mode" ] ] (tokens l) ]) | Tag (Some s, l) -> [ Html.span ~a:[ Html.a_class [ s ] ] (tokens l) ] and tokens t = List.concat_map token t in match tokens t with [] -> [] | l -> [ Html.code ?a l ] and styled style ~emph_level = match style with | `Emphasis -> let a = if emph_level mod 2 = 0 then [] else [ Html.a_class [ "odd" ] ] in (emph_level + 1, Html.em ~a) | `Bold -> (emph_level, Html.b ~a:[]) | `Italic -> (emph_level, Html.i ~a:[]) | `Superscript -> (emph_level, Html.sup ~a:[]) | `Subscript -> (emph_level, Html.sub ~a:[]) let rec internallink ~config ~emph_level ~resolve ?(a = []) target content tooltip = let a = match tooltip with Some s -> Html.a_title s :: a | None -> a in let elt = match target with | Target.Resolved uri -> let href = Link.href ~config ~resolve uri in let content = inline_nolink ~emph_level content in if Config.search_result config then (* When displaying for a search result, links are displayed as regular text. *) Html.span ~a content else let a = Html.a_href href :: (a :> Html_types.a_attrib Html.attrib list) in Html.a ~a content | Unresolved -> (* let title = * Html.a_title (Printf.sprintf "unresolved reference to %S" * (ref_to_string ref) * in *) let a = Html.a_class [ "xref-unresolved" ] :: a in Html.span ~a (inline ~config ~emph_level ~resolve content) in [ (elt :> phrasing Html.elt) ] and inline ~config ?(emph_level = 0) ~resolve (l : Inline.t) : phrasing Html.elt list = let one (t : Inline.one) = let a = class_ t.attr in match t.desc with | Text "" -> [] | Text s -> if a = [] then [ Html.txt s ] else [ Html.span ~a [ Html.txt s ] ] | Entity s -> if a = [] then [ Html.entity s ] else [ Html.span ~a [ Html.entity s ] ] | Linebreak -> [ Html.br ~a () ] | Styled (style, c) -> let emph_level, app_style = styled style ~emph_level in [ app_style @@ inline ~config ~emph_level ~resolve c ] | Link { content = c; _ } when Config.search_result config -> (* When displaying for a search result, links are displayed as regular text. *) let content = inline_nolink ~emph_level c in [ Html.span ~a content ] | Link { target = External href; content = c; _ } -> let a = (a :> Html_types.a_attrib Html.attrib list) in let content = inline_nolink ~emph_level c in [ Html.a ~a:(Html.a_href href :: a) content ] | Link { target = Internal t; content; tooltip } -> internallink ~config ~emph_level ~resolve ~a t content tooltip | Source c -> source (inline ~config ~emph_level ~resolve) ~a ?mode_links:(Config.mode_links config) c | Math s -> [ inline_math s ] | Raw_markup r -> raw_markup r in List.concat_map one l and inline_nolink ?(emph_level = 0) (l : Inline.t) : non_link_phrasing Html.elt list = let one (t : Inline.one) = let a = class_ t.attr in match t.desc with | Text "" -> [] | Text s -> if a = [] then [ Html.txt s ] else [ Html.span ~a [ Html.txt s ] ] | Entity s -> if a = [] then [ Html.entity s ] else [ Html.span ~a [ Html.entity s ] ] | Linebreak -> [ Html.br ~a () ] | Styled (style, c) -> let emph_level, app_style = styled style ~emph_level in [ app_style @@ inline_nolink ~emph_level c ] | Link _ -> assert false | Source c -> source (inline_nolink ~emph_level) ~a c | Math s -> [ inline_math s ] | Raw_markup r -> raw_markup r in List.concat_map one l let heading ~config ~resolve (h : Heading.t) = let a, anchor = match h.label with | Some _ when Config.search_result config -> (* When displaying for a search result, anchor are not added as it would make no sense to add them. *) ([], []) | Some id -> ([ Html.a_id id ], mk_anchor_link id) | None -> ([], []) in let content = inline ~config ~resolve h.title in let source_link = mk_link_to_source ~config ~resolve h.source_anchor in let mk = match h.level with | 0 -> Html.h1 | 1 -> Html.h2 | 2 -> Html.h3 | 3 -> Html.h4 | 4 -> Html.h5 | _ -> Html.h6 in mk ~a (anchor @ content @ source_link) let text_align = function | Table.Left -> [ Html.a_style "text-align:left" ] | Center -> [ Html.a_style "text-align:center" ] | Right -> [ Html.a_style "text-align:right" ] | Default -> [] let cell_kind = function `Header -> Html.th | `Data -> Html.td let rec block ~config ~resolve (l : Block.t) : flow Html.elt list = let as_flow x = (x : phrasing Html.elt list :> flow Html.elt list) in let one (t : Block.one) = let mk_block ?(extra_class = []) mk content = let a = Some (class_ (extra_class @ t.attr)) in [ mk ?a content ] in let mk_media_block media_block target alt = let block = match target with | Target.External url -> media_block url alt | Internal (Resolved uri) -> let url = Link.href ~config ~resolve uri in media_block url alt | Internal Unresolved -> let content = [ Html.txt alt ] in let a = Html.a_class [ "xref-unresolved" ] :: [] in [ Html.span ~a content ] in mk_block Html.div block in match t.desc with | Inline i -> if t.attr = [] then as_flow @@ inline ~config ~resolve i else mk_block Html.span (inline ~config ~resolve i) | Paragraph i -> mk_block Html.p (inline ~config ~resolve i) | List (typ, l) -> let mk = match typ with Ordered -> Html.ol | Unordered -> Html.ul in mk_block mk (List.map (fun x -> Html.li (block ~config ~resolve x)) l) | Table t -> mk_block ~extra_class:[ "odoc-table" ] (fun ?a x -> Html.table ?a x) (mk_rows ~config ~resolve t) | Description l -> let item i = let a = class_ i.Description.attr in let term = (inline ~config ~resolve i.Description.key : phrasing Html.elt list :> flow Html.elt list) in let def = block ~config ~resolve i.Description.definition in Html.li ~a (term @ (Html.txt " " :: def)) in mk_block Html.ul (List.map item l) | Raw_markup r -> raw_markup r | Verbatim s -> mk_block Html.pre [ Html.txt s ] | Source (lang_tag, _classes, _data, c, output) -> let extra_class = [ "language-" ^ lang_tag ] in mk_block Html.div ((mk_block ~extra_class Html.pre (source (inline ~config ~resolve) ?mode_links:(Config.mode_links config) c)) @ block ~config ~resolve output) | Math s -> mk_block Html.div [ block_math s ] | Audio (target, alt) -> let audio src alt = [ Html.audio ~src ~a:[ Html.a_controls (); Html.a_aria "label" [ alt ] ] []; ] in mk_media_block audio target alt | Video (target, alt) -> let video src alt = [ Html.video ~src ~a:[ Html.a_controls (); Html.a_aria "label" [ alt ] ] []; ] in mk_media_block video target alt | Image (target, alt) -> let image src alt = let img = Html.a ~a:[ Html.a_href src; Html.a_class [ "img-link" ] ] [ Html.img ~src ~alt () ] in [ img ] in mk_media_block image target alt in List.concat_map one l and mk_rows ~config ~resolve { align; data } = let mk_row row = let mk_cell ~align (x, h) = let a = text_align align in cell_kind ~a h (block ~config ~resolve x) in let alignment align = match align with align :: q -> (align, q) | [] -> (Table.Default, []) (* Second case is for recovering from a too short alignment list. A warning should have been raised when loading the doc-comment. *) in let acc, _align = List.fold_left (fun (acc, aligns) (x, h) -> let align, aligns = alignment aligns in let cell = mk_cell ~align (x, h) in (cell :: acc, aligns)) ([], align) row in Html.tr (List.rev acc) in List.map mk_row data (* This coercion is actually sound, but is not currently accepted by Tyxml. See https://github.com/ocsigen/tyxml/pull/265 for details Can be replaced by a simple type coercion once this is fixed *) let flow_to_item : flow Html.elt list -> item Html.elt list = fun x -> Html.totl @@ Html.toeltl x let div : ([< Html_types.div_attrib ], [< item ], [> Html_types.div ]) Html.star = Html.Unsafe.node "div" let spec_class attr = class_ ("spec" :: attr) let spec_doc_div ~config ~resolve = function | [] -> [] | docs -> let a = [ Html.a_class [ "spec-doc" ] ] in [ div ~a (flow_to_item @@ block ~config ~resolve docs) ] let rec documentedSrc ~config ~resolve (t : DocumentedSrc.t) : item Html.elt list = let open DocumentedSrc in let take_code l = Doctree.Take.until l ~classify:(function | Code code -> Accum code | Alternative (Expansion { summary; _ }) -> Accum summary | _ -> Stop_and_keep) in let take_descr l = Doctree.Take.until l ~classify:(function | Documented { attrs; anchor; code; doc; markers } -> Accum [ { DocumentedSrc.attrs; anchor; code = `D code; doc; markers } ] | Nested { attrs; anchor; code; doc; markers } -> Accum [ { DocumentedSrc.attrs; anchor; code = `N code; doc; markers } ] | _ -> Stop_and_keep) in let rec to_html t : item Html.elt list = match t with | [] -> [] | (Code _ | Alternative _) :: _ -> let code, _, rest = take_code t in source (inline ~config ~resolve) ?mode_links:(Config.mode_links config) code @ to_html rest | Subpage subp :: _ -> subpage ~config ~resolve subp | (Documented _ | Nested _) :: _ -> let l, _, rest = take_descr t in let one { DocumentedSrc.attrs; anchor; code; doc; markers } = let content = match code with | `D code -> (inline ~config ~resolve code :> item Html.elt list) | `N n -> to_html n in let doc = match doc with | [] -> [] | doc -> let opening, closing = markers in let delim s = [ Html.span ~a:(class_ [ "comment-delim" ]) [ Html.txt s ] ] in [ Html.div ~a:(class_ [ "def-doc" ]) (delim opening @ block ~config ~resolve doc @ delim closing); ] in let extra_attr, extra_class, link = mk_anchor config anchor in let content = (content :> any Html.elt list) in Html.li ~a:(extra_attr @ class_ (attrs @ extra_class)) (link @ content @ doc) in Html.ol (List.map one l) :: to_html rest in to_html t and subpage ~config ~resolve (subp : Subpage.t) : item Html.elt list = items ~config ~resolve subp.content.items and items ~config ~resolve l : item Html.elt list = let rec walk_items acc (t : Item.t list) : item Html.elt list = let continue_with rest elts = (walk_items [@tailcall]) (List.rev_append elts acc) rest in match t with | [] -> List.rev acc | Text _ :: _ as t -> let text, _, rest = Doctree.Take.until t ~classify:(function | Item.Text text -> Accum text | _ -> Stop_and_keep) in let content = flow_to_item @@ block ~config ~resolve text in (continue_with [@tailcall]) rest content | Heading h :: rest -> (continue_with [@tailcall]) rest [ heading ~config ~resolve h ] | Include { attr; anchor; source_anchor; doc; content = { summary; status; content }; } :: rest -> let doc = spec_doc_div ~config ~resolve doc in let included_html = (items content :> item Html.elt list) in let a_class = if List.length content = 0 then [ "odoc-include"; "shadowed-include" ] else [ "odoc-include" ] in let content : item Html.elt list = let details ~open' = let open' = if open' then [ Html.a_open () ] else [] in let summary = let extra_attr, extra_class, anchor_link = mk_anchor config anchor in let link_to_source = mk_link_to_source ~config ~resolve source_anchor in let a = spec_class (attr @ extra_class) @ extra_attr in Html.summary ~a @@ anchor_link @ link_to_source @ source (inline ~config ~resolve) ?mode_links:(Config.mode_links config) summary in let inner = [ Html.details ~a:open' summary (included_html :> any Html.elt list); ] in [ Html.div ~a:[ Html.a_class a_class ] (doc @ inner) ] in match status with | `Inline -> doc @ included_html | `Closed -> details ~open':false | `Open -> details ~open':true | `Default -> details ~open':(Config.open_details config) in (continue_with [@tailcall]) rest content | Declaration { Item.attr; anchor; source_anchor; content; doc } :: rest -> let extra_attr, extra_class, anchor_link = mk_anchor config anchor in let link_to_source = mk_link_to_source ~config ~resolve source_anchor in let a = spec_class (attr @ extra_class) @ extra_attr in let content = anchor_link @ link_to_source @ documentedSrc ~config ~resolve content in let spec = let doc = spec_doc_div ~config ~resolve doc in [ div ~a:[ Html.a_class [ "odoc-spec" ] ] (div ~a content :: doc) ] in (continue_with [@tailcall]) rest spec and items l = walk_items [] l in items l module Toc = struct open Odoc_document.Doctree open Types let on_sub : Subpage.status -> bool = function | `Closed | `Open | `Default -> false | `Inline -> true let gen_toc ~config ~resolve ~path i = let toc = Toc.compute path ~on_sub i in let rec section { Toc.url; text; children } = let text = inline_nolink text in let title = (text : non_link_phrasing Html.elt list :> Html_types.flow5_without_interactive Html.elt list) in let title_str = List.map (Format.asprintf "%a" (Tyxml.Html.pp_elt ())) text |> String.concat ~sep:"" in let href = Link.href ~config ~resolve url in { title; title_str; href; children = List.map section children } in List.map section toc end module Breadcrumbs = struct open Types let page_parent (page : Url.Path.t) = let page = match page with | { parent = Some parent; name = "index"; kind = `LeafPage } -> parent | _ -> page in match page with | { parent = None; name = "index"; kind = `LeafPage } -> None | { parent = Some parent; _ } -> Some parent | { parent = None; _ } -> Some { Url.Path.parent = None; name = "index"; kind = `LeafPage } let home_breadcrumb ~home_name config ~current_path ~home_path = let href = Some (Link.href ~config ~resolve:(Current current_path) (Odoc_document.Url.from_path home_path)) in { href; name = [ Html.txt home_name ]; kind = `LeafPage } let gen_breadcrumbs_no_sidebar ~config ~url = let url = match url with | { Url.Path.name = "index"; parent = Some parent; kind = `LeafPage } -> parent | _ -> url in match url with | { Url.Path.name = "index"; parent = None; kind = `LeafPage } -> let kind = `LeafPage in let current = { href = None; name = [ Html.txt "" ]; kind } in { parents = []; up_url = None; current } | url -> ( (* This is the pre 3.0 way of computing the breadcrumbs *) let rec get_parent_paths x = match x with | [] -> [] | x :: xs -> ( match Odoc_document.Url.Path.of_list (List.rev (x :: xs)) with | Some x -> x :: get_parent_paths xs | None -> get_parent_paths xs) in let to_breadcrumb path = let href = Some (Link.href ~config ~resolve:(Current url) (Odoc_document.Url.from_path path)) in { href; name = [ Html.txt path.name ]; kind = path.kind } in let parent_paths = get_parent_paths (List.rev (Odoc_document.Url.Path.to_list url)) |> List.rev in match List.rev parent_paths with | [] -> assert false | current :: parents -> let up_url = match page_parent current with | None -> None | Some up -> Some (Link.href ~config ~resolve:(Current url) (Odoc_document.Url.from_path up)) in let current = to_breadcrumb current in let parents = List.map to_breadcrumb parents |> List.rev in let home = home_breadcrumb ~home_name:"Index" config ~current_path:url ~home_path: { Url.Path.name = "index"; parent = None; kind = `LeafPage } in { current; parents = home :: parents; up_url }) let gen_breadcrumbs_with_sidebar ~config ~sidebar ~url:current_url = let find_parent = List.find_opt (function | ({ node = { url = { page; anchor = ""; _ }; _ }; _ } : Odoc_document.Sidebar.entry Tree.t) when Url.Path.is_prefix page current_url -> true | _ -> false) in let rec extract acc (tree : Odoc_document.Sidebar.t) = let parent = match find_parent tree with | Some { node = { url; valid_link; content; _ }; children } -> let href = if valid_link then Some (Link.href ~config ~resolve:(Current current_url) url) else None in let name = inline_nolink content in let breadcrumb = { href; name; kind = url.page.kind } in if url.page = current_url then Some (`Current breadcrumb) else Some (`Parent (breadcrumb, children)) | _ -> None in match parent with | Some (`Parent (bc, children)) -> extract (bc :: acc) children | Some (`Current current) -> let up_url = List.find_map (fun (b : Types.breadcrumb) -> b.href) acc in { Types.current; parents = List.rev acc; up_url } | None -> let kind = current_url.kind and name = current_url.name in let current = { href = None; name = [ Html.txt name ]; kind } in let up_url = List.find_map (fun (b : Types.breadcrumb) -> b.href) acc in let parents = List.rev acc in { Types.current; parents; up_url } in let escape = match (Config.home_breadcrumb config, find_parent sidebar) with | Some home_name, Some { node; _ } -> ( match page_parent node.url.page with | None -> [] | Some parent -> [ home_breadcrumb ~home_name config ~current_path:current_url ~home_path:parent; ]) | _ -> [] in extract escape sidebar let gen_breadcrumbs ~config ~sidebar ~url = match sidebar with | None -> gen_breadcrumbs_no_sidebar ~config ~url | Some sidebar -> gen_breadcrumbs_with_sidebar ~config ~sidebar ~url end module Page = struct let on_sub = function | `Page _ -> None | `Include x -> ( match x.Include.status with | `Closed | `Open | `Default -> None | `Inline -> Some 0) let rec include_ ~config ~sidebar { Subpage.content; _ } = page ~config ~sidebar content and subpages ~config ~sidebar subpages = List.map (include_ ~config ~sidebar) subpages and page ~config ~sidebar p : Odoc_document.Renderer.page = let { Page.preamble = _; items = i; url; source_anchor; resources; assets } = Doctree.Labels.disambiguate_page ~enter_subpages:false p in let subpages = subpages ~config ~sidebar @@ Doctree.Subpages.compute p in let resolve = Link.Current url in let breadcrumbs = Breadcrumbs.gen_breadcrumbs ~config ~sidebar ~url in let sidebar_html = match sidebar with | None -> None | Some sidebar -> let sidebar = Odoc_document.Sidebar.to_block sidebar url in (Some (block ~config ~resolve sidebar) :> any Html.elt list option) in let i = Doctree.Shift.compute ~on_sub i in let uses_katex = Doctree.Math.has_math_elements p in let toc = Toc.gen_toc ~config ~resolve ~path:url i in let content = (items ~config ~resolve i :> any Html.elt list) in let header, preamble = Doctree.PageTitle.render_title ?source_anchor p in let header = items ~config ~resolve header in let preamble = items ~config ~resolve preamble in let source_anchor = match source_anchor with | Some url -> Some (Link.href ~config ~resolve url) | None -> None in let shell_name = match Config.shell config with | Some name -> name | None -> if Config.as_json config then "json" else "default" in let (module Shell : Html_shell.S) = match Html_shell.find shell_name with | Some shell -> shell | None -> match Config.shell config with | Some name -> failwith (Printf.sprintf "Shell '%s' not found. Available shells: %s. \ Ensure the shell plugin is installed (run 'dune build \ @install' first)." name (Stdlib.String.concat ", " (Html_shell.list_shells ()))) | None -> Html_shell.default () in Shell.make ~config { url; header; preamble; content; breadcrumbs; toc; sidebar = sidebar_html; sidebar_data = sidebar; uses_katex; source_anchor; resources; assets; children = subpages } and source_page ~config ~sidebar sp = let { Source_page.url; contents } = sp in let resolve = Link.Current sp.url in let breadcrumbs = Breadcrumbs.gen_breadcrumbs ~config ~sidebar ~url in let sidebar_html = match sidebar with | None -> None | Some sidebar -> let sidebar = Odoc_document.Sidebar.to_block sidebar url in (Some (block ~config ~resolve sidebar) :> any Html.elt list option) in let title = url.Url.Path.name and doc = Html_source.html_of_doc ~config ~resolve contents in let header = items ~config ~resolve (Doctree.PageTitle.render_src_title sp) in let shell_name = match Config.shell config with | Some name -> name | None -> if Config.as_json config then "json" else "default" in let (module Shell : Html_shell.S) = match Html_shell.find shell_name with | Some shell -> shell | None -> match Config.shell config with | Some name -> failwith (Printf.sprintf "Shell '%s' not found. Available shells: %s. \ Ensure the shell plugin is installed (run 'dune build \ @install' first)." name (Stdlib.String.concat ", " (Html_shell.list_shells ()))) | None -> Html_shell.default () in Shell.make_src ~config { url; header; breadcrumbs; sidebar = sidebar_html; sidebar_data = sidebar; title; content = [ doc ] } end let render ~config ~sidebar = function | Document.Page page -> [ Page.page ~config ~sidebar page ] | Source_page src -> [ Page.source_page ~config ~sidebar src ] let filepath ~config url = Link.Path.as_filename ~config url let doc ~config ~xref_base_uri b = let resolve = Link.Base xref_base_uri in block ~config ~resolve b let inline ~config ~xref_base_uri b = let resolve = Link.Base xref_base_uri in inline ~config ~resolve b