diff options
author | EuAndreh <eu@euandre.org> | 2020-08-13 08:09:01 -0300 |
---|---|---|
committer | EuAndreh <eu@euandre.org> | 2020-08-13 08:09:01 -0300 |
commit | 9e597a62c4818a3250318e841bdff2e6f3a2e74f (patch) | |
tree | fe1d4df0a4916b696dee42f8c2a7dffe353be9c6 | |
parent | styles.css: Show underline on code block anchors on hover (diff) | |
download | euandre.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.md | 155 |
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 :) |