Skip to content

feat(table): split a cell into rows and columns as Hancom does - #432

Open
airmang wants to merge 16 commits into
mainfrom
feat/table-split-cell
Open

airmang wants to merge 16 commits into
mainfrom
feat/table-split-cell

Conversation

@airmang

@airmang airmang commented Oct 6, 2026

Copy link
Copy Markdown
Owner

바뀐 점

표 구조 편집(hwpx.table_patch.apply_table_ops)은 세로로 합친 칸을 쌓인 칸들로 되돌리는 것(split_cell_vertical)만 할 수 있었다. 칸을 열로 나누거나, 한 행짜리 칸을 여러 줄로 나눌 수 없었다. 이제 한/글의 셀 나누기처럼 칸을 줄·칸으로 나눈다.

apply_table_ops(data, [{"op": "split_cell", "table_index": 0, "row": 1, "col": 1, "cols": 3}])
apply_table_ops(data, [{"op": "split_cell", "table_index": 0, "row": 1, "col": 1, "rows": 2, "cols": 2}])
  • row, col: 나눌 칸의 주소.
  • rows, cols: 2~63(한/글과 같은 한도). 둘 다 주면 열로 먼저 나누고, 나뉜 칸마다 줄로 나눈다.

한/글의 동작

한/글로 3×3 표를 만들고, 칸에 커서를 둔 채 셀 나누기를 해 저장했다.

  • 열로 나누기
    • 칸 폭을 똑같이 나누고, 나머지는 마지막 칸이 갖는다(13984 → 4661·4661·4662).
    • 나뉜 칸의 경계에 새 격자 선이 생긴다.
    • 그 선을 가로지르는 다른 칸은 폭은 그대로이고, 덮는 열만큼 colSpan이 는다.
    • 두 열을 합친 칸을 2칸으로 나누면 이미 있는 선에서 나뉘어 격자가 그대로다.
    • 3칸으로 나누면 새 선 둘이 생기고, 가운데 칸은 두 격자 열을 덮는다.
  • 줄로 나누기
    • 칸이 덮는 행이 똑같이 나뉘면 그 행들에서 나눈다.
    • 아니면 한 행씩 나누고, 모자라는 행을 칸의 마지막 행 뒤에 더한다.
    • 그 행 옆 칸은 새 행까지 덮도록 rowSpan이 는다.
  • 글은 첫 칸에 남는다. 나머지 칸은 그 칸의 테두리·문단 모양·글자 모양을 따른 빈 칸이다.
  • 표 폭(sz)은 그대로다.

split_cell은 이 규칙을 따른다. 다른 점은 줄로 나눈 칸의 저장 높이 하나다.

  • 한/글은 내용으로 높이를 정한다. 마지막 칸은 남는 높이를 받아, 내용보다 낮은 행에서는 0이나 음수가 된다(282 → 1282·0).
  • split_cell은 칸의 저장 높이를 똑같이 나눈다. 한/글에서 '줄 높이를 같게 나누기'를 켠 결과와 같다(3282 → 1641·1641). 한/글은 이 선택이 꺼진 것이 기본이라, 내용보다 높은 행에서는 경계선 자리가 다르다(1282·2000).
  • 어느 쪽이든 한/글은 칸을 내용만큼 그린다. 표 높이(sz)는 전처럼 그대로 둔다.

칸 영역(hp:cellzone)은 덮던 자리를 그대로 덮는다. 열은 좌표로 다시 매기고, 줄은 새 행까지 넓힌다.

코드

  • table_patch.py
    • _split_cell, _split_cell_columns, _split_cell_rows, _cell_at, _regrid_zone_columns, SPLIT_CELL_MAX를 더함.
    • _shift_zones가 touching(새 행 바로 앞에서 끝나는 영역도 넓힘)을 받는다.
    • _blank_cell_text에서 _drop_line_caches를 뺌. 폭이 바뀐 첫 칸의 줄 캐시를 지운다.
    • apply_table_ops 설명, 두 op 표.
  • docs/editor-menu-reverse-map.md: "셀 나누기" 행.
  • 변경 로그.

테스트

tests/test_split_cell.py를 더하고, 한/글 저장본 열아홉 개(기준 표 6, 한/글이 나눈 표 13)를 함께 넣는다.

  • 높이까지 한/글과 같은 판(7쌍)
    • 2칸, 3칸, 넓은 열 2칸, 두 열 합친 칸 2칸·3칸, 서식 칸 2칸, 높은 행 2줄(고르게).
    • 칸마다 주소, 범위, 폭, 높이, 테두리, 문단·글자 모양, 글을 보고, rowCnt·colCnt·표 sz도 맞춘다.
  • 높이 말고는 한/글과 같은 판(6쌍)
    • 2줄, 3줄, 2×2, 세로 합친 칸 2줄·3줄, 서식 칸 2줄.
  • 저장 높이를 똑같이 나누는지(4판), 칸 영역, 거부(1개, 64칸, 칸이 시작하지 않는 주소).

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

🤖 Generated with Claude Code

airmang and others added 12 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>
apply_table_ops could split a cell merged down several rows back into
stacked cells (split_cell_vertical), but not split a cell into columns
or a single row into several. split_cell takes the cell's row and col
and rows and/or cols (2..63):

- into columns the cell's width is shared out evenly, the last part
  taking what is left over; new grid lines go where the parts end, and
  every other cell keeps its width over the grid columns it now covers;
- into rows the parts take the rows the cell covers when they divide
  evenly, else one row each and as many new rows after its last as it
  lacks, which the cells beside that row grow over; the parts share
  out the cell's stored height evenly;
- the content stays in the first part, and the others hold one empty
  paragraph of the cell's format; cell zones keep the area they cover.

Nineteen Hangul-saved files: 3x3 tables made by Hangul and the same
tables after Hangul split a cell (into two and three columns and rows,
both, in a wider column and a taller row, merged across columns and
down rows, a formatted cell). Column splits and the even split of a
taller row match Hangul's cell for cell. For rows lower than their
content Hangul stores heights from the content (0 or less for the last
part) and lays the cells out to their content either way.

_drop_line_caches is split out of _blank_cell_text for the first part.

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