Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,20 @@

모든 중요한 변경 사항은 이 문서에 기록됩니다. 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)과 [Semantic Versioning](https://semver.org/lang/ko/)을 따릅니다.

## [Unreleased] - 2026-04-01
## [2.9.0] - 2026-04-02
### 추가
- `HwpxDocument.get_table_map()`, `find_cell_by_label()`, `fill_by_path()`를 추가해 HWPX 양식/템플릿 표를 문서 순서 기반으로 탐색하고 채울 수 있게 했습니다.
- `hwpx.tools.table_navigation` 모듈을 추가해 엔진 레벨에서 재사용 가능한 표 탐색, 라벨 정규화, 방향 이동, 배치 채우기 helper를 공개했습니다.

### 변경
- 라벨 매칭이 공백 축약, 대소문자 무시, 후행 콜론 허용 규칙을 따르도록 정규화 로직을 추가했습니다.
- 표 자동화 API에 대한 회귀 테스트와 README/API 레퍼런스 문서를 추가했습니다.

## [2.8.3] - 2026-03-10
### 변경
- `hp:tab` 및 `ctrl id="tab"` 지원을 README와 usage 문서에 반영했습니다.
- `Paragraph.text`, `TextExtractor`, 텍스트/HTML/Markdown exporter가 탭 의미를 `\t`로 보존한다는 점을 문서화했습니다.
- `preserve_breaks` 옵션이 탭/줄바꿈 평탄화 여부를 제어한다는 설명을 보강했습니다.
- 저장소와 배포 메타데이터의 라이선스 표기를 실제 `LICENSE` 파일과 일치하도록 정렬했습니다.
- `pyproject.toml`을 PEP 639 방식의 `LicenseRef-python-hwpx-NonCommercial` + `license-files` 구성으로 갱신하고, 잘못된 MIT 분류자를 제거했습니다.
- README 라이선스 배지/섹션을 커스텀 비상업적 라이선스 기준으로 수정하고, wheel/sdist 산출물의 라이선스 메타데이터를 검증하는 회귀 테스트를 추가했습니다.

## [2.8.2] - 2026-03-08
### 변경
Expand Down
41 changes: 41 additions & 0 deletions DevDoc/license-alignment-audit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# License Alignment Audit

Date: 2026-03-10

## Files inspected

- `LICENSE`
- `README.md`
- `pyproject.toml`
- `docs/conf.py`
- `CONTRIBUTING.md`
- `.github/workflows/release.yml`
- `.github/workflows/tests.yml`
- `scripts/build-and-publish.sh`
- `tests/test_packaging_py_typed.py`
- Repo-wide searches across `docs/`, `.github/`, `DevDoc/`, `CHANGELOG.md`, and the repository root for license-related metadata and MIT references

## Contradictions found before this change

- `LICENSE` defined a custom non-commercial license and named `python-hwpx Maintainers` as the copyright holder.
- `README.md` showed an MIT badge and an MIT license section, which contradicted the actual license text.
- `README.md` attributed the license line to `고규현 (Kyuhyun Koh)`, while the `LICENSE` file and package metadata used `python-hwpx Maintainers`.
- `pyproject.toml` used the legacy `license = { file = "LICENSE" }` form and also published the classifier `License :: OSI Approved :: MIT License`, which falsely represented the distribution as MIT-licensed.

## Source of truth

- The repository root `LICENSE` file is the source of truth for license terms.
- This audit treats the project as remaining under its existing custom non-commercial license. No evidence of an intentional relicensing to MIT was found elsewhere in the repository.

## Decision summary

- Preserve the current non-commercial custom license.
- Align public-facing metadata and README wording to that license.
- Use modern packaging metadata that points built distributions back to the root `LICENSE` file without inventing an OSI identifier.
- Remove conflicting MIT wording and the MIT trove classifier rather than replacing it with another potentially ambiguous license classifier.

## Notes on surfaces inspected

- `docs/conf.py` already used `python-hwpx Maintainers` and did not restate MIT licensing.
- No GitHub Pages or docs markdown pages were found to restate the project license.
- The release workflow already builds distributions and runs `twine check`, so it was left in place and used for verification after the metadata update.
28 changes: 28 additions & 0 deletions DevDoc/license-metadata-policy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# License Metadata Policy

## Source of truth

- The root `LICENSE` file defines the project's license terms.
- Metadata changes must reflect the current `LICENSE` text. Do not treat README text, badges, or historical PyPI metadata as authoritative.

## Packaging rule

- `pyproject.toml` must represent the current custom license with `project.license = "LicenseRef-python-hwpx-NonCommercial"`.
- `pyproject.toml` must list `project.license-files = ["LICENSE"]` so both `sdist` and `wheel` carry the license file.
- Keep the build backend compatible with that metadata format by requiring `setuptools>=77.0.0`.

## Classifier rule

- Do not add `License ::` trove classifiers for this project unless the `LICENSE` file changes to a classifier-backed license and the classifier is verified to be accurate.
- For the current custom non-commercial license, leaving license classifiers unset is less ambiguous than picking an approximate classifier.

## README rule

- The README badge and license section must describe the project as using a custom non-commercial license and link to `LICENSE`.
- If contact information is updated, keep it distinct from the copyright/licensing line unless the `LICENSE` file is updated too.

## Verification rule

- Before release or after touching license metadata, run `python -m build` and `twine check dist/*`.
- Inspect built `PKG-INFO` and wheel `METADATA` for `License-Expression: LicenseRef-python-hwpx-NonCommercial` and `License-File: LICENSE`.
- Confirm the wheel contains `.dist-info/licenses/LICENSE` and the sdist contains the root `LICENSE` file.
22 changes: 18 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<p align="center">
<a href="https://pypi.org/project/python-hwpx/"><img src="https://img.shields.io/pypi/v/python-hwpx?color=blue&label=PyPI" alt="PyPI"></a>
<a href="https://pypi.org/project/python-hwpx/"><img src="https://img.shields.io/pypi/pyversions/python-hwpx" alt="Python"></a>
<a href="https://github.com/airmang/python-hwpx/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="License"></a>
<a href="https://github.com/airmang/python-hwpx/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-Custom%20Noncommercial-orange" alt="License: Custom Non-Commercial"></a>
<a href="https://airmang.github.io/python-hwpx/"><img src="https://img.shields.io/badge/docs-Sphinx-8CA1AF" alt="Docs"></a>
</p>
</p>
Expand Down Expand Up @@ -86,6 +86,7 @@ doc.save_to_path("결과물.hwpx")
| 📝 **단락** | 추가/삭제/편집/서식 | 텍스트 설정, 단락 삭제(`remove_paragraph`), 스타일 참조 |
| ✏️ **Run** | 텍스트 조각 | 추가, 교체, 볼드/이탤릭/밑줄/색상 서식 |
| 📊 **표(Table)** | 생성/편집/병합 | N×M 표 생성, 셀 텍스트, 셀 병합/분할, 중첩 테이블 |
| 🧭 **표 자동화** | 탐색/채우기 | 테이블 맵, 라벨 기반 셀 탐색, 경로 기반 배치 채우기 |
| 📑 **섹션** | 추가/삭제 | `add_section(after=)`, `remove_section()`, manifest 자동 관리 |
| 🖼️ **이미지** | 임베드/삭제 | 바이너리 데이터 관리, manifest 자동 등록 |
| ✏️ **도형** | 선/사각형/타원 | OWPML 명세 준수 도형 삽입 |
Expand Down Expand Up @@ -126,6 +127,17 @@ doc.set_footer_text("1 / 10", page_type="BOTH")
# 표 셀 병합·분할
table.merge_cells(0, 0, 1, 1) # (0,0)~(1,1) 병합
table.set_cell_text(0, 0, "병합된 셀", logical=True, split_merged=True)

# 양식형 표 자동 채우기
form = doc.add_table(2, 2)
form.cell(0, 0).text = "성명:"
form.cell(1, 0).text = "소속"

doc.find_cell_by_label("성명") # {"matches": [...], "count": 1}
doc.fill_by_path({
"성명 > right": "홍길동",
"소속 > right": "플랫폼팀",
})
```

### 🔍 텍스트 추출 & 검색
Expand Down Expand Up @@ -257,13 +269,15 @@ pytest

## License

[MIT](LICENSE) © 고규현 (Kyuhyun Koh)
[Custom Non-Commercial License](LICENSE) © python-hwpx Maintainers

Commercial use requires separate permission from the copyright holders.

<br>

## Author
## Maintainer

**고규현** — 광교고등학교 정보·컴퓨터 교사
Primary maintainer/contact: **고규현** — 광교고등학교 정보·컴퓨터 교사

- ✉️ [kokyuhyun@hotmail.com](mailto:kokyuhyun@hotmail.com)
- 🐙 [@airmang](https://github.com/airmang)
6 changes: 6 additions & 0 deletions docs/api_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,12 @@
- 섹션을 삭제합니다. 인스턴스 또는 인덱스를 받습니다. 마지막 섹션 삭제 시 `ValueError`가 발생합니다.
- `add_table(rows, cols, ...) -> HwpxOxmlTable`
- 단락을 삽입하고 그 안에 표 인라인 객체를 생성한 후, 표 래퍼를 반환합니다. `border_fill_id_ref`를 생략하면 헤더 참조 목록에 기본 실선 `borderFill`을 생성하고 표와 셀에 자동으로 연결합니다.
- `get_table_map() -> dict`
- 문서 순서대로 표를 스캔하고 `table_index`, `paragraph_index`, 행·열 수, 추정 헤더 텍스트, 첫 행 미리보기, 빈 표 여부를 반환합니다.
- `find_cell_by_label(label_text, direction="right") -> dict`
- 모든 표를 순회하며 라벨 셀을 찾고, `right`/`down` 방향으로 인접한 타깃 셀 정보를 모두 반환합니다. 라벨 매칭은 공백·대소문자·후행 콜론을 정규화합니다.
- `fill_by_path(mappings) -> dict`
- `"라벨 > 방향 > 방향"` 형식의 경로를 해석해 셀 값을 일괄 기록합니다. 라벨 미발견, 다중 후보, 범위 초과는 개별 실패 항목으로 보고하고 나머지 매핑은 계속 처리합니다.
- `add_shape(shape_type, ...) -> HwpxOxmlInlineObject`
- 새 단락에 태그 이름을 사용하여 인라인 그리기 요소를 삽입합니다.
- `add_control(...) -> HwpxOxmlInlineObject`
Expand Down
9 changes: 5 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
[build-system]
requires = ["setuptools", "wheel"]
requires = ["setuptools>=77.0.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "python-hwpx"
version = "2.8.2"
version = "2.9.0"
description = "Hancom HWPX 패키지를 로드하고 편집하기 위한 Python 유틸리티 모음"
readme = { file = "README.md", content-type = "text/markdown" }
license = { file = "LICENSE" }
license = "LicenseRef-python-hwpx-NonCommercial"
license-files = ["LICENSE"]
requires-python = ">=3.10"
authors = [
{ name = "python-hwpx Maintainers" },
Expand All @@ -16,7 +17,6 @@ keywords = ["hwp", "hwpx", "hancom", "opc", "xml"]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
Expand All @@ -35,6 +35,7 @@ dev = [
"pytest>=7.4",
]
test = [
"build>=1.0",
"pytest>=7.4",
"pytest-cov>=5.0",
]
Expand Down
6 changes: 5 additions & 1 deletion src/hwpx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ def _resolve_version() -> str:
except PackageNotFoundError:
return "0+unknown"

def __getattr__(name: str) -> object:
"""Resolve dynamic module attributes."""

__version__ = _resolve_version()
if name == "__version__":
return _resolve_version()
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

from .tools.text_extractor import (
DEFAULT_NAMESPACES,
Expand Down
33 changes: 32 additions & 1 deletion src/hwpx/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import uuid

from os import PathLike
from typing import Any, BinaryIO, Iterator, Sequence, overload
from typing import TYPE_CHECKING, Any, BinaryIO, Iterator, Mapping, Sequence, overload

from lxml import etree

Expand Down Expand Up @@ -53,6 +53,9 @@

logger = logging.getLogger(__name__)

if TYPE_CHECKING:
from .tools.table_navigation import TableFillResult, TableLabelSearchResult, TableMapResult


def _append_element(
parent: Any,
Expand Down Expand Up @@ -741,6 +744,34 @@ def add_table(
char_pr_id_ref=char_pr_id_ref,
)

def get_table_map(self) -> TableMapResult:
"""Return compact metadata for every table in document order."""

from .tools.table_navigation import get_table_map

return get_table_map(self)

def find_cell_by_label(
self,
label_text: str,
direction: str = "right",
) -> TableLabelSearchResult:
"""Return every label/target cell pair that matches *label_text*."""

from .tools.table_navigation import find_cell_by_label

return find_cell_by_label(self, label_text, direction=direction)

def fill_by_path(
self,
mappings: Mapping[str, str],
) -> TableFillResult:
"""Fill table cells using ``label > direction > ...`` navigation paths."""

from .tools.table_navigation import fill_by_path

return fill_by_path(self, mappings)

def add_shape(
self,
shape_type: str,
Expand Down
24 changes: 24 additions & 0 deletions src/hwpx/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,19 @@
describe_element_path,
strip_namespace,
)
from .table_navigation import (
TableCellReference,
TableFillApplied,
TableFillFailed,
TableFillResult,
TableLabelMatch,
TableLabelSearchResult,
TableMapEntry,
TableMapResult,
fill_by_path,
find_cell_by_label,
get_table_map,
)
from .validator import (
DocumentSchemas,
ValidationIssue,
Expand All @@ -41,6 +54,17 @@
"build_parent_map",
"describe_element_path",
"strip_namespace",
"TableCellReference",
"TableFillApplied",
"TableFillFailed",
"TableFillResult",
"TableLabelMatch",
"TableLabelSearchResult",
"TableMapEntry",
"TableMapResult",
"fill_by_path",
"find_cell_by_label",
"get_table_map",
"FoundElement",
"ObjectFinder",
"PackageValidationIssue",
Expand Down
Loading
Loading