🗑【Sphinx】indexディレクティブからjindexディレクティブを作る

#後日談（最新）

jindex ディレクティブの機能を内容したSphinx拡張を作りました。

https://qiita.com/koKekkoh/items/4563b63fdb8eaa3ef4f9

本記事は「開発日誌」的な意味合い以外には、特に読む必要はありません。

# 後日談
使ってみると「分類」に応じて表示場所は変わるけど、同じ分類の中では期待する順番では表示されない。色々調べてみて、挙動にクセはありますがGlossaryにある機能で十分であることがわかりました。

詳細は別途記事を起こして、こちらからリンクを貼る予定です。本記事は「これくらいの労力で拡張ディレクティブを作ることができるよ」のサンプル事例として残していおきます。
※T.B.D.はなくす予定です。

# １．はじめに

1. ```glossary``` ディレクティブは ```用語<spc>:<spc>分類``` とすることで、索引ページで ```分類``` に応じた分類をする。ひらがな１文字なら「読み」で分類。
1. 同じことを ```index``` でもやりたい。

という動機で始めたjindexディレクティブを作成するまでの過程をQiitaで共有。

## 使い方

- ```.. index::``` の代わりに ```.. jindex::``` を使用
- 名称の末尾に「 ```<spc>:<spc>分類名``` 」を追記する。
    - この「分類名」が「ひらがな一文字」なら、それで分類される。

## 補足

「２．管渠」「３．作業概要」を見て必要な情報を拾ってください。

# ２．環境

- Windows10/cygwin64
- Python 3.8.10
- pip 21.2.4（2021-09-07現在の最新）
- spihnx 4.1.2（2021-09-07現在の最新）

# ３．作業概要
- ```index``` ディレクティブ
- データ構造
- ```domains/index.py``` の改良
- ```jindex.py``` の作成（T.B.D.）
- ```conf.py``` の編集（T.B.D.）
- サンプルと動作確認（T.B.D.）

# ４．作業詳細
## ```index``` ディレクティブ

1. ```sphinxcontrib.yt``` を眺めた経験から、「```class``` で定義されている」「名前に ```index``` がある」「```directive``` という名前の何かを継承している」と当たりが付ける。
2. Sphinxがインストールされているディレクトリでファイルを下がる。
3. ```domains/index.py``` を見つける。
4. これを調べて ```indexnodes['entries']``` のデータ構造が知りたくなる。

実行したコマンド。

