From 498456e19383cb735badb46b87e7a00afd5c7d15 Mon Sep 17 00:00:00 2001 From: Eleven Date: Sat, 25 Jul 2026 00:01:41 +0900 Subject: [PATCH] =?UTF-8?q?=EB=AC=B8=EC=84=9C:=20CLI=20=EB=A0=88=ED=8D=BC?= =?UTF-8?q?=EB=9F=B0=EC=8A=A4=20=EC=9E=90=EB=8F=99=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?+=20=EB=93=9C=EB=A6=AC=ED=94=84=ED=8A=B8=20=EA=B2=8C=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20=E2=80=94=20clap=20=EC=A0=95=EC=9D=98=EB=A5=BC=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=EC=9D=98=20=EB=8B=A8=EC=9D=BC=20=EC=9B=90?= =?UTF-8?q?=EC=B2=9C=EC=9C=BC=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CLI 정의(Cli/Cmd/value_enum)를 lib 타깃(hwp_cli::cli)으로 승격 — 테스트가 clap Command 트리를 introspect할 수 있게 함(main.rs는 파싱·디스패치만). - tests/cli_reference.rs: 명령 트리 → docs/manual/cli-reference.md 결정적 생성. 커밋본과 어긋나면 테스트 실패(= CI 드리프트 게이트), HWP_UPDATE_DOCS=1로 재생성(bless). - 초기 생성본 등재: 14개 명령 전부 — README 수동 표에 누락돼 있던 bookmarks·slots·fill·validate 포함(드리프트 실증 → 자동화 동기). - README: 누락 4개 명령 행 보수 + 자동 생성 레퍼런스 링크 추가. - help 문구의 낡은 마일스톤 표기((M1에서 구현) 등) 제거. 검증: scripts/check.sh 통과, 변조 감지 자가 검증(문서 변조→실패→bless 복원→통과) 확인. Co-Authored-By: Claude Fable 5 --- README.md | 6 + crates/hwp-cli/src/cli.rs | 297 +++++++++++++++++++++++++ crates/hwp-cli/src/commands/cat.rs | 2 +- crates/hwp-cli/src/commands/convert.rs | 4 +- crates/hwp-cli/src/commands/render.rs | 2 +- crates/hwp-cli/src/lib.rs | 7 + crates/hwp-cli/src/main.rs | 296 +----------------------- crates/hwp-cli/tests/cli_reference.rs | 269 ++++++++++++++++++++++ docs/manual/cli-reference.md | 222 ++++++++++++++++++ 9 files changed, 811 insertions(+), 294 deletions(-) create mode 100644 crates/hwp-cli/src/cli.rs create mode 100644 crates/hwp-cli/src/lib.rs create mode 100644 crates/hwp-cli/tests/cli_reference.rs create mode 100644 docs/manual/cli-reference.md diff --git a/README.md b/README.md index d8c44045..58120a70 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,8 @@ hwp mcp --font-dir ./fonts ## 명령 레퍼런스 +전체 자동 생성 레퍼런스는 [docs/manual/cli-reference.md](docs/manual/cli-reference.md) — clap 정의에서 생성되며 코드와의 동기화를 CI 테스트가 강제한다. 아래는 요약이다. + | 명령 | 인자 / 플래그 | 설명 | |---|---|---| | `info ` | `--json` | 포맷/버전/속성/스트림 진단 | @@ -173,6 +175,10 @@ hwp mcp --font-dir ./fonts | `new -o ` | `--from `(생략 시 빈 문서) | markdown/JSON IR에서 새 문서 생성. markdown 목록은 진짜 번호(NUMBER)/글머리(BULLET) 머리 문단으로 들여오고(중첩=수준), H1~H3 제목엔 절 번호(`1.`/`1-1.`/`1-1-1.`, 숫자 시작 제목은 생략)를 접두한다 | | `edit -o ` | `--replace "찾기=>바꾸기"`, `--set-cell "표:행:열=값"`(0-기반), `--set-field "이름=값"`, `--create-field "앵커=>이름"`(또는 `"앵커=>이름=값"`, %clk 누름틀 생성), `--insert-image "앵커=>경로"`(또는 `"앵커=>경로@너비x높이"`mm, png/jpg/bmp/gif 삽입), `--seal "앵커=>경로"`(또는 `"앵커=>경로@크기mm"`, 도장 이미지를 앵커 문구 위에 글 앞 부유 배치 — 기본 20mm), `--set-format "찾기:bold=on,size=16,color=#RRGGBB"`, `--set-align "찾기=left\|right\|center\|justify\|distribute\|divide"`, `--insert-para "앵커=>텍스트"`(앵커 문단 뒤), `--insert-para-before "앵커=>텍스트"`(앞), `--delete-para "텍스트"`, `--add-row "표"`, `--add-col "표"`(또는 `"표:위치"`), `--delete-row "표:행"`, `--delete-col "표:열"`, `--merge-cells "표:r1:c1:r2:c2"`(사각 영역 병합), `--split-cell "표:행:열"`(병합 해제), `--verify` (모두 반복 가능) | 기존 문서 편집. 텍스트·서식·구조(문단/표 행·열·셀 병합/분할) 편집. 삽입 문단·행은 앵커/템플릿 모양을 상속하고 합성 경로로 저장(불변식 적용). 표 인덱스는 **재귀 깊이 우선**(중첩 표 포함, --set-cell과 동일). 열 추가·삭제·셀 병합/분할은 **병합 셀 표도 지원**(정품 1,816개 실측 규칙 — 피병합 셀 생략·행 우선·병합 cellSz). `--add-col`은 전체 표 폭을 유지하며 기존 열을 균등 축소(잔차는 행 마지막 기존 셀); 위치 생략 시 표 끝에 추가. `--add-row`는 병합 없는 템플릿 행이 필요(없으면 거부). `--replace`만 있고 hwpx→hwpx이면 **패키지 보존 고속 경로**(미리보기·호환 블록 바이트 유지, 런 분절 교차 매칭 미지원). `--verify`는 쓰기 후 재읽기로 검증 | | `fields ` | `--json` | 필드/누름틀 목록(이름·종류·값·명령) | +| `bookmarks ` | `--json` | 책갈피(bokm) 목록(이름) | +| `slots ` | `--json` | `{{name}}` 텍스트 자리표시자(템플릿 슬롯) 목록 | +| `fill -o ` | `--set "이름=값"`(반복), `--data `, `--json` | 충실도 보존 템플릿 채우기 — hwpx의 `{{name}}` 치환(패키지 보존). `--data`는 이름→값 JSON 객체 파일로 일괄 채움 | +| `validate ` | `--json` | 구조 검증(mimetype·필수 엔트리·XML 파싱) — 유효 시 종료코드 0 | | `diff --ref ` | `--page `(기본 1), `--dpi `(기본 96), `-o/--out `, `--font-dir `(반복), `--tolerance `(기본 16) | 렌더 결과를 한글 기준 PNG와 비교(잉크 적용률·dx/dy 오프셋·픽셀 차이율·MAE) | | `mcp` | `--font-dir `(반복) | MCP stdio 서버 실행 | | `dump ` | `--stream `, `--raw`, `--json` | [개발자용] 레코드/패키지 구조 덤프 | diff --git a/crates/hwp-cli/src/cli.rs b/crates/hwp-cli/src/cli.rs new file mode 100644 index 00000000..a90e8bce --- /dev/null +++ b/crates/hwp-cli/src/cli.rs @@ -0,0 +1,297 @@ +//! `hwp` CLI 정의 — clap derive 기반 명령/플래그 선언. +//! +//! 이 모듈은 lib 타깃으로 노출된다(`hwp_cli::cli`). bin(`main.rs`)이 파싱·디스패치에 +//! 쓰고, `tests/cli_reference.rs`가 `clap::CommandFactory`로 명령 트리를 introspect해 +//! `docs/manual/cli-reference.md`를 자동 생성한다(코드-문서 동기화 게이트). + +use std::path::PathBuf; + +use clap::{Parser, Subcommand, ValueEnum}; + +#[derive(Parser)] +#[command(name = "hwp", version, about = "HWP/HWPX 문서 처리 도구")] +pub struct Cli { + #[command(subcommand)] + pub cmd: Cmd, +} + +#[derive(Subcommand)] +// Edit 변형이 편집 플래그(Vec 다수)로 커서 다른 변형과 크기차가 크다. +// CLI 명령 enum은 시작 시 한 번만 파싱되므로 크기차는 무의미 — 박싱 대신 허용. +#[allow(clippy::large_enum_variant)] +pub enum Cmd { + /// 파일 정보 표시: 포맷/버전/속성/스트림 목록 + Info { + file: PathBuf, + /// JSON으로 출력 + #[arg(long)] + json: bool, + }, + + /// 텍스트 추출 + Cat { + file: PathBuf, + #[arg(long, value_enum, default_value = "plain")] + format: TextFormat, + /// 본문 파싱 없이 PrvText 미리보기만 출력 + #[arg(long)] + preview: bool, + /// 머리말/꼬리말 텍스트도 추출에 포함 (기본: 제외) + #[arg(long = "with-header-footer")] + with_header_footer: bool, + /// 숨은 설명 텍스트도 추출에 포함 (기본: 제외) + #[arg(long = "with-hidden")] + with_hidden: bool, + /// (markdown 전용) markdown과 함께 각 출력 문자 범위의 원본 좌표(섹션/문단)를 + /// 한 줄 JSON 봉투로 출력 — {"markdown": ..., "segments": [...]} + #[arg(long = "with-segments")] + with_segments: bool, + }, + + /// 포맷 변환 + Convert { + input: PathBuf, + #[arg(short, long)] + output: PathBuf, + /// 출력 포맷 (생략 시 확장자에서 추론) + #[arg(long, value_enum)] + to: Option, + /// 변환 중 보존 불가능한(opaque) 데이터 발견 시 실패 처리 + #[arg(long)] + strict: bool, + /// 줄 배치 캐시 보존 (무수정 왕복 전용 — 한글은 내용과 어긋난 + /// 줄 배치를 변조로 판정하므로 기본은 제거) + #[arg(long)] + preserve_layout: bool, + /// JSON 출력 시 첨부 바이너리(이미지)를 base64로 임베드 (자급식 JSON) + #[arg(long)] + embed_bin: bool, + /// (md) 이미지 추출 디렉터리 — 기본 "<출력스템>.media". 상대경로는 출력 + /// 파일 기준으로 해석하고 링크는 입력한 경로 그대로 쓴다 (예: figs) + #[arg(long)] + media_dir: Option, + /// (md) 머리말/꼬리말 텍스트도 포함 (기본: 제외) + #[arg(long = "with-header-footer")] + with_header_footer: bool, + /// (md) 숨은 설명 텍스트도 포함 (기본: 제외) + #[arg(long = "with-hidden")] + with_hidden: bool, + }, + + /// 페이지 렌더링 + Render { + input: PathBuf, + #[arg(short, long)] + output: PathBuf, + /// 페이지 범위: "1", "1-3", "all" + #[arg(long, default_value = "all")] + pages: String, + #[arg(long, default_value_t = 96.0)] + dpi: f64, + /// 출력 포맷 (생략 시 확장자에서 추론) + #[arg(long, value_enum)] + format: Option, + /// 추가 폰트 디렉터리 (반복 가능) + #[arg(long)] + font_dir: Vec, + }, + + /// 새 문서 생성 + New { + #[arg(short, long)] + output: PathBuf, + /// 입력 markdown/JSON 파일 (생략 시 빈 문서) + #[arg(long)] + from: Option, + /// 메타데이터 설정 "키=값" (키: title|author|subject|keywords, 반복 가능) + #[arg(long = "set-meta")] + set_meta: Vec, + }, + + /// 렌더 결과를 한글 기준 PNG와 비교해 오차 측정 (위치 오프셋·픽셀 차이율) + Diff { + input: PathBuf, + /// 한글에서 같은 페이지를 같은 DPI로 내보낸 기준 PNG + #[arg(long)] + r#ref: PathBuf, + /// 비교할 페이지 (1-기반) + #[arg(long, default_value_t = 1)] + page: usize, + #[arg(long, default_value_t = 96.0)] + dpi: f64, + /// 차이 이미지 출력 경로 (생략 시 .diff.png) + #[arg(short, long)] + out: Option, + /// 추가 폰트 디렉터리 (반복 가능) + #[arg(long)] + font_dir: Vec, + /// 채널 차이 허용 오차 (이하면 동일 취급) + #[arg(long, default_value_t = 16)] + tolerance: u8, + }, + + /// 기존 문서 편집 (텍스트 치환·표 셀 설정) — 이미지·서식 보존 + Edit { + input: PathBuf, + #[arg(short, long)] + output: PathBuf, + /// 텍스트 치환 "찾기=>바꾸기" (반복 가능, 모든 일치 치환) + #[arg(long)] + replace: Vec, + /// 표 셀 설정 "표:행:열=값" (반복 가능, 0-기반 인덱스) + #[arg(long = "set-cell")] + set_cell: Vec, + /// 필드/누름틀 채우기 "이름=값" (반복 가능 — hwp fields로 이름 확인) + #[arg(long = "set-field")] + set_field: Vec, + /// 메타데이터 설정 "키=값" (키: title|author|subject|keywords, 반복 가능) + #[arg(long = "set-meta")] + set_meta: Vec, + /// 누름틀 생성 "앵커=>이름" 또는 "앵커=>이름=값" — 앵커 텍스트 뒤에 %clk 필드 삽입 (반복 가능) + #[arg(long = "create-field")] + create_field: Vec, + /// 책갈피 생성 "앵커=>이름" — 앵커 텍스트 뒤에 bokm 지점 표식 삽입 (반복 가능) + #[arg(long = "create-bookmark")] + create_bookmark: Vec, + /// 하이퍼링크 생성 "앵커=>URL" 또는 "앵커=>표시=>URL" — 앵커 뒤에 %hlk 삽입 (반복 가능) + #[arg(long = "create-hyperlink")] + create_hyperlink: Vec, + /// 이미지 삽입 "앵커=>경로" 또는 "앵커=>경로@너비x높이"(mm) — 앵커 뒤에 그림 삽입 (반복 가능) + #[arg(long = "insert-image")] + insert_image: Vec, + /// 도장 날인 "앵커=>경로" 또는 "앵커=>경로@크기mm" — 앵커 문구 위에 도장 부유 배치 (반복 가능) + #[arg(long = "seal")] + seal: Vec, + /// 글자 서식 "찾기:속성=값,…" (예: "제목:bold=on,size=16,color=#FF0000") + #[arg(long = "set-format")] + set_format: Vec, + /// 문단 정렬 "찾기=정렬" (left/right/center/justify/distribute) + #[arg(long = "set-align")] + set_align: Vec, + /// 문단 삽입 "앵커=>텍스트" — 앵커가 있는 문단 뒤에 새 문단 (반복 가능) + #[arg(long = "insert-para")] + insert_para: Vec, + /// 문단 삽입(앞) "앵커=>텍스트" — 앵커가 있는 문단 앞에 새 문단 (반복 가능) + #[arg(long = "insert-para-before")] + insert_para_before: Vec, + /// 문단 삭제 "텍스트" — 텍스트가 있는 문단 삭제 (반복 가능) + #[arg(long = "delete-para")] + delete_para: Vec, + /// 표 행 추가 "표" — N번째 표 끝에 빈 행 (반복 가능, 0-기반; 병합 셀이 있는 표는 거부) + #[arg(long = "add-row")] + add_row: Vec, + /// 표 열 추가 "표"(끝에) 또는 "표:위치"(삽입) — 전체 폭 유지(기존 열 균등 축소). 병합 셀 표도 지원 (반복 가능, 0-기반) + #[arg(long = "add-col")] + add_col: Vec, + /// 표 행 삭제 "표:행" — N번째 표의 R행 (반복 가능, 0-기반; 병합 행은 거부) + #[arg(long = "delete-row")] + delete_row: Vec, + /// 표 열 삭제 "표:열" — N번째 표의 열 삭제. 전체 폭 유지(남은 열에 재분배). 병합 셀은 축소 (반복 가능, 0-기반) + #[arg(long = "delete-col")] + delete_col: Vec, + /// 셀 병합 "표:r1:c1:r2:c2" — 사각 영역을 좌상단 앵커로 병합 (반복 가능, 0-기반) + #[arg(long = "merge-cells")] + merge_cells: Vec, + /// 셀 분할 "표:행:열" — 병합 셀을 1×1로 분해 (반복 가능, 0-기반) + #[arg(long = "split-cell")] + split_cell: Vec, + /// 쓰기 후 재읽기로 검증 + #[arg(long)] + verify: bool, + }, + + /// 필드/누름틀 목록 표시 (이름·종류·값) + Fields { + file: PathBuf, + /// JSON으로 출력 + #[arg(long)] + json: bool, + }, + + /// 책갈피 목록 표시 (이름) + Bookmarks { + file: PathBuf, + /// JSON으로 출력 + #[arg(long)] + json: bool, + }, + + /// `{{name}}` 텍스트 자리표시자(템플릿 슬롯) 목록 표시 + Slots { + file: PathBuf, + /// JSON으로 출력 + #[arg(long)] + json: bool, + }, + + /// 충실도 보존 템플릿 채우기 (hwpx의 `{{name}}` 치환, 패키지 보존) + Fill { + input: PathBuf, + #[arg(short, long)] + output: PathBuf, + /// 자리표시자 채우기 "이름=값" (반복 가능; `{{이름}}` 치환) + #[arg(long)] + set: Vec, + /// 이름→값 JSON 객체 파일 (일괄 채우기) + #[arg(long)] + data: Option, + /// 치환 요약을 JSON으로 출력 ({output, replaced, counts}) + #[arg(long)] + json: bool, + }, + + /// 구조 검증 (mimetype/필수 엔트리/XML 파싱) — 유효하면 종료코드 0 + Validate { + file: PathBuf, + /// JSON으로 출력 + #[arg(long)] + json: bool, + }, + + /// MCP(Model Context Protocol) stdio 서버 — AI 에이전트용 도구 인터페이스 + Mcp { + /// 렌더/diff 도구의 기본 폰트 디렉터리 (반복 가능) + #[arg(long)] + font_dir: Vec, + }, + + /// [개발자용] 레코드/패키지 구조 덤프 + Dump { + file: PathBuf, + /// 대상 스트림/엔트리 (예: "DocInfo", "BodyText/Section0", "Contents/header.xml") + #[arg(long)] + stream: Option, + /// 레코드 페이로드를 hex로 출력 + #[arg(long)] + raw: bool, + /// JSON으로 출력 + #[arg(long)] + json: bool, + }, +} + +#[derive(Clone, Copy, ValueEnum)] +pub enum TextFormat { + Plain, + Markdown, + Json, + Html, +} + +#[derive(Clone, Copy, ValueEnum)] +pub enum ConvertFormat { + Hwp, + Hwpx, + Md, + Json, + Html, + Pdf, + Odt, +} + +#[derive(Clone, Copy, PartialEq, Eq, ValueEnum)] +pub enum RenderFormat { + Png, + Svg, + Pdf, +} diff --git a/crates/hwp-cli/src/commands/cat.rs b/crates/hwp-cli/src/commands/cat.rs index 034475cb..612d860b 100644 --- a/crates/hwp-cli/src/commands/cat.rs +++ b/crates/hwp-cli/src/commands/cat.rs @@ -8,8 +8,8 @@ use std::path::Path; use hwp_model::Document; -use crate::TextFormat; use crate::format::{FileFormat, detect}; +use hwp_cli::cli::TextFormat; /// 포맷을 감지해 IR로 읽는다 (cat/convert/render 공용). /// diff --git a/crates/hwp-cli/src/commands/convert.rs b/crates/hwp-cli/src/commands/convert.rs index 777c5710..b9d50caf 100644 --- a/crates/hwp-cli/src/commands/convert.rs +++ b/crates/hwp-cli/src/commands/convert.rs @@ -5,8 +5,8 @@ use std::path::Path; -use crate::ConvertFormat; use crate::commands::cat::load_document; +use hwp_cli::cli::ConvertFormat; /// markdown 출력 전용 추가 옵션 (다른 포맷에서는 무시). #[derive(Default)] @@ -41,7 +41,7 @@ pub fn run( output, "all", 96.0, - Some(crate::RenderFormat::Pdf), + Some(hwp_cli::cli::RenderFormat::Pdf), Vec::new(), ); } diff --git a/crates/hwp-cli/src/commands/render.rs b/crates/hwp-cli/src/commands/render.rs index ed6cf3d9..bd9dc151 100644 --- a/crates/hwp-cli/src/commands/render.rs +++ b/crates/hwp-cli/src/commands/render.rs @@ -4,8 +4,8 @@ use std::path::{Path, PathBuf}; -use crate::RenderFormat; use crate::commands::cat::load_document; +use hwp_cli::cli::RenderFormat; pub fn run( input: &Path, diff --git a/crates/hwp-cli/src/lib.rs b/crates/hwp-cli/src/lib.rs new file mode 100644 index 00000000..d9782d31 --- /dev/null +++ b/crates/hwp-cli/src/lib.rs @@ -0,0 +1,7 @@ +//! hwp-cli 라이브러리 표면 — CLI 정의만 노출한다. +//! +//! 명령/플래그 선언(`cli`)을 lib으로 올려 두면 `tests/cli_reference.rs`가 +//! `clap::CommandFactory`로 명령 트리를 introspect해 문서를 자동 생성할 수 있다. +//! 실제 디스패치·명령 구현(`commands`, `format`)은 bin 전용이라 여기서 노출하지 않는다. + +pub mod cli; diff --git a/crates/hwp-cli/src/main.rs b/crates/hwp-cli/src/main.rs index 8794d8f8..07bd8afe 100644 --- a/crates/hwp-cli/src/main.rs +++ b/crates/hwp-cli/src/main.rs @@ -1,299 +1,15 @@ //! hwp — HWP/HWPX 문서 처리 CLI. +//! +//! CLI 정의(`Cli`/`Cmd`/value_enum)는 lib 타깃(`hwp_cli::cli`)에 있다 — 문서 자동 +//! 생성 테스트가 명령 트리를 introspect할 수 있게 하기 위함. 여기서는 파싱과 +//! 서브커맨드 디스패치만 담당한다. mod commands; mod format; -use std::path::PathBuf; +use clap::Parser; -use clap::{Parser, Subcommand, ValueEnum}; - -#[derive(Parser)] -#[command(name = "hwp", version, about = "HWP/HWPX 문서 처리 도구")] -struct Cli { - #[command(subcommand)] - cmd: Cmd, -} - -#[derive(Subcommand)] -// Edit 변형이 편집 플래그(Vec 다수)로 커서 다른 변형과 크기차가 크다. -// CLI 명령 enum은 시작 시 한 번만 파싱되므로 크기차는 무의미 — 박싱 대신 허용. -#[allow(clippy::large_enum_variant)] -enum Cmd { - /// 파일 정보 표시: 포맷/버전/속성/스트림 목록 - Info { - file: PathBuf, - /// JSON으로 출력 - #[arg(long)] - json: bool, - }, - - /// 텍스트 추출 (M1에서 구현) - Cat { - file: PathBuf, - #[arg(long, value_enum, default_value = "plain")] - format: TextFormat, - /// 본문 파싱 없이 PrvText 미리보기만 출력 - #[arg(long)] - preview: bool, - /// 머리말/꼬리말 텍스트도 추출에 포함 (기본: 제외) - #[arg(long = "with-header-footer")] - with_header_footer: bool, - /// 숨은 설명 텍스트도 추출에 포함 (기본: 제외) - #[arg(long = "with-hidden")] - with_hidden: bool, - /// (markdown 전용) markdown과 함께 각 출력 문자 범위의 원본 좌표(섹션/문단)를 - /// 한 줄 JSON 봉투로 출력 — {"markdown": ..., "segments": [...]} - #[arg(long = "with-segments")] - with_segments: bool, - }, - - /// 포맷 변환 (M2부터 단계적 구현) - Convert { - input: PathBuf, - #[arg(short, long)] - output: PathBuf, - /// 출력 포맷 (생략 시 확장자에서 추론) - #[arg(long, value_enum)] - to: Option, - /// 변환 중 보존 불가능한(opaque) 데이터 발견 시 실패 처리 - #[arg(long)] - strict: bool, - /// 줄 배치 캐시 보존 (무수정 왕복 전용 — 한글은 내용과 어긋난 - /// 줄 배치를 변조로 판정하므로 기본은 제거) - #[arg(long)] - preserve_layout: bool, - /// JSON 출력 시 첨부 바이너리(이미지)를 base64로 임베드 (자급식 JSON) - #[arg(long)] - embed_bin: bool, - /// (md) 이미지 추출 디렉터리 — 기본 "<출력스템>.media". 상대경로는 출력 - /// 파일 기준으로 해석하고 링크는 입력한 경로 그대로 쓴다 (예: figs) - #[arg(long)] - media_dir: Option, - /// (md) 머리말/꼬리말 텍스트도 포함 (기본: 제외) - #[arg(long = "with-header-footer")] - with_header_footer: bool, - /// (md) 숨은 설명 텍스트도 포함 (기본: 제외) - #[arg(long = "with-hidden")] - with_hidden: bool, - }, - - /// 페이지 렌더링 (M3에서 구현) - Render { - input: PathBuf, - #[arg(short, long)] - output: PathBuf, - /// 페이지 범위: "1", "1-3", "all" - #[arg(long, default_value = "all")] - pages: String, - #[arg(long, default_value_t = 96.0)] - dpi: f64, - /// 출력 포맷 (생략 시 확장자에서 추론) - #[arg(long, value_enum)] - format: Option, - /// 추가 폰트 디렉터리 (반복 가능) - #[arg(long)] - font_dir: Vec, - }, - - /// 새 문서 생성 (M4부터 구현) - New { - #[arg(short, long)] - output: PathBuf, - /// 입력 markdown/JSON 파일 (생략 시 빈 문서) - #[arg(long)] - from: Option, - /// 메타데이터 설정 "키=값" (키: title|author|subject|keywords, 반복 가능) - #[arg(long = "set-meta")] - set_meta: Vec, - }, - - /// 렌더 결과를 한글 기준 PNG와 비교해 오차 측정 (위치 오프셋·픽셀 차이율) - Diff { - input: PathBuf, - /// 한글에서 같은 페이지를 같은 DPI로 내보낸 기준 PNG - #[arg(long)] - r#ref: PathBuf, - /// 비교할 페이지 (1-기반) - #[arg(long, default_value_t = 1)] - page: usize, - #[arg(long, default_value_t = 96.0)] - dpi: f64, - /// 차이 이미지 출력 경로 (생략 시 .diff.png) - #[arg(short, long)] - out: Option, - /// 추가 폰트 디렉터리 (반복 가능) - #[arg(long)] - font_dir: Vec, - /// 채널 차이 허용 오차 (이하면 동일 취급) - #[arg(long, default_value_t = 16)] - tolerance: u8, - }, - - /// 기존 문서 편집 (텍스트 치환·표 셀 설정) — 이미지·서식 보존 - Edit { - input: PathBuf, - #[arg(short, long)] - output: PathBuf, - /// 텍스트 치환 "찾기=>바꾸기" (반복 가능, 모든 일치 치환) - #[arg(long)] - replace: Vec, - /// 표 셀 설정 "표:행:열=값" (반복 가능, 0-기반 인덱스) - #[arg(long = "set-cell")] - set_cell: Vec, - /// 필드/누름틀 채우기 "이름=값" (반복 가능 — hwp fields로 이름 확인) - #[arg(long = "set-field")] - set_field: Vec, - /// 메타데이터 설정 "키=값" (키: title|author|subject|keywords, 반복 가능) - #[arg(long = "set-meta")] - set_meta: Vec, - /// 누름틀 생성 "앵커=>이름" 또는 "앵커=>이름=값" — 앵커 텍스트 뒤에 %clk 필드 삽입 (반복 가능) - #[arg(long = "create-field")] - create_field: Vec, - /// 책갈피 생성 "앵커=>이름" — 앵커 텍스트 뒤에 bokm 지점 표식 삽입 (반복 가능) - #[arg(long = "create-bookmark")] - create_bookmark: Vec, - /// 하이퍼링크 생성 "앵커=>URL" 또는 "앵커=>표시=>URL" — 앵커 뒤에 %hlk 삽입 (반복 가능) - #[arg(long = "create-hyperlink")] - create_hyperlink: Vec, - /// 이미지 삽입 "앵커=>경로" 또는 "앵커=>경로@너비x높이"(mm) — 앵커 뒤에 그림 삽입 (반복 가능) - #[arg(long = "insert-image")] - insert_image: Vec, - /// 도장 날인 "앵커=>경로" 또는 "앵커=>경로@크기mm" — 앵커 문구 위에 도장 부유 배치 (반복 가능) - #[arg(long = "seal")] - seal: Vec, - /// 글자 서식 "찾기:속성=값,…" (예: "제목:bold=on,size=16,color=#FF0000") - #[arg(long = "set-format")] - set_format: Vec, - /// 문단 정렬 "찾기=정렬" (left/right/center/justify/distribute) - #[arg(long = "set-align")] - set_align: Vec, - /// 문단 삽입 "앵커=>텍스트" — 앵커가 있는 문단 뒤에 새 문단 (반복 가능) - #[arg(long = "insert-para")] - insert_para: Vec, - /// 문단 삽입(앞) "앵커=>텍스트" — 앵커가 있는 문단 앞에 새 문단 (반복 가능) - #[arg(long = "insert-para-before")] - insert_para_before: Vec, - /// 문단 삭제 "텍스트" — 텍스트가 있는 문단 삭제 (반복 가능) - #[arg(long = "delete-para")] - delete_para: Vec, - /// 표 행 추가 "표" — N번째 표 끝에 빈 행 (반복 가능, 0-기반; 병합 셀이 있는 표는 거부) - #[arg(long = "add-row")] - add_row: Vec, - /// 표 열 추가 "표"(끝에) 또는 "표:위치"(삽입) — 전체 폭 유지(기존 열 균등 축소). 병합 셀 표도 지원 (반복 가능, 0-기반) - #[arg(long = "add-col")] - add_col: Vec, - /// 표 행 삭제 "표:행" — N번째 표의 R행 (반복 가능, 0-기반; 병합 행은 거부) - #[arg(long = "delete-row")] - delete_row: Vec, - /// 표 열 삭제 "표:열" — N번째 표의 열 삭제. 전체 폭 유지(남은 열에 재분배). 병합 셀은 축소 (반복 가능, 0-기반) - #[arg(long = "delete-col")] - delete_col: Vec, - /// 셀 병합 "표:r1:c1:r2:c2" — 사각 영역을 좌상단 앵커로 병합 (반복 가능, 0-기반) - #[arg(long = "merge-cells")] - merge_cells: Vec, - /// 셀 분할 "표:행:열" — 병합 셀을 1×1로 분해 (반복 가능, 0-기반) - #[arg(long = "split-cell")] - split_cell: Vec, - /// 쓰기 후 재읽기로 검증 - #[arg(long)] - verify: bool, - }, - - /// 필드/누름틀 목록 표시 (이름·종류·값) - Fields { - file: PathBuf, - /// JSON으로 출력 - #[arg(long)] - json: bool, - }, - - /// 책갈피 목록 표시 (이름) - Bookmarks { - file: PathBuf, - /// JSON으로 출력 - #[arg(long)] - json: bool, - }, - - /// `{{name}}` 텍스트 자리표시자(템플릿 슬롯) 목록 표시 - Slots { - file: PathBuf, - /// JSON으로 출력 - #[arg(long)] - json: bool, - }, - - /// 충실도 보존 템플릿 채우기 (hwpx의 `{{name}}` 치환, 패키지 보존) - Fill { - input: PathBuf, - #[arg(short, long)] - output: PathBuf, - /// 자리표시자 채우기 "이름=값" (반복 가능; `{{이름}}` 치환) - #[arg(long)] - set: Vec, - /// 이름→값 JSON 객체 파일 (일괄 채우기) - #[arg(long)] - data: Option, - /// 치환 요약을 JSON으로 출력 ({output, replaced, counts}) - #[arg(long)] - json: bool, - }, - - /// 구조 검증 (mimetype/필수 엔트리/XML 파싱) — 유효하면 종료코드 0 - Validate { - file: PathBuf, - /// JSON으로 출력 - #[arg(long)] - json: bool, - }, - - /// MCP(Model Context Protocol) stdio 서버 — AI 에이전트용 도구 인터페이스 - Mcp { - /// 렌더/diff 도구의 기본 폰트 디렉터리 (반복 가능) - #[arg(long)] - font_dir: Vec, - }, - - /// [개발자용] 레코드/패키지 구조 덤프 - Dump { - file: PathBuf, - /// 대상 스트림/엔트리 (예: "DocInfo", "BodyText/Section0", "Contents/header.xml") - #[arg(long)] - stream: Option, - /// 레코드 페이로드를 hex로 출력 - #[arg(long)] - raw: bool, - /// JSON으로 출력 - #[arg(long)] - json: bool, - }, -} - -#[derive(Clone, Copy, ValueEnum)] -enum TextFormat { - Plain, - Markdown, - Json, - Html, -} - -#[derive(Clone, Copy, ValueEnum)] -enum ConvertFormat { - Hwp, - Hwpx, - Md, - Json, - Html, - Pdf, - Odt, -} - -#[derive(Clone, Copy, PartialEq, Eq, ValueEnum)] -enum RenderFormat { - Png, - Svg, - Pdf, -} +use hwp_cli::cli::{Cli, Cmd}; fn main() -> anyhow::Result<()> { let cli = Cli::parse(); diff --git a/crates/hwp-cli/tests/cli_reference.rs b/crates/hwp-cli/tests/cli_reference.rs new file mode 100644 index 00000000..7473ca5f --- /dev/null +++ b/crates/hwp-cli/tests/cli_reference.rs @@ -0,0 +1,269 @@ +//! CLI 명령 레퍼런스 자동 생성 + 드리프트 게이트. +//! +//! `clap::CommandFactory`로 `Cli`의 명령 트리를 introspect해 `docs/manual/cli-reference.md` +//! 를 결정적으로 생성한다. 커밋본과 재생성본이 어긋나면 실패한다 — CLI 정의(플래그·help +//! 텍스트)를 바꾸면 문서도 함께 갱신하도록 강제하는 장치. +//! +//! 재생성(bless): `HWP_UPDATE_DOCS=1 cargo test -p hwp-cli --test cli_reference`. + +use clap::builder::StyledStr; +use clap::{Arg, ArgAction, Command, CommandFactory}; +use hwp_cli::cli::Cli; + +/// 커밋된 문서 경로 (crate 기준 상대). +fn doc_path() -> std::path::PathBuf { + std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../docs/manual/cli-reference.md") +} + +const HEADER_COMMENT: &str = ""; + +/// StyledStr → 순수 텍스트(ANSI 없음), 앞뒤 공백 제거. +fn plain(s: &StyledStr) -> String { + s.to_string().trim().to_string() +} + +/// 표 셀 안전 이스케이프: 개행→공백(연속 공백 축약), `|`→`\|`. +fn cell(s: &str) -> String { + let joined = s.split_whitespace().collect::>().join(" "); + joined.replace('|', "\\|") +} + +/// GitHub 앵커: `hwp ` → `hwp-` (이름은 단일 토큰이라 단순 치환으로 충분). +fn anchor(name: &str) -> String { + format!("hwp-{name}") +} + +/// 값을 갖지 않는 액션(불리언 플래그 등)인지. +fn is_flag(action: &ArgAction) -> bool { + matches!( + action, + ArgAction::SetTrue + | ArgAction::SetFalse + | ArgAction::Count + | ArgAction::Help + | ArgAction::HelpShort + | ArgAction::HelpLong + | ArgAction::Version + ) +} + +/// 문서 생성에서 제외할 인자(clap 자동 추가 help/version, 숨김 인자). +fn skip_arg(arg: &Arg) -> bool { + arg.is_hide_set() + || matches!( + arg.get_action(), + ArgAction::Help | ArgAction::HelpShort | ArgAction::HelpLong | ArgAction::Version + ) + || matches!(arg.get_id().as_str(), "help" | "version") +} + +/// 인자의 값 이름 placeholder(예: ``). value_name 없으면 id를 대문자화. +fn value_placeholder(arg: &Arg) -> String { + let name = arg + .get_value_names() + .and_then(|ns| ns.first()) + .map(|s| s.to_string()) + .unwrap_or_else(|| arg.get_id().as_str().to_uppercase()); + format!("`<{name}>`") +} + +/// value_enum의 노출 가능한 값 목록(선언 순서). enum이 아니면 빈 Vec. +fn possible_values(arg: &Arg) -> Vec { + arg.get_possible_values() + .iter() + .filter(|pv| !pv.is_hide_set()) + .map(|pv| pv.get_name().to_string()) + .collect() +} + +/// `render_usage()`를 `hwp …` 형태로 정규화한다. +/// (서브커맨드를 부모에서 꺼내면 프로그램명이 없어 `Usage: …`로 나올 수 있다 — +/// 첫 `` 토큰 뒤 본문만 취해 `hwp <본문>`으로 재조립한다.) +fn usage_line(sub: &Command, name: &str) -> String { + let raw = sub.clone().render_usage().to_string(); + let after_label = raw + .trim() + .strip_prefix("Usage:") + .map(str::trim) + .unwrap_or_else(|| raw.trim()); + let body = match after_label.split_once(name) { + Some((_, rest)) => rest.trim_start(), + None => "", + }; + if body.is_empty() { + format!("hwp {name}") + } else { + format!("hwp {name} {body}") + } +} + +/// 한 서브커맨드의 인자/플래그 표 행들. 선언 순서 유지. +fn arg_rows(sub: &Command) -> Vec { + let mut rows = Vec::new(); + for arg in sub.get_arguments() { + if skip_arg(arg) { + continue; + } + // 1열: 인자/플래그 이름. + let name_col = if arg.is_positional() { + value_placeholder(arg) + } else { + match (arg.get_short(), arg.get_long()) { + (Some(s), Some(l)) => format!("`-{s}, --{l}`"), + (None, Some(l)) => format!("`--{l}`"), + (Some(s), None) => format!("`-{s}`"), + (None, None) => format!("`{}`", arg.get_id().as_str()), + } + }; + + // 2열: 값 (enum 값 목록 또는 placeholder; 불리언 플래그는 빈칸). + let value_col = if is_flag(arg.get_action()) { + String::new() + } else { + let pvs = possible_values(arg); + if !pvs.is_empty() { + pvs.iter() + .map(|v| format!("`{v}`")) + .collect::>() + .join(" \\| ") + } else if arg.is_positional() { + // placeholder는 이미 1열에 있으므로 중복 표기하지 않는다. + String::new() + } else { + value_placeholder(arg) + } + }; + + // 3열: 기본값. + let default_col = arg + .get_default_values() + .iter() + .map(|v| v.to_string_lossy().into_owned()) + .collect::>() + .join(", "); + let default_col = if default_col.is_empty() { + String::new() + } else { + format!("`{default_col}`") + }; + + // 4열: 설명 (help) + 반복 가능 표기. + // help가 이미 "반복 가능"을 담고 있으면(doc comment 관례) 중복을 피한다 — + // Append인데 표기가 없는 플래그에만 표준 마커를 덧붙여 일관성을 맞춘다. + let mut help = arg.get_help().map(plain).unwrap_or_default(); + if matches!(arg.get_action(), ArgAction::Append) && !help.contains("반복 가능") { + if help.is_empty() { + help = "(반복 가능)".to_string(); + } else { + help.push_str(" (반복 가능)"); + } + } + let help_col = cell(&help); + + rows.push(format!( + "| {name_col} | {value_col} | {default_col} | {help_col} |" + )); + } + rows +} + +/// clap 정의에서 마크다운 레퍼런스 전문을 생성한다. +fn generate() -> String { + let root = Cli::command(); + // 노출 대상 서브커맨드(숨김 제외), 선언 순서 유지. + let subs: Vec<&Command> = root + .get_subcommands() + .filter(|c| !c.is_hide_set()) + .collect(); + + let mut out = String::new(); + out.push_str(HEADER_COMMENT); + out.push_str("\n\n# hwp CLI 명령 레퍼런스\n\n"); + out.push_str( + "이 문서는 `hwp` CLI의 clap 정의에서 자동 생성된다. 직접 편집하지 말고, 명령·플래그가 \ + 바뀌면 `HWP_UPDATE_DOCS=1 cargo test -p hwp-cli --test cli_reference`로 재생성하라 — \ + CI 테스트가 코드와 문서의 동기화를 강제한다.\n\n", + ); + + // 명령 색인. + out.push_str("## 명령 색인\n\n"); + for sub in &subs { + let name = sub.get_name(); + out.push_str(&format!("- [`hwp {name}`](#{})\n", anchor(name))); + } + out.push('\n'); + + // 명령별 섹션. + for sub in &subs { + let name = sub.get_name(); + out.push_str(&format!("## `hwp {name}`\n\n")); + + // about / long_about (long_about 우선). + let about = sub + .get_long_about() + .or_else(|| sub.get_about()) + .map(plain) + .unwrap_or_default(); + if !about.is_empty() { + out.push_str(&about); + out.push_str("\n\n"); + } + + // 사용법. + out.push_str(&format!("**사용법:** `{}`\n\n", usage_line(sub, name))); + + // 인자/플래그 표. + let rows = arg_rows(sub); + if rows.is_empty() { + out.push_str("_인자·플래그 없음_\n\n"); + } else { + out.push_str("| 인자/플래그 | 값 | 기본값 | 설명 |\n"); + out.push_str("|---|---|---|---|\n"); + for r in rows { + out.push_str(&r); + out.push('\n'); + } + out.push('\n'); + } + } + + // 파일 끝 개행 1개로 정규화. + while out.ends_with('\n') { + out.pop(); + } + out.push('\n'); + out +} + +#[test] +fn cli_reference_up_to_date() { + let generated = generate(); + let path = doc_path(); + + // bless 모드: 파일을 새로 쓰고 통과. + if std::env::var_os("HWP_UPDATE_DOCS").is_some() { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).expect("docs/manual 디렉터리 생성"); + } + std::fs::write(&path, &generated).expect("cli-reference.md 쓰기"); + eprintln!("cli-reference.md 재생성 완료: {}", path.display()); + return; + } + + // 검증 모드: 커밋본과 비교(Windows CI 대비 CRLF 정규화). + let committed = std::fs::read_to_string(&path).unwrap_or_else(|e| { + panic!( + "cli-reference.md를 읽을 수 없음({e}) — \ + `HWP_UPDATE_DOCS=1 cargo test -p hwp-cli --test cli_reference`로 최초 생성하라: {}", + path.display() + ) + }); + let committed = committed.replace("\r\n", "\n"); + + assert_eq!( + committed, generated, + "\nCLI 정의가 문서와 어긋남 — \ + `HWP_UPDATE_DOCS=1 cargo test -p hwp-cli --test cli_reference`로 재생성한 뒤 \ + diff를 확인해 커밋하라." + ); +} diff --git a/docs/manual/cli-reference.md b/docs/manual/cli-reference.md new file mode 100644 index 00000000..f31ef29e --- /dev/null +++ b/docs/manual/cli-reference.md @@ -0,0 +1,222 @@ + + +# hwp CLI 명령 레퍼런스 + +이 문서는 `hwp` CLI의 clap 정의에서 자동 생성된다. 직접 편집하지 말고, 명령·플래그가 바뀌면 `HWP_UPDATE_DOCS=1 cargo test -p hwp-cli --test cli_reference`로 재생성하라 — CI 테스트가 코드와 문서의 동기화를 강제한다. + +## 명령 색인 + +- [`hwp info`](#hwp-info) +- [`hwp cat`](#hwp-cat) +- [`hwp convert`](#hwp-convert) +- [`hwp render`](#hwp-render) +- [`hwp new`](#hwp-new) +- [`hwp diff`](#hwp-diff) +- [`hwp edit`](#hwp-edit) +- [`hwp fields`](#hwp-fields) +- [`hwp bookmarks`](#hwp-bookmarks) +- [`hwp slots`](#hwp-slots) +- [`hwp fill`](#hwp-fill) +- [`hwp validate`](#hwp-validate) +- [`hwp mcp`](#hwp-mcp) +- [`hwp dump`](#hwp-dump) + +## `hwp info` + +파일 정보 표시: 포맷/버전/속성/스트림 목록 + +**사용법:** `hwp info [OPTIONS] ` + +| 인자/플래그 | 값 | 기본값 | 설명 | +|---|---|---|---| +| `` | | | | +| `--json` | | | JSON으로 출력 | + +## `hwp cat` + +텍스트 추출 + +**사용법:** `hwp cat [OPTIONS] ` + +| 인자/플래그 | 값 | 기본값 | 설명 | +|---|---|---|---| +| `` | | | | +| `--format` | `plain` \| `markdown` \| `json` \| `html` | `plain` | | +| `--preview` | | | 본문 파싱 없이 PrvText 미리보기만 출력 | +| `--with-header-footer` | | | 머리말/꼬리말 텍스트도 추출에 포함 (기본: 제외) | +| `--with-hidden` | | | 숨은 설명 텍스트도 추출에 포함 (기본: 제외) | +| `--with-segments` | | | (markdown 전용) markdown과 함께 각 출력 문자 범위의 원본 좌표(섹션/문단)를 한 줄 JSON 봉투로 출력 — {"markdown": ..., "segments": [...]} | + +## `hwp convert` + +포맷 변환 + +**사용법:** `hwp convert [OPTIONS] --output ` + +| 인자/플래그 | 값 | 기본값 | 설명 | +|---|---|---|---| +| `` | | | | +| `-o, --output` | `` | | | +| `--to` | `hwp` \| `hwpx` \| `md` \| `json` \| `html` \| `pdf` \| `odt` | | 출력 포맷 (생략 시 확장자에서 추론) | +| `--strict` | | | 변환 중 보존 불가능한(opaque) 데이터 발견 시 실패 처리 | +| `--preserve-layout` | | | 줄 배치 캐시 보존 (무수정 왕복 전용 — 한글은 내용과 어긋난 줄 배치를 변조로 판정하므로 기본은 제거) | +| `--embed-bin` | | | JSON 출력 시 첨부 바이너리(이미지)를 base64로 임베드 (자급식 JSON) | +| `--media-dir` | `` | | (md) 이미지 추출 디렉터리 — 기본 "<출력스템>.media". 상대경로는 출력 파일 기준으로 해석하고 링크는 입력한 경로 그대로 쓴다 (예: figs) | +| `--with-header-footer` | | | (md) 머리말/꼬리말 텍스트도 포함 (기본: 제외) | +| `--with-hidden` | | | (md) 숨은 설명 텍스트도 포함 (기본: 제외) | + +## `hwp render` + +페이지 렌더링 + +**사용법:** `hwp render [OPTIONS] --output ` + +| 인자/플래그 | 값 | 기본값 | 설명 | +|---|---|---|---| +| `` | | | | +| `-o, --output` | `` | | | +| `--pages` | `` | `all` | 페이지 범위: "1", "1-3", "all" | +| `--dpi` | `` | `96` | | +| `--format` | `png` \| `svg` \| `pdf` | | 출력 포맷 (생략 시 확장자에서 추론) | +| `--font-dir` | `` | | 추가 폰트 디렉터리 (반복 가능) | + +## `hwp new` + +새 문서 생성 + +**사용법:** `hwp new [OPTIONS] --output ` + +| 인자/플래그 | 값 | 기본값 | 설명 | +|---|---|---|---| +| `-o, --output` | `` | | | +| `--from` | `` | | 입력 markdown/JSON 파일 (생략 시 빈 문서) | +| `--set-meta` | `` | | 메타데이터 설정 "키=값" (키: title\|author\|subject\|keywords, 반복 가능) | + +## `hwp diff` + +렌더 결과를 한글 기준 PNG와 비교해 오차 측정 (위치 오프셋·픽셀 차이율) + +**사용법:** `hwp diff [OPTIONS] --ref ` + +| 인자/플래그 | 값 | 기본값 | 설명 | +|---|---|---|---| +| `` | | | | +| `--ref` | `` | | 한글에서 같은 페이지를 같은 DPI로 내보낸 기준 PNG | +| `--page` | `` | `1` | 비교할 페이지 (1-기반) | +| `--dpi` | `` | `96` | | +| `-o, --out` | `` | | 차이 이미지 출력 경로 (생략 시 .diff.png) | +| `--font-dir` | `` | | 추가 폰트 디렉터리 (반복 가능) | +| `--tolerance` | `` | `16` | 채널 차이 허용 오차 (이하면 동일 취급) | + +## `hwp edit` + +기존 문서 편집 (텍스트 치환·표 셀 설정) — 이미지·서식 보존 + +**사용법:** `hwp edit [OPTIONS] --output ` + +| 인자/플래그 | 값 | 기본값 | 설명 | +|---|---|---|---| +| `` | | | | +| `-o, --output` | `` | | | +| `--replace` | `` | | 텍스트 치환 "찾기=>바꾸기" (반복 가능, 모든 일치 치환) | +| `--set-cell` | `` | | 표 셀 설정 "표:행:열=값" (반복 가능, 0-기반 인덱스) | +| `--set-field` | `` | | 필드/누름틀 채우기 "이름=값" (반복 가능 — hwp fields로 이름 확인) | +| `--set-meta` | `` | | 메타데이터 설정 "키=값" (키: title\|author\|subject\|keywords, 반복 가능) | +| `--create-field` | `` | | 누름틀 생성 "앵커=>이름" 또는 "앵커=>이름=값" — 앵커 텍스트 뒤에 %clk 필드 삽입 (반복 가능) | +| `--create-bookmark` | `` | | 책갈피 생성 "앵커=>이름" — 앵커 텍스트 뒤에 bokm 지점 표식 삽입 (반복 가능) | +| `--create-hyperlink` | `` | | 하이퍼링크 생성 "앵커=>URL" 또는 "앵커=>표시=>URL" — 앵커 뒤에 %hlk 삽입 (반복 가능) | +| `--insert-image` | `` | | 이미지 삽입 "앵커=>경로" 또는 "앵커=>경로@너비x높이"(mm) — 앵커 뒤에 그림 삽입 (반복 가능) | +| `--seal` | `` | | 도장 날인 "앵커=>경로" 또는 "앵커=>경로@크기mm" — 앵커 문구 위에 도장 부유 배치 (반복 가능) | +| `--set-format` | `` | | 글자 서식 "찾기:속성=값,…" (예: "제목:bold=on,size=16,color=#FF0000") (반복 가능) | +| `--set-align` | `` | | 문단 정렬 "찾기=정렬" (left/right/center/justify/distribute) (반복 가능) | +| `--insert-para` | `` | | 문단 삽입 "앵커=>텍스트" — 앵커가 있는 문단 뒤에 새 문단 (반복 가능) | +| `--insert-para-before` | `` | | 문단 삽입(앞) "앵커=>텍스트" — 앵커가 있는 문단 앞에 새 문단 (반복 가능) | +| `--delete-para` | `` | | 문단 삭제 "텍스트" — 텍스트가 있는 문단 삭제 (반복 가능) | +| `--add-row` | `` | | 표 행 추가 "표" — N번째 표 끝에 빈 행 (반복 가능, 0-기반; 병합 셀이 있는 표는 거부) | +| `--add-col` | `` | | 표 열 추가 "표"(끝에) 또는 "표:위치"(삽입) — 전체 폭 유지(기존 열 균등 축소). 병합 셀 표도 지원 (반복 가능, 0-기반) | +| `--delete-row` | `` | | 표 행 삭제 "표:행" — N번째 표의 R행 (반복 가능, 0-기반; 병합 행은 거부) | +| `--delete-col` | `` | | 표 열 삭제 "표:열" — N번째 표의 열 삭제. 전체 폭 유지(남은 열에 재분배). 병합 셀은 축소 (반복 가능, 0-기반) | +| `--merge-cells` | `` | | 셀 병합 "표:r1:c1:r2:c2" — 사각 영역을 좌상단 앵커로 병합 (반복 가능, 0-기반) | +| `--split-cell` | `` | | 셀 분할 "표:행:열" — 병합 셀을 1×1로 분해 (반복 가능, 0-기반) | +| `--verify` | | | 쓰기 후 재읽기로 검증 | + +## `hwp fields` + +필드/누름틀 목록 표시 (이름·종류·값) + +**사용법:** `hwp fields [OPTIONS] ` + +| 인자/플래그 | 값 | 기본값 | 설명 | +|---|---|---|---| +| `` | | | | +| `--json` | | | JSON으로 출력 | + +## `hwp bookmarks` + +책갈피 목록 표시 (이름) + +**사용법:** `hwp bookmarks [OPTIONS] ` + +| 인자/플래그 | 값 | 기본값 | 설명 | +|---|---|---|---| +| `` | | | | +| `--json` | | | JSON으로 출력 | + +## `hwp slots` + +`{{name}}` 텍스트 자리표시자(템플릿 슬롯) 목록 표시 + +**사용법:** `hwp slots [OPTIONS] ` + +| 인자/플래그 | 값 | 기본값 | 설명 | +|---|---|---|---| +| `` | | | | +| `--json` | | | JSON으로 출력 | + +## `hwp fill` + +충실도 보존 템플릿 채우기 (hwpx의 `{{name}}` 치환, 패키지 보존) + +**사용법:** `hwp fill [OPTIONS] --output ` + +| 인자/플래그 | 값 | 기본값 | 설명 | +|---|---|---|---| +| `` | | | | +| `-o, --output` | `` | | | +| `--set` | `` | | 자리표시자 채우기 "이름=값" (반복 가능; `{{이름}}` 치환) | +| `--data` | `` | | 이름→값 JSON 객체 파일 (일괄 채우기) | +| `--json` | | | 치환 요약을 JSON으로 출력 ({output, replaced, counts}) | + +## `hwp validate` + +구조 검증 (mimetype/필수 엔트리/XML 파싱) — 유효하면 종료코드 0 + +**사용법:** `hwp validate [OPTIONS] ` + +| 인자/플래그 | 값 | 기본값 | 설명 | +|---|---|---|---| +| `` | | | | +| `--json` | | | JSON으로 출력 | + +## `hwp mcp` + +MCP(Model Context Protocol) stdio 서버 — AI 에이전트용 도구 인터페이스 + +**사용법:** `hwp mcp [OPTIONS]` + +| 인자/플래그 | 값 | 기본값 | 설명 | +|---|---|---|---| +| `--font-dir` | `` | | 렌더/diff 도구의 기본 폰트 디렉터리 (반복 가능) | + +## `hwp dump` + +[개발자용] 레코드/패키지 구조 덤프 + +**사용법:** `hwp dump [OPTIONS] ` + +| 인자/플래그 | 값 | 기본값 | 설명 | +|---|---|---|---| +| `` | | | | +| `--stream` | `` | | 대상 스트림/엔트리 (예: "DocInfo", "BodyText/Section0", "Contents/header.xml") | +| `--raw` | | | 레코드 페이로드를 hex로 출력 | +| `--json` | | | JSON으로 출력 |