Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@ hwp mcp --font-dir ./fonts

## 명령 레퍼런스

전체 자동 생성 레퍼런스는 [docs/manual/cli-reference.md](docs/manual/cli-reference.md) — clap 정의에서 생성되며 코드와의 동기화를 CI 테스트가 강제한다. 아래는 요약이다.

| 명령 | 인자 / 플래그 | 설명 |
|---|---|---|
| `info <file>` | `--json` | 포맷/버전/속성/스트림 진단 |
Expand All @@ -173,6 +175,10 @@ hwp mcp --font-dir ./fonts
| `new -o <output>` | `--from <md\|json>`(생략 시 빈 문서) | markdown/JSON IR에서 새 문서 생성. markdown 목록은 진짜 번호(NUMBER)/글머리(BULLET) 머리 문단으로 들여오고(중첩=수준), H1~H3 제목엔 절 번호(`1.`/`1-1.`/`1-1-1.`, 숫자 시작 제목은 생략)를 접두한다 |
| `edit <input> -o <output>` | `--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 <file>` | `--json` | 필드/누름틀 목록(이름·종류·값·명령) |
| `bookmarks <file>` | `--json` | 책갈피(bokm) 목록(이름) |
| `slots <file>` | `--json` | `{{name}}` 텍스트 자리표시자(템플릿 슬롯) 목록 |
| `fill <input> -o <output>` | `--set "이름=값"`(반복), `--data <json>`, `--json` | 충실도 보존 템플릿 채우기 — hwpx의 `{{name}}` 치환(패키지 보존). `--data`는 이름→값 JSON 객체 파일로 일괄 채움 |
| `validate <file>` | `--json` | 구조 검증(mimetype·필수 엔트리·XML 파싱) — 유효 시 종료코드 0 |
| `diff <input> --ref <png>` | `--page <n>`(기본 1), `--dpi <f64>`(기본 96), `-o/--out <png>`, `--font-dir <dir>`(반복), `--tolerance <u8>`(기본 16) | 렌더 결과를 한글 기준 PNG와 비교(잉크 적용률·dx/dy 오프셋·픽셀 차이율·MAE) |
| `mcp` | `--font-dir <dir>`(반복) | MCP stdio 서버 실행 |
| `dump <file>` | `--stream <name>`, `--raw`, `--json` | [개발자용] 레코드/패키지 구조 덤프 |
Expand Down
297 changes: 297 additions & 0 deletions crates/hwp-cli/src/cli.rs
Original file line number Diff line number Diff line change
@@ -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<String> 다수)로 커서 다른 변형과 크기차가 크다.
// 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<ConvertFormat>,
/// 변환 중 보존 불가능한(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<PathBuf>,
/// (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<RenderFormat>,
/// 추가 폰트 디렉터리 (반복 가능)
#[arg(long)]
font_dir: Vec<PathBuf>,
},

/// 새 문서 생성
New {
#[arg(short, long)]
output: PathBuf,
/// 입력 markdown/JSON 파일 (생략 시 빈 문서)
#[arg(long)]
from: Option<PathBuf>,
/// 메타데이터 설정 "키=값" (키: title|author|subject|keywords, 반복 가능)
#[arg(long = "set-meta")]
set_meta: Vec<String>,
},

/// 렌더 결과를 한글 기준 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,
/// 차이 이미지 출력 경로 (생략 시 <ref>.diff.png)
#[arg(short, long)]
out: Option<PathBuf>,
/// 추가 폰트 디렉터리 (반복 가능)
#[arg(long)]
font_dir: Vec<PathBuf>,
/// 채널 차이 허용 오차 (이하면 동일 취급)
#[arg(long, default_value_t = 16)]
tolerance: u8,
},