```bash
egrep -i `class.*index.*directive' */*.py
```

- ```*/``` は適当に増減する。
- ファイルが多かったり、ディレクトリが深い場合は ```find``` を使う。

## データ構造

1. それっぽい文字列を予想して、```egrep``` で探す。
1. ```addnodes.py``` を見つける。

```addnodes.py``` にあった説明

> Node for index entries.
> 
> This node is created by the ``index`` directive and has one attribute,
> ``entries``.  Its value is a list of 5-tuples of ``(entrytype, entryname,
> target, ignored, key)``.
> 
> *entrytype* is one of "single", "pair", "double", "triple".
>
> *key* is categorization characters (usually a single character) for
> general index page. For the details of this, please see also:
> :rst:dir:`glossary` and issue #2320.

- ```ignored``` と説明されている場所は。 ```entrytype``` が ```single``` の時は、```entryname``` の先頭に ```!``` があると ```main``` を設定している。何か意味があるのかもしれない。

## ```domains/index.py``` の改良

1. ```cp -p index.py index.py.orig```  でオリジナルを保存
1. print文でentryデータの内容を確認しつつ、改良を使える。

```diff -u``` で出力した変更内容は次の通り

```diff
--- index.py.orig       2021-09-05 01:23:03.208772600 +0900
+++ index.py    2021-09-07 01:03:51.222260000 +0900
@@ -8,6 +8,7 @@
     :license: BSD, see LICENSE for details.
 """

+import re
 from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Tuple

 from docutils import nodes
@@ -87,8 +88,15 @@
         indexnode['entries'] = []
         indexnode['inline'] = False
         self.set_source_info(indexnode)
+        entries = []
         for entry in arguments:
-            indexnode['entries'].extend(process_index_entry(entry, targetnode['ids'][0]))
+            entries.extend(process_index_entry(entry, targetnode['ids'][0]))
+        #print(entries)
+        for entry in entries:
+            parts = re.split(' +: +', entry[1]) + [None]
+            # entry: (indextype, text, target, 'main' or '', index_key)
+            indexnode['entries'].extend([(entry[0], parts[0], entry[2], entry[3], parts[1])])
+        #print(indexnode['entries'])
         return [indexnode, targetnode]
```

- ```process_index_entry()``` で作られたデータを一端作業用の変数にいれて、```entryname``` （コード内では ```entry[1]``` ）の内容を見てデータを作り直す。

## ```jindex.py``` の作成（T.B.D.）

※ディレクトリは読み替えてください。

### 関連ファイルの整理

動くコードがあるので、これをSphinx拡張に仕立て上げる。

その1

```bash
cd /usr/local/lib/python3.8/site-packages/sphinx
mv domains/index.py domains/index.py.new
cp -p domains/index.py.orig domains/index.py
cp domains/index.py.new ~/sphinx/_ext/jindex.py
```

その2

```bash
cd
cd ~/sphinx/_ext/
svn add jindex.py
svn ci -m "copied from site-packages/sphinx/domains/index.py"
vi jindex.py
```

### ```jindex.py``` 編集内容

- 不要な ```import``` を削除

- ```Class Index``` を ```Class JIndex``` に変更

```python
class JIndexDirective(SphinxDirective):
```

- 末尾に ``` : 文字列``` があれば取り出して分類名として使う

```python
        for entry in entries:
            parts = re.split(' +: +', entry[1]) + [None]
            # entry: (indextype, text, target, 'main' or '', index_key)
            indexnode['entries'].extend([(entry[0], parts[0], entry[2], entry[3], parts[1])])
```

- ```setup()``` は この ```JIndex``` を使うように変更

```python
def setup(app: "Sphinx") -> Dict[str, Any]:
    app.add_directive('jindex', JIndexDirective)

    return {
        'version': '4.1.2k1',
        'parallel_read_safe': True,
        'parallel_write_safe': True,
    }
```

### 編集した ```jindex.py``` の内容

```python
import re
from typing import TYPE_CHECKING, Any, Dict, List, Tuple

from docutils import nodes
from docutils.nodes import Node
from docutils.parsers.rst import directives

from sphinx import addnodes
from sphinx.util.docutils import SphinxDirective
from sphinx.util.nodes import process_index_entry
from sphinx.util.typing import OptionSpec

if TYPE_CHECKING:
    from sphinx.application import Sphinx


class JIndexDirective(SphinxDirective):
    has_content = False
    required_arguments = 1
    optional_arguments = 0
    final_argument_whitespace = True
    option_spec: OptionSpec = {
        'name': directives.unchanged,
    }

    def run(self) -> List[Node]:
        arguments = self.arguments[0].split('\n')

        if 'name' in self.options:
            targetname = self.options['name']
            targetnode = nodes.target('', '', names=[targetname])
        else:
            targetid = 'index-%s' % self.env.new_serialno('index')
            targetnode = nodes.target('', '', ids=[targetid])

        self.state.document.note_explicit_target(targetnode)
        indexnode = addnodes.index()
        indexnode['entries'] = []
        indexnode['inline'] = False
        self.set_source_info(indexnode)
        entries = []
        for entry in arguments:
            entries.extend(process_index_entry(entry, targetnode['ids'][0]))
        #print(entries)
        for entry in entries:
            parts = re.split(' +: +', entry[1]) + [None]
            # entry: (indextype, text, target, 'main' or '', index_key)
            indexnode['entries'].extend([(entry[0], parts[0], entry[2], entry[3], parts[1])])
        #print(indexnode['entries'])
        return [indexnode, targetnode]


def setup(app: "Sphinx") -> Dict[str, Any]:
    app.add_directive('jindex', JIndexDirective)

    return {
        'version': '4.1.2k1',
        'parallel_read_safe': True,
        'parallel_write_safe': True,
    }
```

## ```conf.py``` の編集（T.B.D.）

## サンプルと動作確認（T.B.D.）

# ５．関連情報
- [【Sphinx】yogosyu（用語集）をSphinx4.1.2で使えるように修正する](https://qiita.com/koKekkoh/items/e7801c0e567c4f41dbe5)
- [Sphinxの古いバージョンにあったsphinx.addnodes.termsepクラスについて](https://qiita.com/koKekkoh/questions/978fbf7310f12293c25a)

# 以上