Skip to content

Instantly share code, notes, and snippets.

@tk0miya
Last active September 10, 2021 03:26
Show Gist options
  • Save tk0miya/8cfb322fd83b4c0e455efeb886ef1d6d to your computer and use it in GitHub Desktop.
Save tk0miya/8cfb322fd83b4c0e455efeb886ef1d6d to your computer and use it in GitHub Desktop.

Sphinx のアンカーを hash ベースにする実験

無保証です。なんとなく動くことは確認しました。

Well known issue:

  • セクション名などを使って hash 値を生成しているので、同名のセクション名がファイル内に複数回登場すると破綻します。

元ネタはこのあたり参照: https://twitter.com/kmuto/status/1029290347082338304

def on_doctree_resolved(app, doctree, docname):
from hashlib import md5
from docutils import nodes
# add hash-based node-ID to sections
mapping = {}
for node in doctree.traverse(nodes.section):
new_id = md5(node.children[0].astext().encode('utf-8')).hexdigest()
for node_id in node['ids']:
mapping[node_id] = new_id
node['ids'].insert(0, new_id)
# use hash-based node-IDs at local reference
for node in doctree.traverse(nodes.reference):
refid = node.get('refid')
if refid in mapping:
node['refid'] = mapping.get(refid)
# use hash-based node-IDs at toctrees
for _, toctree in app.env.tocs.items():
for node in toctree.traverse(nodes.reference):
if node.get('internal') and node.get('anchorname'):
node['anchorname'] = '#' + md5(node.astext().encode('utf-8')).hexdigest()
def setup(app):
app.connect('doctree-resolved', on_doctree_resolved)
@kmuto
Copy link

kmuto commented Aug 18, 2018

node.children[0]のほうがいいのかな?

@tk0miya
Copy link
Author

tk0miya commented Nov 18, 2019

コメントが付いていたのに気づいていませんでした。
ご指摘のとおりです。

なお、Sphinx 的には (正確には docutils 的には) node[0]node.children[0] は同じ意味です。ですので、どちらで記述しても構いません。どちらかといえば、シンプルに記述できる前者がよく使われている印象があります。

蛇足ですが section ノードの最初の要素は必ず title ノードであるという暗黙のルールがあるので、 node[0].astext() はセクション名のテキスト表現を取得しています。

@yuitowest
Copy link

セクション名にシーケンス番号降って、同名のセクション名がファイル内に複数回登場しても破綻しないようにしたバージョン

def on_doctree_resolved(app, doctree, docname):
    from collections import defaultdict
    from hashlib import md5
    from docutils import nodes

    # add hash-based node-ID to sections
    mapping = {}
    sequences = defaultdict(int)
    for node in doctree.traverse(nodes.section):
        text = node.children[0].astext()
        sequences[text] += 1
        new_id = md5("{}-{}".format(text, sequences[text]).encode('utf-8')).hexdigest()
        for node_id in node['ids']:
            mapping[node_id] = new_id

        node['ids'].insert(0, new_id)

    # use hash-based node-IDs at local reference
    for node in doctree.traverse(nodes.reference):
        refid = node.get('refid')
        if refid in mapping:
            node['refid'] = mapping.get(refid)

    # use hash-based node-IDs at toctrees
    for _, toctree in app.env.tocs.items():
        sequences = defaultdict(int)
        for node in toctree.traverse(nodes.reference):
            if node.get('internal') and node.get('anchorname'):
                text = node.astext()
                sequences[text] += 1
                node['anchorname'] = '#' + md5("{}-{}".format(text, sequences[text]).encode('utf-8')).hexdigest()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment