Skip to content

Latest commit

 

History

History
295 lines (224 loc) · 12.7 KB

File metadata and controls

295 lines (224 loc) · 12.7 KB

hwp2md — Implementation Plan

HWP/HWPX ↔ Markdown 양방향 변환기 (Rust)

목표

한글(HWP/HWPX) 문서를 Markdown으로, Markdown을 HWPX 문서로 변환하는 CLI 도구 + 라이브러리. 모든 HWP 파싱/생성 코드를 자체 구현 — 라이선스 독립성 확보.

설계 원칙

  1. 라이선스 독립: HWP 전용 크레이트(unhwp, hwpforge, hwpers 등) 미사용
  2. Generic 의존성만 사용: cfb, zip, quick-xml, flate2, comrak 등 범용 크레이트
  3. IR 기반 아키텍처: 포맷별 reader/writer가 공통 IR을 통해 변환
  4. 점진적 구현: 텍스트 추출 → 서식 → 테이블 → 이미지 → 수식 순

의존성

# 컨테이너/압축 (범용)
cfb = "0.14"           # OLE2/CFB — HWP 5.0 컨테이너
zip = "2.0"            # ZIP — HWPX 컨테이너
flate2 = "1.0"         # Zlib — HWP 스트림 압축

# 파싱 (범용)
quick-xml = "0.37"     # XML — HWPX 콘텐츠
byteorder = "1.5"      # 바이트 오더 — HWP 레코드
encoding_rs = "0.8"    # 문자 인코딩

# Markdown (범용)
comrak = "0.34"        # GFM Markdown 파서/렌더러

# CLI/유틸 (범용)
clap = "4"
serde = "1"
thiserror = "2"
anyhow = "1"
tracing = "0.1"

아키텍처

HWP 5.0 ──→ hwp::reader (CFB+zlib+record) ──→ IR ──→ md::writer ──→ Markdown
HWPX    ──→ hwpx::reader (ZIP+XML)        ──→ IR ──→ md::writer ──→ Markdown

Markdown ──→ md::parser (comrak)           ──→ IR ──→ hwpx::writer ──→ HWPX

HWP 5.0 Record Format

┌─────────────────────────────────────────┐
│ 4-byte Header (Little Endian)           │
│   bits  0-9:  tag_id (0x3FF mask)       │
│   bits 10-19: level   (0x3FF mask)      │
│   bits 20-31: size    (0xFFF mask)      │
│   if size == 0xFFF → 4-byte extended    │
├─────────────────────────────────────────┤
│ data[size]                              │
└─────────────────────────────────────────┘

Text: UTF-16LE, control chars 0x0000-0x001F (extended controls use 8 code units)

중간 표현 (IR)

Document
├── Metadata (title, author, dates, keywords)
├── Sections[]
│   └── Blocks[]
│       ├── Heading (level, inlines)
│       ├── Paragraph (inlines)
│       ├── Table (rows → cells, col_count)
│       ├── CodeBlock (language, code)
│       ├── BlockQuote (nested blocks)
│       ├── List (ordered, start, items)
│       ├── Image (src, alt)
│       ├── HorizontalRule
│       ├── Footnote (id, content)
│       └── Math (display, tex)
└── Assets[] (name, data, mime_type)

모듈 구조

src/
├── main.rs          — CLI (to-md, to-hwpx, info)
├── lib.rs           — 모듈 선언
├── convert.rs       — 변환 오케스트레이터
├── error.rs         — 에러 타입
├── ir.rs            — 중간 표현
├── hwp/
│   ├── mod.rs       — HWP 모듈 공개 인터페이스
│   ├── model.rs     — HWP 내부 모델 (FileHeader, DocInfo, Section, Paragraph)
│   ├── record.rs    — 레코드 파싱 (4-byte header, tag constants)
│   └── reader.rs    — CFB → zlib → records → IR 변환
├── hwpx/
│   ├── mod.rs       — HWPX 모듈 공개 인터페이스
│   ├── reader.rs    — ZIP+XML → IR 변환
│   └── writer.rs    — IR → HWPX (ZIP+XML) 생성
└── md/
    ├── mod.rs       — Markdown 모듈 공개 인터페이스
    ├── writer.rs    — IR → Markdown 렌더러
    └── parser.rs    — Markdown → IR 파서 (comrak)

구현 단계

Phase 1: 프로젝트 구조 + 기본 파싱 (완료)

자체 구현 기반 프로젝트 재구성.

  • 1.1 Cargo.toml — HWP 전용 크레이트 제거, generic 의존성만
  • 1.2 IR 설계 — Document/Section/Block/Inline 계층 + Asset
  • 1.3 HWP reader — CFB + zlib + record parsing + UTF-16LE text
  • 1.4 HWPX reader — ZIP + XML 파싱
  • 1.5 MD writer — IR → Markdown (GFM 호환)
  • 1.6 MD parser — Markdown → IR (comrak 기반)
  • 1.7 HWPX writer — IR → HWPX (ZIP+XML 생성)
  • 1.8 Convert 오케스트레이터 — to-md, to-hwpx, info 명령

