Repository: getnikola/nikola

Issue:
Support for Sphinx-style “link to here” headers
Sphinx can show ¶ after a header, which offers a link to this section (#foo). This feature can also be useful for some Nikola sites.


Discussion:
That is just a CSS tweak isn't it?
No, it requires special handling in reST/other markup formats. [Pelican plugin](https://github.com/getpelican/pelican-plugins/blob/master/headerid/headerid.py); Sphinx also customizes it like this.
It can be done using CSS+JS:

http://ben.balter.com/2014/03/13/pages-anchor-links/

Or a 3rd-party JS thing: https://github.com/bryanbraun/anchorjs

And I am pretty sure there is a pure-CSS version somewhere, something like

h4:after { content: "<a href=\"#" attr(id) "\">¶</a>" ; }


On Tue, Jan 24, 2017 at 5:01 PM Chris Warrick <notifications@github.com>
wrote:

> No, it requires special handling in reST/other markup formats. Pelican
> plugin
> <https://github.com/getpelican/pelican-plugins/blob/master/headerid/headerid.py>;
> Sphinx also customizes it like this.
>
> —
> You are receiving this because you commented.
>
>
> Reply to this email directly, view it on GitHub
> <https://github.com/getnikola/nikola/issues/2636#issuecomment-274921468>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AAAGK7CICX51PZOtxHI7pnNb48Cg_JPPks5rVliQgaJpZM4LjnQ->
> .
>

All three are ugly hacks.
We can do it in our HTML processing step in Post.compile_html ...

Resolving patch:
diff --git a/nikola/filters.py b/nikola/filters.py
--- a/nikola/filters.py
+++ b/nikola/filters.py
@@ -42,7 +42,7 @@
     typo = None  # NOQA
 import requests
 
-from .utils import req_missing, LOGGER
+from .utils import req_missing, LOGGER, slugify
 
 
 class _ConfigurableFilter(object):
@@ -66,10 +66,10 @@ def apply_to_binary_file(f):
     in place.  Reads files in binary mode.
     """
     @wraps(f)
-    def f_in_file(fname):
+    def f_in_file(fname, *args, **kwargs):
         with open(fname, 'rb') as inf:
             data = inf.read()
-        data = f(data)
+        data = f(data, *args, **kwargs)
         with open(fname, 'wb+') as outf:
             outf.write(data)
 
@@ -84,10 +84,10 @@ def apply_to_text_file(f):
     in place.  Reads files in UTF-8.
     """
     @wraps(f)
-    def f_in_file(fname):
+    def f_in_file(fname, *args, **kwargs):
         with io.open(fname, 'r', encoding='utf-8') as inf:
             data = inf.read()
-        data = f(data)
+        data = f(data, *args, **kwargs)
         with io.open(fname, 'w+', encoding='utf-8') as outf:
             outf.write(data)
 
@@ -398,3 +398,42 @@ def _normalize_html(data):
 
 # The function is used in other filters, so the decorator cannot be used directly.
 normalize_html = apply_to_text_file(_normalize_html)
+
+
+@_ConfigurableFilter(xpath_list='HEADER_PERMALINKS_XPATH_LIST')
+@apply_to_text_file
+def add_header_permalinks(data, xpath_list=None):
+    """Post-process HTML via lxml to add header permalinks Sphinx-style."""
+    doc = lxml.html.document_fromstring(data)
+    # Get language for slugify
+    try:
+        lang = doc.attrib['lang']  # <html lang="…">
+    except KeyError:
+        # Circular import workaround (utils imports filters)
+        from nikola.utils import LocaleBorg
+        lang = LocaleBorg().current_lang
+
+    xpath_set = set()
+    if not xpath_list:
+        xpath_list = ['*//div[@class="e-content entry-content"]//{hx}']
+    for xpath_expr in xpath_list:
+        for hx in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']:
+            xpath_set.add(xpath_expr.format(hx=hx))
+    for x in xpath_set:
+        nodes = doc.findall(x)
+        for node in nodes:
+            parent = node.getparent()
+            if 'id' in node.attrib:
+                hid = node.attrib['id']
+            elif 'id' in parent.attrib:
+                # docutils: <div> has an ID and contains the header
+                hid = parent.attrib['id']
+            else:
+                # Using force-mode, because not every character can appear in a
+                # HTML id
+                node.attrib['id'] = slugify(node.text_content(), lang, True)
+                hid = node.attrib['id']
+
+            new_node = lxml.html.fragment_fromstring('<a href="#{0}" class="headerlink" title="Permalink to this heading">¶</a>'.format(hid))
+            node.append(new_node)
+    return lxml.html.tostring(doc, encoding="unicode")
diff --git a/nikola/utils.py b/nikola/utils.py
--- a/nikola/utils.py
+++ b/nikola/utils.py
@@ -218,7 +218,6 @@ def req_missing(names, purpose, python=True, optional=False):
     return msg
 
 
-from nikola import filters as task_filters  # NOQA
 ENCODING = sys.getfilesystemencoding() or sys.stdin.encoding
 
 
@@ -902,6 +901,9 @@ def current_time(tzinfo=None):
     return dt
 
 
+from nikola import filters as task_filters  # NOQA
+
+
 def apply_filters(task, filters, skip_ext=None):
     """Apply filters to a task.
 


Test patch: