aboutsummaryrefslogblamecommitdiff
path: root/_tils/2020-08-13-anchor-headers-and-code-lines-in-jekyll.md
blob: 656692840c2da1bc2bee30bb4d47c641044bba31 (plain) (tree)
1
2
3
4


                                              
            












































                                                                                
                                                                                








































































































                                                                                                                       
---
title: Anchor headers and code lines in Jekyll
date: 2020-08-13
layout: post
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 :)