hwpforge-foundation 0.1.0

Foundation types for HwpForge (HwpUnit, Color, Interned IDs, Branded Index)
Documentation

HwpForge ๐Ÿ”ฅ

Rust๋กœ ํ•œ๊ธ€(HWP/HWPX) ๋ฌธ์„œ๋ฅผ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋ฐฉ์‹์œผ๋กœ ์ œ์–ด

Hancom ํ•œ๊ธ€ ํŒŒ์ผ ์ฝ๊ธฐ, ์“ฐ๊ธฐ, ๋ณ€ํ™˜

CI crates.io docs.rs License: MIT OR Apache-2.0 MSRV codecov Lines of Code Tests unsafe forbidden


HwpForge๋ž€?

HwpForge๋Š” HWPX ๋ฌธ์„œ(ZIP + XML, KS X 6101)๋ฅผ ๋‹ค๋ฃจ๊ธฐ ์œ„ํ•œ ์˜คํ”ˆ์†Œ์Šค ์ˆœ์ˆ˜ Rust ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ž…๋‹ˆ๋‹ค. ํ•œ๊ตญ์—์„œ ๊ฐ€์žฅ ๋งŽ์ด ์‚ฌ์šฉ๋˜๋Š” ์›Œ๋“œํ”„๋กœ์„ธ์„œ์ธ Hancom ํ•œ๊ธ€์˜ ์ตœ์‹  ํฌ๋งท์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.

LLM-first ์„ค๊ณ„ ๐Ÿ”ฅ โ€” AI ์นœํ™”์ ์ธ Markdown๊ณผ ๊ณต์‹ ํ•œ๊ธ€ ๋ฌธ์„œ ํฌ๋งท(HWPX), ๋‘ ์„ธ๊ณ„๋ฅผ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์ž‡์Šต๋‹ˆ๋‹ค. LLM์ด Markdown์œผ๋กœ ์ž‘์„ฑํ•œ ๋‚ด์šฉ์€ ๊ณต๋ฌธ์„œ ๊ทœ๊ฒฉ์˜ HWPX๋กœ ์ปดํŒŒ์ผ๋˜๊ณ  ๐Ÿ“œ, ๋ฐ˜๋Œ€๋กœ ๊ธฐ์กด HWPX ๋ฌธ์„œ๋Š” AI๊ฐ€ ์‰ฝ๊ฒŒ ์ฝ์„ ์ˆ˜ ์žˆ๋Š” ๊ตฌ์กฐ๋กœ ๊บผ๋‚ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค โš’๏ธ.

  • Full HWPX codec โ€” HWPX ํŒŒ์ผ์„ ์†์‹ค ์—†์ด ๋””์ฝ”๋”ฉ/์ธ์ฝ”๋”ฉ (lossless roundtrip)
  • Markdown bridge โ€” GFM Markdown๊ณผ HWPX ๊ฐ„ ์ƒํ˜ธ ๋ณ€ํ™˜
  • YAML style template โ€” Figma Design Token์ฒ˜๋Ÿผ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์Šคํƒ€์ผ ์ •์˜ (ํฐํŠธ, ํฌ๊ธฐ, ์ƒ‰์ƒ)
  • Type-safe API โ€” branded index, typestate validation, zero unsafe code

๋น ๋ฅธ ์‹œ์ž‘

Cargo.toml์— ์ถ”๊ฐ€:

[dependencies]
hwpforge = "0.1"

๐Ÿ”จ ๋ฌธ์„œ ์ƒ์„ฑ

use hwpforge::core::{Document, Draft, Paragraph, Run, Section, PageSettings};
use hwpforge::foundation::{CharShapeIndex, ParaShapeIndex};

let mut doc = Document::<Draft>::new();
doc.add_section(Section::with_paragraphs(
    vec![Paragraph::with_runs(
        vec![Run::text("Hello, ํ•œ๊ธ€!", CharShapeIndex::new(0))],
        ParaShapeIndex::new(0),
    )],
    PageSettings::a4(),
));

โš’๏ธ HWPX๋กœ ์ธ์ฝ”๋”ฉ

use hwpforge::hwpx::{HwpxEncoder, HwpxStyleStore};
use hwpforge::core::ImageStore;

