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,245 @@
1
+ """
2
+ Aspose.Cells for Python - Data Validation XML Saver
3
+
4
+ This module handles serialization of DataValidation objects to ECMA-376 SpreadsheetML XML.
5
+
6
+ References:
7
+ - ECMA-376 Part 4, Section 3.3.1.30 (dataValidation)
8
+ - ECMA-376 Part 4, Section 3.3.1.31 (dataValidations)
9
+ """
10
+
11
+ import xml.etree.ElementTree as ET
12
+ from .data_validation import (
13
+ DataValidation, DataValidationCollection,
14
+ DataValidationType, DataValidationOperator,
15
+ DataValidationAlertStyle, DataValidationImeMode
16
+ )
17
+
18
+
19
+ # Mapping from enum values to XML attribute values
20
+ TYPE_TO_XML = {
21
+ DataValidationType.NONE: 'none',
22
+ DataValidationType.WHOLE_NUMBER: 'whole',
23
+ DataValidationType.DECIMAL: 'decimal',
24
+ DataValidationType.LIST: 'list',
25
+ DataValidationType.DATE: 'date',
26
+ DataValidationType.TIME: 'time',
27
+ DataValidationType.TEXT_LENGTH: 'textLength',
28
+ DataValidationType.CUSTOM: 'custom',
29
+ }
30
+
31
+ OPERATOR_TO_XML = {
32
+ DataValidationOperator.BETWEEN: 'between',
33
+ DataValidationOperator.NOT_BETWEEN: 'notBetween',
34
+ DataValidationOperator.EQUAL: 'equal',
35
+ DataValidationOperator.NOT_EQUAL: 'notEqual',
36
+ DataValidationOperator.GREATER_THAN: 'greaterThan',
37
+ DataValidationOperator.LESS_THAN: 'lessThan',
38
+ DataValidationOperator.GREATER_THAN_OR_EQUAL: 'greaterThanOrEqual',
39
+ DataValidationOperator.LESS_THAN_OR_EQUAL: 'lessThanOrEqual',
40
+ }
41
+
42
+ ALERT_STYLE_TO_XML = {
43
+ DataValidationAlertStyle.STOP: 'stop',
44
+ DataValidationAlertStyle.WARNING: 'warning',
45
+ DataValidationAlertStyle.INFORMATION: 'information',
46
+ }
47
+
48
+ IME_MODE_TO_XML = {
49
+ DataValidationImeMode.NO_CONTROL: 'noControl',
50
+ DataValidationImeMode.OFF: 'off',
51
+ DataValidationImeMode.ON: 'on',
52
+ DataValidationImeMode.DISABLED: 'disabled',
53
+ DataValidationImeMode.HIRAGANA: 'hiragana',
54
+ DataValidationImeMode.FULL_KATAKANA: 'fullKatakana',
55
+ DataValidationImeMode.HALF_KATAKANA: 'halfKatakana',
56
+ DataValidationImeMode.FULL_ALPHA: 'fullAlpha',
57
+ DataValidationImeMode.HALF_ALPHA: 'halfAlpha',
58
+ DataValidationImeMode.FULL_HANGUL: 'fullHangul',
59
+ DataValidationImeMode.HALF_HANGUL: 'halfHangul',
60
+ }
61
+
62
+
63
+ class DataValidationXmlSaver:
64
+ """
65
+ Saves DataValidation objects to ECMA-376 SpreadsheetML XML format.
66
+ """
67
+
68
+ def __init__(self, namespace='http://schemas.openxmlformats.org/spreadsheetml/2006/main'):
69
+ """
70
+ Initializes the DataValidationXmlSaver.
71
+
72
+ Args:
73
+ namespace (str): The SpreadsheetML namespace URI.
74
+ """
75
+ self.namespace = namespace
76
+
77
+ def save_data_validations(self, validations, parent_element):
78
+ """
79
+ Saves a DataValidationCollection to XML as a child of the parent element.
80
+
81
+ Args:
82
+ validations (DataValidationCollection): The validations to save.
83
+ parent_element (Element): The parent XML element (usually worksheet).
84
+
85
+ Returns:
86
+ Element or None: The created dataValidations element, or None if empty.
87
+ """
88
+ if not validations or validations.count == 0:
89
+ return None
90
+
91
+ # Create dataValidations element
92
+ ns_prefix = '{' + self.namespace + '}'
93
+ dv_collection = ET.SubElement(parent_element, f'{ns_prefix}dataValidations')
94
+
95
+ # Set count attribute
96
+ dv_collection.set('count', str(validations.count))
97
+
98
+ # Set optional attributes
99
+ if validations.disable_prompts:
100
+ dv_collection.set('disablePrompts', '1')
101
+
102
+ if validations.x_window is not None:
103
+ dv_collection.set('xWindow', str(validations.x_window))
104
+
105
+ if validations.y_window is not None:
106
+ dv_collection.set('yWindow', str(validations.y_window))
107
+
108
+ # Add each validation
109
+ for validation in validations:
110
+ self._save_data_validation(validation, dv_collection)
111
+
112
+ return dv_collection
113
+
114
+ def _save_data_validation(self, validation, parent_element):
115
+ """
116
+ Saves a single DataValidation to XML.
117
+
118
+ Args:
119
+ validation (DataValidation): The validation to save.
120
+ parent_element (Element): The parent dataValidations element.
121
+
122
+ Returns:
123
+ Element: The created dataValidation element.
124
+ """
125
+ ns_prefix = '{' + self.namespace + '}'
126
+ dv = ET.SubElement(parent_element, f'{ns_prefix}dataValidation')
127
+
128
+ # Required attribute: sqref
129
+ if validation.sqref:
130
+ dv.set('sqref', validation.sqref)
131
+
132
+ # Type attribute (only if not default 'none')
133
+ if validation.type != DataValidationType.NONE:
134
+ dv.set('type', TYPE_TO_XML.get(validation.type, 'none'))
135
+
136
+ # Operator attribute (only if type uses operators and not default 'between')
137
+ if validation.type in (DataValidationType.WHOLE_NUMBER, DataValidationType.DECIMAL,
138
+ DataValidationType.DATE, DataValidationType.TIME,
139
+ DataValidationType.TEXT_LENGTH):
140
+ if validation.operator != DataValidationOperator.BETWEEN:
141
+ dv.set('operator', OPERATOR_TO_XML.get(validation.operator, 'between'))
142
+
143
+ # Error style (only if not default 'stop')
144
+ if validation.alert_style != DataValidationAlertStyle.STOP:
145
+ dv.set('errorStyle', ALERT_STYLE_TO_XML.get(validation.alert_style, 'stop'))
146
+
147
+ # IME mode (only if not default 'noControl')
148
+ if validation.ime_mode != DataValidationImeMode.NO_CONTROL:
149
+ dv.set('imeMode', IME_MODE_TO_XML.get(validation.ime_mode, 'noControl'))
150
+
151
+ # Boolean attributes (only if not default)
152
+ if validation.allow_blank:
153
+ dv.set('allowBlank', '1')
154
+
155
+ # Note: In ECMA-376, showDropDown="1" means HIDE the dropdown (counterintuitive)
156
+ # So we only set this attribute if we want to HIDE the dropdown
157
+ if not validation.show_dropdown:
158
+ dv.set('showDropDown', '1')
159
+
160
+ if validation.show_input_message:
161
+ dv.set('showInputMessage', '1')
162
+
163
+ if validation.show_error_message:
164
+ dv.set('showErrorMessage', '1')
165
+
166
+ # String attributes (only if set)
167
+ if validation.error_title:
168
+ dv.set('errorTitle', validation.error_title)
169
+
170
+ if validation.error_message:
171
+ dv.set('error', validation.error_message)
172
+
173
+ if validation.input_title:
174
+ dv.set('promptTitle', validation.input_title)
175
+
176
+ if validation.input_message:
177
+ dv.set('prompt', validation.input_message)
178
+
179
+ # Formula elements
180
+ if validation.formula1 is not None:
181
+ formula1_elem = ET.SubElement(dv, f'{ns_prefix}formula1')
182
+ formula1_elem.text = validation.formula1
183
+
184
+ if validation.formula2 is not None:
185
+ formula2_elem = ET.SubElement(dv, f'{ns_prefix}formula2')
186
+ formula2_elem.text = validation.formula2
187
+
188
+ return dv
189
+
190
+ def create_data_validations_xml(self, validations):
191
+ """
192
+ Creates a standalone dataValidations XML element.
193
+
194
+ Args:
195
+ validations (DataValidationCollection): The validations to save.
196
+
197
+ Returns:
198
+ Element: The dataValidations element.
199
+ """
200
+ # Create a temporary parent
201
+ ns_prefix = '{' + self.namespace + '}'
202
+ temp_parent = ET.Element('temp')
203
+
204
+ result = self.save_data_validations(validations, temp_parent)
205
+
206
+ if result is not None:
207
+ return result
208
+
209
+ # Return empty element if no validations
210
+ return ET.Element(f'{ns_prefix}dataValidations', {'count': '0'})
211
+
212
+ def to_xml_string(self, validations, include_declaration=False):
213
+ """
214
+ Converts validations to an XML string.
215
+
216
+ Args:
217
+ validations (DataValidationCollection): The validations to convert.
218
+ include_declaration (bool): Whether to include XML declaration.
219
+
220
+ Returns:
221
+ str: The XML string.
222
+ """
223
+ elem = self.create_data_validations_xml(validations)
224
+
225
+ if include_declaration:
226
+ return '<?xml version="1.0" encoding="UTF-8"?>\n' + ET.tostring(
227
+ elem, encoding='unicode')
228
+ else:
229
+ return ET.tostring(elem, encoding='unicode')
230
+
231
+
232
+ def save_data_validations_to_worksheet_xml(validations, worksheet_element, namespace):
233
+ """
234
+ Convenience function to save data validations to a worksheet XML element.
235
+
236
+ Args:
237
+ validations (DataValidationCollection): The validations to save.
238
+ worksheet_element (Element): The worksheet XML element.
239
+ namespace (str): The SpreadsheetML namespace.
240
+
241
+ Returns:
242
+ Element or None: The created dataValidations element.
243
+ """
244
+ saver = DataValidationXmlSaver(namespace)
245
+ return saver.save_data_validations(validations, worksheet_element)
@@ -0,0 +1,323 @@
1
+ """
2
+ Aspose.Cells for Python - Hyperlink XML Handler Module
3
+
4
+ This module handles loading and saving hyperlinks to/from XLSX XML files
5
+ according to ECMA-376 specification.
6
+
7
+ ECMA-376 Sections:
8
+ - 18.3.1.48: hyperlink element
9
+ - 18.3.1.49: hyperlinks collection
10
+ - 12.3.2.2: Hyperlink Relationships
11
+ """
12
+
13
+ import xml.etree.ElementTree as ET
14
+
15
+
16
+ class HyperlinkXMLLoader:
17
+ """
18
+ Loads hyperlinks from worksheet XML and relationship files.
19
+
20
+ ECMA-376 compliant hyperlink loading from:
21
+ - xl/worksheets/sheet{n}.xml (hyperlink elements)
22
+ - xl/worksheets/_rels/sheet{n}.xml.rels (relationship targets)
23
+ """
24
+
25
+ def __init__(self, namespaces):
26
+ """
27
+ Initializes the hyperlink loader.
28
+
29
+ Args:
30
+ namespaces (dict): XML namespaces for parsing.
31
+ """
32
+ self.ns = namespaces
33
+
34
+ def load_hyperlinks(self, worksheet, worksheet_root, zipf, sheet_num):
35
+ """
36
+ Loads all hyperlinks from worksheet XML and relationships.
37
+
38
+ Args:
39
+ worksheet: The worksheet object to load hyperlinks into.
40
+ worksheet_root: The XML root element of the worksheet.
41
+ zipf: ZipFile object containing the workbook data.
42
+ sheet_num (int): Worksheet number (1-based).
43
+
44
+ Examples:
45
+ >>> loader.load_hyperlinks(worksheet, root, zipf, 1)
46
+ """
47
+ # Load relationships first (for external hyperlinks)
48
+ relationships = self._load_relationships(zipf, sheet_num)
49
+
50
+ # Find hyperlinks element
51
+ hyperlinks_elem = worksheet_root.find('main:hyperlinks', namespaces=self.ns)
52
+ if hyperlinks_elem is None:
53
+ return # No hyperlinks in this worksheet
54
+
55
+ # Load each hyperlink
56
+ for hyperlink_elem in hyperlinks_elem.findall('main:hyperlink', namespaces=self.ns):
57
+ self._load_hyperlink(worksheet, hyperlink_elem, relationships)
58
+
59
+ def _load_relationships(self, zipf, sheet_num):
60
+ """
61
+ Loads relationships from xl/worksheets/_rels/sheet{n}.xml.rels.
62
+
63
+ Args:
64
+ zipf: ZipFile object.
65
+ sheet_num (int): Worksheet number (1-based).
66
+
67
+ Returns:
68
+ dict: Map of relationship ID to target URL.
69
+ """
70
+ relationships = {}
71
+ rels_path = f'xl/worksheets/_rels/sheet{sheet_num}.xml.rels'
72
+
73
+ try:
74
+ rels_content = zipf.read(rels_path)
75
+ rels_root = ET.fromstring(rels_content)
76
+
77
+ # Namespace for relationships
78
+ rels_ns = {'rel': 'http://schemas.openxmlformats.org/package/2006/relationships'}
79
+
80
+ # Find all hyperlink relationships
81
+ for rel_elem in rels_root.findall('rel:Relationship', namespaces=rels_ns):
82
+ rel_type = rel_elem.get('Type', '')
83
+ if 'hyperlink' in rel_type:
84
+ rel_id = rel_elem.get('Id')
85
+ target = rel_elem.get('Target', '')
86
+ relationships[rel_id] = target
87
+
88
+ except KeyError:
89
+ # No relationships file (no external hyperlinks)
90
+ pass
91
+
92
+ return relationships
93
+
94
+ def _load_hyperlink(self, worksheet, hyperlink_elem, relationships):
95
+ """
96
+ Loads a single hyperlink element.
97
+
98
+ Args:
99
+ worksheet: The worksheet object.
100
+ hyperlink_elem: XML element for the hyperlink.
101
+ relationships (dict): Map of relationship IDs to targets.
102
+ """
103
+ # Get hyperlink attributes
104
+ ref = hyperlink_elem.get('ref') # Required
105
+ location = hyperlink_elem.get('location', '') # Internal link
106
+ display = hyperlink_elem.get('display', '')
107
+ tooltip = hyperlink_elem.get('tooltip', '')
108
+
109
+ # Get relationship ID (for external links)
110
+ r_id = hyperlink_elem.get('{http://schemas.openxmlformats.org/officeDocument/2006/relationships}id', '')
111
+
112
+ # Determine address (external) or sub_address (internal)
113
+ address = ""
114
+ sub_address = ""
115
+
116
+ if r_id and r_id in relationships:
117
+ # External hyperlink
118
+ address = relationships[r_id]
119
+ elif location:
120
+ # Internal hyperlink
121
+ sub_address = location
122
+
123
+ # Add hyperlink to worksheet
124
+ try:
125
+ hyperlink = worksheet.hyperlinks.add(
126
+ range_address=ref,
127
+ address=address,
128
+ sub_address=sub_address,
129
+ text_to_display=display,
130
+ screen_tip=tooltip
131
+ )
132
+ # Store relationship ID for saving
133
+ if r_id:
134
+ hyperlink._relationship_id = r_id
135
+ except ValueError as e:
136
+ # Skip invalid hyperlinks
137
+ print(f"Warning: Skipping invalid hyperlink at {ref}: {e}")
138
+
139
+
140
+ class HyperlinkXMLSaver:
141
+ """
142
+ Saves hyperlinks to worksheet XML and relationship files.
143
+
144
+ ECMA-376 compliant hyperlink saving to:
145
+ - xl/worksheets/sheet{n}.xml (hyperlink elements)
146
+ - xl/worksheets/_rels/sheet{n}.xml.rels (relationship targets)
147
+ """
148
+
149
+ def __init__(self):
150
+ """Initializes the hyperlink saver."""
151
+ self._next_rel_id = 1 # Counter for relationship IDs
152
+
153
+ def format_hyperlinks_xml(self, worksheet):
154
+ """
155
+ Formats hyperlinks collection as XML.
156
+
157
+ ECMA-376 Section: 18.3.1.49 (hyperlinks collection)
158
+
159
+ Args:
160
+ worksheet: The worksheet containing hyperlinks.
161
+
162
+ Returns:
163
+ str: XML representation of hyperlinks, or empty string if none.
164
+
165
+ Examples:
166
+ >>> xml = saver.format_hyperlinks_xml(worksheet)
167
+ """
168
+ if worksheet.hyperlinks.count == 0:
169
+ return ''
170
+
171
+ xml = ' <hyperlinks>\n'
172
+
173
+ for hyperlink in worksheet.hyperlinks:
174
+ xml += self._format_hyperlink_xml(hyperlink)
175
+
176
+ xml += ' </hyperlinks>\n'
177
+ return xml
178
+
179
+ def _format_hyperlink_xml(self, hyperlink):
180
+ """
181
+ Formats a single hyperlink as XML.
182
+
183
+ ECMA-376 Section: 18.3.1.48 (hyperlink element)
184
+
185
+ Args:
186
+ hyperlink: The Hyperlink object to format.
187
+
188
+ Returns:
189
+ str: XML representation of the hyperlink.
190
+ """
191
+ attrs = [f'ref="{self._escape_xml(hyperlink.range)}"']
192
+
193
+ # Add r:id for external links
194
+ if hyperlink.address:
195
+ if not hasattr(hyperlink, '_relationship_id') or not hyperlink._relationship_id:
196
+ # Assign new relationship ID
197
+ hyperlink._relationship_id = f'rId{self._next_rel_id}'
198
+ self._next_rel_id += 1
199
+ attrs.append(f'r:id="{hyperlink._relationship_id}"')
200
+
201
+ # Add location for internal links
202
+ if hyperlink.sub_address:
203
+ attrs.append(f'location="{self._escape_xml(hyperlink.sub_address)}"')
204
+
205
+ # Add display text if present
206
+ if hyperlink.text_to_display:
207
+ attrs.append(f'display="{self._escape_xml(hyperlink.text_to_display)}"')
208
+
209
+ # Add tooltip if present
210
+ if hyperlink.screen_tip:
211
+ attrs.append(f'tooltip="{self._escape_xml(hyperlink.screen_tip)}"')
212
+
213
+ return f' <hyperlink {" ".join(attrs)}/>\n'
214
+
215
+ def get_hyperlink_relationships(self, worksheet):
216
+ """
217
+ Gets hyperlink relationships for the worksheet.
218
+
219
+ Returns a list of relationship entries for external hyperlinks.
220
+
221
+ Args:
222
+ worksheet: The worksheet containing hyperlinks.
223
+
224
+ Returns:
225
+ list: List of (rel_id, target) tuples for external hyperlinks.
226
+
227
+ Examples:
228
+ >>> rels = saver.get_hyperlink_relationships(worksheet)
229
+ >>> for rel_id, target in rels:
230
+ ... print(f"{rel_id}: {target}")
231
+ """
232
+ relationships = []
233
+
234
+ for hyperlink in worksheet.hyperlinks:
235
+ if hyperlink.address: # External hyperlink
236
+ rel_id = hyperlink._relationship_id
237
+ target = hyperlink.address
238
+ relationships.append((rel_id, target))
239
+
240
+ return relationships
241
+
242
+ def reset_relationship_counter(self):
243
+ """
244
+ Resets the relationship ID counter.
245
+
246
+ Should be called before processing each worksheet.
247
+ """
248
+ self._next_rel_id = 1
249
+
250
+ def _escape_xml(self, text):
251
+ """
252
+ Escapes special characters for XML.
253
+
254
+ Args:
255
+ text (str): Text to escape.
256
+
257
+ Returns:
258
+ str: XML-escaped text.
259
+ """
260
+ if not text:
261
+ return ""
262
+ text = str(text)
263
+ text = text.replace('&', '&amp;')
264
+ text = text.replace('<', '&lt;')
265
+ text = text.replace('>', '&gt;')
266
+ text = text.replace('"', '&quot;')
267
+ text = text.replace("'", '&apos;')
268
+ return text
269
+
270
+
271
+ class HyperlinkRelationshipWriter:
272
+ """
273
+ Writes hyperlink relationships to _rels files.
274
+
275
+ Creates or updates xl/worksheets/_rels/sheet{n}.xml.rels with hyperlink relationships.
276
+ """
277
+
278
+ @staticmethod
279
+ def format_relationships_xml(relationships, existing_rels=None):
280
+ """
281
+ Formats relationships as XML.
282
+
283
+ Args:
284
+ relationships (list): List of (rel_id, target) tuples for hyperlinks.
285
+ existing_rels (list, optional): Existing non-hyperlink relationships to preserve.
286
+
287
+ Returns:
288
+ str: Complete relationships XML content.
289
+
290
+ Examples:
291
+ >>> xml = writer.format_relationships_xml([('rId1', 'https://example.com')])
292
+ """
293
+ content = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n'
294
+ content += '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">\n'
295
+
296
+ # Add existing non-hyperlink relationships first
297
+ if existing_rels:
298
+ for rel_id, rel_type, target, target_mode in existing_rels:
299
+ mode_attr = f' TargetMode="{target_mode}"' if target_mode else ''
300
+ content += f' <Relationship Id="{rel_id}" Type="{rel_type}" Target="{target}"{mode_attr}/>\n'
301
+
302
+ # Add hyperlink relationships
303
+ for rel_id, target in relationships:
304
+ escaped_target = HyperlinkRelationshipWriter._escape_xml(target)
305
+ content += f' <Relationship Id="{rel_id}" '
306
+ content += f'Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" '
307
+ content += f'Target="{escaped_target}" TargetMode="External"/>\n'
308
+
309
+ content += '</Relationships>\n'
310
+ return content
311
+
312
+ @staticmethod
313
+ def _escape_xml(text):
314
+ """Escapes special characters for XML."""
315
+ if not text:
316
+ return ""
317
+ text = str(text)
318
+ text = text.replace('&', '&amp;')
319
+ text = text.replace('<', '&lt;')
320
+ text = text.replace('>', '&gt;')
321
+ text = text.replace('"', '&quot;')
322
+ text = text.replace("'", '&apos;')
323
+ return text