Skip to content

feat(table): insert rows above a row and with empty cells - #431

Open
airmang wants to merge 16 commits into
mainfrom
feat/table-insert-row-sides
Open

airmang wants to merge 16 commits into
mainfrom
feat/table-insert-row-sides

Conversation

@airmang

@airmang airmang commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

바뀐 점

행 삽입 insert_row_by_clone(hwpx.table_patch.apply_table_ops)은 기준 행 아래에만 넣을 수 있었고, 기준 칸의 글까지 복제했다. 이제 두 선택지를 받는다.

  • side: "above": 기준 행 위에 넣는다.
  • blank: true: 새 칸에 그 칸 서식의 빈 문단 하나만 둔다(한/글과 같음).
apply_table_ops(data, [{"op": "insert_row_by_clone", "table_index": 0, "ref_row": 1,
                        "side": "above", "count": 2, "blank": True}])

한/글의 동작

한/글로 3×3 표를 만들고, 칸에 커서를 둔 채 줄을 추가해 저장했다.

  • 새 행은 기준 행의 저장 높이를 받는다(위·아래 같음).
  • 새 칸은 같은 열 기준 칸의 테두리, 문단 모양, 글자 모양을 따르고 글은 비어 있다.
    • 기준 행에 열을 합친 칸이 있으면 새 행에도 같은 폭의 합친 칸이 생긴다.
  • 세로 합친 칸
    • 삽입 선을 가로지르면 rowSpan이 는다(저장 높이는 그대로).
    • 그 경계에서 끝나거나(아래쪽) 시작하면(위쪽) rowSpan은 그대로이고, 새 행에 그 열의 칸이 따로 생긴다.
  • 한/글은 표 sz의 높이를 배치 높이로 다시 저장한다. python-hwpx는 전처럼 그대로 두고, 한/글은 열 때 표를 다시 배치한다.

insert_row_by_clone은 이 규칙을 따른다. 위쪽 삽입은 아래쪽 규칙을 뒤집은 것이다.

칸 영역

칸 영역(hp:cellzone)은 그 영역 칸들에 테두리·배경을 주고, 한/글은 이를 그린다. 행 삽입과 행·열 삭제가 영역을 옮기지 않아, 남은 영역이 다른 칸에 테두리를 그렸다. 이제 영역이 칸과 함께 움직인다.

  • 행 삽입(아래쪽 포함): 새 행 뒤의 영역은 밀고, 새 행을 가로지르는 영역은 넓힌다. 열 삽입과 같은 도우미 _shift_zones를 쓴다.
  • 행·열 삭제: 영역은 남는 줄만큼 줄고 그 앞에서 지워진 줄만큼 당겨진다. 줄이 모두 지워진 영역은 없어지고, 마지막 영역이면 목록째 없어진다(OWPML은 목록에 영역이 하나 이상이어야 한다).
  • 열 삭제 뒤 칸이 남지 않은 행을 접을 때는, 칸 합치기가 그런 줄을 접을 때처럼 영역을 이웃 줄로 접는다.

코드

  • table_patch.py
    • _clone_row_template과 _insert_row_by_clone이 side, blank를 받는다.
    • _shift_zone_columns를 행·열 공용 _shift_zones로 바꿈.
    • _drop_zone_lines(행·열 삭제), _collapse_zone_line(빈 행 접기)을 더함.
    • 두 op 표가 side, blank를 넘긴다.
    • apply_table_ops 설명.
  • 변경 로그.

테스트

tests/test_insert_row_sides.py를 더하고, 한/글 저장본 스무 개(기준 표 5, 한/글이 행을 넣은 표 15)를 함께 넣는다.

  • 한/글이 넣은 행과의 대조(15쌍)
    • 대상: 아래, 위, 3개, 높은 행 옆(셋), 세로 합친 칸(가로지름·끝남·시작함, 다섯), 가로 합친 칸(아래·위), 열마다 다른 서식(아래·위).
    • 한/글이 만든 기준 표에 blank: true로 같은 행을 넣는다.
    • 칸마다 주소, 범위, 폭, 높이, 테두리, 문단·글자 모양, 글이 한/글 결과와 같은지 본다.
    • rowCnt와 표 폭이 같은지, 표 높이는 그대로인지 본다.
  • 그 밖에 시험하는 것
    • 위쪽 복제의 글 순서, 빈 행.
    • 위쪽 삽입에서의 합친 칸 둘.
    • 칸 영역 이동(아래·위).
    • 잘못된 side의 거부.