let validated = doc.validate().unwrap();
let style_store = HwpxStyleStore::with_default_fonts("ํ•จ์ดˆ๋กฌ๋ฐ”ํƒ•");
let image_store = ImageStore::new();
let bytes = HwpxEncoder::encode(&validated, &style_store, &image_store).unwrap();
std::fs::write("output.hwpx", &bytes).unwrap();

โš’๏ธ HWPX ๋””์ฝ”๋”ฉ

use hwpforge::hwpx::HwpxDecoder;

let result = HwpxDecoder::decode_file("input.hwpx").unwrap();
println!("์„น์…˜ ์ˆ˜: {}", result.document.sections().len());

โš’๏ธ Markdown โ†’ HWPX ๋ณ€ํ™˜

use hwpforge::md::MdDecoder;
use hwpforge::hwpx::{HwpxEncoder, HwpxStyleStore};

let md_doc = MdDecoder::decode_with_default("# ์ œ๋ชฉ\n\nMarkdown์—์„œ ๋ณ€ํ™˜!").unwrap();
let validated = md_doc.document.validate().unwrap();
let style_store = HwpxStyleStore::from_registry(&md_doc.style_registry);
let image_store = hwpforge::core::ImageStore::new();
let bytes = HwpxEncoder::encode(&validated, &style_store, &image_store).unwrap();

Feature Flags

Feature ๊ธฐ๋ณธ๊ฐ’ ์„ค๋ช…
hwpx Yes HWPX encoder/decoder
md โ€” Markdown โ†” Core ๋ณ€ํ™˜
full โ€” ๋ชจ๋“  ๊ธฐ๋Šฅ ํฌํ•จ
# Markdown ์ง€์› ํฌํ•จ
hwpforge = { version = "0.1", features = ["full"] }

๐Ÿ“œ ์ง€์› ์ฝ˜ํ…์ธ 

์นดํ…Œ๊ณ ๋ฆฌ ์š”์†Œ
ํ…์ŠคํŠธ Run, character shape, paragraph shape, style (22๊ฐœ ํ•œ์ปด ๊ธฐ๋ณธ ์Šคํƒ€์ผ)
๊ตฌ์กฐ Table (์ค‘์ฒฉ), Image (๋ฐ”์ด๋„ˆ๋ฆฌ + ๊ฒฝ๋กœ), TextBox, Caption
๋ ˆ์ด์•„์›ƒ ๋‹ค๋‹จ, ํŽ˜์ด์ง€ ์„ค์ •, ๊ฐ€๋กœ/์„ธ๋กœ ๋ฐฉํ–ฅ, ์ œ๋ณธ ์—ฌ๋ฐฑ, master page
๋จธ๋ฆฌ๊ธ€/๋ฐ”๋‹ฅ๊ธ€ Header, Footer, ์ชฝ๋ฒˆํ˜ธ (autoNum)
๊ฐ์ฃผ/๋ฏธ์ฃผ ๊ฐ์ฃผ, ๋ฏธ์ฃผ
๋„ํ˜• ์„ , ํƒ€์›, ๋‹ค๊ฐํ˜•, ํ˜ธ, ๊ณก์„ , ์—ฐ๊ฒฐ์„  (์ฑ„์›€, ํšŒ์ „, ํ™”์‚ดํ‘œ ์ง€์›)
์ˆ˜์‹ HancomEQN script ํ˜•์‹
์ฐจํŠธ 18์ข… chart type (OOXML ํ˜ธํ™˜)
์ฐธ์กฐ ์ฑ…๊ฐˆํ”ผ, ์ƒํ˜ธ ์ฐธ์กฐ, ํ•„๋“œ (๋‚ ์งœ/์‹œ๊ฐ„/์š”์•ฝ), ๋ฉ”๋ชจ, ์ƒ‰์ธ
๋ง๋ง/๊ฒน์นจ ๋ง๋ง (dutmal), ๊ธ€์ž ๊ฒน์นจ
Markdown GFM decode, lossy + lossless encode, YAML frontmatter

์•„ํ‚คํ…์ฒ˜

