Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Merge pull request #2743 from getnikola/add-title-permalinks
Sphinx-style permalinks filter
  • Loading branch information
Kwpolska committed May 14, 2017
2 parents d2347c4 + 1298a14 commit 3270735
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 7 deletions.
12 changes: 12 additions & 0 deletions CHANGES.txt
@@ -1,3 +1,15 @@
New in master
=============

Features
--------

* New ``add_header_permalinks`` filter, for Sphinx-style header links
(Issue #2636)

Bugfixes
--------

New in v7.8.5
=============

Expand Down
17 changes: 17 additions & 0 deletions docs/manual.txt
Expand Up @@ -1911,6 +1911,23 @@ jsonminify
xmlminify
Minify XML files. Suitable for Nikola’s sitemaps and Atom feeds.

add_header_permalinks
Add links next to every header, Sphinx-style. You will need to add styling for the `headerlink` class,
in `custom.css`, for example:

.. code:: css
.headerlink { opacity: 0.1; margin-left: 0.2em; }
.headerlink:hover { opacity: 1; text-decoration: none; }

Additionally, you can provide a custom list of XPath expressions which should be used for finding headers (``{hx}}`` is replaced by headers h1 through h6).
This is required if you use a custom theme that does not use ``"e-content entry-content"`` as a class for post and page contents.

.. code:: python
# Default value:
HEADER_PERMALINKS_XPATH_LIST = ['*//div[@class="e-content entry-content"]//{hx}']
# Include *every* header (not recommended):
# HEADER_PERMALINKS_XPATH_LIST = ['*//{hx}']

You can apply filters to specific posts or pages by using the ``filters`` metadata field:

.. code:: restructuredtext
Expand Down
9 changes: 8 additions & 1 deletion nikola/conf.py.in
Expand Up @@ -586,7 +586,14 @@ GITHUB_COMMIT_SOURCE = True
# (defaults to 'tidy5').
# HTML_TIDY_EXECUTABLE = 'tidy5'


# List of XPath expressions which should be used for finding headers
# ({hx}} is replaced by headers h1 through h6).
# You must change this if you use a custom theme that does not use
# "e-content entry-content" as a class for post and page contents.

# HEADER_PERMALINKS_XPATH_LIST = ['*//div[@class="e-content entry-content"]//{hx}']
# Include *every* header (not recommended):
# HEADER_PERMALINKS_XPATH_LIST = ['*//{hx}']

# Expert setting! Create a gzipped copy of each generated file. Cheap server-
# side optimization for very high traffic sites or low memory servers.
Expand Down
49 changes: 44 additions & 5 deletions nikola/filters.py
Expand Up @@ -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):
Expand All @@ -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)

Expand All @@ -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)

Expand Down Expand Up @@ -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")
4 changes: 3 additions & 1 deletion nikola/utils.py
Expand Up @@ -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


Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit 3270735

Please sign in to comment.