tests/test_table_ops_delete_zones.py를 더한다. 행·열 삭제에서 영역이 당겨지고, 줄고, 없어지는지, 마지막 영역과 함께 목록이 없어지는지 본다. 고치기 전에는 영역이 있는 3판이 모두 틀렸다.

이 PR은 #430 위에 쌓았다. #430이 병합되면 그 커밋들은 이 PR의 diff에서 빠진다.

🤖 Generated with Claude Code

airmang and others added 10 commits October 6, 2026 17:03
apply_table_ops could insert rows (insert_row_by_clone) but no columns.
insert_column_by_clone inserts count columns right of grid column
ref_col (left of it with side "left"), cloning it:

- each new column is as wide as ref_col, and the table grows by it;
- each new cell takes the format of the cell of the same row in
  ref_col, and its text unless blank is set, which leaves one empty
  paragraph as Hangul does;
- a cell merged across the new columns grows its colSpan and width,
  next to a merged cell ending (on the left, starting) at ref_col each
  new column gets an empty cell of its format and rows, and a cell
  merged down ref_col is cloned with its rows;
- cell zones move with their columns.

Eighteen Hangul-saved files: 3x3 tables made by Hangul and the same
tables after Hangul inserted columns (right, left, three, right of a
wider column, merged cells across, ending at and down the column,
formats per row, a table as wide as the text). With blank the inserted
tables match Hangul's cell for cell, but for the stored width of a cell
merged across the new column, which Hangul leaves as it was and draws
by the grid.

The row insertion shares its paragraph id helper (_fresh_ids).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tables written by other programs can hold hp:tc elements with their
paragraphs straight inside, without hp:subList, hp:cellAddr, hp:cellSpan
or hp:cellSz. Deleting a row or a column, inserting rows or columns,
setting or fitting column widths and splitting the table stopped on an
AssertionError there, and inserting a row or reordering rows refused it
for an unrelated reason. _guard_flat, which every one of these edits
calls first, now refuses such a table as it refuses one holding a table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
delete_column on the only column of a one-column table, or on every
column, stopped on a ZeroDivisionError sharing out the freed width
among no columns left. delete_row on every row left a table without
rows and refused it only for the grid it left. Hangul does not delete
the last row or column either; both now refuse and point to
delete_table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
insert_row_by_clone inserted rows below ref_row only, keeping the text
of the cells it clones. It now takes side "above" to insert before the
row, and blank to leave each new cell one empty paragraph of its format
as Hangul does:

- each new row is as high as ref_row, and each new cell takes the
  format of the cell of the same column in ref_row, a cell merged
  across columns there included;
- above the row, a cell merged across the new rows grows its rowSpan
  over them, and over a merged cell starting at ref_row each new row
  gets an empty cell of its format, as below the row for one ending
  there;
- cell zones now move with their rows, below the row too.

Twenty Hangul-saved files: 3x3 tables made by Hangul and the same
tables after Hangul inserted rows (below, above, three, next to a
taller row, merged cells across, ending at and starting at the row,
a cell merged across columns, formats per column). With blank the
inserted tables match Hangul's cell for cell; Hangul also stores the
table's laid-out height anew, which python-hwpx leaves as it was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
delete_row and delete_column left the table's hp:cellzone entries where
they were, so a zone drew its border fill on the cells that moved into
its rows or columns. A zone now keeps the lines it covers that stay,
moved back over the deleted ones before it, and goes when none stays;
the zone list goes with its last zone, as OWPML wants one in it. A row
the column deletion leaves without cells folds its zones into its
neighbours, as a cell merge folds such a line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant