Skip to content

Commit 8ce6773

Browse files
committed
Fix and respect restructured tables and lists
1 parent 3ffa291 commit 8ce6773

12 files changed

Lines changed: 553 additions & 110 deletions

bin/format_rst_file.py

Lines changed: 227 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,9 @@ def _process_rst_content(self, content: str) -> str:
8282
result_lines = []
8383
in_code_block = False
8484
in_license_header = False
85+
in_directive_block = False
8586
code_block_indent = 0
87+
directive_indent = 0
8688

8789
# Check if we start with a license header
8890
if lines and self._is_license_header_start(lines[0]):
@@ -100,6 +102,20 @@ def _process_rst_content(self, content: str) -> str:
100102
i += 1
101103
continue
102104

105+
# Handle RST lists (process entire list at once)
106+
if self._is_list_item(line):
107+
list_lines, next_i = self._collect_list(lines, i)
108+
result_lines.extend(list_lines)
109+
i = next_i
110+
continue
111+
112+
# Handle RST tables (process entire table at once)
113+
if self._is_table_line(line):
114+
table_lines, next_i = self._collect_table(lines, i)
115+
result_lines.extend(table_lines)
116+
i = next_i
117+
continue
118+
103119
# Handle code blocks
104120
if self._is_code_block_start(line):
105121
in_code_block = True
@@ -116,8 +132,24 @@ def _process_rst_content(self, content: str) -> str:
116132
i += 1
117133
continue
118134

119-
# Process regular content
120-
if not in_code_block and not in_license_header:
135+
# Handle RST directive blocks
136+
if self._is_rst_directive_start(line):
137+
in_directive_block = True
138+
directive_indent = self._get_indent_level(line)
139+
result_lines.append(line)
140+
i += 1
141+
continue
142+
143+
if in_directive_block:
144+
if self._is_directive_block_end(line, directive_indent):
145+
in_directive_block = False
146+
else:
147+
result_lines.append(line)
148+
i += 1
149+
continue
150+
151+
# Process regular content (only when not in special blocks)
152+
if not in_code_block and not in_license_header and not in_directive_block:
121153
paragraph_lines, next_i = self._collect_paragraph(lines, i)
122154
processed_lines = self._process_paragraph(paragraph_lines)
123155
result_lines.extend(processed_lines)
@@ -140,6 +172,151 @@ def _is_license_header_end(self, line: str, lines: List[str], index: int) -> boo
140172
and not lines[index + 1].strip().startswith("..")
141173
)
142174

