feat: Style blog posts

This commit is contained in:
Robert Prehn 2020-09-23 15:21:43 -05:00
parent 1e8d7d5301
commit c7a0bf1b3d
11 changed files with 110 additions and 209 deletions

View file

@ -1,101 +0,0 @@
defmodule Content.ShortcodeParser do
use Neotomex.ExGrammar
@root true
define :document, "(comment / shortcode / notcode)*" do
tail ->
tail |> Enum.join
end
define :notcode, "not_open_bracket+" do
content ->
content |> Enum.join
end
define :comment, "<open_bracket> <open_bracket> ([^\\]]+ close_bracket?)+ <close_bracket>" do
[inner] ->
inner
|> Enum.map(fn [chars, nil] -> "#{chars |> Enum.join}]" end)
|> Enum.join
|> (&("[#{&1}")).()
end
define :shortcode, "<open_bracket> <spaces?> name <spaces?> attribute* <close_bracket> <!'('> (notcode? <open_bracket> <'/'> <spaces?> name <spaces?> <close_bracket>)?" do
[name, attributes, nil] ->
Content.Shortcodes.dispatch(name, attributes)
[name, attributes, [content, closing_name]] when closing_name == name ->
Content.Shortcodes.dispatch(name, attributes, content || "")
end
define :attribute, "name <spaces?>"
define :name, "(namechar+)", do: (chars -> Enum.join(chars))
define :namechar, "[A-Za-z0-9] / dash / underscore"
define :dash, "<'-'>"
define :underscore, "<'_'>"
define :open_bracket, "<'['>", do: ["["]
define :not_open_bracket, "[^\\[]"
define :close_bracket, "<']'>", do: ["]"]
define :close_comment, "close_bracket close_bracket"
define :spaces, "[\s\\r\\n]*"
@_neotomex_definitions Map.put(@_neotomex_definitions,
:not_open_bracket,
{{:terminal, ~r/^[^[]/u}, nil})
end
defmodule Content.Shortcodes do
@moduledoc """
For handling wordpress style shortcodes in strings.
"""
def expand_shortcodes(frag) do
{:ok, tree} = Floki.parse_fragment(frag)
case tree do
[text] when is_binary(text) ->
{:ok, result} = processed_text(text) |> Floki.parse_fragment
result
_ ->
tree
|> Floki.traverse_and_update(fn
tag ->
tag |> transform_text_nodes
end)
end
|> Floki.raw_html(encode: false)
end
defp transform_text_nodes({tag_name, attrs, children}) do
new_children =
children
|> Enum.map(fn
text when is_binary(text) ->
{:ok, [result]} = processed_text(text) |> Floki.parse_fragment
result
other -> other
end)
{tag_name, attrs, new_children}
end
defp transform_text_nodes(comment = {:comment, _}) do
comment
end
defp processed_text(text) do
text =
text
|> String.replace("\r", "")
case Content.ShortcodeParser.parse(text) do
{:ok, result, remainder} ->
[result, remainder] |> Enum.join
{:ok, result} ->
result
end
end
def dispatch(tag, _attrs), do: String.upcase(String.reverse(tag))
def dispatch(_tag, _attrs, content) do
String.upcase(String.reverse(content))
end
end

View file

@ -42,8 +42,7 @@ defmodule Content do
def process_content(text) do
text
|> Content.Shortcodes.expand_shortcodes()
|> Earmark.as_html!()
|> Earmark.as_html!(encode: false)
end
# Include shared imports and aliases for views

View file

@ -1,6 +1,6 @@
<%= for post <- @posts do %>
<article class="max-w-lg mx-auto <%= post_class(post) %> h-entry py-12">
<h1 class="text-4xl">
<article class="max-w-xl mx-auto <%= post_class(post) %> h-entry py-12">
<h1 class="text-4xl font-bold">
<%= link to: Routes.posts_path(@conn, :show, post), class: "u-url" do %>
<%= raw post.title %>
<% end %>
@ -9,7 +9,7 @@
<div class="Article-content <%= if post.format, do: post.format.slug %> e-content">
<%= render "thumb.html", post: post, thumbs: @thumbs %>
<div class="Article-content-words">
<%= raw post |> Content.Post.content_page(1) |> Content.Post.before_more |> process_content |> raw %>
<%= raw post |> Content.Post.content_page(1) |> Content.Post.before_more |> process_content |> () |> raw %>
<%= if post.content =~ "<!--more-->" do %>
<p>
<%= link "Keep Reading", to: Routes.posts_path(@conn, :show, post) %>
@ -34,7 +34,7 @@
</article>
<% end %>
<nav class="max-w-lg mx-auto flex justify-center">
<nav class="max-w-xl mx-auto flex justify-center">
<div class="flex shadow rounded">
<%= paginator(1..@last_page, @page, fn first..last, page ->
link page,

View file

@ -1,11 +1,13 @@
<%= floating_form "Leave My Comment", @comment_changeset do %>
<%= form_for @comment_changeset, Routes.comment_path(@conn, :create), fn f -> %>
<%= hidden_input f, :parent %>
<%= hidden_input f, :post_id %>
<%= styled_input f, :author, label: "Your Name" %>
<%= styled_input f, :author_email, label: "Your Email" %>
<%= styled_input f, :author_url, label: "Your Website" %>
<%= styled_input f, :content, label: "Comment", input_helper: :textarea %>
<%= styled_button "Leave My Comment" %>
<div class="mt-12">
<%= floating_form "Leave My Comment", @comment_changeset do %>
<%= form_for @comment_changeset, Routes.comment_path(@conn, :create), fn f -> %>
<%= hidden_input f, :parent %>
<%= hidden_input f, :post_id %>
<%= styled_input f, :author, label: "Your Name" %>
<%= styled_input f, :author_email, label: "Your Email" %>
<%= styled_input f, :author_url, label: "Your Website" %>
<%= styled_input f, :content, label: "Comment", input_helper: :textarea %>
<%= styled_button "Leave My Comment" %>
<% end %>
<% end %>
<% end %>
</div>

View file

@ -1,13 +1,13 @@
<article class="max-w-lg mx-auto <%= post_class(@post) %> h-entry py-12">
<article class="max-w-xl mx-auto <%= post_class(@post) %> h-entry py-12">
<div>
<h1 class="text-4xl">
<h1 class="text-4xl font-bold">
<%= link to: Routes.posts_path(@conn, :show, @post), class: "u-url" do %>
<%= raw @post.title %>
<% end %>
</h1>
<%= post_topmatter(@conn, @post) %>
</div>
<div class="<%= if @post.format, do: @post.format.slug %> e-content py-12">
<div class="<%= if @post.format, do: @post.format.slug %> e-content py-12 Article-content">
<%= render "thumb.html", post: @post, thumbs: @thumbs %>
<%= @post |> Content.Post.content_page(@page) |> process_content |> raw %>
<%= case @post.categories || [] do %>
@ -24,17 +24,21 @@
<% end %>
</div>
<%= render "pagination.html", conn: @conn, post: @post, current_page: @page %>
<%= if @post.comment_status == "open" do %>
<h3 class="text-3xl">Comments</h3>
<%= render "comments.html", post: @post, parent_id: 0, conn: @conn %>
<%=
render "reply_form.html",
comment_changeset: comment_changeset_for_post(@post),
post: @post,
conn: @conn
%>
<% end %>
</article>
<%= if @post.comment_status == "open" do %>
<div class="w-full bg-gray-800 py-12">
<div class="max-w-xl mx-auto">
<h3 class="text-3xl text-white">Comments</h3>
<%= render "comments.html", post: @post, parent_id: 0, conn: @conn %>
<%=
render "reply_form.html",
comment_changeset: comment_changeset_for_post(@post),
post: @post,
conn: @conn
%>
</div>
</div>
<% end %>
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.2/highlight.min.js"></script>
<script charset="UTF-8"

View file

@ -42,7 +42,7 @@ defmodule Content.MixProject do
{:auth, in_umbrella: true},
{:auth_web, in_umbrella: true},
{:core, in_umbrella: true},
{:earmark, "~> 1.4.2" },
{:earmark, "1.4.3"},
{:excoveralls, "~> 0.10", only: [:dev, :test]},
{:floki, "~> 0.25.0"},
{:gettext, "~> 0.11"},

View file

@ -1,75 +0,0 @@
defmodule Content.ShortcodesTest do
use ExUnit.Case
import Content.Shortcodes
describe "shortcodes" do
test "no shortcodes, no problem" do
assert expand_shortcodes("test") == "test"
end
test "double bracket escapes work" do
assert expand_shortcodes("[[test]]") == "[test]"
end
test "escapes enclosing shortcodes" do
assert expand_shortcodes("[[test] abc [/test]]") == "[test] abc [/test]"
end
test "escapes shortcodes with args" do
assert expand_shortcodes("[[test with-args][/test]]") == "[test with-args][/test]"
end
test "expands shortcodes" do
assert expand_shortcodes("[test]") == "TSET"
end
test "expands shortcodes in the middle" do
assert expand_shortcodes("this is a [test] of the shortcode system") == "this is a TSET of the shortcode system"
end
test "expands shortcodes at the end" do
assert expand_shortcodes("this is a [test]") == "this is a TSET"
end
test "expands shortcodes at the beginning" do
assert expand_shortcodes("[test] it up") == "TSET it up"
end
test "handles shortcodes with args" do
assert expand_shortcodes("[test with-args]") == "TSET"
end
test "handles enclosing shortcodes" do
assert expand_shortcodes("[test]Content[/test]") == "TNETNOC"
end
test "handles enclosing shortcodes with args" do
assert expand_shortcodes("[test with-args]Content[/test]") == "TNETNOC"
end
test "handles enclosing shortcodes with no content" do
assert expand_shortcodes("[test with-args][/test]") == ""
end
test "handles strings with carriage returns" do
assert expand_shortcodes(" | \r\n ") == " | \n "
end
test "handles strings with high unicode characters" do
assert expand_shortcodes("") == ""
end
test "handles shortcodes within tags" do
assert expand_shortcodes("<p>[test]<em>chacha</em></p>") == "<p>TSET<em>chacha</em></p>"
end
test "handles mangled shortcodes gracefully" do
assert expand_shortcodes("[[unclosed shortcode") == "[[unclosed shortcode"
end
test "handles comments in html" do
assert expand_shortcodes("<!-- comment -->") == "<!-- comment -->"
end
end
end

View file

@ -6,6 +6,7 @@
@import "@fortawesome/fontawesome-free/css/all.css";
@import "blog";
@import "code";
input[type="checkbox"]::after {

View file

@ -0,0 +1,71 @@
.Article-content a {
@apply text-blue-700 underline;
}
.Article-content h1 a {
@apply text-black no-underline;
}
.Article-content p {
@apply mb-6;
}
.Article-content h1 {
@apply text-4xl font-bold mt-6 mb-3;
}
.Article-content h2 {
@apply text-2xl font-bold mt-6 mb-3;
}
.Article-content h3 {
@apply text-xl font-semibold mt-3 mb-3;
}
.Article-content h4 {
@apply text-lg font-semibold mt-3;
}
.Article-content h5 {
@apply uppercase mt-3;
}
.Article-content h6 {
@apply uppercase font-light mt-3;
}
.Article-content blockquote {
@apply border-l-2 pl-4 italic mb-6;
}
.Article-content blockquote cite {
@apply not-italic font-bold mt-6 block;
}
.Article-content table {
@apply min-w-full leading-normal;
}
.Article-content th {
@apply px-5 py-3 border-b-2 border-gray-200 bg-gray-100 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider;
}
.Article-content td {
@apply px-5 py-5 border-b border-gray-200 bg-white text-sm;
}
.Article-content dt {
@apply uppercase font-light mt-3;
}
.Article-content ul {
@apply list-disc;
}
.Article-content ol {
@apply list-decimal;
}
.Article-content li {
@apply ml-6 pl-2;
}

View file

@ -156,6 +156,7 @@ defmodule CoreWeb.Helpers do
end
end
@spec floating_form(any, atom | %{action: any}, [{:do, any}, ...]) :: {:safe, [...]}
def floating_form(title, changeset, do: content) do
~E"""
<h1 class="relative text-white text-xl font-semibold text-center pb-6"><%= title %></h1>

View file

@ -13,8 +13,7 @@
"crontab": {:hex, :crontab, "1.1.10", "dc9bb1f4299138d47bce38341f5dcbee0aa6c205e864fba7bc847f3b5cb48241", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "1347d889d1a0eda997990876b4894359e34bfbbd688acbb0ba28a2795ca40685"},
"db_connection": {:hex, :db_connection, "2.2.2", "3bbca41b199e1598245b716248964926303b5d4609ff065125ce98bcd368939e", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "642af240d8a8affb93b4ba5a6fcd2bbcbdc327e1a524b825d383711536f8070c"},
"decimal": {:hex, :decimal, "1.9.0", "83e8daf59631d632b171faabafb4a9f4242c514b0a06ba3df493951c08f64d07", [:mix], [], "hexpm", "b1f2343568eed6928f3e751cf2dffde95bfaa19dd95d09e8a9ea92ccfd6f7d85"},
"earmark": {:hex, :earmark, "1.4.10", "bddce5e8ea37712a5bfb01541be8ba57d3b171d3fa4f80a0be9bcf1db417bcaf", [:mix], [{:earmark_parser, ">= 1.4.10", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "12dbfa80810478e521d3ffb941ad9fbfcbbd7debe94e1341b4c4a1b2411c1c27"},
"earmark_parser": {:hex, :earmark_parser, "1.4.10", "6603d7a603b9c18d3d20db69921527f82ef09990885ed7525003c7fe7dc86c56", [:mix], [], "hexpm", "8e2d5370b732385db2c9b22215c3f59c84ac7dda7ed7e544d7c459496ae519c0"},
"earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm", "8cf8a291ebf1c7b9539e3cddb19e9cef066c2441b1640f13c34c1d3cfc825fec"},
"ecto": {:hex, :ecto, "3.4.6", "08f7afad3257d6eb8613309af31037e16c36808dfda5a3cd0cb4e9738db030e4", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6f13a9e2a62e75c2dcfc7207bfc65645ab387af8360db4c89fee8b5a4bf3f70b"},
"ecto_sql": {:hex, :ecto_sql, "3.4.4", "d28bac2d420f708993baed522054870086fd45016a9d09bb2cd521b9c48d32ea", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.0", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "edb49af715dd72f213b66adfd0f668a43c17ed510b5d9ac7528569b23af57fe8"},
"elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm", "d522695b93b7f0b4c0fcb2dfe73a6b905b1c301226a5a55cb42e5b14d509e050"},
@ -42,7 +41,7 @@
"mock": {:hex, :mock, "0.3.5", "feb81f52b8dcf0a0d65001d2fec459f6b6a8c22562d94a965862f6cc066b5431", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "6fae404799408300f863550392635d8f7e3da6b71abdd5c393faf41b131c8728"},
"neotomex": {:hex, :neotomex, "0.1.7", "64f76513653aa87ea7abdde0fd600e56955d838020a13d88f2bf334c88ac3e7a", [:mix], [], "hexpm", "4b87b8f614d1cd89dc8ba80ba0e559bedb3ebf6f6d74cd774fcfdd215e861445"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
"phoenix": {:hex, :phoenix, "1.5.4", "0fca9ce7e960f9498d6315e41fcd0c80bfa6fbeb5fa3255b830c67fdfb7e703f", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4e516d131fde87b568abd62e1b14aa07ba7d5edfd230bab4e25cc9dedbb39135"},
"phoenix": {:hex, :phoenix, "1.5.5", "9a5a197edc1828c5f138a8ef10524dfecc43e36ab435c14578b1e9b4bd98858c", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b10eaf86ad026eafad2ee3dd336f0fb1c95a3711789855d913244e270bde463b"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.1.0", "a044d0756d0464c5a541b4a0bf4bcaf89bffcaf92468862408290682c73ae50d", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "c5e666a341ff104d0399d8f0e4ff094559b2fde13a5985d4cb5023b2c2ac558b"},
"phoenix_html": {:hex, :phoenix_html, "2.14.2", "b8a3899a72050f3f48a36430da507dd99caf0ac2d06c77529b1646964f3d563e", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "58061c8dfd25da5df1ea0ca47c972f161beb6c875cd293917045b92ffe1bf617"},
"phoenix_html_sanitizer": {:hex, :phoenix_html_sanitizer, "1.0.2", "e2c8cfbc83660e362753de127cc957bec3442a8aecdf271fb65a684a906fccf5", [:mix], [{:html_sanitize_ex, "~> 1.0.0", [hex: :html_sanitize_ex, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}], "hexpm", "47aebb08fa954b7ad95f295fb701df9800ee3a489212119c9c6074a65e1e5a10"},