aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEuAndreh <eu@euandre.org>2020-08-13 08:09:01 -0300
committerEuAndreh <eu@euandre.org>2020-08-13 08:09:01 -0300
commit9e597a62c4818a3250318e841bdff2e6f3a2e74f (patch)
treefe1d4df0a4916b696dee42f8c2a7dffe353be9c6
parentstyles.css: Show underline on code block anchors on hover (diff)
downloadeuandre.org-9e597a62c4818a3250318e841bdff2e6f3a2e74f.tar.gz
euandre.org-9e597a62c4818a3250318e841bdff2e6f3a2e74f.tar.xz
Add TIL on Jekyll anchors
-rw-r--r--_tils/2020-08-13-anchor-headers-and-code-lines-in-jekyll.md155
1 files changed, 155 insertions, 0 deletions
diff --git a/_tils/2020-08-13-anchor-headers-and-code-lines-in-jekyll.md b/_tils/2020-08-13-anchor-headers-and-code-lines-in-jekyll.md
new file mode 100644
index 0000000..d9181c3
--- /dev/null
+++ b/_tils/2020-08-13-anchor-headers-and-code-lines-in-jekyll.md
@@ -0,0 +1,155 @@
+---
+title: Anchor headers and code lines in Jekyll
+date: 2020-08-13
+layout: til
+lang: en
+ref: anchor-headers-and-code-lines-in-jekyll
+---
+The default Jekyll toolbox ([Jekyll][0], [kramdown][1] and [rouge][2]) doesn't
+provide with a configuration option to add anchors to headers and code blocks.
+
+[0]: https://jekyllrb.com/
+[1]: https://kramdown.gettalong.org/
+[2]: http://rouge.jneen.net/
+
+The best way I found of doing this is by creating a simple Jekyll plugin, more
+specifically, a [Jekyll hook][3]. These allow you to jump in to the Jekyll build
+and add a processing stage before of after Jekyll performs something.
+
+[3]: https://jekyllrb.com/docs/plugins/hooks/
+
+All you have to do is add the code to `_plugins/my-jekyll-plugin-code.rb`, and
+Jekyll knows to pick it up and call your code on the appropriate time.
+
+## Anchor on headers
+
+Since I wanted to add anchors to headers in all documents, this Jekyll hook
+works on `:documents` after they have been transformed into HTML, the
+`:post_render` phase:
+
+```ruby
+Jekyll::Hooks.register :documents, :post_render do |doc|
+ if doc.output_ext == ".html"
+ doc.output =
+ doc.output.gsub(
+ /<h([1-6])(.*?)id="([\w-]+)"(.*?)>(.*?)<\/h[1-6]>/,
+ '<a href="#\3"><h\1\2id="\3"\4>\5</h\1></a>'
+ )
+ end
+end
+```
+
+I've derived my implementations from two "official"[^official] hooks,
+[jemoji][4] and [jekyll-mentions][5].
+
+[4]: https://github.com/jekyll/jemoji
+[5]: https://github.com/jekyll/jekyll-mentions
+[^official]: I don't know how official they are, I just assumed it because they
+ live in the same organization inside GitHub that Jekyll does.
+
+All I did was to wrap the header tag inside an `<a>`, and set the href of that
+`<a>` to the existing id of the header. Before the hook the HTML looks like:
+
+```html
+...some unmodified text...
+<h2 id="my-header">
+ My header
+</h2>
+...more unmodified text...
+```
+
+And after the hook should turn that into:
+
+```html
+...some unmodified text...
+<a href="#my-header">
+ <h2 id="my-header">
+ My header
+ </h2>
+</a>
+...more unmodified text...
+```
+
+The used regexp tries to match only h1-h6 tags, and keep the rest of the HTML
+attributes untouched, since this isn't a general HTML parser, but the generated HTML
+is somewhat under your control. Use at your own risk because
+[you shouldn't parse HTML with regexps][6]. Also I used this strategy in my
+environment, where no other plugins are installed. I haven't considered how this
+approach may conflict with other Jekyll plugins.
+
+[6]: https://stackoverflow.com/questions/1732348/regex-match-open-tags-except-xhtml-self-contained-tags/1732454#1732454
+
+In the new anchor tag you can add your custom CSS class to style it as you wish.
+
+## Anchor on code blocks
+
+Adding anchors to code blocks needs a little bit of extra work, because line
+numbers themselves don't have preexisting ids, so we need to generate them
+without duplications between multiple code blocks in the same page.
+
+Similarly, this Jekyll hook also works on `:documents` in the `:post_render`
+phase:
+
+```ruby
+PREFIX = '<pre class="lineno">'
+POSTFIX = '</pre>'
+Jekyll::Hooks.register :documents, :post_render do |doc|
+ if doc.output_ext == ".html"
+ code_block_counter = 1
+ doc.output = doc.output.gsub(/<pre class="lineno">[\n0-9]+<\/pre>/) do |match|
+ line_numbers = match
+ .gsub(/<pre class="lineno">([\n0-9]+)<\/pre>/, '\1')
+ .split("\n")
+
+ anchored_line_numbers_array = line_numbers.map do |n|
+ id = "B#{code_block_counter}-L#{n}"
+ "<a id=\"#{id}\" href=\"##{id}\">#{n}</a>"
+ end
+ code_block_counter += 1
+
+ PREFIX + anchored_line_numbers_array.join("\n") + POSTFIX
+ end
+ end
+end
+```
+
+This solution assumes the default Jekyll toolbox with code line numbers turned
+on in `_config.yml`:
+
+```yaml
+kramdown:
+ syntax_highlighter_opts:
+ span:
+ line_numbers: false
+ block:
+ line_numbers: true
+```
+
+The anchors go from B1-L1 to BN-LN, using the `code_block_counter` to track
+which code block we're in and don't duplicate anchor ids. Before the hook the
+HTML looks like:
+
+```html
+...some unmodified text...
+<pre class="lineno">1
+2
+3
+4
+5
+</pre>
+...more unmodified text...
+```
+
+And after the hook should turn that into:
+
+```html
+...some unmodified text...
+<pre class="lineno"><a id="B1-L1" href="#B1-L1">1</a>
+<a id="B1-L2" href="#B1-L2">2</a>
+<a id="B1-L3" href="#B1-L3">3</a>
+<a id="B1-L4" href="#B1-L4">4</a>
+<a id="B1-L5" href="#B1-L5">5</a></pre>
+...more unmodified text...
+```
+
+Happy writing :)