%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '14px', 'lineColor': '#BDBDBD'}}}%%
flowchart TD
    subgraph formats["ํฌ๋งท"]
        HF(["๐Ÿ“„ .hwpx<br/>ํ•œ๊ธ€ ํŒŒ์ผ"]):::file
        MF(["๐Ÿ“ .md<br/>Markdown"]):::file
    end

    subgraph smithy["โš’๏ธ Smithy โ€” ํฌ๋งท ๋ณ€ํ™˜๊ธฐ"]
        SH["hwpforge-smithy-hwpx"]:::smithy
        SM["hwpforge-smithy-md"]:::smithy
    end

    C["๐Ÿ”จ hwpforge-core<br/>ํฌ๋งท ๋…๋ฆฝ ๋ฌธ์„œ ๋ชจ๋ธ (IR)"]:::core
    BP["๐Ÿ“ hwpforge-blueprint<br/>YAML ์Šคํƒ€์ผ ยท ํฐํŠธ ยท ์ƒ‰์ƒ"]:::blueprint
    F["๐Ÿ”ฉ hwpforge-foundation<br/>HwpUnit ยท Color ยท Index"]:::foundation

    HF <--> SH
    MF <--> SM
    SH & SM <--> C
    BP --> SH & SM
    F --> C & BP

    classDef file     fill:#FFFDE7,stroke:#F9A825,color:#5D4037
    classDef smithy   fill:#FFF3E0,stroke:#FB8C00,color:#E65100
    classDef core     fill:#E3F2FD,stroke:#42A5F5,color:#0D47A1
    classDef blueprint fill:#F3E5F5,stroke:#AB47BC,color:#4A148C
    classDef foundation fill:#FAFAFA,stroke:#BDBDBD,color:#424242

ํ•ต์‹ฌ ์›์น™: ๊ตฌ์กฐ(Structure)์™€ ์Šคํƒ€์ผ(Style)์˜ ๋ถ„๋ฆฌ โ€” HTML + CSS์™€ ๊ฐ™์€ ํŒจํ„ด์ž…๋‹ˆ๋‹ค. Core๋Š” ์Šคํƒ€์ผ ์ฐธ์กฐ(index)๋งŒ ๊ฐ€์ง€๊ณ , Blueprint๋Š” ์Šคํƒ€์ผ ์ •์˜(ํฐํŠธ, ํฌ๊ธฐ, ์ƒ‰์ƒ)๋ฅผ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. Smithy compiler๊ฐ€ Core + Blueprint๋ฅผ ํ•ฉ์ณ ์ตœ์ข… ํฌ๋งท์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

ํ”„๋กœ์ ํŠธ ํ˜„ํ™ฉ

์ง€ํ‘œ ๊ฐ’
์ด LOC ~49,200
ํ…Œ์ŠคํŠธ 1,510๊ฐœ (cargo-nextest)
์†Œ์Šค ํŒŒ์ผ 92 .rs
Crate ์ˆ˜ 9๊ฐœ (6๊ฐœ ๋ฐฐํฌ)
์ปค๋ฒ„๋ฆฌ์ง€ 92.65%
Clippy ๊ฒฝ๊ณ  0
Unsafe ์ฝ”๋“œ 0

๊ฐœ๋ฐœ

ํ•„์ˆ˜ ์š”๊ตฌ์‚ฌํ•ญ

  • Rust 1.88+ (MSRV)
  • (๊ถŒ์žฅ) cargo-nextest โ€” ๋ณ‘๋ ฌ ํ…Œ์ŠคํŠธ ์‹คํ–‰
  • (์„ ํƒ) pre-commit โ€” git hook ์ž๋™ํ™”

โš’๏ธ ๋ช…๋ น์–ด

make ci          # fmt + clippy + test + deny + lint (CI์™€ ๋™์ผ)
make test        # cargo nextest run
make clippy      # cargo clippy (๋ชจ๋“  target, ๋ชจ๋“  feature, -D warnings)
make fmt-fix     # rustfmt ์ž๋™ ํฌ๋งท
make doc         # rustdoc ์ƒ์„ฑ (๋ธŒ๋ผ์šฐ์ €์—์„œ ์—ด๋ฆผ)
make cov         # coverage ๋ฆฌํฌํŠธ (90% gate)

ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ

