aspose-cells-foss 25.12.1__py3-none-any.whl → 26.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aspose_cells/__init__.py +88 -0
- aspose_cells/auto_filter.py +527 -0
- aspose_cells/cell.py +483 -0
- aspose_cells/cell_value_handler.py +319 -0
- aspose_cells/cells.py +779 -0
- aspose_cells/cfb_handler.py +445 -0
- aspose_cells/cfb_writer.py +659 -0
- aspose_cells/cfb_writer_minimal.py +337 -0
- aspose_cells/comment_xml.py +475 -0
- aspose_cells/conditional_format.py +1185 -0
- aspose_cells/csv_handler.py +690 -0
- aspose_cells/data_validation.py +911 -0
- aspose_cells/document_properties.py +356 -0
- aspose_cells/encryption_crypto.py +247 -0
- aspose_cells/encryption_params.py +138 -0
- aspose_cells/hyperlink.py +372 -0
- aspose_cells/json_handler.py +185 -0
- aspose_cells/markdown_handler.py +583 -0
- aspose_cells/shared_strings.py +101 -0
- aspose_cells/style.py +841 -0
- aspose_cells/workbook.py +499 -0
- aspose_cells/workbook_hash_password.py +68 -0
- aspose_cells/workbook_properties.py +712 -0
- aspose_cells/worksheet.py +570 -0
- aspose_cells/worksheet_properties.py +1239 -0
- aspose_cells/xlsx_encryptor.py +403 -0
- aspose_cells/xml_autofilter_loader.py +195 -0
- aspose_cells/xml_autofilter_saver.py +173 -0
- aspose_cells/xml_conditional_format_loader.py +215 -0
- aspose_cells/xml_conditional_format_saver.py +351 -0
- aspose_cells/xml_datavalidation_loader.py +239 -0
- aspose_cells/xml_datavalidation_saver.py +245 -0
- aspose_cells/xml_hyperlink_handler.py +323 -0
- aspose_cells/xml_loader.py +986 -0
- aspose_cells/xml_properties_loader.py +512 -0
- aspose_cells/xml_properties_saver.py +607 -0
- aspose_cells/xml_saver.py +1306 -0
- aspose_cells_foss-26.2.2.dist-info/METADATA +190 -0
- aspose_cells_foss-26.2.2.dist-info/RECORD +41 -0
- {aspose_cells_foss-25.12.1.dist-info → aspose_cells_foss-26.2.2.dist-info}/WHEEL +1 -1
- aspose_cells_foss-26.2.2.dist-info/top_level.txt +1 -0
- aspose/__init__.py +0 -14
- aspose/cells/__init__.py +0 -31
- aspose/cells/cell.py +0 -350
- aspose/cells/constants.py +0 -44
- aspose/cells/converters/__init__.py +0 -13
- aspose/cells/converters/csv_converter.py +0 -55
- aspose/cells/converters/json_converter.py +0 -46
- aspose/cells/converters/markdown_converter.py +0 -453
- aspose/cells/drawing/__init__.py +0 -17
- aspose/cells/drawing/anchor.py +0 -172
- aspose/cells/drawing/collection.py +0 -233
- aspose/cells/drawing/image.py +0 -338
- aspose/cells/formats.py +0 -80
- aspose/cells/formula/__init__.py +0 -10
- aspose/cells/formula/evaluator.py +0 -360
- aspose/cells/formula/functions.py +0 -433
- aspose/cells/formula/tokenizer.py +0 -340
- aspose/cells/io/__init__.py +0 -27
- aspose/cells/io/csv/__init__.py +0 -8
- aspose/cells/io/csv/reader.py +0 -88
- aspose/cells/io/csv/writer.py +0 -98
- aspose/cells/io/factory.py +0 -138
- aspose/cells/io/interfaces.py +0 -48
- aspose/cells/io/json/__init__.py +0 -8
- aspose/cells/io/json/reader.py +0 -126
- aspose/cells/io/json/writer.py +0 -119
- aspose/cells/io/md/__init__.py +0 -8
- aspose/cells/io/md/reader.py +0 -161
- aspose/cells/io/md/writer.py +0 -334
- aspose/cells/io/models.py +0 -64
- aspose/cells/io/xlsx/__init__.py +0 -9
- aspose/cells/io/xlsx/constants.py +0 -312
- aspose/cells/io/xlsx/image_writer.py +0 -311
- aspose/cells/io/xlsx/reader.py +0 -284
- aspose/cells/io/xlsx/writer.py +0 -931
- aspose/cells/plugins/__init__.py +0 -6
- aspose/cells/plugins/docling_backend/__init__.py +0 -7
- aspose/cells/plugins/docling_backend/backend.py +0 -535
- aspose/cells/plugins/markitdown_plugin/__init__.py +0 -15
- aspose/cells/plugins/markitdown_plugin/plugin.py +0 -128
- aspose/cells/range.py +0 -210
- aspose/cells/style.py +0 -287
- aspose/cells/utils/__init__.py +0 -54
- aspose/cells/utils/coordinates.py +0 -68
- aspose/cells/utils/exceptions.py +0 -43
- aspose/cells/utils/validation.py +0 -102
- aspose/cells/workbook.py +0 -352
- aspose/cells/worksheet.py +0 -670
- aspose_cells_foss-25.12.1.dist-info/METADATA +0 -189
- aspose_cells_foss-25.12.1.dist-info/RECORD +0 -53
- aspose_cells_foss-25.12.1.dist-info/entry_points.txt +0 -2
- aspose_cells_foss-25.12.1.dist-info/top_level.txt +0 -1
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Aspose.Cells for Python - Markdown Handler Module
|
|
3
|
+
|
|
4
|
+
This module provides Markdown export functionality for workbooks.
|
|
5
|
+
It supports exporting worksheet data as Markdown tables with configurable
|
|
6
|
+
alignment, formatting, and styling options.
|
|
7
|
+
|
|
8
|
+
Features:
|
|
9
|
+
- Export worksheet data to Markdown table format
|
|
10
|
+
- Configurable column alignment (left, center, right)
|
|
11
|
+
- Support for multiple worksheets export
|
|
12
|
+
- Customizable date/time formatting
|
|
13
|
+
- Option to include worksheet names as headers
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import io
|
|
17
|
+
from datetime import datetime, date, time
|
|
18
|
+
from typing import Optional, List, Any, Dict
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MarkdownSaveOptions:
|
|
22
|
+
"""
|
|
23
|
+
Options for saving Markdown files.
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
encoding (str): File encoding. Default is 'utf-8'.
|
|
27
|
+
worksheet_index (int): Index of worksheet to export (-1 for all). Default is 0.
|
|
28
|
+
include_worksheet_name (bool): Include worksheet name as header. Default is True.
|
|
29
|
+
header_level (int): Markdown header level for worksheet name (1-6). Default is 2.
|
|
30
|
+
column_alignments (dict): Column alignment overrides {col_index: 'left'|'center'|'right'}.
|
|
31
|
+
default_alignment (str): Default column alignment. Default is 'left'.
|
|
32
|
+
date_format (str): Format string for date values. Default is '%Y-%m-%d'.
|
|
33
|
+
datetime_format (str): Format string for datetime values. Default is '%Y-%m-%d %H:%M:%S'.
|
|
34
|
+
time_format (str): Format string for time values. Default is '%H:%M:%S'.
|
|
35
|
+
empty_cell_placeholder (str): Text for empty cells. Default is '' (empty string).
|
|
36
|
+
escape_pipes (bool): Escape pipe characters in cell values. Default is True.
|
|
37
|
+
first_row_as_header (bool): Treat first row as table header. Default is True.
|
|
38
|
+
include_row_numbers (bool): Include row numbers as first column. Default is False.
|
|
39
|
+
max_column_width (int): Maximum column width (0 for unlimited). Default is 0.
|
|
40
|
+
trim_whitespace (bool): Trim whitespace from cell values. Default is True.
|
|
41
|
+
float_precision (int): Decimal places for floats (-1 for auto/smart rounding). Default is -1.
|
|
42
|
+
skip_empty_rows (bool): Skip rows where all cells are empty. Default is False.
|
|
43
|
+
newline_replacement (str): Replace newlines in cell values with this string. Default is ' '.
|
|
44
|
+
detect_title_rows (bool): Detect single-cell rows and output as headings. Default is False.
|
|
45
|
+
auto_detect_header (bool): Auto-detect actual header row (skip title rows). Default is False.
|
|
46
|
+
compact_format (bool): Use compact format without cell padding. Default is False.
|
|
47
|
+
simple_separators (bool): Use simple '---' separators without alignment colons. Default is False.
|
|
48
|
+
|
|
49
|
+
Examples:
|
|
50
|
+
>>> options = MarkdownSaveOptions()
|
|
51
|
+
>>> options.default_alignment = 'center'
|
|
52
|
+
>>> options.include_worksheet_name = True
|
|
53
|
+
>>> MarkdownHandler.save_markdown(workbook, 'output.md', options)
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self):
|
|
57
|
+
self.encoding = 'utf-8'
|
|
58
|
+
self.worksheet_index = -1
|
|
59
|
+
self.include_worksheet_name = True
|
|
60
|
+
self.header_level = 2
|
|
61
|
+
self.column_alignments: Dict[int, str] = {}
|
|
62
|
+
self.default_alignment = 'left'
|
|
63
|
+
self.date_format = '%Y-%m-%d'
|
|
64
|
+
self.datetime_format = '%Y-%m-%d %H:%M:%S'
|
|
65
|
+
self.time_format = '%H:%M:%S'
|
|
66
|
+
self.empty_cell_placeholder = ''
|
|
67
|
+
self.escape_pipes = True
|
|
68
|
+
self.first_row_as_header = True
|
|
69
|
+
self.include_row_numbers = False
|
|
70
|
+
self.max_column_width = 0
|
|
71
|
+
self.trim_whitespace = True
|
|
72
|
+
self.float_precision = -1 # -1 means auto (remove trailing zeros), 0+ means fixed decimals
|
|
73
|
+
self.skip_empty_rows = False # Skip rows that are entirely empty
|
|
74
|
+
self.newline_replacement = ' ' # Replace newlines in cell values with this string
|
|
75
|
+
self.detect_title_rows = False # Detect single-cell rows and output as headings
|
|
76
|
+
self.auto_detect_header = False # Auto-detect actual header row (skip title rows)
|
|
77
|
+
self.compact_format = True # Use compact format without cell padding
|
|
78
|
+
self.simple_separators = False # Use simple '---' separators without alignment colons
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class MarkdownHandler:
|
|
82
|
+
"""
|
|
83
|
+
Handles Markdown export operations for workbooks.
|
|
84
|
+
|
|
85
|
+
This class provides static methods to export workbook data to Markdown format,
|
|
86
|
+
creating properly formatted Markdown tables.
|
|
87
|
+
|
|
88
|
+
Examples:
|
|
89
|
+
# Export workbook to Markdown
|
|
90
|
+
>>> wb = Workbook('data.xlsx')
|
|
91
|
+
>>> MarkdownHandler.save_markdown(wb, 'output.md')
|
|
92
|
+
|
|
93
|
+
# Export with custom options
|
|
94
|
+
>>> options = MarkdownSaveOptions()
|
|
95
|
+
>>> options.default_alignment = 'center'
|
|
96
|
+
>>> MarkdownHandler.save_markdown(wb, 'output.md', options)
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
@staticmethod
|
|
100
|
+
def save_markdown(workbook, file_path: str, options: Optional[MarkdownSaveOptions] = None) -> None:
|
|
101
|
+
"""
|
|
102
|
+
Saves a workbook to a Markdown file.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
workbook: The Workbook object to export.
|
|
106
|
+
file_path (str): Path where the Markdown file should be saved.
|
|
107
|
+
options (MarkdownSaveOptions, optional): Export options. Uses defaults if None.
|
|
108
|
+
|
|
109
|
+
Raises:
|
|
110
|
+
IndexError: If the specified worksheet index is out of range.
|
|
111
|
+
IOError: If the file cannot be written.
|
|
112
|
+
|
|
113
|
+
Examples:
|
|
114
|
+
>>> wb = Workbook('data.xlsx')
|
|
115
|
+
>>> MarkdownHandler.save_markdown(wb, 'output.md')
|
|
116
|
+
|
|
117
|
+
>>> options = MarkdownSaveOptions()
|
|
118
|
+
>>> options.include_worksheet_name = False
|
|
119
|
+
>>> MarkdownHandler.save_markdown(wb, 'output.md', options)
|
|
120
|
+
"""
|
|
121
|
+
if options is None:
|
|
122
|
+
options = MarkdownSaveOptions()
|
|
123
|
+
|
|
124
|
+
content = MarkdownHandler.save_markdown_to_string(workbook, options)
|
|
125
|
+
|
|
126
|
+
with open(file_path, 'w', encoding=options.encoding) as f:
|
|
127
|
+
f.write(content)
|
|
128
|
+
|
|
129
|
+
@staticmethod
|
|
130
|
+
def save_markdown_to_string(workbook, options: Optional[MarkdownSaveOptions] = None) -> str:
|
|
131
|
+
"""
|
|
132
|
+
Saves a workbook to a Markdown string.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
workbook: The Workbook object to export.
|
|
136
|
+
options (MarkdownSaveOptions, optional): Export options. Uses defaults if None.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
str: The Markdown content as a string.
|
|
140
|
+
|
|
141
|
+
Examples:
|
|
142
|
+
>>> wb = Workbook('data.xlsx')
|
|
143
|
+
>>> md_content = MarkdownHandler.save_markdown_to_string(wb)
|
|
144
|
+
"""
|
|
145
|
+
if options is None:
|
|
146
|
+
options = MarkdownSaveOptions()
|
|
147
|
+
|
|
148
|
+
output = io.StringIO()
|
|
149
|
+
|
|
150
|
+
# Determine which worksheets to export
|
|
151
|
+
if options.worksheet_index == -1:
|
|
152
|
+
# Export all worksheets
|
|
153
|
+
worksheets = [(i, ws) for i, ws in enumerate(workbook.worksheets)]
|
|
154
|
+
else:
|
|
155
|
+
if options.worksheet_index >= len(workbook.worksheets):
|
|
156
|
+
raise IndexError(f"Worksheet index {options.worksheet_index} out of range")
|
|
157
|
+
worksheets = [(options.worksheet_index, workbook.worksheets[options.worksheet_index])]
|
|
158
|
+
|
|
159
|
+
for idx, (ws_idx, worksheet) in enumerate(worksheets):
|
|
160
|
+
if idx > 0:
|
|
161
|
+
output.write('\n\n')
|
|
162
|
+
|
|
163
|
+
# Write worksheet name as header if enabled
|
|
164
|
+
if options.include_worksheet_name:
|
|
165
|
+
header_prefix = '#' * options.header_level
|
|
166
|
+
output.write(f"{header_prefix} {worksheet.name}\n\n")
|
|
167
|
+
|
|
168
|
+
# Get worksheet data
|
|
169
|
+
rows_data = MarkdownHandler._get_worksheet_data(worksheet)
|
|
170
|
+
|
|
171
|
+
if not rows_data:
|
|
172
|
+
output.write('*No data*\n')
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
# Process title rows and create tables
|
|
176
|
+
if options.detect_title_rows or options.auto_detect_header:
|
|
177
|
+
content = MarkdownHandler._create_markdown_with_titles(rows_data, options)
|
|
178
|
+
else:
|
|
179
|
+
content = MarkdownHandler._create_markdown_table(rows_data, options)
|
|
180
|
+
output.write(content)
|
|
181
|
+
|
|
182
|
+
return output.getvalue()
|
|
183
|
+
|
|
184
|
+
@staticmethod
|
|
185
|
+
def _get_worksheet_data(worksheet) -> List[List[Any]]:
|
|
186
|
+
"""
|
|
187
|
+
Extracts all cell data from a worksheet as a 2D list.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
worksheet: The Worksheet object to extract data from.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
list: 2D list of cell values, organized by rows.
|
|
194
|
+
"""
|
|
195
|
+
from .cells import Cells
|
|
196
|
+
|
|
197
|
+
cells_dict = worksheet.cells._cells
|
|
198
|
+
|
|
199
|
+
if not cells_dict:
|
|
200
|
+
return []
|
|
201
|
+
|
|
202
|
+
# Find the dimensions by parsing cell references
|
|
203
|
+
max_row = 0
|
|
204
|
+
max_col = 0
|
|
205
|
+
|
|
206
|
+
for ref in cells_dict.keys():
|
|
207
|
+
row, col = Cells.coordinate_from_string(ref)
|
|
208
|
+
if row > max_row:
|
|
209
|
+
max_row = row
|
|
210
|
+
if col > max_col:
|
|
211
|
+
max_col = col
|
|
212
|
+
|
|
213
|
+
if max_row == 0 or max_col == 0:
|
|
214
|
+
return []
|
|
215
|
+
|
|
216
|
+
# Build 2D array
|
|
217
|
+
rows_data = []
|
|
218
|
+
for row_idx in range(1, max_row + 1):
|
|
219
|
+
row_data = []
|
|
220
|
+
for col_idx in range(1, max_col + 1):
|
|
221
|
+
ref = Cells.coordinate_to_string(row_idx, col_idx)
|
|
222
|
+
cell = cells_dict.get(ref)
|
|
223
|
+
value = cell.value if cell else None
|
|
224
|
+
row_data.append(value)
|
|
225
|
+
rows_data.append(row_data)
|
|
226
|
+
|
|
227
|
+
return rows_data
|
|
228
|
+
|
|
229
|
+
@staticmethod
|
|
230
|
+
def _is_title_row(row: List[Any]) -> bool:
|
|
231
|
+
"""
|
|
232
|
+
Check if a row is a title row (only first cell has data).
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
row: List of cell values.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
bool: True if row has only first cell with data.
|
|
239
|
+
"""
|
|
240
|
+
if not row:
|
|
241
|
+
return False
|
|
242
|
+
# Check if only the first cell has a non-empty value
|
|
243
|
+
first_has_value = row[0] is not None and str(row[0]).strip() != ''
|
|
244
|
+
rest_empty = all(val is None or str(val).strip() == '' for val in row[1:])
|
|
245
|
+
return first_has_value and rest_empty
|
|
246
|
+
|
|
247
|
+
@staticmethod
|
|
248
|
+
def _is_empty_row(row: List[Any]) -> bool:
|
|
249
|
+
"""
|
|
250
|
+
Check if a row is completely empty.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
row: List of cell values.
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
bool: True if all cells are empty.
|
|
257
|
+
"""
|
|
258
|
+
return all(val is None or str(val).strip() == '' for val in row)
|
|
259
|
+
|
|
260
|
+
@staticmethod
|
|
261
|
+
def _create_markdown_with_titles(rows_data: List[List[Any]], options: MarkdownSaveOptions) -> str:
|
|
262
|
+
"""
|
|
263
|
+
Creates Markdown with title rows converted to headings.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
rows_data: 2D list of cell values.
|
|
267
|
+
options: Export options.
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
str: Markdown content with headings and tables.
|
|
271
|
+
"""
|
|
272
|
+
if not rows_data:
|
|
273
|
+
return ''
|
|
274
|
+
|
|
275
|
+
output = io.StringIO()
|
|
276
|
+
current_table_rows = []
|
|
277
|
+
|
|
278
|
+
def flush_table():
|
|
279
|
+
"""Output accumulated table rows."""
|
|
280
|
+
nonlocal current_table_rows
|
|
281
|
+
if current_table_rows:
|
|
282
|
+
# Skip leading empty rows
|
|
283
|
+
while current_table_rows and MarkdownHandler._is_empty_row(current_table_rows[0]):
|
|
284
|
+
current_table_rows.pop(0)
|
|
285
|
+
if current_table_rows:
|
|
286
|
+
table_md = MarkdownHandler._create_markdown_table(current_table_rows, options)
|
|
287
|
+
output.write(table_md)
|
|
288
|
+
current_table_rows = []
|
|
289
|
+
|
|
290
|
+
for row in rows_data:
|
|
291
|
+
is_empty = MarkdownHandler._is_empty_row(row)
|
|
292
|
+
is_title = MarkdownHandler._is_title_row(row)
|
|
293
|
+
|
|
294
|
+
if is_empty:
|
|
295
|
+
if options.skip_empty_rows:
|
|
296
|
+
continue
|
|
297
|
+
else:
|
|
298
|
+
current_table_rows.append(row)
|
|
299
|
+
elif is_title and options.detect_title_rows:
|
|
300
|
+
# Flush any pending table
|
|
301
|
+
flush_table()
|
|
302
|
+
# Output title as heading (one level below worksheet name)
|
|
303
|
+
title_text = MarkdownHandler._format_value(row[0], options)
|
|
304
|
+
heading_level = options.header_level + 1
|
|
305
|
+
heading_prefix = '#' * heading_level
|
|
306
|
+
output.write(f"\n{heading_prefix} {title_text}\n\n")
|
|
307
|
+
else:
|
|
308
|
+
current_table_rows.append(row)
|
|
309
|
+
|
|
310
|
+
# Flush remaining table
|
|
311
|
+
flush_table()
|
|
312
|
+
|
|
313
|
+
# Clean up multiple consecutive blank lines
|
|
314
|
+
result = output.getvalue()
|
|
315
|
+
while '\n\n\n' in result:
|
|
316
|
+
result = result.replace('\n\n\n', '\n\n')
|
|
317
|
+
return result.lstrip('\n') # Remove leading newlines
|
|
318
|
+
|
|
319
|
+
@staticmethod
|
|
320
|
+
def _create_markdown_table(rows_data: List[List[Any]], options: MarkdownSaveOptions) -> str:
|
|
321
|
+
"""
|
|
322
|
+
Creates a Markdown table from 2D data.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
rows_data: 2D list of cell values.
|
|
326
|
+
options: Export options.
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
str: Markdown table string.
|
|
330
|
+
"""
|
|
331
|
+
if not rows_data:
|
|
332
|
+
return ''
|
|
333
|
+
|
|
334
|
+
# Format all values
|
|
335
|
+
formatted_rows = []
|
|
336
|
+
for row_idx, row in enumerate(rows_data):
|
|
337
|
+
formatted_row = [
|
|
338
|
+
MarkdownHandler._format_value(val, options)
|
|
339
|
+
for val in row
|
|
340
|
+
]
|
|
341
|
+
|
|
342
|
+
# Skip empty rows if option is enabled (but always keep header row)
|
|
343
|
+
if options.skip_empty_rows and row_idx > 0:
|
|
344
|
+
if all(cell == options.empty_cell_placeholder or cell == '' for cell in formatted_row):
|
|
345
|
+
continue
|
|
346
|
+
|
|
347
|
+
formatted_rows.append(formatted_row)
|
|
348
|
+
|
|
349
|
+
# Calculate column widths
|
|
350
|
+
num_cols = max(len(row) for row in formatted_rows) if formatted_rows else 0
|
|
351
|
+
|
|
352
|
+
# Add row numbers column if enabled
|
|
353
|
+
if options.include_row_numbers:
|
|
354
|
+
start_row = 0 if options.first_row_as_header else 1
|
|
355
|
+
for i, row in enumerate(formatted_rows):
|
|
356
|
+
if i == 0 and options.first_row_as_header:
|
|
357
|
+
row.insert(0, '#')
|
|
358
|
+
else:
|
|
359
|
+
row.insert(0, str(i + start_row))
|
|
360
|
+
num_cols += 1
|
|
361
|
+
|
|
362
|
+
# Ensure all rows have the same number of columns
|
|
363
|
+
for row in formatted_rows:
|
|
364
|
+
while len(row) < num_cols:
|
|
365
|
+
row.append(options.empty_cell_placeholder)
|
|
366
|
+
|
|
367
|
+
# Calculate column widths for alignment
|
|
368
|
+
col_widths = [3] * num_cols # Minimum width of 3 for separator
|
|
369
|
+
for row in formatted_rows:
|
|
370
|
+
for i, val in enumerate(row):
|
|
371
|
+
width = len(val)
|
|
372
|
+
if options.max_column_width > 0:
|
|
373
|
+
width = min(width, options.max_column_width)
|
|
374
|
+
col_widths[i] = max(col_widths[i], width)
|
|
375
|
+
|
|
376
|
+
# Build the table
|
|
377
|
+
lines = []
|
|
378
|
+
|
|
379
|
+
# Determine if using compact format
|
|
380
|
+
use_compact = options.compact_format
|
|
381
|
+
|
|
382
|
+
# Header row
|
|
383
|
+
if options.first_row_as_header and formatted_rows:
|
|
384
|
+
header_row = formatted_rows[0]
|
|
385
|
+
if use_compact:
|
|
386
|
+
header_cells = header_row
|
|
387
|
+
else:
|
|
388
|
+
header_cells = [
|
|
389
|
+
MarkdownHandler._pad_cell(val, col_widths[i], options, i)
|
|
390
|
+
for i, val in enumerate(header_row)
|
|
391
|
+
]
|
|
392
|
+
lines.append('| ' + ' | '.join(header_cells) + ' |')
|
|
393
|
+
|
|
394
|
+
# Separator row
|
|
395
|
+
separator_cells = [
|
|
396
|
+
MarkdownHandler._create_separator(col_widths[i], options, i)
|
|
397
|
+
for i in range(num_cols)
|
|
398
|
+
]
|
|
399
|
+
lines.append('| ' + ' | '.join(separator_cells) + ' |')
|
|
400
|
+
|
|
401
|
+
# Data rows
|
|
402
|
+
data_rows = formatted_rows[1:]
|
|
403
|
+
else:
|
|
404
|
+
# Create generic header
|
|
405
|
+
header_cells = [f'Column {i+1}' for i in range(num_cols)]
|
|
406
|
+
if not use_compact:
|
|
407
|
+
header_cells = [
|
|
408
|
+
MarkdownHandler._pad_cell(val, col_widths[i], options, i)
|
|
409
|
+
for i, val in enumerate(header_cells)
|
|
410
|
+
]
|
|
411
|
+
lines.append('| ' + ' | '.join(header_cells) + ' |')
|
|
412
|
+
|
|
413
|
+
# Separator row
|
|
414
|
+
separator_cells = [
|
|
415
|
+
MarkdownHandler._create_separator(col_widths[i], options, i)
|
|
416
|
+
for i in range(num_cols)
|
|
417
|
+
]
|
|
418
|
+
lines.append('| ' + ' | '.join(separator_cells) + ' |')
|
|
419
|
+
|
|
420
|
+
data_rows = formatted_rows
|
|
421
|
+
|
|
422
|
+
# Data rows
|
|
423
|
+
for row in data_rows:
|
|
424
|
+
if use_compact:
|
|
425
|
+
data_cells = row
|
|
426
|
+
else:
|
|
427
|
+
data_cells = [
|
|
428
|
+
MarkdownHandler._pad_cell(val, col_widths[i], options, i)
|
|
429
|
+
for i, val in enumerate(row)
|
|
430
|
+
]
|
|
431
|
+
lines.append('| ' + ' | '.join(data_cells) + ' |')
|
|
432
|
+
|
|
433
|
+
return '\n'.join(lines) + '\n'
|
|
434
|
+
|
|
435
|
+
@staticmethod
|
|
436
|
+
def _format_value(value: Any, options: MarkdownSaveOptions) -> str:
|
|
437
|
+
"""
|
|
438
|
+
Formats a cell value for Markdown output.
|
|
439
|
+
|
|
440
|
+
Args:
|
|
441
|
+
value: The cell value to format.
|
|
442
|
+
options: Export options.
|
|
443
|
+
|
|
444
|
+
Returns:
|
|
445
|
+
str: The formatted string value.
|
|
446
|
+
"""
|
|
447
|
+
if value is None:
|
|
448
|
+
return options.empty_cell_placeholder
|
|
449
|
+
|
|
450
|
+
# Handle datetime types
|
|
451
|
+
if isinstance(value, datetime):
|
|
452
|
+
result = value.strftime(options.datetime_format)
|
|
453
|
+
elif isinstance(value, date):
|
|
454
|
+
result = value.strftime(options.date_format)
|
|
455
|
+
elif isinstance(value, time):
|
|
456
|
+
result = value.strftime(options.time_format)
|
|
457
|
+
elif isinstance(value, bool):
|
|
458
|
+
result = 'Yes' if value else 'No'
|
|
459
|
+
elif isinstance(value, float):
|
|
460
|
+
# Check if it's an integer stored as float
|
|
461
|
+
if value.is_integer():
|
|
462
|
+
result = str(int(value))
|
|
463
|
+
elif options.float_precision >= 0:
|
|
464
|
+
# Use fixed precision
|
|
465
|
+
result = f"{value:.{options.float_precision}f}"
|
|
466
|
+
else:
|
|
467
|
+
# Auto precision: round to reasonable precision and strip trailing zeros
|
|
468
|
+
# Use 10 significant digits to avoid floating point artifacts
|
|
469
|
+
result = f"{value:.10g}"
|
|
470
|
+
else:
|
|
471
|
+
result = str(value)
|
|
472
|
+
|
|
473
|
+
# Replace newlines to prevent breaking table structure
|
|
474
|
+
if options.newline_replacement is not None:
|
|
475
|
+
result = result.replace('\r\n', options.newline_replacement)
|
|
476
|
+
result = result.replace('\n', options.newline_replacement)
|
|
477
|
+
result = result.replace('\r', options.newline_replacement)
|
|
478
|
+
|
|
479
|
+
# Trim whitespace if enabled
|
|
480
|
+
if options.trim_whitespace:
|
|
481
|
+
result = result.strip()
|
|
482
|
+
|
|
483
|
+
# Escape pipe characters if enabled
|
|
484
|
+
if options.escape_pipes:
|
|
485
|
+
result = result.replace('|', '\\|')
|
|
486
|
+
|
|
487
|
+
# Truncate if max width is set
|
|
488
|
+
if options.max_column_width > 0 and len(result) > options.max_column_width:
|
|
489
|
+
result = result[:options.max_column_width - 3] + '...'
|
|
490
|
+
|
|
491
|
+
return result
|
|
492
|
+
|
|
493
|
+
@staticmethod
|
|
494
|
+
def _get_alignment(col_index: int, options: MarkdownSaveOptions) -> str:
|
|
495
|
+
"""
|
|
496
|
+
Gets the alignment for a specific column.
|
|
497
|
+
|
|
498
|
+
Args:
|
|
499
|
+
col_index: Column index (0-based).
|
|
500
|
+
options: Export options.
|
|
501
|
+
|
|
502
|
+
Returns:
|
|
503
|
+
str: Alignment string ('left', 'center', or 'right').
|
|
504
|
+
"""
|
|
505
|
+
return options.column_alignments.get(col_index, options.default_alignment)
|
|
506
|
+
|
|
507
|
+
@staticmethod
|
|
508
|
+
def _create_separator(width: int, options: MarkdownSaveOptions, col_index: int) -> str:
|
|
509
|
+
"""
|
|
510
|
+
Creates a separator cell for the Markdown table.
|
|
511
|
+
|
|
512
|
+
Args:
|
|
513
|
+
width: Column width.
|
|
514
|
+
options: Export options.
|
|
515
|
+
col_index: Column index (0-based).
|
|
516
|
+
|
|
517
|
+
Returns:
|
|
518
|
+
str: Separator string with alignment markers.
|
|
519
|
+
"""
|
|
520
|
+
# Simple separators: just '---' without alignment markers
|
|
521
|
+
if options.simple_separators:
|
|
522
|
+
return '---'
|
|
523
|
+
|
|
524
|
+
alignment = MarkdownHandler._get_alignment(col_index, options)
|
|
525
|
+
|
|
526
|
+
# In compact format, use minimal width for separators but still show alignment
|
|
527
|
+
if options.compact_format:
|
|
528
|
+
if alignment == 'center':
|
|
529
|
+
return ':---:'
|
|
530
|
+
elif alignment == 'right':
|
|
531
|
+
return '---:'
|
|
532
|
+
else: # left (default)
|
|
533
|
+
return '---'
|
|
534
|
+
|
|
535
|
+
# Non-compact format: use full width for separators
|
|
536
|
+
if alignment == 'center':
|
|
537
|
+
return ':' + '-' * width + ':'
|
|
538
|
+
elif alignment == 'right':
|
|
539
|
+
return '-' * (width + 1) + ':'
|
|
540
|
+
else: # left (default)
|
|
541
|
+
return ':' + '-' * (width + 1)
|
|
542
|
+
|
|
543
|
+
@staticmethod
|
|
544
|
+
def _pad_cell(value: str, width: int, options: MarkdownSaveOptions, col_index: int) -> str:
|
|
545
|
+
"""
|
|
546
|
+
Pads a cell value to the specified width with appropriate alignment.
|
|
547
|
+
|
|
548
|
+
Args:
|
|
549
|
+
value: Cell value string.
|
|
550
|
+
width: Target width.
|
|
551
|
+
options: Export options.
|
|
552
|
+
col_index: Column index (0-based).
|
|
553
|
+
|
|
554
|
+
Returns:
|
|
555
|
+
str: Padded cell value.
|
|
556
|
+
"""
|
|
557
|
+
alignment = MarkdownHandler._get_alignment(col_index, options)
|
|
558
|
+
|
|
559
|
+
if len(value) >= width:
|
|
560
|
+
return value
|
|
561
|
+
|
|
562
|
+
if alignment == 'center':
|
|
563
|
+
return value.center(width)
|
|
564
|
+
elif alignment == 'right':
|
|
565
|
+
return value.rjust(width)
|
|
566
|
+
else: # left (default)
|
|
567
|
+
return value.ljust(width)
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def save_workbook_as_markdown(workbook, file_path: str, options: Optional[MarkdownSaveOptions] = None) -> None:
|
|
571
|
+
"""
|
|
572
|
+
Convenience function to save a Workbook to a Markdown file.
|
|
573
|
+
|
|
574
|
+
Args:
|
|
575
|
+
workbook: The Workbook object to export.
|
|
576
|
+
file_path (str): Path where the Markdown file should be saved.
|
|
577
|
+
options (MarkdownSaveOptions, optional): Export options. Uses defaults if None.
|
|
578
|
+
|
|
579
|
+
Examples:
|
|
580
|
+
>>> wb = Workbook('data.xlsx')
|
|
581
|
+
>>> save_workbook_as_markdown(wb, 'output.md')
|
|
582
|
+
"""
|
|
583
|
+
MarkdownHandler.save_markdown(workbook, file_path, options)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import xml.etree.ElementTree as ET
|
|
2
|
+
|
|
3
|
+
class SharedStringTable:
|
|
4
|
+
"""Manages the Shared String Table for XLSX files according to ECMA-376 specification."""
|
|
5
|
+
|
|
6
|
+
def __init__(self):
|
|
7
|
+
"""Initialize an empty shared string table."""
|
|
8
|
+
self.strings = []
|
|
9
|
+
self.string_to_index = {}
|
|
10
|
+
self.count = 0 # Track total occurrences of strings
|
|
11
|
+
|
|
12
|
+
def add_string(self, text):
|
|
13
|
+
"""
|
|
14
|
+
Add a string to the shared string table and return its index.
|
|
15
|
+
If the string already exists, return the existing index.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
text (str): The string to add to the table
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
int: The index of the string in the shared string table
|
|
22
|
+
"""
|
|
23
|
+
if text is None:
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
# Increment total count for ECMA-376 compliance
|
|
27
|
+
self.count += 1
|
|
28
|
+
|
|
29
|
+
if text in self.string_to_index:
|
|
30
|
+
return self.string_to_index[text]
|
|
31
|
+
|
|
32
|
+
index = len(self.strings)
|
|
33
|
+
self.strings.append(text)
|
|
34
|
+
self.string_to_index[text] = index
|
|
35
|
+
return index
|
|
36
|
+
|
|
37
|
+
def get_string(self, index):
|
|
38
|
+
"""
|
|
39
|
+
Get a string from the shared string table by its index.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
index (int): The index of the string to retrieve
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
str: The string at the given index, or None if index is invalid
|
|
46
|
+
"""
|
|
47
|
+
if index is None or index < 0 or index >= len(self.strings):
|
|
48
|
+
return None
|
|
49
|
+
return self.strings[index]
|
|
50
|
+
|
|
51
|
+
def to_xml(self):
|
|
52
|
+
"""
|
|
53
|
+
Convert the shared string table to XML format for XLSX files.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
str: XML representation of the shared string table
|
|
57
|
+
"""
|
|
58
|
+
root = ET.Element('sst', {
|
|
59
|
+
'xmlns': 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
|
|
60
|
+
'count': str(self.count), # Total occurrences
|
|
61
|
+
'uniqueCount': str(len(self.strings)) # Unique strings
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
for text in self.strings:
|
|
65
|
+
si = ET.SubElement(root, 'si')
|
|
66
|
+
t = ET.SubElement(si, 't')
|
|
67
|
+
t.text = text
|
|
68
|
+
|
|
69
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' + ET.tostring(root, encoding='unicode')
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def from_xml(cls, xml_content):
|
|
73
|
+
"""
|
|
74
|
+
Create a SharedStringTable from XML content.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
xml_content (str): XML content of the shared string table
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
SharedStringTable: A new SharedStringTable instance
|
|
81
|
+
"""
|
|
82
|
+
sst = cls()
|
|
83
|
+
if not xml_content:
|
|
84
|
+
return sst
|
|
85
|
+
|
|
86
|
+
root = ET.fromstring(xml_content)
|
|
87
|
+
ns = {'ns': 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'}
|
|
88
|
+
|
|
89
|
+
for si in root.findall('.//ns:si', ns):
|
|
90
|
+
text_parts = [
|
|
91
|
+
t.text if t.text is not None else ''
|
|
92
|
+
for t in si.findall('.//ns:t', ns)
|
|
93
|
+
]
|
|
94
|
+
# Preserve indices for rich text and empty strings
|
|
95
|
+
sst.add_string(''.join(text_parts))
|
|
96
|
+
|
|
97
|
+
return sst
|
|
98
|
+
|
|
99
|
+
def __len__(self):
|
|
100
|
+
"""Return the number of strings in the table."""
|
|
101
|
+
return len(self.strings)
|