24
24
from docutils .parsers .rst .directives import unchanged as directive_param_unchanged
25
25
from docutils .utils import Reporter , SystemMessage
26
26
27
+ _SPECIAL_ATTRIBUTES = (
28
+ "antsibull-code-language" ,
29
+ "antsibull-code-block" ,
30
+ "antsibull-code-lineno" ,
31
+ )
32
+
27
33
28
34
class IgnoreDirective (Directive ):
29
35
"""
@@ -37,14 +43,24 @@ def run(self) -> list:
37
43
38
44
39
45
def mark_antsibull_code_block (
40
- node : nodes .literal_block , * , language : str | None , line : int
46
+ node : nodes .literal_block ,
47
+ * ,
48
+ language : str | None ,
49
+ line : int ,
50
+ other : dict [str , t .Any ] | None = None ,
41
51
) -> None :
42
52
"""
43
53
Mark a literal block as an Antsibull code block with given language and line number.
54
+
55
+ Everything in ``other`` will be available as ``antsibull-other-{key}`` for a key ``key``
56
+ in ``other`` in the node's attributes.
44
57
"""
45
58
node ["antsibull-code-language" ] = language
46
59
node ["antsibull-code-block" ] = True
47
60
node ["antsibull-code-lineno" ] = line
61
+ if other :
62
+ for key , value in other .items ():
63
+ node [f"antsibull-other-{ key } " ] = value
48
64
49
65
50
66
class CodeBlockDirective (Directive ):
@@ -92,7 +108,9 @@ def __init__(
92
108
self ,
93
109
document : nodes .document ,
94
110
content : str ,
95
- callback : t .Callable [[str , int , int , bool , str ], None ],
111
+ callback : t .Callable [
112
+ [str , int , int , bool , bool , str , nodes .literal_block ], None
113
+ ],
96
114
warn_unknown_block : t .Callable [[int | str , int , nodes .literal_block ], None ],
97
115
):
98
116
super ().__init__ (document )
@@ -114,6 +132,12 @@ def visit_error(self, node: nodes.error) -> None:
114
132
115
133
@staticmethod
116
134
def _find_indent (content : str ) -> int | None :
135
+ """
136
+ Given concatenated lines, find the minimum indent if possible.
137
+
138
+ If all lines consist only out of whitespace (or are empty),
139
+ ``None`` is returned.
140
+ """
117
141
min_indent = None
118
142
for line in content .split ("\n " ):
119
143
stripped_line = line .lstrip ()
@@ -123,7 +147,14 @@ def _find_indent(content: str) -> int | None:
123
147
min_indent = indent
124
148
return min_indent
125
149
126
- def _find_offset (self , lineno : int , content : str ) -> tuple [int , int ]:
150
+ def _find_offset (self , lineno : int , content : str ) -> tuple [int , int , bool ]:
151
+ """
152
+ Try to identify the row/col offset of the code in ``content`` in the document.
153
+
154
+ ``lineno`` is assumed to be the line where the code-block starts.
155
+ This function looks for an empty line, followed by the right pattern of
156
+ empty and non-empty lines.
157
+ """
127
158
row_offset = lineno
128
159
found_empty_line = False
129
160
found_content_lines = False
@@ -150,9 +181,15 @@ def _find_offset(self, lineno: int, content: str) -> tuple[int, int]:
150
181
151
182
min_source_indent = self ._find_indent (content )
152
183
col_offset = max (0 , (min_indent or 0 ) - (min_source_indent or 0 ))
153
- return row_offset , col_offset
184
+ return row_offset , col_offset , content_lines == 0
154
185
155
186
def _find_in_code (self , row_offset : int , col_offset : int , content : str ) -> bool :
187
+ """
188
+ Check whether the code can be found at the given row/col offset in a way
189
+ that makes it easy to replace.
190
+
191
+ That is, it is surrounded only by whitespace.
192
+ """
156
193
for index , line in enumerate (content .split ("\n " )):
157
194
if row_offset + index >= len (self .__content_lines ):
158
195
return False
@@ -178,13 +215,29 @@ def visit_literal_block(self, node: nodes.literal_block) -> None:
178
215
179
216
language = node .attributes ["antsibull-code-language" ]
180
217
lineno = node .attributes ["antsibull-code-lineno" ]
181
- row_offset , col_offset = self ._find_offset (lineno , node .rawsource )
218
+ row_offset , col_offset , position_exact = self ._find_offset (
219
+ lineno , node .rawsource
220
+ )
221
+ found_in_code = False
222
+ if position_exact :
223
+ # If we think we have the exact position, try to identify the code.
224
+ # ``found_in_code`` indicates that it is easy to replace the code,
225
+ # and at the same time it's easy to identify it.
226
+ found_in_code = self ._find_in_code (row_offset , col_offset , node .rawsource )
227
+ if not found_in_code :
228
+ position_exact = False
229
+ if not found_in_code :
230
+ # We were not able to find hte code 'the easy way'. This could be because
231
+ # it is inside a table.
232
+ pass # TODO search for the content, f.ex. in tables
182
233
self .__callback (
183
234
language ,
184
235
row_offset ,
185
236
col_offset ,
186
- self ._find_in_code (row_offset , col_offset , node .rawsource ),
237
+ position_exact ,
238
+ found_in_code ,
187
239
node .rawsource .rstrip () + "\n " ,
240
+ node ,
188
241
)
189
242
raise nodes .SkipNode
190
243
@@ -253,13 +306,22 @@ class CodeBlockInfo:
253
306
row_offset : int
254
307
col_offset : int
255
308
309
+ # Whether the position (row/col_offset) is exact.
310
+ # If set to ``False``, the position is approximate and col_offset is often 0.
311
+ position_exact : bool
312
+
256
313
# Whether the code block's contents can be found as-is in the RST file,
257
314
# only indented by whitespace, and with potentially trailing whitespace
258
- directly_in_content : bool
315
+ directly_replacable_in_content : bool
259
316
260
317
# The code block's contents
261
318
content : str
262
319
320
+ # The code block's attributes that start with ``antsibull-``.
321
+ # Special attributes used by ``find_code_blocks()`` to keep track of
322
+ # certain properties are not present.
323
+ attributes : dict [str , t .Any ]
324
+
263
325
264
326
def find_code_blocks (
265
327
content : str ,
@@ -284,20 +346,28 @@ def find_code_blocks(
284
346
# using this ugly list
285
347
results = []
286
348
287
- def callback (
349
+ def callback ( # pylint: disable=too-many-arguments,too-many-positional-arguments
288
350
language : str ,
289
351
row_offset : int ,
290
352
col_offset : int ,
291
- directly_in_content : bool ,
353
+ position_exact : bool ,
354
+ directly_replacable_in_content : bool ,
292
355
content : str ,
356
+ node : nodes .literal_block ,
293
357
) -> None :
294
358
results .append (
295
359
CodeBlockInfo (
296
360
language = language ,
297
361
row_offset = row_offset ,
298
362
col_offset = col_offset ,
299
- directly_in_content = directly_in_content ,
363
+ position_exact = position_exact ,
364
+ directly_replacable_in_content = directly_replacable_in_content ,
300
365
content = content ,
366
+ attributes = {
367
+ key : value
368
+ for key , value in node .attributes .items ()
369
+ if key not in _SPECIAL_ATTRIBUTES and key .startswith ("antsibull-" )
370
+ },
301
371
)
302
372
)
303
373
0 commit comments