mirror of
https://github.com/ash-project/ash_hq.git
synced 2024-09-19 12:53:49 +12:00
improvement: tag improvements/blog improvements
This commit is contained in:
parent
959734b88e
commit
95a6fae77a
8 changed files with 307 additions and 59 deletions
|
@ -29,9 +29,10 @@ defmodule AshHq.Blog.Post do
|
|||
constraints max_length: 250
|
||||
end
|
||||
|
||||
attribute :tag_names, {:array, :string} do
|
||||
attribute :tag_names, {:array, :ci_string} do
|
||||
constraints items: [
|
||||
match: ~r/^[a-zA-Z]*$/
|
||||
match: ~r/^[a-zA-Z]*$/,
|
||||
casing: :lower
|
||||
]
|
||||
end
|
||||
|
||||
|
@ -69,14 +70,14 @@ defmodule AshHq.Blog.Post do
|
|||
defaults [:create, :read, :update]
|
||||
|
||||
read :published do
|
||||
argument :tag, :string
|
||||
argument :tag, :ci_string
|
||||
|
||||
filter expr(
|
||||
state == :published and
|
||||
if is_nil(^arg(:tag)) do
|
||||
true
|
||||
else
|
||||
^arg(:tag) in tag_names
|
||||
^arg(:tag) in type(tag_names, ^{:array, :ci_string})
|
||||
end
|
||||
)
|
||||
end
|
||||
|
@ -114,7 +115,7 @@ defmodule AshHq.Blog.Post do
|
|||
destroy_notifications =
|
||||
AshHq.Blog.Tag.read!()
|
||||
|> Enum.flat_map(fn tag ->
|
||||
if tag.name in all_post_tags do
|
||||
if to_string(tag.name) in all_post_tags do
|
||||
[]
|
||||
else
|
||||
AshHq.Blog.Tag.destroy!(tag, return_notifications?: true)
|
||||
|
|
|
@ -11,9 +11,10 @@ defmodule AshHq.Blog.Tag do
|
|||
end
|
||||
|
||||
attributes do
|
||||
attribute :name, :string do
|
||||
attribute :name, :ci_string do
|
||||
allow_nil? false
|
||||
primary_key? true
|
||||
constraints casing: :lower
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -38,8 +38,6 @@ defmodule AshHq.Docs.Extensions.RenderMarkdown.Changes.RenderMarkdown do
|
|||
#{inspect(html_doc)}
|
||||
""")
|
||||
|
||||
html_doc = AshHq.Docs.Extensions.RenderMarkdown.Highlighter.highlight(html_doc)
|
||||
|
||||
html_doc =
|
||||
case attribute.type do
|
||||
{:array, _} ->
|
||||
|
@ -52,8 +50,6 @@ defmodule AshHq.Docs.Extensions.RenderMarkdown.Changes.RenderMarkdown do
|
|||
Ash.Changeset.force_change_attribute(changeset, opts[:destination], html_doc)
|
||||
|
||||
{:ok, html_doc, _} ->
|
||||
html_doc = AshHq.Docs.Extensions.RenderMarkdown.Highlighter.highlight(html_doc)
|
||||
|
||||
html_doc =
|
||||
case attribute.type do
|
||||
{:array, _} ->
|
||||
|
|
|
@ -12,18 +12,53 @@ defmodule AshHq.Docs.Extensions.RenderMarkdown.Highlighter do
|
|||
|
||||
def highlight(html) do
|
||||
html
|
||||
|> replace_regex(
|
||||
~r/<pre><code(?:\s+class="(\w*)")?>(.*)<\/code><\/pre>/,
|
||||
&highlight_code_block/3
|
||||
)
|
||||
|> replace_regex(~r/<code class="inline">(.*)<\/code>/, &maybe_highlight_module/2)
|
||||
|> Floki.parse_document!()
|
||||
|> Floki.traverse_and_update(fn
|
||||
{"pre", _, [{"code", attrs, [body]}]} when is_binary(body) ->
|
||||
lexer =
|
||||
find_value_class(attrs, fn class ->
|
||||
case Makeup.Registry.fetch_lexer_by_name(class) do
|
||||
{:ok, {lexer, opts}} -> {class, lexer, opts}
|
||||
:error -> nil
|
||||
end
|
||||
end)
|
||||
|
||||
case lexer do
|
||||
{lang, lexer, opts} ->
|
||||
{:keep, render_code(lang, lexer, opts, body)}
|
||||
|
||||
nil ->
|
||||
if find_value_class(attrs, &(&1 == "inline")) do
|
||||
{:keep, maybe_highlight_module(body)}
|
||||
else
|
||||
{:keep,
|
||||
~s(<pre class="code-pre"><code class="text-black dark:text-white">#{body}</code></pre>)}
|
||||
end
|
||||
end
|
||||
|
||||
other ->
|
||||
other
|
||||
end)
|
||||
|> AshHq.Docs.Extensions.RenderMarkdown.RawHTML.raw_html(pretty: true)
|
||||
end
|
||||
|
||||
defp find_value_class(attrs, func) do
|
||||
Enum.find_value(attrs, fn
|
||||
{"class", classes} ->
|
||||
classes
|
||||
|> String.split(" ")
|
||||
|> Enum.find_value(func)
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end)
|
||||
end
|
||||
|
||||
defp replace_regex(string, regex, replacement) do
|
||||
Regex.replace(regex, string, replacement)
|
||||
end
|
||||
|
||||
defp maybe_highlight_module(_full_block, code) do
|
||||
defp maybe_highlight_module(code) do
|
||||
code_without_c =
|
||||
case code do
|
||||
"c:" <> rest ->
|
||||
|
@ -54,7 +89,7 @@ defmodule AshHq.Docs.Extensions.RenderMarkdown.Highlighter do
|
|||
defp try_parse_multi([{text, code} | rest]) do
|
||||
case Code.string_to_quoted(code) do
|
||||
{:ok, {fun, _, []}} when is_atom(fun) ->
|
||||
~s[<code #{text} class="inline maybe-local-call" data-fun="#{fun}">#{code}</code>]
|
||||
~s[<code #{text} class="inline maybe-local-call text-black dark:text-white" data-fun="#{fun}">#{code}</code>]
|
||||
|
||||
{:ok,
|
||||
{:/, _,
|
||||
|
@ -63,17 +98,17 @@ defmodule AshHq.Docs.Extensions.RenderMarkdown.Highlighter do
|
|||
arity
|
||||
]}}
|
||||
when is_atom(fun) and is_integer(arity) ->
|
||||
~s[<code #{text} class="inline maybe-call" data-module="#{Enum.join(parts, ".")}" data-fun="#{fun}" data-arity="#{arity}">#{code}</code>]
|
||||
~s[<code #{text} class="inline maybe-call text-black dark:text-white" data-module="#{Enum.join(parts, ".")}" data-fun="#{fun}" data-arity="#{arity}">#{code}</code>]
|
||||
|
||||
{:ok, {:/, _, [{fun, _, nil}, arity]}} when is_atom(fun) and is_integer(arity) ->
|
||||
~s[<code #{text} class="inline maybe-local-call" data-fun="#{fun}" data-arity="#{arity}">#{code}</code>]
|
||||
~s[<code #{text} class="inline maybe-local-call text-black dark:text-white" data-fun="#{fun}" data-arity="#{arity}">#{code}</code>]
|
||||
|
||||
{:ok, {:__aliases__, _, parts}} ->
|
||||
~s[<code #{text} class="inline maybe-module" data-module="#{Enum.join(parts, ".")}">#{code}</code>]
|
||||
~s[<code #{text} class="inline maybe-module text-black dark:text-white" data-module="#{Enum.join(parts, ".")}">#{code}</code>]
|
||||
|
||||
_ ->
|
||||
if rest == [] do
|
||||
~s[<code class="inline">#{code}</code>]
|
||||
~s[<code class="inline text-black dark:text-white">#{code}</code>]
|
||||
else
|
||||
try_parse_multi(rest)
|
||||
end
|
||||
|
@ -99,38 +134,34 @@ defmodule AshHq.Docs.Extensions.RenderMarkdown.Highlighter do
|
|||
end
|
||||
|
||||
defp render_code(lang, lexer, lexer_opts, code) do
|
||||
if lexer do
|
||||
highlighted =
|
||||
code
|
||||
|> unescape_html()
|
||||
|> IO.iodata_to_binary()
|
||||
|> String.replace(~r/{{mix_dep:.*}}/, fn value ->
|
||||
try do
|
||||
"{{mix_dep:" <> dep = String.trim_trailing(value, "}}")
|
||||
"______#{dep}______"
|
||||
rescue
|
||||
_ ->
|
||||
value
|
||||
end
|
||||
end)
|
||||
|> Makeup.highlight_inner_html(
|
||||
lexer: lexer,
|
||||
lexer_options: lexer_opts,
|
||||
formatter_options: [highlight_tag: "span"]
|
||||
)
|
||||
|> String.replace(~r/______.*______/, fn dep ->
|
||||
value =
|
||||
dep
|
||||
|> String.trim_leading("_")
|
||||
|> String.trim_trailing("_")
|
||||
highlighted =
|
||||
code
|
||||
# |> unescape_html()
|
||||
|> IO.iodata_to_binary()
|
||||
|> String.replace(~r/{{mix_dep:.*}}/, fn value ->
|
||||
try do
|
||||
"{{mix_dep:" <> dep = String.trim_trailing(value, "}}")
|
||||
"______#{dep}______"
|
||||
rescue
|
||||
_ ->
|
||||
value
|
||||
end
|
||||
end)
|
||||
|> Makeup.highlight_inner_html(
|
||||
lexer: lexer,
|
||||
lexer_options: lexer_opts,
|
||||
formatter_options: [highlight_tag: "span"]
|
||||
)
|
||||
|> String.replace(~r/______.*______/, fn dep ->
|
||||
value =
|
||||
dep
|
||||
|> String.trim_leading("_")
|
||||
|> String.trim_trailing("_")
|
||||
|
||||
"{{mix_dep:#{value}}}"
|
||||
end)
|
||||
"{{mix_dep:#{value}}}"
|
||||
end)
|
||||
|
||||
~s(<pre class="code-pre"><code class="makeup #{lang} highlight">#{highlighted}</code></pre>)
|
||||
else
|
||||
~s(<pre class="code-pre"><code class="makeup #{lang} text-black dark:text-white">#{code}</code></pre>)
|
||||
end
|
||||
~s(<pre class="code-pre"><code class="makeup #{lang} highlight">#{highlighted}</code></pre>)
|
||||
end
|
||||
|
||||
entities = [{"&", ?&}, {"<", ?<}, {">", ?>}, {""", ?"}, {"'", ?'}]
|
||||
|
|
221
lib/ash_hq/docs/extensions/render_markdown/raw_html.ex
Normal file
221
lib/ash_hq/docs/extensions/render_markdown/raw_html.ex
Normal file
|
@ -0,0 +1,221 @@
|
|||
defmodule AshHq.Docs.Extensions.RenderMarkdown.RawHTML do
|
||||
# copied from floki to preserve formatting in `pre`
|
||||
@moduledoc false
|
||||
|
||||
@self_closing_tags [
|
||||
"area",
|
||||
"base",
|
||||
"br",
|
||||
"col",
|
||||
"command",
|
||||
"embed",
|
||||
"hr",
|
||||
"img",
|
||||
"input",
|
||||
"keygen",
|
||||
"link",
|
||||
"meta",
|
||||
"param",
|
||||
"source",
|
||||
"track",
|
||||
"wbr"
|
||||
]
|
||||
|
||||
@encoder &HtmlEntities.encode/1
|
||||
|
||||
def raw_html(html_tree, options) do
|
||||
encoder =
|
||||
case Keyword.fetch(options, :encode) do
|
||||
{:ok, true} -> @encoder
|
||||
{:ok, false} -> & &1
|
||||
:error -> default_encoder()
|
||||
end
|
||||
|
||||
padding =
|
||||
case Keyword.fetch(options, :pretty) do
|
||||
{:ok, true} -> %{pad: " ", line_ending: "\n", depth: 0}
|
||||
_ -> :noop
|
||||
end
|
||||
|
||||
IO.iodata_to_binary(build_raw_html(html_tree, [], encoder, padding))
|
||||
end
|
||||
|
||||
defp build_raw_html([], html, _encoder, _padding), do: html
|
||||
|
||||
defp build_raw_html(string, _html, encoder, padding) when is_binary(string) do
|
||||
leftpad_content(padding, encoder.(string))
|
||||
end
|
||||
|
||||
defp build_raw_html(tuple, html, encoder, padding) when is_tuple(tuple),
|
||||
do: build_raw_html([tuple], html, encoder, padding)
|
||||
|
||||
defp build_raw_html([string | tail], html, encoder, padding) when is_binary(string) do
|
||||
build_raw_html(tail, [html, leftpad_content(padding, encoder.(string))], encoder, padding)
|
||||
end
|
||||
|
||||
defp build_raw_html([{:comment, comment} | tail], html, encoder, padding),
|
||||
do: build_raw_html(tail, [html, leftpad(padding), "<!--", comment, "-->"], encoder, padding)
|
||||
|
||||
defp build_raw_html([{:keep, text_html} | tail], html, encoder, padding),
|
||||
do: build_raw_html(tail, [html, text_html], encoder, padding)
|
||||
|
||||
defp build_raw_html([{:pi, tag, attrs} | tail], html, encoder, padding) do
|
||||
build_raw_html(
|
||||
tail,
|
||||
[html, leftpad(padding), "<?", tag, " ", tag_attrs(attrs), "?>"],
|
||||
encoder,
|
||||
padding
|
||||
)
|
||||
end
|
||||
|
||||
defp build_raw_html([{:doctype, type, public, system} | tail], html, encoder, padding) do
|
||||
attr =
|
||||
case {public, system} do
|
||||
{"", ""} -> []
|
||||
{"", system} -> [" SYSTEM \"", system | "\""]
|
||||
{public, system} -> [" PUBLIC \"", public, "\" \"", system | "\""]
|
||||
end
|
||||
|
||||
build_raw_html(
|
||||
tail,
|
||||
[html, leftpad(padding), "<!DOCTYPE ", type, attr | ">"],
|
||||
encoder,
|
||||
padding
|
||||
)
|
||||
end
|
||||
|
||||
defp build_raw_html([{type, attrs, children} | tail], html, encoder, padding) do
|
||||
build_raw_html(
|
||||
tail,
|
||||
[html | tag_for(type, attrs, children, encoder, padding)],
|
||||
encoder,
|
||||
padding
|
||||
)
|
||||
end
|
||||
|
||||
defp tag_attrs(attr_list) do
|
||||
map_intersperse(attr_list, ?\s, &build_attrs/1)
|
||||
end
|
||||
|
||||
defp tag_with_attrs(type, [], children, padding),
|
||||
do: [leftpad(padding), "<", type | close_open_tag(type, children)]
|
||||
|
||||
defp tag_with_attrs(type, attrs, children, padding),
|
||||
do: [leftpad(padding), "<", type, ?\s, tag_attrs(attrs) | close_open_tag(type, children)]
|
||||
|
||||
defp close_open_tag(type, []) when type in @self_closing_tags, do: "/>"
|
||||
defp close_open_tag(_type, _), do: ">"
|
||||
|
||||
defp close_end_tag(type, [], _padding) when type in @self_closing_tags, do: ""
|
||||
|
||||
defp close_end_tag(type, _, padding),
|
||||
do: [leftpad(padding), "</", type, ">", line_ending(padding)]
|
||||
|
||||
defp build_attrs({attr, value}), do: [attr, "=\"", html_escape(value) | "\""]
|
||||
defp build_attrs(attr), do: attr
|
||||
|
||||
defp tag_for(type, attrs, children, encoder, padding) do
|
||||
encoder =
|
||||
case type do
|
||||
"script" -> & &1
|
||||
"style" -> & &1
|
||||
_ -> encoder
|
||||
end
|
||||
|
||||
[
|
||||
tag_with_attrs(type, attrs, children, padding),
|
||||
line_ending(padding),
|
||||
build_raw_html(children, "", encoder, pad_increase(padding)),
|
||||
close_end_tag(type, children, padding)
|
||||
]
|
||||
end
|
||||
|
||||
defp default_encoder do
|
||||
if Application.get_env(:floki, :encode_raw_html, true) do
|
||||
@encoder
|
||||
else
|
||||
& &1
|
||||
end
|
||||
end
|
||||
|
||||
# html_escape
|
||||
# Optimized IO data implementation from Plug.HTML
|
||||
|
||||
defp html_escape(data) when is_binary(data), do: html_escape(data, 0, data, [])
|
||||
defp html_escape(data), do: html_escape(IO.iodata_to_binary(data))
|
||||
|
||||
escapes = [
|
||||
{?<, "<"},
|
||||
{?>, ">"},
|
||||
{?&, "&"},
|
||||
{?", """},
|
||||
{?', "'"}
|
||||
]
|
||||
|
||||
for {match, insert} <- escapes do
|
||||
defp html_escape(<<unquote(match), rest::bits>>, skip, original, acc) do
|
||||
html_escape(rest, skip + 1, original, [acc | unquote(insert)])
|
||||
end
|
||||
end
|
||||
|
||||
defp html_escape(<<_char, rest::bits>>, skip, original, acc) do
|
||||
html_escape(rest, skip, original, acc, 1)
|
||||
end
|
||||
|
||||
defp html_escape(<<>>, _skip, _original, acc) do
|
||||
acc
|
||||
end
|
||||
|
||||
for {match, insert} <- escapes do
|
||||
defp html_escape(<<unquote(match), rest::bits>>, skip, original, acc, len) do
|
||||
part = binary_part(original, skip, len)
|
||||
html_escape(rest, skip + len + 1, original, [acc, part | unquote(insert)])
|
||||
end
|
||||
end
|
||||
|
||||
defp html_escape(<<_char, rest::bits>>, skip, original, acc, len) do
|
||||
html_escape(rest, skip, original, acc, len + 1)
|
||||
end
|
||||
|
||||
defp html_escape(<<>>, 0, original, _acc, _len) do
|
||||
original
|
||||
end
|
||||
|
||||
defp html_escape(<<>>, skip, original, acc, len) do
|
||||
[acc | binary_part(original, skip, len)]
|
||||
end
|
||||
|
||||
# helpers
|
||||
|
||||
# TODO: Use Enum.map_intersperse/3 when we require Elixir v1.10+
|
||||
|
||||
defp map_intersperse([], _, _),
|
||||
do: []
|
||||
|
||||
defp map_intersperse([last], _, mapper),
|
||||
do: [mapper.(last)]
|
||||
|
||||
defp map_intersperse([head | rest], separator, mapper),
|
||||
do: [mapper.(head), separator | map_intersperse(rest, separator, mapper)]
|
||||
|
||||
defp leftpad(:noop), do: ""
|
||||
defp leftpad(%{pad: pad, depth: depth}), do: String.duplicate(pad, depth)
|
||||
|
||||
defp leftpad_content(:noop, string), do: string
|
||||
|
||||
defp leftpad_content(padding, string) do
|
||||
trimmed = String.trim(string)
|
||||
|
||||
if trimmed == "" do
|
||||
""
|
||||
else
|
||||
[leftpad(padding), trimmed, line_ending(padding)]
|
||||
end
|
||||
end
|
||||
|
||||
defp pad_increase(:noop), do: :noop
|
||||
defp pad_increase(padder = %{depth: depth}), do: %{padder | depth: depth + 1}
|
||||
|
||||
defp line_ending(:noop), do: ""
|
||||
defp line_ending(%{line_ending: line_ending}), do: line_ending
|
||||
end
|
|
@ -85,7 +85,7 @@ defmodule AshHqWeb.Pages.Blog do
|
|||
<h3 class="text-lg font-bold mb-1">All Tags:</h3>
|
||||
<div class="flex gap-2 flex-wrap w-full">
|
||||
{#for tag <- @tags}
|
||||
<Tag tag={tag.name} />
|
||||
<Tag tag={to_string(tag.name)} />
|
||||
{/for}
|
||||
</div>
|
||||
</div>
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,7 +1,5 @@
|
|||
name
|
||||
name
|
||||
ash
|
||||
release
|
||||
elixir
|
||||
Ash
|
||||
Community
|
||||
Elixir
|
||||
community
|
||||
|
|
|
Loading…
Reference in a new issue