HwpForge/
โ”œโ”€โ”€ crates/
โ”‚   โ”œโ”€โ”€ hwpforge/                 # Umbrella crate (re-exports)
โ”‚   โ”œโ”€โ”€ hwpforge-foundation/      # ๊ธฐ๋ณธ ํƒ€์ž… (HwpUnit, Color, Index<T>)
โ”‚   โ”œโ”€โ”€ hwpforge-core/            # ๋ฌธ์„œ ๋ชจ๋ธ (์Šคํƒ€์ผ ์ฐธ์กฐ๋งŒ)
โ”‚   โ”œโ”€โ”€ hwpforge-blueprint/       # YAML ํ…œํ”Œ๋ฆฟ (Figma ํŒจํ„ด)
โ”‚   โ”œโ”€โ”€ hwpforge-smithy-hwpx/     # HWPX codec (ZIP+XML โ†” Core)
โ”‚   โ”œโ”€โ”€ hwpforge-smithy-md/       # Markdown codec (MD โ†” Core)
โ”‚   โ”œโ”€โ”€ hwpforge-smithy-hwp5/     # HWP5 decoder (์˜ˆ์ •)
โ”‚   โ”œโ”€โ”€ hwpforge-bindings-py/     # Python bindings (์˜ˆ์ •)
โ”‚   โ””โ”€โ”€ hwpforge-bindings-cli/    # CLI ๋„๊ตฌ (์˜ˆ์ •)
โ”œโ”€โ”€ tests/                        # ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ + golden fixture
โ””โ”€โ”€ examples/                     # ๐Ÿ“œ ์‚ฌ์šฉ ์˜ˆ์ œ + ์ƒ์„ฑ๋œ HWPX ํŒŒ์ผ

๋กœ๋“œ๋งต

์ถœ์‹œ ์˜ˆ์ •

  • HWP5 ์ฝ๊ธฐ โ€” ๊ตฌํ˜• ๋ฐ”์ด๋„ˆ๋ฆฌ ํฌ๋งท(.hwp) ๋””์ฝ”๋”

  • MCP ์„œ๋ฒ„ โ€” Claude, GPT ๋“ฑ LLM์ด tool๋กœ ์ง์ ‘ HWPX ์ƒ์„ฑ

  • CLI ๋„๊ตฌ โ€” hwpforge convert doc.md doc.hwpx ํ•œ ์ค„ ๋ณ€ํ™˜

  • HWPX ์™„์ „ ์ง€์› โ€” ์–‘์‹ ์ปจํŠธ๋กค, ๋ณ€๊ฒฝ ์ถ”์ , OLE ๊ฐ์ฒด

  • Python ๋ฐ”์ธ๋”ฉ โ€” pip install hwpforge๋กœ ์„ค์น˜, PyPI ๋ฐฐํฌ

๋ผ์ด์„ ์Šค

๋‹ค์Œ ์ค‘ ํ•˜๋‚˜๋ฅผ ์„ ํƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

Acknowledgements

