kordoc 3.16.2 → 3.18.0

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.
Files changed (56) hide show
  1. package/README.md +37 -12
  2. package/dist/{-C4FVXHIP.js → -OUCPI6B3.js} +9 -7
  3. package/dist/{chunk-HECUYZZK.js → chunk-4YEZTZAR.js} +79 -3
  4. package/dist/chunk-4YEZTZAR.js.map +1 -0
  5. package/dist/{chunk-72LLOLFX.cjs → chunk-5HDBLRVP.cjs} +45 -9
  6. package/dist/chunk-5HDBLRVP.cjs.map +1 -0
  7. package/dist/{chunk-4WI7QXEG.js → chunk-6Y4KKPC3.js} +2 -2
  8. package/dist/chunk-DCZVOIEO.cjs.map +1 -1
  9. package/dist/{chunk-EJ2ZIYTO.js → chunk-GIXXV52X.js} +39 -3
  10. package/dist/chunk-GIXXV52X.js.map +1 -0
  11. package/dist/chunk-GS7T56RP.cjs.map +1 -1
  12. package/dist/{chunk-EUEKDZ7O.js → chunk-PWHM3KAA.js} +535 -83
  13. package/dist/chunk-PWHM3KAA.js.map +1 -0
  14. package/dist/{chunk-WR5LHRXO.js → chunk-VP34RO24.js} +3 -3
  15. package/dist/{chunk-IS3IK7UT.js → chunk-YWIYGXLI.js} +39 -3
  16. package/dist/chunk-YWIYGXLI.js.map +1 -0
  17. package/dist/{chunk-E4Y73XRH.js → chunk-ZTUQYITH.js} +127 -68
  18. package/dist/chunk-ZTUQYITH.js.map +1 -0
  19. package/dist/cli.js +18 -15
  20. package/dist/cli.js.map +1 -1
  21. package/dist/formula-5NKVS2LR.cjs.map +1 -1
  22. package/dist/index.cjs +1045 -459
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.d.cts +121 -3
  25. package/dist/index.d.ts +121 -3
  26. package/dist/index.js +784 -198
  27. package/dist/index.js.map +1 -1
  28. package/dist/mcp.js +22 -10
  29. package/dist/mcp.js.map +1 -1
  30. package/dist/page-range-P7SDW6LR.cjs.map +1 -1
  31. package/dist/{parser-GWL72XYS.js → parser-5D6BQYTX.js} +2 -2
  32. package/dist/{parser-CMPNM6ZL.js → parser-NZAE7PBE.js} +3 -3
  33. package/dist/{parser-YVRTKGKO.cjs → parser-S34YR2SH.cjs} +14 -14
  34. package/dist/parser-S34YR2SH.cjs.map +1 -0
  35. package/dist/provider-G4C2V2PD.cjs.map +1 -1
  36. package/dist/render-HRAW7ASR.js +10 -0
  37. package/dist/seal-CK64CXWO.js +10 -0
  38. package/dist/{watch-WLI6K3IR.js → watch-2EFXBRYK.js} +7 -7
  39. package/package.json +1 -1
  40. package/dist/chunk-72LLOLFX.cjs.map +0 -1
  41. package/dist/chunk-E4Y73XRH.js.map +0 -1
  42. package/dist/chunk-EJ2ZIYTO.js.map +0 -1
  43. package/dist/chunk-EUEKDZ7O.js.map +0 -1
  44. package/dist/chunk-HECUYZZK.js.map +0 -1
  45. package/dist/chunk-IS3IK7UT.js.map +0 -1
  46. package/dist/parser-YVRTKGKO.cjs.map +0 -1
  47. package/dist/render-A2LFMJCP.js +0 -10
  48. package/dist/seal-KA4VAV2Z.js +0 -10
  49. /package/dist/{-C4FVXHIP.js.map → -OUCPI6B3.js.map} +0 -0
  50. /package/dist/{chunk-4WI7QXEG.js.map → chunk-6Y4KKPC3.js.map} +0 -0
  51. /package/dist/{chunk-WR5LHRXO.js.map → chunk-VP34RO24.js.map} +0 -0
  52. /package/dist/{parser-GWL72XYS.js.map → parser-5D6BQYTX.js.map} +0 -0
  53. /package/dist/{parser-CMPNM6ZL.js.map → parser-NZAE7PBE.js.map} +0 -0
  54. /package/dist/{render-A2LFMJCP.js.map → render-HRAW7ASR.js.map} +0 -0
  55. /package/dist/{seal-KA4VAV2Z.js.map → seal-CK64CXWO.js.map} +0 -0
  56. /package/dist/{watch-WLI6K3IR.js.map → watch-2EFXBRYK.js.map} +0 -0
package/README.md CHANGED
@@ -71,7 +71,7 @@ MCP 등록 대신 스킬(SKILL.md) 형태로 쓰려면:
71
71
  단순한 텍스트 추출을 넘어, **공문서 처리를 위한 모든 과정**을 자동화합니다.
72
72
 
73
73
  * **📄 어떤 문서든 마크다운으로**: `HWP3` (구버전), `HWP`(5.x), `HWPX`, `HWPML`, `PDF`, `XLS`, `XLSX`, `DOCX` 파일을 즉시 `Markdown`으로 변환합니다. AI(LLM)가 문서를 읽고 분석하기 가장 좋은 상태로 만들어줍니다.
74
- * **📊 복잡한 표(Table) 완벽 재현**: 선이 없는 PDF나 복잡하게 병합된 HWP 표도 구조를 분석하여 정확한 마크다운 테이블로 복원합니다.
74
+ * **📊 복잡한 표(Table) 완벽 재현**: 선이 없는 PDF나 복잡하게 병합된 HWP 표도 구조를 분석하여 정확한 마크다운 테이블로 복원합니다. 법령 개정안 PDF의 신구조문대비표도 통째로 살립니다 (v3.16.2).
75
75
  * **🔍 신구대조표 자동 생성**: 두 문서의 차이점을 분석하여 무엇이 바뀌었는지 한눈에 보여줍니다. (HWP와 HWPX 간의 비교도 가능!)
76
76
  * **📝 마크다운을 다시 HWPX로**: AI가 작성한 내용을 다시 보고서 양식(`HWPX`)으로 되돌려줍니다. 이제 복사-붙여넣기 노가다에서 해방되세요.
77
77
  * **🔄 서식 보존 무손실 라운드트립 (v3.0)**: 변환된 마크다운을 편집해서 `patchHwpx`(HWPX) / `patchHwp`(HWP 5.x 바이너리)에 넘기면, **원본 서식을 1바이트도 건드리지 않고** 바뀐 문단/표 셀의 텍스트만 원본 안에서 교체합니다. v3.7부터는 **표에 행을 추가/삭제하는 편집**도 원본 서식을 승계하며 반영되고, v3.8부터는 HWP 5.x의 **빈 셀에 값 넣기**도 지원합니다.
@@ -83,11 +83,22 @@ MCP 등록 대신 스킬(SKILL.md) 형태로 쓰려면:
83
83
 
84
84
  ---
85
85
 
