Reimplement table col/row conditionalization (r10)
Reimplement table column and row removal to be output format agnostic. Change-Id: I4822d53d37fd4604bf45c4bc4a315c8fc904376a Signed-off-by: Ron Stone <ronald.stone@windriver.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
476
remove-grid-columns.py
Normal file
476
remove-grid-columns.py
Normal file
@@ -0,0 +1,476 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script to remove columns from reStructuredText grid tables based on column names.
|
||||
Only processes grid tables that are declared using the .. table:: directive.
|
||||
Column names to remove are specified using field lists in the RST file itself.
|
||||
|
||||
Usage:
|
||||
python remove_rst_columns.py input.rst -o output.rst
|
||||
python remove_rst_columns.py input.rst --output output.rst
|
||||
|
||||
If no output file is specified, the result is printed to stdout.
|
||||
|
||||
The RST file should contain field list entries specifying columns to remove:
|
||||
:remove-column-from-html-table: Column1, Column2, Column3
|
||||
|
||||
Note: Only grid tables declared with the .. table:: directive will be modified.
|
||||
Standalone grid tables without the directive will be left unchanged.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import re
|
||||
import os
|
||||
from typing import List, Tuple, Optional
|
||||
|
||||
|
||||
def parse_grid_table(lines: List[str], start_idx: int) -> Tuple[List[List[List[str]]], List[int], int]:
|
||||
"""
|
||||
Parse a reStructuredText grid table starting at the given line index.
|
||||
|
||||
Returns:
|
||||
- table_data: List of rows, where each row is a list of cells,
|
||||
and each cell is a list of lines (to preserve multi-line content)
|
||||
- col_widths: List of column widths
|
||||
- end_idx: Index of the line after the table
|
||||
"""
|
||||
# Find the table boundaries
|
||||
table_lines = []
|
||||
i = start_idx
|
||||
|
||||
# Skip to first border line
|
||||
while i < len(lines) and not lines[i].strip().startswith('+'):
|
||||
i += 1
|
||||
|
||||
if i >= len(lines):
|
||||
return [], [], start_idx
|
||||
|
||||
# Collect all table lines
|
||||
while i < len(lines) and (lines[i].strip().startswith('+') or lines[i].strip().startswith('|')):
|
||||
table_lines.append(lines[i])
|
||||
i += 1
|
||||
|
||||
if not table_lines:
|
||||
return [], [], start_idx
|
||||
|
||||
# Parse column positions from the first border line
|
||||
border_line = table_lines[0]
|
||||
col_positions = []
|
||||
for match in re.finditer(r'\+', border_line):
|
||||
col_positions.append(match.start())
|
||||
|
||||
if len(col_positions) < 2:
|
||||
return [], [], i
|
||||
|
||||
# Calculate column widths
|
||||
col_widths = []
|
||||
for j in range(len(col_positions) - 1):
|
||||
col_widths.append(col_positions[j + 1] - col_positions[j] - 1)
|
||||
|
||||
# Parse table data
|
||||
table_data = []
|
||||
current_row = None
|
||||
|
||||
for line in table_lines:
|
||||
if line.strip().startswith('+'):
|
||||
# Border line - if we have a current row, add it to table_data
|
||||
if current_row is not None:
|
||||
table_data.append(current_row)
|
||||
current_row = None
|
||||
elif line.strip().startswith('|'):
|
||||
# Data line
|
||||
if current_row is None:
|
||||
current_row = [[] for _ in range(len(col_positions) - 1)]
|
||||
|
||||
# Extract cell contents
|
||||
for j in range(len(col_positions) - 1):
|
||||
start_pos = col_positions[j] + 1
|
||||
end_pos = col_positions[j + 1]
|
||||
cell_content = line[start_pos:end_pos].rstrip() # Only strip right whitespace
|
||||
|
||||
# Add this line to the cell (preserving empty lines and indentation)
|
||||
current_row[j].append(cell_content)
|
||||
|
||||
# Add the last row if it exists
|
||||
if current_row is not None:
|
||||
table_data.append(current_row)
|
||||
|
||||
return table_data, col_widths, i
|
||||
|
||||
|
||||
def find_column_removal_directives(lines: List[str]) -> List[str]:
|
||||
"""
|
||||
Find field list entries that specify columns to remove.
|
||||
Looks for entries like: :remove-column-from-html-table: Column1, Column2
|
||||
|
||||
Returns a list of column names to remove.
|
||||
"""
|
||||
columns_to_remove = []
|
||||
|
||||
for line in lines:
|
||||
stripped_line = line.strip()
|
||||
|
||||
# Look for the field list entry
|
||||
if stripped_line.startswith(':remove-column-from-html-table:'):
|
||||
# Extract the column names after the colon
|
||||
field_content = stripped_line[len(':remove-column-from-html-table:'):].strip()
|
||||
|
||||
if field_content:
|
||||
# Split by comma and clean up each column name
|
||||
column_names = [name.strip() for name in field_content.split(',')]
|
||||
columns_to_remove.extend([name for name in column_names if name])
|
||||
|
||||
return columns_to_remove
|
||||
|
||||
|
||||
def check_docs_build_context(lines: List[str]) -> None:
|
||||
"""
|
||||
Check if the docs-build-context directive matches the DOCS_BUILD_CONTEXT environment variable.
|
||||
Exit the script if they don't match.
|
||||
"""
|
||||
docs_context = None
|
||||
|
||||
for line in lines:
|
||||
stripped_line = line.strip()
|
||||
if stripped_line.startswith(':docs-build-context:'):
|
||||
field_content = stripped_line[len(':docs-build-context:'):].strip()
|
||||
if field_content:
|
||||
docs_context = field_content
|
||||
break
|
||||
|
||||
if docs_context is not None:
|
||||
env_context = os.environ.get('DOCS_BUILD_CONTEXT')
|
||||
if env_context != docs_context:
|
||||
print(f"Docs-build-context '{docs_context}' does not match DOCS_BUILD_CONTEXT environment variable '{env_context}'. Skipping.", file=sys.stderr)
|
||||
sys.exit(0)
|
||||
print(f"docs-build-context '{docs_context}' matches environment variable", file=sys.stderr)
|
||||
|
||||
|
||||
def should_remove_emptied_rows(lines: List[str]) -> bool:
|
||||
"""
|
||||
Check if the remove-column-emptied-row directive is set to 1.
|
||||
"""
|
||||
for line in lines:
|
||||
stripped_line = line.strip()
|
||||
if stripped_line.startswith(':remove-column-emptied-row:'):
|
||||
field_content = stripped_line[len(':remove-column-emptied-row:'):].strip()
|
||||
return field_content == '1'
|
||||
return False
|
||||
|
||||
|
||||
def find_first_table_position(lines: List[str]) -> int:
|
||||
"""
|
||||
Find the position of the first table in the document.
|
||||
Returns the line number of the first table, or len(lines) if no table found.
|
||||
"""
|
||||
for i, line in enumerate(lines):
|
||||
if line.strip().startswith('+') and '-' in line:
|
||||
# Check if this is actually a table by looking for the directive
|
||||
is_table, _, _, _ = find_table_directive(lines, i)
|
||||
if is_table:
|
||||
return i
|
||||
return len(lines)
|
||||
|
||||
|
||||
def find_column_indices(headers: List[List[str]], columns_to_remove: List[str]) -> List[int]:
|
||||
"""Find the indices of columns to remove based on header names."""
|
||||
indices = []
|
||||
|
||||
# Convert headers (which are lists of lines) to single strings for comparison
|
||||
header_strings = []
|
||||
for header_cell in headers:
|
||||
# Join all lines in the cell and strip whitespace
|
||||
header_text = '\n'.join(header_cell).strip()
|
||||
header_strings.append(header_text)
|
||||
|
||||
for col_name in columns_to_remove:
|
||||
# Try exact match first
|
||||
if col_name in header_strings:
|
||||
indices.append(header_strings.index(col_name))
|
||||
else:
|
||||
# Try case-insensitive match
|
||||
for i, header in enumerate(header_strings):
|
||||
if header.lower() == col_name.lower():
|
||||
indices.append(i)
|
||||
break
|
||||
else:
|
||||
print(f"Warning: Column '{col_name}' not found in table headers", file=sys.stderr)
|
||||
|
||||
return sorted(set(indices), reverse=True) # Remove duplicates and sort in reverse order
|
||||
|
||||
|
||||
def remove_columns_and_empty_rows(table_data: List[List[List[str]]], col_indices: List[int], remove_empty_rows: bool = False) -> List[List[List[str]]]:
|
||||
"""Remove specified columns from table data and optionally remove rows that become empty."""
|
||||
new_table = []
|
||||
for row in table_data:
|
||||
new_row = row.copy()
|
||||
for idx in col_indices:
|
||||
if idx < len(new_row):
|
||||
new_row.pop(idx)
|
||||
|
||||
# If remove_empty_rows is True, check if the row is now empty (all cells are empty or whitespace)
|
||||
if remove_empty_rows:
|
||||
row_is_empty = True
|
||||
for cell in new_row:
|
||||
cell_content = '\n'.join(cell).strip()
|
||||
if cell_content:
|
||||
row_is_empty = False
|
||||
break
|
||||
if row_is_empty:
|
||||
continue # Skip this row as it's now empty
|
||||
|
||||
new_table.append(new_row)
|
||||
return new_table
|
||||
|
||||
|
||||
def calculate_column_widths(table_data: List[List[List[str]]]) -> List[int]:
|
||||
"""Calculate the minimum width needed for each column, considering multi-line content."""
|
||||
if not table_data:
|
||||
return []
|
||||
|
||||
num_cols = len(table_data[0])
|
||||
col_widths = [0] * num_cols
|
||||
|
||||
for row in table_data:
|
||||
for i, cell in enumerate(row):
|
||||
if i < len(col_widths):
|
||||
# Find the maximum line width in this cell
|
||||
max_line_width = 0
|
||||
for line in cell:
|
||||
max_line_width = max(max_line_width, len(line.rstrip()))
|
||||
col_widths[i] = max(col_widths[i], max_line_width)
|
||||
|
||||
return col_widths
|
||||
|
||||
|
||||
def generate_border_line(col_widths: List[int], indent: str = '') -> str:
|
||||
"""Generate a border line for the table with proper indentation."""
|
||||
parts = ['+']
|
||||
for width in col_widths:
|
||||
parts.append('-' * width + '+')
|
||||
return indent + ''.join(parts)
|
||||
|
||||
|
||||
def generate_data_lines(row: List[List[str]], col_widths: List[int], indent: str = '') -> List[str]:
|
||||
"""Generate data lines for a table row, handling multi-line cells with proper indentation."""
|
||||
# Find the maximum number of lines in any cell of this row
|
||||
max_lines = max(len(cell) for cell in row) if row else 0
|
||||
|
||||
data_lines = []
|
||||
|
||||
for line_idx in range(max_lines):
|
||||
parts = ['|']
|
||||
for col_idx, cell in enumerate(row):
|
||||
if col_idx < len(col_widths):
|
||||
# Get the content for this line of the cell, or empty string if no more lines
|
||||
if line_idx < len(cell):
|
||||
cell_content = cell[line_idx].rstrip()
|
||||
else:
|
||||
cell_content = ''
|
||||
|
||||
# Pad the cell content to the column width
|
||||
padded_cell = cell_content.ljust(col_widths[col_idx])
|
||||
parts.append(padded_cell + '|')
|
||||
|
||||
data_lines.append(indent + ''.join(parts))
|
||||
|
||||
return data_lines
|
||||
|
||||
|
||||
def rebuild_table(table_data: List[List[List[str]]], indent: str = '') -> List[str]:
|
||||
"""Rebuild the grid table as a list of lines with proper indentation and preserved formatting."""
|
||||
if not table_data:
|
||||
return []
|
||||
|
||||
col_widths = calculate_column_widths(table_data)
|
||||
border_line = generate_border_line(col_widths, indent)
|
||||
|
||||
lines = [border_line]
|
||||
|
||||
for i, row in enumerate(table_data):
|
||||
data_lines = generate_data_lines(row, col_widths, indent)
|
||||
lines.extend(data_lines)
|
||||
lines.append(border_line)
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def find_table_directive(lines: List[str], start_idx: int) -> Tuple[bool, int, Optional[str], str]:
|
||||
"""
|
||||
Check if there's a .. table:: directive before the given grid table.
|
||||
The directive must be the first non-empty line when looking backwards from the table.
|
||||
|
||||
Returns:
|
||||
- is_table_directive: True if this grid table is declared with .. table::
|
||||
- directive_start: Index where the directive starts
|
||||
- table_title: Optional title from the directive
|
||||
- table_indent: The indentation string used for the table
|
||||
"""
|
||||
# Get the indentation of the table itself
|
||||
table_line = lines[start_idx]
|
||||
table_indent = ''
|
||||
for char in table_line:
|
||||
if char in ' \t':
|
||||
table_indent += char
|
||||
else:
|
||||
break
|
||||
|
||||
# Look backwards from the grid table start to find the first non-empty line
|
||||
i = start_idx - 1
|
||||
|
||||
# Skip empty lines immediately before the table
|
||||
while i >= 0 and lines[i].strip() == '':
|
||||
i -= 1
|
||||
|
||||
if i < 0:
|
||||
return False, -1, None, ''
|
||||
|
||||
# The first non-empty line must be the .. table:: directive
|
||||
line = lines[i]
|
||||
stripped_line = line.strip()
|
||||
|
||||
if stripped_line.startswith('.. table::') or \
|
||||
stripped_line.startswith(':header-rows') or \
|
||||
stripped_line.startswith(':widths'):
|
||||
# Found the directive as the first non-empty line
|
||||
table_title = None
|
||||
if len(stripped_line) > 10: # More than just ".. table::"
|
||||
table_title = stripped_line[10:].strip()
|
||||
return True, i, table_title, table_indent
|
||||
|
||||
# First non-empty line is not .. table:: directive
|
||||
return False, -1, None, ''
|
||||
|
||||
|
||||
def process_rst_file(content: str, columns_to_remove: List[str], remove_empty_rows: bool = False) -> str:
|
||||
"""Process the entire RST content and remove specified columns from grid tables declared with .. table:: directive."""
|
||||
lines = content.split('\n')
|
||||
result_lines = []
|
||||
i = 0
|
||||
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
|
||||
# Check if this line might be the start of a grid table
|
||||
if line.strip().startswith('+') and '-' in line:
|
||||
# Check if this grid table is declared with .. table:: directive
|
||||
is_directive_table, directive_start, table_title, table_indent = find_table_directive(lines, i)
|
||||
|
||||
if is_directive_table:
|
||||
# Try to parse as a grid table
|
||||
table_data, original_col_widths, end_idx = parse_grid_table(lines, i)
|
||||
|
||||
if table_data and len(table_data) > 0:
|
||||
# Assume first row contains headers
|
||||
headers = table_data[0]
|
||||
col_indices = find_column_indices(headers, columns_to_remove)
|
||||
|
||||
# Add any lines we haven't processed yet up to the current position
|
||||
while len(result_lines) < i:
|
||||
result_lines.append(lines[len(result_lines)])
|
||||
|
||||
if col_indices:
|
||||
# Remove specified columns and rebuild table with preserved indentation
|
||||
new_table_data = remove_columns_and_empty_rows(table_data, col_indices, remove_empty_rows)
|
||||
new_table_lines = rebuild_table(new_table_data, table_indent)
|
||||
result_lines.extend(new_table_lines)
|
||||
|
||||
title_info = f" ('{table_title}')" if table_title else ""
|
||||
removed_cols_info = ['\n'.join(headers[idx]).strip() for idx in sorted(col_indices)]
|
||||
empty_rows_info = " and empty rows" if remove_empty_rows else ""
|
||||
print(f"Processed table{title_info}: removed columns {removed_cols_info}{empty_rows_info}", file=sys.stderr)
|
||||
else:
|
||||
# No columns to remove, keep original table
|
||||
result_lines.extend(lines[i:end_idx])
|
||||
|
||||
i = end_idx
|
||||
else:
|
||||
# Not a valid grid table, keep the line
|
||||
result_lines.append(line)
|
||||
i += 1
|
||||
else:
|
||||
# Grid table not declared with .. table:: directive, keep it unchanged
|
||||
result_lines.append(line)
|
||||
i += 1
|
||||
else:
|
||||
# Regular line, keep it
|
||||
result_lines.append(line)
|
||||
i += 1
|
||||
|
||||
return '\n'.join(result_lines)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Remove columns from reStructuredText grid tables declared with .. table:: directive',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
%(prog)s input.rst -o output.rst
|
||||
%(prog)s input.rst --output output.rst
|
||||
cat input.rst | %(prog)s - > output.rst
|
||||
|
||||
The RST file should contain field list entries specifying columns to remove:
|
||||
:remove-column-from-html-table: Column1, Column2, Column3
|
||||
|
||||
Note: Only processes grid tables declared with the .. table:: directive.
|
||||
Standalone grid tables will be left unchanged.
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument('input',
|
||||
help='Input RST file (use "-" for stdin)')
|
||||
parser.add_argument('-o', '--output',
|
||||
help='Output file (default: stdout)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Read input
|
||||
if args.input == '-':
|
||||
content = sys.stdin.read()
|
||||
else:
|
||||
try:
|
||||
with open(args.input, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
except IOError as e:
|
||||
print(f"Error reading input file: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Find columns to remove from the content itself
|
||||
lines = content.split('\n')
|
||||
|
||||
# Check docs-build-context against environment variable
|
||||
check_docs_build_context(lines)
|
||||
|
||||
# Find columns to remove and whether to remove emptied rows
|
||||
columns_to_remove = find_column_removal_directives(lines)
|
||||
remove_empty_rows = should_remove_emptied_rows(lines)
|
||||
|
||||
if not columns_to_remove:
|
||||
print("No column removal directives found in the file.", file=sys.stderr)
|
||||
print("Looking for: :remove-column-from-html-table: Column1, Column2", file=sys.stderr)
|
||||
|
||||
if remove_empty_rows:
|
||||
print("Will remove rows that become empty after column removal.", file=sys.stderr)
|
||||
|
||||
# Process content
|
||||
try:
|
||||
result = process_rst_file(content, columns_to_remove, remove_empty_rows)
|
||||
except Exception as e:
|
||||
print(f"Error processing file: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Write output
|
||||
if args.output:
|
||||
try:
|
||||
with open(args.output, 'w', encoding='utf-8') as f:
|
||||
f.write(result)
|
||||
except IOError as e:
|
||||
print(f"Error writing output file: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(result, end='')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
153
remove-list-columns.py
Normal file
153
remove-list-columns.py
Normal file
@@ -0,0 +1,153 @@
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
def parse_meta_directives(lines):
|
||||
directives = {}
|
||||
for line in lines:
|
||||
if ":remove-column-from-html-table:" in line:
|
||||
directives["remove_column"] = line.split(":", 2)[2].strip()
|
||||
if ":remove-column-emptied-row:" in line:
|
||||
directives["remove_emptied_row"] = line.split(":", 2)[2].strip() == "1"
|
||||
return directives
|
||||
|
||||
def extract_table_blocks(lines):
|
||||
blocks = []
|
||||
current = []
|
||||
inside = False
|
||||
for line in lines:
|
||||
if line.strip().startswith(".. list-table::"):
|
||||
inside = True
|
||||
current = [line]
|
||||
elif inside and line.startswith(" ") or line.strip() == "":
|
||||
current.append(line)
|
||||
elif inside:
|
||||
blocks.append(current)
|
||||
inside = False
|
||||
if inside:
|
||||
blocks.append(current)
|
||||
return blocks
|
||||
|
||||
def split_table_row(row_lines):
|
||||
"""Splits a table row (beginning with '*') into a list of cells."""
|
||||
cells = []
|
||||
current_cell = []
|
||||
for line in row_lines:
|
||||
if re.match(r'^\s*\*\s+-', line): # First cell in row
|
||||
parts = re.split(r'\s*\*\s+-\s*', line, maxsplit=1)
|
||||
current_cell = [parts[1]]
|
||||
elif re.match(r'^\s*-\s+', line): # New cell
|
||||
cells.append(current_cell)
|
||||
current_cell = [line.strip()[2:]]
|
||||
else:
|
||||
current_cell.append(line.strip())
|
||||
cells.append(current_cell)
|
||||
return cells
|
||||
|
||||
def join_cells(cells, base_indent=" "):
|
||||
"""Reconstructs a list-table row from cell lists."""
|
||||
line = f"{base_indent}* - " + cells[0][0]
|
||||
lines = [line]
|
||||
for line in cells[0][1:]:
|
||||
lines.append(base_indent + " " + line)
|
||||
for cell in cells[1:]:
|
||||
lines.append(base_indent + " - " + cell[0])
|
||||
for l in cell[1:]:
|
||||
lines.append(base_indent + " " + l)
|
||||
return lines
|
||||
|
||||
def process_table(table_lines, col_to_remove, remove_empty_row=False):
|
||||
processed = []
|
||||
table_rows = []
|
||||
header_index = -1
|
||||
header_row = []
|
||||
buffer = []
|
||||
|
||||
for line in table_lines:
|
||||
if re.match(r'\s*\*\s+-', line):
|
||||
if buffer:
|
||||
table_rows.append(buffer)
|
||||
buffer = [line]
|
||||
elif buffer != [] and (line.strip() == "" or re.match(r'^\s*(-|[^*].*)$', line)):
|
||||
buffer.append(line)
|
||||
else:
|
||||
if buffer:
|
||||
table_rows.append(buffer)
|
||||
buffer = []
|
||||
processed.append(line)
|
||||
|
||||
if buffer:
|
||||
table_rows.append(buffer)
|
||||
|
||||
# Parse header row
|
||||
for i, row in enumerate(table_rows):
|
||||
if i == 0:
|
||||
cells = split_table_row(row)
|
||||
flat_cells = [' '.join(c).strip() for c in cells]
|
||||
if col_to_remove not in flat_cells:
|
||||
return table_lines # Don't modify
|
||||
header_index = flat_cells.index(col_to_remove)
|
||||
header_row = cells
|
||||
break
|
||||
|
||||
if header_index == -1:
|
||||
return table_lines # Don't modify
|
||||
|
||||
# Remove the column from each row
|
||||
new_rows = []
|
||||
for row in table_rows:
|
||||
cells = split_table_row(row)
|
||||
if header_index >= len(cells):
|
||||
continue
|
||||
if remove_empty_row and all(not ''.join(cell).strip() for cell in cells[:header_index] + cells[header_index+1:]):
|
||||
continue
|
||||
del cells[header_index]
|
||||
new_rows.append(join_cells(cells))
|
||||
|
||||
return processed + [""] + [line for row in new_rows for line in row]
|
||||
|
||||
def process_file(path):
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
directives = parse_meta_directives(lines)
|
||||
if "remove_column" not in directives:
|
||||
return
|
||||
|
||||
table_blocks = extract_table_blocks(lines)
|
||||
output_lines = []
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
if line.strip().startswith(".. list-table::"):
|
||||
# Find the table block and replace
|
||||
for block in table_blocks:
|
||||
if lines[i:i+len(block)] == block:
|
||||
processed = process_table(
|
||||
block,
|
||||
directives["remove_column"],
|
||||
directives.get("remove_emptied_row", False)
|
||||
)
|
||||
output_lines.extend(processed)
|
||||
i += len(block)
|
||||
break
|
||||
else:
|
||||
output_lines.append(line)
|
||||
i += 1
|
||||
|
||||
with open(path, 'w', encoding='utf-8') as f:
|
||||
f.writelines(l + ("\n" if not l.endswith("\n") else "") for l in output_lines)
|
||||
|
||||
def scan_dir(directory):
|
||||
for root, _, files in os.walk(directory):
|
||||
for name in files:
|
||||
if name.endswith(".rst"):
|
||||
process_file(os.path.join(root, name))
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: python remove-columns.py <directory>")
|
||||
sys.exit(1)
|
||||
process_file(sys.argv[1])
|
||||
# scan_dir(sys.argv[1])
|
||||
|
68
remove_empty-grid_rows.py
Normal file
68
remove_empty-grid_rows.py
Normal file
@@ -0,0 +1,68 @@
|
||||
import sys
|
||||
import re
|
||||
|
||||
def is_grid_table_line(line):
|
||||
return re.match(r'^\+[-=+]+$', line.strip()) is not None
|
||||
|
||||
def is_grid_row_empty(row):
|
||||
# A grid row is considered empty if the cells contain only whitespace or are just column dividers
|
||||
content = re.sub(r'[+|]', '', row)
|
||||
return content.strip() == ''
|
||||
|
||||
|
||||
def remove_empty_grid_rows(lines):
|
||||
result = []
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
|
||||
if is_grid_table_line(line):
|
||||
result.append(line)
|
||||
|
||||
# Check if there's a data line and a closing border after this
|
||||
if i + 2 < len(lines):
|
||||
data_line = lines[i + 1]
|
||||
next_border = lines[i + 2]
|
||||
|
||||
if is_grid_table_line(next_border):
|
||||
if is_grid_row_empty(data_line):
|
||||
# Skip this row (data + border)
|
||||
i += 3
|
||||
continue
|
||||
else:
|
||||
# Keep data row and closing border
|
||||
result.append(data_line)
|
||||
result.append(next_border)
|
||||
i += 3
|
||||
continue
|
||||
else:
|
||||
# Not a properly structured row — just advance
|
||||
i += 1
|
||||
else:
|
||||
# Not enough lines ahead — just advance
|
||||
i += 1
|
||||
else:
|
||||
result.append(line)
|
||||
i += 1
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def remove_empty_table_rows(filepath):
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
lines = remove_empty_grid_rows(lines)
|
||||
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
f.writelines(lines)
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: python remove_empty-grid_rows.py path/to/file.rst")
|
||||
sys.exit(1)
|
||||
|
||||
filepath = sys.argv[1]
|
||||
remove_empty_table_rows(filepath)
|
||||
print(f"Processed: {filepath}")
|
||||
|
203
remove_empty-list_rows.py
Normal file
203
remove_empty-list_rows.py
Normal file
@@ -0,0 +1,203 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script to remove empty rows from reStructuredText grid tables and list tables.
|
||||
Supports both grid tables (with +---+ borders) and simple list tables.
|
||||
"""
|
||||
|
||||
import re
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def is_list_row_empty(row_lines):
|
||||
"""
|
||||
Check if a list table row contains only empty cells.
|
||||
|
||||
Args:
|
||||
row_lines (list): Lines that make up the row
|
||||
|
||||
Returns:
|
||||
bool: True if row contains only empty cells, False otherwise
|
||||
"""
|
||||
# Join all lines and remove list markers
|
||||
content = ' '.join(row_lines)
|
||||
# Remove the first list marker
|
||||
content = re.sub(r'^\s*\*\s*-\s*', '', content)
|
||||
# Remove additional cell separators
|
||||
content = re.sub(r'\s*-\s*', ' ', content)
|
||||
# Check if anything meaningful remains
|
||||
return not content.strip()
|
||||
|
||||
|
||||
def clean_list_table(table_text):
|
||||
"""
|
||||
Remove empty rows from a reStructuredText list table.
|
||||
|
||||
Args:
|
||||
table_text (str): The complete list table text
|
||||
|
||||
Returns:
|
||||
str: Cleaned table text with empty rows removed
|
||||
"""
|
||||
lines = table_text.split('\n')
|
||||
cleaned_lines = []
|
||||
|
||||
current_row_lines = []
|
||||
in_row = False
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
# Check if this line starts a new row (begins with *)
|
||||
if re.match(r'^\s*\*\s*-', line):
|
||||
# Process previous row if it exists
|
||||
if current_row_lines:
|
||||
if not is_list_row_empty(current_row_lines):
|
||||
cleaned_lines.extend(current_row_lines)
|
||||
current_row_lines = []
|
||||
|
||||
# Start new row
|
||||
current_row_lines = [line]
|
||||
in_row = True
|
||||
elif in_row and re.match(r'^\s+-', line):
|
||||
# This is a continuation of the current row (additional columns)
|
||||
current_row_lines.append(line)
|
||||
elif in_row and line.strip() == '':
|
||||
# Empty line might end the row, but could also be within row
|
||||
# Look ahead to see if next line continues the row
|
||||
if i + 1 < len(lines) and re.match(r'^\s*-', lines[i + 1]):
|
||||
current_row_lines.append(line) # Part of current row
|
||||
else:
|
||||
# End of row
|
||||
if not is_list_row_empty(current_row_lines):
|
||||
cleaned_lines.extend(current_row_lines)
|
||||
current_row_lines = []
|
||||
cleaned_lines.append(line)
|
||||
in_row = False
|
||||
elif in_row and re.match(r'^\s+\S', line):
|
||||
# Multi-line cell content
|
||||
current_row_lines.append(line)
|
||||
else:
|
||||
# Not in a row, or row ended
|
||||
if current_row_lines:
|
||||
if not is_list_row_empty(current_row_lines):
|
||||
cleaned_lines.extend(current_row_lines)
|
||||
current_row_lines = []
|
||||
cleaned_lines.append(line)
|
||||
in_row = False
|
||||
|
||||
# Handle the last row
|
||||
if current_row_lines and not is_list_row_empty(current_row_lines):
|
||||
cleaned_lines.extend(current_row_lines)
|
||||
|
||||
return '\n'.join(cleaned_lines)
|
||||
|
||||
|
||||
def process_content(content):
|
||||
"""
|
||||
Process the entire content, finding and cleaning all tables.
|
||||
|
||||
Args:
|
||||
content (str): Full document content
|
||||
|
||||
Returns:
|
||||
str: Content with cleaned tables
|
||||
"""
|
||||
lines = content.split('\n')
|
||||
result_lines = []
|
||||
i = 0
|
||||
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
|
||||
# Check for list table start
|
||||
if re.match(r'^\s*\.\.\s+(list-table::|table::)', line):
|
||||
# Found list table directive, collect the entire table
|
||||
table_lines = [line]
|
||||
i += 1
|
||||
|
||||
# Collect all indented lines that belong to this directive
|
||||
while i < len(lines):
|
||||
current_line = lines[i]
|
||||
if (current_line.strip() == '' or
|
||||
current_line.startswith(' ') or
|
||||
current_line.startswith('\t')):
|
||||
table_lines.append(current_line)
|
||||
i += 1
|
||||
elif re.match(r'^\s*\*\s*-', current_line):
|
||||
# This is a list table row
|
||||
table_lines.append(current_line)
|
||||
i += 1
|
||||
else:
|
||||
break
|
||||
|
||||
# Clean the list table
|
||||
table_text = '\n'.join(table_lines)
|
||||
cleaned_table = clean_list_table(table_text)
|
||||
result_lines.extend(cleaned_table.split('\n'))
|
||||
|
||||
else:
|
||||
result_lines.append(line)
|
||||
i += 1
|
||||
|
||||
return '\n'.join(result_lines)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Remove empty rows from reStructuredText tables'
|
||||
)
|
||||
parser.add_argument(
|
||||
'input_file',
|
||||
help='Input .rst file path'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-o', '--output',
|
||||
help='Output file path (default: overwrite input file)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--dry-run',
|
||||
action='store_true',
|
||||
help='Show what would be changed without modifying files'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Read input file
|
||||
try:
|
||||
input_path = Path(args.input_file)
|
||||
with open(input_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
except FileNotFoundError:
|
||||
print(f"Error: File '{args.input_file}' not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"Error reading file: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Clean the content
|
||||
cleaned_content = process_content(content)
|
||||
|
||||
# Handle output
|
||||
if args.dry_run:
|
||||
print("=== DRY RUN MODE ===")
|
||||
if cleaned_content != content:
|
||||
print("Changes would be made:")
|
||||
print("=" * 50)
|
||||
print(cleaned_content)
|
||||
print("=" * 50)
|
||||
else:
|
||||
print("No changes needed.")
|
||||
else:
|
||||
output_path = Path(args.output) if args.output else input_path
|
||||
|
||||
try:
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
f.write(cleaned_content)
|
||||
print(f"Successfully cleaned tables in '{output_path}'")
|
||||
except Exception as e:
|
||||
print(f"Error writing file: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
8
tox.ini
8
tox.ini
@@ -11,6 +11,8 @@ setenv = VIRTUAL_ENV={envdir}
|
||||
OS_TEST_TIMEOUT=60
|
||||
LC_ALL=C
|
||||
DOCS_BUILD_CONTEXT=starlingx
|
||||
drop_table_cols = {env:DROP_TABLE_COLS:bash -c 'for f in $(grep -rl -e :remove-column-from-html-table: doc/source/*); do python3 remove-list-columns.py "$f" -o "$f"; python3 remove-grid-columns.py "$f" -o "$f"; done'}
|
||||
drop_table_rows = {env:DROP_TABLE_ROWS:bash -c 'for f in $(grep -rl -e :remove-empty-table-rows: doc/source/*); do python3 remove_empty-list_rows.py "$f" -o "$f"; python3 remove_empty-grid_rows.py "$f" -o "$f"; done'}
|
||||
deps = -r{toxinidir}/test-requirements.txt
|
||||
|
||||
[testenv:prebuild-docs]
|
||||
@@ -27,6 +29,8 @@ commands =
|
||||
bash ./fetch-ports-files.sh
|
||||
python py_2_xlsx.py tmp/platform_firewall.py tmp/constants.py tmp/FW_PORTS.xlsx
|
||||
python xlst_2_csv.py tmp/FW_PORTS.xlsx doc/source/shared/FW_PORTS.csv --columns Source Port Protocol Network Desc HTTPS Note _stx --sort_orders Port=asc --filters _stx=y
|
||||
{[testenv]drop_table_cols}
|
||||
{[testenv]drop_table_rows}
|
||||
|
||||
[testenv:postbuild-docs]
|
||||
commands =
|
||||
@@ -34,8 +38,8 @@ commands =
|
||||
git restore doc/source/dist_cloud/kubernetes/*
|
||||
bash hw-updates.sh
|
||||
# bash hide-empty-rows.sh doc/build/html
|
||||
bash -c 'python hide-empty-rows.py $(grep -rl --include="*.html" "post-build-hide-empty-table-rows" doc/build/html/*)'
|
||||
bash -c 'python hide-table-columns.py $(grep -rl --include=*.html --exclude=doc_contribute_guide.html remove-column-from-html-table doc/build/html/*)'
|
||||
# bash -c 'python hide-empty-rows.py $(grep -rl --include="*.html" "post-build-hide-empty-table-rows" doc/build/html/*)'
|
||||
# bash -c 'python hide-table-columns.py $(grep -rl --include=*.html --exclude=doc_contribute_guide.html remove-column-from-html-table doc/build/html/*)'
|
||||
bash htmlChecks.sh doc/build/html
|
||||
|
||||
[testenv:docs]
|
||||
|
Reference in New Issue
Block a user