175+
def _is_list_item(self, line: str) -> bool:
176+
"""Check if line is an RST list item."""
177+
stripped = line.strip()
178+
if not stripped:
179+
return False
180+
181+
# RST list patterns
182+
list_patterns = [
183+
r"^\s*\*\s+", # Bullet list: * item
184+
r"^\s*\+\s+", # Bullet list: + item
185+
r"^\s*-\s+", # Bullet list: - item
186+
r"^\s*\d+\.\s+", # Numbered list: 1. item
187+
r"^\s*#\.\s+", # Auto-numbered list: #. item
188+
r"^\s*\([a-zA-Z0-9]+\)\s+", # Parenthesized list: (a) item
189+
r"^\s*[a-zA-Z]\.\s+", # Letter list: a. item
190+
r"^\s*[IVX]+\.\s+", # Roman numeral list: I. item
191+
]
192+
193+
return any(re.match(pattern, line) for pattern in list_patterns)
194+
195+
def _collect_list(self, lines: List[str], start_idx: int) -> Tuple[List[str], int]:
196+
"""
197+
Collect all lines that are part of an RST list.
198+
199+
Args:
200+
lines: All lines in the document
201+
start_idx: Starting index
202+
203+
Returns:
204+
Tuple of (list_lines, next_index)
205+
"""
206+
list_lines = []
207+
i = start_idx
208+
base_indent = self._get_indent_level(lines[start_idx])
209+
210+
while i < len(lines):
211+
line = lines[i]
212+
213+
# If it's a list item at the same or deeper indentation, include it
214+
if self._is_list_item(line):
215+
current_indent = self._get_indent_level(line)
216+
if current_indent >= base_indent:
217+
list_lines.append(line)
218+
i += 1
219+
continue
220+
else:
221+
# List item at shallower indentation, end current list
222+
break
223+
224+
# If it's an empty line, check if the list continues
225+
if not line.strip():
226+
# Look ahead to see if list continues
227+
if i + 1 < len(lines):
228+
next_line = lines[i + 1]
229+
if (
230+
self._is_list_item(next_line)
231+
and self._get_indent_level(next_line) >= base_indent
232+
):
233+
list_lines.append(line) # Include the empty line
234+
i += 1
235+
continue
236+
elif (
237+
next_line.strip()
238+
and self._get_indent_level(next_line) > base_indent
239+
):
240+
# Continuation of list item content
241+
list_lines.append(line)
242+
i += 1
243+
continue
244+
# Empty line and no more list content, end list
245+
break
246+
247+
# If it's indented content (continuation of list item), include it
248+
current_indent = self._get_indent_level(line)
249+
if line.strip() and current_indent > base_indent:
250+
list_lines.append(line)
251+
i += 1
252+
continue
253+
254+
# If it's not a list item, not empty, and not indented continuation, end list
255+
break
256+
257+
return list_lines, i
258+
259+
def _is_table_line(self, line: str) -> bool:
260+
"""Check if line is part of an RST table."""
261+
stripped = line.strip()
262+
if not stripped:
263+
return False
264+
265+
# Grid table patterns
266+
# Lines made of =, -, +, and spaces (table borders)
267+
if re.match(r"^[=\-+\s]+$", stripped) and len(stripped) > 3:
268+
return True
269+
270+
# Simple table patterns (lines with multiple spaces that could be column separators)
271+
# But be more conservative - look for patterns that are clearly tabular
272+
if " " in stripped and not stripped.startswith(".."):
273+
# Check if it looks like a table row (has multiple column-like segments)
274+
segments = [s.strip() for s in stripped.split(" ") if s.strip()]
275+
if len(segments) >= 2:
276+
return True
277+
278+
return False
279+
280+
def _collect_table(self, lines: List[str], start_idx: int) -> Tuple[List[str], int]:
281+
"""
282+
Collect all lines that are part of an RST table.
283+
284+
Args:
285+
lines: All lines in the document
286+
start_idx: Starting index
287+
288+
Returns:
289+
Tuple of (table_lines, next_index)
290+
"""
291+
table_lines = []
292+
i = start_idx
293+
294+
# Collect all consecutive table-related lines
295+
while i < len(lines):
296+
line = lines[i]
297+
298+
# If it's a table line, include it
299+
if self._is_table_line(line):
300+
table_lines.append(line)
301+
i += 1
302+
continue
303+
304+
# If it's an empty line, check if the next line is also a table line
305+
if not line.strip():
306+
# Look ahead to see if table continues
307+
if i + 1 < len(lines) and self._is_table_line(lines[i + 1]):
308+
table_lines.append(line) # Include the empty line
309+
i += 1
310+
continue
311+
else:
312+
# Empty line and no more table content, end table
313+
break
314+
315+
# If it's not a table line and not empty, end table
316+
break
317+
318+
return table_lines, i
319+
143320
def _is_code_block_start(self, line: str) -> bool:
144321
"""Check if line starts a code block."""
145322
return bool(re.match(r"^\s*\.\.\s+(code-block|literalinclude)::", line))
@@ -151,6 +328,48 @@ def _is_code_block_end(self, line: str, code_block_indent: int) -> bool:
151328
current_indent = self._get_indent_level(line)
152329
return current_indent <= code_block_indent
153330

