python-hwpx 2.2__tar.gz → 2.3__tar.gz
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.
- {python_hwpx-2.2 → python_hwpx-2.3}/PKG-INFO +1 -1
- {python_hwpx-2.2 → python_hwpx-2.3}/pyproject.toml +1 -1
- {python_hwpx-2.2 → python_hwpx-2.3}/src/hwpx/oxml/document.py +78 -38
- {python_hwpx-2.2 → python_hwpx-2.3}/src/python_hwpx.egg-info/PKG-INFO +1 -1
- {python_hwpx-2.2 → python_hwpx-2.3}/tests/test_document_formatting.py +42 -16
- {python_hwpx-2.2 → python_hwpx-2.3}/LICENSE +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/README.md +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/setup.cfg +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/src/hwpx/__init__.py +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/src/hwpx/data/Skeleton.hwpx +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/src/hwpx/document.py +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/src/hwpx/opc/package.py +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/src/hwpx/opc/xml_utils.py +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/src/hwpx/oxml/__init__.py +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/src/hwpx/oxml/body.py +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/src/hwpx/oxml/common.py +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/src/hwpx/oxml/header.py +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/src/hwpx/oxml/header_part.py +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/src/hwpx/oxml/memo.py +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/src/hwpx/oxml/paragraph.py +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/src/hwpx/oxml/parser.py +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/src/hwpx/oxml/schema.py +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/src/hwpx/oxml/section.py +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/src/hwpx/oxml/table.py +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/src/hwpx/oxml/utils.py +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/src/hwpx/package.py +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/src/hwpx/py.typed +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/src/hwpx/templates.py +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/src/hwpx/tools/__init__.py +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/src/hwpx/tools/_schemas/header.xsd +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/src/hwpx/tools/_schemas/section.xsd +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/src/hwpx/tools/object_finder.py +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/src/hwpx/tools/text_extractor.py +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/src/hwpx/tools/validator.py +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/src/python_hwpx.egg-info/SOURCES.txt +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/src/python_hwpx.egg-info/entry_points.txt +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/src/python_hwpx.egg-info/requires.txt +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/src/python_hwpx.egg-info/top_level.txt +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/tests/test_coverage_targets.py +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/tests/test_document_context_manager.py +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/tests/test_document_save_api.py +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/tests/test_inline_models.py +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/tests/test_integration_hwpx_compatibility.py +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/tests/test_memo_and_style_editing.py +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/tests/test_opc_package.py +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/tests/test_oxml_parsing.py +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/tests/test_packaging_py_typed.py +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/tests/test_repr_snapshots.py +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/tests/test_section_headers.py +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/tests/test_tables_default_border.py +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/tests/test_text_extractor_annotations.py +0 -0
- {python_hwpx-2.2 → python_hwpx-2.3}/tests/test_version_metadata.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-hwpx"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.3"
|
|
8
8
|
description = "Hancom HWPX 패키지를 로드하고 편집하기 위한 Python 유틸리티 모음"
|
|
9
9
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -1782,33 +1782,71 @@ class HwpxOxmlTable:
|
|
|
1782
1782
|
def rows(self) -> list[HwpxOxmlTableRow]:
|
|
1783
1783
|
return [HwpxOxmlTableRow(row, self) for row in self.element.findall(f"{_HP}tr")]
|
|
1784
1784
|
|
|
1785
|
-
def _build_cell_grid(self) -> dict[tuple[int, int], HwpxTableGridPosition]:
|
|
1786
|
-
mapping: dict[tuple[int, int], HwpxTableGridPosition] = {}
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1785
|
+
def _build_cell_grid(self) -> dict[tuple[int, int], HwpxTableGridPosition]:
|
|
1786
|
+
mapping: dict[tuple[int, int], HwpxTableGridPosition] = {}
|
|
1787
|
+
|
|
1788
|
+
def _is_deactivated_cell(
|
|
1789
|
+
cell: HwpxOxmlTableCell, span: tuple[int, int]
|
|
1790
|
+
) -> bool:
|
|
1791
|
+
span_row, span_col = span
|
|
1792
|
+
if span_row != 1 or span_col != 1:
|
|
1793
|
+
return False
|
|
1794
|
+
if cell.width != 0 or cell.height != 0:
|
|
1795
|
+
return False
|
|
1796
|
+
for text_element in cell.element.findall(f".//{_HP}t"):
|
|
1797
|
+
if text_element.text:
|
|
1798
|
+
return False
|
|
1799
|
+
return True
|
|
1800
|
+
|
|
1801
|
+
for row in self.element.findall(f"{_HP}tr"):
|
|
1802
|
+
for cell_element in row.findall(f"{_HP}tc"):
|
|
1803
|
+
wrapper = HwpxOxmlTableCell(cell_element, self, row)
|
|
1804
|
+
start_row, start_col = wrapper.address
|
|
1805
|
+
span_row, span_col = wrapper.span
|
|
1806
|
+
wrapper_span = (span_row, span_col)
|
|
1807
|
+
wrapper_is_deactivated = _is_deactivated_cell(wrapper, wrapper_span)
|
|
1808
|
+
for logical_row in range(start_row, start_row + span_row):
|
|
1809
|
+
for logical_col in range(start_col, start_col + span_col):
|
|
1810
|
+
key = (logical_row, logical_col)
|
|
1811
|
+
existing = mapping.get(key)
|
|
1812
|
+
entry = HwpxTableGridPosition(
|
|
1797
1813
|
row=logical_row,
|
|
1798
1814
|
column=logical_col,
|
|
1799
1815
|
cell=wrapper,
|
|
1800
1816
|
anchor=(start_row, start_col),
|
|
1801
|
-
span=(span_row, span_col),
|
|
1802
|
-
)
|
|
1803
|
-
if (
|
|
1804
|
-
existing is not None
|
|
1805
|
-
and existing.cell.element is not wrapper.element
|
|
1806
|
-
):
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1817
|
+
span=(span_row, span_col),
|
|
1818
|
+
)
|
|
1819
|
+
if (
|
|
1820
|
+
existing is not None
|
|
1821
|
+
and existing.cell.element is not wrapper.element
|
|
1822
|
+
):
|
|
1823
|
+
existing_span = existing.span
|
|
1824
|
+
existing_spans_multiple = (
|
|
1825
|
+
existing_span[0] != 1 or existing_span[1] != 1
|
|
1826
|
+
)
|
|
1827
|
+
wrapper_spans_multiple = (
|
|
1828
|
+
wrapper_span[0] != 1 or wrapper_span[1] != 1
|
|
1829
|
+
)
|
|
1830
|
+
existing_is_deactivated = _is_deactivated_cell(
|
|
1831
|
+
existing.cell, existing_span
|
|
1832
|
+
)
|
|
1833
|
+
|
|
1834
|
+
if (
|
|
1835
|
+
wrapper_is_deactivated
|
|
1836
|
+
and existing_spans_multiple
|
|
1837
|
+
):
|
|
1838
|
+
continue
|
|
1839
|
+
if (
|
|
1840
|
+
existing_is_deactivated
|
|
1841
|
+
and wrapper_spans_multiple
|
|
1842
|
+
):
|
|
1843
|
+
mapping[key] = entry
|
|
1844
|
+
continue
|
|
1845
|
+
raise ValueError(
|
|
1846
|
+
"table grid contains overlapping cell spans"
|
|
1847
|
+
)
|
|
1848
|
+
mapping[key] = entry
|
|
1849
|
+
return mapping
|
|
1812
1850
|
|
|
1813
1851
|
def _grid_entry(self, row_index: int, col_index: int) -> HwpxTableGridPosition:
|
|
1814
1852
|
if row_index < 0 or col_index < 0:
|
|
@@ -2096,21 +2134,23 @@ class HwpxOxmlTable:
|
|
|
2096
2134
|
if cell.element is not target.element:
|
|
2097
2135
|
removal_elements.add(cell.element)
|
|
2098
2136
|
|
|
2099
|
-
if not removal_elements and target.span == (new_row_span, new_col_span):
|
|
2100
|
-
return target
|
|
2101
|
-
|
|
2102
|
-
for element in removal_elements:
|
|
2103
|
-
row_element = element_to_row.get(element)
|
|
2104
|
-
if row_element is
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2137
|
+
if not removal_elements and target.span == (new_row_span, new_col_span):
|
|
2138
|
+
return target
|
|
2139
|
+
|
|
2140
|
+
for element in removal_elements:
|
|
2141
|
+
row_element = element_to_row.get(element)
|
|
2142
|
+
if row_element is None:
|
|
2143
|
+
continue
|
|
2144
|
+
wrapper = HwpxOxmlTableCell(element, self, row_element)
|
|
2145
|
+
wrapper.set_span(1, 1)
|
|
2146
|
+
wrapper.set_size(0, 0)
|
|
2147
|
+
for text_element in element.findall(f".//{_HP}t"):
|
|
2148
|
+
text_element.text = ""
|
|
2149
|
+
|
|
2150
|
+
target.set_span(new_row_span, new_col_span)
|
|
2151
|
+
target.set_size(total_width or target.width, total_height or target.height)
|
|
2152
|
+
self.mark_dirty()
|
|
2153
|
+
return target
|
|
2114
2154
|
|
|
2115
2155
|
@dataclass
|
|
2116
2156
|
class HwpxOxmlParagraph:
|
|
@@ -413,28 +413,54 @@ def test_table_cell_text_marks_cell_dirty_attribute() -> None:
|
|
|
413
413
|
assert table.cell(0, 0).element.get("dirty") == "1"
|
|
414
414
|
|
|
415
415
|
|
|
416
|
-
def test_table_merge_cells_updates_spans_and_structure() -> None:
|
|
416
|
+
def test_table_merge_cells_updates_spans_and_structure() -> None:
|
|
417
417
|
section_element = ET.Element(f"{HS}sec")
|
|
418
418
|
section = HwpxOxmlSection("section0.xml", section_element)
|
|
419
419
|
manifest = ET.Element("manifest")
|
|
420
420
|
root = HwpxOxmlDocument(manifest, [section], [])
|
|
421
421
|
document = HwpxDocument(cast(HwpxPackage, object()), root)
|
|
422
422
|
|
|
423
|
-
table = document.add_table(3, 3, section=section)
|
|
424
|
-
initial_width = table.cell(0, 0).width
|
|
425
|
-
initial_height = table.cell(0, 0).height
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
423
|
+
table = document.add_table(3, 3, section=section)
|
|
424
|
+
initial_width = table.cell(0, 0).width
|
|
425
|
+
initial_height = table.cell(0, 0).height
|
|
426
|
+
table.set_cell_text(0, 1, "Top Right")
|
|
427
|
+
table.set_cell_text(1, 0, "Bottom Left")
|
|
428
|
+
table.set_cell_text(1, 1, "Bottom Right")
|
|
429
|
+
section.reset_dirty()
|
|
430
|
+
|
|
431
|
+
merged = table.merge_cells(0, 0, 1, 1)
|
|
432
|
+
|
|
433
|
+
physical_by_address = {
|
|
434
|
+
cell.address: cell for row in table.rows for cell in row.cells
|
|
435
|
+
}
|
|
436
|
+
covered_top_right = physical_by_address[(0, 1)]
|
|
437
|
+
covered_bottom_left = physical_by_address[(1, 0)]
|
|
438
|
+
covered_bottom_right = physical_by_address[(1, 1)]
|
|
439
|
+
|
|
440
|
+
assert merged.span == (2, 2)
|
|
441
|
+
assert merged.width >= initial_width
|
|
442
|
+
assert merged.height >= initial_height
|
|
443
|
+
assert table.cell(0, 1).element is merged.element
|
|
444
|
+
assert table.cell(1, 0).element is merged.element
|
|
445
|
+
assert table.cell(1, 1).element is merged.element
|
|
446
|
+
assert len(table.rows[0].cells) == 3
|
|
447
|
+
assert len(table.rows[1].cells) == 3
|
|
448
|
+
assert covered_top_right.element is not merged.element
|
|
449
|
+
assert covered_top_right.span == (1, 1)
|
|
450
|
+
assert covered_top_right.width == 0
|
|
451
|
+
assert covered_top_right.height == 0
|
|
452
|
+
assert covered_top_right.text == ""
|
|
453
|
+
assert covered_bottom_left.element is not merged.element
|
|
454
|
+
assert covered_bottom_left.span == (1, 1)
|
|
455
|
+
assert covered_bottom_left.width == 0
|
|
456
|
+
assert covered_bottom_left.height == 0
|
|
457
|
+
assert covered_bottom_left.text == ""
|
|
458
|
+
assert covered_bottom_right.element is not merged.element
|
|
459
|
+
assert covered_bottom_right.span == (1, 1)
|
|
460
|
+
assert covered_bottom_right.width == 0
|
|
461
|
+
assert covered_bottom_right.height == 0
|
|
462
|
+
assert covered_bottom_right.text == ""
|
|
463
|
+
assert section.dirty is True
|
|
438
464
|
|
|
439
465
|
|
|
440
466
|
def test_table_merge_cells_rejects_partial_overlap() -> None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|