improvement: tag improvements/blog improvements

This commit is contained in:
Zach Daniel 2022-12-26 14:22:41 -05:00
parent 959734b88e
commit 95a6fae77a
8 changed files with 307 additions and 59 deletions

View file

@ -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)

View file

@ -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

View file

@ -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, _} ->

View file

@ -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 = [{"&amp;", ?&}, {"&lt;", ?<}, {"&gt;", ?>}, {"&quot;", ?"}, {"&#39;", ?'}]

View 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 = [
{?<, "&lt;"},
{?>, "&gt;"},
{?&, "&amp;"},
{?", "&quot;"},
{?', "&#39;"}
]
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

View file

@ -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

View file

@ -1,7 +1,5 @@
name
name
ash
release
elixir
Ash
Community
Elixir
community

1 name
2 ash
3 release
4 elixir
5 Ash community
Community
Elixir