Phase 2: HWP 파싱 고도화 (1주)

HWP 5.0 파서 정확도 향상.

  • 2.1 테이블 파싱 — CTRL_TABLE + LIST_HEADER + 셀 내 paragraphs
  • 2.2 이미지/바이너리 — BinData 참조 + 이미지 추출
  • 2.3 하이퍼링크 — CTRL 오브젝트에서 URL 추출
  • 2.4 각주/미주 — CTRL_FOOTNOTE/CTRL_ENDNOTE 파싱
  • 2.5 수식 — EQEDIT 스크립트 → LaTeX 변환
  • 2.6 DRM/암호화 감지 — 명확한 에러 메시지
  • 2.7 샘플 테스트 — 다양한 HWP 파일로 검증

Phase 3: HWPX 파싱 고도화 (1주)

HWPX XML 파서 정확도 향상.

  • 3.1 스타일 상속 — header.xml 스타일 정의 파싱
  • 3.2 테이블 — colspan/rowspan 처리
  • 3.3 이미지 — BinData 참조 해결
  • 3.4 각주/미주 — XML 요소 매핑
  • 3.5 수식 — OWPML 수식 요소 → LaTeX
  • 3.6 정부 공문서 HWPX 샘플 검증

Phase 4: Markdown 렌더러 고도화 (1주)

  • 4.1 GFM 호환 검증 — comrak 왕복 테스트
  • 4.2 복잡한 테이블 — colspan/rowspan → HTML 폴백
  • 4.3 이미지 참조 — 상대경로 / inline base64 옵션
  • 4.4 frontmatter — YAML 메타데이터
  • 4.5 info 명령 — 문서 통계 (페이지, 글자수, 스타일)

Phase 5: HWPX 라이터 고도화 (1주)

  • 5.1 스타일 매핑 — heading/paragraph 스타일 정의
  • 5.2 테이블 생성 — Markdown 테이블 → HWPX 표
  • 5.3 이미지 삽입 — Asset → HWPX BinData
  • 5.4 YAML 스타일 템플릿 — 커스텀 스타일 지원
  • 5.5 한글 2022+ 호환 검증
  • 5.6 라운드트립 테스트 — HWP→MD→HWPX 왕복 검증

Phase 6: CLI 완성 + 배포 (1주)

  • 6.1 에러 처리 — 사용자 친화적 에러 메시지
  • 6.2 배치 변환 — 디렉토리 일괄 변환
  • 6.3 CI/CD — GitHub Actions (build + test + clippy)
  • 6.4 테스트 커버리지 80%+
  • 6.5 crates.io 배포
  • 6.6 README 업데이트

변환 매핑 테이블

HWP/HWPX 요소 Markdown 매핑 비고
제목 (개요 1~6) # ~ ###### 수준 매핑
본문 일반 텍스트
굵게/기울임 **bold** / *italic*
밑줄 <u>text</u> HTML 폴백
취소선 ~~text~~ GFM
위첨자/아래첨자 <sup>/<sub> HTML 폴백
하이퍼링크 [text](url)
GFM table colspan → HTML 폴백
이미지 ![alt](path) 파일 추출
코드 블록 ```lang ``` 고정폭 스타일 추론
인용 > text
순서/비순서 목록 1. / - 중첩 지원
각주 [^1] GFM footnotes
수식 $LaTeX$ 한글 수식 → LaTeX
머리글/바닥글 <!-- header/footer --> 마커 양방향 round-trip
다단 단일 단으로 평탄화

제한사항

  • HWP DRM (배포용) 문서는 지원하지 않음
  • 다단 레이아웃은 단일 단으로 평탄화
  • 복잡한 테이블 (colspan/rowspan)은 HTML 폴백
  • 머리글/바닥글은 <!-- header/footer --> HTML 코멘트 마커로 round-trip
  • MD → HWP (바이너리)는 지원하지 않음 — HWPX만 출력
  • 한글 수식 → LaTeX 변환은 기본적인 수준만 지원

Sprint 90 완료 (2026-05-30)

Sprint 89 완료 (2026-05-30). strikeout text + color #span; H1/H2 순서+레벨 이중 핀; MD→HWPX→MD bold/heading 왕복 안정성.

  • P1: 관(subsection) 감지 — 대형 법령 픽스처 확보 시 검토 (BLOCKER 유지) — 이월
  • P2: HWPX inline link 통합 테스트 — ✅ multiple links (URL isolation), unsafe URL (writer gate pin)
  • P3: 복합 인라인 서식 통합 테스트 — ✅ bold+underline → <u>**text**</u>, bold+italic+color → <span>***text***</span>
  • P4: 긴 문서 통합 테스트 — ✅ H1/para/H2/code/para IR 위치 순서 + Markdown ATX 순서

Sprint 91 완료 (2026-05-30)

