使用角色和指令擴展語法

概觀

reStructuredText 和 MyST 的語法都可以通過創建新的 指令 (用於區塊層級元素) 和 角色 (用於行內元素) 來擴展。

在本教學中,我們將擴展 Sphinx 以添加

  • 一個 hello 角色,它將簡單地輸出文字 Hello {text}!

  • 一個 hello 指令,它將簡單地輸出文字 Hello {text}!,作為一個段落。

對於此擴展,您將需要一些 Python 的基本理解,我們還將介紹 docutils API 的各個方面。

設定專案

您可以使用現有的 Sphinx 專案,或使用 sphinx-quickstart 建立一個新的專案。

有了這個,我們將把擴展添加到專案中,在 source 資料夾中

  1. source 中建立一個 _ext 資料夾

  2. _ext 資料夾中建立一個名為 helloworld.py 的新 Python 檔案

這是一個您可能獲得的資料夾結構範例

└── source
    ├── _ext
    │   └── helloworld.py
    ├── conf.py
    ├── index.rst

撰寫擴展

打開 helloworld.py 並貼上以下程式碼

 1from __future__ import annotations
 2
 3from docutils import nodes
 4
 5from sphinx.application import Sphinx
 6from sphinx.util.docutils import SphinxDirective, SphinxRole
 7from sphinx.util.typing import ExtensionMetadata
 8
 9
10class HelloRole(SphinxRole):
11    """A role to say hello!"""
12
13    def run(self) -> tuple[list[nodes.Node], list[nodes.system_message]]:
14        node = nodes.inline(text=f'Hello {self.text}!')
15        return [node], []
16
17
18class HelloDirective(SphinxDirective):
19    """A directive to say hello!"""
20
21    required_arguments = 1
22
23    def run(self) -> list[nodes.Node]:
24        paragraph_node = nodes.paragraph(text=f'hello {self.arguments[0]}!')
25        return [paragraph_node]
26
27
28def setup(app: Sphinx) -> ExtensionMetadata:
29    app.add_role('hello', HelloRole())
30    app.add_directive('hello', HelloDirective)
31
32    return {
33        'version': '0.1',
34        'parallel_read_safe': True,
35        'parallel_write_safe': True,
36    }

在這個範例中,發生了一些重要的事情

角色類別

我們的新角色在 HelloRole 類別中宣告。

1class HelloRole(SphinxRole):
2    """A role to say hello!"""
3
4    def run(self) -> tuple[list[nodes.Node], list[nodes.system_message]]:
5        node = nodes.inline(text=f'Hello {self.text}!')
6        return [node], []

這個類別擴展了 SphinxRole 類別。該類別包含一個 run 方法,這是每個角色的要求。它包含角色的主要邏輯,並返回一個包含以下內容的元組

  • 要由 Sphinx 處理的行內層級 docutils 節點列表。

  • (可選)系統訊息節點列表

指令類別

我們的新指令在 HelloDirective 類別中宣告。

1class HelloDirective(SphinxDirective):
2    """A directive to say hello!"""
3
4    required_arguments = 1
5
6    def run(self) -> list[nodes.Node]:
7        paragraph_node = nodes.paragraph(text=f'hello {self.arguments[0]}!')
8        return [paragraph_node]

這個類別擴展了 SphinxDirective 類別。該類別包含一個 run 方法,這是每個指令的要求。它包含指令的主要邏輯,並返回要由 Sphinx 處理的區塊層級 docutils 節點列表。它還包含一個 required_arguments 屬性,該屬性告訴 Sphinx 指令需要多少個參數。

什麼是 docutils 節點?

當 Sphinx 解析文件時,它會創建一個「抽象語法樹」(AST),其中節點以結構化方式表示文件的內容,這通常獨立於任何一種輸入(rST、MyST 等)或輸出(HTML、LaTeX 等)格式。它是一棵樹,因為每個節點都可以有子節點,依此類推

<document>
   <paragraph>
      <text>
         Hello world!

docutils 套件提供了許多 內建節點,用於表示不同類型的內容,例如文字、段落、參考、表格等。

每種節點類型通常只接受一組特定的直接子節點,例如 document 節點應僅包含「區塊層級」節點,例如 paragraphsectiontable 等,而 paragraph 節點應僅包含「行內層級」節點,例如 textemphasisstrong 等。

另請參閱

關於 建立指令建立角色 的 docutils 文件。

setup 函數

此函數是必要的。我們使用它將我們的新指令插入到 Sphinx 中。

def setup(app: Sphinx) -> ExtensionMetadata:
    app.add_role('hello', HelloRole())
    app.add_directive('hello', HelloDirective)

    return {
        'version': '0.1',
        'parallel_read_safe': True,
        'parallel_write_safe': True,
    }

您可以做的最簡單的事情是調用 Sphinx.add_role()Sphinx.add_directive() 方法,這就是我們在這裡所做的。對於這個特定的調用,第一個參數是角色/指令本身的名稱,如同在 reStructuredText 檔案中使用的一樣。在本例中,我們將使用 hello。例如

Some intro text here...

.. hello:: world

Some text with a :hello:`world` role.

我們還返回 擴展元數據,它指示我們擴展的版本,以及使用擴展進行平行讀取和寫入都是安全的。

使用擴展

擴展必須在您的 conf.py 檔案中宣告,才能讓 Sphinx 知道它。這裡有兩個必要的步驟

  1. 使用 sys.path.append_ext 目錄添加到 Python 路徑。這應該放在檔案的頂部。

  2. 更新或建立 extensions 列表,並將擴展檔案名稱添加到列表中

例如

import sys
from pathlib import Path

sys.path.append(str(Path('_ext').resolve()))

extensions = ['helloworld']

提示

因為我們還沒有將我們的擴展作為 Python 套件 安裝,所以我們需要修改 Python 路徑,以便 Sphinx 可以找到我們的擴展。這就是為什麼我們需要調用 sys.path.append

您現在可以在檔案中使用擴展。例如

Some intro text here...

.. hello:: world

Some text with a :hello:`world` role.

上面的範例將產生

Some intro text here...

Hello world!

Some text with a hello world! role.

延伸閱讀

這是一個建立新角色和指令的擴展的基本原理。

有關更進階的範例,請參閱 擴展建置流程

如果您希望在多個專案或與他人分享您的擴展,請查看 第三方擴展 章節。