@kimdayoun/hwpx-mcp 0.3.3 → 0.3.4
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.
- package/CHANGELOG.md +53 -0
- package/README.md +13 -3
- package/dist/HwpxDocument.d.ts +123 -0
- package/dist/HwpxDocument.js +828 -202
- package/dist/XmlWellFormed.d.ts +5 -0
- package/dist/XmlWellFormed.js +52 -0
- package/dist/index.js +22 -22
- package/package.json +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,59 @@
|
|
|
5
5
|
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/) 를 따르고,
|
|
6
6
|
버전은 [유의적 버전](https://semver.org/lang/ko/) 을 따릅니다.
|
|
7
7
|
|
|
8
|
+
## [0.3.4] - 2026-09-24
|
|
9
|
+
|
|
10
|
+
0.3.3 을 실제 업무 양식·실문서에 쓴 2차 사용 회신(6건)을 재현해 고쳤습니다.
|
|
11
|
+
같은 시험을 게시된 버전과 로컬 빌드에 돌리는 버전 매트릭스를 만들었고,
|
|
12
|
+
0.3.0 은 3/11, 0.3.3 은 5/11, 이 버전은 11/11 을 통과합니다.
|
|
13
|
+
변경 내역은 [#16](https://github.com/Dayoooun/hwpx-mcp/pull/16) 에 있습니다.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **행을 추가한 파일을 한/글이 열지 못하고 멈추던 문제.** 새 행 아래 행들의
|
|
18
|
+
`rowAddr` 를 다시 매기지 않아 같은 번호가 두 행에 생겼습니다. 사용자 신고는 한/글 2020,
|
|
19
|
+
재현은 한/글 2024 에서 했습니다. 한/글 원본 142건의
|
|
20
|
+
첫 표에서 0.3.3 은 142건 모두 번호가 겹쳤고, 이제 0건입니다. 세로 병합 한가운데에
|
|
21
|
+
넣으면 거부하고, 병합 아래에 넣은 새 행도 모든 열을 덮습니다.
|
|
22
|
+
- **표를 품은 문단을 `update_paragraph_text_preserve_styles` 로 고치면 문서가 깨지던 문제.**
|
|
23
|
+
문단 위치 캐시가 한/글 원본 문단 70,677개 중 16,271개에서 다른 문단을 가리켰고,
|
|
24
|
+
run 을 셀 때 표 칸 안 문단까지 셌습니다. 원본 60건 기준 제대로 바뀐 문단 3 → 55,
|
|
25
|
+
표 칸 글자가 바뀐 경우 44 → 0. 표 요소에 `update_paragraph_text` 를 걸면
|
|
26
|
+
성공이라 답하던 것도 이제 실패로 알려 줍니다.
|
|
27
|
+
- **`save_document(verify_integrity)` 가 깨진 XML 을 통과시키던 문제.** 글자 모양 세 가지만
|
|
28
|
+
보던 검사를 실제 XML 파싱으로 바꿨고, 실패하면 저장하지 않습니다. 한/글 원본 275건
|
|
29
|
+
(XML 2,046개)에서 잘못 거부한 것은 0건입니다.
|
|
30
|
+
- **그림이 든 표 칸에 글을 쓰면 저장본이 깨지던 문제.** 칸 글 정규식이 그림 안
|
|
31
|
+
`<hc:transMatrix>` 를 `<hc:t>` 로 읽었습니다. 한/글 원본 209건 중 18건에서
|
|
32
|
+
재현됐고 이제 0건입니다.
|
|
33
|
+
- **구역이 둘 이상인 문서에서 표 번호가 도구마다 다르던 문제.** `get_table_map` 이
|
|
34
|
+
`table_index_in_section` 도 함께 돌려주고, 도구 설명에 어느 번호를 쓰는지 적었습니다.
|
|
35
|
+
- **`insert_section`·`delete_section` 이 파일에 저장되지 않던 문제.** 두 구역 문서를
|
|
36
|
+
저장하고 다시 열면 1구역만 남았습니다.
|
|
37
|
+
- **열을 추가하면 표가 본문 폭을 넘던 문제.** 원래 표 폭 안에서 칸 폭을 같은 비율로
|
|
38
|
+
다시 나눕니다.
|
|
39
|
+
- **표 편집을 여러 번 한 뒤 저장하면 편집 순서가 뒤바뀌던 문제.** 저장할 때 칸 쓰기,
|
|
40
|
+
병합, 행·열 추가와 삭제를 호출한 순서가 아니라 종류별로 묶어 적용했습니다.
|
|
41
|
+
그래서 새로 넣은 행이나 열에 쓴 글이 빠지거나 다른 칸에 들어갔고, 행을 두 번 넣거나
|
|
42
|
+
지우면 두 번째 위치가 어긋났으며, 행을 넣은 뒤 그 아래를 병합하면 병합이 빠졌습니다.
|
|
43
|
+
0.3.3 에도 있던 문제로, 시험한 여섯 경우 모두 0.3.3 에서 틀리고 이 버전에서 맞습니다.
|
|
44
|
+
열을 지운 뒤 칸에 쓴 글이 빠지던 것(0.3.3 도 같음)과, 행을 지운 뒤 병합 옆에 행을 넣을 때
|
|
45
|
+
병합을 가르는지 한 행 어긋나게 판단하던 것도 함께 고쳤습니다.
|
|
46
|
+
- **병합이 한 행의 칸을 모두 덮으면 한/글이 파일을 변환하지 못하던 문제.** 두 행을 전체 폭으로
|
|
47
|
+
병합하거나 1열 표를 세로로 병합하면 저장본에 칸이 하나도 없는 행이 생겼고, 한/글 2024 는
|
|
48
|
+
이 파일에서 PDF 를 만들지 못했습니다. 한/글 원본 275건에는 칸 없는 행이 한 건도 없습니다.
|
|
49
|
+
이제 이런 병합은 어느 행이 비는지 알려 주고 거부합니다. 0.3.3 에도 있던 문제입니다.
|
|
50
|
+
- **글자 모양이 섞인 문단을 통째로 바꾸면 뒤쪽이 굵게 바뀌던 문제.** run 0 을 바꾸면
|
|
51
|
+
첫 run 의 글자 모양으로 문단 전체를 씁니다.
|
|
52
|
+
- **실패한 호출도 `isError: false` 로 오던 문제.**
|
|
53
|
+
|
|
54
|
+
### 알려진 제한
|
|
55
|
+
|
|
56
|
+
- 표 칸 글 쓰기에서, 칸 안에 문단이 여럿이거나 그림만 있는 칸은 글이 원래 자리에
|
|
57
|
+
들어가지 않을 수 있습니다(한/글 원본 209건 중 67건, 0.3.3 과 같음).
|
|
58
|
+
- 새로 만든 표는 바깥 여백 때문에 한/글에서 오른쪽 여백선을 1.16pt 넘습니다.
|
|
59
|
+
이 버전 이전부터 있던 동작입니다.
|
|
60
|
+
|
|
8
61
|
## [0.3.3] - 2026-09-24
|
|
9
62
|
|
|
10
63
|
0.3.2 는 npm 에 게시하지 않았습니다. 게시 전에 윈도우 한/글로 직접 렌더하다가,
|
package/README.md
CHANGED
|
@@ -298,14 +298,24 @@ AI 동작:
|
|
|
298
298
|
|
|
299
299
|
## 테스트
|
|
300
300
|
|
|
301
|
+
테스트는 네 층으로 나뉘고, CI(`.github/workflows/mcp-server-ci.yml`)가 층마다 따로 돌려
|
|
302
|
+
하나라도 실패하면 merge 를 막습니다.
|
|
303
|
+
|
|
301
304
|
```bash
|
|
302
305
|
cd mcp-server
|
|
303
|
-
npm test #
|
|
306
|
+
npm test # 모든 층을 한 번에 (vitest)
|
|
307
|
+
npm run test:unit # 단위: src/**/*.test.ts, tests/unit
|
|
308
|
+
npm run test:module # 모듈: 문서 API → 저장 → 다시 열기로 판정
|
|
309
|
+
npm run test:regression # 회귀: 신고 1건당 파일 1개, 신고 문장을 주석으로 붙임
|
|
310
|
+
npm run test:e2e # 종단간: 빌드한 MCP 서버를 stdio 로 띄워 호출
|
|
304
311
|
npm run test:security # 빌드 후 실제 MCP 저장 경로 공격·반복 저장·실패 정리 검사
|
|
305
|
-
|
|
306
|
-
node test-new-persistence-e2e.mjs # 8개 persistence E2E 테스트
|
|
312
|
+
npm run test:versions -- 0.3.3 local # 같은 e2e 를 게시된 버전과 로컬 빌드에 돌려 표로 비교
|
|
307
313
|
```
|
|
308
314
|
|
|
315
|
+
e2e 는 `HWPX_MCP_SERVER` 로 대상 서버를 고릅니다. `local`(기본)은 이 저장소의
|
|
316
|
+
`dist/index.js`, `npm:0.3.3` 은 게시된 버전입니다. `local` 서버를 띄울 Node 는
|
|
317
|
+
`HWPX_MCP_NODE` 로 바꿉니다(CI 는 Node 18·22 로 돌립니다).
|
|
318
|
+
|
|
309
319
|
문단 저장 회귀 테스트는 개인 PC의 문서 경로 대신 합성 HWPX를 사용합니다.
|
|
310
320
|
여러 `hp:t`로 나뉜 텍스트·빈 문자열·XML 특수문자·동일한 문단 ID/본문의 대상 선택을 검사하며,
|
|
311
321
|
중첩 표 삭제는 최상위 표 목록과 셀 내부 중첩 표의 보존을 각각 확인합니다.
|
package/dist/HwpxDocument.d.ts
CHANGED
|
@@ -70,6 +70,12 @@ export declare class HwpxDocument {
|
|
|
70
70
|
private _redoStack;
|
|
71
71
|
private _pendingTextReplacements;
|
|
72
72
|
private _pendingDirectTextUpdates;
|
|
73
|
+
/**
|
|
74
|
+
* `col` is the cell's position in the memory row; `colAddr` is its grid column.
|
|
75
|
+
* They differ after a merge: memory keeps covered cells, the XML drops them.
|
|
76
|
+
* The XML writer finds the target by colAddr so a write made after a merge
|
|
77
|
+
* lands in the right cell (writes now replay in call order).
|
|
78
|
+
*/
|
|
73
79
|
private _pendingTableCellUpdates;
|
|
74
80
|
private _pendingNestedTableInserts;
|
|
75
81
|
private _pendingImageInserts;
|
|
@@ -96,10 +102,25 @@ export declare class HwpxDocument {
|
|
|
96
102
|
private _pendingTableRowDeletes;
|
|
97
103
|
private _pendingTableColumnInserts;
|
|
98
104
|
private _pendingTableColumnDeletes;
|
|
105
|
+
/**
|
|
106
|
+
* Call order of every pending edit that names a table cell or row/column by
|
|
107
|
+
* index. Each such index is relative to the table as it was at call time,
|
|
108
|
+
* so save must replay these edits in call order (applyTableOpsInCallOrder).
|
|
109
|
+
* A WeakMap keeps the queue element types unchanged and drops entries with
|
|
110
|
+
* their ops (undo, section delete).
|
|
111
|
+
*/
|
|
112
|
+
private _tableOpSeq;
|
|
113
|
+
private _tableOpCounter;
|
|
99
114
|
private _pendingParagraphCopies;
|
|
100
115
|
private _pendingParagraphMoves;
|
|
101
116
|
private _pendingHeaderUpdates;
|
|
102
117
|
private _pendingFooterUpdates;
|
|
118
|
+
/**
|
|
119
|
+
* New sections to materialise as Contents/sectionN.xml on save, in call
|
|
120
|
+
* order. `templateFrom` is the section whose <hp:secPr> (page size, margins)
|
|
121
|
+
* the new section copies — Hancom's own "insert section" does the same.
|
|
122
|
+
*/
|
|
123
|
+
private _pendingSectionOps;
|
|
103
124
|
private _charPrCache;
|
|
104
125
|
private _originalCharPrCount?;
|
|
105
126
|
private constructor();
|
|
@@ -130,6 +151,8 @@ export declare class HwpxDocument {
|
|
|
130
151
|
get isDirty(): boolean;
|
|
131
152
|
get zip(): JSZip | null;
|
|
132
153
|
get content(): HwpxContent;
|
|
154
|
+
/** Push a table-structure or table-cell edit and remember its call order. */
|
|
155
|
+
private queueTableOp;
|
|
133
156
|
private saveState;
|
|
134
157
|
private serializeContent;
|
|
135
158
|
private deserializeContent;
|
|
@@ -353,9 +376,19 @@ export declare class HwpxDocument {
|
|
|
353
376
|
/**
|
|
354
377
|
* Get table map with headers - maps table indices to their header paragraphs
|
|
355
378
|
* Returns array of table info including the header text from the preceding paragraph
|
|
379
|
+
*
|
|
380
|
+
* Two indices are returned because they differ once a document has more than
|
|
381
|
+
* one section:
|
|
382
|
+
* - `table_index_in_section` — what every table tool (update_table_cell,
|
|
383
|
+
* get_table_cell, insert_table_row, …) expects together with
|
|
384
|
+
* `section_index`. Use this one.
|
|
385
|
+
* - `table_index` — position across the whole document, kept for callers
|
|
386
|
+
* that list tables. Passing it to a table tool in section 1+ addresses a
|
|
387
|
+
* DIFFERENT table (reported 2026-09-24: map said 5, the tool needed 4).
|
|
356
388
|
*/
|
|
357
389
|
getTableMap(): Array<{
|
|
358
390
|
table_index: number;
|
|
391
|
+
table_index_in_section: number;
|
|
359
392
|
section_index: number;
|
|
360
393
|
header: string;
|
|
361
394
|
rows: number;
|
|
@@ -893,6 +926,12 @@ export declare class HwpxDocument {
|
|
|
893
926
|
pageSettings: PageSettings;
|
|
894
927
|
}[];
|
|
895
928
|
insertSection(afterSectionIndex: number): number;
|
|
929
|
+
/**
|
|
930
|
+
* Add `delta` to every section number held by a pending edit that is >= from.
|
|
931
|
+
* Covers all pending arrays generically: any numeric field whose name is
|
|
932
|
+
* sectionIndex or ends in "Section"/"SectionIndex" (source/target pairs).
|
|
933
|
+
*/
|
|
934
|
+
private shiftPendingSectionIndices;
|
|
896
935
|
deleteSection(sectionIndex: number): boolean;
|
|
897
936
|
getStyles(): {
|
|
898
937
|
id: number;
|
|
@@ -1074,6 +1113,19 @@ export declare class HwpxDocument {
|
|
|
1074
1113
|
* Generate an empty cell XML for split operations.
|
|
1075
1114
|
*/
|
|
1076
1115
|
private generateEmptyCell;
|
|
1116
|
+
/**
|
|
1117
|
+
* Replay every pending table edit (cell text, merge/split, nested table,
|
|
1118
|
+
* cell image, cell hanging indent, row/column insert/delete) in call order.
|
|
1119
|
+
*
|
|
1120
|
+
* Each index an edit carries is relative to the table as it was when the
|
|
1121
|
+
* edit was made. Applying by kind (all cell writes, then all row inserts,
|
|
1122
|
+
* then all column inserts ...) wrote text into the pre-insert layout; and
|
|
1123
|
+
* the row appliers sort their own queue by index, which reorders two
|
|
1124
|
+
* inserts or two deletes on the same table. So edits that change a table's
|
|
1125
|
+
* row/column layout run one at a time. Runs of layout-preserving edits
|
|
1126
|
+
* (cell text, indents, images, nested tables) go to their applier together.
|
|
1127
|
+
*/
|
|
1128
|
+
private applyTableOpsInCallOrder;
|
|
1077
1129
|
/**
|
|
1078
1130
|
* Apply table cell updates to XML while preserving original structure.
|
|
1079
1131
|
* This function modifies only the text content of specific cells,
|
|
@@ -1230,6 +1282,23 @@ export declare class HwpxDocument {
|
|
|
1230
1282
|
* Finds hp:run elements and updates their hp:t content.
|
|
1231
1283
|
*/
|
|
1232
1284
|
private replaceRunsInParagraphDirect;
|
|
1285
|
+
/** Container elements whose content belongs to OTHER paragraphs or objects. */
|
|
1286
|
+
private static readonly NESTED_CONTENT;
|
|
1287
|
+
/** Direct <hp:run> children of a paragraph (runs of nested paragraphs excluded). */
|
|
1288
|
+
private findDirectChildRuns;
|
|
1289
|
+
/**
|
|
1290
|
+
* A run's own markup with every nested container (table, equation, text box,
|
|
1291
|
+
* note…) blanked out, so its <hp:t> are the run's own text only.
|
|
1292
|
+
*/
|
|
1293
|
+
private ownRunText;
|
|
1294
|
+
/**
|
|
1295
|
+
* Apply `fn` to the parts of a run that are its own text, leaving nested
|
|
1296
|
+
* containers byte-for-byte intact. With `blank`, nested containers are
|
|
1297
|
+
* replaced by an empty marker instead (for reading).
|
|
1298
|
+
*/
|
|
1299
|
+
private mapOwnRunText;
|
|
1300
|
+
/** End offset of the <hp:name> element opening at `start` (handles nesting and self-closing). */
|
|
1301
|
+
private findElementEnd;
|
|
1233
1302
|
/**
|
|
1234
1303
|
* Replace text in a single run directly using pre-computed target location.
|
|
1235
1304
|
* Simpler version for single-run updates.
|
|
@@ -1411,6 +1480,17 @@ export declare class HwpxDocument {
|
|
|
1411
1480
|
* Add image entry to content.hpf manifest
|
|
1412
1481
|
*/
|
|
1413
1482
|
private addImageToContentHpf;
|
|
1483
|
+
/**
|
|
1484
|
+
* Apply section inserts/deletes to Contents/sectionN.xml, in call order.
|
|
1485
|
+
*
|
|
1486
|
+
* File numbers must keep matching memory section indices, so an insert
|
|
1487
|
+
* renames later files up one (section1 → section2 …) and a delete removes
|
|
1488
|
+
* its file and renames later files down one. content.hpf gets a manifest
|
|
1489
|
+
* item and a spine itemref per section, and header.xml's secCnt follows.
|
|
1490
|
+
*/
|
|
1491
|
+
private applySectionOpsToZip;
|
|
1492
|
+
/** A section XML holding one empty paragraph with the template's <hp:secPr>. */
|
|
1493
|
+
private buildEmptySectionXml;
|
|
1414
1494
|
/**
|
|
1415
1495
|
* Add hp:pic tag to section XML
|
|
1416
1496
|
*/
|
|
@@ -1725,6 +1805,38 @@ export declare class HwpxDocument {
|
|
|
1725
1805
|
* @returns Cell XML content and its position, or null if not found
|
|
1726
1806
|
*/
|
|
1727
1807
|
private findTableCellInXml;
|
|
1808
|
+
/**
|
|
1809
|
+
* Scale this table's column widths so they sum to its <hp:sz width>.
|
|
1810
|
+
*
|
|
1811
|
+
* Column widths are read from cells whose colSpan is 1 (the first one seen
|
|
1812
|
+
* per colAddr). Every cell then gets the sum of the scaled widths of the
|
|
1813
|
+
* columns it spans, so merged cells stay aligned. Rounding leftovers go to
|
|
1814
|
+
* the last column so the total is exact. Nested tables are not touched.
|
|
1815
|
+
*/
|
|
1816
|
+
private fitColumnsToTableWidth;
|
|
1817
|
+
/**
|
|
1818
|
+
* Locate one of a cell's OWN address/span attributes (`colAddr`, `rowAddr`,
|
|
1819
|
+
* `colSpan`, `rowSpan`) in `cellXml`, returning the value and the absolute
|
|
1820
|
+
* index of its digits so callers can rewrite it in place.
|
|
1821
|
+
*
|
|
1822
|
+
* Hancom writes them on `<hp:cellAddr>`/`<hp:cellSpan>` after the cell's
|
|
1823
|
+
* sub-list (209/209 corpus files). Hand-made files may put them on the
|
|
1824
|
+
* `<hp:tc>` start tag instead, which the parser also accepts. A nested
|
|
1825
|
+
* table's cells live inside the sub-list, so only the tail is searched for
|
|
1826
|
+
* the child form and only the start tag for the attribute form.
|
|
1827
|
+
*/
|
|
1828
|
+
private cellOwnAttr;
|
|
1829
|
+
/** Rewrite one of a cell's own attributes (see cellOwnAttr); no-op if absent. */
|
|
1830
|
+
private setCellOwnAttr;
|
|
1831
|
+
/** A cell's own <hp:cellSz width> (after its sub-list, so never a nested table's). */
|
|
1832
|
+
private cellOwnWidth;
|
|
1833
|
+
/** Rewrite a cell's own <hp:cellSz width>; no-op if the cell has none or width <= 0. */
|
|
1834
|
+
private setCellOwnWidth;
|
|
1835
|
+
/**
|
|
1836
|
+
* Add `delta` to the rowAddr of every cell of THIS table whose rowAddr is
|
|
1837
|
+
* >= fromRow. Nested tables inside cells keep their own addresses.
|
|
1838
|
+
*/
|
|
1839
|
+
private shiftTableRowAddrs;
|
|
1728
1840
|
/**
|
|
1729
1841
|
* Clone a table cell for a newly inserted row: same cell attributes, same
|
|
1730
1842
|
* first-paragraph formatting, but a single paragraph holding `text`.
|
|
@@ -1733,6 +1845,17 @@ export declare class HwpxDocument {
|
|
|
1733
1845
|
* charPrIDRef is kept so the new text matches the template cell's font.
|
|
1734
1846
|
*/
|
|
1735
1847
|
private cloneCellWithText;
|
|
1848
|
+
/**
|
|
1849
|
+
* Source cells for a new row inserted after `afterRow`, one per column
|
|
1850
|
+
* position, in column order, covering every column 0..colCnt-1 exactly once.
|
|
1851
|
+
*
|
|
1852
|
+
* For each column: the cell that STARTS there in the template row (keeping
|
|
1853
|
+
* its colSpan so horizontal merges carry over), otherwise the nearest row
|
|
1854
|
+
* above whose own cell starts there. A column no row starts is skipped by the
|
|
1855
|
+
* colSpan of the cell covering it. Returned XML still carries the source
|
|
1856
|
+
* addresses; the caller rewrites rowAddr/rowSpan.
|
|
1857
|
+
*/
|
|
1858
|
+
private gridCellsForNewRow;
|
|
1736
1859
|
private applyTableRowInsertsToXml;
|
|
1737
1860
|
private applyTableRowDeletesToXml;
|
|
1738
1861
|
private applyTableColumnInsertsToXml;
|