86
- ## v3.16.0 변경사항
86
+ ## v3.18.0 변경사항
87
+
88
+ - **🎨 서식 프로필**: 표의 위상뿐 아니라 **테두리·음영·열 실측폭·셀 글꼴까지** 원본 문서 없이 재현합니다. `hwpxToProfile(hwpx)`로 레퍼런스 서식만 JSON으로 추출하고, `markdownToHwpx(md, { profile })`로 다른 문서에 그 서식을 입힙니다 — 원본 유출 없이 기관 서식만 공유·재현(이슈 #41, 스키마 [`docs/format-profile-spec.md`](docs/format-profile-spec.md)). 스키마·예시 기여: [@chiclooc-rgb](https://github.com/chiclooc-rgb) (PR #42).
89
+
90
+ ## v3.17.0 변경사항
91
+
92
+ - **🖼️ 레이아웃 렌더 충실도**: 글꼴이 문서 지정대로 나오는 per-run 폰트(고딕 제목이 바탕체로 나오던 것 해소), 표지+본문 다구역 문서 전체 렌더, 가로(landscape) 문서 프레임 회전(우측 잘림 해소), 연속 표 문단의 페이지 포개짐 분리.
93
+ - **✍️ 결재란 겹침 해소 (reflow)**: 조판 캐시 없는 문서에서 결재란 라벨표·스탬프표가 같은 자리에 포개 찍히던 것을 한컴과 동일하게 나란히 배치. 중첩표 셀 높이 과소측정도 함께 수정.
94
+
95
+ ## v3.16 변경사항
87
96
 
88
97
  - **📊 차트 생성**: 마크다운 ```chart 펜스(type/cat/계열 라인)가 한컴 네이티브 차트(OOXML chartSpace)로 생성됩니다 — 막대·선·원·도넛·영역·분산·방사형 등 20종, 계열/조각 색 지정, 잘못된 펜스는 코드블록 폴백.
89
98
  - **🔴 도장/서명 자동 날인**: `kordoc seal` — "(인)"·"서명 또는 인" 앵커를 찾아 도장 PNG를 글 앞 부유로 배치. 표/페이지를 키우지 않아 날인 후 서식이 밀리지 않습니다 (MCP `place_seal` 포함). 중첩표·글상자·탭/줄바꿈 문단은 위치가 근사이며 결과 `warnings` 로 고지됩니다 — 한컴에서 확인 후 `--dx`/`--dy`(dx_mm/dy_mm)로 미세조정하세요.
90
99
  - **🔌 Claude Code 플러그인**: `/plugin marketplace add chrisryugj/kordoc` → `.hwp`/`.hwpx`/공문서 요청에 kordoc 스킬 자동 활성화.
100
+ - **🩹 3.16.1 패치**: 통합 검증 리뷰 결함 55건 일괄 수정 — 도장 배치(rowspan·colspan·중첩표 원점), 차트 값 파서(천단위 콤마·CRLF 마크다운), 양식 채우기 가드(require_unique), CLI `fill -o` 출력 등 "성공 메시지 뒤에 조용히 틀린 산출물" 계열 소탕.
101
+ - **🩹 3.16.2 패치**: 신구조문대비표의 `<신 설>` 표기를 텍스트 상자로 오인해 표 전체를 문단으로 해체하던 PDF 파서 결함 수정 — 30p 개정안 대비표가 통째 1표로 복원.
91
102
 
92
103
  ## v3.15.0 변경사항
93
104
 
@@ -177,6 +188,7 @@ MCP 등록 대신 스킬(SKILL.md) 형태로 쓰려면:
177
188
  - **📐 실측 텍스트 메트릭 엔진**: 함초롬바탕 정품 TTF에서 글자 폭을 전수 추출해 한글 프로그램 없이 줄폭·줄바꿈을 계산합니다. 실제 결재문서의 조판 결과와 대조해 **줄바꿈점 98% 일치** 검증.
178
189
  - **🪗 자동 장평(`autoFit`)**: 한두 글자가 다음 줄로 넘어가는(orphan) 문단만 골라 장평을 95→90%로 줄여 한 줄에 담습니다. 공문서 작성 관행 그대로.
179
190
  - **📊 HTML 표 생성**: 병합(colspan/rowspan)·중첩 표가 든 마크다운도 `markdownToHwpx`로 구조 그대로 HWPX 표가 됩니다 — parse↔generate 표 라운드트립 완성.
191
+ - **🎨 서식 프로필**: 표의 위상뿐 아니라 **테두리·음영·열 실측폭·셀 글꼴까지** 원본 없이 재현합니다. `hwpxToProfile(hwpx)`로 서식만 JSON으로 추출하고, `markdownToHwpx(md, { profile })`로 다른 문서에 그 서식을 입힙니다 — 원본 유출 없이 기관 서식만 공유·재현(이슈 #41, 스키마: [`docs/format-profile-spec.md`](docs/format-profile-spec.md)).
180
192
  - **🗂️ 다중값 채우기**: `fillForm` 값에 배열(`string[]`)을 주면 같은 라벨의 등장 순서대로 하나씩 소진 — 반복 양식·명부형 표(헤더+여러 행) 채우기.
181
193
  - **🛡️ 무결성 픽스**: 채우기/패치 후 한컴이 "문서가 변조되었습니다" 경고를 띄우던 문제(줄 레이아웃 캐시 잔존), 생성 표 테두리가 보이지 않던 문제(borderFill id 규약) 수정.
182
194
 
@@ -521,19 +533,23 @@ CLI로도: `kordoc generate 보고서.md -o 보고서.hwpx --preset 보고서`
521
533
  ### 레이아웃 보존 렌더 (HWPX → SVG)
522
534
 
523
535
  한컴이 HWPX에 저장하는 조판 캐시(줄 좌표·셀 그리드·개체 앵커)를 그대로 SVG 절대배치로
524
- 그립니다. 조판 엔진이 없으므로 빠르고, 서버에 한컴 설치 없이 원본 모양 미리보기를 만들 수
525
- 있습니다. **한컴(또는 조판 캐시를 기록하는 편집기)에서 저장한 파일 전용** — `markdownToHwpx`
526
- 산출물엔 조판 캐시가 없어 에러를 반환합니다. 현재 1페이지·수식/도형 개체 미지원.
536
+ 그립니다. 조판 엔진 없이 빠르고, 서버에 한컴 설치 없이 원본 모양 미리보기를 만들 수
537
+ 있습니다. 다페이지 세로 스택·검색어 형광펜·그리기 도형 지원(v3.14~15). 조판 캐시가 없는
538
+ 파일(`markdownToHwpx` 산출물·AI 생성본·편집본)은 `reflow: true`를 주면 **순수 TS reflow
539
+ 엔진**이 직접 조판합니다(v3.15). 수식 개체는 미지원.
527
540
 
528
541
  ```typescript
529
542
  import { renderHwpxToSvg } from "kordoc"
530
543
 
531
- const r = await renderHwpxToSvg(readFileSync("결재문서.hwpx"))
544
+ const r = await renderHwpxToSvg(readFileSync("결재문서.hwpx"), { highlights: ["예산"] })
532
545
  writeFileSync("결재문서.svg", r.svg)
533
- // r.width/r.height (pt), r.stats { texts, images, tables }, r.warnings
546
+ // r.width/r.height (pt), r.pageCount, r.stats { texts, images, tables }, r.warnings
547
+
548
+ const g = await renderHwpxToSvg(generatedHwpx, { reflow: true }) // 조판 캐시 없는 생성본
534
549
  ```
535
550
 
536
- CLI로도: `kordoc render 결재문서.hwpx -o 결재문서.svg`
551
+ CLI로도: `kordoc render 결재문서.hwpx -o 결재문서.svg` (`--reflow`·`--highlight 예산,집행`),
552
+ 연속 렌더는 `kordoc render-worker`(stdin NDJSON, 미리보기 앱 연동용)
537
553
 
538
554
  ### 페이지 범위 지정
539
555
 
@@ -582,7 +598,11 @@ npx kordoc 보고서.hwpx --pages 1-3 # 페이지 범위
582
598
  npx kordoc fill 신청서.hwpx -f '성명=홍길동,주소=서울' -o 결과.hwpx # 양식 채우기
583
599
  npx kordoc fill 신청서.hwpx -j values.json -o 결과.hwpx # JSON 파일로 채우기
584
600
  npx kordoc fill 신청서.hwpx --dry-run # 필드 목록만 확인
585
- npx kordoc render 결재문서.hwpx -o 미리보기.svg # 레이아웃 보존 SVG 렌더
601
+ npx kordoc generate 보고서.md -o 보고서.hwpx --preset 보고서 # 마크다운 공문서 HWPX
602
+ npx kordoc patch 원본.hwpx 편집.md -o 반영.hwpx # 서식 보존 라운드트립 패치 (.hwp도 자동 분기)
603
+ npx kordoc seal 신청서.hwpx --image 도장.png --anchor "(인)" -o 날인.hwpx # 도장/서명 날인
604
+ npx kordoc validate 산출물.hwpx # HWPX 구조 검증 (ZIP·필수 파트·XML)
605
+ npx kordoc render 결재문서.hwpx -o 미리보기.svg # 레이아웃 보존 SVG 렌더 (--reflow 지원)
586
606
  npx kordoc watch ./수신함 -d ./변환결과 # 폴더 감시 모드
587
607
  npx kordoc watch ./문서 --webhook https://api/hook # 웹훅 알림
588
608
  ```
@@ -623,7 +643,7 @@ npx -y kordoc setup
623
643
  }