331+
def _is_rst_directive_start(self, line: str) -> bool:
332+
"""Check if line starts an RST directive that has indented content."""
333+
# Match RST directives that typically have indented content
334+
directive_patterns = [
335+
r"^\s*\.\.\s+list-table::",
336+
r"^\s*\.\.\s+table::",
337+
r"^\s*\.\.\s+csv-table::",
338+
r"^\s*\.\.\s+image::",
339+
r"^\s*\.\.\s+figure::",
340+
r"^\s*\.\.\s+note::",
341+
r"^\s*\.\.\s+warning::",
342+
r"^\s*\.\.\s+attention::",
343+
r"^\s*\.\.\s+caution::",
344+
r"^\s*\.\.\s+danger::",
345+
r"^\s*\.\.\s+error::",
346+
r"^\s*\.\.\s+hint::",
347+
r"^\s*\.\.\s+important::",
348+
r"^\s*\.\.\s+tip::",
349+
r"^\s*\.\.\s+admonition::",
350+
r"^\s*\.\.\s+sidebar::",
351+
r"^\s*\.\.\s+topic::",
352+
r"^\s*\.\.\s+rubric::",
353+
r"^\s*\.\.\s+epigraph::",
354+
r"^\s*\.\.\s+highlights::",
355+
r"^\s*\.\.\s+pull-quote::",
356+
r"^\s*\.\.\s+compound::",
357+
r"^\s*\.\.\s+container::",
358+
r"^\s*\.\.\s+raw::",
359+
r"^\s*\.\.\s+include::",
360+
r"^\s*\.\.\s+math::",
361+
r"^\s*\.\.\s+\w+::", # Generic directive pattern
362+
]
363+
364+
return any(re.match(pattern, line) for pattern in directive_patterns)
365+
366+
def _is_directive_block_end(self, line: str, directive_indent: int) -> bool:
367+
"""Check if directive block ends."""
368+
if not line.strip():
369+
return False
370+
current_indent = self._get_indent_level(line)
371+
return current_indent <= directive_indent
372+
154373
def _get_indent_level(self, line: str) -> int:
155374
"""Get the indentation level of a line."""
156375
return len(line) - len(line.lstrip())
@@ -189,6 +408,8 @@ def _collect_paragraph(
189408
if (
190409
not line.strip()
191410
or self._is_special_line(line)
411+
or self._is_table_line(line) # Stop at table lines
412+
or self._is_list_item(line) # Stop at list items
192413
or abs(self._get_indent_level(line) - base_indent) > 2
193414
):
194415
break
@@ -200,12 +421,12 @@ def _collect_paragraph(
200421

201422
def _is_special_line(self, line: str) -> bool:
202423
"""Check if a line is a special RST construct."""
424+
stripped = line.strip()
425+
203426
patterns = [
204427
r"^\.\.", # RST directives
205428
r'^[=\-~^"#*+<>]{3,}$', # RST headers
206429
r"^:", # RST fields
207-
r"^\s*\*\s", # List items
208-
r"^\s*\d+\.\s", # Numbered lists
209430
r"^\s*\.\.\s+_", # RST targets
210431
]
211432

@@ -309,6 +530,8 @@ def is_rst_file(file_path: Path) -> bool:
309530
except StopIteration:
310531
break
311532

533+
content = "".join(lines)
534+
312535
# Look for common RST patterns
313536
rst_patterns = [
314537
r"^\.\. ", # RST directives

tutorial_advanced_packaging.rst

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ Setup for the Tutorial
2323

2424
.. note::
2525

26-
We do not recommend doing this section of the tutorial in a production Spack instance.
26+
We do not recommend doing this section of the tutorial in a
27+
production Spack instance.
2728

2829
The tutorial uses custom package definitions with missing sections that will be filled in during the tutorial.
2930
These package definitions are stored in a separate package repository, which can be enabled with:
@@ -53,10 +54,11 @@ Now, you are ready to set your preferred ``EDITOR`` and continue with the rest o
5354

5455
.. note::
5556

56-
Several of these packages depend on an MPI implementation.
57-
You can use OpenMPI if you install it from scratch, but this is slow (>10 min.).
58-
A binary cache of MPICH may be provided, in which case you can force the package to use it and install quickly.
59-
All tutorial examples with packages that depend on MPICH include the spec syntax for building with it.
57+
Several of these packages depend on an MPI implementation. You can use
58+
OpenMPI if you install it from scratch, but this is slow (>10 min.).
59+
A binary cache of MPICH may be provided, in which case you can force
60+
the package to use it and install quickly. All tutorial examples with
61+
packages that depend on MPICH include the spec syntax for building with it.
6062

6163
.. _adv_pkg_tutorial_start:
6264

tutorial_basics.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,9 @@ If we install it "out of the box," it will build with OpenMPI.
171171
:language: console
172172

173173
Spack packages can also have build options, called variants.
174-
Boolean variants can be specified using the ``+`` (enable) and ``~`` or ``-`` (disable) sigils.
175-
There are two sigils for "disable" to avoid conflicts with shell parsing in different situations.
174+
Boolean variants can be specified using the ``+`` (enable) and ``~`` or ``-``
175+
(disable) sigils. There are two sigils for "disable" to avoid conflicts
176+
with shell parsing in different situations.
176177
Variants (boolean or otherwise) can also be specified using the same syntax as compiler flags.
177178
Here we can install HDF5 without MPI support.
178179

tutorial_binary_cache.rst

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -177,9 +177,10 @@ For convenience you can also run ``spack buildcache push --update-index ...`` to
177177

178178
.. note::
179179

180-
As of Spack 0.22, build caches can be used across different Linux distros.
181-
The concretizer will reuse specs that have a host compatible ``libc`` dependency (e.g. ``glibc`` or ``musl``).
182-
For packages compiled with ``gcc`` (and a few others), users do not have to install compilers first, as the build cache contains the compiler runtime libraries as a separate package.
180+
As of Spack 0.22, build caches can be used across different Linux distros. The concretizer
181+
will reuse specs that have a host compatible ``libc`` dependency (e.g. ``glibc`` or ``musl``).
182+
For packages compiled with ``gcc`` (and a few others), users do not have to install compilers
183+
first, as the build cache contains the compiler runtime libraries as a separate package.
183184

184185
After an index is created, it's possible to list the available packages in the build cache:
185186

@@ -247,7 +248,8 @@ Let's add a simple text editor like ``vim`` to our previous environment next to
247248

248249
.. note::
249250

250-
You may want to change ``mirrors::`` to ``mirrors:`` in the ``spack.yaml`` file to avoid a source build of ``vim`` --- but a source build should be quick.
251+
You may want to change ``mirrors::`` to ``mirrors:`` in the ``spack.yaml`` file to avoid
252+
a source build of ``vim`` --- but a source build should be quick.
251253

252254
.. code-block:: console
253255
@@ -290,10 +292,11 @@ For those familiar with ``Dockerfile`` syntax, it would structurally look like t
290292
This approach is still valid, and the ``spack containerize`` command continues to exist, but it has a few downsides:
291293

292294
* When ``RUN spack -e /root/env install`` fails, ``docker`` will not cache the layer, meaning
293-
that all dependencies that did install successfully are lost.
294-
Troubleshooting the build typically means starting from scratch in ``docker run`` or on the host system.
295+
that all dependencies that did install successfully are lost. Troubleshooting the build
296+
typically means starting from scratch in ``docker run`` or on the host system.
295297
* In certain CI environments, it is not possible to use ``docker build``. For example, the
296-
CI script itself may already run in a docker container, and running ``docker build`` *safely* inside a container is tricky.
298+
CI script itself may already run in a docker container, and running ``docker build`` *safely*
299+
inside a container is tricky.
297300

298301
The takeaway is that Spack decouples the steps that ``docker build`` combines: build isolation, running the build, and creating an image.
299302
You can run ``spack install`` on your host machine or in a container, and run ``spack buildcache push`` separately to create an image.

tutorial_buildsystems.rst

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,16 @@ Package Class Hierarchy
2929

3030
node [
3131
shape = "record"
32-
] edge [
32+
]
33+
edge [
3334
arrowhead = "empty"
3435
]
3536

36-
PackageBase -> Package [dir=back] PackageBase -> MakefilePackage [dir=back] PackageBase -> AutotoolsPackage [dir=back] PackageBase -> CMakePackage [dir=back] PackageBase -> PythonPackage [dir=back]
37+
PackageBase -> Package [dir=back]
38+
PackageBase -> MakefilePackage [dir=back]
39+
PackageBase -> AutotoolsPackage [dir=back]
40+
PackageBase -> CMakePackage [dir=back]
41+
PackageBase -> PythonPackage [dir=back]
3742
}
3843

3944
The above diagram gives a high level view of the class hierarchy and how each package relates.
@@ -90,8 +95,8 @@ Let's take a quick look at some the internals of the :code:`Autotools` class:
9095
This will open the :code:`AutotoolsPackage` file in your text editor.
9196

9297
.. note::
93-
The examples showing code for these classes is abridged to avoid having long examples.
94-
We only show what is relevant to the packager.
98+
The examples showing code for these classes is abridged to avoid having
99+
long examples. We only show what is relevant to the packager.
95100

96101

97102
.. literalinclude:: _spack_root/lib/spack/spack/build_systems/autotools.py

0 commit comments

Comments
 (0)