|
2 | 2 | sphinxnotes.snippet
|
3 | 3 | ~~~~~~~~~~~~~~~~~~~
|
4 | 4 |
|
5 |
| -:copyright: Copyright 2020 Shengyu Zhang |
| 5 | +Sphinx extension entrypoint. |
| 6 | +
|
| 7 | +:copyright: Copyright 2024 Shengyu Zhang |
6 | 8 | :license: BSD, see LICENSE for details.
|
7 | 9 | """
|
8 | 10 |
|
9 |
| -from __future__ import annotations |
10 |
| -from typing import List, Tuple, Optional, TYPE_CHECKING |
11 |
| -import itertools |
12 |
| -from os import path |
13 |
| - |
14 |
| -from docutils import nodes |
15 |
| - |
16 |
| -if TYPE_CHECKING: |
17 |
| - from sphinx.environment import BuildEnvironment |
18 |
| - |
19 |
| -__version__ = '1.1.1' |
20 |
| - |
21 |
| - |
22 |
| -class Snippet(object): |
23 |
| - """ |
24 |
| - Snippet is base class of reStructuredText snippet. |
25 |
| -
|
26 |
| - :param nodes: Document nodes that make up this snippet |
27 |
| - """ |
28 |
| - |
29 |
| - #: docname where the snippet is located, can be referenced by |
30 |
| - # :rst:role:`doc`. |
31 |
| - docname: str |
32 |
| - |
33 |
| - #: Absolute path of the source file. |
34 |
| - file: str |
35 |
| - |
36 |
| - #: Line number range of snippet, in the source file which is left closed |
37 |
| - #: and right opened. |
38 |
| - lineno: Tuple[int, int] |
39 |
| - |
40 |
| - #: The original reStructuredText of snippet |
41 |
| - rst: List[str] |
42 |
| - |
43 |
| - #: The possible identifier key of snippet, which is picked from nodes' |
44 |
| - #: (or nodes' parent's) `ids attr`_. |
45 |
| - #: |
46 |
| - #: .. _ids attr: https://docutils.sourceforge.io/docs/ref/doctree.html#ids |
47 |
| - refid: Optional[str] |
48 |
| - |
49 |
| - def __init__(self, *nodes: nodes.Node) -> None: |
50 |
| - assert len(nodes) != 0 |
51 |
| - |
52 |
| - env: BuildEnvironment = nodes[0].document.settings.env |
53 |
| - self.file = nodes[0].source |
54 |
| - self.docname = env.path2doc(self.file) |
55 |
| - |
56 |
| - lineno = [float('inf'), -float('inf')] |
57 |
| - for node in nodes: |
58 |
| - if not node.line: |
59 |
| - continue # Skip node that have None line, I dont know why |
60 |
| - lineno[0] = min(lineno[0], _line_of_start(node)) |
61 |
| - lineno[1] = max(lineno[1], _line_of_end(node)) |
62 |
| - self.lineno = lineno |
63 |
| - |
64 |
| - lines = [] |
65 |
| - with open(self.file, 'r') as f: |
66 |
| - start = self.lineno[0] - 1 |
67 |
| - stop = self.lineno[1] - 1 |
68 |
| - for line in itertools.islice(f, start, stop): |
69 |
| - lines.append(line.strip('\n')) |
70 |
| - self.rst = lines |
71 |
| - |
72 |
| - # Find exactly one ID attr in nodes |
73 |
| - self.refid = None |
74 |
| - for node in nodes: |
75 |
| - if node['ids']: |
76 |
| - self.refid = node['ids'][0] |
77 |
| - break |
78 |
| - |
79 |
| - # If no ID found, try parent |
80 |
| - if not self.refid: |
81 |
| - for node in nodes: |
82 |
| - if node.parent['ids']: |
83 |
| - self.refid = node.parent['ids'][0] |
84 |
| - break |
85 |
| - |
86 |
| - |
87 |
| -class Text(Snippet): |
88 |
| - #: Text of snippet |
89 |
| - text: str |
90 |
| - |
91 |
| - def __init__(self, node: nodes.Node) -> None: |
92 |
| - super().__init__(node) |
93 |
| - self.text = node.astext() |
94 |
| - |
95 |
| - |
96 |
| -class CodeBlock(Text): |
97 |
| - #: Language of code block |
98 |
| - language: str |
99 |
| - #: Caption of code block |
100 |
| - caption: Optional[str] |
101 |
| - |
102 |
| - def __init__(self, node: nodes.literal_block) -> None: |
103 |
| - assert isinstance(node, nodes.literal_block) |
104 |
| - super().__init__(node) |
105 |
| - self.language = node['language'] |
106 |
| - self.caption = node.get('caption') |
107 |
| - |
108 |
| - |
109 |
| -class WithCodeBlock(object): |
110 |
| - code_blocks: List[CodeBlock] |
111 |
| - |
112 |
| - def __init__(self, nodes: nodes.Nodes) -> None: |
113 |
| - self.code_blocks = [] |
114 |
| - for n in nodes.traverse(nodes.literal_block): |
115 |
| - self.code_blocks.append(self.CodeBlock(n)) |
116 |
| - |
117 |
| - |
118 |
| -class Title(Text): |
119 |
| - def __init__(self, node: nodes.title) -> None: |
120 |
| - assert isinstance(node, nodes.title) |
121 |
| - super().__init__(node) |
122 |
| - |
123 |
| - |
124 |
| -class WithTitle(object): |
125 |
| - title: Optional[Title] |
126 |
| - |
127 |
| - def __init__(self, node: nodes.Node) -> None: |
128 |
| - title_node = node.next_node(nodes.title) |
129 |
| - self.title = Title(title_node) if title_node else None |
130 |
| - |
131 |
| - |
132 |
| -class Section(Snippet, WithTitle): |
133 |
| - def __init__(self, node: nodes.section) -> None: |
134 |
| - assert isinstance(node, nodes.section) |
135 |
| - Snippet.__init__(self, node) |
136 |
| - WithTitle.__init__(self, node) |
137 |
| - |
138 |
| - |
139 |
| -class Document(Section): |
140 |
| - #: A set of absolute paths of dependent files for document. |
141 |
| - #: Obtained from :attr:`BuildEnvironment.dependencies`. |
142 |
| - deps: set[str] |
143 |
| - |
144 |
| - def __init__(self, node: nodes.document) -> None: |
145 |
| - assert isinstance(node, nodes.document) |
146 |
| - super().__init__(node.next_node(nodes.section)) |
147 |
| - |
148 |
| - # Record document's dependent files |
149 |
| - self.deps = set() |
150 |
| - env: BuildEnvironment = node.settings.env |
151 |
| - for dep in env.dependencies[self.docname]: |
152 |
| - # Relative to documentation root -> Absolute path of file system. |
153 |
| - self.deps.add(path.join(env.srcdir, dep)) |
154 |
| - |
155 |
| - |
156 |
| -################ |
157 |
| -# Nodes helper # |
158 |
| -################ |
159 |
| - |
160 |
| - |
161 |
| -def _line_of_start(node: nodes.Node) -> int: |
162 |
| - assert node.line |
163 |
| - if isinstance(node, nodes.title): |
164 |
| - if isinstance(node.parent.parent, nodes.document): |
165 |
| - # Spceial case for Document Title / Subtitle |
166 |
| - return 1 |
167 |
| - else: |
168 |
| - # Spceial case for section title |
169 |
| - return node.line - 1 |
170 |
| - elif isinstance(node, nodes.section): |
171 |
| - if isinstance(node.parent, nodes.document): |
172 |
| - # Spceial case for top level section |
173 |
| - return 1 |
174 |
| - else: |
175 |
| - # Spceial case for section |
176 |
| - return node.line - 1 |
177 |
| - return node.line |
178 |
| - |
179 | 11 |
|
180 |
| -def _line_of_end(node: nodes.Node) -> Optional[int]: |
181 |
| - next_node = node.next_node(descend=False, siblings=True, ascend=True) |
182 |
| - while next_node: |
183 |
| - if next_node.line: |
184 |
| - return _line_of_start(next_node) |
185 |
| - next_node = next_node.next_node( |
186 |
| - # Some nodes' line attr is always None, but their children has |
187 |
| - # valid line attr |
188 |
| - descend=True, |
189 |
| - # If node and its children have not valid line attr, try use line |
190 |
| - # of next node |
191 |
| - ascend=True, |
192 |
| - siblings=True, |
193 |
| - ) |
194 |
| - # No line found, return the max line of source file |
195 |
| - if node.source: |
196 |
| - with open(node.source) as f: |
197 |
| - return sum(1 for line in f) |
198 |
| - raise AttributeError('None source attr of node %s' % node) |
| 12 | +def setup(app): |
| 13 | + # **WARNING**: We don't import these packages globally, because the current |
| 14 | + # package (sphinxnotes.snippet) is always resloved when importing |
| 15 | + # sphinxnotes.snippet.*. If we import packages here, eventually we will |
| 16 | + # load a lot of packages from the Sphinx. It will seriously **SLOW DOWN** |
| 17 | + # the startup time of our CLI tool (sphinxnotes.snippet.cli). |
| 18 | + # |
| 19 | + # .. seealso:: https://github.com/sphinx-notes/snippet/pull/31 |
| 20 | + from .ext import ( |
| 21 | + SnippetBuilder, |
| 22 | + on_config_inited, |
| 23 | + on_env_get_outdated, |
| 24 | + on_doctree_resolved, |
| 25 | + on_builder_finished, |
| 26 | + ) |
| 27 | + |
| 28 | + app.add_builder(SnippetBuilder) |
| 29 | + |
| 30 | + app.add_config_value('snippet_config', {}, '') |
| 31 | + app.add_config_value('snippet_patterns', {'*': ['.*']}, '') |
| 32 | + |
| 33 | + app.connect('config-inited', on_config_inited) |
| 34 | + app.connect('env-get-outdated', on_env_get_outdated) |
| 35 | + app.connect('doctree-resolved', on_doctree_resolved) |
| 36 | + app.connect('build-finished', on_builder_finished) |
0 commit comments