defmodule AshHqWeb.Pages.Docs do use Surface.LiveComponent alias Phoenix.LiveView.JS alias AshHqWeb.Components.{CalloutText, DocSidebar, RightNav, Tag} alias AshHqWeb.Routes require Logger prop(params, :map, required: true) prop(change_versions, :event, required: true) prop(selected_versions, :map, required: true) prop(libraries, :list, default: []) prop(uri, :string) prop(sidebar_state, :map, required: true) prop(collapse_sidebar, :event, required: true) prop(expand_sidebar, :event, required: true) data(library, :any) data(extension, :any) data(docs, :any) data(library_version, :any) data(guide, :any) data(doc_path, :list, default: []) data(dsls, :list, default: []) data(dsl, :any) data(options, :list, default: []) data(module, :any) @spec render(any) :: Phoenix.LiveView.Rendered.t() def render(assigns) do ~F"""
{#if @doc_path && @doc_path != []}
{#case @doc_path} {#match [item]}
{item}
{#match path} {#for item <- :lists.droplast(path)} {item} {/for} {List.last(path)} {/case}
{/if}
""" end defp render_source_code_link(assigns, module_or_function, library, library_version) do ~F""" {#if module_or_function.file} {""} {/if} """ end defp modules_in_scope(nil, _, _, _), do: [] defp modules_in_scope(_, nil, _, _), do: [] defp modules_in_scope(dsl, %{dsls: dsls}, libraries, selected_versions) do dsl_path = dsl.path ++ [dsl.name] dsls |> Enum.filter(fn potential_parent -> List.starts_with?(dsl_path, potential_parent.path ++ [potential_parent.name]) end) |> Enum.flat_map(fn dsl -> dsl.imports || [] end) |> Enum.flat_map(fn mod_name -> case Enum.find_value(libraries, fn library -> Enum.find_value(library.versions, fn version -> if version.id == selected_versions[library.id] do Enum.find(version.modules, &(&1.name == mod_name)) end end) end) do nil -> Logger.warn("No such module found called #{inspect(mod_name)}") [] module -> [module] end end) end defp child_dsls(_, nil), do: [] defp child_dsls(nil, _), do: [] defp child_dsls(%{dsls: dsls}, dsl) do dsl_path = dsl.path ++ [dsl.name] dsl_path_count = Enum.count(dsl_path) Enum.filter(dsls, fn potential_child -> potential_child_path = potential_child.path ++ [potential_child.name] List.starts_with?(potential_child_path, dsl_path) && Enum.count(potential_child_path) - dsl_path_count == 1 end) end defp positional_options(options) do options |> Enum.filter(& &1.argument_index) |> Enum.sort_by(& &1.argument_index) end defp render_functions(assigns, functions, type, header) do ~F""" {#case Enum.filter(functions, &(&1.type == type))} {#match []} {#match functions}

{header}

{#for function <- functions}

{#for head <- function.heads} {head} {/for} {raw(render_replacements(assigns, AshHq.Docs.Extensions.RenderMarkdown.render!(function, :doc)))}

{/for} {/case} """ end defp source_link(%AshHq.Docs.Module{file: file}, library, library_version) do "https://github.com/ash-project/#{library.name}/tree/#{library_version.version}/#{file}" end defp source_link(%AshHq.Docs.Function{file: file, line: line}, library, library_version) do if line do "https://github.com/ash-project/#{library.name}/tree/#{library_version.version}/#{file}#L#{line}" else "https://github.com/ash-project/#{library.name}/tree/#{library_version.version}/#{file}" end end def path_to_name(path, name) do Enum.map_join(path ++ [name], "-", &Routes.sanitize_name/1) end defp render_tags(assigns, option) do ~F""" {#if option.required} Required {/if} """ end def show_sidebar() do %JS{} |> JS.toggle( to: "#mobile-sidebar-container", in: { "transition ease-in duration-100", "opacity-0", "opacity-100" }, out: { "transition ease-out duration-75", "opacity-100", "opacity-0" } ) end def update(assigns, socket) do {:ok, socket |> assign(assigns) |> assign_library() |> assign_extension() |> assign_guide() |> assign_module() |> assign_dsl() |> assign_docs()} end defp assign_guide(socket) do guide = cond do socket.assigns[:params]["guide"] && socket.assigns.library_version -> Enum.find(socket.assigns.library_version.guides, fn guide -> guide.route == Enum.join(socket.assigns[:params]["guide"], "/") end) socket.assigns.library_version && socket.assigns.library_version.default_guide && !socket.assigns[:params]["dsl_path"] && !socket.assigns[:params]["module"] && !socket.assigns[:params]["extension"] -> Enum.find(socket.assigns.library_version.guides, fn guide -> guide.name == socket.assigns.library_version.default_guide end) true -> nil end assign(socket, :guide, guide) end defp assign_dsl(socket) do case socket.assigns[:params]["dsl_path"] do nil -> assign(socket, :dsl, nil) path -> dsl = Enum.find( socket.assigns.extension.dsls, fn dsl -> Enum.map(dsl.path, &Routes.sanitize_name/1) ++ [Routes.sanitize_name(dsl.name)] == path end ) new_state = Map.put(socket.assigns.sidebar_state, dsl.id, "open") unless socket.assigns.sidebar_state[dsl.id] == "open" do send(self(), {:new_sidebar_state, new_state}) end socket |> assign( :dsl, dsl ) end end defp assign_module(socket) do if socket.assigns.library && socket.assigns.library_version && socket.assigns[:params]["module"] do module = Enum.find( socket.assigns.library_version.modules, &(Routes.sanitize_name(&1.name) == socket.assigns[:params]["module"]) ) assign(socket, module: module ) else assign(socket, :module, nil) end end defp assign_docs(socket) do cond do socket.assigns.module -> assign(socket, docs: AshHq.Docs.Extensions.RenderMarkdown.render!(socket.assigns.module, :doc), doc_path: [socket.assigns.library.name, socket.assigns.module.name], options: [] ) socket.assigns.dsl -> assign(socket, docs: AshHq.Docs.Extensions.RenderMarkdown.render!(socket.assigns.dsl, :doc), doc_path: [ socket.assigns.library.name, socket.assigns.extension.name ] ++ socket.assigns.dsl.path ++ [socket.assigns.dsl.name], options: Enum.filter( socket.assigns.extension.options, &(&1.path == socket.assigns.dsl.path ++ [socket.assigns.dsl.name]) ) ) socket.assigns.extension -> assign(socket, docs: AshHq.Docs.Extensions.RenderMarkdown.render!(socket.assigns.extension, :doc), doc_path: [socket.assigns.library.name, socket.assigns.extension.name], options: [] ) socket.assigns.guide -> assign(socket, docs: AshHq.Docs.Extensions.RenderMarkdown.render!(socket.assigns.guide, :text), doc_path: [socket.assigns.library.name, socket.assigns.guide.name], options: [] ) true -> assign(socket, docs: "", doc_path: [], dsls: []) end end defp render_replacements(assigns, docs) do docs |> render_links(assigns) |> render_mix_deps(assigns) end defp render_mix_deps(docs, assigns) do String.replace(docs, ~r/^(?!\<\/code\>){{mix_dep:.*}}/, fn text -> try do "{{mix_dep:" <> library = String.trim_trailing(text, "}}") render_mix_dep(assigns, library, text) rescue e -> Logger.error("Invalid link #{inspect(e)}") text end end) end defp render_mix_dep(assigns, library, source) do library = Enum.find(assigns[:libraries], &(&1.name == library)) || raise "No such library in link: #{source}" selected_versions = assigns[:selected_versions] version = if selected_versions[library.id] == "latest" do Enum.find(library.versions, &String.contains?(&1.version, ".")) || AshHqWeb.Helpers.latest_version(library) else case Enum.find(library.versions, &(&1.id == selected_versions[library.id])) do nil -> nil version -> version end end if String.contains?(version, ".") do case String.split(version, ".") do [major, minor, "0"] -> ~s({:#{library.name}, "~> #{major}.#{minor}"}) _ -> ~s({:#{library.name}, "~> #{version}"}) end else ~s({:#{library.name}, github: "ash-project/#{library.name}", branch: "#{version}"}) end end defp render_links(docs, assigns) do String.replace(docs, ~r/(?!){{link:.*}}(?!<\/code>)/, fn text -> try do "{{link:" <> rest = String.trim_trailing(text, "}}") [library, type, item] = String.split(rest, ":") render_link(assigns, library, type, item, text) rescue e -> Logger.error("Invalid link #{inspect(e)}") text end end) end defp render_link(assigns, library, type, item, source) do library = Enum.find(assigns[:libraries], &(&1.name == library)) || raise "No such library in link: #{source}" selected_versions = assigns[:selected_versions] version = if selected_versions[library.id] in ["latest", nil, ""] do Enum.find(library.versions, &String.contains?(&1.version, ".")) || AshHqWeb.Helpers.latest_version(library) else case Enum.find(library.versions, &(&1.id == selected_versions[library.id])) do nil -> nil version -> version end end if is_nil(version) do raise "no version for library" else case type do "guide" -> guide = Enum.find(version.guides, &(&1.name == item)) || raise "No such guide in link: #{source}" """ #{item} """ "dsl" -> name = item |> String.split("/") |> Enum.join(".") """ #{name} """ "module" -> """ #{item} """ "extension" -> """ #{item} """ type -> raise "unimplemented link type #{inspect(type)} in #{source}" end end end defp assign_extension(socket) do if socket.assigns.library_version && socket.assigns[:params]["extension"] do extensions = socket.assigns.library_version.extensions assign(socket, extension: Enum.find(extensions, fn extension -> Routes.sanitize_name(extension.name) == socket.assigns[:params]["extension"] end) ) else assign(socket, :extension, nil) end end def mount(socket) do {:ok, socket} end defp assign_library(socket) do if !socket.assigns[:library] || socket.assigns.params["library"] != Routes.sanitize_name(socket.assigns.library.name) do case Enum.find( socket.assigns.libraries, &(Routes.sanitize_name(&1.name) == socket.assigns.params["library"]) ) do nil -> assign(socket, library: nil, library_version: nil) library -> socket = if socket.assigns[:params]["version"] do library_version = case socket.assigns[:params]["version"] do "latest" -> AshHqWeb.Helpers.latest_version(library) version -> Enum.find( library.versions, &(Routes.sanitize_name(&1.version) == version) ) end if library_version do new_selected_versions = Map.put(socket.assigns.selected_versions, library.id, library_version.id) assign( socket, selected_versions: new_selected_versions, library_version: library_version ) |> push_event("selected-versions", new_selected_versions) else assign(socket, :library_version, nil) end else assign(socket, :library_version, nil) end assign(socket, :library, library) end else socket end end end