/// 기존 문서 편집 (텍스트 치환·표 셀 설정) — 이미지·서식 보존
Edit {
input: PathBuf,
#[arg(short, long)]
output: PathBuf,
/// 텍스트 치환 "찾기=>바꾸기" (반복 가능, 모든 일치 치환)
#[arg(long)]
replace: Vec<String>,
/// 표 셀 설정 "표:행:열=값" (반복 가능, 0-기반 인덱스)
#[arg(long = "set-cell")]
set_cell: Vec<String>,
/// 필드/누름틀 채우기 "이름=값" (반복 가능 — hwp fields로 이름 확인)
#[arg(long = "set-field")]
set_field: Vec<String>,
/// 메타데이터 설정 "키=값" (키: title|author|subject|keywords, 반복 가능)
#[arg(long = "set-meta")]
set_meta: Vec<String>,
/// 누름틀 생성 "앵커=>이름" 또는 "앵커=>이름=값" — 앵커 텍스트 뒤에 %clk 필드 삽입 (반복 가능)
#[arg(long = "create-field")]
create_field: Vec<String>,
/// 책갈피 생성 "앵커=>이름" — 앵커 텍스트 뒤에 bokm 지점 표식 삽입 (반복 가능)
#[arg(long = "create-bookmark")]
create_bookmark: Vec<String>,
/// 하이퍼링크 생성 "앵커=>URL" 또는 "앵커=>표시=>URL" — 앵커 뒤에 %hlk 삽입 (반복 가능)
#[arg(long = "create-hyperlink")]
create_hyperlink: Vec<String>,
/// 이미지 삽입 "앵커=>경로" 또는 "앵커=>경로@너비x높이"(mm) — 앵커 뒤에 그림 삽입 (반복 가능)
#[arg(long = "insert-image")]
insert_image: Vec<String>,
/// 도장 날인 "앵커=>경로" 또는 "앵커=>경로@크기mm" — 앵커 문구 위에 도장 부유 배치 (반복 가능)
#[arg(long = "seal")]
seal: Vec<String>,
/// 글자 서식 "찾기:속성=값,…" (예: "제목:bold=on,size=16,color=#FF0000")
#[arg(long = "set-format")]
set_format: Vec<String>,
/// 문단 정렬 "찾기=정렬" (left/right/center/justify/distribute)
#[arg(long = "set-align")]
set_align: Vec<String>,
/// 문단 삽입 "앵커=>텍스트" — 앵커가 있는 문단 뒤에 새 문단 (반복 가능)
#[arg(long = "insert-para")]
insert_para: Vec<String>,
/// 문단 삽입(앞) "앵커=>텍스트" — 앵커가 있는 문단 앞에 새 문단 (반복 가능)
#[arg(long = "insert-para-before")]
insert_para_before: Vec<String>,
/// 문단 삭제 "텍스트" — 텍스트가 있는 문단 삭제 (반복 가능)
#[arg(long = "delete-para")]
delete_para: Vec<String>,
/// 표 행 추가 "표" — N번째 표 끝에 빈 행 (반복 가능, 0-기반; 병합 셀이 있는 표는 거부)
#[arg(long = "add-row")]
add_row: Vec<String>,
/// 표 열 추가 "표"(끝에) 또는 "표:위치"(삽입) — 전체 폭 유지(기존 열 균등 축소). 병합 셀 표도 지원 (반복 가능, 0-기반)
#[arg(long = "add-col")]
add_col: Vec<String>,
/// 표 행 삭제 "표:행" — N번째 표의 R행 (반복 가능, 0-기반; 병합 행은 거부)
#[arg(long = "delete-row")]
delete_row: Vec<String>,
/// 표 열 삭제 "표:열" — N번째 표의 열 삭제. 전체 폭 유지(남은 열에 재분배). 병합 셀은 축소 (반복 가능, 0-기반)
#[arg(long = "delete-col")]
delete_col: Vec<String>,
/// 셀 병합 "표:r1:c1:r2:c2" — 사각 영역을 좌상단 앵커로 병합 (반복 가능, 0-기반)
#[arg(long = "merge-cells")]
merge_cells: Vec<String>,
/// 셀 분할 "표:행:열" — 병합 셀을 1×1로 분해 (반복 가능, 0-기반)
#[arg(long = "split-cell")]
split_cell: Vec<String>,
/// 쓰기 후 재읽기로 검증
#[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<String>,
/// 이름→값 JSON 객체 파일 (일괄 채우기)
#[arg(long)]
data: Option<PathBuf>,
/// 치환 요약을 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<PathBuf>,
},

/// [개발자용] 레코드/패키지 구조 덤프
Dump {
file: PathBuf,
/// 대상 스트림/엔트리 (예: "DocInfo", "BodyText/Section0", "Contents/header.xml")
#[arg(long)]
stream: Option<String>,
/// 레코드 페이로드를 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,
}
2 changes: 1 addition & 1 deletion crates/hwp-cli/src/commands/cat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 공용).
///
Expand Down
4 changes: 2 additions & 2 deletions crates/hwp-cli/src/commands/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -41,7 +41,7 @@ pub fn run(
output,
"all",
96.0,
Some(crate::RenderFormat::Pdf),
Some(hwp_cli::cli::RenderFormat::Pdf),
Vec::new(),
);
}
Expand Down
2 changes: 1 addition & 1 deletion crates/hwp-cli/src/commands/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions crates/hwp-cli/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//! hwp-cli 라이브러리 표면 — CLI 정의만 노출한다.
//!
//! 명령/플래그 선언(`cli`)을 lib으로 올려 두면 `tests/cli_reference.rs`가
//! `clap::CommandFactory`로 명령 트리를 introspect해 문서를 자동 생성할 수 있다.
//! 실제 디스패치·명령 구현(`commands`, `format`)은 bin 전용이라 여기서 노출하지 않는다.

pub mod cli;
Loading
Loading