ash_hq/lib/ash_hq_web/pages/docs.ex

494 lines
15 KiB
Elixir
Raw Normal View History

2022-03-28 10:26:35 +13:00
defmodule AshHqWeb.Pages.Docs do
use Surface.LiveComponent
2022-03-29 11:05:19 +13:00
alias Phoenix.LiveView.JS
2022-04-01 05:36:44 +13:00
alias AshHqWeb.Components.{CalloutText, DocSidebar, RightNav, Tag}
2022-03-30 05:12:28 +13:00
alias AshHqWeb.Routes
2022-06-06 06:03:45 +12:00
require Logger
2022-03-28 10:26:35 +13:00
2022-06-06 06:03:45 +12:00
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
2022-03-28 10:26:35 +13:00
@spec render(any) :: Phoenix.LiveView.Rendered.t()
def render(assigns) do
~F"""
2022-04-01 09:59:53 +13:00
<div class="grid content-start overflow-hidden">
2022-04-05 18:20:36 +12:00
<div class="xl:hidden flex flex-row justify-start space-x-12 items-center border-b border-t border-gray-600 py-3">
2022-03-29 11:05:19 +13:00
<button class="dark:hover:text-gray-600" phx-click={show_sidebar()}>
<Heroicons.Outline.MenuIcon class="w-8 h-8 ml-4" />
</button>
{#if @doc_path && @doc_path != []}
<div class="flex flex-row space-x-1 items-center">
{#case @doc_path}
{#match [item]}
<div class="dark:text-white">
{item}
</div>
{#match path}
{#for item <- :lists.droplast(path)}
2022-03-30 05:12:28 +13:00
<span class="text-gray-400">
{item}
</span>
2022-03-29 11:05:19 +13:00
<Heroicons.Outline.ChevronRightIcon class="w-3 h-3" />
{/for}
2022-03-30 05:12:28 +13:00
<span class="dark:text-white">
2022-03-30 17:40:17 +13:00
<CalloutText>{List.last(path)}</CalloutText>
2022-03-30 05:12:28 +13:00
</span>
2022-03-29 11:05:19 +13:00
{/case}
</div>
{/if}
</div>
2022-04-08 18:59:39 +12:00
<span class="grid overflow-hidden xl:hidden">
2022-04-02 08:11:17 +13:00
<div
id="mobile-sidebar-container"
2022-04-02 11:49:26 +13:00
class="overflow-hidden hidden fixed w-min h-full transition bg-white dark:bg-primary-black"
2022-04-02 08:11:17 +13:00
>
2022-03-30 05:12:28 +13:00
<DocSidebar
id="mobile-sidebar"
libraries={@libraries}
extension={@extension}
2022-04-01 19:43:09 +13:00
sidebar_state={@sidebar_state}
collapse_sidebar={@collapse_sidebar}
expand_sidebar={@expand_sidebar}
2022-03-30 17:40:17 +13:00
module={@module}
2022-03-30 05:12:28 +13:00
guide={@guide}
library={@library}
library_version={@library_version}
selected_versions={@selected_versions}
2022-03-30 17:40:17 +13:00
dsl={@dsl}
2022-03-30 05:12:28 +13:00
/>
</div>
</span>
2022-04-02 11:49:26 +13:00
<div class="grow overflow-hidden flex flex-row h-full justify-center space-x-12 bg-white dark:bg-primary-black">
2022-03-29 11:05:19 +13:00
<DocSidebar
2022-03-30 05:12:28 +13:00
id="sidebar"
2022-04-02 11:49:26 +13:00
class="hidden xl:block mt-10"
2022-03-30 17:40:17 +13:00
module={@module}
2022-03-29 11:05:19 +13:00
libraries={@libraries}
extension={@extension}
2022-04-01 19:43:09 +13:00
sidebar_state={@sidebar_state}
collapse_sidebar={@collapse_sidebar}
expand_sidebar={@expand_sidebar}
2022-03-29 11:05:19 +13:00
guide={@guide}
library={@library}
library_version={@library_version}
selected_versions={@selected_versions}
2022-03-30 17:40:17 +13:00
dsl={@dsl}
2022-03-29 11:05:19 +13:00
/>
2022-03-30 05:12:28 +13:00
<div
id="docs-window"
2022-04-02 11:49:26 +13:00
class="w-full prose dark:bg-primary-black md:max-w-1xl lg:max-w-2xl xl:max-w-3xl dark:prose-invert overflow-y-scroll overflow-x-visible mt-14"
2022-03-30 05:12:28 +13:00
phx-hook="Docs"
>
2022-04-02 11:49:26 +13:00
<div id="module-docs" class="w-full nav-anchor text-black dark:text-white">
2022-06-08 08:37:34 +12:00
{raw(render_replacements(assigns, @docs))}
2022-04-01 09:59:53 +13:00
</div>
2022-03-30 17:40:17 +13:00
{#if @module}
2022-04-01 14:29:58 +13:00
<h1>Callbacks</h1>
{render_functions(assigns, @module.functions, :callback)}
<h1>Functions</h1>
{render_functions(assigns, @module.functions, :function)}
<h1>Macros</h1>
{render_functions(assigns, @module.functions, :macro)}
2022-03-30 17:40:17 +13:00
{/if}
{#if !Enum.empty?(@options)}
<div class="ml-2">
<table>
{#for option <- @options}
2022-03-30 18:07:17 +13:00
<tr id={Routes.sanitize_name(option.name)}>
2022-03-30 17:40:17 +13:00
<td>
<div class="flex flex-row items-baseline">
2022-03-30 18:07:17 +13:00
<a href={"##{Routes.sanitize_name(option.name)}"}>
2022-03-30 17:40:17 +13:00
<Heroicons.Outline.LinkIcon class="h-3 m-3" />
</a>
<CalloutText>{option.name}</CalloutText>
</div>
</td>
<td>
{option.type}
</td>
<td>
{render_tags(assigns, option)}
</td>
<td>
{raw(AshHq.Docs.Extensions.RenderMarkdown.render!(option, :doc))}
</td>
</tr>
{/for}
</table>
</div>
2022-03-30 05:12:28 +13:00
{/if}
2022-03-28 10:26:35 +13:00
</div>
2022-04-01 05:36:44 +13:00
{#if @module}
2022-04-02 11:49:26 +13:00
<div class="w-min overflow-y-scroll overflow-x-visible mt-14 dark:bg-primary-black bg-opacity-70">
2022-04-02 08:11:17 +13:00
<RightNav functions={@module.functions} module={@module.name} />
2022-04-01 05:36:44 +13:00
</div>
{/if}
2022-03-28 10:26:35 +13:00
</div>
2022-03-30 05:12:28 +13:00
</div>
"""
end
2022-04-01 14:29:58 +13:00
defp render_functions(assigns, functions, type) do
~F"""
{#for function <- Enum.filter(functions, &(&1.type == type))}
<div class="rounded-lg bg-slate-700 bg-opacity-50 px-2 mb-2 pb-1">
<p class="">
<div class="">
<div class="flex flex-row items-baseline h-min">
<a href={"##{type}-#{Routes.sanitize_name(function.name)}-#{function.arity}"}>
<Heroicons.Outline.LinkIcon class="h-3 m-3" />
</a>
<h2 class="nav-anchor" id={"#{type}-#{Routes.sanitize_name(function.name)}-#{function.arity}"}>{function.name}/{function.arity}</h2>
</div>
</div>
{#for head <- function.heads}
<code class="makeup elixir">{head}</code>
{/for}
{raw(AshHq.Docs.Extensions.RenderMarkdown.render!(function, :doc))}
</p>
</div>
{/for}
"""
end
2022-03-30 05:12:28 +13:00
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}
<Tag color={:red}>
Required
</Tag>
{/if}
"""
end
2022-03-29 11:05:19 +13:00
def show_sidebar() do
%JS{}
|> JS.toggle(
to: "#mobile-sidebar-container",
2022-03-30 05:12:28 +13:00
in: {
"transition ease-in duration-100",
"opacity-0",
"opacity-100"
},
out: {
"transition ease-out duration-75",
"opacity-100",
"opacity-0"
}
2022-03-29 11:05:19 +13:00
)
2022-03-28 10:26:35 +13:00
end
def update(assigns, socket) do
2022-03-29 08:47:43 +13:00
{:ok,
2022-03-30 17:40:17 +13:00
socket
|> assign(assigns)
2022-03-29 08:47:43 +13:00
|> assign_library()
|> assign_extension()
|> assign_guide()
2022-03-30 17:40:17 +13:00
|> assign_module()
|> assign_dsl()
2022-03-29 08:47:43 +13:00
|> assign_docs()}
end
defp assign_guide(socket) do
guide =
if socket.assigns[:params]["guide"] && socket.assigns.library_version do
Enum.find(socket.assigns.library_version.guides, fn guide ->
2022-04-05 18:20:36 +12:00
guide.route == Enum.join(socket.assigns[:params]["guide"], "/")
2022-03-29 08:47:43 +13:00
end)
end
assign(socket, :guide, guide)
end
2022-03-30 17:40:17 +13:00
defp assign_dsl(socket) do
2022-03-30 18:07:17 +13:00
case socket.assigns[:params]["dsl_path"] do
2022-03-30 17:40:17 +13:00
nil ->
assign(socket, :dsl, nil)
2022-03-30 18:07:17 +13:00
path ->
2022-03-30 17:40:17 +13:00
dsl =
Enum.find(
socket.assigns.extension.dsls,
fn dsl ->
Enum.map(dsl.path, &Routes.sanitize_name/1) ++ [Routes.sanitize_name(dsl.name)] ==
path
end
)
2022-04-01 19:43:09 +13:00
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(
2022-03-30 17:40:17 +13:00
:dsl,
dsl
)
end
end
defp assign_module(socket) do
if socket.assigns.library && socket.assigns.library_version &&
socket.assigns[:params]["module"] do
2022-03-30 18:07:17 +13:00
module =
Enum.find(
socket.assigns.library_version.modules,
&(Routes.sanitize_name(&1.name) == socket.assigns[:params]["module"])
)
2022-03-30 17:40:17 +13:00
assign(socket,
2022-03-30 18:07:17 +13:00
module: module
2022-03-30 17:40:17 +13:00
)
else
assign(socket, :module, nil)
end
end
2022-03-29 08:47:43 +13:00
defp assign_docs(socket) do
cond do
2022-03-30 17:40:17 +13:00
socket.assigns.module ->
assign(socket,
docs: AshHq.Docs.Extensions.RenderMarkdown.render!(socket.assigns.module, :doc),
2022-04-01 05:36:44 +13:00
doc_path: [socket.assigns.library.name, socket.assigns.module.name],
2022-03-30 17:40:17 +13:00
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])
)
)
2022-03-29 08:47:43 +13:00
socket.assigns.extension ->
2022-03-29 11:05:19 +13:00
assign(socket,
2022-03-30 05:12:28 +13:00
docs: AshHq.Docs.Extensions.RenderMarkdown.render!(socket.assigns.extension, :doc),
doc_path: [socket.assigns.library.name, socket.assigns.extension.name],
2022-03-30 17:40:17 +13:00
options: []
2022-03-29 11:05:19 +13:00
)
2022-03-29 08:47:43 +13:00
socket.assigns.guide ->
2022-03-29 11:05:19 +13:00
assign(socket,
2022-03-30 05:12:28 +13:00
docs: AshHq.Docs.Extensions.RenderMarkdown.render!(socket.assigns.guide, :text),
doc_path: [socket.assigns.library.name, socket.assigns.guide.name],
options: []
2022-03-29 11:05:19 +13:00
)
2022-03-29 08:47:43 +13:00
socket.assigns.library_version ->
2022-03-29 11:05:19 +13:00
assign(socket,
2022-03-30 05:12:28 +13:00
docs:
AshHq.Docs.Extensions.RenderMarkdown.render!(socket.assigns.library_version, :doc),
doc_path: [socket.assigns.library.name],
options: []
2022-03-29 11:05:19 +13:00
)
2022-03-29 08:47:43 +13:00
true ->
2022-03-30 05:12:28 +13:00
assign(socket, docs: "", doc_path: [], dsls: [])
2022-03-29 08:47:43 +13:00
end
2022-03-28 10:26:35 +13:00
end
2022-06-08 08:37:34 +12:00
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, ".")) ||
Enum.at(library.versions, 0)
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/^(?!\<\/code\>){{link:.*}}/, fn text ->
2022-06-06 06:03:45 +12:00
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] == "latest" do
2022-06-08 08:37:34 +12:00
Enum.find(library.versions, &String.contains?(&1.version, ".")) ||
Enum.at(library.versions, 0)
2022-06-06 06:03:45 +12:00
else
case Enum.find(library.versions, &(&1.id == selected_versions[library.id])) do
nil ->
nil
version ->
version
end
end
if type == "guide" do
guide =
Enum.find(version.guides, &(&1.name == item)) || raise "No such guide in link: #{source}"
"""
<a href="#{Routes.doc_link(guide, assigns[:selected_versions])}">#{item}</a>
"""
else
raise "unimplemented link type #{inspect(type)} in #{source}"
end
end
2022-03-28 10:26:35 +13:00
defp assign_extension(socket) do
2022-04-08 18:59:39 +12:00
if socket.assigns.library_version && socket.assigns[:params]["extension"] do
extensions = socket.assigns.library_version.extensions
2022-03-29 08:47:43 +13:00
assign(socket,
extension:
Enum.find(extensions, fn extension ->
2022-03-30 05:12:28 +13:00
Routes.sanitize_name(extension.name) == socket.assigns[:params]["extension"]
2022-03-29 08:47:43 +13:00
end)
)
else
assign(socket, :extension, nil)
end
2022-03-28 10:26:35 +13:00
end
def mount(socket) do
{:ok, socket}
end
defp assign_library(socket) do
if !socket.assigns[:library] ||
2022-03-30 05:12:28 +13:00
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
2022-03-29 08:47:43 +13:00
nil ->
assign(socket, library: nil, library_version: nil)
library ->
2022-03-28 10:26:35 +13:00
socket =
if socket.assigns[:params]["version"] do
library_version =
2022-04-08 18:59:39 +12:00
case socket.assigns[:params]["version"] do
"latest" ->
Enum.find(library.versions, &String.contains?(&1.version, ".")) ||
Enum.at(library.versions, 0)
version ->
Enum.find(
library.versions,
&(Routes.sanitize_name(&1.version) == version)
)
end
2022-03-28 10:26:35 +13:00
if library_version do
new_selected_versions =
Map.put(socket.assigns.selected_versions, library.id, library_version.id)
assign(
socket,
2022-03-29 08:47:43 +13:00
selected_versions: new_selected_versions,
library_version: library_version
2022-03-28 10:26:35 +13:00
)
|> push_event("selected-versions", new_selected_versions)
else
2022-03-29 08:47:43 +13:00
assign(socket, :library_version, nil)
2022-03-28 10:26:35 +13:00
end
else
2022-03-29 08:47:43 +13:00
assign(socket, :library_version, nil)
2022-03-28 10:26:35 +13:00
end
assign(socket, :library, library)
end
else
socket
end
end
end