Sprint 90 완료 (2026-05-30). 1527 tests. hyperlink URL isolation + unsafe gate pin; combined inline formatting; complex doc ordering.

  • P1: 관(subsection) 감지 — BLOCKER 유지 — 이월
  • P2: integration.rs 분할 — ✅ integration_formatting.rs 분리 (3418 → 3075 lines, -343 lines)
  • P3: charPr 조합 테스트 — ✅ strikethrough+underline <u>~~t~~</u>, bold+strikethrough ~~**t**~~, italic+underline <u>*t*</u>
  • P4: CTRL_RUBY/BinData — 이미 충분히 커버됨, 스킵

Sprint 92 완료 (2026-05-30)

Sprint 91 완료 (2026-05-30). 1530 tests. formatting split + 3 combination tests. integration.rs still 3075 lines.

  • P1: 관(subsection) 감지 — BLOCKER 유지 — 이월
  • P2: integration_hyperlink.rs 분리 — ✅ 4 hyperlink tests 이전 (3075 → 2913 lines)
  • P3: 인라인 링크 + 포맷 조합 테스트 — ✅ bold+link [**t**](url), italic+link [*t*](url), color+link [<span>t</span>](url)
  • P4: MD→HWPX→MD 리스트 왕복 — ✅ ordered list IR-structural assertion + text presence + ordering

Sprint 93 완료 (2026-05-31)

Sprint 92 완료 (2026-05-30). 1534 tests. hyperlink split + formatted link + list roundtrip.

  • P1: 관(subsection) 감지 — BLOCKER 유지 — 이월
  • P2: integration_footnote.rs 분리 — ✅ 5 footnote/noteRef tests (2913 → 2719 lines)
  • P3: 경계 테스트 — ✅ bold+italic+link [***t***](url), paren URL [t](<url>) angle-bracket
  • P4: unordered list roundtrip — ✅ Block::List { ordered: false } structural pin

Sprint 94 완료 (2026-05-31)

Sprint 93 완료 (2026-05-31). 1537 tests. footnote split + paren URL + unordered list roundtrip.

  • P1: 관(subsection) 감지 — BLOCKER 유지 — 이월
  • P2: integration_list.rs 분리 — ✅ 5 list tests 이전 (2719 → 2504 lines)
  • P3: read_fixture → fixtures/mod.rs — ✅ pub fn 공유, 모든 4개 test file에서 local copy 제거
  • P4: Table MD→HWPX→MD roundtrip — ✅ Block::Table 구조 + 2-column separator + Alice<90<Bob 조합

Sprint 95 완료 (2026-05-31)

Sprint 94 완료 (2026-05-31). 1538 tests. list split + shared read_fixture + table roundtrip.

  • P1: 관(subsection) 감지 — BLOCKER 유지 — 이월
  • P2: integration_blocks.rs 분리 — ✅ 4 block tests 이전 (equation×2, HR, blockquote)
  • P3: italic/strikethrough roundtrip — ✅ 3-phase 패턴
  • P4: code block roundtrip — ✅ blockquote/HR 대체 (lossy encoding 발견)

Sprint 96 완료 (2026-05-31)

Sprint 95 완료 (2026-05-31). 1541 tests. blocks split + italic/strikethrough/code roundtrip.

  • P1: 관(subsection) 감지 — BLOCKER 유지 — 이월
  • P2: integration_image.rs 분리 — ✅ 2 image tests 이전 (Sprint 84 P4)
  • P3: roundtrip helpers — ✅ ir_hwpx_roundtrip + any_inline_in_doc (top-level paragraphs 한정)
  • P4: BlockQuote/HR negative assertion — ✅ 손실 계약 pre+!still 패턴으로 executable화

Sprint 97 완료 (2026-05-31)

Sprint 96 완료 (2026-05-31). 1543 tests. image split + roundtrip helpers + lossy contracts.

  • P1: 관(subsection) 감지 — BLOCKER 유지 — 이월
  • P2: integration_table_blocks.rs 분리 — ✅ colspan + no-lang code block 2개 이전
  • P3: any_block_in_doc → fixtures/mod.rs — ✅ lossy-contract tests 리팩 (-12 lines)
  • P4: 헤딩 roundtrip — ✅ H1/H2/H3 hp:styleIDRef numeric → parse_hwpx_style_ref lossless

Sprint 98 로드맵

Sprint 97 완료 (2026-05-31). 1544 tests. table/code split + heading roundtrip. integration.rs 2430 lines.

  • P1: 관(subsection) 감지 — BLOCKER 계속 유지
  • P2: integration.rs 추가 분할 — multi-section/roundtrip 섹션 분리 (Sprint 88 P4 + 89 P4)
  • P3: 인라인 서식 추가 roundtrip — underline MD→HWPX→MD + superscript/subscript roundtrip
  • P4: Heading H4-H6 roundtrip + 레벨 clamp 확인 (writer clamps to 1-6; H7→H6)

라이선스

GPL-3.0-only

참조