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.
Files changed (93) hide show
  1. aspose_cells/__init__.py +88 -0
  2. aspose_cells/auto_filter.py +527 -0
  3. aspose_cells/cell.py +483 -0
  4. aspose_cells/cell_value_handler.py +319 -0
  5. aspose_cells/cells.py +779 -0
  6. aspose_cells/cfb_handler.py +445 -0
  7. aspose_cells/cfb_writer.py +659 -0
  8. aspose_cells/cfb_writer_minimal.py +337 -0
  9. aspose_cells/comment_xml.py +475 -0
  10. aspose_cells/conditional_format.py +1185 -0
  11. aspose_cells/csv_handler.py +690 -0
  12. aspose_cells/data_validation.py +911 -0
  13. aspose_cells/document_properties.py +356 -0
  14. aspose_cells/encryption_crypto.py +247 -0
  15. aspose_cells/encryption_params.py +138 -0
  16. aspose_cells/hyperlink.py +372 -0
  17. aspose_cells/json_handler.py +185 -0
  18. aspose_cells/markdown_handler.py +583 -0
  19. aspose_cells/shared_strings.py +101 -0
  20. aspose_cells/style.py +841 -0
  21. aspose_cells/workbook.py +499 -0
  22. aspose_cells/workbook_hash_password.py +68 -0
  23. aspose_cells/workbook_properties.py +712 -0
  24. aspose_cells/worksheet.py +570 -0
  25. aspose_cells/worksheet_properties.py +1239 -0
  26. aspose_cells/xlsx_encryptor.py +403 -0
  27. aspose_cells/xml_autofilter_loader.py +195 -0
  28. aspose_cells/xml_autofilter_saver.py +173 -0
  29. aspose_cells/xml_conditional_format_loader.py +215 -0
  30. aspose_cells/xml_conditional_format_saver.py +351 -0
  31. aspose_cells/xml_datavalidation_loader.py +239 -0
  32. aspose_cells/xml_datavalidation_saver.py +245 -0
  33. aspose_cells/xml_hyperlink_handler.py +323 -0
  34. aspose_cells/xml_loader.py +986 -0
  35. aspose_cells/xml_properties_loader.py +512 -0
  36. aspose_cells/xml_properties_saver.py +607 -0
  37. aspose_cells/xml_saver.py +1306 -0
  38. aspose_cells_foss-26.2.2.dist-info/METADATA +190 -0
  39. aspose_cells_foss-26.2.2.dist-info/RECORD +41 -0
  40. {aspose_cells_foss-25.12.1.dist-info → aspose_cells_foss-26.2.2.dist-info}/WHEEL +1 -1
  41. aspose_cells_foss-26.2.2.dist-info/top_level.txt +1 -0
  42. aspose/__init__.py +0 -14
  43. aspose/cells/__init__.py +0 -31
  44. aspose/cells/cell.py +0 -350
  45. aspose/cells/constants.py +0 -44
  46. aspose/cells/converters/__init__.py +0 -13
  47. aspose/cells/converters/csv_converter.py +0 -55
  48. aspose/cells/converters/json_converter.py +0 -46
  49. aspose/cells/converters/markdown_converter.py +0 -453
  50. aspose/cells/drawing/__init__.py +0 -17
  51. aspose/cells/drawing/anchor.py +0 -172
  52. aspose/cells/drawing/collection.py +0 -233
  53. aspose/cells/drawing/image.py +0 -338
  54. aspose/cells/formats.py +0 -80
  55. aspose/cells/formula/__init__.py +0 -10
  56. aspose/cells/formula/evaluator.py +0 -360
  57. aspose/cells/formula/functions.py +0 -433
  58. aspose/cells/formula/tokenizer.py +0 -340
  59. aspose/cells/io/__init__.py +0 -27
  60. aspose/cells/io/csv/__init__.py +0 -8
  61. aspose/cells/io/csv/reader.py +0 -88
  62. aspose/cells/io/csv/writer.py +0 -98
  63. aspose/cells/io/factory.py +0 -138
  64. aspose/cells/io/interfaces.py +0 -48
  65. aspose/cells/io/json/__init__.py +0 -8
  66. aspose/cells/io/json/reader.py +0 -126
  67. aspose/cells/io/json/writer.py +0 -119
  68. aspose/cells/io/md/__init__.py +0 -8
  69. aspose/cells/io/md/reader.py +0 -161
  70. aspose/cells/io/md/writer.py +0 -334
  71. aspose/cells/io/models.py +0 -64
  72. aspose/cells/io/xlsx/__init__.py +0 -9
  73. aspose/cells/io/xlsx/constants.py +0 -312
  74. aspose/cells/io/xlsx/image_writer.py +0 -311
  75. aspose/cells/io/xlsx/reader.py +0 -284
  76. aspose/cells/io/xlsx/writer.py +0 -931
  77. aspose/cells/plugins/__init__.py +0 -6
  78. aspose/cells/plugins/docling_backend/__init__.py +0 -7
  79. aspose/cells/plugins/docling_backend/backend.py +0 -535
  80. aspose/cells/plugins/markitdown_plugin/__init__.py +0 -15
  81. aspose/cells/plugins/markitdown_plugin/plugin.py +0 -128
  82. aspose/cells/range.py +0 -210
  83. aspose/cells/style.py +0 -287
  84. aspose/cells/utils/__init__.py +0 -54
  85. aspose/cells/utils/coordinates.py +0 -68
  86. aspose/cells/utils/exceptions.py +0 -43
  87. aspose/cells/utils/validation.py +0 -102
  88. aspose/cells/workbook.py +0 -352
  89. aspose/cells/worksheet.py +0 -670
  90. aspose_cells_foss-25.12.1.dist-info/METADATA +0 -189
  91. aspose_cells_foss-25.12.1.dist-info/RECORD +0 -53
  92. aspose_cells_foss-25.12.1.dist-info/entry_points.txt +0 -2
  93. 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)