624
644
  ```
625
645
 
626
- **8개 도구:**
646
+ **11개 도구:**
627
647
 
628
648
  | 도구 | 설명 |
629
649
  |------|------|
@@ -634,7 +654,10 @@ npx -y kordoc setup
634
654
  | `parse_table` | N번째 테이블만 추출 |
635
655
  | `compare_documents` | 두 문서 비교 (크로스 포맷) |
636
656
  | `parse_form` | 양식 필드를 JSON으로 추출 |
637
- | `fill_form` | 양식 템플릿에 값 채우기 (HWPX 원본 서식 보존) |
657
+ | `fill_form` | 양식 템플릿에 값 채우기 (HWPX 원본 서식 보존, 서식/유일성 가드) |
658
+ | `patch_document` | 편집된 마크다운을 원본 HWPX/HWP에 서식 보존 반영 (v3.3) |
659
+ | `generate_document` | 마크다운(표·수식·차트 포함) → HWPX 생성, 공문서 프리셋 (v3.5) |
660
+ | `place_seal` | 도장/서명 이미지를 앵커 문구 위에 부유 배치 (v3.16) |
638
661
 
639
662
  ## API
640
663
 
@@ -671,7 +694,9 @@ npx -y kordoc setup
671
694
  | `markdownToPdf(markdown, options?)` | Markdown → PDF 생성 (Print Renderer) |
672
695
  | `blocksToPdf(blocks, options?)` | IRBlock[] → PDF 생성 |
673
696
  | `renderHtml(blocks, options?)` | IRBlock[] → 인쇄용 HTML |
674
- | `renderHwpxToSvg(buffer, options?)` | HWPX 조판 캐시 → 레이아웃 보존 SVG (v3.10) |
697
+ | `renderHwpxToSvg(buffer, options?)` | HWPX → 레이아웃 보존 SVG — 다페이지·형광펜·도형, 캐시 없으면 `reflow` (v3.10~15) |
698
+ | `placeSealHwpx(buffer, seals)` | 도장/서명 이미지를 앵커 문구 위에 부유 배치 (v3.16) |
699
+ | `validateHwpx(buffer)` | HWPX 구조 검증 — ZIP·mimetype·필수 파트·XML 웰폼드 (v3.16) |
675
700
  | `blocksToMarkdown(blocks)` | IRBlock[] → Markdown 문자열 |
676
701
 
677
702
  ### 타입
@@ -13,6 +13,7 @@ import {
13
13
  fillHwpx,
14
14
  fillWithUniqueGuard,
15
15
  formatFillValue,
16
+ hwpxToProfile,
16
17
  inferFieldType,
17
18
  isLabelCell,
18
19
  markdownToHwpx,
@@ -33,10 +34,10 @@ import {
33
34
  patchHwpxBlocks,
34
35
  renderHtml,
35
36
  validateHwpx
36
- } from "./chunk-EUEKDZ7O.js";
37
+ } from "./chunk-PWHM3KAA.js";
37
38
  import {
38
39
  blocksToMarkdown
39
- } from "./chunk-IS3IK7UT.js";
40
+ } from "./chunk-YWIYGXLI.js";
40
41
  import {
41
42
  detectFormat,
42
43
  detectOle2Format,
@@ -53,7 +54,7 @@ import {
53
54
  buildRangeSplices,
54
55
  placeSealHwpx,
55
56
  scanSectionXml
56
- } from "./chunk-WR5LHRXO.js";
57
+ } from "./chunk-VP34RO24.js";
57
58
  import {
58
59
  SPACE_EM_FIXED,
59
60
  SPACE_EM_FONT,
@@ -63,11 +64,11 @@ import {
63
64
  renderHwpxToSvg,
64
65
  simulateWrap,
65
66
  simulateWrapKeepWord
66
- } from "./chunk-E4Y73XRH.js";
67
- import "./chunk-HECUYZZK.js";
67
+ } from "./chunk-ZTUQYITH.js";
68
+ import "./chunk-4YEZTZAR.js";
68
69
  import {
69
70
  VERSION
70
- } from "./chunk-4WI7QXEG.js";
71
+ } from "./chunk-6Y4KKPC3.js";
71
72
  export {
72
73
  HwpxSession,
73
74
  PRESET_ALIAS,
@@ -94,6 +95,7 @@ export {
94
95
  fillWithUniqueGuard,
95
96
  fitRatioForFewerLines,
96
97
  formatFillValue,
98
+ hwpxToProfile,
97
99
  inferFieldType,
98
100
  isHwpxFile,
99
101
  isLabelCell,
@@ -125,4 +127,4 @@ export {
125
127
  simulateWrapKeepWord,
126
128
  validateHwpx
127
129
  };
128
- //# sourceMappingURL=-C4FVXHIP.js.map
130
+ //# sourceMappingURL=-OUCPI6B3.js.map
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  KordocError
4
- } from "./chunk-4WI7QXEG.js";
4
+ } from "./chunk-6Y4KKPC3.js";
5
5
 
6
6
  // src/hwpx/parser-shared.ts
7
7
  import { DOMParser } from "@xmldom/xmldom";
@@ -65,6 +65,77 @@ var DEFAULT_PARA_GEOM = {
65
65
  spaceAfter: 0
66
66
  };
67
67
  var DEFAULT_CHAR = { height: 1e3, bold: false, italic: false, underline: false, ratio: 100, spacing: 0 };
68
+ var FONT_ALIASES = {
69
+ "\uD568\uCD08\uB86C\uBC14\uD0D5": "'HCR Batang','\uD568\uCD08\uB86C\uBC14\uD0D5','\uD55C\uCEF4\uBC14\uD0D5'",
70
+ "\uD55C\uCEF4\uBC14\uD0D5": "'HCR Batang','\uD568\uCD08\uB86C\uBC14\uD0D5','\uD55C\uCEF4\uBC14\uD0D5'",
71
+ "\uD568\uCD08\uB86C\uB3CB\uC6C0": "'HCR Dotum','\uD568\uCD08\uB86C\uB3CB\uC6C0','\uD55C\uCEF4\uB3CB\uC6C0'",
72
+ "\uD55C\uCEF4\uB3CB\uC6C0": "'HCR Dotum','\uD568\uCD08\uB86C\uB3CB\uC6C0','\uD55C\uCEF4\uB3CB\uC6C0'",
73
+ "\uB9D1\uC740 \uACE0\uB515": "'Malgun Gothic','\uB9D1\uC740 \uACE0\uB515'",
74
+ "\uB9D1\uC740\uACE0\uB515": "'Malgun Gothic','\uB9D1\uC740 \uACE0\uB515'",
75
+ "\uAD74\uB9BC": "'Gulim','\uAD74\uB9BC'",
76
+ "\uAD74\uB9BC\uCCB4": "'GulimChe','\uAD74\uB9BC\uCCB4','Gulim'",
77
+ "\uB3CB\uC6C0": "'Dotum','\uB3CB\uC6C0'",
78
+ "\uB3CB\uC6C0\uCCB4": "'DotumChe','\uB3CB\uC6C0\uCCB4','Dotum'",
79
+ "\uBC14\uD0D5": "'Batang','\uBC14\uD0D5'",
80
+ "\uBC14\uD0D5\uCCB4": "'BatangChe','\uBC14\uD0D5\uCCB4','Batang'",
81
+ "\uAD81\uC11C": "'Gungsuh','\uAD81\uC11C'",
82
+ "\uAD81\uC11C\uCCB4": "'GungsuhChe','\uAD81\uC11C\uCCB4','Gungsuh'",
83
+ "\uB098\uB214\uACE0\uB515": "'NanumGothic','\uB098\uB214\uACE0\uB515'",
84
+ "\uB098\uB214\uBA85\uC870": "'NanumMyeongjo','\uB098\uB214\uBA85\uC870'",
85
+ "\uB9D1\uC740 \uACE0\uB515 Semilight": "'Malgun Gothic Semilight','\uB9D1\uC740 \uACE0\uB515'"
86
+ };
87
+ function isSerifFace(face) {
88
+ return /바탕|명조|Batang|Myeong|Mincho|궁서|Gungsuh|Serif|신명|순명|Song|송/i.test(face);
89
+ }
90
+ function cssQuote(name) {
91
+ return /^[A-Za-z0-9_-]+$/.test(name) ? name : `'${name.replace(/['"\\]/g, "")}'`;
92
+ }
93
+ function hwpFaceToCssStack(face) {
94
+ const trimmed = (face ?? "").trim();
95
+ if (!trimmed) return "";
96
+ const generic = isSerifFace(trimmed) ? "'HCR Batang','Batang','Noto Serif KR',serif" : "'Malgun Gothic','HCR Dotum','Noto Sans KR',sans-serif";
97
+ const head = FONT_ALIASES[trimmed] ?? cssQuote(trimmed);
98
+ return `${head},${generic}`;
99
+ }
100
+ function collectHangulFonts(root) {
101
+ const map = /* @__PURE__ */ new Map();
102
+ const findFaces = (el, depth) => {
103
+ if (depth > 24) return null;
104
+ for (const ch of Array.from(el.childNodes)) {
105
+ if (ch.nodeType !== 1) continue;
106
+ const e = ch;
107
+ if ((e.tagName || "").replace(/^[^:]+:/, "") === "fontfaces") return e;
108
+ const f = findFaces(e, depth + 1);
109
+ if (f) return f;
110
+ }
111
+ return null;
112
+ };
113
+ const faces = findFaces(root, 0);
114
+ if (!faces) return map;
115
+ let group = null;
116
+ let firstGroup = null;
117
+ for (const ch of Array.from(faces.childNodes)) {
118
+ if (ch.nodeType !== 1) continue;
119
+ const e = ch;
120
+ if ((e.tagName || "").replace(/^[^:]+:/, "") !== "fontface") continue;
121
+ if (!firstGroup) firstGroup = e;
122
+ if ((e.getAttribute("lang") ?? "").toUpperCase() === "HANGUL") {
123
+ group = e;
124
+ break;
125
+ }
126
+ }
127
+ group = group ?? firstGroup;
128
+ if (!group) return map;
129
+ for (const ch of Array.from(group.childNodes)) {
130
+ if (ch.nodeType !== 1) continue;
131
+ const e = ch;
132
+ if ((e.tagName || "").replace(/^[^:]+:/, "") !== "font") continue;
133
+ const id = e.getAttribute("id");
134
+ const face = e.getAttribute("face");
135
+ if (id != null && face) map.set(id, face);
136
+ }
137
+ return map;
138
+ }
68
139
  function borderWidthPt(v) {
69
140
  const n = parseFloat(v ?? "");
70
141
  if (!Number.isFinite(n)) return 0.34;
@@ -116,6 +187,7 @@ function parseRenderStyles(headXml) {
116
187
  const doc = createXmlParser().parseFromString(headXml, "text/xml");
117
188
  const root = doc.documentElement;
118
189
  if (!root) return styles;
190
+ const hangulFonts = collectHangulFonts(root);
119
191
  const walk = (el) => {
120
192
  const tag = (el.tagName || "").replace(/^[^:]+:/, "");
121
193
  if (tag === "charPr") {
@@ -125,6 +197,9 @@ function parseRenderStyles(headXml) {
125
197
  const spacingEl = findChildByLocalName(el, "spacing");
126
198
  const underlineEl = findChildByLocalName(el, "underline");
127
199
  const textColor = el.getAttribute("textColor");
200
+ const fontRef = findChildByLocalName(el, "fontRef");
201
+ const fontId = fontRef?.getAttribute("hangul") ?? fontRef?.getAttribute("latin");
202
+ const face = fontId != null ? hangulFonts.get(fontId) : void 0;
128
203
  styles.charPr.set(id, {
129
204
  height: Number(el.getAttribute("height")) || 1e3,
130
205
  bold: findChildByLocalName(el, "bold") != null,
@@ -132,7 +207,8 @@ function parseRenderStyles(headXml) {
132
207
  underline: underlineEl != null && (underlineEl.getAttribute("type") ?? "NONE") !== "NONE",
133
208
  color: textColor && textColor !== "#000000" && textColor.toLowerCase() !== "none" ? textColor : void 0,
134
209
  ratio: Number(ratioEl?.getAttribute("hangul")) || 100,
135
- spacing: Number(spacingEl?.getAttribute("hangul")) || 0
210
+ spacing: Number(spacingEl?.getAttribute("hangul")) || 0,
211
+ fontFamily: face ? hwpFaceToCssStack(face) : void 0
136
212
  });
137
213
  }
138
214
  } else if (tag === "paraPr") {
@@ -182,4 +258,4 @@ export {
182
258
  DEFAULT_CHAR,
183
259
  parseRenderStyles
184
260
  };
185
- //# sourceMappingURL=chunk-HECUYZZK.js.map
261
+ //# sourceMappingURL=chunk-4YEZTZAR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/hwpx/parser-shared.ts","../src/render/head-styles.ts"],"sourcesContent":["/**\n * HWPX 파서 공유 상수/타입/유틸 (parser.ts에서 분리).\n * ZIP 한도, 섹션 공유 상태(SectionShared), walk 컨텍스트(WalkCtx), XML 유틸.\n */\n\nimport { DOMParser } from \"@xmldom/xmldom\"\nimport { KordocError } from \"../utils.js\"\nimport type { CellContext, IRBlock, ParseWarning } from \"../types.js\"\n// WalkCtx.styleMap 타입 참조 — 타입 전용이라 styles.ts와의 순환은 컴파일 시 소거됨\nimport type { HwpxStyleMap } from \"./styles.js\"\n\nexport const MAX_DECOMPRESS_SIZE = 100 * 1024 * 1024\n/** 손상 ZIP 복구 시 최대 엔트리 수 */\nexport const MAX_ZIP_ENTRIES = 500\n\n/** colSpan/rowSpan을 안전한 범위로 클램핑 */\nexport function clampSpan(val: number, max: number): number {\n return Math.max(1, Math.min(val, max))\n}\n\n/** XML DOM 재귀 최대 깊이 — 악성 파일의 스택 오버플로 방지.\n * 좌표계가 다른 hwp5 MAX_NEST_DEPTH(8, 표 중첩 단계)·filler/소스맵 16\n * (표 중첩 단계)과 달리 이건 \"XML 요소\" 깊이라 표 1단이 여러 depth를\n * 소모한다 — 상수 통일 금지 (의미가 다름) */\nexport const MAX_XML_DEPTH = 200\n\n/** 셀 컨텍스트 확장 — 중첩표/이미지/다중문단 블록과 제목셀 여부를 IRCell로 전달 (v3.0) */\nexport interface CellCtxEx extends CellContext {\n blocks?: IRBlock[]\n /** 중첩표/이미지 등 구조 콘텐츠 존재 — true일 때만 IRCell.blocks로 attach */\n hasStructure?: boolean\n isHeader?: boolean\n}\n\nexport interface TableState {\n rows: CellContext[][]\n currentRow: CellContext[]\n cell: CellCtxEx | null\n /** hp:caption 텍스트 — IRTable.caption으로 전달 (v3.0) */\n caption?: string\n}\n\n/** 섹션 간 공유 상태 — 자동번호 카운터, 머리말/꼬리말, 변경추적 */\nexport interface SectionShared {\n /** numbering id → 레벨별(1..10) 카운터. 0 = 미사용(start값으로 초기화) */\n numState: Map<string, number[]>\n pageText: { headers: string[]; footers: string[] }\n track: { deleteDepth: number; warned: boolean }\n}\n\nexport function createSectionShared(): SectionShared {\n return { numState: new Map(), pageText: { headers: [], footers: [] }, track: { deleteDepth: 0, warned: false } }\n}\n\n/** walk 함수들이 공유하는 파싱 컨텍스트 — 개별 optional 파라미터를 하나로 묶어 시그니처 안정화 */\nexport interface WalkCtx {\n styleMap?: HwpxStyleMap\n warnings?: ParseWarning[]\n sectionNum?: number\n shared: SectionShared\n /** secPr outlineShapeIDRef — 개요(OUTLINE) 문단이 사용하는 numbering id */\n outlineNumId?: string\n}\n\n/** xmldom DOMParser 생성 — onError 콜백으로 malformed XML 경고 수집 */\nexport function createXmlParser(warnings?: ParseWarning[]): DOMParser {\n return new DOMParser({\n onError(level: \"warn\" | \"error\" | \"fatalError\", msg: string) {\n if (level === \"fatalError\") throw new KordocError(`XML 파싱 실패: ${msg}`)\n warnings?.push({ code: \"MALFORMED_XML\", message: `XML ${level === \"warn\" ? \"경고\" : \"오류\"}: ${msg}` })\n },\n })\n}\n\n/** 수집된 머리말/꼬리말을 본문 앞/뒤 문단으로 배치 */\nexport function applyPageText(blocks: IRBlock[], shared: SectionShared): void {\n const { headers, footers } = shared.pageText\n if (headers.length > 0) {\n blocks.unshift(...headers.map(t => ({ type: \"paragraph\" as const, text: t, pageNumber: 1 })))\n }\n if (footers.length > 0) {\n blocks.push(...footers.map(t => ({ type: \"paragraph\" as const, text: t })))\n }\n}\n\n/** 자식 중 지정된 localName(접두사 제거)을 가진 첫 번째 Element 반환 */\nexport function findChildByLocalName(parent: Element, name: string): Element | null {\n const children = parent.childNodes\n if (!children) return null\n for (let i = 0; i < children.length; i++) {\n const ch = children[i] as Element\n if (ch.nodeType !== 1) continue\n const tag = (ch.tagName || ch.localName || \"\").replace(/^[^:]+:/, \"\")\n if (tag === name) return ch\n }\n return null\n}\n\n/** 노드 내 모든 텍스트를 재귀적으로 추출 */\nexport function extractTextFromNode(node: Node): string {\n let result = \"\"\n const children = node.childNodes\n if (!children) return result\n for (let i = 0; i < children.length; i++) {\n const child = children[i]\n if (child.nodeType === 3) result += child.textContent || \"\"\n else if (child.nodeType === 1) result += extractTextFromNode(child)\n }\n return result.trim()\n}\n","/**\n * 레이아웃 보존 렌더 — header.xml 스타일 테이블 (charPr/paraPr/borderFill).\n * 파서 IR과 무관하게 렌더에 필요한 속성만 추출한다.\n */\n\nimport { createXmlParser, findChildByLocalName } from \"../hwpx/parser-shared.js\"\n\nexport interface RenderCharStyle {\n /** 글자 크기 (1/100pt — 1000 = 10pt) */\n height: number\n bold: boolean\n italic: boolean\n underline: boolean\n /** #RRGGBB — 검정이면 undefined */\n color?: string\n /** 장평 % */\n ratio: number\n /** 자간 % */\n spacing: number\n /** CSS font-family 스택 — charPr fontRef(hangul)에서 해석. 없으면 root 기본 폴백 */\n fontFamily?: string\n}\n\nexport type ParaAlign = \"JUSTIFY\" | \"LEFT\" | \"RIGHT\" | \"CENTER\" | \"DISTRIBUTE\" | \"DISTRIBUTE_SPACE\"\n\n/** reflow(Tier-2)용 문단 기하 — 줄간격·여백. paraPr `<hh:margin>`·`<hh:lineSpacing>`은\n * `<hp:switch>/<hp:case>` 안(손자)이라 재귀 탐색으로 뽑는다. 단위 HWPUNIT. */\nexport interface RenderParaGeom {\n /** PERCENT(기본 160=160%) / FIXED / BETWEEN_LINES / AT_LEAST */\n lineSpacingType: string\n lineSpacingValue: number\n /** 왼쪽 들여쓰기(HWPUNIT) */\n marginLeft: number\n marginRight: number\n /** 첫 줄 들여쓰기/내어쓰기 — 음수=둘째 줄부터 더 들어감(hanging) */\n marginIntent: number\n /** 문단 위 간격(prev) */\n spaceBefore: number\n /** 문단 아래 간격(next) */\n spaceAfter: number\n}\n\nexport const DEFAULT_PARA_GEOM: RenderParaGeom = {\n lineSpacingType: \"PERCENT\", lineSpacingValue: 160,\n marginLeft: 0, marginRight: 0, marginIntent: 0, spaceBefore: 0, spaceAfter: 0,\n}\n\nexport interface RenderBorderEdge {\n /** SOLID/DASH/DOT… — NONE은 edge 자체를 생략 */\n type: string\n /** pt 단위 굵기 */\n widthPt: number\n color: string\n}\n\nexport interface RenderBorderFill {\n left?: RenderBorderEdge\n right?: RenderBorderEdge\n top?: RenderBorderEdge\n bottom?: RenderBorderEdge\n /** 배경색 #RRGGBB (없으면 undefined) */\n fill?: string\n}\n\nexport interface RenderStyles {\n charPr: Map<string, RenderCharStyle>\n paraAlign: Map<string, ParaAlign>\n /** reflow용 문단 기하 (줄간격·여백) — id별 */\n paraGeom: Map<string, RenderParaGeom>\n borderFill: Map<string, RenderBorderFill>\n}\n\nexport const DEFAULT_CHAR: RenderCharStyle = { height: 1000, bold: false, italic: false, underline: false, ratio: 100, spacing: 0 }\n\n// ─── 글꼴 매핑 (HWP fontfaces face → CSS font-family 스택) ──────────────\n//\n// HWPX charPr 는 <hh:fontRef hangul=\"N\">으로 fontfaces 테이블의 실제 글꼴명을 가리킨다.\n// 렌더는 이 글꼴명을 SVG <text font-family>로 내보내 뷰어(WebView2/브라우저)가 원본과\n// 같은 글꼴로 그리게 한다. 미설치 글꼴은 계열(명조=serif / 고딕=sans) 폴백으로 수렴시켜\n// 최소한 획 계열(바탕↔돋움)은 원본과 일치시킨다 — 공문서 제목 고딕이 바탕체로 나오던 회귀 해소.\n\n/** 자주 쓰는 글꼴의 별칭 스택 (한글명↔영문 시스템명 병기 — 뷰어 OS별 등록명 차이 흡수) */\nconst FONT_ALIASES: Record<string, string> = {\n \"함초롬바탕\": \"'HCR Batang','함초롬바탕','한컴바탕'\",\n \"한컴바탕\": \"'HCR Batang','함초롬바탕','한컴바탕'\",\n \"함초롬돋움\": \"'HCR Dotum','함초롬돋움','한컴돋움'\",\n \"한컴돋움\": \"'HCR Dotum','함초롬돋움','한컴돋움'\",\n \"맑은 고딕\": \"'Malgun Gothic','맑은 고딕'\",\n \"맑은고딕\": \"'Malgun Gothic','맑은 고딕'\",\n \"굴림\": \"'Gulim','굴림'\",\n \"굴림체\": \"'GulimChe','굴림체','Gulim'\",\n \"돋움\": \"'Dotum','돋움'\",\n \"돋움체\": \"'DotumChe','돋움체','Dotum'\",\n \"바탕\": \"'Batang','바탕'\",\n \"바탕체\": \"'BatangChe','바탕체','Batang'\",\n \"궁서\": \"'Gungsuh','궁서'\",\n \"궁서체\": \"'GungsuhChe','궁서체','Gungsuh'\",\n \"나눔고딕\": \"'NanumGothic','나눔고딕'\",\n \"나눔명조\": \"'NanumMyeongjo','나눔명조'\",\n \"맑은 고딕 Semilight\": \"'Malgun Gothic Semilight','맑은 고딕'\",\n}\n\n/** 명조/바탕 계열(serif) 여부 — 아니면 고딕/돋움(sans)으로 본다 */\nfunction isSerifFace(face: string): boolean {\n return /바탕|명조|Batang|Myeong|Mincho|궁서|Gungsuh|Serif|신명|순명|Song|송/i.test(face)\n}\n\n/** CSS font-family 토큰 인용 — 영숫자·하이픈만이면 무인용, 그 외(공백·한글)는 작은따옴표 */\nfunction cssQuote(name: string): string {\n return /^[A-Za-z0-9_-]+$/.test(name) ? name : `'${name.replace(/['\"\\\\]/g, \"\")}'`\n}\n\n/**\n * HWP 글꼴명 → CSS font-family 스택. 정확한 글꼴 먼저, 이어서 계열 폴백(명조=serif /\n * 고딕=sans, 각 한국어 렌더 가능 글꼴 포함)을 붙여 미설치 시에도 획 계열을 보존한다.\n */\nexport function hwpFaceToCssStack(face: string | null | undefined): string {\n const trimmed = (face ?? \"\").trim()\n if (!trimmed) return \"\"\n const generic = isSerifFace(trimmed)\n ? \"'HCR Batang','Batang','Noto Serif KR',serif\"\n : \"'Malgun Gothic','HCR Dotum','Noto Sans KR',sans-serif\"\n const head = FONT_ALIASES[trimmed] ?? cssQuote(trimmed)\n return `${head},${generic}`\n}\n\n/** fontfaces 테이블에서 HANGUL 그룹의 font id → 글꼴명 맵 (없으면 첫 그룹) */\nfunction collectHangulFonts(root: Element): Map<string, string> {\n const map = new Map<string, string>()\n const findFaces = (el: Element, depth: number): Element | null => {\n if (depth > 24) return null\n for (const ch of Array.from(el.childNodes)) {\n if (ch.nodeType !== 1) continue\n const e = ch as Element\n if ((e.tagName || \"\").replace(/^[^:]+:/, \"\") === \"fontfaces\") return e\n const f = findFaces(e, depth + 1)\n if (f) return f\n }\n return null\n }\n const faces = findFaces(root, 0)\n if (!faces) return map\n let group: Element | null = null\n let firstGroup: Element | null = null\n for (const ch of Array.from(faces.childNodes)) {\n if (ch.nodeType !== 1) continue\n const e = ch as Element\n if ((e.tagName || \"\").replace(/^[^:]+:/, \"\") !== \"fontface\") continue\n if (!firstGroup) firstGroup = e\n if ((e.getAttribute(\"lang\") ?? \"\").toUpperCase() === \"HANGUL\") { group = e; break }\n }\n group = group ?? firstGroup\n if (!group) return map\n for (const ch of Array.from(group.childNodes)) {\n if (ch.nodeType !== 1) continue\n const e = ch as Element\n if ((e.tagName || \"\").replace(/^[^:]+:/, \"\") !== \"font\") continue\n const id = e.getAttribute(\"id\")\n const face = e.getAttribute(\"face\")\n if (id != null && face) map.set(id, face)\n }\n return map\n}\n\n/** \"0.12 mm\" / \"0.5 mm\" → pt */\nfunction borderWidthPt(v: string | null | undefined): number {\n const n = parseFloat(v ?? \"\")\n if (!Number.isFinite(n)) return 0.34\n return n * 2.834645 // mm → pt\n}\n\nfunction parseEdge(el: Element | null): RenderBorderEdge | undefined {\n if (!el) return undefined\n const type = el.getAttribute(\"type\") ?? \"NONE\"\n if (type === \"NONE\") return undefined\n return { type, widthPt: borderWidthPt(el.getAttribute(\"width\")), color: el.getAttribute(\"color\") ?? \"#000000\" }\n}\n\n/** 서브트리에서 localName이 일치하는 첫 요소를 재귀 탐색 (switch/case 래핑 대응) */\nfunction findDeep(el: Element, name: string, depth = 0): Element | null {\n if (depth > 32) return null\n const children = el.childNodes\n if (!children) return null\n for (let i = 0; i < children.length; i++) {\n const ch = children[i]\n if (ch.nodeType !== 1) continue\n const e = ch as Element\n if ((e.tagName || \"\").replace(/^[^:]+:/, \"\") === name) return e\n const found = findDeep(e, name, depth + 1)\n if (found) return found\n }\n return null\n}\n\n/** paraPr 요소 → 문단 기하 (margin·lineSpacing은 hp:switch/case 안이라 재귀 탐색) */\nfunction parseParaGeom(el: Element): RenderParaGeom {\n const g: RenderParaGeom = { ...DEFAULT_PARA_GEOM }\n const ls = findDeep(el, \"lineSpacing\")\n if (ls) {\n g.lineSpacingType = ls.getAttribute(\"type\") ?? \"PERCENT\"\n g.lineSpacingValue = Number(ls.getAttribute(\"value\")) || 160\n }\n const margin = findDeep(el, \"margin\")\n if (margin) {\n const v = (n: string): number => {\n const c = findDeep(margin, n)\n return c ? Number(c.getAttribute(\"value\")) || 0 : 0\n }\n g.marginLeft = v(\"left\")\n g.marginRight = v(\"right\")\n g.marginIntent = v(\"intent\")\n g.spaceBefore = v(\"prev\")\n g.spaceAfter = v(\"next\")\n }\n return g\n}\n\n/** header.xml(구버전 head.xml) → 렌더용 스타일 맵 */\nexport function parseRenderStyles(headXml: string): RenderStyles {\n const styles: RenderStyles = { charPr: new Map(), paraAlign: new Map(), paraGeom: new Map(), borderFill: new Map() }\n const doc = createXmlParser().parseFromString(headXml, \"text/xml\")\n const root = doc.documentElement as unknown as Element | null\n if (!root) return styles\n\n // fontfaces(HANGUL) 선파싱 — charPr fontRef 해석에 쓴다\n const hangulFonts = collectHangulFonts(root)\n\n const walk = (el: Element): void => {\n const tag = (el.tagName || \"\").replace(/^[^:]+:/, \"\")\n if (tag === \"charPr\") {\n const id = el.getAttribute(\"id\")\n if (id != null) {\n const ratioEl = findChildByLocalName(el, \"ratio\")\n const spacingEl = findChildByLocalName(el, \"spacing\")\n const underlineEl = findChildByLocalName(el, \"underline\")\n const textColor = el.getAttribute(\"textColor\")\n const fontRef = findChildByLocalName(el, \"fontRef\")\n const fontId = fontRef?.getAttribute(\"hangul\") ?? fontRef?.getAttribute(\"latin\")\n const face = fontId != null ? hangulFonts.get(fontId) : undefined\n styles.charPr.set(id, {\n height: Number(el.getAttribute(\"height\")) || 1000,\n bold: findChildByLocalName(el, \"bold\") != null,\n italic: findChildByLocalName(el, \"italic\") != null,\n underline: underlineEl != null && (underlineEl.getAttribute(\"type\") ?? \"NONE\") !== \"NONE\",\n color: textColor && textColor !== \"#000000\" && textColor.toLowerCase() !== \"none\" ? textColor : undefined,\n ratio: Number(ratioEl?.getAttribute(\"hangul\")) || 100,\n spacing: Number(spacingEl?.getAttribute(\"hangul\")) || 0,\n fontFamily: face ? hwpFaceToCssStack(face) : undefined,\n })\n }\n } else if (tag === \"paraPr\") {\n const id = el.getAttribute(\"id\")\n if (id != null) {\n const align = findChildByLocalName(el, \"align\")\n styles.paraAlign.set(id, (align?.getAttribute(\"horizontal\") as ParaAlign) || \"JUSTIFY\")\n styles.paraGeom.set(id, parseParaGeom(el))\n }\n } else if (tag === \"borderFill\") {\n const id = el.getAttribute(\"id\")\n if (id != null) {\n const bf: RenderBorderFill = {\n left: parseEdge(findChildByLocalName(el, \"leftBorder\")),\n right: parseEdge(findChildByLocalName(el, \"rightBorder\")),\n top: parseEdge(findChildByLocalName(el, \"topBorder\")),\n bottom: parseEdge(findChildByLocalName(el, \"bottomBorder\")),\n }\n const fillBrush = findChildByLocalName(el, \"fillBrush\")\n const winBrush = fillBrush ? findChildByLocalName(fillBrush, \"winBrush\") : null\n const face = winBrush?.getAttribute(\"faceColor\")\n if (face && face.toLowerCase() !== \"none\") bf.fill = face\n styles.borderFill.set(id, bf)\n }\n }\n const children = el.childNodes\n for (let i = 0; i < children.length; i++) {\n const ch = children[i]\n if (ch.nodeType === 1) walk(ch as Element)\n }\n }\n walk(root)\n return styles\n}\n"],"mappings":"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","names":[]}
@@ -1,5 +1,5 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// src/utils.ts
2
- var VERSION = true ? "3.16.2" : "0.0.0-dev";
2
+ var VERSION = true ? "3.18.0" : "0.0.0-dev";
3
3
  function toArrayBuffer(buf) {
4
4
  if (buf.byteOffset === 0 && buf.byteLength === buf.buffer.byteLength) {
5
5
  return buf.buffer;
@@ -389,7 +389,13 @@ function flattenLayoutTables(blocks) {
389
389
  if (numCols < 4 && (totalNewlines > 5 || numRows <= 2 && totalTextLen > 300)) {
390
390
  for (let r = 0; r < numRows; r++) {
391
391
  for (let c = 0; c < numCols; c++) {
392
- const cellText = _optionalChain([cells, 'access', _15 => _15[r], 'optionalAccess', _16 => _16[c], 'optionalAccess', _17 => _17.text, 'optionalAccess', _18 => _18.trim, 'call', _19 => _19()]);
392
+ const cell = _optionalChain([cells, 'access', _15 => _15[r], 'optionalAccess', _16 => _16[c]]);
393
+ if (!cell) continue;
394
+ if (_optionalChain([cell, 'access', _17 => _17.blocks, 'optionalAccess', _18 => _18.length])) {
395
+ result.push(...flattenLayoutTables(cell.blocks));
396
+ continue;
397
+ }
398
+ const cellText = _optionalChain([cell, 'access', _19 => _19.text, 'optionalAccess', _20 => _20.trim, 'call', _21 => _21()]);
393
399
  if (!cellText) continue;
394
400
  for (const line of cellText.split("\n")) {
395
401
  const trimmed = line.trim();
@@ -405,6 +411,35 @@ function flattenLayoutTables(blocks) {
405
411
  }
406
412
  return result;
407
413
  }
414
+ var RUNNING_HEADER_MAX_LEN = 40;
415
+ var RUNNING_HEADER_MIN_FREQ = 3;
416
+ function isRunningHeaderCandidate(block) {
417
+ if (block.type !== "paragraph" && block.type !== "heading") return false;
418
+ const text = _optionalChain([block, 'access', _22 => _22.text, 'optionalAccess', _23 => _23.trim, 'call', _24 => _24()]);
419
+ if (!text || text.length > RUNNING_HEADER_MAX_LEN) return false;
420
+ return /^\d+\.\s/.test(text);
421
+ }
422
+ function dedupeRunningHeaders(blocks) {
423
+ const freq = /* @__PURE__ */ new Map();
424
+ for (const block of blocks) {
425
+ if (!isRunningHeaderCandidate(block)) continue;
426
+ const text = block.text.trim();
427
+ freq.set(text, (_nullishCoalesce(freq.get(text), () => ( 0))) + 1);
428
+ }
429
+ const seen = /* @__PURE__ */ new Set();
430
+ const result = [];
431
+ for (const block of blocks) {
432
+ if (isRunningHeaderCandidate(block)) {
433
+ const text = block.text.trim();
434
+ if ((_nullishCoalesce(freq.get(text), () => ( 0))) >= RUNNING_HEADER_MIN_FREQ) {
435
+ if (seen.has(text)) continue;
436
+ seen.add(text);
437
+ }
438
+ }
439
+ result.push(block);
440
+ }
441
+ return result;
442
+ }
408
443
  function blocksToMarkdown(blocks) {
409
444
  const lines = [];
410
445
  for (let i = 0; i < blocks.length; i++) {
@@ -442,7 +477,7 @@ function blocksToMarkdown(blocks) {
442
477
  if (!text) continue;
443
478
  if (/^\[별표\s*\d+/.test(text)) {
444
479
  const nextBlock = blocks[i + 1];
445
- if (_optionalChain([nextBlock, 'optionalAccess', _20 => _20.type]) === "paragraph" && nextBlock.text && /관련\)?$/.test(nextBlock.text)) {
480
+ if (_optionalChain([nextBlock, 'optionalAccess', _25 => _25.type]) === "paragraph" && nextBlock.text && /관련\)?$/.test(nextBlock.text)) {
446
481
  lines.push("", `## ${escapeGfm(text)} ${escapeGfm(nextBlock.text)}`, "");
447
482
  i++;
448
483
  } else {
@@ -490,13 +525,13 @@ function hasMergedCells(table) {
490
525
  function hasNestedTables(table) {
491
526
  for (const row of table.cells) {
492
527
  for (const cell of row) {
493
- if (_optionalChain([cell, 'access', _21 => _21.blocks, 'optionalAccess', _22 => _22.some, 'call', _23 => _23((b) => b.type === "table" && b.table)])) return true;
528
+ if (_optionalChain([cell, 'access', _26 => _26.blocks, 'optionalAccess', _27 => _27.some, 'call', _28 => _28((b) => b.type === "table" && b.table)])) return true;
494
529
  }
495
530
  }
496
531
  return false;
497
532
  }
498
533
  function cellInnerHtml(cell) {
499
- if (_optionalChain([cell, 'access', _24 => _24.blocks, 'optionalAccess', _25 => _25.length])) {
534
+ if (_optionalChain([cell, 'access', _29 => _29.blocks, 'optionalAccess', _30 => _30.length])) {
500
535
  return cell.blocks.map((b) => {
501
536
  if (b.type === "table" && b.table) {
502
537
  const cap = b.table.caption ? sanitizeText(b.table.caption) : "";
@@ -529,7 +564,7 @@ function tableToHtml(table) {
529
564
  const rowHtml = [];
530
565
  for (let c = 0; c < numCols; c++) {
531
566
  if (skip.has(`${r},${c}`)) continue;
532
- const cell = _optionalChain([cells, 'access', _26 => _26[r], 'optionalAccess', _27 => _27[c]]);
567
+ const cell = _optionalChain([cells, 'access', _31 => _31[r], 'optionalAccess', _32 => _32[c]]);
533
568
  if (!cell) continue;
534
569
  for (let dr = 0; dr < cell.rowSpan; dr++) {
535
570
  for (let dc = 0; dc < cell.colSpan; dc++) {
@@ -574,7 +609,7 @@ function tableToMarkdown(table) {
574
609
  for (let r = 0; r < numRows; r++) {
575
610
  for (let c = 0; c < numCols; c++) {
576
611
  if (skip.has(`${r},${c}`)) continue;
577
- const cell = _optionalChain([cells, 'access', _28 => _28[r], 'optionalAccess', _29 => _29[c]]);
612
+ const cell = _optionalChain([cells, 'access', _33 => _33[r], 'optionalAccess', _34 => _34[c]]);
578
613
  if (!cell) continue;
579
614
  display[r][c] = escapeGfm(sanitizeText(cell.text)).replace(/\|/g, "\\|").replace(/\n/g, "<br>");
580
615
  for (let dr = 0; dr < cell.rowSpan; dr++) {
@@ -647,5 +682,6 @@ var HEADING_RATIO_H3 = 1.15;
647
682
 
648
683
 
649
684
 
650
- exports.VERSION = VERSION; exports.toArrayBuffer = toArrayBuffer; exports.KordocError = KordocError; exports.isPathTraversal = isPathTraversal; exports.normalizeSectionHref = normalizeSectionHref; exports.compareSectionPaths = compareSectionPaths; exports.precheckZipSize = precheckZipSize; exports.stripDtd = stripDtd; exports.sanitizeHref = sanitizeHref; exports.safeMin = safeMin; exports.safeMax = safeMax; exports.classifyError = classifyError; exports.mapPuaText = mapPuaText; exports.MAX_COLS = MAX_COLS; exports.MAX_ROWS = MAX_ROWS; exports.buildTable = buildTable; exports.convertTableToText = convertTableToText; exports.flattenLayoutTables = flattenLayoutTables; exports.blocksToMarkdown = blocksToMarkdown; exports.HEADING_RATIO_H1 = HEADING_RATIO_H1; exports.HEADING_RATIO_H2 = HEADING_RATIO_H2; exports.HEADING_RATIO_H3 = HEADING_RATIO_H3;
651
- //# sourceMappingURL=chunk-72LLOLFX.cjs.map
685
+
686
+ exports.VERSION = VERSION; exports.toArrayBuffer = toArrayBuffer; exports.KordocError = KordocError; exports.isPathTraversal = isPathTraversal; exports.normalizeSectionHref = normalizeSectionHref; exports.compareSectionPaths = compareSectionPaths; exports.precheckZipSize = precheckZipSize; exports.stripDtd = stripDtd; exports.sanitizeHref = sanitizeHref; exports.safeMin = safeMin; exports.safeMax = safeMax; exports.classifyError = classifyError; exports.mapPuaText = mapPuaText; exports.MAX_COLS = MAX_COLS; exports.MAX_ROWS = MAX_ROWS; exports.buildTable = buildTable; exports.convertTableToText = convertTableToText; exports.flattenLayoutTables = flattenLayoutTables; exports.dedupeRunningHeaders = dedupeRunningHeaders; exports.blocksToMarkdown = blocksToMarkdown; exports.HEADING_RATIO_H1 = HEADING_RATIO_H1; exports.HEADING_RATIO_H2 = HEADING_RATIO_H2; exports.HEADING_RATIO_H3 = HEADING_RATIO_H3;
687
+ //# sourceMappingURL=chunk-5HDBLRVP.cjs.map