HwpForge๋Š” ๊ฑฐ์ธ๋“ค์˜ ์–ด๊นจ ์œ„์— ์„œ ์žˆ์Šต๋‹ˆ๋‹ค.

  • Hancom โ€” HWPX ํฌ๋งท์˜ ๊ณต๊ฐœ ๋ฌธ์„œ์™€ KS X 6101 (OWPML) ๊ตญ๊ฐ€ ํ‘œ์ค€์ด ์—†์—ˆ๋‹ค๋ฉด ์ด ํ”„๋กœ์ ํŠธ๋Š” ์‹œ์ž‘์กฐ์ฐจ ํ•  ์ˆ˜ ์—†์—ˆ์Šต๋‹ˆ๋‹ค. ํฌ๋งท์„ ๊ณต๊ฐœํ•ด ์ฃผ์‹  Hancom์— ๊ฐ์‚ฌ๋“œ๋ฆฝ๋‹ˆ๋‹ค.

  • openhwp โ€” Rust๋กœ HWP/HWPX๋ฅผ ๋‹ค๋ฃจ๋Š” IR ๊ธฐ๋ฐ˜ ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„์—์„œ ํฐ ์˜๊ฐ์„ ๋ฐ›์•˜์Šต๋‹ˆ๋‹ค. HwpForge์˜ Core ๋ ˆ์ด์–ด๊ฐ€ ์กด์žฌํ•  ์ˆ˜ ์žˆ์—ˆ๋˜ ๊ฒƒ์€ openhwp์ด ๋จผ์ € ๊ทธ ๊ธธ์„ ๊ฑธ์—ˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

  • hwpxlib โ€” Java๋กœ ์ž‘์„ฑ๋œ ๊ฐ€์žฅ ์„ฑ์ˆ™ํ•œ HWPX ๊ตฌํ˜„์ฒด์ž…๋‹ˆ๋‹ค. ์ŠคํŽ™๊ณผ ์‹ค์ œ ๋™์ž‘์˜ ์ฐจ์ด๋ฅผ ํŒŒ์•…ํ•˜๋Š” ๋ฐ ๊ฒฐ์ •์ ์ธ ์ฐธ๊ณ ๊ฐ€ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

  • hwp.js โ€” HWP5 ํฌ๋งท์˜ quirks์™€ edge case๋ฅผ ๊ผผ๊ผผํžˆ ๋ฌธ์„œํ™”ํ•œ ํ”„๋กœ์ ํŠธ์ž…๋‹ˆ๋‹ค. ๋ฐ”์ด๋„ˆ๋ฆฌ ํฌ๋งท์˜ ์–ด๋‘์šด ๊ตฌ์„์„ ๋ฐํ˜€ ์ค€ ๋•๋ถ„์— ์‹œํ–‰์ฐฉ์˜ค๋ฅผ ํฌ๊ฒŒ ์ค„์ผ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

  • hwpx-owpml-model โ€” Hancom์ด ์ง์ ‘ ๊ณต๊ฐœํ•œ C++ OWPML ๋ชจ๋ธ ๊ตฌํ˜„์ฒด๋กœ, ์Šคํ‚ค๋งˆ ํ•ด์„์˜ ์ตœ์ข… ๊ธฐ์ค€์œผ๋กœ ์‚ผ์•˜์Šต๋‹ˆ๋‹ค.

  • Rust ์ƒํƒœ๊ณ„ โ€” serde, quick-xml, pulldown-cmark, zip ๋“ฑ ๋›ฐ์–ด๋‚œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค ๋•๋ถ„์— HwpForge ์ „์ฒด๋ฅผ zero unsafe ์ˆœ์ˆ˜ Rust๋กœ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. Rust ์ปค๋ฎค๋‹ˆํ‹ฐ์™€ Ferris ๐Ÿฆ€์—๊ฒŒ ๊ฐ์‚ฌ๋“œ๋ฆฝ๋‹ˆ๋‹ค.

  • Claude by Anthropic โ€” HwpForge์˜ ์„ค๊ณ„, ๊ตฌํ˜„, ํ…Œ์ŠคํŠธ, ๋ฌธ์„œํ™” ์ „ ๊ณผ์ •์—์„œ Claude Code๊ฐ€ ๊ฐœ๋ฐœ ํŒŒํŠธ๋„ˆ๋กœ ํ•จ๊ป˜ํ–ˆ์Šต๋‹ˆ๋‹ค. LLM-first๋ฅผ ํ‘œ๋ฐฉํ•˜๋Š” ํ”„๋กœ์ ํŠธ๋‹ต๊ฒŒ, AI์™€ ์‚ฌ๋žŒ์ด ํ˜‘์—…ํ•˜์—ฌ ๋งŒ๋“ค์–ด๋‚ธ ๊ฒฐ๊ณผ๋ฌผ์ž…๋‹ˆ๋‹ค.


์‡ ๋ถ€๋ฆฌ (SoeBuri) ํ•œ์ปด ๋ฌธ์„œ๋ฅผ ๋ถˆ์— ๋‹ฌ๊ตฌ์–ด ๋‹จ๋‹จํ•˜๊ฒŒ ๋ฒผ๋ ค๋‚ด๋Š” ๋Œ€์žฅ์žฅ์ด ์˜ค๋ฆฌ๋„ˆ๊ตฌ๋ฆฌ ๐Ÿ”ฅ