fixes, styling improvement including inlined code

This commit is contained in:
Adam Piontek 2021-04-06 14:45:54 -04:00
parent 97f9c4a9f2
commit 816fdbb484
5 changed files with 121 additions and 27 deletions

View file

@ -63,7 +63,7 @@ defmodule Home73k.Blog.Post do
# """ parse_body/1 # """ parse_body/1
# Convert body markdown to html, and highlight code fence blocks # Convert body markdown to html, and highlight code fence blocks
defp parse_body({fm, md}) do defp parse_body({fm, md}) do
html = Earmark.as_html!(md) |> Highlighter.highlight_code_blocks() html = Earmark.as_html!(md) |> Highlighter.highlight_all()
Map.put(fm, :body, html) Map.put(fm, :body, html)
end end

View file

@ -8,7 +8,14 @@ defmodule Home73k.Highlighter do
@chroma_bin Home73k.app_chroma_bin() |> Path.expand() @chroma_bin Home73k.app_chroma_bin() |> Path.expand()
@doc """ @doc """
Highlights all code block in an already generated HTML document. Highlights all code in HTML (fenced code blocks and inlined code)
"""
def highlight_all(html) do
highlight_code_blocks(html) |> highlight_code_inlined()
end
@doc """
Highlights fenced code blocks in HTML document
""" """
def highlight_code_blocks(html) do def highlight_code_blocks(html) do
~r/<pre><code(?:\s+class="(\w*)")?>([^<]*)<\/code><\/pre>/ ~r/<pre><code(?:\s+class="(\w*)")?>([^<]*)<\/code><\/pre>/
@ -16,6 +23,31 @@ defmodule Home73k.Highlighter do
end end
defp highlight_code_block(_full_block, lang, code) do defp highlight_code_block(_full_block, lang, code) do
# perform the code highlighting
highlighted = highlight_code(lang, code)
# return properly wrapped highlighted code
~s(<pre class="chroma"><code class="language-#{lang}">#{highlighted}</code></pre>)
end
@doc """
Highlights inlined code in HTML document
"""
def highlight_code_inlined(html) do
~r/<code(?:\s+class="inline lang-(\w*)")?>([^<]*)<\/code>/
|> Regex.replace(html, &highlight_code_inline(&1, &2, &3))
end
defp highlight_code_inline(_full_block, lang, code) do
# perform the code highlighting
highlighted = highlight_code(lang, code)
# return properly wrapped highlighted code
~s(<code class="inline chroma language-#{lang}">#{highlighted}</code>)
end
# """
# Performs code highlighting using chroma
# """
defp highlight_code(lang, code) do
# unescape the code # unescape the code
unescaped_code = unescape_html(code) |> IO.iodata_to_binary() unescaped_code = unescape_html(code) |> IO.iodata_to_binary()
@ -25,12 +57,12 @@ defmodule Home73k.Highlighter do
# use chroma to highlight the code via temp file # use chroma to highlight the code via temp file
bin_args = ["-l", lang, "-f", "html", "--html-only", "--html-prevent-surrounding-pre", tmp_file] bin_args = ["-l", lang, "-f", "html", "--html-only", "--html-prevent-surrounding-pre", tmp_file]
{highlighted, _} = System.cmd(@chroma_bin, bin_args) System.cmd(@chroma_bin, bin_args) |> elem(0)
# return properly wrapped highlighted code
~s(<pre class="chroma"><code class="language-#{lang}">#{highlighted}</code></pre>)
end end
# """
# Code below for unescaping html, since it's escaped by Earmark markdown parsing
# """
entities = [{"&amp;", ?&}, {"&lt;", ?<}, {"&gt;", ?>}, {"&quot;", ?"}, {"&#39;", ?'}] entities = [{"&amp;", ?&}, {"&lt;", ?<}, {"&gt;", ?>}, {"&quot;", ?"}, {"&#39;", ?'}]
for {encoded, decoded} <- entities do for {encoded, decoded} <- entities do

View file

@ -60,7 +60,7 @@ defmodule Home73kWeb.BlogLive do
defp init_per_live_action(:show, socket, %{"id" => id}) do defp init_per_live_action(:show, socket, %{"id" => id}) do
post = Blog.get_post_by_id!(id) post = Blog.get_post_by_id!(id)
socket socket
|> assign(:page_title, "Blog \\ post.title") |> assign(:page_title, "Blog \\ #{post.title}")
|> assign(:posts, [post]) |> assign(:posts, [post])
|> assign(:page_count, nil) |> assign(:page_count, nil)
|> assign_prev_next(0) |> assign_prev_next(0)

View file

@ -35,7 +35,7 @@ I also whitelisted several relevant domains from both the bottom of the Big Bloc
We found my partner couldn't use a website related to her profession. It would just get stuck loading and it wasn't clear why. With Chrome (can do this in Firefox, too) I was able to use F12 to open the DevTools, click Network, reload the page, and see in red what was failing to load. We found my partner couldn't use a website related to her profession. It would just get stuck loading and it wasn't clear why. With Chrome (can do this in Firefox, too) I was able to use F12 to open the DevTools, click Network, reload the page, and see in red what was failing to load.
In this case, the Pi-hole was blocking "cdns.gigya.com," a content delivery service hosting a javascript the page wanted to use. Off to the Pi-hole web interface (or cli with `pihole -w`) to quickly whitelist, and the site worked again. In this case, the Pi-hole was blocking "cdns.gigya.com," a content delivery service hosting a javascript the page wanted to use. Off to the Pi-hole web interface (or cli with ``pihole -w``{:.lang-shell}) to quickly whitelist, and the site worked again.
The same for "prod.imgur.map.fastlylb.net," apparently needed for imgur to work. The same for "prod.imgur.map.fastlylb.net," apparently needed for imgur to work.

View file

@ -18,7 +18,9 @@ I set to incorporating my blog posts and previous static webpack-based design in
For one, as much as I love Elxiir, the only server-side syntax-highlighting package out there doesn't support enough languages for my needs. I didn't like the idea of using client-side javascript, so I decided to leverage [Chroma's cli](https://github.com/alecthomas/chroma#command-line-interface). Hooking elixir's System.cmd/3 in with a trimmed & modified version of [the ExDoc syntax highlighter code](https://github.com/elixir-lang/ex_doc/blob/d5cde30f55c7e0cde486ec3878067aee82ccc924/lib/ex_doc/highlighter.ex), after parsing the markdown to html, I'm able to isolate all fenced code in a post, write each code fragment to a temp file, apply highlight classes via chroma, and replace the code in the post. For one, as much as I love Elxiir, the only server-side syntax-highlighting package out there doesn't support enough languages for my needs. I didn't like the idea of using client-side javascript, so I decided to leverage [Chroma's cli](https://github.com/alecthomas/chroma#command-line-interface). Hooking elixir's System.cmd/3 in with a trimmed & modified version of [the ExDoc syntax highlighter code](https://github.com/elixir-lang/ex_doc/blob/d5cde30f55c7e0cde486ec3878067aee82ccc924/lib/ex_doc/highlighter.ex), after parsing the markdown to html, I'm able to isolate all fenced code in a post, write each code fragment to a temp file, apply highlight classes via chroma, and replace the code in the post.
*(As an aside, I at first tried using [Pygments' `pygmentize` cli](https://pygments.org/docs/cmdline/), which worked fine, but I realized it didn't seem to support vue templates, and I do some work with vue. At some point it might be fun to try to figure out how to implement the lexers I need in the elixir native project [makeup](https://github.com/elixir-makeup/makeup) but that's a big task to tackle some other day!)* *(As an aside, I at first tried using the [Pygments cli](https://pygments.org/docs/cmdline/), which worked fine, but I realized it didn't seem to support vue templates, and I do some work with vue. At some point it might be fun to try to figure out how to implement the lexers I need in the elixir native project [makeup](https://github.com/elixir-makeup/makeup) but that's a big task to tackle some other day!)*
Incidentally, [Earmark](https://github.com/pragdave/earmark)'s markdown parsing includes support for setting the language for inline code with a braced suffix --- e.g., `` `console.log('test');`{:.lang-javascript} ``{:.lang-markdown} --- so my highlighter code below includes support for such code elements.
I also found code hot-reloading wasn't working great if I added or removed a file, even if I canceled all elixir processes and started it up again. I had to get this fixed because the plan was to deploy new content and other changes via git repository post-receive hook, and while I can script the recompilation, and probably add in a `--force` flag or something, I wanted it cleaner. Plus I just wanted the convenience of writing posts in dev mode with a preview. I also found code hot-reloading wasn't working great if I added or removed a file, even if I canceled all elixir processes and started it up again. I had to get this fixed because the plan was to deploy new content and other changes via git repository post-receive hook, and while I can script the recompilation, and probably add in a `--force` flag or something, I wanted it cleaner. Plus I just wanted the convenience of writing posts in dev mode with a preview.
@ -35,15 +37,60 @@ end
Following that change, new markdown files are recognized and included as expected. Following that change, new markdown files are recognized and included as expected.
And, FWIW, here's the meat of my modified highlighter using chroma (NOTE: the CSS styles can be exported separately (like so: `~/go/bin/chroma -s base16-snazzy --html-styles > _chroma.css`), or you can use styles from Pygments, including custom styles like [nord_pygments](https://github.com/sbrisard/nord_pygments)). Once included in your app.scss, if you use purgecss like me, you'll need to add the chroma class (or whatever class you're using) to the safelist for the webpack plugin: `safelist: {greedy: [/phx/, /topbar/, /inline/, /chroma/]}` .) And, FWIW, here's my modified highlighter module using chroma (I've exported the CSS styles separately (like so: `~/go/bin/chroma -s base16-snazzy --html-styles > _chroma.css`{:.lang-shell}) to include in my `app.scss`. If you use purgecss like me, you'll need to add the `.chroma`{:.lang-css} class to the safelist for the webpack plugin: `safelist: {greedy: [/phx/, /topbar/, /inline/, /chroma/]}`{:.lang-javascript} .)
```elixir ```elixir
defmodule Home73k.Highlighter do
@moduledoc """
Performs code highlighting.
"""
alias Home73k.Temp
# This refers to a method that gets the chroma binary location from application config
@chroma_bin Home73k.app_chroma_bin() |> Path.expand()
@doc """
Highlights all code in HTML (fenced code blocks and inlined code)
"""
def highlight_all(html) do
highlight_code_blocks(html) |> highlight_code_inlined()
end
@doc """
Highlights fenced code blocks in HTML document
"""
def highlight_code_blocks(html) do def highlight_code_blocks(html) do
~r/<pre><code(?:\s+class="(\w*)")?>([^<]*)<\/code><\/pre>/ ~r/<pre><code(?:\s+class="(\w*)")?>([^<]*)<\/code><\/pre>/
|> Regex.replace(html, &highlight_code_block(&1, &2, &3)) |> Regex.replace(html, &highlight_code_block(&1, &2, &3))
end end
defp highlight_code_block(_full_block, lang, code) do defp highlight_code_block(_full_block, lang, code) do
# perform the code highlighting
highlighted = highlight_code(lang, code)
# return properly wrapped highlighted code
~s(<pre class="chroma"><code class="language-#{lang}">#{highlighted}</code></pre>)
end
@doc """
Highlights inlined code in HTML document
"""
def highlight_code_inlined(html) do
~r/<code(?:\s+class="inline lang-(\w*)")?>([^<]*)<\/code>/
|> Regex.replace(html, &highlight_code_inline(&1, &2, &3))
end
defp highlight_code_inline(_full_block, lang, code) do
# perform the code highlighting
highlighted = highlight_code(lang, code)
# return properly wrapped highlighted code
~s(<code class="inline chroma language-#{lang}">#{highlighted}</code>)
end
# """
# Performs code highlighting using chroma
# """
defp highlight_code(lang, code) do
# unescape the code # unescape the code
unescaped_code = unescape_html(code) |> IO.iodata_to_binary() unescaped_code = unescape_html(code) |> IO.iodata_to_binary()
@ -53,11 +100,26 @@ defp highlight_code_block(_full_block, lang, code) do
# use chroma to highlight the code via temp file # use chroma to highlight the code via temp file
bin_args = ["-l", lang, "-f", "html", "--html-only", "--html-prevent-surrounding-pre", tmp_file] bin_args = ["-l", lang, "-f", "html", "--html-only", "--html-prevent-surrounding-pre", tmp_file]
# The '@chroma_bin' module attribute retrieves the configured System.cmd(@chroma_bin, bin_args) |> elem(0)
# location of the chroma cli binary from the application config. end
{highlighted, _} = System.cmd(@chroma_bin, bin_args)
# return properly wrapped highlighted code # """
~s(<pre class="chroma"><code class="language-#{lang}">#{highlighted}</code></pre>) # Code below for unescaping html, since it's escaped by Earmark markdown parsing
# """
entities = [{"&amp;", ?&}, {"&lt;", ?<}, {"&gt;", ?>}, {"&quot;", ?"}, {"&#39;", ?'}]
for {encoded, decoded} <- entities do
defp unescape_html(unquote(encoded) <> rest) do
[unquote(decoded) | unescape_html(rest)]
end
end
defp unescape_html(<<c, rest::binary>>) do
[c | unescape_html(rest)]
end
defp unescape_html(<<>>) do
[]
end
end end
``` ```