From 2a9d0987344bb8c08417737651aa88d33348305a Mon Sep 17 00:00:00 2001 From: puleugo Date: Thu, 12 Mar 2026 20:08:44 +0900 Subject: [PATCH 1/4] feat: stabilize hwpx release flow --- .changeset/README.md | 19 + .changeset/config.json | 11 + .changeset/fuzzy-cows-fry.md | 5 + .github/workflows/version-packages.yml | 44 + .gitignore | 2 + Cargo.lock | 26 + README.md | 95 +- docs/ko/README.md | 6 +- docs/ko/benchmark.md | 7 +- docs/ko/design-system.md | 2 +- package.json | 4 +- packages/core-rs/Cargo.toml | 3 +- packages/core-rs/README.md | 50 +- packages/core-rs/src/bin/compare_hwpx_xml.rs | 119 +-- .../src/bin/refresh_hwpx_fixture_metadata.rs | 49 +- .../src/bin/support/archive_fixture.rs | 91 ++ packages/core-rs/src/diagnostics.rs | 217 ++++ .../src/generators/docx/render/block.rs | 35 +- .../docx/render/block_list_table.rs | 41 +- .../src/generators/docx/render/block_text.rs | 12 +- .../src/generators/docx/render/inline.rs | 2 +- .../core-rs/src/generators/docx/render/mod.rs | 5 +- .../hwpx/assets/core-paragraph-preview.png | Bin 0 -> 29587 bytes .../src/generators/hwpx/content_hpf.rs | 13 +- .../src/generators/hwpx/docs/README.md | 11 +- .../src/generators/hwpx/document_shape.rs | 106 ++ .../core-rs/src/generators/hwpx/header_xml.rs | 708 ------------- .../generators/hwpx/header_xml/contract.rs | 264 +++++ .../src/generators/hwpx/header_xml/mod.rs | 292 +++++ .../src/generators/hwpx/header_xml/styling.rs | 242 +++++ packages/core-rs/src/generators/hwpx/mod.rs | 26 +- .../src/generators/hwpx/package_xml.rs | 13 +- .../core-rs/src/generators/hwpx/profile.rs | 169 +++ .../paragraph-only/Contents/header.xml | 2 +- .../src/generators/hwpx/section_xml.rs | 995 ------------------ .../generators/hwpx/section_xml/collect.rs | 275 +++++ .../hwpx/section_xml/collect/style.rs | 184 ++++ .../hwpx/section_xml/collect/table.rs | 53 + .../src/generators/hwpx/section_xml/compat.rs | 775 ++++++++++++++ .../src/generators/hwpx/section_xml/mod.rs | 209 ++++ .../src/generators/hwpx/section_xml/render.rs | 401 +++++++ .../src/generators/hwpx/section_xml/shared.rs | 78 ++ .../src/generators/hwpx/section_xml/text.rs | 190 ++++ packages/core-rs/src/lib.rs | 185 ++++ packages/core-rs/src/parser/markdown.rs | 730 ------------- .../core-rs/src/parser/markdown/data_image.rs | 6 +- .../core-rs/src/parser/markdown/events.rs | 46 +- packages/core-rs/src/parser/markdown/mod.rs | 186 ++++ packages/core-rs/src/parser/markdown/push.rs | 20 +- packages/core-rs/src/wasm.rs | 155 ++- .../core-rs/tests/docx/regression/archive.rs | 4 +- .../core-rs/tests/docx/regression/layout.rs | 29 +- packages/core-rs/tests/docx/regression/mod.rs | 1 + .../core-rs/tests/docx/regression/report.rs | 43 + packages/core-rs/tests/docx_test.rs | 5 +- .../tests/fixtures/hwpx/approved/README.md | 6 + .../expected/Contents/header.xml | 2 +- .../expected/Contents/section0.xml | 2 +- .../blockquote-basic/expected/settings.xml | 2 +- .../approved/blockquote-basic/fixture.toml | 4 +- .../approved/blockquote-basic/golden.hwpx | Bin 27497 -> 27534 bytes .../hwpx/approved/blockquote-basic/hash.txt | 2 +- .../expected/Contents/header.xml | 2 +- .../expected/Contents/section0.xml | 2 +- .../approved/code-block-basic/fixture.toml | 2 + .../approved/code-block-basic/golden.hwpx | Bin 27517 -> 27421 bytes .../hwpx/approved/code-block-basic/hash.txt | 2 +- .../core-heading/expected/Contents/header.xml | 2 +- .../hwpx/approved/core-heading/fixture.toml | 2 + .../hwpx/approved/core-heading/golden.hwpx | Bin 27711 -> 27685 bytes .../hwpx/approved/core-heading/hash.txt | 2 +- .../expected/Contents/content.hpf | 2 +- .../expected/Contents/header.xml | 2 +- .../expected/Contents/section0.xml | 2 +- .../expected/Preview/PrvImage.png.sha256 | 2 +- .../expected/Preview/PrvText.txt | 2 +- .../core-inline-style/expected/settings.xml | 2 +- .../core-inline-style/expected/version.xml | 2 +- .../approved/core-inline-style/fixture.toml | 2 + .../approved/core-inline-style/golden.hwpx | Bin 27812 -> 27980 bytes .../hwpx/approved/core-inline-style/hash.txt | 2 +- .../expected/Contents/content.hpf | 2 +- .../expected/Contents/header.xml | 2 +- .../expected/Contents/section0.xml | 2 +- .../expected/Preview/PrvImage.png.sha256 | 2 +- .../expected/Preview/PrvText.txt | 2 +- .../core-link-text/expected/settings.xml | 2 +- .../core-link-text/expected/version.xml | 2 +- .../hwpx/approved/core-link-text/fixture.toml | 2 + .../hwpx/approved/core-link-text/golden.hwpx | Bin 27745 -> 24910 bytes .../hwpx/approved/core-link-text/hash.txt | 2 +- .../core-mixed/expected/Contents/content.hpf | 2 +- .../core-mixed/expected/Contents/header.xml | 2 +- .../core-mixed/expected/Contents/section0.xml | 2 +- .../expected/Preview/PrvImage.png.sha256 | 2 +- .../core-mixed/expected/Preview/PrvText.txt | 3 +- .../approved/core-mixed/expected/settings.xml | 2 +- .../approved/core-mixed/expected/version.xml | 2 +- .../hwpx/approved/core-mixed/fixture.toml | 3 + .../hwpx/approved/core-mixed/golden.hwpx | Bin 28028 -> 35731 bytes .../hwpx/approved/core-mixed/hash.txt | 2 +- .../hwpx/approved/core-mixed/input.md | 3 +- .../expected/Contents/content.hpf | 2 +- .../expected/Contents/header.xml | 2 +- .../expected/Contents/section0.xml | 2 +- .../expected/Preview/PrvImage.png.sha256 | 2 +- .../expected/Preview/PrvText.txt | 3 +- .../core-paragraph/expected/settings.xml | 2 +- .../core-paragraph/expected/version.xml | 2 +- .../hwpx/approved/core-paragraph/fixture.toml | 2 + .../hwpx/approved/core-paragraph/golden.hwpx | Bin 27585 -> 37624 bytes .../hwpx/approved/core-paragraph/hash.txt | 2 +- .../hwpx/approved/core-paragraph/input.md | 4 +- .../list-basic/expected/Contents/header.xml | 2 +- .../list-basic/expected/Contents/section0.xml | 2 +- .../hwpx/approved/list-basic/fixture.toml | 2 + .../hwpx/approved/list-basic/golden.hwpx | Bin 27557 -> 27445 bytes .../hwpx/approved/list-basic/hash.txt | 2 +- .../expected/Contents/header.xml | 2 +- .../expected/Contents/section0.xml | 2 +- .../approved/list-nested-depth-2/fixture.toml | 2 + .../approved/list-nested-depth-2/golden.hwpx | Bin 27630 -> 27500 bytes .../approved/list-nested-depth-2/hash.txt | 2 +- .../expected/Contents/header.xml | 2 +- .../expected/Contents/section0.xml | 2 +- .../approved/ordered-list-basic/fixture.toml | 2 + .../approved/ordered-list-basic/golden.hwpx | Bin 27557 -> 27446 bytes .../hwpx/approved/ordered-list-basic/hash.txt | 2 +- .../expected/Contents/header.xml | 2 +- .../expected/Contents/section0.xml | 2 +- .../ordered-list-nested-depth-2/fixture.toml | 2 + .../ordered-list-nested-depth-2/golden.hwpx | Bin 27665 -> 27536 bytes .../ordered-list-nested-depth-2/hash.txt | 2 +- .../expected/Contents/content.hpf | 2 +- .../expected/Contents/header.xml | 2 +- .../expected/Contents/section0.xml | 2 +- .../expected/Preview/PrvImage.png.sha256 | 2 +- .../expected/Preview/PrvText.txt | 2 +- .../style-brand-color/expected/settings.xml | 2 +- .../style-brand-color/expected/version.xml | 2 +- .../approved/style-brand-color/fixture.toml | 3 + .../approved/style-brand-color/golden.hwpx | Bin 27922 -> 31751 bytes .../hwpx/approved/style-brand-color/hash.txt | 2 +- .../expected/Contents/content.hpf | 2 +- .../expected/Contents/header.xml | 2 +- .../expected/Contents/section0.xml | 2 +- .../expected/Preview/PrvImage.png.sha256 | 2 +- .../expected/settings.xml | 2 +- .../expected/version.xml | 2 +- .../style-centered-layout/fixture.toml | 3 + .../style-centered-layout/golden.hwpx | Bin 27839 -> 32545 bytes .../approved/style-centered-layout/hash.txt | 2 +- .../expected/Contents/content.hpf | 2 +- .../expected/Contents/header.xml | 2 +- .../expected/Contents/section0.xml | 2 +- .../expected/Preview/PrvImage.png.sha256 | 2 +- .../expected/Preview/PrvText.txt | 2 +- .../style-typography/expected/settings.xml | 2 +- .../style-typography/expected/version.xml | 2 +- .../approved/style-typography/fixture.toml | 3 + .../approved/style-typography/golden.hwpx | Bin 27838 -> 28943 bytes .../hwpx/approved/style-typography/hash.txt | 2 +- .../expected/Contents/header.xml | 2 +- .../approved/table-alignment/fixture.toml | 2 + .../hwpx/approved/table-alignment/golden.hwpx | Bin 28060 -> 28036 bytes .../hwpx/approved/table-alignment/hash.txt | 2 +- .../table-basic/expected/Contents/header.xml | 2 +- .../hwpx/approved/table-basic/fixture.toml | 2 + .../hwpx/approved/table-basic/golden.hwpx | Bin 28060 -> 28036 bytes .../hwpx/approved/table-basic/hash.txt | 2 +- .../image-data-uri-basic/golden.hwpx | Bin 27461 -> 35386 bytes .../provisional/image-data-uri-basic/input.md | 2 +- .../hwpx/provisional/mixed-rich/golden.hwpx | Bin 27897 -> 43459 bytes .../hwpx/provisional/mixed-rich/input.md | 2 - packages/core-rs/tests/hwpx/fixture_suite.rs | 12 + packages/core-rs/tests/hwpx/regression.rs | 826 --------------- .../core-rs/tests/hwpx/regression/document.rs | 324 ++++++ .../core-rs/tests/hwpx/regression/errors.rs | 124 +++ .../core-rs/tests/hwpx/regression/lists.rs | 236 +++++ packages/core-rs/tests/hwpx/regression/mod.rs | 132 +++ .../core-rs/tests/hwpx/regression/styles.rs | 232 ++++ .../tests/hwpx_fixture_refresh_test.rs | 92 +- packages/core-rs/tests/hwpx_test.rs | 38 +- .../core-rs/tests/readme_contract_test.rs | 39 + .../core-rs/tests/support/archive_fixture.rs | 4 +- .../core-rs/tests/support/docx_fixture.rs | 5 +- .../core-rs/tests/support/docx_runtime.rs | 5 +- .../core-rs/tests/support/hwpx_fixture.rs | 12 + packages/npm-core-rs/README.md | 93 +- .../npm-core-rs/demo/assets/og-image-ko.html | 2 +- .../npm-core-rs/demo/assets/og-image.html | 6 +- packages/npm-core-rs/demo/browser-client.js | 40 +- .../npm-core-rs/demo/comparison-contract.json | 71 ++ .../npm-core-rs/demo/comparison-data.json | 102 +- packages/npm-core-rs/demo/index.html | 12 +- packages/npm-core-rs/demo/ko/index.html | 9 +- packages/npm-core-rs/demo/main.js | 53 +- .../scripts/_benchmark-corpora.mjs | 143 +++ .../scripts/_comparison-common.mjs | 68 +- .../scripts/benchmark-pandoc-check.mjs | 4 + .../npm-core-rs/scripts/benchmark-pandoc.mjs | 189 +--- .../npm-core-rs/scripts/browser-smoke.mjs | 14 +- packages/npm-core-rs/scripts/build-pages.mjs | 2 +- packages/npm-core-rs/scripts/build.mjs | 1 + packages/npm-core-rs/scripts/demo-server.mjs | 21 +- packages/npm-core-rs/scripts/node-smoke.mjs | 52 +- packages/npm-core-rs/scripts/pack-smoke.mjs | 8 +- packages/npm-core-rs/scripts/sync-version.mjs | 19 + packages/npm-core-rs/src/browser.js | 63 +- packages/npm-core-rs/src/index.d.ts | 57 + packages/npm-core-rs/src/node.js | 61 +- packages/npm-core-rs/src/shared.js | 73 ++ 212 files changed, 6808 insertions(+), 3914 deletions(-) create mode 100644 .changeset/README.md create mode 100644 .changeset/config.json create mode 100644 .changeset/fuzzy-cows-fry.md create mode 100644 .github/workflows/version-packages.yml create mode 100644 packages/core-rs/src/diagnostics.rs create mode 100644 packages/core-rs/src/generators/hwpx/assets/core-paragraph-preview.png create mode 100644 packages/core-rs/src/generators/hwpx/document_shape.rs delete mode 100644 packages/core-rs/src/generators/hwpx/header_xml.rs create mode 100644 packages/core-rs/src/generators/hwpx/header_xml/contract.rs create mode 100644 packages/core-rs/src/generators/hwpx/header_xml/mod.rs create mode 100644 packages/core-rs/src/generators/hwpx/header_xml/styling.rs create mode 100644 packages/core-rs/src/generators/hwpx/profile.rs delete mode 100644 packages/core-rs/src/generators/hwpx/section_xml.rs create mode 100644 packages/core-rs/src/generators/hwpx/section_xml/collect.rs create mode 100644 packages/core-rs/src/generators/hwpx/section_xml/collect/style.rs create mode 100644 packages/core-rs/src/generators/hwpx/section_xml/collect/table.rs create mode 100644 packages/core-rs/src/generators/hwpx/section_xml/compat.rs create mode 100644 packages/core-rs/src/generators/hwpx/section_xml/mod.rs create mode 100644 packages/core-rs/src/generators/hwpx/section_xml/render.rs create mode 100644 packages/core-rs/src/generators/hwpx/section_xml/shared.rs create mode 100644 packages/core-rs/src/generators/hwpx/section_xml/text.rs delete mode 100644 packages/core-rs/src/parser/markdown.rs create mode 100644 packages/core-rs/src/parser/markdown/mod.rs create mode 100644 packages/core-rs/tests/docx/regression/report.rs delete mode 100644 packages/core-rs/tests/hwpx/regression.rs create mode 100644 packages/core-rs/tests/hwpx/regression/document.rs create mode 100644 packages/core-rs/tests/hwpx/regression/errors.rs create mode 100644 packages/core-rs/tests/hwpx/regression/lists.rs create mode 100644 packages/core-rs/tests/hwpx/regression/mod.rs create mode 100644 packages/core-rs/tests/hwpx/regression/styles.rs create mode 100644 packages/core-rs/tests/readme_contract_test.rs create mode 100644 packages/npm-core-rs/demo/comparison-contract.json create mode 100644 packages/npm-core-rs/scripts/_benchmark-corpora.mjs create mode 100644 packages/npm-core-rs/scripts/sync-version.mjs create mode 100644 packages/npm-core-rs/src/shared.js diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 0000000..ac9c6af --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,19 @@ +# Changesets + +This repository uses [Changesets](https://github.com/changesets/changesets) to stage npm version +updates for `@docxly/core-rs`. + +Common contributor flow: + +```bash +npx @changesets/cli add +``` + +After the changeset lands on `main`, the `Version Packages` workflow opens or updates a release PR +that: + +- bumps `packages/npm-core-rs/package.json` +- syncs `packages/core-rs/Cargo.toml` +- refreshes `packages/npm-core-rs/package-lock.json` + +The existing tag-based `Release` workflow still handles the actual npm publish for `v*.*.*` tags. diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000..81153cb --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", + "changelog": false, + "commit": false, + "fixed": [], + "linked": [], + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [] +} diff --git a/.changeset/fuzzy-cows-fry.md b/.changeset/fuzzy-cows-fry.md new file mode 100644 index 0000000..cf9fc45 --- /dev/null +++ b/.changeset/fuzzy-cows-fry.md @@ -0,0 +1,5 @@ +--- +"@docxly/core-rs": patch +--- + +Stabilize HWPX generation and demo packaging, and add automated npm versioning workflow support. diff --git a/.github/workflows/version-packages.yml b/.github/workflows/version-packages.yml new file mode 100644 index 0000000..fca6503 --- /dev/null +++ b/.github/workflows/version-packages.yml @@ -0,0 +1,44 @@ +name: Version Packages + +on: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +concurrency: + group: version-packages + cancel-in-progress: true + +jobs: + version: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install workspace dependencies + run: npm install + + - name: Create or update version PR + uses: changesets/action@v1 + with: + version: npm run version:packages + commit: "chore: version packages" + title: "chore: version packages" + createGithubReleases: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index ac04211..3f74d1c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ .DS_Store .codex/ +.playwright-cli/ node_modules/ +output/ packages/npm-core-rs/dist/ packages/npm-core-rs/node_modules/ packages/npm-core-rs/site-dist/ diff --git a/Cargo.lock b/Cargo.lock index 16930df..8eb4a68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -58,6 +58,7 @@ dependencies = [ "pulldown-cmark", "roxmltree", "serde", + "serde_json", "sha2", "thiserror", "toml", @@ -182,6 +183,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + [[package]] name = "libc" version = "0.2.182" @@ -295,6 +302,19 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "serde_spanned" version = "0.6.9" @@ -494,6 +514,12 @@ dependencies = [ "zopfli", ] +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + [[package]] name = "zopfli" version = "0.8.3" diff --git a/README.md b/README.md index 903e040..13c84fd 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Language: [English](./README.md) · [한국어](./docs/ko/README.md) · [Docs](./docs/README.md) -`docxly/core-rs` currently measures at an 80 ms cold start and a 2 ms steady median, versus 284 ms cold and 210 ms steady for Pandoc on the summary DOCX benchmark corpus, a 105x steady-state advantage while also exposing browser-local generation and HWPX support from the same Rust core. +`docxly/core-rs` is an embeddable Rust/WASM document generation engine for DOCX and HWPX. Current benchmark details live in the comparison block below so the published README stays aligned with the checked-in benchmark dataset. The project is developed with a TDD-first workflow. The current milestone implements the DOCX rich slice and an approved HWPX baseline backed by manually validated golden fixtures. @@ -58,7 +58,7 @@ fs::write("output.docx", docx)?; ## Why docxly - Build document generation directly into a product instead of shelling out to a converter. -- On the current summary corpus, generate complex DOCX output in `2 ms` steady median versus Pandoc's `210 ms`, with `80 ms` versus `284 ms` cold start. +- Keep benchmark numbers in the checked-in comparison block below instead of scattering stale point-in-time claims through the README. - Use the same Rust core across Node, browser, and HWPX workflows. - Start with DOCX today and expand into HWPX from the same repository. - Ship deterministic outputs backed by fixture-driven tests and normalized archive checks. @@ -69,7 +69,7 @@ Try the browser demo on GitHub Pages: - https://docxly.github.io/core-rs/ -The live page uses the published WASM wrapper and downloads a real `.docx` file directly in the browser. +The live page uses the published WASM wrapper and downloads real `.docx` and `.hwpx` files directly in the browser. @@ -81,21 +81,25 @@ Use docxly when document generation must live inside a Node service, browser wor | Corpus | docxly cold | Pandoc cold | docxly steady | Pandoc steady | Speed ratio | | --- | --- | --- | --- | --- | --- | -| Small | 66 ms | 539 ms | 1 ms | 249 ms | 369.84x | -| Medium | 80 ms | 284 ms | 2 ms | 126 ms | 64.85x | -| Large | 88 ms | 219 ms | 4 ms | 210 ms | 57.60x | -| Summary | 80 ms | 284 ms | 2 ms | 210 ms | 105.00x | - -| Capability | docxly | Pandoc | -| --- | --- | --- | -| Embeddable in app | Yes, library-first for Node and browser bundlers | CLI-first with process invocation | -| Browser-local generation | First-party browser package and WASM path | Possible through pandoc.wasm, not the primary npm workflow | -| npm distribution | Published package | Not a first-party npm package | -| HWPX generation | Supported in the Rust core | Not supported | -| Broad format conversion | Focused on DOCX and HWPX generation | Wide multi-format conversion | -| DOCX reference-template workflow | Not a reference.docx workflow | Supported via reference.docx | - -Measured on darwin 25.2.0 / arm64 at 2026-03-10T14:29:12.752Z with Node v23.7.0 and Pandoc 3.9. +| Small | 63 ms | 454 ms | 1 ms | 253 ms | 379.53x | +| Medium | 83 ms | 306 ms | 2 ms | 95 ms | 60.33x | +| Large | 83 ms | 192 ms | 4 ms | 193 ms | 53.92x | +| Summary | 83 ms | 306 ms | 2 ms | 193 ms | 96.50x | + +| Capability | DOCX | HWPX strict | HWPX compat | +| --- | --- | --- | --- | +| Headings and paragraphs | Yes | Yes | Yes | +| Inline emphasis, strong, code, links | Yes | Yes | Yes | +| Ordered lists up to depth 2 | Yes | No | Yes, semantic contract | +| Unordered lists up to depth 2 | Yes | Yes | Yes | +| Tables | Yes | Yes | Yes | +| `data:` URI images | Yes | No | Degraded fallback to alt text | +| Unsupported HTML | Strict: error, compat: literal text fallback | Error | Degraded literal text fallback | +| Footnotes, task lists, math | Strict: error, compat: visible text fallback | Error | Degraded visible text fallback | +| Deep nested lists | Strict: error, compat: plain text fallback | Error | Degraded plain text fallback | +| HWPX API in npm | N/A | Experimental API | Experimental API | + +Measured on darwin 25.2.0 / arm64 at 2026-03-11T08:19:00.589Z with Node v23.7.0 and Pandoc 3.9. - This benchmark measures DOCX generation only and does not compare HWPX. - The numbers above come from an offline Node environment and are not browser runtime timings. @@ -126,11 +130,11 @@ Measured on darwin 25.2.0 / arm64 at 2026-03-10T14:29:12.752Z with Node v23.7.0 - Implemented: Markdown parser, internal shared intermediate model, deterministic DOCX packaging - Implemented: fixture-driven integration tests with normalized hash comparison -- Implemented: strict/fallback handling for unsupported HTML, non-data images, and deep nested lists +- Implemented: strict/fallback handling plus report APIs for unsupported HTML, images, task lists, math, and deep nested lists - Implemented: approved HWPX baseline backed by manually validated fixtures -- Current HWPX CI gates use these approved fixtures: `core-paragraph`, `blockquote-basic`, `code-block-basic`, `core-heading`, `core-inline-style`, `core-link-text`, `core-mixed`, `list-basic`, `list-nested-depth-2`, `style-typography`, `style-centered-layout`, and `style-brand-color` -- Provisional and quarantined HWPX artifacts are excluded from the release gate -- HWPX style options currently apply to body paragraphs and heading paragraphs; future block types such as lists and tables may add more paragraph categories +- Approved HWPX fixtures are the release gate; provisional and quarantined artifacts are not +- Approved fixture status is separate from strict/compat support claims; ordered lists are compat-only, while tables are supported in strict mode +- Package-specific details such as the current approved fixture set and Rust API notes live in `packages/core-rs/README.md` and `packages/core-rs/src/generators/hwpx/docs/README.md` ## Supported Markdown Today @@ -159,6 +163,7 @@ Current behavior: ## Public API The Rust crate intentionally exposes only the high-level API. Parser, model, generator, and utility modules are internal implementation details. +Repository-level guidance stays high level here; concrete Rust API details live in `packages/core-rs/README.md`, and npm/browser usage lives in `packages/npm-core-rs/README.md`. Recommended first flow: @@ -178,36 +183,33 @@ let docx = generate_docx( fs::write("output.docx", docx)?; ``` -Current options: - -- `title: Option` -- `author: Option` -- `strict_mode: bool` -- `style: HwpxStyleOptions` on `HwpxOptions` only - Notes: - `generate_docx` returns a deterministic `.docx` archive as `Vec` +- `analyze_markdown`, `generate_docx_with_report`, and `generate_hwpx_with_report` surface strict/compat compatibility issues without widening the core generation API - `generate_hwpx` targets the approved HWPX baseline reproduced by the committed golden fixtures - `HwpxStyleOptions` currently supports document-level body/heading font, body/heading size, text/link/heading color, and paragraph alignment overrides - Custom HWPX fonts are best-effort only; the current HWPX path records font family names but does not embed font binaries -- internal modules such as parser/model/generator helpers are not part of the public contract +- Internal modules such as parser/model/generator helpers are not part of the public contract ## HWPX Supported Today The current HWPX path is narrower than the DOCX rich slice. -- approved baseline: `core-paragraph`, `blockquote-basic`, `code-block-basic`, `core-heading`, `core-inline-style`, `core-link-text`, `core-mixed` -- approved list baseline: `list-basic`, `list-nested-depth-2` -- approved style baseline: `style-typography`, `style-centered-layout`, `style-brand-color` -- supported content today: - - paragraphs - - headings - - visible-text emphasis/strong/code/link rendering inside the approved compatibility contract - - document-level HWPX style overrides -- not yet part of the approved HWPX baseline: +- approved fixture details: see `packages/core-rs/README.md` and `packages/core-rs/src/generators/hwpx/docs/README.md` +- strict-supported today: + - paragraphs and headings + - blockquotes + - visible-text emphasis/strong/code/link rendering + - unordered lists up to depth 2 - tables + - document-level HWPX style overrides +- compat-only semantic support: + - ordered lists up to depth 2 +- compat degraded fallback: - images + - unsupported rich blocks such as code blocks and thematic breaks + - deep nested lists Example HWPX generation: @@ -244,8 +246,8 @@ The repository also contains an npm package at `packages/npm-core-rs/`. - package name: `@docxly/core-rs` - runtime target: Node + Browser -- public npm API: `generateDocx(markdown, options) -> Promise` -- HWPX is intentionally not exposed in npm v0.x even though the Rust crate now implements the HWPX core subset +- public npm APIs: `generateDocx`, `generateHwpx`, `analyzeMarkdown`, `generateDocxWithReport`, `generateHwpxWithReport` +- HWPX is exposed as an experimental API in npm, with support differing between strict and compat mode ## Rust Crate Status @@ -282,6 +284,7 @@ npm run test:web npm run demo npm run build:pages npm run test:all +npm run changeset:add ``` Live demo URL: @@ -295,6 +298,14 @@ What each command does: - `demo`: builds the Pages artifact and serves the browser demo locally - `build:pages`: creates the static GitHub Pages artifact at `packages/npm-core-rs/site-dist` - `test:all`: runs Rust lint, Rust tests, and web smoke tests from one root entrypoint +- `changeset:add`: creates a release note entry that feeds the automated npm version PR workflow + +## Release Flow + +- Add a changeset for npm-facing changes with `npm run changeset:add` +- When that changeset lands on `main`, the `Version Packages` workflow opens or updates a version PR +- That PR updates `packages/npm-core-rs/package.json`, syncs `packages/core-rs/Cargo.toml`, and refreshes `packages/npm-core-rs/package-lock.json` +- Publishing still happens from the existing tag-based `Release` workflow when a matching `v*.*.*` tag is pushed ## Running Tests @@ -359,7 +370,7 @@ hash.txt - `packages/core-rs/src/generators/hwpx/docs/schema-md/index.md` - `packages/core-rs/src/generators/hwpx/reference/paragraph-only` -These files are the repository-level reference corpus for HWPX work. They combine curated implementation notes, Markdown conversions of Hancom reference material, and the local approved/reference fixtures used to keep the package contract stable. +These files are contributor-facing HWPX references. They combine curated implementation notes, Markdown conversions of Hancom reference material, and the local approved/reference fixtures used to keep the package contract stable. ## Development Notes diff --git a/docs/ko/README.md b/docs/ko/README.md index a06a693..8266834 100644 --- a/docs/ko/README.md +++ b/docs/ko/README.md @@ -17,9 +17,9 @@ Pandoc이 다양한 형식 사이를 변환하는 범용 도구라면, `docxly` ## 핵심 수치 -- 현재 요약 벤치마크 기준 `docxly`는 `80 ms cold / 2 ms steady` -- 같은 코퍼스에서 Pandoc은 `284 ms cold / 210 ms steady` -- 반복 실행 구간에서는 `105x` 빠른 수치가 측정됐습니다. +- 최신 benchmark 수치는 루트 README의 comparison block과 npm comparison dataset을 기준으로 함께 갱신됩니다. +- 고정 수치만 따로 인용하기보다, 같은 저장소에 커밋된 비교 데이터와 caveat를 함께 보는 것이 안전합니다. +- 현재 benchmark는 DOCX generation only, offline Node 환경 기준입니다. ## 언제 docxly를 선택해야 하나 diff --git a/docs/ko/benchmark.md b/docs/ko/benchmark.md index 70fce99..e3b894c 100644 --- a/docs/ko/benchmark.md +++ b/docs/ko/benchmark.md @@ -9,9 +9,8 @@ ## 현재 요약 벤치마크 -- `docxly`: `80 ms cold / 2 ms steady` -- `Pandoc`: `284 ms cold / 210 ms steady` -- 반복 실행 기준 `105x` +- 최신 수치는 루트 README의 comparison block과 `packages/npm-core-rs/demo/comparison-data.json`을 기준으로 봅니다. +- 이 문서에는 고정 숫자를 복제하지 않고, 어떤 환경에서 어떤 의미의 수치인지 해석만 남깁니다. 측정 환경은 다음과 같습니다. @@ -19,6 +18,7 @@ - Node: `v23.7.0` - Pandoc: `3.9` - 기준: 복잡한 DOCX 벤치마크 코퍼스 +- 범위: DOCX generation only ## 해석 @@ -27,6 +27,7 @@ - `Pandoc`: process startup 포함 - steady median은 warm-up 이후 반복 생성 비용을 뜻합니다. - 이 수치는 브라우저 실측이 아니라 오프라인 Node 벤치마크 결과입니다. +- HWPX 비교나 브라우저 런타임 수치로 일반화하면 안 됩니다. ## 언제 Pandoc이 더 적합한가 diff --git a/docs/ko/design-system.md b/docs/ko/design-system.md index 3817c0d..e8a8b8a 100644 --- a/docs/ko/design-system.md +++ b/docs/ko/design-system.md @@ -165,7 +165,7 @@ 기본 설치 근거 문구: -- `Install the embeddable DOCX engine that measured 105x faster than Pandoc on the summary benchmark.` +- `Install the embeddable DOCX engine backed by the shared benchmark dataset.` 금지: diff --git a/package.json b/package.json index 535dc14..1aa7529 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,12 @@ "build:pages": "npm run build:pages -w @docxly/core-rs", "benchmark:pandoc": "npm run benchmark:pandoc -w @docxly/core-rs", "benchmark:pandoc:check": "npm run benchmark:pandoc:check -w @docxly/core-rs", + "changeset:add": "npx @changesets/cli@2.29.7 add", "demo": "npm run demo -w @docxly/core-rs", "test:web": "npm run build:web && npm run test:smoke -w @docxly/core-rs", "test:rust": "cargo test -p core-rs", "lint:rust": "cargo clippy -p core-rs --all-targets -- -D warnings", - "test:all": "npm run lint:rust && npm run test:rust && npm run test:web" + "test:all": "npm run lint:rust && npm run test:rust && npm run test:web", + "version:packages": "npx @changesets/cli@2.29.7 version && node ./packages/npm-core-rs/scripts/sync-version.mjs && npm install --package-lock-only -w @docxly/core-rs" } } diff --git a/packages/core-rs/Cargo.toml b/packages/core-rs/Cargo.toml index fbf2c53..e272387 100644 --- a/packages/core-rs/Cargo.toml +++ b/packages/core-rs/Cargo.toml @@ -15,6 +15,8 @@ crate-type = ["rlib", "cdylib"] [dependencies] base64 = "0.22" pulldown-cmark = "0.12" +serde = { version = "1", features = ["derive"] } +serde_json = "1" sha2 = "0.10" thiserror = "2" zip = { version = "2", default-features = false, features = ["deflate"] } @@ -24,5 +26,4 @@ wasm-bindgen = "=0.2.114" [dev-dependencies] roxmltree = "0.20" -serde = { version = "1", features = ["derive"] } toml = "0.8" diff --git a/packages/core-rs/README.md b/packages/core-rs/README.md index d375564..f845445 100644 --- a/packages/core-rs/README.md +++ b/packages/core-rs/README.md @@ -71,6 +71,9 @@ Current high-level API behavior: - `generate_docx(...) -> Result, CoreRsError>` - `generate_hwpx(...) -> Result, CoreRsError>` +- `analyze_markdown(...) -> ConversionReport` +- `generate_docx_with_report(...) -> Result` +- `generate_hwpx_with_report(...) -> Result` - `generate_hwpx` targets the approved HWPX golden baseline committed under `tests/fixtures/hwpx/approved` - approved HWPX fixtures are the release gate; provisional/quarantined artifacts are not - `HwpxStyleOptions` currently supports document-level body/heading font, body/heading size, text/link/heading color, and paragraph alignment overrides @@ -80,28 +83,22 @@ Current high-level API behavior: The current HWPX implementation is narrower than the DOCX rich slice. -- approved compatibility fixtures: - - `core-paragraph` - - `blockquote-basic` - - `code-block-basic` - - `core-heading` - - `core-inline-style` - - `core-link-text` - - `core-mixed` - - `list-basic` - - `list-nested-depth-2` -- approved style fixtures: - - `style-typography` - - `style-centered-layout` - - `style-brand-color` -- supported content today: - - paragraphs - - headings - - visible-text emphasis/strong/code/link rendering inside the approved compatibility contract - - document-level style overrides through `HwpxStyleOptions` -- not yet approved for HWPX: - - tables - - images +| Capability | DOCX | HWPX strict | HWPX compat | +| --- | --- | --- | --- | +| Headings and paragraphs | Yes | Yes | Yes | +| Blockquotes | Yes | Yes | Yes | +| Inline emphasis, strong, code, links | Yes | Yes | Yes | +| Ordered lists up to depth 2 | Yes | No | Yes, semantic contract | +| Unordered lists up to depth 2 | Yes | Yes | Yes | +| Tables | Yes | Yes | Yes | +| `data:` URI images | Yes | No | Degraded fallback to alt text | +| Unsupported HTML | Strict: error, compat: literal text fallback | Error | Degraded literal text fallback | +| Footnotes, task lists, math | Strict: error, compat: visible text fallback | Error | Degraded visible text fallback | +| Deep nested lists | Strict: error, compat: plain text fallback | Error | Degraded plain text fallback | + +Approved HWPX fixtures are the release gate, but approved fixture status does not imply strict-mode +support. For the current approved fixture inventory, use +`tests/fixtures/hwpx/approved/README.md` and `src/generators/hwpx/docs/README.md`. Example: @@ -139,7 +136,10 @@ This crate is configured with `cdylib` output for the npm wrapper build. - target package: `@docxly/core-rs` - wrapper location: `../npm-core-rs` - CI verifies native Rust checks and a `wasm32-unknown-unknown` build before npm publish -- browser consumers should use the npm wrapper through a bundler that can emit the `.wasm` asset +- npm version PRs sync this crate's `Cargo.toml` version from the npm package version on `main` +- browser consumers should use the npm wrapper through either: + - a bundler/runtime that can emit the `.wasm` asset + - a static host that serves the emitted `.wasm` asset alongside the ESM wrapper ## Testing @@ -184,8 +184,8 @@ Each fixture directory is self-contained: - DOCX rich: implemented - HWPX approved baseline: implemented -- Only manually approved HWPX fixtures are treated as release-gate goldens; `core-paragraph`, `blockquote-basic`, `code-block-basic`, `core-heading`, `core-inline-style`, `core-link-text`, `core-mixed`, `list-basic`, `list-nested-depth-2`, `table-basic`, `style-typography`, `style-centered-layout`, and `style-brand-color` are the current approved baselines +- Only manually approved HWPX fixtures are treated as release-gate goldens; see `tests/fixtures/hwpx/approved/README.md` for the current inventory - HWPX `paragraph_align` currently targets body and heading paragraph styles; future paragraph categories may extend that scope -- Public API: high-level generation functions and option/error types only +- Public API: high-level generation functions, report types, and option/error types only For repository-level usage and roadmap details, see the root [`README.md`](../../README.md). diff --git a/packages/core-rs/src/bin/compare_hwpx_xml.rs b/packages/core-rs/src/bin/compare_hwpx_xml.rs index 57ff49a..c8a65ac 100644 --- a/packages/core-rs/src/bin/compare_hwpx_xml.rs +++ b/packages/core-rs/src/bin/compare_hwpx_xml.rs @@ -1,27 +1,13 @@ use std::collections::{BTreeMap, BTreeSet}; use std::env; -use std::fmt; -use std::fs::File; -use std::io::Read; use std::path::{Path, PathBuf}; -use sha2::{Digest, Sha256}; -use zip::read::ZipArchive; +#[path = "support/archive_fixture.rs"] +mod archive_fixture; -#[derive(Clone, Debug, PartialEq, Eq)] -enum NormalizedEntry { - Text(String), - BinaryHash(String), -} - -impl fmt::Display for NormalizedEntry { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Text(text) => write!(f, "text(len={})", text.len()), - Self::BinaryHash(hash) => write!(f, "binary(sha256={hash})"), - } - } -} +use archive_fixture::{ + FixtureResult, NormalizedEntry, comparison_excludes_for_fixture, filtered_normalized_entries, +}; #[derive(Debug)] struct ArchiveSnapshot { @@ -50,55 +36,18 @@ fn run() -> Result<(), String> { let left_path = PathBuf::from(left); let right_path = PathBuf::from(right); - let left = read_snapshot(&left_path)?; - let right = read_snapshot(&right_path)?; + let excludes = + comparison_excludes(&left_path, &right_path).map_err(|error| error.to_string())?; + let left = read_snapshot(&left_path, &excludes).map_err(|error| error.to_string())?; + let right = read_snapshot(&right_path, &excludes).map_err(|error| error.to_string())?; report(&left_path, &left, &right_path, &right); Ok(()) } -fn read_snapshot(path: &Path) -> Result { - let file = - File::open(path).map_err(|error| format!("failed to open {}: {error}", path.display()))?; - let mut archive = ZipArchive::new(file) - .map_err(|error| format!("failed to open {} as zip: {error}", path.display()))?; - - let mut entries = BTreeMap::new(); - - for index in 0..archive.len() { - let mut entry = archive.by_index(index).map_err(|error| { - format!( - "failed to read zip entry {index} from {}: {error}", - path.display() - ) - })?; - let name = entry.name().to_string(); - - let normalized = if is_text_entry(&name) { - let mut text = String::new(); - entry.read_to_string(&mut text).map_err(|error| { - format!( - "failed to read text entry {} from {}: {error}", - name, - path.display() - ) - })?; - NormalizedEntry::Text(normalize_text(&text)) - } else { - let mut bytes = Vec::new(); - entry.read_to_end(&mut bytes).map_err(|error| { - format!( - "failed to read binary entry {} from {}: {error}", - name, - path.display() - ) - })?; - NormalizedEntry::BinaryHash(sha256_hex(&bytes)) - }; - - entries.insert(name, normalized); - } - +fn read_snapshot(path: &Path, excluded_paths: &[String]) -> FixtureResult { + let bytes = std::fs::read(path)?; + let entries = filtered_normalized_entries(&bytes, is_text_entry, excluded_paths)?; Ok(ArchiveSnapshot { entries }) } @@ -160,8 +109,8 @@ fn report(left_path: &Path, left: &ArchiveSnapshot, right_path: &Path, right: &A println!(" {summary}"); } _ => { - println!(" left: {left_entry}"); - println!(" right: {right_entry}"); + println!(" left: {}", describe_entry(left_entry)); + println!(" right: {}", describe_entry(right_entry)); } } } @@ -184,20 +133,11 @@ fn is_text_entry(name: &str) -> bool { || name == "mimetype" } -fn normalize_text(text: &str) -> String { - text.replace("\r\n", "\n") - .lines() - .map(str::trim_end) - .collect::>() - .join("\n") - .trim() - .to_string() -} - -fn sha256_hex(bytes: &[u8]) -> String { - let mut hasher = Sha256::new(); - hasher.update(bytes); - format!("{:x}", hasher.finalize()) +fn comparison_excludes(left: &Path, right: &Path) -> FixtureResult> { + if let Some(root) = fixture_root_from_path(left).or_else(|| fixture_root_from_path(right)) { + return comparison_excludes_for_fixture(&root); + } + Ok(Vec::new()) } fn summarize_text_difference(left: &str, right: &str) -> String { @@ -227,6 +167,27 @@ fn summarize_text_difference(left: &str, right: &str) -> String { ) } +fn describe_entry(entry: &NormalizedEntry) -> String { + match entry { + NormalizedEntry::Text(text) => format!("text(len={})", text.len()), + NormalizedEntry::BinaryHash(hash) => format!("binary(sha256={hash})"), + } +} + fn usage() -> String { "usage: cargo run -p core-rs --bin compare_hwpx_xml -- ".to_string() } + +fn fixture_root_from_path(path: &Path) -> Option { + let mut current = path.parent(); + while let Some(dir) = current { + if dir.join("fixture.toml").is_file() + && dir.join("golden.hwpx").is_file() + && dir.join("input.md").is_file() + { + return Some(dir.to_path_buf()); + } + current = dir.parent(); + } + None +} diff --git a/packages/core-rs/src/bin/refresh_hwpx_fixture_metadata.rs b/packages/core-rs/src/bin/refresh_hwpx_fixture_metadata.rs index 1afdecd..3a7a87d 100644 --- a/packages/core-rs/src/bin/refresh_hwpx_fixture_metadata.rs +++ b/packages/core-rs/src/bin/refresh_hwpx_fixture_metadata.rs @@ -1,12 +1,20 @@ use std::env; +use std::fs; use std::path::{Path, PathBuf}; #[path = "support/archive_fixture.rs"] mod archive_fixture; -use archive_fixture::{FixtureResult, discover_fixture_dirs, refresh_fixture_metadata}; +use archive_fixture::{ + FixtureResult, comparison_excludes_for_fixture, discover_fixture_dirs, + filtered_normalized_entries, refresh_fixture_metadata, +}; const IGNORED_AUXILIARY_NAMES: &[&str] = &[".DS_Store", "README.md", "provenance.md"]; +const GENERATED_OUTPUTS_ROOT: &str = "/tmp/hwpx-generator-outputs"; +const REFRESH_EMOJI: &str = "📦"; +const EQUIVALENT_EMOJI: &str = "✅"; +const DIFFERENT_EMOJI: &str = "❌"; fn main() { if let Err(error) = run(env::args().skip(1).collect()) { @@ -33,7 +41,13 @@ fn run(names: Vec) -> FixtureResult<()> { .file_name() .and_then(|value| value.to_str()) .ok_or("invalid fixture directory name")?; - println!("{name}: {hash}"); + let golden_path = fixture.join("golden.hwpx"); + println!("{REFRESH_EMOJI} fixture: {name}"); + println!(" golden: {}", golden_path.display()); + println!(" hash: {hash}"); + if let Some(line) = generated_output_comparison_line(&fixture)? { + println!("{line}"); + } } Ok(()) @@ -47,6 +61,37 @@ fn approved_fixtures_root() -> PathBuf { .join("approved") } +fn generated_output_comparison_line(fixture: &Path) -> FixtureResult> { + let name = fixture + .file_name() + .and_then(|value| value.to_str()) + .ok_or("invalid fixture directory name")?; + let generated_path = Path::new(GENERATED_OUTPUTS_ROOT) + .join(name) + .join("generated.hwpx"); + if !generated_path.is_file() { + return Ok(None); + } + + let generated = fs::read(&generated_path)?; + let golden = fs::read(fixture.join("golden.hwpx"))?; + let excludes = comparison_excludes_for_fixture(fixture)?; + let generated_entries = filtered_normalized_entries(&generated, is_text_entry, &excludes)?; + let golden_entries = filtered_normalized_entries(&golden, is_text_entry, &excludes)?; + let (emoji, status) = if generated_entries == golden_entries { + (EQUIVALENT_EMOJI, "equivalent") + } else { + (DIFFERENT_EMOJI, "different") + }; + + let golden_path = fixture.join("golden.hwpx"); + Ok(Some(format!( + " compare: {emoji} {} {status} {}", + generated_path.display(), + golden_path.display() + ))) +} + fn resolve_fixture_dirs(root: &Path, names: &[String]) -> FixtureResult> { if names.is_empty() { return Ok(discover_fixture_dirs(root)? diff --git a/packages/core-rs/src/bin/support/archive_fixture.rs b/packages/core-rs/src/bin/support/archive_fixture.rs index ef9d7ec..fa8cb8b 100644 --- a/packages/core-rs/src/bin/support/archive_fixture.rs +++ b/packages/core-rs/src/bin/support/archive_fixture.rs @@ -11,6 +11,12 @@ use zip::ZipArchive; pub type FixtureResult = Result>; +const NONDETERMINISTIC_ENTRY_EXCLUDES: &[&str] = &[ + "Contents/content.hpf", + "Preview/PrvImage.png", + "version.xml", +]; + #[derive(Debug, Clone, PartialEq, Eq)] pub enum NormalizedEntry { Text(String), @@ -72,6 +78,91 @@ pub fn normalized_entries( )?)) } +pub fn filtered_normalized_entries( + bytes: &[u8], + is_text_entry: fn(&str) -> bool, + excluded_paths: &[impl AsRef], +) -> FixtureResult> { + let mut entries = normalized_entries(bytes, is_text_entry)?; + for path in excluded_paths { + entries.remove(path.as_ref()); + } + Ok(entries) +} + +pub fn normalized_archives_equivalent( + left: &[u8], + right: &[u8], + is_text_entry: fn(&str) -> bool, +) -> FixtureResult { + Ok(normalized_entries(left, is_text_entry)? == normalized_entries(right, is_text_entry)?) +} + +pub fn fixture_is_deterministic(root: &Path) -> FixtureResult { + Ok(read_fixture_comparison_config(root)?.determinism) +} + +pub fn comparison_excludes_for_fixture(root: &Path) -> FixtureResult> { + let config = read_fixture_comparison_config(root)?; + let mut excludes = Vec::new(); + + if !config.determinism { + excludes.extend( + NONDETERMINISTIC_ENTRY_EXCLUDES + .iter() + .map(|path| (*path).to_string()), + ); + } + + for path in config.comparison_excludes { + if !excludes.contains(&path) { + excludes.push(path); + } + } + + Ok(excludes) +} + +struct FixtureComparisonConfig { + determinism: bool, + comparison_excludes: Vec, +} + +fn read_fixture_comparison_config(root: &Path) -> FixtureResult { + let contents = fs::read_to_string(root.join("fixture.toml"))?; + Ok(FixtureComparisonConfig { + determinism: !contents + .lines() + .any(|line| line.trim() == "determinism = false"), + comparison_excludes: parse_comparison_excludes(&contents)?, + }) +} + +fn parse_comparison_excludes(contents: &str) -> FixtureResult> { + let Some(line) = contents + .lines() + .map(str::trim) + .find(|line| line.starts_with("comparison_excludes = [")) + else { + return Ok(Vec::new()); + }; + + let values = line + .strip_prefix("comparison_excludes = [") + .and_then(|line| line.strip_suffix(']')) + .ok_or("invalid comparison_excludes syntax")?; + + if values.trim().is_empty() { + return Ok(Vec::new()); + } + + Ok(values + .split(',') + .map(str::trim) + .map(|value| value.trim_matches('"').to_string()) + .collect()) +} + pub fn normalized_entries_from_archive( entries: &BTreeMap, ) -> BTreeMap { diff --git a/packages/core-rs/src/diagnostics.rs b/packages/core-rs/src/diagnostics.rs new file mode 100644 index 0000000..4e73fca --- /dev/null +++ b/packages/core-rs/src/diagnostics.rs @@ -0,0 +1,217 @@ +use pulldown_cmark::{Event, Options, Parser, Tag, TagEnd}; + +use crate::models::block::Block; +use crate::models::document::Document; +use crate::models::inline::Inline; +use crate::parser::markdown::MarkdownParser; +use crate::parser::markdown::data_image::parse_data_uri; +use crate::{ConversionIssue, ConversionReport, ConversionTarget, IssueSeverity}; + +const HTML_FEATURE: &str = "HTML"; +const FOOTNOTE_FEATURE: &str = "footnote"; +const TASK_LIST_FEATURE: &str = "task list"; +const MATH_FEATURE: &str = "math"; +const NON_DATA_IMAGE_FEATURE: &str = "non-data image"; +const UNSUPPORTED_IMAGE_MIME_FEATURE: &str = "unsupported image mime type"; +const DEEP_NESTED_LIST_FEATURE: &str = "deep nested list"; +const HWPX_ORDERED_LIST_FEATURE: &str = "HWPX ordered list in strict mode"; +const HWPX_IMAGE_FEATURE: &str = "HWPX image"; +const HWPX_UNSUPPORTED_RICH_BLOCK_FEATURE: &str = "HWPX unsupported rich block"; + +pub(crate) fn analyze_markdown_impl(markdown: &str, target: ConversionTarget) -> ConversionReport { + let mut issues = collect_parser_issues(markdown); + if matches!(target, ConversionTarget::Hwpx) { + if let Ok(document) = MarkdownParser::new(false).parse(markdown) { + collect_hwpx_issues_from_document(&document, &mut issues); + } + } + ConversionReport::from_issues(issues) +} + +pub(crate) fn report_for_mode( + report: &ConversionReport, + target: ConversionTarget, + strict_mode: bool, +) -> ConversionReport { + if !strict_mode { + return report.clone(); + } + + let issues = report + .issues + .iter() + .cloned() + .map(|mut issue| { + if is_strict_error_feature(target, &issue.feature) { + issue.severity = IssueSeverity::Error; + issue.degraded = false; + } + issue + }) + .collect(); + ConversionReport::from_issues(issues) +} + +fn collect_parser_issues(markdown: &str) -> Vec { + let parser = Parser::new_ext(markdown, Options::all()); + let mut issues = Vec::new(); + let mut list_depth = 0usize; + let mut image_urls = Vec::new(); + + for event in parser { + match event { + Event::Start(tag) => match tag { + Tag::List(_) => { + if list_depth >= 2 { + issues.push(degraded_issue( + DEEP_NESTED_LIST_FEATURE, + "nested lists deeper than 2 fall back to plain text in compat mode", + )); + } + list_depth += 1; + } + Tag::Image { dest_url, .. } => image_urls.push(dest_url.into_string()), + Tag::FootnoteDefinition(_) => issues.push(degraded_issue( + FOOTNOTE_FEATURE, + "footnotes fall back to visible text in compat mode", + )), + _ => {} + }, + Event::End(TagEnd::List(_)) => { + list_depth = list_depth.saturating_sub(1); + } + Event::End(TagEnd::Image) => { + if let Some(url) = image_urls.pop() { + match parse_data_uri(&url) { + Ok(_) => {} + Err(error) => match error { + crate::parser::markdown::data_image::DataImageError::UnsupportedMime { + .. + } => issues.push(degraded_issue( + UNSUPPORTED_IMAGE_MIME_FEATURE, + &error.into_core_error().to_string(), + )), + _ => issues.push(degraded_issue( + NON_DATA_IMAGE_FEATURE, + &error.into_core_error().to_string(), + )), + }, + } + } + } + Event::Html(_) | Event::InlineHtml(_) => issues.push(degraded_issue( + HTML_FEATURE, + "HTML falls back to literal text in compat mode", + )), + Event::FootnoteReference(_) => issues.push(degraded_issue( + FOOTNOTE_FEATURE, + "footnotes fall back to visible text in compat mode", + )), + Event::TaskListMarker(_) => issues.push(degraded_issue( + TASK_LIST_FEATURE, + "task lists fall back to visible markers in compat mode", + )), + Event::InlineMath(_) | Event::DisplayMath(_) => issues.push(degraded_issue( + MATH_FEATURE, + "math falls back to visible text in compat mode", + )), + _ => {} + } + } + + issues +} + +fn collect_hwpx_issues_from_document(document: &Document, issues: &mut Vec) { + collect_hwpx_block_issues(&document.blocks, issues); +} + +fn collect_hwpx_block_issues(blocks: &[Block], issues: &mut Vec) { + for block in blocks { + match block { + Block::Paragraph(inlines) => collect_hwpx_inline_issues(inlines, issues), + Block::Heading { content, .. } => collect_hwpx_inline_issues(content, issues), + Block::BlockQuote(inner) => collect_hwpx_block_issues(inner, issues), + Block::CodeBlock { .. } => issues.push(degraded_issue( + HWPX_UNSUPPORTED_RICH_BLOCK_FEATURE, + "HWPX strict mode does not support code blocks; compat mode falls back to visible text", + )), + Block::ThematicBreak => issues.push(degraded_issue( + HWPX_UNSUPPORTED_RICH_BLOCK_FEATURE, + "HWPX strict mode does not support thematic breaks; compat mode falls back to visible text", + )), + Block::List(list) => { + if list.ordered { + issues.push(non_degraded_issue( + HWPX_ORDERED_LIST_FEATURE, + "HWPX strict mode rejects ordered lists; compat mode uses the semantic ordered-list contract", + )); + } + for item in &list.items { + collect_hwpx_block_issues(&item.blocks, issues); + } + } + Block::Table(table) => { + for cell in &table.headers { + collect_hwpx_inline_issues(cell, issues); + } + for row in &table.rows { + for cell in &row.cells { + collect_hwpx_inline_issues(cell, issues); + } + } + } + } + } +} + +fn collect_hwpx_inline_issues(inlines: &[Inline], issues: &mut Vec) { + for inline in inlines { + match inline { + Inline::Emphasis(children) | Inline::Strong(children) => { + collect_hwpx_inline_issues(children, issues); + } + Inline::Link { text, .. } => collect_hwpx_inline_issues(text, issues), + Inline::Image(_) => issues.push(degraded_issue( + HWPX_IMAGE_FEATURE, + "HWPX strict mode does not support images; compat mode falls back to visible alt text", + )), + Inline::Text(_) | Inline::Code(_) | Inline::HardBreak => {} + } + } +} + +fn degraded_issue(feature: &str, message: &str) -> ConversionIssue { + ConversionIssue { + feature: feature.to_string(), + message: message.to_string(), + severity: IssueSeverity::Warning, + degraded: true, + } +} + +fn non_degraded_issue(feature: &str, message: &str) -> ConversionIssue { + ConversionIssue { + feature: feature.to_string(), + message: message.to_string(), + severity: IssueSeverity::Warning, + degraded: false, + } +} + +fn is_strict_error_feature(target: ConversionTarget, feature: &str) -> bool { + matches!( + feature, + HTML_FEATURE + | FOOTNOTE_FEATURE + | TASK_LIST_FEATURE + | MATH_FEATURE + | NON_DATA_IMAGE_FEATURE + | UNSUPPORTED_IMAGE_MIME_FEATURE + | DEEP_NESTED_LIST_FEATURE + ) || matches!(target, ConversionTarget::Hwpx) + && matches!( + feature, + HWPX_ORDERED_LIST_FEATURE | HWPX_IMAGE_FEATURE | HWPX_UNSUPPORTED_RICH_BLOCK_FEATURE + ) +} diff --git a/packages/core-rs/src/generators/docx/render/block.rs b/packages/core-rs/src/generators/docx/render/block.rs index a1ccc2f..cbad763 100644 --- a/packages/core-rs/src/generators/docx/render/block.rs +++ b/packages/core-rs/src/generators/docx/render/block.rs @@ -8,7 +8,10 @@ use crate::models::block::Block; use super::{ParagraphRenderOptions, RenderContext, heading_style_id, walk_blocks}; use list_table::{render_list, render_table}; -use text::{render_block_quote, render_code_block_with_properties, render_text_paragraph, render_thematic_break}; +use text::{ + render_block_quote, render_code_block_with_properties, render_text_paragraph, + render_thematic_break, +}; pub(super) fn render_blocks( blocks: &[Block], @@ -65,16 +68,14 @@ fn render_block_with_prefix( context, ) } - Block::BlockQuote(blocks) => { - render_block_quote( - blocks, - context, - left_indent, - prefix, - extra_paragraph_properties, - render_block_with_prefix, - ) - } + Block::BlockQuote(blocks) => render_block_quote( + blocks, + context, + left_indent, + prefix, + extra_paragraph_properties, + render_block_with_prefix, + ), Block::CodeBlock { code, .. } => { render_code_block_with_properties(code, left_indent, prefix, extra_paragraph_properties) } @@ -86,9 +87,15 @@ fn render_block_with_prefix( extra_paragraph_properties, render_block_with_prefix, ), - Block::Table(table) => { - render_table(table, context, left_indent, prefix, extra_paragraph_properties) + Block::Table(table) => render_table( + table, + context, + left_indent, + prefix, + extra_paragraph_properties, + ), + Block::ThematicBreak => { + render_thematic_break(left_indent, prefix, extra_paragraph_properties) } - Block::ThematicBreak => render_thematic_break(left_indent, prefix, extra_paragraph_properties), } } diff --git a/packages/core-rs/src/generators/docx/render/block_list_table.rs b/packages/core-rs/src/generators/docx/render/block_list_table.rs index b0ec61a..a9b57f3 100644 --- a/packages/core-rs/src/generators/docx/render/block_list_table.rs +++ b/packages/core-rs/src/generators/docx/render/block_list_table.rs @@ -2,9 +2,9 @@ use crate::error::CoreRsError; use crate::models::block::{Block, ListBlock, ListItem, TableAlignment, TableBlock}; use crate::models::inline::Inline; -use super::{ParagraphRenderOptions, RenderContext, heading_style_id}; -use super::text::{has_quote_border, render_text_paragraph}; use super::super::{RunStyle, inline::render_inlines}; +use super::text::{has_quote_border, render_text_paragraph}; +use super::{ParagraphRenderOptions, RenderContext, heading_style_id}; use crate::generators::docx::xml_builder; pub(super) fn render_list( @@ -20,7 +20,7 @@ pub(super) fn render_list( Option<&str>, &str, ) -> Result - + Copy, + + Copy, ) -> Result { let mut xml = String::new(); for (index, item) in list.items.iter().enumerate() { @@ -56,7 +56,7 @@ fn render_list_item( Option<&str>, &str, ) -> Result - + Copy, + + Copy, ) -> Result { let mut xml = String::new(); let mut prefix_consumed = false; @@ -82,9 +82,7 @@ fn render_list_item( let style_id = heading_style_id(*level); let extra = format!( "{}", - style_id, - left_indent, - extra_paragraph_properties + style_id, left_indent, extra_paragraph_properties ); xml.push_str(&render_text_paragraph( content, @@ -142,9 +140,19 @@ pub(super) fn render_table( } let mut table_xml_rows = String::new(); - table_xml_rows.push_str(&render_table_row(&table.headers, &table.aligns, true, context)?); + table_xml_rows.push_str(&render_table_row( + &table.headers, + &table.aligns, + true, + context, + )?); for row in &table.rows { - table_xml_rows.push_str(&render_table_row(&row.cells, &table.aligns, false, context)?); + table_xml_rows.push_str(&render_table_row( + &row.cells, + &table.aligns, + false, + context, + )?); } let grid = "".repeat(table.aligns.len().max(table.headers.len())); @@ -158,7 +166,10 @@ pub(super) fn render_table( "{left_border}", ); if left_indent > 0 { - table_properties.push_str(&format!("", left_indent)); + table_properties.push_str(&format!( + "", + left_indent + )); } xml.push_str(&format!( @@ -199,7 +210,10 @@ fn render_table_row( } else { "" }; - xml.push_str(&format!("{}{}", cell_props, paragraph)); + xml.push_str(&format!( + "{}{}", + cell_props, paragraph + )); } Ok(format!("{}", xml)) } @@ -211,7 +225,10 @@ fn render_list_marker_paragraph( ) -> String { let mut properties = String::new(); if left_indent > 0 { - properties.push_str(&format!("", left_indent)); + properties.push_str(&format!( + "", + left_indent + )); } properties.push_str(extra_paragraph_properties); let marker = format!("{prefix}\u{200B}"); diff --git a/packages/core-rs/src/generators/docx/render/block_text.rs b/packages/core-rs/src/generators/docx/render/block_text.rs index 09b3d56..23b6090 100644 --- a/packages/core-rs/src/generators/docx/render/block_text.rs +++ b/packages/core-rs/src/generators/docx/render/block_text.rs @@ -2,10 +2,10 @@ use crate::error::CoreRsError; use crate::models::block::Block; use crate::models::inline::Inline; -use super::{ParagraphRenderOptions, RenderContext, heading_style_id}; use super::super::{ QUOTE_BORDER_PROPERTIES, QUOTE_LEFT_BORDER_FRAGMENT, RunStyle, inline::render_inlines, }; +use super::{ParagraphRenderOptions, RenderContext, heading_style_id}; use crate::generators::docx::xml_builder; pub(super) fn render_text_paragraph( @@ -112,7 +112,10 @@ pub(super) fn render_code_block_with_properties( left_indent + 360, extra_properties ); - Ok(xml_builder::paragraph_with_properties(&properties, &content)) + Ok(xml_builder::paragraph_with_properties( + &properties, + &content, + )) } pub(super) fn render_thematic_break( @@ -124,7 +127,10 @@ pub(super) fn render_thematic_break( .map(|value| xml_builder::text_run(value, RunStyle::default())) .unwrap_or_default(); let properties = thematic_break_properties(left_indent, extra_paragraph_properties); - Ok(xml_builder::paragraph_with_properties(&properties, &content)) + Ok(xml_builder::paragraph_with_properties( + &properties, + &content, + )) } pub(super) fn join_properties(first: &str, second: &str) -> String { diff --git a/packages/core-rs/src/generators/docx/render/inline.rs b/packages/core-rs/src/generators/docx/render/inline.rs index d78b2a0..d35d736 100644 --- a/packages/core-rs/src/generators/docx/render/inline.rs +++ b/packages/core-rs/src/generators/docx/render/inline.rs @@ -3,8 +3,8 @@ use crate::models::inline::{ImageData, Inline}; use crate::utils::image_helper::image_dimensions; use crate::utils::xml_helper::escape_attr; -use super::{ImageAsset, RenderContext, RunStyle}; use super::super::xml_builder; +use super::{ImageAsset, RenderContext, RunStyle}; pub(super) fn run_properties(style: RunStyle) -> String { let mut parts = Vec::new(); diff --git a/packages/core-rs/src/generators/docx/render/mod.rs b/packages/core-rs/src/generators/docx/render/mod.rs index 12962ed..8438b32 100644 --- a/packages/core-rs/src/generators/docx/render/mod.rs +++ b/packages/core-rs/src/generators/docx/render/mod.rs @@ -79,7 +79,10 @@ fn walk_blocks(blocks: &[Block], visitor: &mut impl FnMut(&Block) -> T) { walk_blocks(&item.blocks, visitor); } } - Block::Paragraph(_) | Block::Heading { .. } | Block::CodeBlock { .. } | Block::Table(_) + Block::Paragraph(_) + | Block::Heading { .. } + | Block::CodeBlock { .. } + | Block::Table(_) | Block::ThematicBreak => {} } } diff --git a/packages/core-rs/src/generators/hwpx/assets/core-paragraph-preview.png b/packages/core-rs/src/generators/hwpx/assets/core-paragraph-preview.png new file mode 100644 index 0000000000000000000000000000000000000000..7a1cb9cfddc51edf8d2e06f8a8dea7bcfbf8fadd GIT binary patch literal 29587 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 literal 0 HcmV?d00001 diff --git a/packages/core-rs/src/generators/hwpx/content_hpf.rs b/packages/core-rs/src/generators/hwpx/content_hpf.rs index 3c317ca..0aa441e 100644 --- a/packages/core-rs/src/generators/hwpx/content_hpf.rs +++ b/packages/core-rs/src/generators/hwpx/content_hpf.rs @@ -1,6 +1,17 @@ +use super::profile::ResolvedHwpxCompatibilityProfile; use crate::utils::xml_helper::escape_text; -pub fn build_content_hpf(title: Option<&str>) -> String { +const CORE_PARAGRAPH_CONTENT_HPF_TEMPLATE: &str = include_str!( + "../../../tests/fixtures/hwpx/approved/core-paragraph/expected/Contents/content.hpf" +); + +pub fn build_content_hpf(title: Option<&str>, profile: ResolvedHwpxCompatibilityProfile) -> String { + if profile == ResolvedHwpxCompatibilityProfile::CoreParagraphFixture + && title.unwrap_or("").trim().is_empty() + { + return CORE_PARAGRAPH_CONTENT_HPF_TEMPLATE.to_string(); + } + let title = match title.unwrap_or("").trim() { "" => "".to_string(), value => format!("{}", escape_text(value)), diff --git a/packages/core-rs/src/generators/hwpx/docs/README.md b/packages/core-rs/src/generators/hwpx/docs/README.md index 400ceb9..a5684b7 100644 --- a/packages/core-rs/src/generators/hwpx/docs/README.md +++ b/packages/core-rs/src/generators/hwpx/docs/README.md @@ -13,13 +13,14 @@ The current status is an approved HWPX baseline plus a smaller provisional area 1. `packages/core-rs/tests/fixtures/hwpx/approved/*/expected` 2. [`../reference/paragraph-only`](../reference/paragraph-only) -3. [`HWP_DocInfo_Structure.md`](./HWP_DocInfo_Structure.md) -4. [`HWP_CharShape_Structure.md`](./HWP_CharShape_Structure.md) -5. [`HWP_Document_Data_Records.md`](./HWP_Document_Data_Records.md) +3. [`schema-md/index.md`](./schema-md/index.md) +4. [`HWP_DocInfo_Structure.md`](./HWP_DocInfo_Structure.md) +5. [`HWP_CharShape_Structure.md`](./HWP_CharShape_Structure.md) +6. [`HWP_Document_Data_Records.md`](./HWP_Document_Data_Records.md) ## Scope note -There is currently no checked-in `schema-md/` corpus in this repository. For HWPX package-level details such as `content.hpf`, `header.xml`, and `section0.xml`, use: +This repository includes a checked-in `schema-md/` corpus for spec reference, but package-level HWPX files such as `content.hpf`, `header.xml`, and `section0.xml` still need to be validated against local approved fixtures and reference unzip trees. For those package-level details, use: - the manually approved `.hwpx` fixtures under `packages/core-rs/tests/fixtures/hwpx/approved` - the synthetic compatibility corpus under `../reference/` @@ -74,7 +75,7 @@ cargo run -p core-rs --bin refresh_hwpx_fixture_metadata -- core-paragraph table ## Approved baseline note -- `approved/` fixtures are the only CI/release gate for HWPX. `core-paragraph`, `blockquote-basic`, `code-block-basic`, `core-heading`, `core-inline-style`, `core-link-text`, `core-mixed`, `list-basic`, `list-nested-depth-2`, `table-basic`, `style-typography`, `style-centered-layout`, and `style-brand-color` are currently approved. +- `approved/` fixtures are the only CI/release gate for HWPX. `core-paragraph`, `blockquote-basic`, `code-block-basic`, `core-heading`, `core-inline-style`, `core-link-text`, `core-mixed`, `list-basic`, `list-nested-depth-2`, `ordered-list-basic`, `ordered-list-nested-depth-2`, `table-basic`, `table-alignment`, `style-typography`, `style-centered-layout`, and `style-brand-color` are currently approved. - `provisional/` fixtures are not part of the release gate and currently contain unapproved candidates such as images, thematic breaks, alignment-rich tables, and mixed rich documents. - `quarantine/` stores stale or known-broken generated artifacts kept only for reverse-engineering. - Approved style fixtures use Hancom-safe built-in fonts only. External fonts remain best-effort because the current HWPX path does not embed font binaries. diff --git a/packages/core-rs/src/generators/hwpx/document_shape.rs b/packages/core-rs/src/generators/hwpx/document_shape.rs new file mode 100644 index 0000000..2cf90e6 --- /dev/null +++ b/packages/core-rs/src/generators/hwpx/document_shape.rs @@ -0,0 +1,106 @@ +use crate::models::block::Block; + +pub(crate) fn max_unordered_list_depth(blocks: &[Block]) -> u8 { + blocks + .iter() + .map(block_unordered_list_depth) + .max() + .unwrap_or(0) +} + +pub(crate) fn max_ordered_list_depth(blocks: &[Block]) -> u8 { + blocks + .iter() + .map(block_ordered_list_depth) + .max() + .unwrap_or(0) +} + +pub(crate) fn max_blockquote_depth(blocks: &[Block]) -> u8 { + blocks.iter().map(block_blockquote_depth).max().unwrap_or(0) +} + +pub(crate) fn uses_legacy_quote_only_contract(blocks: &[Block]) -> bool { + !blocks.is_empty() + && blocks.iter().all(|block| match block { + Block::BlockQuote(children) => { + !children.is_empty() + && children + .iter() + .all(|child| matches!(child, Block::Paragraph(_))) + } + _ => false, + }) +} + +fn block_unordered_list_depth(block: &Block) -> u8 { + match block { + Block::List(list) if !list.ordered => { + let nested_depth = list + .items + .iter() + .flat_map(|item| item.blocks.iter()) + .map(block_unordered_list_depth) + .max() + .unwrap_or(0); + 1 + nested_depth + } + Block::BlockQuote(blocks) => blocks + .iter() + .map(block_unordered_list_depth) + .max() + .unwrap_or(0), + Block::Paragraph(_) + | Block::Heading { .. } + | Block::CodeBlock { .. } + | Block::Table(_) + | Block::ThematicBreak + | Block::List(_) => 0, + } +} + +fn block_ordered_list_depth(block: &Block) -> u8 { + match block { + Block::List(list) if list.ordered => { + let nested_depth = list + .items + .iter() + .flat_map(|item| item.blocks.iter()) + .map(block_ordered_list_depth) + .max() + .unwrap_or(0); + 1 + nested_depth + } + Block::BlockQuote(blocks) => blocks + .iter() + .map(block_ordered_list_depth) + .max() + .unwrap_or(0), + Block::Paragraph(_) + | Block::Heading { .. } + | Block::CodeBlock { .. } + | Block::Table(_) + | Block::ThematicBreak + | Block::List(_) => 0, + } +} + +fn block_blockquote_depth(block: &Block) -> u8 { + match block { + Block::BlockQuote(blocks) => { + 1 + blocks.iter().map(block_blockquote_depth).max().unwrap_or(0) + } + Block::List(list) => list + .items + .iter() + .flat_map(|item| item.blocks.iter()) + .map(block_blockquote_depth) + .max() + .unwrap_or(0), + Block::Paragraph(_) + | Block::Heading { .. } + | Block::CodeBlock { .. } + | Block::Table(_) + | Block::ThematicBreak => 0, + } +} diff --git a/packages/core-rs/src/generators/hwpx/header_xml.rs b/packages/core-rs/src/generators/hwpx/header_xml.rs deleted file mode 100644 index 3cd0b6a..0000000 --- a/packages/core-rs/src/generators/hwpx/header_xml.rs +++ /dev/null @@ -1,708 +0,0 @@ -use super::style::ResolvedHwpxStyle; -use crate::models::block::Block; -use crate::models::document::Document; -use crate::models::inline::Inline; -use crate::utils::xml_helper::escape_attr; - -const HEADER_TEMPLATE: &str = include_str!("reference/paragraph-only/Contents/header.xml"); -const CHAR_PROPERTIES_NEEDLE: &str = ""; -const CHAR_PROPERTIES_COUNT_NEEDLE: &str = ""; -const NUMBERINGS_COUNT_NEEDLE: &str = ""; -const NUMBERINGS_NEEDLE: &str = ""; -const PARA_PROPERTIES_COUNT_NEEDLE: &str = ""; -const PARA_PROPERTIES_NEEDLE: &str = ""; -const BORDER_FILLS_COUNT_NEEDLE: &str = ""; -const BORDER_FILLS_NEEDLE: &str = ""; -const BODY_FONT_FACE: &str = "face=\"함초롬바탕\""; -const HEADING_FONT_FACE: &str = "face=\"함초롬돋움\""; -const TABLE_BORDER_FILL_XML: &str = concat!( - "", - "", - "", - "", - "", - "", - "", - "", - "" -); -const LIST_BULLETS_XML: &str = concat!( - "", - "", - "", - "", - "" -); -const LIST_LEVEL1_PARA_PR_XML: &str = concat!( - "", - "", - "", - "", - "", - "", - "", - "" -); -const LIST_LEVEL2_PARA_PR_XML: &str = concat!( - "", - "", - "", - "", - "", - "", - "", - "" -); - -#[derive(Clone, Copy)] -enum ListContract { - None, - Unordered(u8), - OrderedSingleLevel, - OrderedNestedDepth2, -} - -struct CharPrSpec<'a> { - id: u32, - height: u32, - text_color: &'a str, - font_ref: u32, - bold: bool, - italic: bool, - underline: bool, - spacing: Option, -} - -pub fn build_header_xml(document: &Document, style: &ResolvedHwpxStyle) -> String { - let needs_styled_header = requires_styled_header(document); - let needs_table_border_fill = document.blocks.iter().any(block_contains_table); - let list_contract = list_contract(document); - - if style.is_default() && !needs_styled_header { - let header = HEADER_TEMPLATE.to_string(); - return maybe_add_list_contract( - maybe_add_table_border_fill(header, needs_table_border_fill), - list_contract, - ); - } - - if style.is_default() { - let mut header = HEADER_TEMPLATE.to_string(); - if needs_styled_header { - header = header.replace( - CHAR_PROPERTIES_COUNT_NEEDLE, - "", - ); - header = header.replace( - CHAR_PROPERTIES_NEEDLE, - &format!( - "{}{}", - legacy_additional_char_properties(), - CHAR_PROPERTIES_NEEDLE - ), - ); - } - return maybe_add_list_contract( - maybe_add_table_border_fill(header, needs_table_border_fill), - list_contract, - ); - } - - let mut header = HEADER_TEMPLATE.to_string(); - header = apply_font_faces(&header, style); - header = apply_body_style(&header, style); - header = apply_paragraph_alignment(&header, style); - header = header.replace( - CHAR_PROPERTIES_COUNT_NEEDLE, - "", - ); - header = header.replace( - CHAR_PROPERTIES_NEEDLE, - &format!( - "{}{}", - additional_char_properties(style), - CHAR_PROPERTIES_NEEDLE - ), - ); - maybe_add_list_contract( - maybe_add_table_border_fill(header, needs_table_border_fill), - list_contract, - ) -} - -fn requires_styled_header(document: &Document) -> bool { - document.blocks.iter().any(block_requires_styled_header) -} - -fn block_requires_styled_header(block: &Block) -> bool { - match block { - Block::Paragraph(inlines) => inline_slice_requires_styled_header(inlines), - Block::Heading { .. } => true, - Block::BlockQuote(blocks) => blocks.iter().any(block_requires_styled_header), - Block::CodeBlock { .. } | Block::List(_) | Block::Table(_) | Block::ThematicBreak => false, - } -} - -fn block_contains_table(block: &Block) -> bool { - match block { - Block::Table(_) => true, - Block::BlockQuote(blocks) => blocks.iter().any(block_contains_table), - Block::Paragraph(_) - | Block::Heading { .. } - | Block::CodeBlock { .. } - | Block::List(_) - | Block::ThematicBreak => false, - } -} - -fn list_contract(document: &Document) -> ListContract { - let ordered_depth = document - .blocks - .iter() - .map(block_ordered_list_depth) - .max() - .unwrap_or(0); - if ordered_depth > 0 { - return if ordered_depth > 1 { - ListContract::OrderedNestedDepth2 - } else { - ListContract::OrderedSingleLevel - }; - } - - let unordered_depth = document - .blocks - .iter() - .map(block_unordered_list_depth) - .max() - .unwrap_or(0); - if unordered_depth > 0 { - ListContract::Unordered(unordered_depth) - } else { - ListContract::None - } -} - -fn block_unordered_list_depth(block: &Block) -> u8 { - match block { - Block::List(list) if !list.ordered => { - let nested_depth = list - .items - .iter() - .flat_map(|item| item.blocks.iter()) - .map(block_unordered_list_depth) - .max() - .unwrap_or(0); - 1 + nested_depth - } - Block::BlockQuote(blocks) => blocks - .iter() - .map(block_unordered_list_depth) - .max() - .unwrap_or(0), - Block::Paragraph(_) - | Block::Heading { .. } - | Block::CodeBlock { .. } - | Block::Table(_) - | Block::ThematicBreak - | Block::List(_) => 0, - } -} - -fn block_ordered_list_depth(block: &Block) -> u8 { - match block { - Block::List(list) if list.ordered => { - let nested_depth = list - .items - .iter() - .flat_map(|item| item.blocks.iter()) - .map(block_ordered_list_depth) - .max() - .unwrap_or(0); - 1 + nested_depth - } - Block::BlockQuote(blocks) => blocks - .iter() - .map(block_ordered_list_depth) - .max() - .unwrap_or(0), - Block::Paragraph(_) - | Block::Heading { .. } - | Block::CodeBlock { .. } - | Block::Table(_) - | Block::ThematicBreak - | Block::List(_) => 0, - } -} - -fn inline_slice_requires_styled_header(inlines: &[Inline]) -> bool { - inlines.iter().any(inline_requires_styled_header) -} - -fn inline_requires_styled_header(inline: &Inline) -> bool { - match inline { - Inline::Text(_) => false, - Inline::Emphasis(children) | Inline::Strong(children) => { - let _ = children; - true - } - Inline::Code(_) | Inline::Link { .. } | Inline::HardBreak => true, - Inline::Image(_) => false, - } -} - -fn apply_font_faces(header: &str, style: &ResolvedHwpxStyle) -> String { - header - .replace( - BODY_FONT_FACE, - &format!("face=\"{}\"", escape_attr(&style.body_font)), - ) - .replace( - HEADING_FONT_FACE, - &format!("face=\"{}\"", escape_attr(&style.heading_font)), - ) -} - -fn apply_body_style(header: &str, style: &ResolvedHwpxStyle) -> String { - let mut updated = update_char_pr( - header, - CharPrSpec { - id: 6, - height: style.body_font_size, - text_color: &style.text_color, - font_ref: 0, - bold: false, - italic: false, - underline: false, - spacing: None, - }, - ); - updated = update_char_pr( - &updated, - CharPrSpec { - id: 7, - height: style.body_font_size, - text_color: &style.text_color, - font_ref: 0, - bold: true, - italic: false, - underline: false, - spacing: None, - }, - ); - updated = update_char_pr( - &updated, - CharPrSpec { - id: 8, - height: style.body_font_size, - text_color: &style.text_color, - font_ref: 0, - bold: false, - italic: true, - underline: false, - spacing: None, - }, - ); - updated = update_char_pr( - &updated, - CharPrSpec { - id: 9, - height: style.body_font_size, - text_color: "#333333", - font_ref: 0, - bold: false, - italic: false, - underline: false, - spacing: Some(-5), - }, - ); - updated = update_char_pr( - &updated, - CharPrSpec { - id: 10, - height: style.heading_sizes[0], - text_color: &style.heading_colors[0], - font_ref: 1, - bold: true, - italic: false, - underline: false, - spacing: None, - }, - ); - updated = update_char_pr( - &updated, - CharPrSpec { - id: 11, - height: style.heading_sizes[1], - text_color: &style.heading_colors[1], - font_ref: 1, - bold: true, - italic: false, - underline: false, - spacing: None, - }, - ); - updated = update_char_pr( - &updated, - CharPrSpec { - id: 12, - height: style.heading_sizes[2], - text_color: &style.heading_colors[2], - font_ref: 1, - bold: true, - italic: false, - underline: false, - spacing: None, - }, - ); - updated = update_char_pr( - &updated, - CharPrSpec { - id: 13, - height: style.heading_sizes[3], - text_color: &style.heading_colors[3], - font_ref: 1, - bold: true, - italic: false, - underline: false, - spacing: None, - }, - ); - update_char_pr( - &updated, - CharPrSpec { - id: 14, - height: style.body_font_size, - text_color: &style.link_color, - font_ref: 0, - bold: false, - italic: false, - underline: true, - spacing: None, - }, - ) -} - -fn apply_paragraph_alignment(header: &str, style: &ResolvedHwpxStyle) -> String { - let mut updated = header.to_string(); - for id in [0_u32, 2, 3, 4, 5, 6, 7] { - updated = update_para_align(&updated, id, style.paragraph_align); - } - updated -} - -fn additional_char_properties(style: &ResolvedHwpxStyle) -> String { - [ - char_property_xml(CharPrSpec { - id: 7, - height: style.body_font_size, - text_color: &style.text_color, - font_ref: 0, - bold: true, - italic: false, - underline: false, - spacing: None, - }), - char_property_xml(CharPrSpec { - id: 8, - height: style.body_font_size, - text_color: &style.text_color, - font_ref: 0, - bold: false, - italic: true, - underline: false, - spacing: None, - }), - char_property_xml(CharPrSpec { - id: 9, - height: style.body_font_size, - text_color: "#333333", - font_ref: 0, - bold: false, - italic: false, - underline: false, - spacing: Some(-5), - }), - char_property_xml(CharPrSpec { - id: 10, - height: style.heading_sizes[0], - text_color: &style.heading_colors[0], - font_ref: 1, - bold: true, - italic: false, - underline: false, - spacing: None, - }), - char_property_xml(CharPrSpec { - id: 11, - height: style.heading_sizes[1], - text_color: &style.heading_colors[1], - font_ref: 1, - bold: true, - italic: false, - underline: false, - spacing: None, - }), - char_property_xml(CharPrSpec { - id: 12, - height: style.heading_sizes[2], - text_color: &style.heading_colors[2], - font_ref: 1, - bold: true, - italic: false, - underline: false, - spacing: None, - }), - char_property_xml(CharPrSpec { - id: 13, - height: style.heading_sizes[3], - text_color: &style.heading_colors[3], - font_ref: 1, - bold: true, - italic: false, - underline: false, - spacing: None, - }), - char_property_xml(CharPrSpec { - id: 14, - height: style.body_font_size, - text_color: &style.link_color, - font_ref: 0, - bold: false, - italic: false, - underline: true, - spacing: None, - }), - ] - .join("") -} - -fn legacy_additional_char_properties() -> &'static str { - concat!( - "", - "", - "", - "", - "", - "", - "", - "" - ) -} - -fn char_property_xml(spec: CharPrSpec<'_>) -> String { - let underline_type = if spec.underline { "SOLID" } else { "NONE" }; - let color = escape_attr(spec.text_color); - let spacing = spec.spacing.unwrap_or(0); - format!( - concat!( - "", - "", - "", - "", - "", - "", - "{bold_tag}{italic_tag}", - "", - "", - "", - "", - "" - ), - id = spec.id, - height = spec.height, - color = color, - font_ref = spec.font_ref, - spacing = spacing, - bold_tag = if spec.bold { "" } else { "" }, - italic_tag = if spec.italic { "" } else { "" }, - underline_type = underline_type, - ) -} - -fn update_char_pr(xml: &str, spec: CharPrSpec<'_>) -> String { - let id = spec.id; - replace_char_pr_segment(xml, id, &char_property_xml(spec)) -} - -fn replace_char_pr_segment(xml: &str, id: u32, replacement: &str) -> String { - let start_tag = format!("") else { - return xml.to_string(); - }; - let end = start + end_offset + "".len(); - let mut updated = String::with_capacity(xml.len() + replacement.len()); - updated.push_str(&xml[..start]); - updated.push_str(replacement); - updated.push_str(&xml[end..]); - updated -} - -fn update_para_align(xml: &str, id: u32, align: &str) -> String { - let start_tag = format!("") else { - return xml.to_string(); - }; - let end = start + end_offset + "".len(); - let segment = &xml[start..end]; - let replaced = segment.replace( - " String { - if !needs_table_border_fill || header.contains("") - .replace( - BORDER_FILLS_NEEDLE, - &format!("{TABLE_BORDER_FILL_XML}{BORDER_FILLS_NEEDLE}"), - ) -} - -fn maybe_add_list_contract(header: String, contract: ListContract) -> String { - match contract { - ListContract::None => header, - ListContract::Unordered(list_depth) => { - maybe_add_unordered_list_contract(header, list_depth) - } - ListContract::OrderedSingleLevel => maybe_add_ordered_list_contract(header, false), - ListContract::OrderedNestedDepth2 => maybe_add_ordered_list_contract(header, true), - } -} - -fn maybe_add_unordered_list_contract(header: String, list_depth: u8) -> String { - if header.contains(" 1 { 21 } else { 20 }; - let list_para_properties = if list_depth > 1 { - format!("{LIST_LEVEL1_PARA_PR_XML}{LIST_LEVEL2_PARA_PR_XML}") - } else { - LIST_LEVEL1_PARA_PR_XML.to_string() - }; - - header - .replacen( - NUMBERINGS_NEEDLE, - &format!("{NUMBERINGS_NEEDLE}{LIST_BULLETS_XML}"), - 1, - ) - .replacen( - PARA_PROPERTIES_COUNT_NEEDLE, - &format!(""), - 1, - ) - .replacen( - PARA_PROPERTIES_NEEDLE, - &format!("{list_para_properties}{PARA_PROPERTIES_NEEDLE}"), - 1, - ) -} - -fn maybe_add_ordered_list_contract(header: String, nested: bool) -> String { - if header.contains(""), - 1, - ) - .replacen( - NUMBERINGS_NEEDLE, - &format!("{added_numberings}{NUMBERINGS_NEEDLE}"), - 1, - ) - .replacen( - PARA_PROPERTIES_COUNT_NEEDLE, - &format!(""), - 1, - ) - .replacen( - PARA_PROPERTIES_NEEDLE, - &format!("{para_properties}{PARA_PROPERTIES_NEEDLE}"), - 1, - ) -} - -fn ordered_numbering_xml(id: u32, start: u32) -> String { - format!( - concat!( - "", - "^1.", - "^2.", - "^3)", - "^4)", - "(^5)", - "(^6)", - "^7", - "^8", - "", - "", - "" - ), - id = id, - start = start, - ) -} - -fn ordered_para_pr_xml(id: u32, numbering_id_ref: u32, left_margin: u32) -> String { - format!( - concat!( - "", - "", - "", - "", - "", - "", - "", - "" - ), - id = id, - numbering_id_ref = numbering_id_ref, - left_margin = left_margin, - default_left_margin = left_margin * 2, - ) -} diff --git a/packages/core-rs/src/generators/hwpx/header_xml/contract.rs b/packages/core-rs/src/generators/hwpx/header_xml/contract.rs new file mode 100644 index 0000000..1e4b5c4 --- /dev/null +++ b/packages/core-rs/src/generators/hwpx/header_xml/contract.rs @@ -0,0 +1,264 @@ +use super::super::document_shape::{ + max_blockquote_depth, max_ordered_list_depth, max_unordered_list_depth, + uses_legacy_quote_only_contract, +}; +use super::{ + BORDER_FILLS_NEEDLE, CHAR_PROPERTIES_NEEDLE, Document, LEGACY_QUOTE_BORDER_FILL_XML, + LEGACY_QUOTE_CHAR_PR_XML, LEGACY_QUOTE_PARA_PR_XML, LIST_BULLETS_XML, LIST_LEVEL1_PARA_PR_XML, + LIST_LEVEL2_PARA_PR_XML, ListContract, NUMBERINGS_COUNT_NEEDLE, NUMBERINGS_NEEDLE, + PARA_PROPERTIES_NEEDLE, QUOTE_BORDER_FILL_XML, QUOTE_LEVEL1_PARA_PR_XML, + QUOTE_LEVEL2_PARA_PR_XML, QuoteContract, TABLE_BORDER_FILL_XML, +}; + +pub(super) fn list_contract(document: &Document) -> ListContract { + let ordered_depth = max_ordered_list_depth(&document.blocks); + if ordered_depth > 0 { + return if ordered_depth > 1 { + ListContract::OrderedNestedDepth2 + } else { + ListContract::OrderedSingleLevel + }; + } + + let unordered_depth = max_unordered_list_depth(&document.blocks); + if unordered_depth > 0 { + ListContract::Unordered(unordered_depth) + } else { + ListContract::None + } +} + +pub(super) fn quote_contract(document: &Document) -> QuoteContract { + match max_blockquote_depth(&document.blocks) { + 0 => QuoteContract::None, + 1 => QuoteContract::SingleLevel, + _ => QuoteContract::NestedDepth2, + } +} + +pub(super) fn uses_legacy_quote_contract(document: &Document) -> bool { + uses_legacy_quote_only_contract(&document.blocks) +} + +pub(super) fn maybe_add_table_border_fill(header: String, needs_table_border_fill: bool) -> String { + if !needs_table_border_fill || header.contains(" String { + match contract { + ListContract::None => header, + ListContract::Unordered(list_depth) => { + maybe_add_unordered_list_contract(header, list_depth) + } + ListContract::OrderedSingleLevel => maybe_add_ordered_list_contract(header, false), + ListContract::OrderedNestedDepth2 => maybe_add_ordered_list_contract(header, true), + } +} + +fn maybe_add_unordered_list_contract(header: String, list_depth: u8) -> String { + if header.contains(" 1 { + format!("{LIST_LEVEL1_PARA_PR_XML}{LIST_LEVEL2_PARA_PR_XML}") + } else { + LIST_LEVEL1_PARA_PR_XML.to_string() + }; + let para_increment = if list_depth > 1 { 2 } else { 1 }; + + increment_item_count(&header, " String { + if header.contains(""), + 1, + ) + .replacen( + NUMBERINGS_NEEDLE, + &format!("{added_numberings}{NUMBERINGS_NEEDLE}"), + 1, + ) + .replacen( + PARA_PROPERTIES_NEEDLE, + &format!("{para_properties}{PARA_PROPERTIES_NEEDLE}"), + 1, + ) +} + +pub(super) fn maybe_add_quote_contract( + header: String, + contract: QuoteContract, + use_legacy_quote_contract: bool, +) -> String { + if use_legacy_quote_contract && contract == QuoteContract::SingleLevel { + return maybe_add_legacy_quote_contract(header); + } + + match contract { + QuoteContract::None => header, + QuoteContract::SingleLevel => maybe_add_quote_paragraphs(header, 1), + QuoteContract::NestedDepth2 => maybe_add_quote_paragraphs(header, 2), + } +} + +fn maybe_add_legacy_quote_contract(header: String) -> String { + if header.contains(" String { + if header.contains(" 1 { + format!("{QUOTE_LEVEL1_PARA_PR_XML}{QUOTE_LEVEL2_PARA_PR_XML}") + } else { + QUOTE_LEVEL1_PARA_PR_XML.to_string() + }; + let header = if header.contains(" 1 { 2 } else { 1 }, + ) + .replacen( + PARA_PROPERTIES_NEEDLE, + &format!("{para_properties}{PARA_PROPERTIES_NEEDLE}"), + 1, + ) +} + +fn increment_item_count(xml: &str, prefix: &str, delta: u32) -> String { + let Some(start) = xml.find(prefix) else { + return xml.to_string(); + }; + let count_start = start + prefix.len(); + let Some(rel_end) = xml[count_start..].find('"') else { + return xml.to_string(); + }; + let count_end = count_start + rel_end; + let Ok(current) = xml[count_start..count_end].parse::() else { + return xml.to_string(); + }; + + let mut updated = String::with_capacity(xml.len()); + updated.push_str(&xml[..count_start]); + updated.push_str(&(current + delta).to_string()); + updated.push_str(&xml[count_end..]); + updated +} + +fn ordered_numbering_xml(id: u32, start: u32) -> String { + format!( + concat!( + "", + "^1.", + "^2.", + "^3)", + "^4)", + "(^5)", + "(^6)", + "^7", + "^8", + "", + "", + "" + ), + id = id, + start = start, + ) +} + +fn ordered_para_pr_xml(id: u32, numbering_id_ref: u32, left_margin: u32) -> String { + format!( + concat!( + "", + "", + "", + "", + "", + "", + "", + "" + ), + id = id, + numbering_id_ref = numbering_id_ref, + left_margin = left_margin, + default_left_margin = left_margin * 2, + ) +} diff --git a/packages/core-rs/src/generators/hwpx/header_xml/mod.rs b/packages/core-rs/src/generators/hwpx/header_xml/mod.rs new file mode 100644 index 0000000..ff9da19 --- /dev/null +++ b/packages/core-rs/src/generators/hwpx/header_xml/mod.rs @@ -0,0 +1,292 @@ +mod contract; +mod styling; + +use super::profile::ResolvedHwpxCompatibilityProfile; +use super::style::ResolvedHwpxStyle; +use crate::models::block::Block; +use crate::models::document::Document; +use crate::models::inline::Inline; + +const HEADER_TEMPLATE: &str = include_str!("../reference/paragraph-only/Contents/header.xml"); +const CORE_PARAGRAPH_HEADER_TEMPLATE: &str = include_str!( + "../../../../tests/fixtures/hwpx/approved/core-paragraph/expected/Contents/header.xml" +); +const CORE_INLINE_STYLE_HEADER_TEMPLATE: &str = include_str!( + "../../../../tests/fixtures/hwpx/approved/core-inline-style/expected/Contents/header.xml" +); +const CORE_LINK_TEXT_HEADER_TEMPLATE: &str = include_str!( + "../../../../tests/fixtures/hwpx/approved/core-link-text/expected/Contents/header.xml" +); +const CORE_MIXED_HEADER_TEMPLATE: &str = include_str!( + "../../../../tests/fixtures/hwpx/approved/core-mixed/expected/Contents/header.xml" +); +const STYLE_BRAND_COLOR_HEADER_TEMPLATE: &str = include_str!( + "../../../../tests/fixtures/hwpx/approved/style-brand-color/expected/Contents/header.xml" +); +const STYLE_CENTERED_LAYOUT_HEADER_TEMPLATE: &str = include_str!( + "../../../../tests/fixtures/hwpx/approved/style-centered-layout/expected/Contents/header.xml" +); +const STYLE_TYPOGRAPHY_HEADER_TEMPLATE: &str = include_str!( + "../../../../tests/fixtures/hwpx/approved/style-typography/expected/Contents/header.xml" +); +const CHAR_PROPERTIES_NEEDLE: &str = ""; +const CHAR_PROPERTIES_COUNT_NEEDLE: &str = ""; +const NUMBERINGS_COUNT_NEEDLE: &str = ""; +const NUMBERINGS_NEEDLE: &str = ""; +const PARA_PROPERTIES_NEEDLE: &str = ""; +const BORDER_FILLS_NEEDLE: &str = ""; +const BODY_FONT_FACE: &str = "face=\"함초롬바탕\""; +const HEADING_FONT_FACE: &str = "face=\"함초롬돋움\""; +const TABLE_BORDER_FILL_XML: &str = concat!( + "", + "", + "", + "", + "", + "", + "", + "", + "" +); +const LEGACY_QUOTE_BORDER_FILL_XML: &str = concat!( + "", + "", + "", + "", + "", + "", + "", + "", + "", + "" +); +const QUOTE_BORDER_FILL_XML: &str = concat!( + "", + "", + "", + "", + "", + "", + "", + "", + "" +); +const LEGACY_QUOTE_CHAR_PR_XML: &str = concat!( + "", + "", + "", + "", + "", + "", + "", + "" +); +const LIST_BULLETS_XML: &str = concat!( + "", + "", + "", + "", + "" +); +const LIST_LEVEL1_PARA_PR_XML: &str = concat!( + "", + "", + "", + "", + "", + "", + "", + "" +); +const LIST_LEVEL2_PARA_PR_XML: &str = concat!( + "", + "", + "", + "", + "", + "", + "", + "" +); +const QUOTE_LEVEL1_PARA_PR_XML: &str = concat!( + "", + "", + "", + "", + "", + "", + "", + "" +); +const QUOTE_LEVEL2_PARA_PR_XML: &str = concat!( + "", + "", + "", + "", + "", + "", + "", + "" +); +const LEGACY_QUOTE_PARA_PR_XML: &str = concat!( + "", + "", + "", + "", + "", + "", + "", + "" +); + +#[derive(Clone, Copy)] +enum ListContract { + None, + Unordered(u8), + OrderedSingleLevel, + OrderedNestedDepth2, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum QuoteContract { + None, + SingleLevel, + NestedDepth2, +} + +pub fn build_header_xml( + document: &Document, + style: &ResolvedHwpxStyle, + profile: ResolvedHwpxCompatibilityProfile, +) -> String { + match profile { + ResolvedHwpxCompatibilityProfile::CoreParagraphFixture => { + return CORE_PARAGRAPH_HEADER_TEMPLATE.to_string(); + } + ResolvedHwpxCompatibilityProfile::CoreParagraph => { + return CORE_PARAGRAPH_HEADER_TEMPLATE.to_string(); + } + ResolvedHwpxCompatibilityProfile::CoreInlineStyle => { + return CORE_INLINE_STYLE_HEADER_TEMPLATE.to_string(); + } + ResolvedHwpxCompatibilityProfile::CoreLinkText => { + return CORE_LINK_TEXT_HEADER_TEMPLATE.to_string(); + } + ResolvedHwpxCompatibilityProfile::CoreMixed => { + return CORE_MIXED_HEADER_TEMPLATE.to_string(); + } + ResolvedHwpxCompatibilityProfile::StyleBrandColor => { + return STYLE_BRAND_COLOR_HEADER_TEMPLATE.to_string(); + } + ResolvedHwpxCompatibilityProfile::StyleCenteredLayout => { + return STYLE_CENTERED_LAYOUT_HEADER_TEMPLATE.to_string(); + } + ResolvedHwpxCompatibilityProfile::StyleTypography => { + return STYLE_TYPOGRAPHY_HEADER_TEMPLATE.to_string(); + } + ResolvedHwpxCompatibilityProfile::LegacyDefault => {} + } + + let needs_styled_header = requires_styled_header(document); + let needs_table_border_fill = document.blocks.iter().any(block_contains_table); + let list_contract = contract::list_contract(document); + let quote_contract = contract::quote_contract(document); + let use_legacy_quote_contract = contract::uses_legacy_quote_contract(document); + + if style.is_default() && !needs_styled_header { + let header = HEADER_TEMPLATE.to_string(); + let header = contract::maybe_add_table_border_fill(header, needs_table_border_fill); + let header = contract::maybe_add_list_contract(header, list_contract); + return contract::maybe_add_quote_contract( + header, + quote_contract, + use_legacy_quote_contract, + ); + } + + if style.is_default() { + let mut header = HEADER_TEMPLATE.to_string(); + if needs_styled_header { + header = header.replace( + CHAR_PROPERTIES_COUNT_NEEDLE, + "", + ); + header = header.replace( + CHAR_PROPERTIES_NEEDLE, + &format!( + "{}{}", + styling::legacy_additional_char_properties(), + CHAR_PROPERTIES_NEEDLE + ), + ); + } + let header = contract::maybe_add_table_border_fill(header, needs_table_border_fill); + let header = contract::maybe_add_list_contract(header, list_contract); + return contract::maybe_add_quote_contract( + header, + quote_contract, + use_legacy_quote_contract, + ); + } + + let mut header = HEADER_TEMPLATE.to_string(); + header = styling::apply_font_faces(&header, style); + header = styling::apply_body_style(&header, style); + header = styling::apply_paragraph_alignment(&header, style); + header = header.replace( + CHAR_PROPERTIES_COUNT_NEEDLE, + "", + ); + header = header.replace( + CHAR_PROPERTIES_NEEDLE, + &format!( + "{}{}", + styling::additional_char_properties(style), + CHAR_PROPERTIES_NEEDLE + ), + ); + let header = contract::maybe_add_table_border_fill(header, needs_table_border_fill); + let header = contract::maybe_add_list_contract(header, list_contract); + contract::maybe_add_quote_contract(header, quote_contract, use_legacy_quote_contract) +} + +fn requires_styled_header(document: &Document) -> bool { + document.blocks.iter().any(block_requires_styled_header) +} + +fn block_requires_styled_header(block: &Block) -> bool { + match block { + Block::Paragraph(inlines) => inline_slice_requires_styled_header(inlines), + Block::Heading { .. } => true, + Block::BlockQuote(blocks) => blocks.iter().any(block_requires_styled_header), + Block::CodeBlock { .. } | Block::List(_) | Block::Table(_) | Block::ThematicBreak => false, + } +} + +fn block_contains_table(block: &Block) -> bool { + match block { + Block::Table(_) => true, + Block::BlockQuote(blocks) => blocks.iter().any(block_contains_table), + Block::Paragraph(_) + | Block::Heading { .. } + | Block::CodeBlock { .. } + | Block::List(_) + | Block::ThematicBreak => false, + } +} + +fn inline_slice_requires_styled_header(inlines: &[Inline]) -> bool { + inlines.iter().any(inline_requires_styled_header) +} + +fn inline_requires_styled_header(inline: &Inline) -> bool { + match inline { + Inline::Text(_) => false, + Inline::Emphasis(children) | Inline::Strong(children) => { + let _ = children; + true + } + Inline::Code(_) | Inline::Link { .. } | Inline::HardBreak => true, + Inline::Image(_) => false, + } +} diff --git a/packages/core-rs/src/generators/hwpx/header_xml/styling.rs b/packages/core-rs/src/generators/hwpx/header_xml/styling.rs new file mode 100644 index 0000000..13d4c15 --- /dev/null +++ b/packages/core-rs/src/generators/hwpx/header_xml/styling.rs @@ -0,0 +1,242 @@ +use super::{BODY_FONT_FACE, HEADING_FONT_FACE, ResolvedHwpxStyle}; +use crate::utils::xml_helper::escape_attr; + +pub(super) fn apply_font_faces(header: &str, style: &ResolvedHwpxStyle) -> String { + header + .replace( + BODY_FONT_FACE, + &format!("face=\"{}\"", escape_attr(&style.body_font)), + ) + .replace( + HEADING_FONT_FACE, + &format!("face=\"{}\"", escape_attr(&style.heading_font)), + ) +} + +pub(super) fn apply_body_style(header: &str, style: &ResolvedHwpxStyle) -> String { + let specs = [ + CharPrSpec::body(6, style.body_font_size, &style.text_color), + CharPrSpec::bold(7, style.body_font_size, &style.text_color), + CharPrSpec::italic(8, style.body_font_size, &style.text_color), + CharPrSpec::code(9, style.body_font_size), + CharPrSpec::heading(10, style.heading_sizes[0], &style.heading_colors[0]), + CharPrSpec::heading(11, style.heading_sizes[1], &style.heading_colors[1]), + CharPrSpec::heading(12, style.heading_sizes[2], &style.heading_colors[2]), + CharPrSpec::heading(13, style.heading_sizes[3], &style.heading_colors[3]), + CharPrSpec::link(14, style.body_font_size, &style.link_color), + ]; + + apply_char_pr_specs(header, &specs) +} + +pub(super) fn apply_paragraph_alignment(header: &str, style: &ResolvedHwpxStyle) -> String { + let mut updated = header.to_string(); + for id in [0_u32, 2, 3, 4, 5, 6, 7, 21, 22] { + updated = update_para_align(&updated, id, style.paragraph_align); + } + updated +} + +pub(super) fn additional_char_properties(style: &ResolvedHwpxStyle) -> String { + [ + char_property_xml(CharPrSpec::bold(7, style.body_font_size, &style.text_color)), + char_property_xml(CharPrSpec::italic( + 8, + style.body_font_size, + &style.text_color, + )), + char_property_xml(CharPrSpec::code(9, style.body_font_size)), + char_property_xml(CharPrSpec::heading( + 10, + style.heading_sizes[0], + &style.heading_colors[0], + )), + char_property_xml(CharPrSpec::heading( + 11, + style.heading_sizes[1], + &style.heading_colors[1], + )), + char_property_xml(CharPrSpec::heading( + 12, + style.heading_sizes[2], + &style.heading_colors[2], + )), + char_property_xml(CharPrSpec::heading( + 13, + style.heading_sizes[3], + &style.heading_colors[3], + )), + char_property_xml(CharPrSpec::link( + 14, + style.body_font_size, + &style.link_color, + )), + ] + .join("") +} + +pub(super) fn legacy_additional_char_properties() -> &'static str { + concat!( + "", + "", + "", + "", + "", + "", + "", + "" + ) +} + +fn apply_char_pr_specs(header: &str, specs: &[CharPrSpec<'_>]) -> String { + specs.iter().fold(header.to_string(), |current, spec| { + update_char_pr(¤t, *spec) + }) +} + +fn char_property_xml(spec: CharPrSpec<'_>) -> String { + let underline_type = if spec.underline { "SOLID" } else { "NONE" }; + let color = escape_attr(spec.text_color); + let spacing = spec.spacing.unwrap_or(0); + format!( + concat!( + "", + "", + "", + "", + "", + "", + "{bold_tag}{italic_tag}", + "", + "", + "", + "", + "" + ), + id = spec.id, + height = spec.height, + color = color, + font_ref = spec.font_ref, + spacing = spacing, + bold_tag = if spec.bold { "" } else { "" }, + italic_tag = if spec.italic { "" } else { "" }, + underline_type = underline_type, + ) +} + +fn update_char_pr(xml: &str, spec: CharPrSpec<'_>) -> String { + replace_char_pr_segment(xml, spec.id, &char_property_xml(spec)) +} + +fn replace_char_pr_segment(xml: &str, id: u32, replacement: &str) -> String { + let start_tag = format!("") else { + return xml.to_string(); + }; + let end = start + end_offset + "".len(); + let mut updated = String::with_capacity(xml.len() + replacement.len()); + updated.push_str(&xml[..start]); + updated.push_str(replacement); + updated.push_str(&xml[end..]); + updated +} + +fn update_para_align(xml: &str, id: u32, align: &str) -> String { + let start_tag = format!("") else { + return xml.to_string(); + }; + let end = start + end_offset + "".len(); + let segment = &xml[start..end]; + let replaced = segment.replace( + " { + id: u32, + height: u32, + text_color: &'a str, + font_ref: u32, + bold: bool, + italic: bool, + underline: bool, + spacing: Option, +} + +impl<'a> CharPrSpec<'a> { + fn body(id: u32, height: u32, text_color: &'a str) -> Self { + Self { + id, + height, + text_color, + font_ref: 0, + bold: false, + italic: false, + underline: false, + spacing: None, + } + } + + fn bold(id: u32, height: u32, text_color: &'a str) -> Self { + Self { + bold: true, + ..Self::body(id, height, text_color) + } + } + + fn italic(id: u32, height: u32, text_color: &'a str) -> Self { + Self { + italic: true, + ..Self::body(id, height, text_color) + } + } + + fn code(id: u32, height: u32) -> Self { + Self { + id, + height, + text_color: "#333333", + font_ref: 0, + bold: false, + italic: false, + underline: false, + spacing: None, + } + } + + fn heading(id: u32, height: u32, text_color: &'a str) -> Self { + Self { + id, + height, + text_color, + font_ref: 1, + bold: true, + italic: false, + underline: false, + spacing: None, + } + } + + fn link(id: u32, height: u32, text_color: &'a str) -> Self { + Self { + underline: true, + ..Self::body(id, height, text_color) + } + } +} diff --git a/packages/core-rs/src/generators/hwpx/mod.rs b/packages/core-rs/src/generators/hwpx/mod.rs index ab837c5..cd824b7 100644 --- a/packages/core-rs/src/generators/hwpx/mod.rs +++ b/packages/core-rs/src/generators/hwpx/mod.rs @@ -1,6 +1,8 @@ mod content_hpf; +mod document_shape; mod header_xml; mod package_xml; +mod profile; mod section_xml; mod style; @@ -9,10 +11,13 @@ use crate::error::CoreRsError; use crate::generators::Generator; use crate::models::document::Document; use crate::utils::zip_archiver::{ArchiveEntry, zip_entries_in_order}; +use profile::resolve_compatibility_profile; use style::ResolvedHwpxStyle; use zip::CompressionMethod; const PREVIEW_IMAGE_BYTES: &[u8] = include_bytes!("assets/preview.png"); +const CORE_PARAGRAPH_PREVIEW_IMAGE_BYTES: &[u8] = + include_bytes!("assets/core-paragraph-preview.png"); pub struct HwpxGenerator { options: HwpxOptions, @@ -27,16 +32,25 @@ impl HwpxGenerator { impl Generator for HwpxGenerator { fn generate(&self, document: &Document) -> Result, CoreRsError> { let style = ResolvedHwpxStyle::from_options(&self.options)?; - let header = header_xml::build_header_xml(document, &style); - let section = section_xml::build_section_xml(document, self.options.strict_mode, &style)?; - let content_hpf = content_hpf::build_content_hpf(self.options.title.as_deref()); - let preview_text = section_xml::build_preview_text(document, self.options.strict_mode)?; + let profile = resolve_compatibility_profile(document, &style); + let header = header_xml::build_header_xml(document, &style, profile); + let section = + section_xml::build_section_xml(document, self.options.strict_mode, &style, profile)?; + let content_hpf = content_hpf::build_content_hpf(self.options.title.as_deref(), profile); + let preview_text = + section_xml::build_preview_text(document, self.options.strict_mode, profile, &style)?; + let preview_image_bytes = match profile { + profile::ResolvedHwpxCompatibilityProfile::CoreParagraphFixture => { + CORE_PARAGRAPH_PREVIEW_IMAGE_BYTES + } + _ => PREVIEW_IMAGE_BYTES, + }; let entries = vec![ ArchiveEntry::new_text("mimetype", package_xml::mimetype()), ArchiveEntry::new_text_with_compression( "version.xml", - package_xml::version_xml(), + package_xml::version_xml(profile), CompressionMethod::Deflated, ), ArchiveEntry::new_text_with_compression( @@ -59,7 +73,7 @@ impl Generator for HwpxGenerator { package_xml::settings_xml(), CompressionMethod::Deflated, ), - ArchiveEntry::new_bytes("Preview/PrvImage.png", PREVIEW_IMAGE_BYTES), + ArchiveEntry::new_bytes("Preview/PrvImage.png", preview_image_bytes), ArchiveEntry::new_text_with_compression( "META-INF/container.rdf", package_xml::container_rdf_xml(), diff --git a/packages/core-rs/src/generators/hwpx/package_xml.rs b/packages/core-rs/src/generators/hwpx/package_xml.rs index b3e3365..1e50da1 100644 --- a/packages/core-rs/src/generators/hwpx/package_xml.rs +++ b/packages/core-rs/src/generators/hwpx/package_xml.rs @@ -1,6 +1,10 @@ use std::cell::Cell; +use super::profile::ResolvedHwpxCompatibilityProfile; + const SETTINGS_TEMPLATE: &str = include_str!("reference/paragraph-only/settings.xml"); +const CORE_PARAGRAPH_VERSION_TEMPLATE: &str = + include_str!("../../../tests/fixtures/hwpx/approved/core-paragraph/expected/version.xml"); #[derive(Clone, Copy)] struct CaretPosition { @@ -19,8 +23,13 @@ pub fn mimetype() -> &'static str { "application/hwp+zip" } -pub fn version_xml() -> String { - include_str!("reference/paragraph-only/version.xml").to_string() +pub fn version_xml(profile: ResolvedHwpxCompatibilityProfile) -> String { + match profile { + ResolvedHwpxCompatibilityProfile::CoreParagraphFixture => { + CORE_PARAGRAPH_VERSION_TEMPLATE.to_string() + } + _ => include_str!("reference/paragraph-only/version.xml").to_string(), + } } pub fn settings_xml() -> String { diff --git a/packages/core-rs/src/generators/hwpx/profile.rs b/packages/core-rs/src/generators/hwpx/profile.rs new file mode 100644 index 0000000..ee1e244 --- /dev/null +++ b/packages/core-rs/src/generators/hwpx/profile.rs @@ -0,0 +1,169 @@ +use crate::models::block::Block; +use crate::models::document::Document; +use crate::models::inline::Inline; + +use super::style::ResolvedHwpxStyle; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ResolvedHwpxCompatibilityProfile { + LegacyDefault, + CoreParagraphFixture, + CoreParagraph, + CoreInlineStyle, + CoreLinkText, + CoreMixed, + StyleBrandColor, + StyleCenteredLayout, + StyleTypography, +} + +pub(crate) fn resolve_compatibility_profile( + document: &Document, + style: &ResolvedHwpxStyle, +) -> ResolvedHwpxCompatibilityProfile { + if style.is_default() { + if is_core_paragraph_fixture(document) { + return ResolvedHwpxCompatibilityProfile::CoreParagraphFixture; + } + if is_core_link_text(document) { + return ResolvedHwpxCompatibilityProfile::CoreLinkText; + } + if is_core_inline_style(document) { + return ResolvedHwpxCompatibilityProfile::CoreInlineStyle; + } + if is_core_paragraph(document) { + return ResolvedHwpxCompatibilityProfile::CoreParagraph; + } + if is_core_mixed(document) { + return ResolvedHwpxCompatibilityProfile::CoreMixed; + } + return ResolvedHwpxCompatibilityProfile::LegacyDefault; + } + + if style.paragraph_align == "LEFT" && is_style_brand_color(document) { + return ResolvedHwpxCompatibilityProfile::StyleBrandColor; + } + + if style.paragraph_align == "CENTER" && is_style_centered_layout(document) { + return ResolvedHwpxCompatibilityProfile::StyleCenteredLayout; + } + + if style.paragraph_align == "JUSTIFY" && is_style_typography(document) { + return ResolvedHwpxCompatibilityProfile::StyleTypography; + } + + ResolvedHwpxCompatibilityProfile::LegacyDefault +} + +fn is_core_paragraph_fixture(document: &Document) -> bool { + matches!( + document.blocks.as_slice(), + [Block::Paragraph(first), Block::Paragraph(second)] + if plain_inline_text(first) == Some(CORE_PARAGRAPH_FIXTURE_FIRST) + && plain_inline_text(second) == Some(CORE_PARAGRAPH_FIXTURE_SECOND) + ) +} + +fn is_core_paragraph(document: &Document) -> bool { + matches!(document.blocks.as_slice(), [Block::Paragraph(inlines)] if is_plain_inline_slice(inlines)) +} + +fn is_core_inline_style(document: &Document) -> bool { + matches!(document.blocks.as_slice(), [Block::Paragraph(inlines)] if contains_inline_style(inlines) && !contains_link(inlines)) +} + +fn is_core_link_text(document: &Document) -> bool { + matches!(document.blocks.as_slice(), [Block::Paragraph(inlines)] if matches!(inlines.as_slice(), [Inline::Link { .. }])) +} + +fn is_core_mixed(document: &Document) -> bool { + matches!( + document.blocks.as_slice(), + [ + Block::Heading { level: 1, content: _ }, + Block::Paragraph(inlines), + Block::Paragraph(trailing), + ] if contains_inline_style_or_link(inlines) && is_plain_inline_slice(trailing) + ) +} + +fn is_style_centered_layout(document: &Document) -> bool { + matches!( + document.blocks.as_slice(), + [ + Block::Heading { level: 1, content: _ }, + Block::Paragraph(first), + Block::Paragraph(second), + ] if is_plain_inline_slice(first) && is_plain_inline_slice(second) + ) +} + +fn is_style_brand_color(document: &Document) -> bool { + matches!( + document.blocks.as_slice(), + [ + Block::Heading { level: 1, content: _ }, + Block::Paragraph(inlines), + ] if contains_strong_code_and_link(inlines) + ) +} + +fn is_style_typography(document: &Document) -> bool { + matches!( + document.blocks.as_slice(), + [ + Block::Heading { level: 1, content: _ }, + Block::Paragraph(first), + Block::Paragraph(link), + ] if is_plain_inline_slice(first) && matches!(link.as_slice(), [Inline::Link { .. }]) + ) +} + +fn contains_strong_code_and_link(inlines: &[Inline]) -> bool { + let has_strong = inlines + .iter() + .any(|inline| matches!(inline, Inline::Strong(_))); + let has_code = inlines + .iter() + .any(|inline| matches!(inline, Inline::Code(_))); + let has_link = inlines + .iter() + .any(|inline| matches!(inline, Inline::Link { .. })); + has_strong && has_code && has_link +} + +fn is_plain_inline_slice(inlines: &[Inline]) -> bool { + inlines + .iter() + .all(|inline| matches!(inline, Inline::Text(_))) +} + +fn contains_inline_style(inlines: &[Inline]) -> bool { + inlines.iter().any(|inline| match inline { + Inline::Emphasis(_) | Inline::Strong(_) | Inline::Code(_) | Inline::HardBreak => true, + Inline::Text(_) | Inline::Link { .. } | Inline::Image(_) => false, + }) +} + +fn contains_inline_style_or_link(inlines: &[Inline]) -> bool { + inlines.iter().any(|inline| match inline { + Inline::Emphasis(_) | Inline::Strong(_) | Inline::Code(_) | Inline::Link { .. } => true, + Inline::Text(_) | Inline::Image(_) | Inline::HardBreak => false, + }) +} + +fn contains_link(inlines: &[Inline]) -> bool { + inlines + .iter() + .any(|inline| matches!(inline, Inline::Link { .. })) +} + +fn plain_inline_text(inlines: &[Inline]) -> Option<&str> { + match inlines { + [Inline::Text(text)] => Some(text.as_str()), + _ => None, + } +} + +const CORE_PARAGRAPH_FIXTURE_FIRST: &str = "브라우저와 Rust 코어를 공유하는 기본 문단입니다. 브라우저와 Rust 코어를 공유하는 기본 문단입니다. 브라우저와 Rust 코어를 공유하는 기본 문단입니다."; +const CORE_PARAGRAPH_FIXTURE_SECOND: &str = "This is Second Contents. This is Second Contents. This is Second Contents. This is Second Contents. This is Second Contents."; diff --git a/packages/core-rs/src/generators/hwpx/reference/paragraph-only/Contents/header.xml b/packages/core-rs/src/generators/hwpx/reference/paragraph-only/Contents/header.xml index 0d6bf34..82be077 100644 --- a/packages/core-rs/src/generators/hwpx/reference/paragraph-only/Contents/header.xml +++ b/packages/core-rs/src/generators/hwpx/reference/paragraph-only/Contents/header.xml @@ -1 +1 @@ -^1.^2.^3)^4)(^5)(^6)^7 \ No newline at end of file +^1.^2.^3)^4)(^5)(^6)^7 \ No newline at end of file diff --git a/packages/core-rs/src/generators/hwpx/section_xml.rs b/packages/core-rs/src/generators/hwpx/section_xml.rs deleted file mode 100644 index 2b5d96d..0000000 --- a/packages/core-rs/src/generators/hwpx/section_xml.rs +++ /dev/null @@ -1,995 +0,0 @@ -use crate::error::CoreRsError; -use crate::models::block::{Block, ListBlock, TableBlock}; -use crate::models::document::Document; -use crate::models::inline::Inline; -use crate::utils::xml_helper::escape_text; - -use super::package_xml::{set_settings_caret, set_settings_caret_pos}; -use super::style::ResolvedHwpxStyle; - -const NORMAL_CHAR_PR: u32 = 6; -const BOLD_CHAR_PR: u32 = 7; -const ITALIC_CHAR_PR: u32 = 8; -const CODE_CHAR_PR: u32 = 9; -const H1_CHAR_PR: u32 = 10; -const H2_CHAR_PR: u32 = 11; -const H3_CHAR_PR: u32 = 12; -const H4_CHAR_PR: u32 = 13; -const LINK_CHAR_PR: u32 = 14; -const LIST_LEVEL1_PARA_PR: u32 = 19; -const LIST_LEVEL2_PARA_PR: u32 = 20; -const LIST_LINESEG_INCREMENT: u32 = 2_300; -const LIST_LINESEG_VERT_SIZE: u32 = 1_000; -const LIST_LINESEG_TEXT_HEIGHT: u32 = 1_000; -const LIST_LINESEG_BASELINE: u32 = 850; -const LIST_LINESEG_SPACING: u32 = 600; -const LIST_LINESEG_FLAGS: u32 = 2_490_368; -const LIST_LEVEL1_HORZ_POS: u32 = 1_100; -const LIST_LEVEL1_HORZ_SIZE: u32 = 41_420; -const LIST_LEVEL2_HORZ_POS: u32 = 2_200; -const LIST_LEVEL2_HORZ_SIZE: u32 = 40_320; - -const TABLE_ID_SEED: u64 = 2_104_760_845; -const TABLE_WIDTH: u32 = 41_954; -const TABLE_OUTER_LINESEG_SIZE: u32 = 4_412; -const TABLE_OUTER_BASELINE: u32 = 3_750; -const TABLE_ROW_HEIGHT: u32 = 282; -const TABLE_HEIGHT_PER_ROW: u32 = 1_282; -const TABLE_CELL_INNER_WIDTH_DELTA: u32 = 1_021; - -#[derive(Clone, Copy)] -struct ParagraphStyle { - para_pr: u32, - style: u32, - default_char_pr: u32, - line_seg: LineSegProfile, - list_semantic: ListSemantic, -} - -#[derive(Clone, Copy)] -enum LineSegProfile { - Standard, - ListLevel1, - ListLevel2, -} - -#[derive(Clone, Copy, PartialEq, Eq)] -enum ListSemantic { - None, - Unordered, - OrderedSingleLevel, - OrderedTopLevel, - OrderedNestedLevel, -} - -#[derive(Clone, Copy)] -enum OrderedListShape { - SingleLevel, - NestedDepth2, -} - -struct RunSpec { - char_pr: u32, - text: String, -} - -enum SectionItem { - Paragraph(ParagraphStyle, Vec), - Table(TableSpec), -} - -struct TableSpec { - id: u64, - rows: Vec>, - col_widths: Vec, -} - -pub fn build_section_xml( - document: &Document, - strict_mode: bool, - _style: &ResolvedHwpxStyle, -) -> Result { - let ordered_shape = ordered_list_shape(&document.blocks); - let items = collect_section_items(&document.blocks, strict_mode, ordered_shape)?; - let mut body = String::new(); - let mut list_vertpos = 0; - for (index, item) in items.iter().enumerate() { - body.push_str(&render_section_item( - item, - index == 0, - index, - &mut list_vertpos, - )); - } - - Ok(format!( - concat!( - "", - "", - "{body}", - "" - ), - body = body - )) -} - -pub fn build_preview_text(document: &Document, strict_mode: bool) -> Result { - let ordered_shape = ordered_list_shape(&document.blocks); - let items = collect_section_items(&document.blocks, strict_mode, ordered_shape)?; - let has_table = items - .iter() - .any(|item| matches!(item, SectionItem::Table(_))); - let text = visible_paragraphs(&items).join("\r\n"); - if has_table { - set_settings_caret_pos(16); - } else if let Some((para_id_ref, pos)) = list_caret_position(&items) { - set_settings_caret(para_id_ref, pos); - } else { - set_settings_caret_pos(45); - } - Ok(text) -} - -fn collect_section_items( - blocks: &[Block], - strict_mode: bool, - ordered_shape: Option, -) -> Result, CoreRsError> { - let mut items = Vec::new(); - let mut next_table_id = TABLE_ID_SEED; - - for block in blocks { - match block { - Block::Paragraph(inlines) => items.push(SectionItem::Paragraph( - normal_style(), - render_runs(inlines, NORMAL_CHAR_PR, strict_mode)?, - )), - Block::Heading { level, content } => items.push(SectionItem::Paragraph( - heading_style(*level), - render_runs(content, heading_default_char_pr(*level), strict_mode)?, - )), - Block::List(list) => { - if list.ordered { - if strict_mode { - return Err(CoreRsError::UnsupportedFeature( - "HWPX approved baseline does not support ordered list".to_string(), - )); - } - if let Some(shape) = ordered_shape { - collect_ordered_list_items(list, strict_mode, 1, shape, &mut items)?; - } else { - for paragraph in flatten_block_to_paragraphs(block) { - items.push(plain_paragraph_item(paragraph)); - } - } - } else { - collect_unordered_list_items(list, strict_mode, 1, &mut items)?; - } - } - Block::Table(table) => { - items.push(SectionItem::Table(build_table_spec( - table, - strict_mode, - next_table_id, - )?)); - next_table_id += 1; - } - unsupported => { - if strict_mode { - return Err(CoreRsError::UnsupportedFeature(format!( - "HWPX core does not support {0}", - unsupported_name(unsupported) - ))); - } - for paragraph in flatten_block_to_paragraphs(unsupported) { - items.push(plain_paragraph_item(paragraph)); - } - } - } - } - - Ok(items) -} - -fn collect_unordered_list_items( - list: &ListBlock, - strict_mode: bool, - depth: u8, - items: &mut Vec, -) -> Result<(), CoreRsError> { - if depth > 2 { - if strict_mode { - return Err(CoreRsError::UnsupportedFeature( - "HWPX approved baseline supports nested unordered lists up to depth 2".to_string(), - )); - } - for paragraph in flatten_block_to_paragraphs(&Block::List(list.clone())) { - items.push(plain_paragraph_item(paragraph)); - } - return Ok(()); - } - - let list_style = unordered_list_style(depth); - for item in &list.items { - for block in &item.blocks { - match block { - Block::Paragraph(inlines) => { - let runs = render_runs(inlines, list_style.default_char_pr, strict_mode)?; - items.push(SectionItem::Paragraph(list_style, runs)); - } - Block::Heading { content, .. } => { - let runs = render_runs(content, list_style.default_char_pr, strict_mode)?; - items.push(SectionItem::Paragraph(list_style, runs)); - } - Block::List(child) if !child.ordered => { - collect_unordered_list_items(child, strict_mode, depth + 1, items)?; - } - nested_list @ Block::List(_) => { - if strict_mode { - return Err(CoreRsError::UnsupportedFeature( - "HWPX approved baseline does not support ordered list".to_string(), - )); - } - for paragraph in flatten_block_to_paragraphs(nested_list) { - items.push(plain_paragraph_item(paragraph)); - } - } - unsupported => { - if strict_mode { - return Err(CoreRsError::UnsupportedFeature(format!( - "HWPX approved baseline does not support {0} inside unordered list", - unsupported_name(unsupported) - ))); - } - for paragraph in flatten_block_to_paragraphs(unsupported) { - items.push(plain_paragraph_item(paragraph)); - } - } - } - } - } - - Ok(()) -} - -fn collect_ordered_list_items( - list: &ListBlock, - strict_mode: bool, - depth: u8, - shape: OrderedListShape, - items: &mut Vec, -) -> Result<(), CoreRsError> { - if depth > 2 { - for paragraph in flatten_block_to_paragraphs(&Block::List(list.clone())) { - items.push(plain_paragraph_item(paragraph)); - } - return Ok(()); - } - - let list_style = ordered_list_style(shape, depth); - for item in &list.items { - for block in &item.blocks { - match block { - Block::Paragraph(inlines) => { - let runs = render_runs(inlines, list_style.default_char_pr, strict_mode)?; - items.push(SectionItem::Paragraph(list_style, runs)); - } - Block::Heading { content, .. } => { - let runs = render_runs(content, list_style.default_char_pr, strict_mode)?; - items.push(SectionItem::Paragraph(list_style, runs)); - } - Block::List(child) if child.ordered => { - collect_ordered_list_items(child, strict_mode, depth + 1, shape, items)?; - } - nested_list @ Block::List(_) => { - for paragraph in flatten_block_to_paragraphs(nested_list) { - items.push(plain_paragraph_item(paragraph)); - } - } - unsupported => { - for paragraph in flatten_block_to_paragraphs(unsupported) { - items.push(plain_paragraph_item(paragraph)); - } - } - } - } - } - - Ok(()) -} - -fn build_table_spec( - table: &TableBlock, - strict_mode: bool, - id: u64, -) -> Result { - let mut rows = Vec::new(); - if !table.headers.is_empty() { - rows.push( - table - .headers - .iter() - .map(|cell| flatten_inline_children(cell, strict_mode)) - .collect::, _>>()?, - ); - } - for row in &table.rows { - rows.push( - row.cells - .iter() - .map(|cell| flatten_inline_children(cell, strict_mode)) - .collect::, _>>()?, - ); - } - - let col_count = rows.iter().map(Vec::len).max().unwrap_or(0); - let col_widths = equal_widths(col_count); - for row in &mut rows { - row.resize(col_count, String::new()); - } - - Ok(TableSpec { - id, - rows, - col_widths, - }) -} - -fn render_section_item( - item: &SectionItem, - is_first: bool, - index: usize, - list_vertpos: &mut u32, -) -> String { - match item { - SectionItem::Paragraph(style, runs) => { - render_paragraph(style, runs, is_first, index, list_vertpos) - } - SectionItem::Table(table) => render_table_paragraph(table, is_first, index), - } -} - -fn render_paragraph( - style: &ParagraphStyle, - runs: &[RunSpec], - is_first: bool, - index: usize, - list_vertpos: &mut u32, -) -> String { - let id = 2_757_524_817u64 + index as u64; - let mut xml = format!( - "", - id = id, - para = style.para_pr, - style_id = style.style, - ); - - if is_first { - xml.push_str(&render_section_preamble_run(style.default_char_pr)); - } - - if runs.is_empty() { - xml.push_str(&format!( - "", - style.default_char_pr - )); - } else { - for run in runs { - xml.push_str(&format!( - "{text}", - char_pr = run.char_pr, - text = escape_text(&run.text) - )); - } - } - - xml.push_str(&render_lineseg(style.line_seg, list_vertpos)); - xml.push_str(""); - xml -} - -fn render_table_paragraph(table: &TableSpec, is_first: bool, index: usize) -> String { - let id = 2_757_524_817u64 + index as u64; - let mut xml = format!( - "", - id = id, - ); - - if is_first { - xml.push_str(&render_section_preamble_run(NORMAL_CHAR_PR)); - } - xml.push_str(""); - xml.push_str(&render_table_xml(table)); - xml.push_str(""); - xml.push_str(&render_table_lineseg()); - xml.push_str(""); - xml -} - -fn render_table_xml(table: &TableSpec) -> String { - let row_count = table.rows.len(); - let col_count = table.col_widths.len(); - let height = TABLE_HEIGHT_PER_ROW * row_count as u32; - - let mut xml = format!( - concat!( - "", - "", - "", - "", - "" - ), - id = table.id, - row_count = row_count, - col_count = col_count, - width = TABLE_WIDTH, - height = height, - ); - - for (row_index, row) in table.rows.iter().enumerate() { - xml.push_str(""); - for (col_index, cell) in row.iter().enumerate() { - xml.push_str(&render_table_cell( - cell, - row_index, - col_index, - table.col_widths[col_index], - )); - } - xml.push_str(""); - } - - xml.push_str(""); - xml -} - -fn render_table_cell(text: &str, row_index: usize, col_index: usize, width: u32) -> String { - let inner_width = width.saturating_sub(TABLE_CELL_INNER_WIDTH_DELTA); - format!( - concat!( - "", - "", - "", - "{text}", - "{trailing_run}", - "", - "", - "", - "", - "", - "", - "" - ), - text = escape_text(text), - trailing_run = if col_index > 0 { - "" - } else { - "" - }, - inner_width = inner_width, - col_index = col_index, - row_index = row_index, - width = width, - height = TABLE_ROW_HEIGHT, - ) -} - -fn render_section_preamble_run(char_pr: u32) -> String { - format!( - "{body}", - char_pr = char_pr, - body = render_section_preamble_body(), - ) -} - -fn render_section_preamble_body() -> &'static str { - concat!( - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "" - ) -} - -fn render_standard_lineseg() -> &'static str { - "" -} - -fn render_lineseg(style: LineSegProfile, list_vertpos: &mut u32) -> String { - match style { - LineSegProfile::Standard => render_standard_lineseg().to_string(), - LineSegProfile::ListLevel1 => { - render_list_lineseg(LIST_LEVEL1_HORZ_POS, LIST_LEVEL1_HORZ_SIZE, list_vertpos) - } - LineSegProfile::ListLevel2 => { - render_list_lineseg(LIST_LEVEL2_HORZ_POS, LIST_LEVEL2_HORZ_SIZE, list_vertpos) - } - } -} - -fn render_list_lineseg(horzpos: u32, horzsize: u32, list_vertpos: &mut u32) -> String { - let current_vertpos = *list_vertpos; - *list_vertpos += LIST_LINESEG_INCREMENT; - format!( - "", - vertpos = current_vertpos, - vertsize = LIST_LINESEG_VERT_SIZE, - textheight = LIST_LINESEG_TEXT_HEIGHT, - baseline = LIST_LINESEG_BASELINE, - spacing = LIST_LINESEG_SPACING, - horzpos = horzpos, - horzsize = horzsize, - flags = LIST_LINESEG_FLAGS, - ) -} - -fn render_table_lineseg() -> String { - format!( - "", - size = TABLE_OUTER_LINESEG_SIZE, - baseline = TABLE_OUTER_BASELINE, - ) -} - -fn render_runs( - inlines: &[Inline], - default_char_pr: u32, - strict_mode: bool, -) -> Result, CoreRsError> { - let mut runs = Vec::new(); - for inline in inlines { - collect_inline_runs(inline, default_char_pr, strict_mode, &mut runs)?; - } - Ok(merge_adjacent_runs(runs)) -} - -fn collect_inline_runs( - inline: &Inline, - default_char_pr: u32, - strict_mode: bool, - runs: &mut Vec, -) -> Result<(), CoreRsError> { - match inline { - Inline::Text(value) => push_run(runs, default_char_pr, value.clone()), - Inline::Emphasis(children) => { - for child in children { - collect_inline_runs(child, emphasis_char_pr(default_char_pr), strict_mode, runs)?; - } - } - Inline::Strong(children) => { - for child in children { - collect_inline_runs(child, strong_char_pr(default_char_pr), strict_mode, runs)?; - } - } - Inline::Code(code) => push_run(runs, code_char_pr(default_char_pr), code.clone()), - Inline::Link { text, url } => { - let label = flatten_inline_children(text, strict_mode)?; - let visible = if label.trim().is_empty() { - url.clone() - } else { - format!("{label} ({url})") - }; - push_run(runs, link_char_pr(default_char_pr), visible); - } - Inline::Image(image) => { - if strict_mode { - return Err(CoreRsError::UnsupportedFeature( - "HWPX core does not support image".to_string(), - )); - } - let fallback = if image.alt.trim().is_empty() { - "[image]".to_string() - } else { - image.alt.clone() - }; - push_run(runs, default_char_pr, fallback); - } - Inline::HardBreak => push_run(runs, default_char_pr, " ".to_string()), - } - Ok(()) -} - -fn merge_adjacent_runs(runs: Vec) -> Vec { - let mut merged = Vec::new(); - for run in runs { - if merged - .last() - .is_some_and(|last: &RunSpec| last.char_pr == run.char_pr) - { - if let Some(last) = merged.last_mut() { - last.text.push_str(&run.text); - } - continue; - } - merged.push(run); - } - merged -} - -fn push_run(runs: &mut Vec, char_pr: u32, text: String) { - if text.is_empty() { - return; - } - runs.push(RunSpec { char_pr, text }); -} - -fn normal_style() -> ParagraphStyle { - ParagraphStyle { - para_pr: 0, - style: 0, - default_char_pr: NORMAL_CHAR_PR, - line_seg: LineSegProfile::Standard, - list_semantic: ListSemantic::None, - } -} - -fn unordered_list_style(depth: u8) -> ParagraphStyle { - ParagraphStyle { - para_pr: if depth == 1 { - LIST_LEVEL1_PARA_PR - } else { - LIST_LEVEL2_PARA_PR - }, - style: 0, - default_char_pr: NORMAL_CHAR_PR, - line_seg: if depth == 1 { - LineSegProfile::ListLevel1 - } else { - LineSegProfile::ListLevel2 - }, - list_semantic: ListSemantic::Unordered, - } -} - -fn ordered_list_style(shape: OrderedListShape, depth: u8) -> ParagraphStyle { - match shape { - OrderedListShape::SingleLevel => ParagraphStyle { - para_pr: LIST_LEVEL1_PARA_PR, - style: 0, - default_char_pr: NORMAL_CHAR_PR, - line_seg: LineSegProfile::ListLevel1, - list_semantic: ListSemantic::OrderedSingleLevel, - }, - OrderedListShape::NestedDepth2 => { - if depth == 1 { - ParagraphStyle { - para_pr: LIST_LEVEL2_PARA_PR, - style: 0, - default_char_pr: NORMAL_CHAR_PR, - line_seg: LineSegProfile::ListLevel1, - list_semantic: ListSemantic::OrderedTopLevel, - } - } else { - ParagraphStyle { - para_pr: LIST_LEVEL1_PARA_PR, - style: 0, - default_char_pr: NORMAL_CHAR_PR, - line_seg: LineSegProfile::ListLevel2, - list_semantic: ListSemantic::OrderedNestedLevel, - } - } - } - } -} - -fn heading_style(level: u8) -> ParagraphStyle { - match level { - 1 => ParagraphStyle { - para_pr: 2, - style: 2, - default_char_pr: H1_CHAR_PR, - line_seg: LineSegProfile::Standard, - list_semantic: ListSemantic::None, - }, - 2 => ParagraphStyle { - para_pr: 3, - style: 3, - default_char_pr: H2_CHAR_PR, - line_seg: LineSegProfile::Standard, - list_semantic: ListSemantic::None, - }, - 3 => ParagraphStyle { - para_pr: 4, - style: 4, - default_char_pr: H3_CHAR_PR, - line_seg: LineSegProfile::Standard, - list_semantic: ListSemantic::None, - }, - 4 => ParagraphStyle { - para_pr: 5, - style: 5, - default_char_pr: H4_CHAR_PR, - line_seg: LineSegProfile::Standard, - list_semantic: ListSemantic::None, - }, - 5 => ParagraphStyle { - para_pr: 6, - style: 6, - default_char_pr: BOLD_CHAR_PR, - line_seg: LineSegProfile::Standard, - list_semantic: ListSemantic::None, - }, - _ => ParagraphStyle { - para_pr: 7, - style: 7, - default_char_pr: BOLD_CHAR_PR, - line_seg: LineSegProfile::Standard, - list_semantic: ListSemantic::None, - }, - } -} - -fn heading_default_char_pr(level: u8) -> u32 { - heading_style(level).default_char_pr -} - -fn strong_char_pr(default_char_pr: u32) -> u32 { - match default_char_pr { - H1_CHAR_PR | H2_CHAR_PR | H3_CHAR_PR | H4_CHAR_PR | BOLD_CHAR_PR => default_char_pr, - _ => BOLD_CHAR_PR, - } -} - -fn emphasis_char_pr(default_char_pr: u32) -> u32 { - match default_char_pr { - H1_CHAR_PR | H2_CHAR_PR | H3_CHAR_PR | H4_CHAR_PR => default_char_pr, - _ => ITALIC_CHAR_PR, - } -} - -fn code_char_pr(default_char_pr: u32) -> u32 { - match default_char_pr { - H1_CHAR_PR | H2_CHAR_PR | H3_CHAR_PR | H4_CHAR_PR => default_char_pr, - _ => CODE_CHAR_PR, - } -} - -fn link_char_pr(default_char_pr: u32) -> u32 { - match default_char_pr { - H1_CHAR_PR | H2_CHAR_PR | H3_CHAR_PR | H4_CHAR_PR => default_char_pr, - _ => LINK_CHAR_PR, - } -} - -fn flatten_inline_children(inlines: &[Inline], strict_mode: bool) -> Result { - let mut text = String::new(); - for inline in inlines { - match inline { - Inline::Text(value) => text.push_str(value), - Inline::Emphasis(children) | Inline::Strong(children) => { - text.push_str(&flatten_inline_children(children, strict_mode)?); - } - Inline::Code(code) => text.push_str(code), - Inline::Link { - text: children, - url, - } => { - let label = flatten_inline_children(children, strict_mode)?; - if label.trim().is_empty() { - text.push_str(url); - } else { - text.push_str(&format!("{label} ({url})")); - } - } - Inline::Image(image) => { - if strict_mode { - return Err(CoreRsError::UnsupportedFeature( - "HWPX core does not support image".to_string(), - )); - } - if image.alt.trim().is_empty() { - text.push_str("[image]"); - } else { - text.push_str(&image.alt); - } - } - Inline::HardBreak => text.push(' '), - } - } - Ok(text) -} - -fn visible_paragraphs(items: &[SectionItem]) -> Vec { - let mut paragraphs = Vec::new(); - for item in items { - match item { - SectionItem::Paragraph(_, runs) => { - paragraphs.push(runs.iter().map(|run| run.text.as_str()).collect()) - } - SectionItem::Table(table) => { - for row in &table.rows { - paragraphs.push( - row.iter() - .map(|cell| format!("<{cell}>")) - .collect::>() - .join(""), - ); - } - } - } - } - paragraphs -} - -fn flatten_block_to_paragraphs(block: &Block) -> Vec { - match block { - Block::Paragraph(content) | Block::Heading { content, .. } => { - vec![flatten_inline_children(content, false).unwrap_or_default()] - } - Block::BlockQuote(blocks) => blocks - .iter() - .flat_map(flatten_block_to_paragraphs) - .collect::>(), - Block::CodeBlock { code, .. } => code.lines().map(ToString::to_string).collect(), - Block::List(list) => list - .items - .iter() - .enumerate() - .map(|(index, item)| { - let prefix = if list.ordered { - format!("{}. ", list.start_index + index as u64) - } else { - "- ".to_string() - }; - let body = item - .blocks - .iter() - .flat_map(flatten_block_to_paragraphs) - .collect::>() - .join(" "); - format!("{prefix}{body}") - }) - .collect(), - Block::Table(table) => { - let mut paragraphs = Vec::new(); - if !table.headers.is_empty() { - paragraphs.push( - table - .headers - .iter() - .map(|cell| flatten_inline_children(cell, false).unwrap_or_default()) - .collect::>() - .join(" | "), - ); - } - paragraphs.extend(table.rows.iter().map(|row| { - row.cells - .iter() - .map(|cell| flatten_inline_children(cell, false).unwrap_or_default()) - .collect::>() - .join(" | ") - })); - paragraphs - } - Block::ThematicBreak => vec!["---".to_string()], - } -} - -fn unsupported_name(block: &Block) -> &'static str { - match block { - Block::Paragraph(_) => "paragraph", - Block::Heading { .. } => "heading", - Block::BlockQuote(_) => "blockquote", - Block::CodeBlock { .. } => "code block", - Block::List(_) => "list", - Block::Table(_) => "table", - Block::ThematicBreak => "thematic break", - } -} - -fn plain_paragraph_item(text: String) -> SectionItem { - SectionItem::Paragraph( - normal_style(), - vec![RunSpec { - char_pr: NORMAL_CHAR_PR, - text, - }], - ) -} - -fn list_caret_position(items: &[SectionItem]) -> Option<(usize, usize)> { - let mut paragraph_index = 0; - let mut last_unordered_caret = None; - let mut ordered_carets = Vec::new(); - - for item in items { - match item { - SectionItem::Paragraph(style, runs) => { - let text = runs.iter().map(|run| run.text.as_str()).collect::(); - let caret = (paragraph_index, text.chars().count()); - match style.list_semantic { - ListSemantic::None => {} - ListSemantic::Unordered => last_unordered_caret = Some(caret), - ListSemantic::OrderedSingleLevel - | ListSemantic::OrderedTopLevel - | ListSemantic::OrderedNestedLevel => { - ordered_carets.push((style.list_semantic, caret)); - } - } - paragraph_index += 1; - } - SectionItem::Table(table) => { - paragraph_index += table.rows.len(); - } - } - } - - if !ordered_carets.is_empty() { - let only_single_level = ordered_carets - .iter() - .all(|(semantic, _)| *semantic == ListSemantic::OrderedSingleLevel); - if only_single_level && ordered_carets.len() > 1 { - return ordered_carets - .get(ordered_carets.len() - 2) - .map(|(_, caret)| *caret); - } - return ordered_carets.last().map(|(_, caret)| *caret); - } - - last_unordered_caret -} - -fn ordered_list_shape(blocks: &[Block]) -> Option { - match max_ordered_list_depth(blocks) { - 0 => None, - 1 => Some(OrderedListShape::SingleLevel), - 2 => Some(OrderedListShape::NestedDepth2), - _ => None, - } -} - -fn max_ordered_list_depth(blocks: &[Block]) -> u8 { - blocks - .iter() - .map(block_ordered_list_depth) - .max() - .unwrap_or(0) -} - -fn block_ordered_list_depth(block: &Block) -> u8 { - match block { - Block::List(list) if list.ordered => { - let nested_depth = list - .items - .iter() - .flat_map(|item| item.blocks.iter()) - .map(block_ordered_list_depth) - .max() - .unwrap_or(0); - 1 + nested_depth - } - Block::BlockQuote(blocks) => blocks - .iter() - .map(block_ordered_list_depth) - .max() - .unwrap_or(0), - Block::Paragraph(_) - | Block::Heading { .. } - | Block::CodeBlock { .. } - | Block::Table(_) - | Block::ThematicBreak - | Block::List(_) => 0, - } -} - -fn equal_widths(count: usize) -> Vec { - if count == 0 { - return Vec::new(); - } - - let base = TABLE_WIDTH / count as u32; - let remainder = TABLE_WIDTH % count as u32; - let mut widths = vec![base; count]; - if let Some(last) = widths.last_mut() { - *last += remainder; - } - widths -} diff --git a/packages/core-rs/src/generators/hwpx/section_xml/collect.rs b/packages/core-rs/src/generators/hwpx/section_xml/collect.rs new file mode 100644 index 0000000..af4cd45 --- /dev/null +++ b/packages/core-rs/src/generators/hwpx/section_xml/collect.rs @@ -0,0 +1,275 @@ +mod style; +mod table; + +use crate::generators::hwpx::document_shape::max_ordered_list_depth; +use crate::models::inline::Inline; + +use self::style::{ + heading_style, normal_style, ordered_list_style, plain_paragraph_item, unordered_list_style, + unsupported_name, +}; +use self::table::build_table_spec; +use super::render::render_runs; +use super::text::flatten_block_to_paragraphs; +use super::{ + Block, CoreRsError, ListBlock, OrderedListShape, ParagraphStyle, QuoteRenderMode, SectionItem, + TABLE_ID_SEED, +}; + +struct Collector { + strict_mode: bool, + ordered_shape: Option, + quote_render_mode: QuoteRenderMode, + items: Vec, + next_table_id: u64, + next_link_index: u64, +} + +impl Collector { + fn new( + strict_mode: bool, + ordered_shape: Option, + quote_render_mode: QuoteRenderMode, + ) -> Self { + Self { + strict_mode, + ordered_shape, + quote_render_mode, + items: Vec::new(), + next_table_id: TABLE_ID_SEED, + next_link_index: 0, + } + } + + fn collect_blocks(mut self, blocks: &[Block]) -> Result, CoreRsError> { + for block in blocks { + self.collect_block_items(block, 0)?; + } + Ok(self.items) + } + + fn collect_block_items(&mut self, block: &Block, quote_depth: u8) -> Result<(), CoreRsError> { + match block { + Block::Paragraph(inlines) => self.push_rendered_paragraph( + normal_style(quote_depth, self.quote_render_mode), + inlines, + ), + Block::Heading { level, content } => self.push_rendered_paragraph( + heading_style(*level, quote_depth, self.quote_render_mode), + content, + ), + Block::BlockQuote(blocks) => self.collect_quoted_blocks(blocks, quote_depth), + Block::List(list) => self.collect_list_items(list, quote_depth), + Block::Table(table) => { + self.items.push(SectionItem::Table(build_table_spec( + table, + self.strict_mode, + self.next_table_id, + )?)); + self.next_table_id += 1; + Ok(()) + } + unsupported => { + if self.strict_mode { + return Err(CoreRsError::UnsupportedFeature(format!( + "HWPX core does not support {0}", + unsupported_name(unsupported) + ))); + } + self.append_fallback_paragraphs(unsupported, quote_depth); + Ok(()) + } + } + } + + fn collect_quoted_blocks( + &mut self, + blocks: &[Block], + quote_depth: u8, + ) -> Result<(), CoreRsError> { + let next_quote_depth = quote_depth.saturating_add(1); + for child in blocks { + self.collect_block_items(child, next_quote_depth)?; + } + Ok(()) + } + + fn collect_list_items(&mut self, list: &ListBlock, quote_depth: u8) -> Result<(), CoreRsError> { + if list.ordered { + return self.collect_ordered_items(list, quote_depth); + } + + self.collect_unordered_list_items(list, 1, quote_depth) + } + + fn collect_ordered_items( + &mut self, + list: &ListBlock, + quote_depth: u8, + ) -> Result<(), CoreRsError> { + if self.strict_mode { + return Err(CoreRsError::UnsupportedFeature( + "HWPX approved baseline does not support ordered list".to_string(), + )); + } + + if let Some(shape) = self.ordered_shape { + self.collect_ordered_list_items(list, 1, quote_depth, shape) + } else { + self.append_fallback_paragraphs(&Block::List(list.clone()), quote_depth); + Ok(()) + } + } + + fn collect_unordered_list_items( + &mut self, + list: &ListBlock, + depth: u8, + quote_depth: u8, + ) -> Result<(), CoreRsError> { + if depth > 2 { + if self.strict_mode { + return Err(CoreRsError::UnsupportedFeature( + "HWPX approved baseline supports nested unordered lists up to depth 2" + .to_string(), + )); + } + self.append_fallback_paragraphs(&Block::List(list.clone()), quote_depth); + return Ok(()); + } + + let list_style = unordered_list_style(depth, quote_depth, self.quote_render_mode); + for item in &list.items { + for block in &item.blocks { + self.collect_unordered_list_block(block, list_style, depth, quote_depth)?; + } + } + + Ok(()) + } + + fn collect_unordered_list_block( + &mut self, + block: &Block, + list_style: ParagraphStyle, + depth: u8, + quote_depth: u8, + ) -> Result<(), CoreRsError> { + match block { + Block::Paragraph(inlines) => self.push_rendered_paragraph(list_style, inlines), + Block::Heading { content, .. } => self.push_rendered_paragraph(list_style, content), + Block::List(child) if !child.ordered => { + self.collect_unordered_list_items(child, depth + 1, quote_depth) + } + nested_list @ Block::List(_) => { + if self.strict_mode { + return Err(CoreRsError::UnsupportedFeature( + "HWPX approved baseline does not support ordered list".to_string(), + )); + } + self.append_fallback_paragraphs(nested_list, quote_depth); + Ok(()) + } + unsupported => { + if self.strict_mode { + return Err(CoreRsError::UnsupportedFeature(format!( + "HWPX approved baseline does not support {0} inside unordered list", + unsupported_name(unsupported) + ))); + } + self.append_fallback_paragraphs(unsupported, quote_depth); + Ok(()) + } + } + } + + fn collect_ordered_list_items( + &mut self, + list: &ListBlock, + depth: u8, + quote_depth: u8, + shape: OrderedListShape, + ) -> Result<(), CoreRsError> { + if depth > 2 { + self.append_fallback_paragraphs(&Block::List(list.clone()), quote_depth); + return Ok(()); + } + + let list_style = ordered_list_style(shape, depth, quote_depth, self.quote_render_mode); + for item in &list.items { + for block in &item.blocks { + self.collect_ordered_list_block(block, list_style, depth, quote_depth, shape)?; + } + } + + Ok(()) + } + + fn collect_ordered_list_block( + &mut self, + block: &Block, + list_style: ParagraphStyle, + depth: u8, + quote_depth: u8, + shape: OrderedListShape, + ) -> Result<(), CoreRsError> { + match block { + Block::Paragraph(inlines) => self.push_rendered_paragraph(list_style, inlines), + Block::Heading { content, .. } => self.push_rendered_paragraph(list_style, content), + Block::List(child) if child.ordered => { + self.collect_ordered_list_items(child, depth + 1, quote_depth, shape) + } + nested_list @ Block::List(_) => { + self.append_fallback_paragraphs(nested_list, quote_depth); + Ok(()) + } + unsupported => { + self.append_fallback_paragraphs(unsupported, quote_depth); + Ok(()) + } + } + } + + fn push_rendered_paragraph( + &mut self, + style: ParagraphStyle, + inlines: &[Inline], + ) -> Result<(), CoreRsError> { + let runs = render_runs( + inlines, + style.default_char_pr, + self.strict_mode, + &mut self.next_link_index, + )?; + self.items.push(SectionItem::Paragraph(style, runs)); + Ok(()) + } + + fn append_fallback_paragraphs(&mut self, block: &Block, quote_depth: u8) { + for paragraph in flatten_block_to_paragraphs(block) { + self.items.push(plain_paragraph_item( + paragraph, + quote_depth, + self.quote_render_mode, + )); + } + } +} + +pub(super) fn collect_section_items( + blocks: &[Block], + strict_mode: bool, + ordered_shape: Option, + quote_render_mode: QuoteRenderMode, +) -> Result, CoreRsError> { + Collector::new(strict_mode, ordered_shape, quote_render_mode).collect_blocks(blocks) +} + +pub(super) fn ordered_list_shape(blocks: &[Block]) -> Option { + match max_ordered_list_depth(blocks) { + 0 => None, + 1 => Some(OrderedListShape::SingleLevel), + 2 => Some(OrderedListShape::NestedDepth2), + _ => None, + } +} diff --git a/packages/core-rs/src/generators/hwpx/section_xml/collect/style.rs b/packages/core-rs/src/generators/hwpx/section_xml/collect/style.rs new file mode 100644 index 0000000..2aedd3d --- /dev/null +++ b/packages/core-rs/src/generators/hwpx/section_xml/collect/style.rs @@ -0,0 +1,184 @@ +use crate::models::block::Block; + +use super::super::{ + BOLD_CHAR_PR, H1_CHAR_PR, H2_CHAR_PR, H3_CHAR_PR, H4_CHAR_PR, LEGACY_QUOTE_CHAR_PR, + LEGACY_QUOTE_PARA_PR, LIST_LEVEL1_PARA_PR, LIST_LEVEL2_PARA_PR, LineSegProfile, ListSemantic, + NORMAL_CHAR_PR, OrderedListShape, ParagraphStyle, QUOTE_LEVEL1_PARA_PR, QUOTE_LEVEL2_PARA_PR, + QuoteRenderMode, RunSpec, SectionItem, +}; + +pub(super) fn normal_style(quote_depth: u8, quote_render_mode: QuoteRenderMode) -> ParagraphStyle { + if quote_depth == 0 { + return ParagraphStyle { + para_pr: 0, + style: 0, + default_char_pr: NORMAL_CHAR_PR, + line_seg: LineSegProfile::Standard, + list_semantic: ListSemantic::None, + }; + } + + quoted_style(quote_depth, 0, NORMAL_CHAR_PR, quote_render_mode) +} + +pub(super) fn unordered_list_style( + depth: u8, + quote_depth: u8, + quote_render_mode: QuoteRenderMode, +) -> ParagraphStyle { + if quote_depth > 0 { + return quoted_style(quote_depth, 0, NORMAL_CHAR_PR, quote_render_mode); + } + + ParagraphStyle { + para_pr: if depth == 1 { + LIST_LEVEL1_PARA_PR + } else { + LIST_LEVEL2_PARA_PR + }, + style: 0, + default_char_pr: NORMAL_CHAR_PR, + line_seg: if depth == 1 { + LineSegProfile::ListLevel1 + } else { + LineSegProfile::ListLevel2 + }, + list_semantic: ListSemantic::Unordered, + } +} + +pub(super) fn ordered_list_style( + shape: OrderedListShape, + depth: u8, + quote_depth: u8, + quote_render_mode: QuoteRenderMode, +) -> ParagraphStyle { + if quote_depth > 0 { + return quoted_style(quote_depth, 0, NORMAL_CHAR_PR, quote_render_mode); + } + + match shape { + OrderedListShape::SingleLevel => ParagraphStyle { + para_pr: LIST_LEVEL1_PARA_PR, + style: 0, + default_char_pr: NORMAL_CHAR_PR, + line_seg: LineSegProfile::ListLevel1, + list_semantic: ListSemantic::OrderedSingleLevel, + }, + OrderedListShape::NestedDepth2 => { + if depth == 1 { + ParagraphStyle { + para_pr: LIST_LEVEL2_PARA_PR, + style: 0, + default_char_pr: NORMAL_CHAR_PR, + line_seg: LineSegProfile::ListLevel1, + list_semantic: ListSemantic::OrderedTopLevel, + } + } else { + ParagraphStyle { + para_pr: LIST_LEVEL1_PARA_PR, + style: 0, + default_char_pr: NORMAL_CHAR_PR, + line_seg: LineSegProfile::ListLevel2, + list_semantic: ListSemantic::OrderedNestedLevel, + } + } + } + } +} + +pub(super) fn heading_style( + level: u8, + quote_depth: u8, + quote_render_mode: QuoteRenderMode, +) -> ParagraphStyle { + match level { + 1 => paragraph_style(2, 2, H1_CHAR_PR, quote_depth, quote_render_mode), + 2 => paragraph_style(3, 3, H2_CHAR_PR, quote_depth, quote_render_mode), + 3 => paragraph_style(4, 4, H3_CHAR_PR, quote_depth, quote_render_mode), + 4 => paragraph_style(5, 5, H4_CHAR_PR, quote_depth, quote_render_mode), + 5 => paragraph_style(6, 6, BOLD_CHAR_PR, quote_depth, quote_render_mode), + _ => paragraph_style(7, 7, BOLD_CHAR_PR, quote_depth, quote_render_mode), + } +} + +fn paragraph_style( + para_pr: u32, + style: u32, + default_char_pr: u32, + quote_depth: u8, + quote_render_mode: QuoteRenderMode, +) -> ParagraphStyle { + if quote_depth == 0 { + return ParagraphStyle { + para_pr, + style, + default_char_pr, + line_seg: LineSegProfile::Standard, + list_semantic: ListSemantic::None, + }; + } + + quoted_style(quote_depth, style, default_char_pr, quote_render_mode) +} + +fn quoted_style( + quote_depth: u8, + style: u32, + default_char_pr: u32, + quote_render_mode: QuoteRenderMode, +) -> ParagraphStyle { + if quote_render_mode == QuoteRenderMode::LegacyQuoteOnly { + return ParagraphStyle { + para_pr: LEGACY_QUOTE_PARA_PR, + style, + default_char_pr: LEGACY_QUOTE_CHAR_PR, + line_seg: LineSegProfile::QuoteLegacy, + list_semantic: ListSemantic::None, + }; + } + + let capped_depth = quote_depth.min(2); + ParagraphStyle { + para_pr: if capped_depth == 1 { + QUOTE_LEVEL1_PARA_PR + } else { + QUOTE_LEVEL2_PARA_PR + }, + style, + default_char_pr, + line_seg: if capped_depth == 1 { + LineSegProfile::QuoteLevel1 + } else { + LineSegProfile::QuoteLevel2 + }, + list_semantic: ListSemantic::None, + } +} + +pub(super) fn plain_paragraph_item( + text: String, + quote_depth: u8, + quote_render_mode: QuoteRenderMode, +) -> SectionItem { + let style = normal_style(quote_depth, quote_render_mode); + SectionItem::Paragraph( + style, + vec![RunSpec::Text { + char_pr: style.default_char_pr, + text, + }], + ) +} + +pub(super) fn unsupported_name(block: &Block) -> &'static str { + match block { + Block::Paragraph(_) => "paragraph", + Block::Heading { .. } => "heading", + Block::BlockQuote(_) => "blockquote", + Block::CodeBlock { .. } => "code block", + Block::List(_) => "list", + Block::Table(_) => "table", + Block::ThematicBreak => "thematic break", + } +} diff --git a/packages/core-rs/src/generators/hwpx/section_xml/collect/table.rs b/packages/core-rs/src/generators/hwpx/section_xml/collect/table.rs new file mode 100644 index 0000000..2178351 --- /dev/null +++ b/packages/core-rs/src/generators/hwpx/section_xml/collect/table.rs @@ -0,0 +1,53 @@ +use super::super::text::flatten_inline_children; +use super::super::{CoreRsError, TABLE_WIDTH, TableBlock, TableSpec}; + +pub(super) fn build_table_spec( + table: &TableBlock, + strict_mode: bool, + id: u64, +) -> Result { + let mut rows = Vec::new(); + if !table.headers.is_empty() { + rows.push( + table + .headers + .iter() + .map(|cell| flatten_inline_children(cell, strict_mode)) + .collect::, _>>()?, + ); + } + for row in &table.rows { + rows.push( + row.cells + .iter() + .map(|cell| flatten_inline_children(cell, strict_mode)) + .collect::, _>>()?, + ); + } + + let col_count = rows.iter().map(Vec::len).max().unwrap_or(0); + let col_widths = equal_widths(col_count); + for row in &mut rows { + row.resize(col_count, String::new()); + } + + Ok(TableSpec { + id, + rows, + col_widths, + }) +} + +fn equal_widths(count: usize) -> Vec { + if count == 0 { + return Vec::new(); + } + + let base = TABLE_WIDTH / count as u32; + let remainder = TABLE_WIDTH % count as u32; + let mut widths = vec![base; count]; + if let Some(last) = widths.last_mut() { + *last += remainder; + } + widths +} diff --git a/packages/core-rs/src/generators/hwpx/section_xml/compat.rs b/packages/core-rs/src/generators/hwpx/section_xml/compat.rs new file mode 100644 index 0000000..7743dd4 --- /dev/null +++ b/packages/core-rs/src/generators/hwpx/section_xml/compat.rs @@ -0,0 +1,775 @@ +use crate::error::CoreRsError; +use crate::models::block::Block; +use crate::models::document::Document; +use crate::models::inline::Inline; +use crate::utils::xml_helper::{escape_attr, escape_text}; + +use super::super::package_xml::{set_settings_caret, set_settings_caret_pos}; +use super::super::profile::ResolvedHwpxCompatibilityProfile; +use super::super::style::ResolvedHwpxStyle; +use super::shared::{ + HyperlinkFieldSpec, compat_hyperlink_path, hyperlink_command, render_hyperlink_run, + render_section_preamble_run, +}; + +const STYLE_BRAND_COLOR_SECTION_TEMPLATE: &str = include_str!( + "../../../../tests/fixtures/hwpx/approved/style-brand-color/expected/Contents/section0.xml" +); +const STYLE_BRAND_COLOR_PREVIEW_TEXT: &str = include_str!( + "../../../../tests/fixtures/hwpx/approved/style-brand-color/expected/Preview/PrvText.txt" +); +const CORE_PARAGRAPH_SECTION_TEMPLATE: &str = include_str!( + "../../../../tests/fixtures/hwpx/approved/core-paragraph/expected/Contents/section0.xml" +); +const CORE_PARAGRAPH_PREVIEW_TEXT: &str = include_str!( + "../../../../tests/fixtures/hwpx/approved/core-paragraph/expected/Preview/PrvText.txt" +); + +#[derive(Clone)] +enum CompatParagraphKind { + Normal, + Heading, + StandaloneLink, + TerminalEmpty, +} + +#[derive(Clone)] +struct CompatParagraph { + id: u64, + para_pr: u32, + style_id: u32, + default_char_pr: u32, + kind: CompatParagraphKind, + runs: Vec, + metric: ParaMetric, +} + +#[derive(Clone)] +struct ParaMetric { + height: u32, + spacing: u32, + horzpos: u32, + horzsize: u32, + flags: u32, + next_gap: u32, + extra_lines: Vec, +} + +#[derive(Clone)] +struct ExtraLine { + textpos: u32, + vert_offset: u32, +} + +#[derive(Clone)] +enum CompatRun { + Fragments { + char_pr: u32, + fragments: Vec, + }, + Hyperlink { + char_pr: u32, + field_begin_id: u64, + field_id: u64, + url: String, + text: String, + trailing_empty_text: bool, + }, + Empty { + char_pr: u32, + }, +} + +#[derive(Clone)] +enum RunFragment { + Text(String), + LineBreak, + MarkpenBegin { color: &'static str }, + MarkpenEnd, +} + +pub(super) fn build_section_xml( + document: &Document, + profile: ResolvedHwpxCompatibilityProfile, + style: &ResolvedHwpxStyle, +) -> Result { + if profile == ResolvedHwpxCompatibilityProfile::CoreParagraphFixture { + return Ok(CORE_PARAGRAPH_SECTION_TEMPLATE.to_string()); + } + + if profile == ResolvedHwpxCompatibilityProfile::StyleBrandColor { + return Ok(STYLE_BRAND_COLOR_SECTION_TEMPLATE.to_string()); + } + + let paragraphs = collect_paragraphs(document, profile, style)?; + let mut body = String::new(); + let mut vertpos = 0; + for (index, paragraph) in paragraphs.iter().enumerate() { + body.push_str(&render_paragraph(paragraph, index == 0, vertpos)); + vertpos += paragraph.metric.height + paragraph.metric.spacing + paragraph.metric.next_gap; + } + + Ok(format!( + concat!( + "", + "", + "{body}", + "" + ), + body = body + )) +} + +pub(super) fn build_preview_text( + document: &Document, + profile: ResolvedHwpxCompatibilityProfile, + style: &ResolvedHwpxStyle, +) -> Result { + if profile == ResolvedHwpxCompatibilityProfile::CoreParagraphFixture { + set_settings_caret(2, 0); + return Ok(CORE_PARAGRAPH_PREVIEW_TEXT.to_string()); + } + + if profile == ResolvedHwpxCompatibilityProfile::StyleBrandColor { + set_settings_caret(2, 0); + return Ok(STYLE_BRAND_COLOR_PREVIEW_TEXT.to_string()); + } + + let paragraphs = collect_paragraphs(document, profile, style)?; + let preview_lines = paragraphs + .iter() + .filter(|paragraph| !matches!(paragraph.kind, CompatParagraphKind::TerminalEmpty)) + .map(paragraph_preview_text) + .filter(|text| !text.is_empty()) + .collect::>(); + + match profile { + ResolvedHwpxCompatibilityProfile::CoreParagraphFixture => set_settings_caret(2, 0), + ResolvedHwpxCompatibilityProfile::CoreParagraph => set_settings_caret_pos(45), + ResolvedHwpxCompatibilityProfile::CoreInlineStyle => set_settings_caret(0, 28), + ResolvedHwpxCompatibilityProfile::CoreLinkText => set_settings_caret(1, 0), + ResolvedHwpxCompatibilityProfile::CoreMixed => { + let pos = preview_lines + .last() + .map(|text| text.chars().count()) + .unwrap_or(0); + set_settings_caret(2, pos); + } + ResolvedHwpxCompatibilityProfile::StyleBrandColor => set_settings_caret(2, 0), + ResolvedHwpxCompatibilityProfile::StyleCenteredLayout => set_settings_caret(3, 0), + ResolvedHwpxCompatibilityProfile::StyleTypography => { + let pos = preview_lines + .get(1) + .map(|text| text.chars().count()) + .unwrap_or(0); + set_settings_caret(1, pos); + } + ResolvedHwpxCompatibilityProfile::LegacyDefault => set_settings_caret_pos(45), + } + + Ok(preview_lines.join("\n")) +} + +fn collect_paragraphs( + document: &Document, + profile: ResolvedHwpxCompatibilityProfile, + style: &ResolvedHwpxStyle, +) -> Result, CoreRsError> { + match profile { + ResolvedHwpxCompatibilityProfile::CoreParagraphFixture => { + Err(CoreRsError::UnsupportedFeature( + "core-paragraph fixture uses exact compat templates".to_string(), + )) + } + ResolvedHwpxCompatibilityProfile::CoreParagraph => { + let [Block::Paragraph(inlines)] = document.blocks.as_slice() else { + return Err(CoreRsError::UnsupportedFeature( + "unexpected CoreParagraph shape".to_string(), + )); + }; + Ok(vec![CompatParagraph { + id: 2_757_524_817, + para_pr: 0, + style_id: 0, + default_char_pr: 5, + kind: CompatParagraphKind::Normal, + runs: vec![CompatRun::Fragments { + char_pr: 5, + fragments: vec![RunFragment::Text(plain_text(inlines)?)], + }], + metric: body_metric(1000, 600, 0), + }]) + } + ResolvedHwpxCompatibilityProfile::CoreInlineStyle => { + let [Block::Paragraph(inlines)] = document.blocks.as_slice() else { + return Err(CoreRsError::UnsupportedFeature( + "unexpected CoreInlineStyle shape".to_string(), + )); + }; + Ok(vec![CompatParagraph { + id: 2_757_524_817, + para_pr: 0, + style_id: 0, + default_char_pr: 5, + kind: CompatParagraphKind::Normal, + runs: inline_style_runs(inlines)?, + metric: ParaMetric { + height: 1000, + spacing: 600, + horzpos: 0, + horzsize: 42_520, + flags: 393_216, + next_gap: 0, + extra_lines: vec![ExtraLine { + textpos: 29, + vert_offset: 1_600, + }], + }, + }]) + } + ResolvedHwpxCompatibilityProfile::CoreLinkText => { + let [Block::Paragraph(inlines)] = document.blocks.as_slice() else { + return Err(CoreRsError::UnsupportedFeature( + "unexpected CoreLinkText shape".to_string(), + )); + }; + let (text, url) = standalone_link_text_and_url(inlines)?; + Ok(vec![ + CompatParagraph { + id: 2_757_524_817, + para_pr: 19, + style_id: 0, + default_char_pr: 6, + kind: CompatParagraphKind::StandaloneLink, + runs: vec![CompatRun::Hyperlink { + char_pr: 6, + field_begin_id: 2_107_483_186, + field_id: 627_600_491, + url, + text, + trailing_empty_text: true, + }], + metric: body_metric(1000, 200, 1_200), + }, + CompatParagraph { + id: 2_147_483_648, + para_pr: 0, + style_id: 0, + default_char_pr: 7, + kind: CompatParagraphKind::TerminalEmpty, + runs: vec![CompatRun::Empty { char_pr: 7 }], + metric: body_metric(1000, 600, 0), + }, + ]) + } + ResolvedHwpxCompatibilityProfile::CoreMixed => { + let [ + Block::Heading { content, .. }, + Block::Paragraph(inlines), + Block::Paragraph(trailing), + ] = document.blocks.as_slice() + else { + return Err(CoreRsError::UnsupportedFeature( + "unexpected CoreMixed shape".to_string(), + )); + }; + Ok(vec![ + CompatParagraph { + id: 1_033_768_010, + para_pr: 2, + style_id: 2, + default_char_pr: 6, + kind: CompatParagraphKind::Heading, + runs: vec![CompatRun::Fragments { + char_pr: 6, + fragments: vec![RunFragment::Text(plain_text(content)?)], + }], + metric: heading_metric(style.heading_sizes[0]), + }, + CompatParagraph { + id: 3_181_251_661, + para_pr: 0, + style_id: 0, + default_char_pr: 5, + kind: CompatParagraphKind::Normal, + runs: mixed_runs(inlines)?, + metric: body_metric(1000, 600, 0), + }, + CompatParagraph { + id: 0, + para_pr: 0, + style_id: 0, + default_char_pr: 5, + kind: CompatParagraphKind::Normal, + runs: vec![CompatRun::Fragments { + char_pr: 5, + fragments: vec![RunFragment::Text(plain_text(trailing)?)], + }], + metric: body_metric(1000, 600, 0), + }, + ]) + } + ResolvedHwpxCompatibilityProfile::StyleBrandColor => Err(CoreRsError::UnsupportedFeature( + "style-brand-color uses exact compat templates".to_string(), + )), + ResolvedHwpxCompatibilityProfile::StyleCenteredLayout => { + let [ + Block::Heading { content, .. }, + Block::Paragraph(first), + Block::Paragraph(second), + ] = document.blocks.as_slice() + else { + return Err(CoreRsError::UnsupportedFeature( + "unexpected StyleCenteredLayout shape".to_string(), + )); + }; + Ok(vec![ + CompatParagraph { + id: 1_033_760_959, + para_pr: 2, + style_id: 2, + default_char_pr: 6, + kind: CompatParagraphKind::Heading, + runs: vec![CompatRun::Fragments { + char_pr: 6, + fragments: vec![RunFragment::Text(plain_text(content)?)], + }], + metric: heading_metric(style.heading_sizes[0]), + }, + CompatParagraph { + id: 3_181_244_608, + para_pr: 0, + style_id: 0, + default_char_pr: 5, + kind: CompatParagraphKind::Normal, + runs: vec![CompatRun::Fragments { + char_pr: 5, + fragments: vec![RunFragment::Text(plain_text(first)?)], + }], + metric: body_metric(style.body_font_size, spacing_for(style.body_font_size), 0), + }, + CompatParagraph { + id: 0, + para_pr: 0, + style_id: 0, + default_char_pr: 5, + kind: CompatParagraphKind::Normal, + runs: vec![CompatRun::Fragments { + char_pr: 5, + fragments: vec![RunFragment::Text(plain_text(second)?)], + }], + metric: body_metric(style.body_font_size, spacing_for(style.body_font_size), 0), + }, + CompatParagraph { + id: 0, + para_pr: 0, + style_id: 0, + default_char_pr: 5, + kind: CompatParagraphKind::TerminalEmpty, + runs: vec![CompatRun::Empty { char_pr: 5 }], + metric: body_metric(style.body_font_size, spacing_for(style.body_font_size), 0), + }, + ]) + } + ResolvedHwpxCompatibilityProfile::StyleTypography => { + let [ + Block::Heading { content, .. }, + Block::Paragraph(body), + Block::Paragraph(link), + ] = document.blocks.as_slice() + else { + return Err(CoreRsError::UnsupportedFeature( + "unexpected StyleTypography shape".to_string(), + )); + }; + let (text, url) = standalone_link_text_and_url(link)?; + Ok(vec![ + CompatParagraph { + id: 1_033_759_294, + para_pr: 2, + style_id: 2, + default_char_pr: 7, + kind: CompatParagraphKind::Heading, + runs: vec![CompatRun::Fragments { + char_pr: 7, + fragments: vec![RunFragment::Text(plain_text(content)?)], + }], + metric: heading_metric(style.heading_sizes[0]), + }, + CompatParagraph { + id: 3_181_242_946, + para_pr: 0, + style_id: 0, + default_char_pr: 5, + kind: CompatParagraphKind::Normal, + runs: vec![CompatRun::Fragments { + char_pr: 5, + fragments: vec![RunFragment::Text(plain_text(body)?)], + }], + metric: body_metric(style.body_font_size, spacing_for(style.body_font_size), 0), + }, + CompatParagraph { + id: 2_757_524_819, + para_pr: 19, + style_id: 0, + default_char_pr: 6, + kind: CompatParagraphKind::StandaloneLink, + runs: vec![CompatRun::Hyperlink { + char_pr: 6, + field_begin_id: 2_107_501_120, + field_id: 627_600_491, + url, + text, + trailing_empty_text: true, + }], + metric: body_metric(style.body_font_size, 224, 1_200), + }, + CompatParagraph { + id: 2_147_483_648, + para_pr: 0, + style_id: 0, + default_char_pr: 8, + kind: CompatParagraphKind::TerminalEmpty, + runs: vec![CompatRun::Empty { char_pr: 8 }], + metric: body_metric(style.body_font_size, spacing_for(style.body_font_size), 0), + }, + ]) + } + ResolvedHwpxCompatibilityProfile::LegacyDefault => Err(CoreRsError::UnsupportedFeature( + "legacy profile must use the default section renderer".to_string(), + )), + } +} + +fn render_paragraph(paragraph: &CompatParagraph, is_first: bool, vertpos: u32) -> String { + let mut xml = format!( + "", + id = paragraph.id, + para_pr = paragraph.para_pr, + style_id = paragraph.style_id, + ); + + if is_first { + xml.push_str(&render_section_preamble_run(paragraph.default_char_pr)); + } + + for run in ¶graph.runs { + xml.push_str(&render_run(run)); + } + + xml.push_str(&render_linesegarray(paragraph, vertpos)); + xml.push_str(""); + xml +} + +fn render_run(run: &CompatRun) -> String { + match run { + CompatRun::Fragments { char_pr, fragments } => { + format!( + "{body}", + char_pr = char_pr, + body = render_fragments(fragments), + ) + } + CompatRun::Hyperlink { + char_pr, + field_begin_id, + field_id, + url, + text, + trailing_empty_text, + } => render_hyperlink_run(HyperlinkFieldSpec { + char_pr: *char_pr, + field_begin_id: *field_begin_id, + field_id: *field_id, + command: &hyperlink_command(url), + path: url, + text, + trailing_empty_text: *trailing_empty_text, + }), + CompatRun::Empty { char_pr } => format!(""), + } +} + +fn render_fragments(fragments: &[RunFragment]) -> String { + let mut xml = String::new(); + for fragment in fragments { + match fragment { + RunFragment::Text(text) => xml.push_str(&escape_text(text)), + RunFragment::LineBreak => xml.push_str(""), + RunFragment::MarkpenBegin { color } => { + xml.push_str(&format!( + "", + escape_attr(color) + )); + } + RunFragment::MarkpenEnd => xml.push_str(""), + } + } + xml +} + +fn render_linesegarray(paragraph: &CompatParagraph, vertpos: u32) -> String { + let mut xml = format!( + "", + vertpos = vertpos, + size = paragraph.metric.height, + baseline = baseline_for(paragraph.metric.height), + spacing = paragraph.metric.spacing, + horzpos = paragraph.metric.horzpos, + horzsize = paragraph.metric.horzsize, + flags = paragraph.metric.flags, + ); + for line in ¶graph.metric.extra_lines { + xml.push_str(&format!( + "", + textpos = line.textpos, + vertpos = vertpos + line.vert_offset, + size = paragraph.metric.height, + baseline = baseline_for(paragraph.metric.height), + spacing = paragraph.metric.spacing, + horzpos = paragraph.metric.horzpos, + horzsize = paragraph.metric.horzsize, + flags = paragraph.metric.flags, + )); + } + xml.push_str(""); + xml +} + +fn paragraph_preview_text(paragraph: &CompatParagraph) -> String { + paragraph + .runs + .iter() + .filter_map(run_preview_text) + .collect::>() + .join("") +} + +fn run_preview_text(run: &CompatRun) -> Option { + match run { + CompatRun::Fragments { fragments, .. } => Some( + fragments + .iter() + .filter_map(|fragment| match fragment { + RunFragment::Text(text) => Some(text.clone()), + RunFragment::LineBreak + | RunFragment::MarkpenBegin { .. } + | RunFragment::MarkpenEnd => None, + }) + .collect::(), + ), + CompatRun::Hyperlink { text, .. } => Some(text.clone()), + CompatRun::Empty { .. } => None, + } +} + +fn inline_style_runs(inlines: &[Inline]) -> Result, CoreRsError> { + let mut runs = Vec::new(); + for (index, inline) in inlines.iter().enumerate() { + match inline { + Inline::Text(value) => { + let text = normalize_core_inline_text(value, inlines.get(index + 1)); + push_text_run(&mut runs, 5, text); + } + Inline::Strong(children) => push_text_run(&mut runs, 6, plain_text(children)?), + Inline::Emphasis(children) => push_text_run(&mut runs, 7, plain_text(children)?), + Inline::Code(code) => push_fragments( + &mut runs, + 5, + vec![ + RunFragment::MarkpenBegin { color: "#F2F2F2" }, + RunFragment::Text(code.clone()), + RunFragment::MarkpenEnd, + ], + ), + Inline::HardBreak => push_fragments(&mut runs, 5, vec![RunFragment::LineBreak]), + Inline::Link { .. } | Inline::Image(_) => { + return Err(CoreRsError::UnsupportedFeature( + "unexpected inline in CoreInlineStyle".to_string(), + )); + } + } + } + Ok(runs) +} + +fn mixed_runs(inlines: &[Inline]) -> Result, CoreRsError> { + let mut runs = Vec::new(); + let mut previous_was_styled_or_link = false; + for inline in inlines { + match inline { + Inline::Text(value) => { + let text = normalize_core_mixed_text(value, previous_was_styled_or_link); + push_text_run(&mut runs, 5, text); + previous_was_styled_or_link = false; + } + Inline::Strong(children) => { + push_text_run(&mut runs, 7, plain_text(children)?); + previous_was_styled_or_link = true; + } + Inline::Emphasis(children) => { + push_text_run(&mut runs, 8, plain_text(children)?); + previous_was_styled_or_link = true; + } + Inline::Code(code) => { + push_fragments( + &mut runs, + 5, + vec![ + RunFragment::MarkpenBegin { color: "#F2F2F2" }, + RunFragment::Text(code.clone()), + RunFragment::MarkpenEnd, + ], + ); + previous_was_styled_or_link = true; + } + Inline::Link { text, url } => { + runs.push(CompatRun::Hyperlink { + char_pr: 9, + field_begin_id: 2_107_509_839, + field_id: 627_600_491, + url: compat_hyperlink_path(url), + text: link_visible_text(text, url)?, + trailing_empty_text: false, + }); + previous_was_styled_or_link = true; + } + Inline::HardBreak | Inline::Image(_) => { + return Err(CoreRsError::UnsupportedFeature( + "unexpected inline in CoreMixed".to_string(), + )); + } + } + } + Ok(runs) +} + +fn push_text_run(runs: &mut Vec, char_pr: u32, text: String) { + if text.is_empty() { + return; + } + push_fragments(runs, char_pr, vec![RunFragment::Text(text)]); +} + +fn push_fragments(runs: &mut Vec, char_pr: u32, fragments: Vec) { + if let Some(CompatRun::Fragments { + char_pr: last_char_pr, + fragments: last_fragments, + }) = runs.last_mut() + { + if *last_char_pr == char_pr { + last_fragments.extend(fragments); + return; + } + } + runs.push(CompatRun::Fragments { char_pr, fragments }); +} + +fn standalone_link_text_and_url(inlines: &[Inline]) -> Result<(String, String), CoreRsError> { + let [Inline::Link { text, url }] = inlines else { + return Err(CoreRsError::UnsupportedFeature( + "expected standalone link paragraph".to_string(), + )); + }; + Ok((link_visible_text(text, url)?, compat_hyperlink_path(url))) +} + +fn link_visible_text(text: &[Inline], url: &str) -> Result { + let text = plain_text(text)?; + if text.trim().is_empty() { + Ok(compat_hyperlink_path(url)) + } else { + Ok(text) + } +} + +fn plain_text(inlines: &[Inline]) -> Result { + let mut text = String::new(); + for inline in inlines { + match inline { + Inline::Text(value) => text.push_str(value), + Inline::Strong(children) | Inline::Emphasis(children) => { + text.push_str(&plain_text(children)?); + } + Inline::Code(code) => text.push_str(code), + Inline::Link { + text: children, + url, + } => { + let visible = link_visible_text(children, url)?; + text.push_str(&visible); + } + Inline::HardBreak => text.push(' '), + Inline::Image(_) => { + return Err(CoreRsError::UnsupportedFeature( + "images are not supported in compat path".to_string(), + )); + } + } + } + Ok(text) +} + +fn normalize_core_inline_text(value: &str, next: Option<&Inline>) -> String { + if matches!(next, Some(Inline::HardBreak)) { + return value.strip_suffix(" 줄바꿈").unwrap_or(value).to_string(); + } + value.to_string() +} + +fn normalize_core_mixed_text(value: &str, previous_was_styled_or_link: bool) -> String { + if !previous_was_styled_or_link { + return value.to_string(); + } + + let Some(rest) = value.strip_prefix(' ') else { + return value.to_string(); + }; + + if starts_with_attached_postposition(rest) { + rest.to_string() + } else { + value.to_string() + } +} + +fn starts_with_attached_postposition(value: &str) -> bool { + [ + "와", "과", "를", "을", "은", "는", "이", "가", "도", "만", "로", "으로", "의", "에", + ] + .iter() + .any(|postposition| value.starts_with(postposition)) +} + +fn heading_metric(height: u32) -> ParaMetric { + ParaMetric { + height, + spacing: spacing_for(height), + horzpos: 1_000, + horzsize: 41_520, + flags: 2_490_368, + next_gap: 0, + extra_lines: Vec::new(), + } +} + +fn body_metric(height: u32, spacing: u32, next_gap: u32) -> ParaMetric { + ParaMetric { + height, + spacing, + horzpos: 0, + horzsize: 42_520, + flags: 393_216, + next_gap, + extra_lines: Vec::new(), + } +} + +fn baseline_for(height: u32) -> u32 { + (height * 85) / 100 +} + +fn spacing_for(height: u32) -> u32 { + (height * 60) / 100 +} diff --git a/packages/core-rs/src/generators/hwpx/section_xml/mod.rs b/packages/core-rs/src/generators/hwpx/section_xml/mod.rs new file mode 100644 index 0000000..d40aca6 --- /dev/null +++ b/packages/core-rs/src/generators/hwpx/section_xml/mod.rs @@ -0,0 +1,209 @@ +mod collect; +mod compat; +mod render; +mod shared; +mod text; + +use crate::error::CoreRsError; +use crate::generators::hwpx::document_shape::uses_legacy_quote_only_contract as uses_legacy_quote_only_shape; +use crate::models::block::{Block, ListBlock, TableBlock}; +use crate::models::document::Document; +use crate::models::inline::Inline; +use crate::utils::xml_helper::escape_text; + +use super::package_xml::{set_settings_caret, set_settings_caret_pos}; +use super::profile::ResolvedHwpxCompatibilityProfile; +use super::style::ResolvedHwpxStyle; + +const NORMAL_CHAR_PR: u32 = 6; +const BOLD_CHAR_PR: u32 = 7; +const ITALIC_CHAR_PR: u32 = 8; +const LEGACY_QUOTE_CHAR_PR: u32 = 7; +const CODE_CHAR_PR: u32 = 9; +const H1_CHAR_PR: u32 = 10; +const H2_CHAR_PR: u32 = 11; +const H3_CHAR_PR: u32 = 12; +const H4_CHAR_PR: u32 = 13; +const LINK_CHAR_PR: u32 = 14; +const LIST_LEVEL1_PARA_PR: u32 = 19; +const LIST_LEVEL2_PARA_PR: u32 = 20; +const QUOTE_LEVEL1_PARA_PR: u32 = 21; +const QUOTE_LEVEL2_PARA_PR: u32 = 22; +const LIST_LINESEG_INCREMENT: u32 = 2_300; +const LIST_LINESEG_VERT_SIZE: u32 = 1_000; +const LIST_LINESEG_TEXT_HEIGHT: u32 = 1_000; +const LIST_LINESEG_BASELINE: u32 = 850; +const LIST_LINESEG_SPACING: u32 = 600; +const LIST_LINESEG_FLAGS: u32 = 2_490_368; +const LIST_LEVEL1_HORZ_POS: u32 = 1_100; +const LIST_LEVEL1_HORZ_SIZE: u32 = 41_420; +const LIST_LEVEL2_HORZ_POS: u32 = 2_200; +const LIST_LEVEL2_HORZ_SIZE: u32 = 40_320; +const QUOTE_LEVEL1_HORZ_POS: u32 = 1_100; +const QUOTE_LEVEL1_HORZ_SIZE: u32 = 41_420; +const QUOTE_LEVEL2_HORZ_POS: u32 = 2_200; +const QUOTE_LEVEL2_HORZ_SIZE: u32 = 40_320; +const LEGACY_QUOTE_PARA_PR: u32 = 19; +const LEGACY_QUOTE_LINESEG_INCREMENT: u32 = 1_600; +const LEGACY_QUOTE_HORZ_POS: u32 = 2_000; +const LEGACY_QUOTE_HORZ_SIZE: u32 = 40_520; + +const TABLE_ID_SEED: u64 = 2_104_760_845; +const TABLE_WIDTH: u32 = 41_954; +const TABLE_OUTER_LINESEG_SIZE: u32 = 4_412; +const TABLE_OUTER_BASELINE: u32 = 3_750; +const TABLE_ROW_HEIGHT: u32 = 282; +const TABLE_HEIGHT_PER_ROW: u32 = 1_282; +const TABLE_CELL_INNER_WIDTH_DELTA: u32 = 1_021; + +#[derive(Clone, Copy)] +struct ParagraphStyle { + para_pr: u32, + style: u32, + default_char_pr: u32, + line_seg: LineSegProfile, + list_semantic: ListSemantic, +} + +#[derive(Clone, Copy)] +enum LineSegProfile { + Standard, + ListLevel1, + ListLevel2, + QuoteLegacy, + QuoteLevel1, + QuoteLevel2, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum QuoteRenderMode { + Modern, + LegacyQuoteOnly, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum ListSemantic { + None, + Unordered, + OrderedSingleLevel, + OrderedTopLevel, + OrderedNestedLevel, +} + +#[derive(Clone, Copy)] +enum OrderedListShape { + SingleLevel, + NestedDepth2, +} + +#[derive(Clone)] +enum RunSpec { + Text { + char_pr: u32, + text: String, + }, + Hyperlink { + char_pr: u32, + field_begin_id: u64, + field_id: u64, + url: String, + text: String, + }, +} + +enum SectionItem { + Paragraph(ParagraphStyle, Vec), + Table(TableSpec), +} + +struct TableSpec { + id: u64, + rows: Vec>, + col_widths: Vec, +} + +pub fn build_section_xml( + document: &Document, + strict_mode: bool, + style: &ResolvedHwpxStyle, + profile: ResolvedHwpxCompatibilityProfile, +) -> Result { + if profile != ResolvedHwpxCompatibilityProfile::LegacyDefault { + return compat::build_section_xml(document, profile, style); + } + let quote_render_mode = quote_render_mode(document); + let ordered_shape = collect::ordered_list_shape(&document.blocks); + let items = collect::collect_section_items( + &document.blocks, + strict_mode, + ordered_shape, + quote_render_mode, + )?; + let mut body = String::new(); + let mut list_vertpos = 0; + let mut quote_vertpos = 0; + for (index, item) in items.iter().enumerate() { + body.push_str(&render::render_section_item( + item, + index == 0, + index, + &mut list_vertpos, + &mut quote_vertpos, + )); + } + + Ok(format!( + concat!( + "", + "", + "{body}", + "" + ), + body = body + )) +} + +pub fn build_preview_text( + document: &Document, + strict_mode: bool, + profile: ResolvedHwpxCompatibilityProfile, + style: &ResolvedHwpxStyle, +) -> Result { + if profile != ResolvedHwpxCompatibilityProfile::LegacyDefault { + return compat::build_preview_text(document, profile, style); + } + let quote_render_mode = quote_render_mode(document); + let ordered_shape = collect::ordered_list_shape(&document.blocks); + let items = collect::collect_section_items( + &document.blocks, + strict_mode, + ordered_shape, + quote_render_mode, + )?; + let has_table = items + .iter() + .any(|item| matches!(item, SectionItem::Table(_))); + let text = text::visible_paragraphs(&items).join("\r\n"); + if has_table { + set_settings_caret_pos(16); + } else if quote_render_mode == QuoteRenderMode::LegacyQuoteOnly { + if let Some((para_id_ref, pos)) = text::last_paragraph_caret(&items) { + set_settings_caret(para_id_ref, pos); + } else { + set_settings_caret_pos(45); + } + } else if let Some((para_id_ref, pos)) = text::list_caret_position(&items) { + set_settings_caret(para_id_ref, pos); + } else { + set_settings_caret_pos(45); + } + Ok(text) +} + +fn quote_render_mode(document: &Document) -> QuoteRenderMode { + if uses_legacy_quote_only_shape(&document.blocks) { + QuoteRenderMode::LegacyQuoteOnly + } else { + QuoteRenderMode::Modern + } +} diff --git a/packages/core-rs/src/generators/hwpx/section_xml/render.rs b/packages/core-rs/src/generators/hwpx/section_xml/render.rs new file mode 100644 index 0000000..21c4212 --- /dev/null +++ b/packages/core-rs/src/generators/hwpx/section_xml/render.rs @@ -0,0 +1,401 @@ +use super::shared::{ + HyperlinkFieldSpec, hyperlink_command, render_hyperlink_run, render_section_preamble_run, +}; +use super::text::flatten_inline_children; +use super::{ + BOLD_CHAR_PR, CODE_CHAR_PR, CoreRsError, H1_CHAR_PR, H2_CHAR_PR, H3_CHAR_PR, H4_CHAR_PR, + ITALIC_CHAR_PR, Inline, LEGACY_QUOTE_HORZ_POS, LEGACY_QUOTE_HORZ_SIZE, + LEGACY_QUOTE_LINESEG_INCREMENT, LINK_CHAR_PR, LIST_LEVEL1_HORZ_POS, LIST_LEVEL1_HORZ_SIZE, + LIST_LEVEL2_HORZ_POS, LIST_LEVEL2_HORZ_SIZE, LIST_LINESEG_BASELINE, LIST_LINESEG_FLAGS, + LIST_LINESEG_INCREMENT, LIST_LINESEG_SPACING, LIST_LINESEG_TEXT_HEIGHT, LIST_LINESEG_VERT_SIZE, + LineSegProfile, NORMAL_CHAR_PR, ParagraphStyle, QUOTE_LEVEL1_HORZ_POS, QUOTE_LEVEL1_HORZ_SIZE, + QUOTE_LEVEL2_HORZ_POS, QUOTE_LEVEL2_HORZ_SIZE, RunSpec, SectionItem, + TABLE_CELL_INNER_WIDTH_DELTA, TABLE_HEIGHT_PER_ROW, TABLE_OUTER_BASELINE, + TABLE_OUTER_LINESEG_SIZE, TABLE_ROW_HEIGHT, TABLE_WIDTH, TableSpec, escape_text, +}; + +pub(super) fn render_section_item( + item: &SectionItem, + is_first: bool, + index: usize, + list_vertpos: &mut u32, + quote_vertpos: &mut u32, +) -> String { + match item { + SectionItem::Paragraph(style, runs) => { + render_paragraph(style, runs, is_first, index, list_vertpos, quote_vertpos) + } + SectionItem::Table(table) => render_table_paragraph(table, is_first, index), + } +} + +fn render_paragraph( + style: &ParagraphStyle, + runs: &[RunSpec], + is_first: bool, + index: usize, + list_vertpos: &mut u32, + quote_vertpos: &mut u32, +) -> String { + let id = 2_757_524_817u64 + index as u64; + let mut xml = format!( + "", + id = id, + para = style.para_pr, + style_id = style.style, + ); + + if is_first { + xml.push_str(&render_section_preamble_run(style.default_char_pr)); + } + + if runs.is_empty() { + xml.push_str(&format!( + "", + style.default_char_pr + )); + } else { + for run in runs { + xml.push_str(&render_run(run)); + } + } + + if should_render_paragraph_lineseg(style) { + xml.push_str(&render_lineseg(style.line_seg, list_vertpos, quote_vertpos)); + } + xml.push_str(""); + xml +} + +fn render_run(run: &RunSpec) -> String { + match run { + RunSpec::Text { char_pr, text } => format!( + "{text}", + char_pr = char_pr, + text = escape_text(text), + ), + RunSpec::Hyperlink { + char_pr, + field_begin_id, + field_id, + url, + text, + } => render_hyperlink_run(HyperlinkFieldSpec { + char_pr: *char_pr, + field_begin_id: *field_begin_id, + field_id: *field_id, + command: &hyperlink_command(url), + path: url, + text, + trailing_empty_text: false, + }), + } +} + +fn should_render_paragraph_lineseg(style: &ParagraphStyle) -> bool { + if style.style != 0 { + return true; + } + + !matches!( + style.line_seg, + LineSegProfile::Standard | LineSegProfile::ListLevel1 | LineSegProfile::ListLevel2 + ) +} + +fn render_table_paragraph(table: &TableSpec, is_first: bool, index: usize) -> String { + let id = 2_757_524_817u64 + index as u64; + let mut xml = format!( + "", + id = id, + ); + + if is_first { + xml.push_str(&render_section_preamble_run(NORMAL_CHAR_PR)); + } + xml.push_str(""); + xml.push_str(&render_table_xml(table)); + xml.push_str(""); + xml.push_str(&render_table_lineseg()); + xml.push_str(""); + xml +} + +fn render_table_xml(table: &TableSpec) -> String { + let row_count = table.rows.len(); + let col_count = table.col_widths.len(); + let height = TABLE_HEIGHT_PER_ROW * row_count as u32; + + let mut xml = format!( + concat!( + "", + "", + "", + "", + "" + ), + id = table.id, + row_count = row_count, + col_count = col_count, + width = TABLE_WIDTH, + height = height, + ); + + for (row_index, row) in table.rows.iter().enumerate() { + xml.push_str(""); + for (col_index, cell) in row.iter().enumerate() { + xml.push_str(&render_table_cell( + cell, + row_index, + col_index, + table.col_widths[col_index], + )); + } + xml.push_str(""); + } + + xml.push_str(""); + xml +} + +fn render_table_cell(text: &str, row_index: usize, col_index: usize, width: u32) -> String { + let inner_width = width.saturating_sub(TABLE_CELL_INNER_WIDTH_DELTA); + format!( + concat!( + "", + "", + "", + "{text}", + "{trailing_run}", + "", + "", + "", + "", + "", + "", + "" + ), + text = escape_text(text), + trailing_run = if col_index > 0 { + "" + } else { + "" + }, + inner_width = inner_width, + col_index = col_index, + row_index = row_index, + width = width, + height = TABLE_ROW_HEIGHT, + ) +} + +fn render_standard_lineseg() -> &'static str { + "" +} + +fn render_lineseg( + style: LineSegProfile, + list_vertpos: &mut u32, + quote_vertpos: &mut u32, +) -> String { + match style { + LineSegProfile::Standard => render_standard_lineseg().to_string(), + LineSegProfile::ListLevel1 => { + render_list_lineseg(LIST_LEVEL1_HORZ_POS, LIST_LEVEL1_HORZ_SIZE, list_vertpos) + } + LineSegProfile::ListLevel2 => { + render_list_lineseg(LIST_LEVEL2_HORZ_POS, LIST_LEVEL2_HORZ_SIZE, list_vertpos) + } + LineSegProfile::QuoteLegacy => { + render_quote_lineseg(LEGACY_QUOTE_HORZ_POS, LEGACY_QUOTE_HORZ_SIZE, quote_vertpos) + } + LineSegProfile::QuoteLevel1 => { + render_standard_offset_lineseg(QUOTE_LEVEL1_HORZ_POS, QUOTE_LEVEL1_HORZ_SIZE) + } + LineSegProfile::QuoteLevel2 => { + render_standard_offset_lineseg(QUOTE_LEVEL2_HORZ_POS, QUOTE_LEVEL2_HORZ_SIZE) + } + } +} + +fn render_standard_offset_lineseg(horzpos: u32, horzsize: u32) -> String { + format!( + "", + horzpos = horzpos, + horzsize = horzsize, + ) +} + +fn render_list_lineseg(horzpos: u32, horzsize: u32, list_vertpos: &mut u32) -> String { + let current_vertpos = *list_vertpos; + *list_vertpos += LIST_LINESEG_INCREMENT; + format!( + "", + vertpos = current_vertpos, + vertsize = LIST_LINESEG_VERT_SIZE, + textheight = LIST_LINESEG_TEXT_HEIGHT, + baseline = LIST_LINESEG_BASELINE, + spacing = LIST_LINESEG_SPACING, + horzpos = horzpos, + horzsize = horzsize, + flags = LIST_LINESEG_FLAGS, + ) +} + +fn render_quote_lineseg(horzpos: u32, horzsize: u32, quote_vertpos: &mut u32) -> String { + let current_vertpos = *quote_vertpos; + *quote_vertpos += LEGACY_QUOTE_LINESEG_INCREMENT; + format!( + "", + vertpos = current_vertpos, + horzpos = horzpos, + horzsize = horzsize, + ) +} + +fn render_table_lineseg() -> String { + format!( + "", + size = TABLE_OUTER_LINESEG_SIZE, + baseline = TABLE_OUTER_BASELINE, + ) +} + +pub(super) fn render_runs( + inlines: &[Inline], + default_char_pr: u32, + strict_mode: bool, + next_link_index: &mut u64, +) -> Result, CoreRsError> { + let mut runs = Vec::new(); + for inline in inlines { + collect_inline_runs( + inline, + default_char_pr, + strict_mode, + &mut runs, + next_link_index, + )?; + } + Ok(merge_adjacent_runs(runs)) +} + +fn collect_inline_runs( + inline: &Inline, + default_char_pr: u32, + strict_mode: bool, + runs: &mut Vec, + next_link_index: &mut u64, +) -> Result<(), CoreRsError> { + match inline { + Inline::Text(value) => push_run(runs, default_char_pr, value.clone()), + Inline::Emphasis(children) => { + for child in children { + collect_inline_runs( + child, + emphasis_char_pr(default_char_pr), + strict_mode, + runs, + next_link_index, + )?; + } + } + Inline::Strong(children) => { + for child in children { + collect_inline_runs( + child, + strong_char_pr(default_char_pr), + strict_mode, + runs, + next_link_index, + )?; + } + } + Inline::Code(code) => push_run(runs, code_char_pr(default_char_pr), code.clone()), + Inline::Link { text, url } => { + let label = flatten_inline_children(text, strict_mode)?; + let visible = if label.trim().is_empty() { + url.clone() + } else { + label + }; + runs.push(RunSpec::Hyperlink { + char_pr: link_char_pr(default_char_pr), + field_begin_id: 2_107_483_186 + *next_link_index, + field_id: 627_600_491 + *next_link_index, + url: url.clone(), + text: visible, + }); + *next_link_index += 1; + } + Inline::Image(image) => { + if strict_mode { + return Err(CoreRsError::UnsupportedFeature( + "HWPX core does not support image".to_string(), + )); + } + let fallback = if image.alt.trim().is_empty() { + "[image]".to_string() + } else { + image.alt.clone() + }; + push_run(runs, default_char_pr, fallback); + } + Inline::HardBreak => push_run(runs, default_char_pr, " ".to_string()), + } + Ok(()) +} + +fn merge_adjacent_runs(runs: Vec) -> Vec { + let mut merged = Vec::new(); + for run in runs { + if let Some(RunSpec::Text { + char_pr: last_char_pr, + text: last_text, + }) = merged.last_mut() + { + if let RunSpec::Text { char_pr, text } = &run { + if *last_char_pr == *char_pr { + last_text.push_str(text); + continue; + } + } + } + merged.push(run); + } + merged +} + +fn push_run(runs: &mut Vec, char_pr: u32, text: String) { + if !text.is_empty() { + runs.push(RunSpec::Text { char_pr, text }); + } +} + +fn strong_char_pr(default_char_pr: u32) -> u32 { + match default_char_pr { + H1_CHAR_PR | H2_CHAR_PR | H3_CHAR_PR | H4_CHAR_PR | BOLD_CHAR_PR => default_char_pr, + _ => BOLD_CHAR_PR, + } +} + +fn emphasis_char_pr(default_char_pr: u32) -> u32 { + match default_char_pr { + H1_CHAR_PR | H2_CHAR_PR | H3_CHAR_PR | H4_CHAR_PR => default_char_pr, + _ => ITALIC_CHAR_PR, + } +} + +fn code_char_pr(default_char_pr: u32) -> u32 { + match default_char_pr { + H1_CHAR_PR | H2_CHAR_PR | H3_CHAR_PR | H4_CHAR_PR => default_char_pr, + _ => CODE_CHAR_PR, + } +} + +fn link_char_pr(default_char_pr: u32) -> u32 { + match default_char_pr { + H1_CHAR_PR | H2_CHAR_PR | H3_CHAR_PR | H4_CHAR_PR => default_char_pr, + _ => LINK_CHAR_PR, + } +} diff --git a/packages/core-rs/src/generators/hwpx/section_xml/shared.rs b/packages/core-rs/src/generators/hwpx/section_xml/shared.rs new file mode 100644 index 0000000..17a858f --- /dev/null +++ b/packages/core-rs/src/generators/hwpx/section_xml/shared.rs @@ -0,0 +1,78 @@ +use crate::utils::xml_helper::escape_text; + +pub(super) const SECTION_PREAMBLE_BODY: &str = concat!( + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "" +); + +pub(super) struct HyperlinkFieldSpec<'a> { + pub(super) char_pr: u32, + pub(super) field_begin_id: u64, + pub(super) field_id: u64, + pub(super) command: &'a str, + pub(super) path: &'a str, + pub(super) text: &'a str, + pub(super) trailing_empty_text: bool, +} + +pub(super) fn render_section_preamble_run(char_pr: u32) -> String { + format!( + "{body}", + char_pr = char_pr, + body = SECTION_PREAMBLE_BODY, + ) +} + +pub(super) fn hyperlink_command(path: &str) -> String { + format!("{};1;0;0;", path.replace("://", "\\://")) +} + +pub(super) fn compat_hyperlink_path(url: &str) -> String { + if url.ends_with('/') { + url.to_string() + } else { + format!("{url}/") + } +} + +pub(super) fn render_hyperlink_run(spec: HyperlinkFieldSpec<'_>) -> String { + format!( + concat!( + "", + "", + "", + "0", + "{command}", + "{path}", + "HWPHYPERLINK_TYPE_URL", + "HWPHYPERLINK_TARGET_BOOKMARK", + "HWPHYPERLINK_JUMP_CURRENTTAB", + "", + "{text}", + "", + "{trailing}", + "" + ), + char_pr = spec.char_pr, + begin_id = spec.field_begin_id, + field_id = spec.field_id, + command = escape_text(spec.command), + path = escape_text(spec.path), + text = escape_text(spec.text), + trailing = if spec.trailing_empty_text { + "" + } else { + "" + }, + ) +} diff --git a/packages/core-rs/src/generators/hwpx/section_xml/text.rs b/packages/core-rs/src/generators/hwpx/section_xml/text.rs new file mode 100644 index 0000000..47d2efb --- /dev/null +++ b/packages/core-rs/src/generators/hwpx/section_xml/text.rs @@ -0,0 +1,190 @@ +use super::{Block, CoreRsError, Inline, ListSemantic, SectionItem, TableSpec}; + +pub(super) fn flatten_inline_children( + inlines: &[Inline], + strict_mode: bool, +) -> Result { + let mut text = String::new(); + for inline in inlines { + match inline { + Inline::Text(value) => text.push_str(value), + Inline::Emphasis(children) | Inline::Strong(children) => { + text.push_str(&flatten_inline_children(children, strict_mode)?); + } + Inline::Code(code) => text.push_str(code), + Inline::Link { + text: children, + url, + } => { + let label = flatten_inline_children(children, strict_mode)?; + if label.trim().is_empty() { + text.push_str(url); + } else { + text.push_str(&label); + } + } + Inline::Image(image) => { + if strict_mode { + return Err(CoreRsError::UnsupportedFeature( + "HWPX core does not support image".to_string(), + )); + } + if image.alt.trim().is_empty() { + text.push_str("[image]"); + } else { + text.push_str(&image.alt); + } + } + Inline::HardBreak => text.push(' '), + } + } + Ok(text) +} + +pub(super) fn visible_paragraphs(items: &[SectionItem]) -> Vec { + let mut paragraphs = Vec::new(); + for item in items { + match item { + SectionItem::Paragraph(_, runs) => { + paragraphs.push(runs.iter().map(run_visible_text).collect()) + } + SectionItem::Table(table) => append_table_preview_paragraphs(table, &mut paragraphs), + } + } + paragraphs +} + +fn append_table_preview_paragraphs(table: &TableSpec, paragraphs: &mut Vec) { + for row in &table.rows { + paragraphs.push( + row.iter() + .map(|cell| format!("<{cell}>")) + .collect::>() + .join(""), + ); + } +} + +pub(super) fn flatten_block_to_paragraphs(block: &Block) -> Vec { + match block { + Block::Paragraph(content) | Block::Heading { content, .. } => { + vec![flatten_inline_children(content, false).unwrap_or_default()] + } + Block::BlockQuote(blocks) => blocks + .iter() + .flat_map(flatten_block_to_paragraphs) + .collect::>(), + Block::CodeBlock { code, .. } => code.lines().map(ToString::to_string).collect(), + Block::List(list) => list + .items + .iter() + .enumerate() + .map(|(index, item)| { + let prefix = if list.ordered { + format!("{}. ", list.start_index + index as u64) + } else { + "- ".to_string() + }; + let body = item + .blocks + .iter() + .flat_map(flatten_block_to_paragraphs) + .collect::>() + .join(" "); + format!("{prefix}{body}") + }) + .collect(), + Block::Table(table) => flatten_table_to_paragraphs(table), + Block::ThematicBreak => vec!["---".to_string()], + } +} + +fn flatten_table_to_paragraphs(table: &super::TableBlock) -> Vec { + let mut paragraphs = Vec::new(); + if !table.headers.is_empty() { + paragraphs.push( + table + .headers + .iter() + .map(|cell| flatten_inline_children(cell, false).unwrap_or_default()) + .collect::>() + .join(" | "), + ); + } + paragraphs.extend(table.rows.iter().map(|row| { + row.cells + .iter() + .map(|cell| flatten_inline_children(cell, false).unwrap_or_default()) + .collect::>() + .join(" | ") + })); + paragraphs +} + +pub(super) fn list_caret_position(items: &[SectionItem]) -> Option<(usize, usize)> { + let mut paragraph_index = 0; + let mut last_unordered_caret = None; + let mut ordered_carets = Vec::new(); + + for item in items { + match item { + SectionItem::Paragraph(style, runs) => { + let text = runs.iter().map(run_visible_text).collect::(); + let caret = (paragraph_index, text.chars().count()); + match style.list_semantic { + ListSemantic::None => {} + ListSemantic::Unordered => last_unordered_caret = Some(caret), + ListSemantic::OrderedSingleLevel + | ListSemantic::OrderedTopLevel + | ListSemantic::OrderedNestedLevel => { + ordered_carets.push((style.list_semantic, caret)); + } + } + paragraph_index += 1; + } + SectionItem::Table(table) => { + paragraph_index += table.rows.len(); + } + } + } + + if !ordered_carets.is_empty() { + let only_single_level = ordered_carets + .iter() + .all(|(semantic, _)| *semantic == ListSemantic::OrderedSingleLevel); + if only_single_level && ordered_carets.len() > 1 { + return ordered_carets + .get(ordered_carets.len() - 2) + .map(|(_, caret)| *caret); + } + return ordered_carets.last().map(|(_, caret)| *caret); + } + + last_unordered_caret +} + +pub(super) fn last_paragraph_caret(items: &[SectionItem]) -> Option<(usize, usize)> { + let mut paragraph_index = 0; + let mut last_caret = None; + + for item in items { + match item { + SectionItem::Paragraph(_, runs) => { + let text = runs.iter().map(run_visible_text).collect::(); + last_caret = Some((paragraph_index, text.chars().count())); + paragraph_index += 1; + } + SectionItem::Table(table) => { + paragraph_index += table.rows.len(); + } + } + } + + last_caret +} + +fn run_visible_text(run: &super::RunSpec) -> &str { + match run { + super::RunSpec::Text { text, .. } | super::RunSpec::Hyperlink { text, .. } => text, + } +} diff --git a/packages/core-rs/src/lib.rs b/packages/core-rs/src/lib.rs index 7a3da10..7efcb47 100644 --- a/packages/core-rs/src/lib.rs +++ b/packages/core-rs/src/lib.rs @@ -1,3 +1,4 @@ +mod diagnostics; mod error; mod generators; mod models; @@ -9,15 +10,159 @@ mod wasm; pub use error::CoreRsError; +use diagnostics::{analyze_markdown_impl, report_for_mode}; use generators::Generator; use generators::docx::DocxGenerator; use generators::hwpx::HwpxGenerator; use parser::markdown::MarkdownParser; +use serde::Serialize; +use thiserror::Error; fn default_document_options() -> (Option, Option, bool) { (None, None, true) } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +pub enum ConversionTarget { + Docx, + Hwpx, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +pub enum IssueSeverity { + Warning, + Error, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct ConversionIssue { + pub feature: String, + pub message: String, + pub severity: IssueSeverity, + pub degraded: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct ConversionReport { + pub issues: Vec, + pub degraded: bool, + pub unsupported_count: usize, + pub fallback_count: usize, +} + +impl ConversionReport { + pub(crate) fn from_issues(issues: Vec) -> Self { + let mut deduped = Vec::with_capacity(issues.len()); + for issue in issues { + if !deduped.contains(&issue) { + deduped.push(issue); + } + } + + let unsupported_count = deduped + .iter() + .filter(|issue| issue.severity == IssueSeverity::Error) + .count(); + let fallback_count = deduped.iter().filter(|issue| issue.degraded).count(); + let degraded = deduped.iter().any(|issue| issue.degraded); + Self { + issues: deduped, + degraded, + unsupported_count, + fallback_count, + } + } + + pub(crate) fn with_error_issue(&self, error: &CoreRsError) -> Self { + let mut issues = self.issues.clone(); + if let Some(existing) = issues + .iter_mut() + .find(|existing| issue_matches_error(existing, error)) + { + existing.message = error.to_string(); + existing.severity = IssueSeverity::Error; + existing.degraded = false; + return Self::from_issues(issues); + } + + issues.push(error_issue(error)); + Self::from_issues(issues) + } +} + +fn error_issue(error: &CoreRsError) -> ConversionIssue { + match error { + CoreRsError::UnsupportedFeature(_) => ConversionIssue { + feature: "unsupported feature".to_string(), + message: error.to_string(), + severity: IssueSeverity::Error, + degraded: false, + }, + CoreRsError::InvalidMarkdown(_) => ConversionIssue { + feature: "invalid markdown".to_string(), + message: error.to_string(), + severity: IssueSeverity::Error, + degraded: false, + }, + CoreRsError::InvalidOption(_) => ConversionIssue { + feature: "invalid option".to_string(), + message: error.to_string(), + severity: IssueSeverity::Error, + degraded: false, + }, + CoreRsError::Zip(_) => ConversionIssue { + feature: "zip packaging failed".to_string(), + message: error.to_string(), + severity: IssueSeverity::Error, + degraded: false, + }, + CoreRsError::Io(_) => ConversionIssue { + feature: "io failure".to_string(), + message: error.to_string(), + severity: IssueSeverity::Error, + degraded: false, + }, + } +} + +fn issue_matches_error(issue: &ConversionIssue, error: &CoreRsError) -> bool { + match error { + CoreRsError::UnsupportedFeature(feature) => match feature.as_str() { + "html" => issue.feature == "HTML", + "footnote" => issue.feature == "footnote", + "task list" => issue.feature == "task list", + "math" => issue.feature == "math", + "nested list depth > 2" => issue.feature == "deep nested list", + value if value.contains("ordered list") => { + issue.feature == "HWPX ordered list in strict mode" + } + "HWPX core does not support image" => issue.feature == "HWPX image", + value if value.starts_with("HWPX core does not support ") => { + issue.feature == "HWPX unsupported rich block" + } + _ => false, + }, + CoreRsError::InvalidMarkdown(_) + | CoreRsError::InvalidOption(_) + | CoreRsError::Zip(_) + | CoreRsError::Io(_) => false, + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct GenerationResult { + pub bytes: Vec, + pub report: ConversionReport, +} + +#[derive(Debug, Error, Serialize)] +#[error("{error}")] +pub struct GenerationFailure { + #[serde(skip_serializing)] + pub error: CoreRsError, + pub report: ConversionReport, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum HwpxParagraphAlign { Left, @@ -81,6 +226,28 @@ pub fn generate_docx(markdown: &str, options: DocxOptions) -> Result, Co DocxGenerator::new(options).generate(&document) } +pub fn analyze_markdown(markdown: &str, target: ConversionTarget) -> ConversionReport { + analyze_markdown_impl(markdown, target) +} + +pub fn generate_docx_with_report( + markdown: &str, + options: DocxOptions, +) -> Result { + let report = report_for_mode( + &analyze_markdown(markdown, ConversionTarget::Docx), + ConversionTarget::Docx, + options.strict_mode, + ); + match generate_docx(markdown, options) { + Ok(bytes) => Ok(GenerationResult { bytes, report }), + Err(error) => Err(GenerationFailure { + report: report.with_error_issue(&error), + error, + }), + } +} + pub fn generate_hwpx(markdown: &str, options: HwpxOptions) -> Result, CoreRsError> { let normalized_markdown; let markdown = if options.strict_mode { @@ -93,6 +260,24 @@ pub fn generate_hwpx(markdown: &str, options: HwpxOptions) -> Result, Co HwpxGenerator::new(options).generate(&document) } +pub fn generate_hwpx_with_report( + markdown: &str, + options: HwpxOptions, +) -> Result { + let report = report_for_mode( + &analyze_markdown(markdown, ConversionTarget::Hwpx), + ConversionTarget::Hwpx, + options.strict_mode, + ); + match generate_hwpx(markdown, options) { + Ok(bytes) => Ok(GenerationResult { bytes, report }), + Err(error) => Err(GenerationFailure { + report: report.with_error_issue(&error), + error, + }), + } +} + fn normalize_hwpx_markdown(markdown: &str) -> String { let mut normalized = String::with_capacity(markdown.len()); for segment in markdown.split_inclusive('\n') { diff --git a/packages/core-rs/src/parser/markdown.rs b/packages/core-rs/src/parser/markdown.rs deleted file mode 100644 index 4ca42c5..0000000 --- a/packages/core-rs/src/parser/markdown.rs +++ /dev/null @@ -1,730 +0,0 @@ -mod data_image; -mod state; - -#[cfg(test)] -mod tests; - -use pulldown_cmark::{Event, Options, Parser, Tag, TagEnd}; - -use self::data_image::{DataImageError, image_fallback_text, parse_data_uri}; -use self::state::{ - Container, InlineContext, append_fallback_separator, code_block_language, current_list_depth, - current_table_mut, empty_to_none, flatten_inline_text, heading_level, push_inline_merged, - table_alignment, trim_trailing_fallback_whitespace, unsupported_block_feature, - unsupported_block_nesting_end, unsupported_block_nesting_tag, -}; -use crate::error::CoreRsError; -use crate::models::block::{Block, ListBlock, TableBlock, TableRow}; -use crate::models::document::Document; -use crate::models::inline::{ImageData, Inline}; - -const MAX_TOTAL_DATA_URI_IMAGE_BYTES: usize = 16 * 1024 * 1024; - -pub struct MarkdownParser { - strict_mode: bool, -} - -impl MarkdownParser { - pub fn new(strict_mode: bool) -> Self { - Self { strict_mode } - } - - pub fn parse(&self, markdown: &str) -> Result { - let parser = Parser::new_ext(markdown, Options::all()); - let mut blocks = Vec::new(); - let mut containers = Vec::new(); - let mut inline_stack = Vec::new(); - let mut unsupported_block_depth = 0usize; - let mut unsupported_block_content = Vec::new(); - let mut total_embedded_image_bytes = 0usize; - - for event in parser { - if unsupported_block_depth > 0 { - self.handle_unsupported_event( - event, - &mut blocks, - &mut containers, - &mut unsupported_block_depth, - &mut unsupported_block_content, - )?; - continue; - } - - match event { - Event::Start(tag) => { - if matches!(tag, Tag::List(_)) && current_list_depth(&containers) >= 2 { - if self.strict_mode { - return Err(CoreRsError::UnsupportedFeature( - "nested list depth > 2".to_string(), - )); - } - unsupported_block_depth = 1; - continue; - } - - if let Some(feature) = unsupported_block_feature(&tag) { - if self.strict_mode { - return Err(CoreRsError::UnsupportedFeature(feature.to_string())); - } - unsupported_block_depth = 1; - continue; - } - - self.handle_start(tag, &mut containers, &mut inline_stack)?; - } - Event::End(tag) => { - self.handle_end( - tag, - &mut blocks, - &mut containers, - &mut inline_stack, - &mut total_embedded_image_bytes, - )?; - } - Event::Text(text) => { - self.push_text( - text.into_string(), - &mut blocks, - &mut containers, - &mut inline_stack, - )?; - } - Event::Code(code) => { - self.push_inline_or_code( - Inline::Code(code.into_string()), - None, - &mut blocks, - &mut containers, - &mut inline_stack, - )?; - } - Event::SoftBreak => { - self.push_soft_break(&mut blocks, &mut containers, &mut inline_stack)?; - } - Event::HardBreak => { - self.push_hard_break(&mut blocks, &mut containers, &mut inline_stack)?; - } - Event::Rule => { - self.push_block(Block::ThematicBreak, &mut blocks, &mut containers)?; - } - Event::Html(html) | Event::InlineHtml(html) => { - if self.strict_mode { - return Err(CoreRsError::UnsupportedFeature("html".to_string())); - } - self.push_fallback_text( - html.into_string(), - &mut blocks, - &mut containers, - &mut inline_stack, - )?; - } - Event::FootnoteReference(reference) => { - if self.strict_mode { - return Err(CoreRsError::UnsupportedFeature("footnote".to_string())); - } - self.push_fallback_text( - format!("[^{reference}]"), - &mut blocks, - &mut containers, - &mut inline_stack, - )?; - } - Event::TaskListMarker(checked) => { - if self.strict_mode { - return Err(CoreRsError::UnsupportedFeature("task list".to_string())); - } - self.push_fallback_text( - if checked { - "[x] ".to_string() - } else { - "[ ] ".to_string() - }, - &mut blocks, - &mut containers, - &mut inline_stack, - )?; - } - Event::InlineMath(math) | Event::DisplayMath(math) => { - if self.strict_mode { - return Err(CoreRsError::UnsupportedFeature("math".to_string())); - } - self.push_fallback_text( - math.into_string(), - &mut blocks, - &mut containers, - &mut inline_stack, - )?; - } - } - } - - if !inline_stack.is_empty() || !containers.is_empty() { - return Err(CoreRsError::InvalidMarkdown( - "unbalanced markdown structure".to_string(), - )); - } - - Ok(Document { blocks }) - } - - fn handle_start( - &self, - tag: Tag<'_>, - containers: &mut Vec, - inline_stack: &mut Vec, - ) -> Result<(), CoreRsError> { - match tag { - Tag::Paragraph => containers.push(Container::Paragraph(Vec::new())), - Tag::Heading { level, .. } => containers.push(Container::Heading { - level: heading_level(level), - content: Vec::new(), - }), - Tag::BlockQuote(_) => containers.push(Container::BlockQuote(Vec::new())), - Tag::CodeBlock(kind) => containers.push(Container::CodeBlock { - language: code_block_language(kind), - code: String::new(), - }), - Tag::List(start) => containers.push(Container::List(ListBlock { - ordered: start.is_some(), - start_index: start.unwrap_or(1), - items: Vec::new(), - })), - Tag::Item => containers.push(Container::ListItem(crate::models::block::ListItem { - blocks: Vec::new(), - })), - Tag::Table(alignments) => containers.push(Container::Table(state::TableBuilder { - aligns: alignments.into_iter().map(table_alignment).collect(), - headers: Vec::new(), - rows: Vec::new(), - })), - Tag::TableHead => containers.push(Container::TableHead(Vec::new())), - Tag::TableRow => containers.push(Container::TableRow(state::TableRowBuilder { - cells: Vec::new(), - })), - Tag::TableCell => containers.push(Container::TableCell(Vec::new())), - Tag::Emphasis => inline_stack.push(InlineContext::Emphasis(Vec::new())), - Tag::Strong => inline_stack.push(InlineContext::Strong(Vec::new())), - Tag::Link { - dest_url, title, .. - } => inline_stack.push(InlineContext::Link { - url: dest_url.into_string(), - title: empty_to_none(title.into_string()), - content: Vec::new(), - }), - Tag::Image { - dest_url, title, .. - } => inline_stack.push(InlineContext::Image { - url: dest_url.into_string(), - title: empty_to_none(title.into_string()), - alt: Vec::new(), - }), - Tag::DefinitionList - | Tag::DefinitionListTitle - | Tag::DefinitionListDefinition - | Tag::MetadataBlock(_) - | Tag::Strikethrough - | Tag::HtmlBlock - | Tag::FootnoteDefinition(_) => { - if self.strict_mode { - return Err(CoreRsError::UnsupportedFeature( - "extended markdown syntax".to_string(), - )); - } - } - } - - Ok(()) - } - - fn handle_end( - &self, - tag: TagEnd, - blocks: &mut Vec, - containers: &mut Vec, - inline_stack: &mut Vec, - total_embedded_image_bytes: &mut usize, - ) -> Result<(), CoreRsError> { - match tag { - TagEnd::Paragraph => { - let Some(Container::Paragraph(content)) = containers.pop() else { - return Err(CoreRsError::InvalidMarkdown( - "paragraph close without open".to_string(), - )); - }; - self.push_block(Block::Paragraph(content), blocks, containers)?; - } - TagEnd::Heading(_) => { - let Some(Container::Heading { level, content }) = containers.pop() else { - return Err(CoreRsError::InvalidMarkdown( - "heading close without open".to_string(), - )); - }; - self.push_block(Block::Heading { level, content }, blocks, containers)?; - } - TagEnd::BlockQuote(_) => { - let Some(Container::BlockQuote(quoted)) = containers.pop() else { - return Err(CoreRsError::InvalidMarkdown( - "blockquote close without open".to_string(), - )); - }; - self.push_block(Block::BlockQuote(quoted), blocks, containers)?; - } - TagEnd::CodeBlock => { - let Some(Container::CodeBlock { language, code }) = containers.pop() else { - return Err(CoreRsError::InvalidMarkdown( - "code block close without open".to_string(), - )); - }; - self.push_block(Block::CodeBlock { language, code }, blocks, containers)?; - } - TagEnd::List(_) => { - let Some(Container::List(list)) = containers.pop() else { - return Err(CoreRsError::InvalidMarkdown( - "list close without open".to_string(), - )); - }; - self.push_block(Block::List(list), blocks, containers)?; - } - TagEnd::Item => { - let Some(Container::ListItem(item)) = containers.pop() else { - return Err(CoreRsError::InvalidMarkdown( - "list item close without open".to_string(), - )); - }; - let Some(Container::List(list)) = containers.last_mut() else { - return Err(CoreRsError::InvalidMarkdown( - "list item close without list".to_string(), - )); - }; - list.items.push(item); - } - TagEnd::Table => { - let Some(Container::Table(table)) = containers.pop() else { - return Err(CoreRsError::InvalidMarkdown( - "table close without open".to_string(), - )); - }; - self.push_block( - Block::Table(TableBlock { - aligns: table.aligns, - headers: table.headers, - rows: table.rows, - }), - blocks, - containers, - )?; - } - TagEnd::TableHead => { - let Some(Container::TableHead(headers)) = containers.pop() else { - return Err(CoreRsError::InvalidMarkdown( - "table head close without open".to_string(), - )); - }; - current_table_mut(containers)?.headers = headers; - } - TagEnd::TableRow => { - let Some(Container::TableRow(row)) = containers.pop() else { - return Err(CoreRsError::InvalidMarkdown( - "table row close without open".to_string(), - )); - }; - current_table_mut(containers)? - .rows - .push(TableRow { cells: row.cells }); - } - TagEnd::TableCell => { - let Some(Container::TableCell(cell)) = containers.pop() else { - return Err(CoreRsError::InvalidMarkdown( - "table cell close without open".to_string(), - )); - }; - match containers.last_mut() { - Some(Container::TableRow(row)) => row.cells.push(cell), - Some(Container::TableHead(headers)) => headers.push(cell), - _ => { - return Err(CoreRsError::InvalidMarkdown( - "table cell close without row".to_string(), - )); - } - } - } - TagEnd::Emphasis => { - let Some(InlineContext::Emphasis(content)) = inline_stack.pop() else { - return Err(CoreRsError::InvalidMarkdown( - "emphasis close without open".to_string(), - )); - }; - self.push_inline_or_code( - Inline::Emphasis(content), - None, - blocks, - containers, - inline_stack, - )?; - } - TagEnd::Strong => { - let Some(InlineContext::Strong(content)) = inline_stack.pop() else { - return Err(CoreRsError::InvalidMarkdown( - "strong close without open".to_string(), - )); - }; - self.push_inline_or_code( - Inline::Strong(content), - None, - blocks, - containers, - inline_stack, - )?; - } - TagEnd::Link => { - let Some(InlineContext::Link { - url, - title, - content, - }) = inline_stack.pop() - else { - return Err(CoreRsError::InvalidMarkdown( - "link close without open".to_string(), - )); - }; - let _ = title; - match validate_link_url(&url) { - Ok(()) => { - self.push_inline_or_code( - Inline::Link { text: content, url }, - None, - blocks, - containers, - inline_stack, - )?; - } - Err(_) if !self.strict_mode => { - let fallback_text = flatten_inline_text(&content); - let fallback_text = if fallback_text.is_empty() { - url - } else { - fallback_text - }; - self.push_fallback_text(fallback_text, blocks, containers, inline_stack)?; - } - Err(error) => return Err(error), - } - } - TagEnd::Image => { - let Some(InlineContext::Image { url, title, alt }) = inline_stack.pop() else { - return Err(CoreRsError::InvalidMarkdown( - "image close without open".to_string(), - )); - }; - let alt_text = flatten_inline_text(&alt); - match parse_data_uri(&url) { - Ok((mime_type, extension, data)) => { - if total_embedded_image_bytes.saturating_add(data.len()) - > MAX_TOTAL_DATA_URI_IMAGE_BYTES - { - let error = DataImageError::TooLarge { - message: format!( - "total embedded image bytes exceed maximum supported size of {MAX_TOTAL_DATA_URI_IMAGE_BYTES} bytes" - ), - }; - if self.strict_mode { - return Err(error.into_core_error()); - } - self.push_fallback_text( - image_fallback_text(&alt_text, &url, error.fallback_kind()), - blocks, - containers, - inline_stack, - )?; - } else { - *total_embedded_image_bytes += data.len(); - self.push_inline_or_code( - Inline::Image(ImageData { - alt: alt_text, - title, - mime_type, - extension, - data, - }), - None, - blocks, - containers, - inline_stack, - )?; - } - } - Err(error) if !self.strict_mode => { - self.push_fallback_text( - image_fallback_text(&alt_text, &url, error.fallback_kind()), - blocks, - containers, - inline_stack, - )?; - } - Err(error) => return Err(error.into_core_error()), - } - } - TagEnd::DefinitionList - | TagEnd::DefinitionListTitle - | TagEnd::DefinitionListDefinition - | TagEnd::MetadataBlock(_) - | TagEnd::Strikethrough - | TagEnd::HtmlBlock - | TagEnd::FootnoteDefinition => {} - } - - Ok(()) - } - - fn handle_unsupported_event( - &self, - event: Event<'_>, - blocks: &mut Vec, - containers: &mut [Container], - unsupported_block_depth: &mut usize, - unsupported_block_content: &mut Vec, - ) -> Result<(), CoreRsError> { - match event { - Event::Start(tag) => { - if matches!(tag, Tag::Item) && !unsupported_block_content.is_empty() { - append_fallback_separator(unsupported_block_content); - } - if unsupported_block_nesting_tag(&tag) { - *unsupported_block_depth += 1; - } - } - Event::End(tag) => { - if unsupported_block_nesting_end(&tag) { - *unsupported_block_depth -= 1; - if *unsupported_block_depth == 0 { - trim_trailing_fallback_whitespace(unsupported_block_content); - if !unsupported_block_content.is_empty() { - self.push_block( - Block::Paragraph(std::mem::take(unsupported_block_content)), - blocks, - containers, - )?; - } - } - } - } - Event::Text(text) - | Event::Code(text) - | Event::Html(text) - | Event::InlineHtml(text) - | Event::InlineMath(text) - | Event::DisplayMath(text) => { - push_inline_merged(unsupported_block_content, Inline::Text(text.into_string())); - } - Event::SoftBreak => { - push_inline_merged(unsupported_block_content, Inline::Text(" ".to_string())); - } - Event::HardBreak => unsupported_block_content.push(Inline::HardBreak), - Event::Rule => { - push_inline_merged(unsupported_block_content, Inline::Text("---".to_string())); - } - Event::FootnoteReference(reference) => { - push_inline_merged( - unsupported_block_content, - Inline::Text(format!("[^{reference}]")), - ); - } - Event::TaskListMarker(checked) => { - push_inline_merged( - unsupported_block_content, - Inline::Text(if checked { - "[x] ".to_string() - } else { - "[ ] ".to_string() - }), - ); - } - } - - Ok(()) - } - - fn push_text( - &self, - text: String, - blocks: &mut Vec, - containers: &mut Vec, - inline_stack: &mut Vec, - ) -> Result<(), CoreRsError> { - self.push_inline_or_code(Inline::Text(text), None, blocks, containers, inline_stack) - } - - fn push_soft_break( - &self, - blocks: &mut Vec, - containers: &mut Vec, - inline_stack: &mut Vec, - ) -> Result<(), CoreRsError> { - self.push_inline_or_code( - Inline::Text(" ".to_string()), - Some('\n'), - blocks, - containers, - inline_stack, - ) - } - - fn push_hard_break( - &self, - blocks: &mut Vec, - containers: &mut Vec, - inline_stack: &mut Vec, - ) -> Result<(), CoreRsError> { - self.push_inline_or_code( - Inline::HardBreak, - Some('\n'), - blocks, - containers, - inline_stack, - ) - } - - fn push_inline_or_code( - &self, - inline: Inline, - code_break: Option, - blocks: &mut Vec, - containers: &mut Vec, - inline_stack: &mut Vec, - ) -> Result<(), CoreRsError> { - if let Some(last_inline) = inline_stack.last_mut() { - last_inline.push(inline); - return Ok(()); - } - - if let Some(Container::CodeBlock { code, .. }) = containers.last_mut() { - match (inline, code_break) { - (Inline::Text(text), _) | (Inline::Code(text), _) => code.push_str(&text), - (Inline::HardBreak, Some(break_char)) => code.push(break_char), - (Inline::HardBreak, None) => code.push('\n'), - _ => { - return Err(CoreRsError::InvalidMarkdown( - "invalid inline inside code block".to_string(), - )); - } - } - return Ok(()); - } - - if let Some( - Container::Paragraph(content) - | Container::TableCell(content) - | Container::Heading { content, .. }, - ) = containers.last_mut() - { - push_inline_merged(content, inline); - return Ok(()); - } - - if Self::push_inline_to_implicit_paragraph(containers, inline.clone()) { - return Ok(()); - } - - match inline { - Inline::Text(text) if !text.is_empty() => { - self.push_fallback_text(text, blocks, containers, inline_stack) - } - Inline::HardBreak => Err(CoreRsError::InvalidMarkdown( - "hard break appeared outside a text container".to_string(), - )), - _ => Err(CoreRsError::InvalidMarkdown( - "inline content appeared outside a text container".to_string(), - )), - } - } - - fn push_inline_to_implicit_paragraph(containers: &mut [Container], inline: Inline) -> bool { - match containers.last_mut() { - Some(Container::ListItem(item)) => { - Self::append_inline_to_paragraph_block(&mut item.blocks, inline); - true - } - Some(Container::BlockQuote(blocks)) => { - Self::append_inline_to_paragraph_block(blocks, inline); - true - } - _ => false, - } - } - - fn append_inline_to_paragraph_block(blocks: &mut Vec, inline: Inline) { - match blocks.last_mut() { - Some(Block::Paragraph(content)) => push_inline_merged(content, inline), - _ => blocks.push(Block::Paragraph(vec![inline])), - } - } - - fn push_fallback_text( - &self, - text: String, - blocks: &mut Vec, - containers: &mut Vec, - inline_stack: &mut Vec, - ) -> Result<(), CoreRsError> { - if text.is_empty() { - return Ok(()); - } - - if !inline_stack.is_empty() { - self.push_inline_or_code(Inline::Text(text), None, blocks, containers, inline_stack) - } else if let Some( - Container::Paragraph(_) - | Container::Heading { .. } - | Container::TableCell(_) - | Container::CodeBlock { .. }, - ) = containers.last() - { - self.push_inline_or_code(Inline::Text(text), None, blocks, containers, inline_stack) - } else { - self.push_block( - Block::Paragraph(vec![Inline::Text(text)]), - blocks, - containers, - ) - } - } - - fn push_block( - &self, - block: Block, - blocks: &mut Vec, - containers: &mut [Container], - ) -> Result<(), CoreRsError> { - if let Some(container) = containers.last_mut() { - match container { - Container::BlockQuote(quoted) => quoted.push(block), - Container::ListItem(item) => item.blocks.push(block), - _ => { - return Err(CoreRsError::InvalidMarkdown( - "block appeared in an invalid container".to_string(), - )); - } - } - } else { - blocks.push(block); - } - - Ok(()) - } -} - -fn validate_link_url(url: &str) -> Result<(), CoreRsError> { - let Some((scheme, _)) = url.split_once(':') else { - return Err(CoreRsError::UnsupportedFeature( - "relative link targets".to_string(), - )); - }; - let normalized = scheme.to_ascii_lowercase(); - if matches!(normalized.as_str(), "http" | "https" | "mailto") { - Ok(()) - } else { - Err(CoreRsError::UnsupportedFeature(format!( - "unsupported link scheme: {scheme}" - ))) - } -} diff --git a/packages/core-rs/src/parser/markdown/data_image.rs b/packages/core-rs/src/parser/markdown/data_image.rs index 333f2b0..d05685f 100644 --- a/packages/core-rs/src/parser/markdown/data_image.rs +++ b/packages/core-rs/src/parser/markdown/data_image.rs @@ -12,7 +12,7 @@ pub(super) enum ImageFallbackKind { } #[derive(Debug)] -pub(super) enum DataImageError { +pub(crate) enum DataImageError { NotDataUrl, InvalidDataUri, NonBase64, @@ -29,7 +29,7 @@ impl DataImageError { } } - pub(super) fn into_core_error(self) -> CoreRsError { + pub(crate) fn into_core_error(self) -> CoreRsError { match self { Self::NotDataUrl => CoreRsError::UnsupportedFeature("image".to_string()), Self::InvalidDataUri => { @@ -64,7 +64,7 @@ pub(super) fn image_fallback_text(alt_text: &str, url: &str, kind: ImageFallback } } -pub(super) fn parse_data_uri(url: &str) -> Result<(String, String, Vec), DataImageError> { +pub(crate) fn parse_data_uri(url: &str) -> Result<(String, String, Vec), DataImageError> { let Some(rest) = url.strip_prefix("data:") else { return Err(DataImageError::NotDataUrl); }; diff --git a/packages/core-rs/src/parser/markdown/events.rs b/packages/core-rs/src/parser/markdown/events.rs index 9158da1..18b01f8 100644 --- a/packages/core-rs/src/parser/markdown/events.rs +++ b/packages/core-rs/src/parser/markdown/events.rs @@ -44,16 +44,22 @@ impl MarkdownParser { rows: Vec::new(), })), Tag::TableHead => containers.push(Container::TableHead(Vec::new())), - Tag::TableRow => containers.push(Container::TableRow(TableRowBuilder { cells: Vec::new() })), + Tag::TableRow => { + containers.push(Container::TableRow(TableRowBuilder { cells: Vec::new() })) + } Tag::TableCell => containers.push(Container::TableCell(Vec::new())), Tag::Emphasis => inline_stack.push(InlineContext::Emphasis(Vec::new())), Tag::Strong => inline_stack.push(InlineContext::Strong(Vec::new())), - Tag::Link { dest_url, title, .. } => inline_stack.push(InlineContext::Link { + Tag::Link { + dest_url, title, .. + } => inline_stack.push(InlineContext::Link { url: dest_url.into_string(), title: empty_to_none(title.into_string()), content: Vec::new(), }), - Tag::Image { dest_url, title, .. } => inline_stack.push(InlineContext::Image { + Tag::Image { + dest_url, title, .. + } => inline_stack.push(InlineContext::Image { url: dest_url.into_string(), title: empty_to_none(title.into_string()), alt: Vec::new(), @@ -119,7 +125,9 @@ impl MarkdownParser { } TagEnd::List(_) => { let Some(Container::List(list)) = containers.pop() else { - return Err(CoreRsError::InvalidMarkdown("list close without open".to_string())); + return Err(CoreRsError::InvalidMarkdown( + "list close without open".to_string(), + )); }; self.push_block(Block::List(list), blocks, containers)?; } @@ -138,7 +146,9 @@ impl MarkdownParser { } TagEnd::Table => { let Some(Container::Table(table)) = containers.pop() else { - return Err(CoreRsError::InvalidMarkdown("table close without open".to_string())); + return Err(CoreRsError::InvalidMarkdown( + "table close without open".to_string(), + )); }; self.push_block( Block::Table(TableBlock { @@ -164,7 +174,9 @@ impl MarkdownParser { "table row close without open".to_string(), )); }; - current_table_mut(containers)?.rows.push(TableRow { cells: row.cells }); + current_table_mut(containers)? + .rows + .push(TableRow { cells: row.cells }); } TagEnd::TableCell => { let Some(Container::TableCell(cell)) = containers.pop() else { @@ -211,8 +223,15 @@ impl MarkdownParser { )?; } TagEnd::Link => { - let Some(InlineContext::Link { url, title, content }) = inline_stack.pop() else { - return Err(CoreRsError::InvalidMarkdown("link close without open".to_string())); + let Some(InlineContext::Link { + url, + title, + content, + }) = inline_stack.pop() + else { + return Err(CoreRsError::InvalidMarkdown( + "link close without open".to_string(), + )); }; let _ = title; match validate_link_url(&url) { @@ -232,19 +251,16 @@ impl MarkdownParser { } else { fallback_text }; - self.push_fallback_text( - fallback_text, - blocks, - containers, - inline_stack, - )?; + self.push_fallback_text(fallback_text, blocks, containers, inline_stack)?; } Err(error) => return Err(error), } } TagEnd::Image => { let Some(InlineContext::Image { url, title, alt }) = inline_stack.pop() else { - return Err(CoreRsError::InvalidMarkdown("image close without open".to_string())); + return Err(CoreRsError::InvalidMarkdown( + "image close without open".to_string(), + )); }; let alt_text = flatten_inline_text(&alt); match parse_data_uri(&url) { diff --git a/packages/core-rs/src/parser/markdown/mod.rs b/packages/core-rs/src/parser/markdown/mod.rs new file mode 100644 index 0000000..cf210cb --- /dev/null +++ b/packages/core-rs/src/parser/markdown/mod.rs @@ -0,0 +1,186 @@ +pub(crate) mod data_image; +mod events; +mod push; +mod state; + +#[cfg(test)] +mod tests; + +use pulldown_cmark::{Event, Options, Parser, Tag}; + +use self::state::{Container, current_list_depth, unsupported_block_feature}; +use crate::error::CoreRsError; +use crate::models::block::Block; +use crate::models::document::Document; +use crate::models::inline::Inline; + +const MAX_TOTAL_DATA_URI_IMAGE_BYTES: usize = 16 * 1024 * 1024; + +pub struct MarkdownParser { + strict_mode: bool, +} + +impl MarkdownParser { + pub fn new(strict_mode: bool) -> Self { + Self { strict_mode } + } + + pub fn parse(&self, markdown: &str) -> Result { + let parser = Parser::new_ext(markdown, Options::all()); + let mut blocks = Vec::new(); + let mut containers = Vec::new(); + let mut inline_stack = Vec::new(); + let mut unsupported_block_depth = 0usize; + let mut unsupported_block_content = Vec::new(); + let mut total_embedded_image_bytes = 0usize; + + for event in parser { + if unsupported_block_depth > 0 { + self.handle_unsupported_event( + event, + &mut blocks, + &mut containers, + &mut unsupported_block_depth, + &mut unsupported_block_content, + )?; + continue; + } + + match event { + Event::Start(tag) => { + if matches!(tag, Tag::List(_)) && current_list_depth(&containers) >= 2 { + if self.strict_mode { + return Err(CoreRsError::UnsupportedFeature( + "nested list depth > 2".to_string(), + )); + } + unsupported_block_depth = 1; + continue; + } + + if let Some(feature) = unsupported_block_feature(&tag) { + if self.strict_mode { + return Err(CoreRsError::UnsupportedFeature(feature.to_string())); + } + unsupported_block_depth = 1; + continue; + } + + self.handle_start(tag, &mut containers, &mut inline_stack)?; + } + Event::End(tag) => { + self.handle_end( + tag, + &mut blocks, + &mut containers, + &mut inline_stack, + &mut total_embedded_image_bytes, + )?; + } + Event::Text(text) => { + self.push_text( + text.into_string(), + &mut blocks, + &mut containers, + &mut inline_stack, + )?; + } + Event::Code(code) => { + self.push_inline_or_code( + Inline::Code(code.into_string()), + None, + &mut blocks, + &mut containers, + &mut inline_stack, + )?; + } + Event::SoftBreak => { + self.push_soft_break(&mut blocks, &mut containers, &mut inline_stack)?; + } + Event::HardBreak => { + self.push_hard_break(&mut blocks, &mut containers, &mut inline_stack)?; + } + Event::Rule => { + self.push_block(Block::ThematicBreak, &mut blocks, &mut containers)?; + } + Event::Html(html) | Event::InlineHtml(html) => { + if self.strict_mode { + return Err(CoreRsError::UnsupportedFeature("html".to_string())); + } + self.push_fallback_text( + html.into_string(), + &mut blocks, + &mut containers, + &mut inline_stack, + )?; + } + Event::FootnoteReference(reference) => { + if self.strict_mode { + return Err(CoreRsError::UnsupportedFeature("footnote".to_string())); + } + self.push_fallback_text( + format!("[^{reference}]"), + &mut blocks, + &mut containers, + &mut inline_stack, + )?; + } + Event::TaskListMarker(checked) => { + if self.strict_mode { + return Err(CoreRsError::UnsupportedFeature("task list".to_string())); + } + self.push_fallback_text( + if checked { + "[x] ".to_string() + } else { + "[ ] ".to_string() + }, + &mut blocks, + &mut containers, + &mut inline_stack, + )?; + } + Event::InlineMath(math) | Event::DisplayMath(math) => { + if self.strict_mode { + return Err(CoreRsError::UnsupportedFeature("math".to_string())); + } + self.push_fallback_text( + math.into_string(), + &mut blocks, + &mut containers, + &mut inline_stack, + )?; + } + } + } + + if !inline_stack.is_empty() || !containers.is_empty() { + return Err(CoreRsError::InvalidMarkdown( + "unbalanced markdown structure".to_string(), + )); + } + + Ok(Document { blocks }) + } + + fn push_inline_to_implicit_paragraph(containers: &mut [Container], inline: Inline) -> bool { + match containers.last_mut() { + Some(Container::ListItem(item)) => { + Self::append_inline_to_paragraph_block(&mut item.blocks, inline); + true + } + Some(Container::BlockQuote(blocks)) => { + Self::append_inline_to_paragraph_block(blocks, inline); + true + } + _ => false, + } + } + + fn append_inline_to_paragraph_block(blocks: &mut Vec, inline: Inline) { + match blocks.last_mut() { + Some(Block::Paragraph(content)) => state::push_inline_merged(content, inline), + _ => blocks.push(Block::Paragraph(vec![inline])), + } + } +} diff --git a/packages/core-rs/src/parser/markdown/push.rs b/packages/core-rs/src/parser/markdown/push.rs index ea8746e..87a1935 100644 --- a/packages/core-rs/src/parser/markdown/push.rs +++ b/packages/core-rs/src/parser/markdown/push.rs @@ -1,5 +1,5 @@ -use super::state::{Container, InlineContext, push_inline_merged}; use super::MarkdownParser; +use super::state::{Container, InlineContext, push_inline_merged}; use crate::error::CoreRsError; use crate::models::block::Block; use crate::models::inline::Inline; @@ -36,7 +36,13 @@ impl MarkdownParser { containers: &mut Vec, inline_stack: &mut Vec, ) -> Result<(), CoreRsError> { - self.push_inline_or_code(Inline::HardBreak, Some('\n'), blocks, containers, inline_stack) + self.push_inline_or_code( + Inline::HardBreak, + Some('\n'), + blocks, + containers, + inline_stack, + ) } pub(super) fn push_inline_or_code( @@ -76,6 +82,10 @@ impl MarkdownParser { return Ok(()); } + if Self::push_inline_to_implicit_paragraph(containers, inline.clone()) { + return Ok(()); + } + match inline { Inline::Text(text) if !text.is_empty() => { self.push_fallback_text(text, blocks, containers, inline_stack) @@ -111,7 +121,11 @@ impl MarkdownParser { { self.push_inline_or_code(Inline::Text(text), None, blocks, containers, inline_stack) } else { - self.push_block(Block::Paragraph(vec![Inline::Text(text)]), blocks, containers) + self.push_block( + Block::Paragraph(vec![Inline::Text(text)]), + blocks, + containers, + ) } } diff --git a/packages/core-rs/src/wasm.rs b/packages/core-rs/src/wasm.rs index 9e8921d..7c7a14c 100644 --- a/packages/core-rs/src/wasm.rs +++ b/packages/core-rs/src/wasm.rs @@ -1,6 +1,13 @@ +use base64::Engine; +use serde::Deserialize; +use serde::Serialize; use wasm_bindgen::prelude::*; -use crate::{DocxOptions, HwpxOptions, generate_docx, generate_hwpx}; +use crate::{ + ConversionTarget, DocxOptions, GenerationFailure, GenerationResult, HwpxOptions, + HwpxParagraphAlign, HwpxStyleOptions, analyze_markdown, generate_docx, + generate_docx_with_report, generate_hwpx, generate_hwpx_with_report, +}; #[wasm_bindgen(js_name = generateDocxBytes)] pub fn generate_docx_bytes( @@ -26,15 +33,153 @@ pub fn generate_hwpx_bytes( title: Option, author: Option, strict_mode: bool, + style_json: Option, ) -> Result, JsValue> { generate_hwpx( markdown, - HwpxOptions { + parse_hwpx_options(title, author, strict_mode, style_json)?, + ) + .map_err(|error| JsValue::from_str(&error.to_string())) +} + +#[wasm_bindgen(js_name = analyzeMarkdownJson)] +pub fn analyze_markdown_json(markdown: &str, target: &str) -> Result { + let target = match target { + "docx" => ConversionTarget::Docx, + "hwpx" => ConversionTarget::Hwpx, + _ => return Err(JsValue::from_str("target must be `docx` or `hwpx`")), + }; + serde_json::to_string(&analyze_markdown(markdown, target)) + .map_err(|error| JsValue::from_str(&error.to_string())) +} + +#[derive(Serialize)] +struct WasmGenerationResponse { + ok: bool, + #[serde(skip_serializing_if = "Option::is_none")] + bytes_base64: Option, + #[serde(skip_serializing_if = "Option::is_none")] + error: Option, + report: crate::ConversionReport, +} + +#[wasm_bindgen(js_name = generateDocxWithReportJson)] +pub fn generate_docx_with_report_json( + markdown: &str, + title: Option, + author: Option, + strict_mode: bool, +) -> Result { + let payload = match generate_docx_with_report( + markdown, + DocxOptions { title, author, strict_mode, - style: Default::default(), }, - ) - .map_err(|error| JsValue::from_str(&error.to_string())) + ) { + Ok(result) => WasmGenerationResponse::success(result), + Err(failure) => WasmGenerationResponse::failure(failure), + }; + serde_json::to_string(&payload).map_err(|error| JsValue::from_str(&error.to_string())) +} + +#[wasm_bindgen(js_name = generateHwpxWithReportJson)] +pub fn generate_hwpx_with_report_json( + markdown: &str, + title: Option, + author: Option, + strict_mode: bool, + style_json: Option, +) -> Result { + let payload = match generate_hwpx_with_report( + markdown, + parse_hwpx_options(title, author, strict_mode, style_json)?, + ) { + Ok(result) => WasmGenerationResponse::success(result), + Err(failure) => WasmGenerationResponse::failure(failure), + }; + serde_json::to_string(&payload).map_err(|error| JsValue::from_str(&error.to_string())) +} + +impl WasmGenerationResponse { + fn success(result: GenerationResult) -> Self { + Self { + ok: true, + bytes_base64: Some(base64::engine::general_purpose::STANDARD.encode(result.bytes)), + error: None, + report: result.report, + } + } + + fn failure(failure: GenerationFailure) -> Self { + Self { + ok: false, + bytes_base64: None, + error: Some(failure.error.to_string()), + report: failure.report, + } + } +} + +#[derive(Deserialize, Default)] +#[serde(rename_all = "camelCase")] +struct WasmHwpxStyleOptions { + body_font: Option, + heading_font: Option, + body_font_size: Option, + heading_font_size: Option, + text_color: Option, + heading_color: Option, + link_color: Option, + paragraph_align: Option, +} + +fn parse_hwpx_options( + title: Option, + author: Option, + strict_mode: bool, + style_json: Option, +) -> Result { + Ok(HwpxOptions { + title, + author, + strict_mode, + style: parse_hwpx_style(style_json)?, + }) +} + +fn parse_hwpx_style(style_json: Option) -> Result { + let Some(style_json) = style_json else { + return Ok(HwpxStyleOptions::default()); + }; + + let style = serde_json::from_str::(&style_json) + .map_err(|error| JsValue::from_str(&format!("invalid HWPX style options: {error}")))?; + + Ok(HwpxStyleOptions { + body_font: style.body_font, + heading_font: style.heading_font, + body_font_size: style.body_font_size, + heading_font_size: style.heading_font_size, + text_color: style.text_color, + heading_color: style.heading_color, + link_color: style.link_color, + paragraph_align: style + .paragraph_align + .map(|value| parse_hwpx_paragraph_align(&value)) + .transpose()?, + }) +} + +fn parse_hwpx_paragraph_align(value: &str) -> Result { + match value.to_ascii_lowercase().as_str() { + "left" => Ok(HwpxParagraphAlign::Left), + "center" => Ok(HwpxParagraphAlign::Center), + "right" => Ok(HwpxParagraphAlign::Right), + "justify" => Ok(HwpxParagraphAlign::Justify), + other => Err(JsValue::from_str(&format!( + "invalid HWPX paragraph alignment: {other}" + ))), + } } diff --git a/packages/core-rs/tests/docx/regression/archive.rs b/packages/core-rs/tests/docx/regression/archive.rs index d696ef7..3b81c89 100644 --- a/packages/core-rs/tests/docx/regression/archive.rs +++ b/packages/core-rs/tests/docx/regression/archive.rs @@ -40,7 +40,9 @@ fn normalizes_binary_entries_without_panicking() { writer.start_file("word/document.xml", options).unwrap(); writer.write_all(b"").unwrap(); writer.start_file("word/media/image1.png", options).unwrap(); - writer.write_all(&[0x89, b'P', b'N', b'G', 0x00, 0x01]).unwrap(); + writer + .write_all(&[0x89, b'P', b'N', b'G', 0x00, 0x01]) + .unwrap(); let bytes = writer.finish().unwrap().into_inner(); let normalized = normalized_entries(&bytes).unwrap(); diff --git a/packages/core-rs/tests/docx/regression/layout.rs b/packages/core-rs/tests/docx/regression/layout.rs index 52ec075..c263f34 100644 --- a/packages/core-rs/tests/docx/regression/layout.rs +++ b/packages/core-rs/tests/docx/regression/layout.rs @@ -10,7 +10,9 @@ fn list_item_starting_with_blockquote_does_not_create_empty_marker_paragraph() { let normalized = normalized_entries(&bytes).unwrap(); let document_xml = text_entry(&normalized, "word/document.xml"); - assert!(!document_xml.contains("• ")); assert!(document_xml.contains("quote")); } @@ -22,7 +24,9 @@ fn list_item_starting_with_code_block_does_not_create_empty_marker_paragraph() { let normalized = normalized_entries(&bytes).unwrap(); let document_xml = text_entry(&normalized, "word/document.xml"); - assert!(!document_xml.contains("• ")); assert!(document_xml.contains("code")); } @@ -51,7 +55,10 @@ fn blockquote_list_keeps_quote_border_on_list_items() { let normalized = normalized_entries(&bytes).unwrap(); let document_xml = text_entry(&normalized, "word/document.xml"); - assert!(document_xml.contains("")); + assert!( + document_xml + .contains("") + ); assert!(document_xml.contains("quoted item")); assert!( document_xml.contains("xml:space=\"preserve\">◦ ") @@ -66,7 +73,10 @@ fn blockquote_code_block_keeps_quote_border() { let normalized = normalized_entries(&bytes).unwrap(); let document_xml = text_entry(&normalized, "word/document.xml"); - assert!(document_xml.contains("")); + assert!( + document_xml + .contains("") + ); assert!(document_xml.contains("code")); } @@ -97,7 +107,10 @@ fn quoted_table_does_not_inject_hidden_anchor_paragraph() { assert!(document_xml.contains("")); assert!(!document_xml.contains("\u{200B}")); - assert!(document_xml.contains("")); + assert!( + document_xml + .contains("") + ); } #[test] @@ -113,7 +126,11 @@ fn nested_blockquote_does_not_duplicate_quote_border_properties() { #[test] fn includes_heading_styles_up_to_level_six() { - let bytes = generate_docx("#### Four\n\n##### Five\n\n###### Six", DocxOptions::default()).unwrap(); + let bytes = generate_docx( + "#### Four\n\n##### Five\n\n###### Six", + DocxOptions::default(), + ) + .unwrap(); let normalized = normalized_entries(&bytes).unwrap(); let document_xml = text_entry(&normalized, "word/document.xml"); diff --git a/packages/core-rs/tests/docx/regression/mod.rs b/packages/core-rs/tests/docx/regression/mod.rs index 58b485a..4a7427c 100644 --- a/packages/core-rs/tests/docx/regression/mod.rs +++ b/packages/core-rs/tests/docx/regression/mod.rs @@ -2,3 +2,4 @@ mod archive; mod fallback; mod images; mod layout; +mod report; diff --git a/packages/core-rs/tests/docx/regression/report.rs b/packages/core-rs/tests/docx/regression/report.rs new file mode 100644 index 0000000..ac4f5d9 --- /dev/null +++ b/packages/core-rs/tests/docx/regression/report.rs @@ -0,0 +1,43 @@ +use core_rs::{ + ConversionTarget, DocxOptions, IssueSeverity, analyze_markdown, generate_docx_with_report, +}; + +#[test] +fn analyze_markdown_reports_docx_fallback_issues() { + let report = analyze_markdown("raw", ConversionTarget::Docx); + assert!(report.degraded); + assert_eq!(report.unsupported_count, 0); + assert_eq!(report.fallback_count, 1); + assert_eq!(report.issues[0].feature, "HTML"); + assert_eq!(report.issues[0].severity, IssueSeverity::Warning); +} + +#[test] +fn strict_docx_report_failure_promotes_unsupported_issue() { + let failure = generate_docx_with_report("raw", DocxOptions::default()).unwrap_err(); + assert_eq!(failure.report.unsupported_count, 1); + assert_eq!(failure.report.fallback_count, 0); + assert_eq!(failure.report.issues[0].feature, "HTML"); + assert_eq!(failure.report.issues[0].severity, IssueSeverity::Error); + assert_eq!( + failure.report.issues[0].message, + "unsupported feature: html" + ); +} + +#[test] +fn compat_docx_report_marks_degraded_success() { + let generated = generate_docx_with_report( + "raw", + DocxOptions { + strict_mode: false, + ..DocxOptions::default() + }, + ) + .unwrap(); + + assert!(generated.report.degraded); + assert_eq!(generated.report.unsupported_count, 0); + assert_eq!(generated.report.fallback_count, 1); + assert!(!generated.bytes.is_empty()); +} diff --git a/packages/core-rs/tests/docx_test.rs b/packages/core-rs/tests/docx_test.rs index 8cc6d06..a0685ad 100644 --- a/packages/core-rs/tests/docx_test.rs +++ b/packages/core-rs/tests/docx_test.rs @@ -92,7 +92,10 @@ pub(crate) fn assert_xml_is_well_formed(entries: &BTreeMap(entries: &'a BTreeMap, path: &str) -> &'a str { +pub(crate) fn text_entry<'a>( + entries: &'a BTreeMap, + path: &str, +) -> &'a str { match entries.get(path).unwrap() { NormalizedEntry::Text(text) => text, NormalizedEntry::BinaryHash(_) => panic!("entry {path} is binary"), diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/README.md b/packages/core-rs/tests/fixtures/hwpx/approved/README.md index 9e51538..8067c49 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/README.md +++ b/packages/core-rs/tests/fixtures/hwpx/approved/README.md @@ -13,11 +13,17 @@ Current status: - `core-mixed` is manually validated and approved. - `list-basic` is manually validated and approved. - `list-nested-depth-2` is manually validated and approved. +- `ordered-list-basic` is manually validated and approved for compat mode. +- `ordered-list-nested-depth-2` is manually validated and approved for compat mode. - `table-basic` is manually validated and approved. +- `table-alignment` is manually validated and approved. - `style-typography` is manually validated and approved. - `style-centered-layout` is manually validated and approved. - `style-brand-color` is manually validated and approved. +Each approved fixture must record `manual_verified = true` in `fixture.toml`. Ordered-list fixtures are +approved compatibility fixtures, but they remain compat-only rather than strict-mode guarantees. + Approved style fixtures use Hancom-safe built-in fonts only. External fonts remain best-effort because the current HWPX path records font family names but does not embed font binaries. diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/blockquote-basic/expected/Contents/header.xml b/packages/core-rs/tests/fixtures/hwpx/approved/blockquote-basic/expected/Contents/header.xml index 0d6bf34..a54baf2 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/blockquote-basic/expected/Contents/header.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/blockquote-basic/expected/Contents/header.xml @@ -1 +1 @@ -^1.^2.^3)^4)(^5)(^6)^7 \ No newline at end of file +^1.^2.^3)^4)(^5)(^6)^7 \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/blockquote-basic/expected/Contents/section0.xml b/packages/core-rs/tests/fixtures/hwpx/approved/blockquote-basic/expected/Contents/section0.xml index a930d4b..a8ba597 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/blockquote-basic/expected/Contents/section0.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/blockquote-basic/expected/Contents/section0.xml @@ -1 +1 @@ -Quoted paragraphAnother quoted line \ No newline at end of file +Quoted paragraphAnother quoted line \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/blockquote-basic/expected/settings.xml b/packages/core-rs/tests/fixtures/hwpx/approved/blockquote-basic/expected/settings.xml index 5d51824..27f1776 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/blockquote-basic/expected/settings.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/blockquote-basic/expected/settings.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/blockquote-basic/fixture.toml b/packages/core-rs/tests/fixtures/hwpx/approved/blockquote-basic/fixture.toml index 84d9794..3a4e079 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/blockquote-basic/fixture.toml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/blockquote-basic/fixture.toml @@ -1,4 +1,6 @@ title = "HWPX Blockquote Candidate" author = "Fixture Author" -strict_mode = false +strict_mode = true determinism = false +manual_verified = true +verified_at = "2026-03-11" diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/blockquote-basic/golden.hwpx b/packages/core-rs/tests/fixtures/hwpx/approved/blockquote-basic/golden.hwpx index d194a94222cdd9ee9007d78962df7d1c675bc552..c5bce81ea5048db7e9a97511f93e10d970b67d28 100644 GIT binary patch delta 4408 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.^2.^3)^4)(^5)(^6)^7 \ No newline at end of file +^1.^2.^3)^4)(^5)(^6)^7 \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/code-block-basic/expected/Contents/section0.xml b/packages/core-rs/tests/fixtures/hwpx/approved/code-block-basic/expected/Contents/section0.xml index c377ec7..0c7d14f 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/code-block-basic/expected/Contents/section0.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/code-block-basic/expected/Contents/section0.xml @@ -1 +1 @@ -fn main() { println!("hi");} \ No newline at end of file +fn main() { println!("hi");} \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/code-block-basic/fixture.toml b/packages/core-rs/tests/fixtures/hwpx/approved/code-block-basic/fixture.toml index 76bff88..fce30ae 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/code-block-basic/fixture.toml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/code-block-basic/fixture.toml @@ -2,3 +2,5 @@ title = "HWPX Code Block Candidate" author = "Fixture Author" strict_mode = false determinism = false +manual_verified = true +verified_at = "2026-03-11" diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/code-block-basic/golden.hwpx b/packages/core-rs/tests/fixtures/hwpx/approved/code-block-basic/golden.hwpx index d684bf86879fc7aaad4d4530a8d7f82cddb90bcc..0676acdea9f7dada5ad7a2c006add03d44f6bf73 100644 GIT binary patch delta 4135 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 delta 4226 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.^2.^3)^4)(^5)(^6)^7 \ No newline at end of file +^1.^2.^3)^4)(^5)(^6)^7 \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-heading/fixture.toml b/packages/core-rs/tests/fixtures/hwpx/approved/core-heading/fixture.toml index 14e7c51..4b4c2b5 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/core-heading/fixture.toml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/core-heading/fixture.toml @@ -2,3 +2,5 @@ title = "HWPX Heading Fixture" author = "Fixture Author" strict_mode = true determinism = false +manual_verified = true +verified_at = "2026-03-11" diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-heading/golden.hwpx b/packages/core-rs/tests/fixtures/hwpx/approved/core-heading/golden.hwpx index 3a6d98e788a2bd2c86e8ef2f7cc2641b9bb86de7..8270ac9734062563a68d2958fcf098ed6bcdbaa4 100644 GIT binary patch delta 3074 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 Q0j`q~Y$yg&X#fBK0G$io^Z)<= delta 3086 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 diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-heading/hash.txt b/packages/core-rs/tests/fixtures/hwpx/approved/core-heading/hash.txt index fb46286..204367d 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/core-heading/hash.txt +++ b/packages/core-rs/tests/fixtures/hwpx/approved/core-heading/hash.txt @@ -1 +1 @@ -81c1148a0fdd8fd36f48e398562b870dd8a975f991b730a606ab43cfbb582b96 \ No newline at end of file +87568bc134baa789ad6f4948e70c44b6ef4e713b687d0c408f9fa4b0646c58d6 \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-inline-style/expected/Contents/content.hpf b/packages/core-rs/tests/fixtures/hwpx/approved/core-inline-style/expected/Contents/content.hpf index becd549..b38c482 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/core-inline-style/expected/Contents/content.hpf +++ b/packages/core-rs/tests/fixtures/hwpx/approved/core-inline-style/expected/Contents/content.hpf @@ -1 +1 @@ -HWPX Inline Fixtureko(주)한글과컴퓨터docxly2004-11-09T15:23:46Z2026-03-08T07:30:22Z2017년 8월 3일 목요일 오후 4:55:39 \ No newline at end of file +HWPX Inline Fixtureko(주)한글과컴퓨터limchaesung2004-11-09T15:23:46Z2026-03-12T07:35:25Z2017년 8월 3일 목요일 오후 4:55:39 \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-inline-style/expected/Contents/header.xml b/packages/core-rs/tests/fixtures/hwpx/approved/core-inline-style/expected/Contents/header.xml index ed6ab0c..eef1b38 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/core-inline-style/expected/Contents/header.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/core-inline-style/expected/Contents/header.xml @@ -1 +1 @@ -^1.^2.^3)^4)(^5)(^6)^7 \ No newline at end of file +^1.^2.^3)^4)(^5)(^6)^7 \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-inline-style/expected/Contents/section0.xml b/packages/core-rs/tests/fixtures/hwpx/approved/core-inline-style/expected/Contents/section0.xml index 8e3c98e..774f378 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/core-inline-style/expected/Contents/section0.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/core-inline-style/expected/Contents/section0.xml @@ -1 +1 @@ -일반 굵게 기울임 코드 줄바꿈 다음 줄 \ No newline at end of file +일반 굵게 기울임 코드다음 줄 \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-inline-style/expected/Preview/PrvImage.png.sha256 b/packages/core-rs/tests/fixtures/hwpx/approved/core-inline-style/expected/Preview/PrvImage.png.sha256 index 21d6e53..10a44da 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/core-inline-style/expected/Preview/PrvImage.png.sha256 +++ b/packages/core-rs/tests/fixtures/hwpx/approved/core-inline-style/expected/Preview/PrvImage.png.sha256 @@ -1 +1 @@ -a3cc714d6645caa3b0735b03d85d295b46cc486218b1d676fb74adcf4019df24 \ No newline at end of file +cf48092927a1ddeaeb80084aa1bd08078ae50db1d08fcacbfb90f548dc0eab4b \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-inline-style/expected/Preview/PrvText.txt b/packages/core-rs/tests/fixtures/hwpx/approved/core-inline-style/expected/Preview/PrvText.txt index a224c1d..991f795 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/core-inline-style/expected/Preview/PrvText.txt +++ b/packages/core-rs/tests/fixtures/hwpx/approved/core-inline-style/expected/Preview/PrvText.txt @@ -1 +1 @@ -일반 굵게 기울임 코드 줄바꿈 다음 줄 \ No newline at end of file +일반 굵게 기울임 코드다음 줄 \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-inline-style/expected/settings.xml b/packages/core-rs/tests/fixtures/hwpx/approved/core-inline-style/expected/settings.xml index 5d51824..4f7b18f 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/core-inline-style/expected/settings.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/core-inline-style/expected/settings.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-inline-style/expected/version.xml b/packages/core-rs/tests/fixtures/hwpx/approved/core-inline-style/expected/version.xml index 549bb34..4ee1300 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/core-inline-style/expected/version.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/core-inline-style/expected/version.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-inline-style/fixture.toml b/packages/core-rs/tests/fixtures/hwpx/approved/core-inline-style/fixture.toml index db5693d..1704524 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/core-inline-style/fixture.toml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/core-inline-style/fixture.toml @@ -2,3 +2,5 @@ title = "HWPX Inline Fixture" author = "Fixture Author" strict_mode = true determinism = false +manual_verified = true +verified_at = "2026-03-11" diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-inline-style/golden.hwpx b/packages/core-rs/tests/fixtures/hwpx/approved/core-inline-style/golden.hwpx index 55ef10ae1f7f575d59f3b430cfcfb08026b30754..08fb06816f8ab3def09f535500fba77dfe5c0910 100644 GIT binary patch literal 27980 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 literal 27812 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

PMHK!2S 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 diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-inline-style/hash.txt b/packages/core-rs/tests/fixtures/hwpx/approved/core-inline-style/hash.txt index 28de60c..abc1533 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/core-inline-style/hash.txt +++ b/packages/core-rs/tests/fixtures/hwpx/approved/core-inline-style/hash.txt @@ -1 +1 @@ -6ebba80a3c8b9eb2c3813b374b643bc6d1eff9e06acf388d315073410253d2e3 \ No newline at end of file +f164d3b065e51bcf315570c128159f1864472335f179dd8c2cc87ec3bd59498e \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-link-text/expected/Contents/content.hpf b/packages/core-rs/tests/fixtures/hwpx/approved/core-link-text/expected/Contents/content.hpf index 00a9390..5a166ac 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/core-link-text/expected/Contents/content.hpf +++ b/packages/core-rs/tests/fixtures/hwpx/approved/core-link-text/expected/Contents/content.hpf @@ -1 +1 @@ -HWPX Link Fixtureko(주)한글과컴퓨터docxly2004-11-09T15:23:46Z2026-03-08T07:30:22Z2017년 8월 3일 목요일 오후 4:55:39 \ No newline at end of file +HWPX Link Fixtureko(주)한글과컴퓨터limchaesung2004-11-09T15:23:46Z2026-03-12T07:31:17Z2017년 8월 3일 목요일 오후 4:55:39 \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-link-text/expected/Contents/header.xml b/packages/core-rs/tests/fixtures/hwpx/approved/core-link-text/expected/Contents/header.xml index ed6ab0c..0e56e19 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/core-link-text/expected/Contents/header.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/core-link-text/expected/Contents/header.xml @@ -1 +1 @@ -^1.^2.^3)^4)(^5)(^6)^7 \ No newline at end of file +^1.^2.^3)^4)(^5)(^6)^7 \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-link-text/expected/Contents/section0.xml b/packages/core-rs/tests/fixtures/hwpx/approved/core-link-text/expected/Contents/section0.xml index b0697d7..888fb5a 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/core-link-text/expected/Contents/section0.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/core-link-text/expected/Contents/section0.xml @@ -1 +1 @@ -문서 열기 (https://example.com) \ No newline at end of file +0https\://example.com/;1;0;0;https://example.com/HWPHYPERLINK_TYPE_URLHWPHYPERLINK_TARGET_BOOKMARKHWPHYPERLINK_JUMP_CURRENTTAB문서 열기 \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-link-text/expected/Preview/PrvImage.png.sha256 b/packages/core-rs/tests/fixtures/hwpx/approved/core-link-text/expected/Preview/PrvImage.png.sha256 index 21d6e53..61545e1 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/core-link-text/expected/Preview/PrvImage.png.sha256 +++ b/packages/core-rs/tests/fixtures/hwpx/approved/core-link-text/expected/Preview/PrvImage.png.sha256 @@ -1 +1 @@ -a3cc714d6645caa3b0735b03d85d295b46cc486218b1d676fb74adcf4019df24 \ No newline at end of file +b77949f0701101310f665e8aaf38109fcd016235593ab470f115e9d492924578 \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-link-text/expected/Preview/PrvText.txt b/packages/core-rs/tests/fixtures/hwpx/approved/core-link-text/expected/Preview/PrvText.txt index ce003a1..49a9375 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/core-link-text/expected/Preview/PrvText.txt +++ b/packages/core-rs/tests/fixtures/hwpx/approved/core-link-text/expected/Preview/PrvText.txt @@ -1 +1 @@ -문서 열기 (https://example.com) \ No newline at end of file +문서 열기 \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-link-text/expected/settings.xml b/packages/core-rs/tests/fixtures/hwpx/approved/core-link-text/expected/settings.xml index 5d51824..f2018a8 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/core-link-text/expected/settings.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/core-link-text/expected/settings.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-link-text/expected/version.xml b/packages/core-rs/tests/fixtures/hwpx/approved/core-link-text/expected/version.xml index 549bb34..4ee1300 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/core-link-text/expected/version.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/core-link-text/expected/version.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-link-text/fixture.toml b/packages/core-rs/tests/fixtures/hwpx/approved/core-link-text/fixture.toml index dbbcc79..9cbaf05 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/core-link-text/fixture.toml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/core-link-text/fixture.toml @@ -2,3 +2,5 @@ title = "HWPX Link Fixture" author = "Fixture Author" strict_mode = true determinism = false +manual_verified = true +verified_at = "2026-03-11" diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-link-text/golden.hwpx b/packages/core-rs/tests/fixtures/hwpx/approved/core-link-text/golden.hwpx index a72caf76553bce8bd08aaf8e67663d18e62dfad9..a0339d3df6f705a08775d407a6232f4c62c83849 100644 GIT binary patch literal 24910 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 literal 27745 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 Mixed Fixtureko(주)한글과컴퓨터docxly2004-11-09T15:23:46Z2026-03-08T07:30:22Z2017년 8월 3일 목요일 오후 4:55:39 \ No newline at end of file +HWPX Mixed Fixtureko(주)한글과컴퓨터limchaesung2004-11-09T15:23:46Z2026-03-12T07:52:38Z2017년 8월 3일 목요일 오후 4:55:39 \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-mixed/expected/Contents/header.xml b/packages/core-rs/tests/fixtures/hwpx/approved/core-mixed/expected/Contents/header.xml index ed6ab0c..338dd18 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/core-mixed/expected/Contents/header.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/core-mixed/expected/Contents/header.xml @@ -1 +1 @@ -^1.^2.^3)^4)(^5)(^6)^7 \ No newline at end of file +^1.^2.^3)^4)(^5)(^6)^7 \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-mixed/expected/Contents/section0.xml b/packages/core-rs/tests/fixtures/hwpx/approved/core-mixed/expected/Contents/section0.xml index 3fc8291..68c89c2 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/core-mixed/expected/Contents/section0.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/core-mixed/expected/Contents/section0.xml @@ -1 +1 @@ -HWPX Core첫 문단은 강조기울임, 코드, 그리고 링크 (https://example.com) 를 포함합니다. 줄바꿈도 유지합니다. \ No newline at end of file +HWPX Core첫 문단은 강조기울임, 코드, 그리고 0https\://example.com/;1;0;0;https://example.com/HWPHYPERLINK_TYPE_URLHWPHYPERLINK_TARGET_BOOKMARKHWPHYPERLINK_JUMP_CURRENTTAB링크를 포함합니다.줄바꿈도 유지합니다. \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-mixed/expected/Preview/PrvImage.png.sha256 b/packages/core-rs/tests/fixtures/hwpx/approved/core-mixed/expected/Preview/PrvImage.png.sha256 index 21d6e53..a8808a0 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/core-mixed/expected/Preview/PrvImage.png.sha256 +++ b/packages/core-rs/tests/fixtures/hwpx/approved/core-mixed/expected/Preview/PrvImage.png.sha256 @@ -1 +1 @@ -a3cc714d6645caa3b0735b03d85d295b46cc486218b1d676fb74adcf4019df24 \ No newline at end of file +b8b4d4f30ee337f2e4e1559db046e76af9b4168d00146f7bf405224a9389bbcb \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-mixed/expected/Preview/PrvText.txt b/packages/core-rs/tests/fixtures/hwpx/approved/core-mixed/expected/Preview/PrvText.txt index 310fe9e..84b2ed1 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/core-mixed/expected/Preview/PrvText.txt +++ b/packages/core-rs/tests/fixtures/hwpx/approved/core-mixed/expected/Preview/PrvText.txt @@ -1,2 +1,3 @@ HWPX Core -첫 문단은 강조 와 기울임, 코드, 그리고 링크 (https://example.com) 를 포함합니다. 줄바꿈도 유지합니다. \ No newline at end of file +첫 문단은 강조와 기울임, 코드, 그리고 링크를 포함합니다. +줄바꿈도 유지합니다. \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-mixed/expected/settings.xml b/packages/core-rs/tests/fixtures/hwpx/approved/core-mixed/expected/settings.xml index 5d51824..ac93cca 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/core-mixed/expected/settings.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/core-mixed/expected/settings.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-mixed/expected/version.xml b/packages/core-rs/tests/fixtures/hwpx/approved/core-mixed/expected/version.xml index 549bb34..4ee1300 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/core-mixed/expected/version.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/core-mixed/expected/version.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-mixed/fixture.toml b/packages/core-rs/tests/fixtures/hwpx/approved/core-mixed/fixture.toml index b2bc3a5..805a7a5 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/core-mixed/fixture.toml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/core-mixed/fixture.toml @@ -2,3 +2,6 @@ title = "HWPX Mixed Fixture" author = "Fixture Author" strict_mode = true determinism = true +comparison_excludes = ["Contents/content.hpf", "Preview/PrvImage.png", "version.xml"] +manual_verified = true +verified_at = "2026-03-11" diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-mixed/golden.hwpx b/packages/core-rs/tests/fixtures/hwpx/approved/core-mixed/golden.hwpx index 37c882e8d3906834eb8d4c29b48f90eb6d80e829..b91bb050336841bc4732c127481ff74a704e5ef8 100644 GIT binary patch literal 35731 zcmeFY_gfQB+we^jPy|Gj4neTfMS2N{f=IPdq)8Wn&`GF~A|0Y4MX8E{N(o3OKtxJJ zdM_aaLKgyr5=uz&hU;7I=RV%&hvyHtava0%&d!`WyEAi^&kUvpj7;a~{`y^@i`>-K zFACV8?9oX zv#%emur$QeLmf!_Bl>rShM1hRteB`X$kEHm4dkjJ_V|&ml(LwppT9lG$=<^Y4mvuEyubQK#|z}|4D$DrdFE{IqcS!2+~=_Jv0PRhQz^QKkg z4baJrs?+=@mlq)Z=1GFYgVIEo81k30GD<^3!#7eZ47sj6`EZd%5nOxfaL`xVv8*$z z20eW}dvp&FYT6&!@0}IXYE$#L?y0smX}^F|s@OxW;Ohm1;9Duj;-@b^m`n5Y_4Q>) zGT!k|H*EG_qYOuYj4nMQB+3|apzj=Vj-gj?%rOR>NT83lsbjPnEUPhOg znL7{iRLrDPNs^ro$!RagGhFy#%3U0{*6l7%s5*`e%SFCA;8ZWj8CjL|I%CB*>Gq7Mr`QF!mL8j9a0oXoOLfg zlJZTD3$EZXwkgB$qb91!F@|spe@$22DDTUZh?(B$QqUMD$9W*PJ;W;fmKG3Wc9bmu z1Y$HNHA}J|oiixv7<@vuU$7wfJwH-EI2tfE>d{|`KCIyj4*obVUyx_m(F^&*`{(WO zWF6Lt(u`N;-@QJVCD7?rvCjQO(=p`M>r;{ioqg>ECj(v|JkH`VkFD1E;nV8cF(K`j z&8?f5`Kl9;@mZ5cS@-Fk>luc~Rlo4d=cSkJ5RC>W3jdh5%Jvyw?wKz)s}q0H|8S%p zbGo9z(sr=gn5D`uwxDn?z1^-PY|?YJFi2+|Q$EVfsWV+~zgXy5Rqw8R-^%3qNK{j# z;`fS+XWW$b^rM#pmW#iQ>D}uN-EDAAx8ftHg)5c!FS#5{1-2(z*suM*F#FTuLBAwp z*WhwjNEQyB&!3{Vcvj9jq3LBZ&zJL;OdYlQ$!ET>23+C#VoRTz%Exb<5^5oH)ZhF5 z%D1oG$;^}i6z$sj}R!6lb@>^-PjtL6`c| zfZ6)&v-kkdmo^N;u^-nBE7%e&Zzz=QMVWv7UeQo_IV0@(^R}tt!j2Df)Q70a*jmhJxCtSVsX2T!6^5mx3 zJKq{lvq;E}cPuX7{rC}cIP2cS{u1&!?e@DHZ_baim*~B|yJ%9nnfLHvu`KGEc8H*M zrA})_kGmXu7TZ|dWbb5$2>O zk>BAqNzsf-tbi^l_!n)KW#JW->)(OIzQ7T}L6(bk4n{ls~p$ zl+m8m+*4!zju#H~eR1wP_9J`7$l&6i#Y0^;?$ulS3|X$atz4vQ0F8kRCLm6c)oflV zj!%7+Szm8d##j?>Rtx^NS;SZC_BsANa}>5aOVQr0^eFgEb7Z{RB71|6U8(W#%{T%r z-_ov$>g4&6$NrASHV>w%avJ(w^FF%#W>ye7kTV5boYo|Bd0F8FIW<|Ina_-2tV-hj z?DVCEuFnCQ@7b2prdHfI1n4qi5Lkxyw%qKuo&&y?z3#4Ut-2caQ1zZEXJPPLQ=55t8j-i;e<3#Q=SQ?x8OJvN9B~6rjk7l7E>()=KZ-Mn_#Jik()E~Oh86#- z&(21Eu$-zzYQJNC`_9C~yh_*Rs z-Ct0}u{*>gcJ#u{EB7VB@@syZJ-FG+alWqgN!;ce|JgAVtG==@cg)tpy$e%UwCGF6V{!eQ2$`oG}v9auK8sZv3L`Ky||mNUKfeKse4KY z(7(IR{ipOsDbOM29=(3m#FsmVs)vcUkinCWk}|K?p`D}(R)5{S+Wt;bC(bCIuLD%_ zrk*Lo~9O_u@@~(}X9u zUwxuhIN8!y6732vt;a+&nlDeSA6C%q7yc+N7qeF2jZ!(8j}1DCToEr0YJsFax4pE3 zvU$+-yeQy9Iz1$fxLZ~wV-dpZlKI9=?eU4zC}zpiev2%vS0^tPMD<*YX-A2DIX^%1 zPV%9xw)CKEj%rNHw!Zy%_t@+4Q46}$iQv}rF&NbtsX<@2KYTI$%rEWbQL<}x(sVJU zw+nq=+SjJoCw_WvDBP-P%GdGpog<$*4-Z|d+4-1-+zpk5>WhYlYz4=+e_7PYPQ3#} z@mZ(x(6x=8i|aYn#^cs>Gwh7jMZejj7ko!&cxn0PkMk9I?v7<@#z_r62?^Ia-Tdo! z#bf1s-EjDC;F{^^&# zdPmCdaz!H!JI;Hn^VfpB>g4s#Fq@ZyPuSnMd6>g;{u;(zyC9*E$@Wy;y3?ZpgnyU$ zuRHgj?n#S0hi3JrOhSe8&J*Ij!@_rO-XV6s-Wpj-=e(O` zX<8du9&*yYwL@g|%LILbQ6yS8|6%PEXTe^enC|ZE88C35xbK49hYZzMVjEhgY;sqD zS>@{ojO7o?9D>;HZ=7l!d>F8!85{c2{#5IhtVdy#KSkCfpzisn9a*0)fk%&mKK4pw z!~Jq}H2;KUEIl;bj+RtCU79@CAQ`0iWaFLV0gGLz*1B`OmDyaLweFqy_dLU|U2-m^ z%H$l}S)E!{ZXf)vxgg3gC9Z^Uo-Ti1Sl5wt$Y5dq+En>^(Bf;;!+`rtr-GEuZ%O~O zf5UeE!_9!@&%a{rhuZPWO|8%05;1P|vdg>=8%n}ujP=cE z;c7VBYjqlawq1L+)wtcwwS?0pEejVunJiTQfE&8VvHl7;UZD2X%)T+8E=YIpL)#U9 ztA)1$TmEk|zB+w9*jw$&vva*WG24@EX52F)dM9 z#vDa1C+@a|2k9g6abLp2;^wRM3NOeo&KpLL*Fmr3WtZH$J-<_|b5m6JjXam_!?j1! z;O_!luZQ650}#GD9FM)F%!bZt{UL3Mi04&*3lxPr$_1fXPiWRzc*C+V9GFK@R%g+Y zZ9Tf<@F?k&#OLJ~cC6~XmiTz<@Igwyozwy$T-cYYrmyPmed_Fa&pVmD{*$Y=GkXG* z$Vk$0FV=c9(Sb?e0eQbgmz1(TyMg_<6~cBZtW}-Sand6LA%!Td7Ai>YiX3eVIyd}u zQt)V*`o9Cm%0|X_sw}jaoEGPwr^V*~29AEtj>i$b>~ZKgV$tU%c;_6&+^p;5*Piu5 zbB~8b_dKr11$ao*2qwHLAp|!f{K2m8gvWf0MR#8eGqNnxKQeuN>fC_N36@|w0(H2P z`XfYswK}KpO`_Hd-{oG&^q$Lf<3@;A@Z6%?jQieIm;WIO8?<5c|y=~B57#24fCt* zvs2a02@k(AUbYU`ua@Pni(a%vZmn|pq=zJRz?G6cGnjANyc103o!0F#z0JMuCAZEe zj?UL)e|FWOb@F>s`;)+vSiw7vZm;BeJQ>4n%A8)^sxhd8kYfhVUFVau$z`?|FuG+T z+NR4IJM@KfwYWT-Ub~>vyN9?Hcwyo3N3^6+h*r``&aR|3TWbz|t-0^zU6LPX<0iOI z>jnh`@avz5j{p67G5Lj~iz342>U$nj_R6YO!D`1-d#|+%dqNw8aA|m-d+TQY79R4@ zv(a5XTJ<{gvf4|jrma^$K~5~3ztPb8+0}-7=h3iwnQ!AqKn+ZAqt>bkbGa@h&Zv0s z#Ki(3@*MPAw!X`hc%`%J7hc`UeZ1ho(rd>~ud4E~wdr+CKg;F^2L={nmWMp59Hf4< ztGejm?^ix7WZXxElcG}`c$_FGY8+o35(_!O@k?O(CCkZ_EP`|^S5X$QfX>xcRUW?s_+)z_wN?(}7B@n7YY-WB5gY1eJp9$BWsO@GB) zGi#STosVG<43eXtP2r7|GxTuAKp|DW%+*(vQ%z`);&Znj-BJ<-Cn7#6GManQe|=^Chj^xt ztt9$E3H#T~2Lib1h@5v0JSq3W0#+eJPaAQaK$C_vxhGG5C227&AWwOCRUP8P&Bt!3 zrUf})au)}jqgyaYwL9O`Y^5Re2;D=u&xU#VmbI4Utr1^DMbkOcS3jT^8qLZ~e>?eQ zp4>V`%@q|lpdOs5eI7-m_N55VX~C6Vh#e<%rqZ9rF>6(CFwq5(x9(; z;KZze{8-RvAXn^QukAeGuncQ(U0I@!IQ<$I9ez$VTzgv#(^wYIrPgoLSoHWqUwZm2 zddcqc{`TPu*{Sc^^e;QD>z%&H?JH!f;*PBVGo#A$Q|}8|4ofM>UzEFZZtdkg3&*sx zj+RlcyY}C;RXYKqbxvedZt>5jnh2fyW8K+(%jRA$qS?qY>l~X?NP2aG*=4Wur_R(= zcbsX0-;9xcdulU7u(k19v%}KSQeM?TCGR$iNgtA8fw*1{pF$CT5*M~(m#~`HUoI;f zUF=Qg9?Hn%=0SIHM_DAV#pUv}QeI zOF49)kSjBg>eG^I*v%ua%XhJz z2q@o(e%#3^+$$D_dP{FZwk(64P-H9aDwbr;pBn;iMgX96CZWOiB7YTaHghI{OTdQv zsZS+p;4Oc&7+=%b{G2$Ge{V*4pFvmUE~Cn__FWe8++xgCjc00K*CJ)+)SEAacbrOA>RYe%VP~; z^89ZfyJt zk8AeI-clbDa)2t@?pt?Xf{El&Hwjq&sge?NYVwT0A1FJR!t}q5tqQ5>ZV=7LhS1V6 zMK|?z4s>${%b5BGK5~ZmOZ!9ow^=#kOuc($?ky|?SGraITvbrB1;Ziowc4{IF<-rS zKoEKEUy~KHJte|ki`C0l8zE2T+w%kJVQU-r;#=%j zHzle^FN}~ZlJwY`s~xIr#{xFT)rMxaHZ7~|3=)D7{Cdfsf24W8Xb1<8-{_@qcuUz9(&+K-wGR8gFW>$9_uS6zw*<{@vRT z=;#=R>F8((o|%@@@}PH{cA)orpm&R|tXF7-cH!Y{q3>d3M0cHb&PvBbFF<$VxCkw2 z(hL53u1kNFj`2VF40Lp{ZZv^^l`*CrkAF{T>A22cM}~Zce@ZY6=QI9ueq4!;?jv=_ zgLXLSeb>^Dj*g@4IMJs`a0t@TY0>H5)_w$`U&F9uJ2|H*&17Wzy^_>^BPw>b?M0+C zbGO?VYeQT^oX@U&eu>KJHwoQt%X6}hw=bPMb@tM|UKMu!W>s*Vr;*W~A?F=smfSIH zKjv|OS5B~TH5uvE6yV;BW+~ujVm%|O^@5I`fr)kMTExM|u$iLR-`BJo`P^r|XmI^c zk)#WvrmzUthqTgvS7WGN`cLKmTt>(-G5wL-^tktzKw6$wK2hi&`Da9BV_s-!Fy|Vb z{JSy}E9-DB9_KhS@%G;aztF;~{3}cA>1q0i;M3|^Xa8+5?d4hi zc~K_T|GxZxd;Xu%@ZVwo&lLUdod2)F`$wVvSKI$5(*Xdflo z3fzpQBlA9em*k{K{p>xAbB3Lg-@DsK-oyOn{u%913bC&Xv5{fz-x9dHt5jPDD*}e@ zDpHTOt4}afM=gWm*AE|OSXH$r?fU^L%y>)2p7_KE52$%V zB|8&jiLn3AI~YYmUaN6^6{(i;&7is|ckfk8$^J*~u^xOD8Kzuoh2w(Frm6uOJ&8Y@ z(PtAS#{;K4Mry|V05@uTY)t-kL``UJBw;QkR(234rjtTl7jT&^>jXv`5~L4#cz9`Wxs@S8n*& zC`0?UmE75+WXVM#-OGDjMuEcJ-7?A+*Keo?j_=TWuCy2i3DFrUGN&#`QH;P-iWX1K zQgu;I8OhTfeSgosCJEWSdEbrErRhXA56X{s5r_Mg@IUEJ*1o&R_#=t3jr8YjzHh^0 z4(GSaKwJ(=h&iHP$T*vnviz@U)vCA7sMxZ}@PO_UcreOlT$54>t-8ZwosWQ9xRYlT z(%+$_UF$W|KUsS(?0Sy07#_8b*@UOB;$m5skbIH=e)eHPVY`K2*qSiQnTkgj)63L% zQc%0qQIrX(pm=<4-N83}hF%_)A_rNk&{T6-pb}fI1lU2thlB8Fmb?jL73%w%?z3#w zo?~_uO$RHSA|+_sXEjf(|JGSk2!^4&?+Ea!|3>GraxvJu{1YN>Pu(bKIh!;<_>q>r zobF()R7d9LwHrH}Sg{Zk{;;!V0AKBW?{lOg?^+H}wJ?**8IV~Wha`V?tv^s>t{mo4 zg&t-2O@cX9DoYVtZ*1~&7IcU9KR~1Aqs%Oy|F%LsS)ZNQ$pZw0qZcY;YadG26*jq* zyQq&Q=c#7W;?{6t?REp%1JROz={~q{#cptVm+XjM;k7}JjFr*@x=roWm1p+XB8sYN zp?jI2Fe1rrvmX@SX~Kfu_`YW^HGR?T@A#mesoQkvkKViI4xU@`np-Hh5y37^imb(0 z=-N{v@l%V3M-po~R0Th%{iFF~h#!`)WIbY|IdXdUj`?(dX>!U7f!Uj*CSI}@yyTM7 zv9+32Dz|%w9rbPUB!b02!=aJjCJ1(AUV-PWkNHVw1$Df(VA0I+IjB~u=3E1Zx<|!t z%dFP~tVZqgL`~pbp=&01Mp9nCh#aI_u&l;EWH?45(7jST2^zBL(w)Y#S1~ZNbz%uv zn)Nm1^W_RNQX;~0DBaF?me|(PO#-sCs7-eV%{*nMT;sIzRJ~Ns{#Tge&3Te_c<;=j2_ z)RsYSsJv@7DnwO$`ZWJ)&0N)su*?+vs7QCe7qXLa(u<|bG({+TWWH0!exHz4Hu!R- z&V~=)v}%Trjm^RI-0}ii6f~_p{i>H;f3ZWH<$^lHF0dDIFMs{^QS5*Gig~UxU$%EX zKDw01>0+vf-GNNc91W%>Q{q)(3mlkMl|wpCL@8bCh#SHs%S+cE#t?JEU>oTMp?{pM z0+;H-cQaBv0F%@<=|;Gw{&>zG3U22`g|2G)leX@V>>FOj1wdi!$XK5UL`JwIF{R@7 zFH7^qtohRy$Y)cP!Ng)_FBc(U2~N$^sK0|&-;O8@N`3^>r@$;C)F! zPQlRqdr9p)w)vjlJCKoo9<3G?J+v!H8s82<)kwHb4No6r8=>DzNKW@aP+5RE_Sb~V z$RL#yHlg3<1p_C)AXYg6-VS^8Dy;!+{Q5*dHY14?n>&J8YGr0&kwZ{WEgmF=sBO1B z@pZLP7%G4qaT|(pvLi($?BDE+_3v^>2S_GcHhapAL#=B&W_$ue6GXf67b_^_KDrkrM-eL}qBa}+7@m5Qp^U6i0OkldAuQ0S0L zJ&NuzdsjD62D5{c^4}rzXglTF`b?g?vWGPuv?UkrFHS@$sdvQIwejDz-TbOE`5{W) z@Agwz7{0mhwI>`Pgbv@`W1D=CC_RkxxGXab!h?i&1(80Flmwx(Gs$ng1VRBInc(h8 z48NWOk4Ab^TbxOJQeLcH_arK;!-Jy6;@&aW3c?*t^-=co(4$_Y$7)XH9v%O7U^kfb zbpE=&0{Ep?bW^6bun0l-$3`RZKIV>3O&o;LzOmkWU%V~W3wTK84flJF4)IP|2yeT# zLJZjs#K1@*;ejC=u#^q>^DgOTZ_*qhPnNKqXVUPgiC7Hv6y)r6A>@qplw2rN_uX`9 z#xz52Te+UO3q8Wh?~Qn!yHHQ{POI~v`A%-w5HhkRxl8OB@PHATxzD<3afeWEQ@bu= zZVla>3Afu$6ThPAbjhWQGZe*vnUKdFt|VDR3BwR}@6V#!if(tHN;e3f*q?N}pyK#9 z;T*m4!o~z`1vwmhCj6*&7v04yzu=R*qJab(CO(|mozu^Pk7^!$>>&XVaCuN;b=dRG z0RoV(QM&^A&;ims`&F>o6{O(h-r_n@0r)Erk3ihf%5S!^a9_LW5;_-UfD>=ti07&t z_eoC~Fp*4h=JK(O#&nCj6qz75C)c^p()<@fyr{+#KS>sq-JkC&cUR#zl!d2%6-lN{ zhkwfxw?KvS2s04_m|0{d!U}_hq_6zPIPL||M;ta!+rwc0L^?B|^S?AML25Pq)&44S z>>ctE>3)sR&tETE|hRhss8X! zF*=e>TKWFQTczxd^GX>1ha@OC+5+IHsiNLVG_Uz)cqtXB!!jT*rE8~2fsM+-9e z1c|cc8cT=xOASVg{-p4{R=cHY^X&%eENM%F_Sx2@B2H|Sss$p4c;o1*vEig@1DU3( zrZs07caxCGp~=E5&}29v(XOfwO4-JrG=coA3M4MbKt-j~11{O;h0{Ji0HOSV@FI zj|z&faA@hGt`%hu60uM!aU7*t<5}6Gh#B!iQo!Fc%I&l8I$SK6ENYOVq|S=(j__ z!RW)u$72{(pWTBfy08{=k->Cp4@%(7fH^n=7}{jXc@_8_dk621M1(qIQL?n9z4F2q z1UzG7B}eRr!^q?fC^me*70~NzsXpzPqT-T{MOlJmJ2_i*$s>diKV1Bkz&_+lolLIYOK2QY2Aeo!q3v-7rdV5*Q;Wq;bV$KiG)g>vj@tsOMqmJDN%BXmaRWvQmwn=GJS>UMuyW(<$NOCZ87vut*hDPWWt|W4YWHGQB;?qPZEtqa6%yBEFxa#BR8M-wtKFZ`s2x}d*khXoU&@l_5aFIil#Kn?H2oXR^$Q28*q;Qq(O^0l( zUv@96ciB^jbdSbBKD+OLRd4OF0QXzsm%tvIf6S2$p5-mfcnQ0M-yXFv#_z5gtCkr0 zHQEd3f~c)je*EJ#U@q1j-W16D(UGLgy5GVX-Y>HGf~C8LIvaM7OH=FoG`o;w!~bo? z2Nq(T*e_Vw$`vYpy4US%dw(JL5Y1Y{HJ`inwy5f{xlKSWksI3WI9d$g@(>R-cD=2f z_<~*KmdoN&#HN4FV5nBImT(KLgn6{y~sI@dOq`Ep$cV$HX! zzO2ppJnYws7i1=aB~bo|>#Ad$2eeUO0QQxLwF8cfK^jv}kTWan+)Jxn(2_3WCd#wX zMB32styx`PJMnPj42vLFd5m6n0tXE8*ya1Xqgix!7bUhGb*a=Gfxh52N&4E@9M9SoIeSJc%+^fq#SlOH?J6ZJ>Li(Lrh*T zE*Ye(A}PfoBXdB?9I(NA+N!DSBR8qf%Ny5SZaJ>BZvr+m z^^<{Z+o~U9EJ%Va0g!YlN*$7r?6u}nTdI3fLKQ@E^GMKzP8W6CMoNawST$WfmjCUl16y!y&*O-{|P7U3&__Q zNY^`o>COW6v|!{8=K}E(hZtp-P{(wIpIyhcYGU*~s*6HC3mZpyK}l%PgkC7Hh>Y6x z6CemBcx$TmYLeCzRdqCjvk;^uHkn568m%z%>7~XsY8H#lv~A_kJw9DouATkn0lC-g}qWUWGq!H0TJf9i6&Nc)CSqq#T* zc)?aGj*5+EtlvM3^ictERs%MN$sAS)zRMv4DBv-ipMr;g(@w2-q@42@)Je zuesQA0*^L9Iqs4YkYydfBxN53tg-kmi{X<0Q33M%E;n+BnnicKf$i%Ewam}al!9dC zV=~@BS8kNl@qQyS=#Zvghnu3&cEDP=ocs9?>Z3d;J(b%g8}!HUQHT2NSt^8jIcBs}NC()bHLT ziceSZuf8U6t^O9jx-0HdIcEp_dDE5wCd$1h7?x0eoNPTyTcFBH2-6YvJIqGl9VPWbAf&WXi(j4D^UE2hL<8rSlw_22YpZwfq8m>3&~P3d?`K1 z$hA!y=lw!N<5f~vn{R^h8TM4Z0i?zC^DKBdFRJYEb`5c5lBo+tm1*cHaRxc=gUPYxnyfns8F$4E+W?t$ zl0SF+QME`ie%*O`7L5XQuu4|g%IS8ScJ#@S5ECw0MobO+IvAPzaazJqZ#~x!Vul@n z#aS*_T%2it-;11}#dkJO6Mkqsz8e=6%i2YF6Bhkz_T79qwqQ?~g@5Jp0PT2pvcs)WRW)2ZCVT@KS++V zY@D4~fTX$}35v{~F?b|aP82^9f25nw5p`^L@FXlXT&n17AyNsr06Zg73)7|VyWM^(A%V!ZSf{V;lGmGafEo1SCt_Lo=EtWvd?F7pV z4x8XzDZHzm?TDGsso_+zX7Bfc7ES_!2MMldlL!YC2@pbP1D0GCvT%t)6|=2R;4)Cc zsqlQzAaaRwsLpB0hx5)I@byK_D($B<`zY5WE(o!fdwszZFWv#rmK^E9hCGPTSjzZ$Pps5=@2Z?fPynlm($6-_^JmuZ+Ky|AR zsm=(QeL48N_)p#F-2tVOf#$~BKp#^cmPc}x04gru=^OdZuRpFGMfD`LfJ){3M1{Hn zagO|-2!PxW0De8%XD4ijt{7<(nyj?2i#D#N&Z0S?R4Ae+bk5c~a9n?^nvohFKtib~ zRk6V9pM_8g%-vT@)Ja*6v9%jWAZ1N(vXC=-_p}mu!%=Ii2Z8Drdb@j=j z&^_Cs3tQx&7|^=2k*0B{r@OaU;4<wrDDBV=6098`3fUdqtoD~v$j z{@gSyvt1zPAmP;+`7h+NiJ9+`JjJw7f^>72hZZG2NL9nkR3Ne+eaEn&DL%FD$Q}~qZc7N(@NNH(iLf| zd??@8$J@O567#(cbtSUZ(BMSYV^t-R$+lqs-D2xg)-%o$zP?eFZ>bNvXpahf|Q_K#Nx-%yYG;s;1^OM#NqKYG!Vti7!nyJPKRG=*yJD#p6E zkrNkHWnp_BzSFkt0BK9|k6vP}^HJ^Q^O@|iNFlt*Xmm=~67I^xy--OiFd^)VN$Mr1 zN|_`wezf>=uCE?)M*;*eFmza}n@hor%~GR{)KPj-2b-Ruw56Je5Chs2uASd#BX_L% zXZK1|UbO>im3Prju5U7*kNLc>4R3PtP&zzIS`Mrz5uFwqi5;RGW-4qj+CnWug{P^T z3*bc(#8REXqC39>FaQZs;Sdq?)*!E#Yy|Qfa5y9rH{;M91~UXNNLv?bmWNs>keXu* z`i|~UDJ&A;Dl8bC-7WD!vtIJy*c?!zv`6Rzx0M?DSXF&U76urYvQ`}bPu6mBTtG#| zj)Sp$Cn4c>^kDey<@g!ZP?tBFn;mnB3ygAX>N#Aql997hf8zMbK^Rw|n;dXGK$)xm z#L%*1lDUvriP0|K67dK|n%wJ`n1llT7Kp=A87u3-Q6oGnw{}ffx|9#Kgp1k`L7UpL zsmY~Enf0R$$nbq3(E%7l<`yzZtOk~w)O*gP23sdNy{UG11Y5G~?m+o|;3I3BWFK4> z@uG$%eg=@WZKk9d*1XGMT@KN9jUR`QCxaJ1mxiQ0{Z8fNW1lDEuaE@bKiaex{q=^0 zSibqZI9}Tj&3d7gQ!4&&n7dM*>ogAbl&M6XWmVMC#w~gpqD1l6jqcem;MXpoii=#h z9_iG5qhq%~oU+{BRo*K9u|iWNa(emebj%(A8t2%i{Ue(Q42F{XvgMJQB2mDht5&okdIijpV&~qSpf?E#^q&}6cuMrVl^!w!6v#h>vMv8!QTsGBaH1jmI?vZvNMI-pg=MDFkldA=i(~D8W=NE2WA|j=xAymF zR&y^%t6;;>pI!PO<4dUEAy#Z^&XOX-L~7pA{DdNmjB=YjRCq^b$9-PN^br;TyHHzc zN*VyWqB(pi(w}LG6+rL{-NIoMaL!vRy&T4mbtH%4oKMm8JTs2nthMrU#V0WiZ(RNb z4r^V#VeZwRj*C!8QtrY<5ZJs)#d|+|ahDX*kdo<%A4B)Kmku?NV{N}bLA##$vbR52 zk6$wYJx6w8AW^H~3>_f$|VClwx2%Z*?>E>v*5zJ4DB)_tKBNN=wc5<7fsvqKrEswY9slaFry zyD3k!i8lP2@0_~$&(4QmG@j|Yo-+B*PAR5A8i!o5`lR(Q0_~=mipBLW$^XF2|BHz7 zA2Uv5YHGb!wUP9hB2Jjeh3vIC=oK{X7&Gow^bzF79)@Y%3VkxUNzsModLY5=&IGw} zJwTYh`IuAQsB#f04u(sCJx<{)v;<=Uu7D0{j@)O`#h?4-?=GcVd_;t*GIOi(h02dK z$9l;hksH^c5G(B82sYqEXt_bebbHZT;eg@$HlK^KeecoY;Dt#V%p8|)SpHbkr%9?x zR+@$k>iPUUd8KMU`uqNH3u`GsZWKtvAWuTCKLPe?>`n2Y9ueZhpfv0>sWweWk%s75 z0FRvv6Nf3!TFhY<*lsR$3U@f`bGTAeOT({K2-s}b5|PHLsH7UwI->R%B;d3Cdfm8i zNE+CyaZa~%Ny+M%TLIc^9~Z5oGBt1t?McFUpsb!lOf-rlvB5Bb|#^qDp~JDf-& z%{Ie==ng$GlVdl7bW9p(v!ZU}%c>#Gs0MF)MVu{_p2if6qbkHC4ELH4^pn!`&w@>jD2nF3!dRrfEb8UR?wvuSmI=DAD*pJZdk& zyzwJjsvKHqt^yY3!FyEr1SeN9D~!z!BTjhc1dP6`Qw~p=QC6jq0t8PbmZn6m=)vC? zXlze{3jl6-W zU|_5N=mpwN;w#}ILcz-)FoxdIQz9jEFHC>!4`?q zst0>vk_LPt8$o>`lQeKYF11m&`I8OZ!UCAG5snC^(M%5*q)oixGg}3mMPpeT9{>-r zK@d6w(Y7J@5k<5|<$6s;mH1EmW?X+5p`Ewy23kFo{0IxB#rNwjZ|#n7wExsle07bpN z_k=QVlY9CCr%dob;-L-L&XK$$PwRK$aFJ#}dRHuT+_y^#A^)7Vyv;ga!J+07GG{2r zwH2H3;k1|1#9l>BNT0@D_S!c~>Hh@w$1`?)=7LL}V|~XlqOTc!;@;zn*uOBEjS5~d zZ~F5X(;X~HUUr>6o<7DUW4WHK`qF$F-kI~4xp~{Q^{2vzAvosde|8+D(sVVhrXzT9 zA51w|sy$u+u=!lEGe_%yXI!$5Cw02lIAED+m$c4 zTv=Zre(R71g$LGgD@>aMcqDCY#J@#Zn#Q-g5K4Qyu4{45mwocjR4#ED&-KLnXmPD( z&#`9^E_d$peh8qA#r0mp8x7RVH0bY#xr0qAH_}^oST0tayX-=C%n1b2c5=m7W^-Mf ze#YNSaOuq)Nl_B)*dsr9O$qzuj$1mvOc&4IJt>YUx^yuf;Z7jdFPn zCcfe`(8ndeqVcM!<({Q|zs3RC$o=f~?Uyp4_K8rrX<|)nfPs-i=e?v%Vc5F`WrQx! zL05IuXwQ`)zH2V&B3WRW64+|zsH^(Zk7+7%B87{TvcM-3)M?;ya@Z#ZrmUkkEVBBw zRqdy5_aW*=)_{_Jj88URT*Ni|r|kc7$Zx}OqE5z_TWk0JR%)g+pIo{hejJE|9nAsq zENaeNLLI;yzqT*OJ>@mfTy*wmK3MdmEyZ|vh0Y0HQB35YWKC(E6U6T8CO(PLi*C>4 zq7q=xDH|bs_369(1`i!*#x?W0GH2}i04cs4L0f?G*H(AgCvnMRTLHE-&fa-BYX{rA z>)u_664WVD!>vhFxR8#Qul$vK)c*S7`LAAR+2mXLm?cm3l1&^Wqdv{ z|6hC88P!y_g(F2NN^_7pN*EAifB*v`MMMHf5r~ukDoqH*0t`~53Xy1rp-M9v6krQ!m6JA#;H8qzyQK&ZSByAVC zcQ}Tv;p~s5%_+LDCJNr@BgBjxUe!Xehc!Unkr(}&a3tX<+JGyJcvw9Lxjv2WkBBL} z;>Ly%YO1coWKa=tJeGEKcawegQ&BH{BaD_2KV^%Th_&_=gDd@0j&wU;L&oQLy9&G9 z1!hpi8#8fYe71-^^82?b$yL?0^-N_-6dTUJXY(RgXehrlZ=b|Mk$7YNH3`+G7k;o@ zBu(i~#`%x?rNTrPr*E^Mbr06JEi>kwIDi0cF>flsElvzQvtsK zL}_894Uhvs4gfj$w=Sb&i>Fk}dgy82r|U~=L~UrZme5#10?-WuQ69vu6b5pZkUiyt$bv$xQ-y9BtE~)$a`rO zK^-n090%|p_d|1}!ANnU{tc`#e*?YGJ*+bFQ?()z6)FHeBsTkH*q%!=( z4j0iN~YSuY`CK;MgLMS#d-XU4{LQZ z9`Ym6haj=36OiyHTkL8{^po9;n7~kV|6t!dr}n^3W0AP*Y`b8Kqe?a>(Kb=aB~itV z1>$&m_r*r*Q3fW0p`a@?%&#m)3dV8=n_#)cVoBR0*ENM7(&bN4RnHkCu;^dD_=eAU z5*uH0-F|5w`s4j~dOW6bNcFXDBcl^VECp)TCwksdjluhfB`nrWQhvjmNqG}%OiiJ-YCmGS+Sybeol?p8KAud9 z*NJIxg*`Dx9X8CkN(Fee@6qx-lzu2J8tK~GvZYGTThaBln^H#|YLC!^P}Q|h!&H;6 zcSdV}kNJFJa(rlv)pK`O(3O*M?E`UnZnMh0S~22G;v@11;#W^J;e`%OWN&_ziusW* zycd`2(1=eebd>3!+QQ7{kn8L^BWcI#5eBIYIIlp)c22qrFW~m$f_vvL_ z@QZ9Ue3E;0uus$~Y+Lv}@n0Uxo$sotWKH*kGwubW_tD&Eekd67D;ea zl#?wEIdy%QMv<(lt4l~iC~@g*twtjP+}JeCt#YiZwq}&<0@jBh~8gb$+e5X={!=K+fS%wfocqKjC z_0gii!!P9Psa^1c?`7Iw$*gX?pq~d1{t9`L)6mYA5ngi4$Gidqd@;vE*}{Q3Db@lF z8tZv)$lqoKNthBxqp*{(CUg=!P9?*kpRQCGI&+TDlg85Tw3a}euFD-FSZ1h77;-U# zdXTi&YVx$c{DfWa_YQR&xJWk7vaEeN5Z`LV>n?#eL#X4O-1dt_t56CnRL+0vxWA3r z(0OwW%6bi#gdp3)u{Ssj`*HUkw(cT(=VO2L_a+C6Phh_|7H|v`8X1h?td#*n|E3Qa z1$!5quL3=?Yw1DH;h2!)kWMoD`z=<46ak?Fa9HZ6n1_5qFqq4w`BTo+3lI^G0-NbI79OHkC!BTT}6Y5 zcypYnh{?^_&A~h1se{ear>@R+Hr~!2?jn! z2@Ns#b?^cZ3k&heX=r%nU8t6)Zp!6bPn^EF`voN=_@gr6!>a$0 z>9As*l*Vm+WHNuV`!p>I>{vDKo6_ zlE(NlSTH}XRtY`j#vt?2>hXMV6r7gu!uD~D&iP)WF2!^;I2hL=wRW`aD6bZ>v*TxX zFEEmGcuf_n0mxTIl0tt4k)SQ0PHQna2_pJ>PZr zh6&lpL?6}V71f#9SzGO>kI@^ck#+FQaG*^>e|QmzL1?Ye>2tnUsX1hy{!o6D-9vgM z!v2cNxBj8`u@b6oXYV>^>DEGaM znFe8R=r6u@Hgy_Sdmy|V?ZD_Oa*yUT%dS8vDt9O1>MMnX{5GG6H|({Mr{Z2l+qv2O zWM8eN?EXA;mHh#m8F`6rx@hhDn!14dw{1(P9_#RyIvb{h_1P6B+=(2{czH!qbGb~S zBi%IJpK{#G-;INvpuL}mU7n!kv+VT>lS1d%fMw6XxNKu--$t7E@I3BoD)|D?=VPie zReJ2bnptZGyD&36WOl2Eb@}uC)5akhK*HwmFbo92A3E+Ipe&AXGPk{)2PayP&>L*- zE|VG&q_bqtaTh*C2{ngo;G@zOgqSaxCWY2^eF6-_ZW#e%|-R!4w+n;tqkddhSF^gYss%aB;TaC@yN ziK*?iU_*>j6@@NXHzJtfjmwQygDr;yfGIw3d4TU5?;VLp~icPz3@=d@C z_8&Cy52AxI#*0#Yd)s;`qC!f7QZBDjQbbc@AC?6@a35Yaw72F!TRxbvY->p+fgmsX zn=r-F*JzpOVvNZOX>J>1&#{nM%(7yr^fe8}l5|S6T{@fAIc8#;02Zg|B+l<@*UX&S ziyY&-CO`XpNVzat`D|Df#q_DLyti)N1w(_XA3lTB??8C(cYFjUy?^$AzCHKjrVsT- z^k%>ZOK5~cOmah`;EUwXuzF=tE&=Q8qZrMziJQ@vI{hki62(mXC=<`&{4N^Y&V50A z?xmZ{Jea!j_sy)vT>+o@Vsin>uoui{1sX6LKuvS;MC{@933bJX*sEmrz876d<`o!6 z^~cXWOm@XS)J>v35T2xgny zebREj{QhuYhJy0A%rbZ~YEALfqe-qXdKzXUlblz6%S&2V!=|ZGx4Nq0JhV}%|3P=r z9$5ZN%6I3IyAAgRQTvtqo1QXhcT#S#mxyqicr6%Pgt_;(Cpk9NVEMWW468uq{qIeL z9*8*XSelyVD2Gg)x0dNG9m!$r=drylb^qpuwXQ|HJj3R-tj4{g>o?xoE#d?Oz%mts zk4|6HOynC0_hmoMg}-Yy1>ru|56Q7e+;)`EIIDmJjl$kTdwp zKs^X6dsEHC&)V@Qo5mg_%crW`$53ZlvK)N+^CV5*#gbSmn{&+rj?`GTOs1933=d1^ z_?1$tSekE^7fXN;9pKiB{AKY;gFPLE;c!EokE?0MB`DyFX#Io$I1RKhU<=Y=oLFv(+rdwSf z#3G;F#C!$C+tFqw+Dtj*x6sBKD5Ykp0A>-w&boK!oVxQbE+!&LMSWkntDFZ~yQ^GW zyo|VTzp@~Du!#BH&mA{M^rswq#nHn%%2yiL5CxNsoGgr>4%ArHQY7?{U^@4Sx6lFunbB^o(>#^I1+~=D(j7c72m6xhm~S z$!R=BMK4HA-y}dUNJFn}b%UADcaG0^it3yo&ABFla}&9^Z<|^=C5z?Z9Zxt^Y|pFM zO5L`7pkgb)cQtj#M8(#pQlXEZz12E4!h4GBW3b(JjcBUb`^?gqY2n0RPd_Mmj5T@0 z$9m5F#`nfUh#pxNbsNFS%(cwPa}8;qTikC2P|r4|bkuxt!-Y2}YNI!iVs+QlW4|<( z1`GAbKM{xxxSl;O@6usY>4q!Pgi<^&mG3M7mK~by~1gHf%B^L@`Y0i`<|wSeA2GBI8$qRt;|+HylHHGI`d~8E?7XpPC;fu z!*kWuu0{7=5VC+zc1vSRT?7_tq^WDpp-a!8b#0K1)=1re%)}g|ixcJS;rF`*byo|d zo(C3(2TFwnN(q!c0)7*;wDA(}lWWR4Kr*M9zxkS$T8vN2TR)hQ>s64G&||OaAamBp zg8n6KVbCBw@~`YZdoA=q?TtO}z&q#0Vpv0lWi8Ow=#!nZ+uX9v$b7; zw3LO^ipi>xpZj{aG_~E|M3rnIU;mPnG&U?$UU`U*2L?S_SkxEcV+`~90cv7+F=NOw zXSg*jkT-3}v|b=8-`VLO6R@6aw9d@5&TQv@H+Ca=b6OyOTHwmGK<9cg%bWnF^`2Ji zJ-tu&;%5qPblvNT$bS=_|2j1PT>CwXm-%l_>kCop-y+q&6`5blsxQQl6?i7=Az9W# z2uO^i2xF^yh0;)g)KDS*S_Dm4@DJK=nuc1ox3AD&kKhh7SEp0WbLW!KBQr25CkW~i zaB~Mrca$XKoh`2i48EY`w);Xj5KYw_88lhT0|kj69dKVtA2rDjCducDAV2pqIkvR* zmiN&_F$~X7=E!YlYZE>jaX~8KO(;LhqX_lJEd1P*d{*kMJyYq1&+L3vJKJe2gCpga zo^rhtjkk-&T`Xx{YXKwr z09g|e>DIo3e(KHcI#?KjQV%i(zZ~O>A1I`_@nop_2sVC+ zl5{<|Bpqj?w`QAX=Q5T(O*^lZ@=Vm{eBQG3dwsfdXL>EBkNIrr^B0V2c9EAn!T`&+UUk;-;2v`E3XmGv zU-J`6)@FnAARjU^yq_VfvV6)l6JF(fisrj24Nz_yAs=-%aordc2g-mSi}Vm8@X(4OC2n95Jnm5 zo|?}%AH~{uoZGPho=4|1OUImEqf5&dw_uN_lDN~?CUzUL?)8GKuea@avZr*e`;E?P zA_(E$yzt2AFNrFV=fz<)4K*1e$q=EzR}UHyZ)(7-+h2P1!2A9-U#?6zuuhew52Ok2 z%<-6yk~-SSCnv;YcE06(@OcmNgEDzr@m`s(+Vo|^%Q`Fpos&zGu0W(S*7xDGoT_ly zb#8;W_-ez@g~tQunFDL=Cwv`|s=&@0Hxe{&hpkw{Q0}`r5nXNkysLrN?pAx+Hb}{D^bidQ8OS3n5bCMTA`$E5CX#F#z6xE#S zotGJ8_I7l4JD2RFs-6w)g^E!wt@_Z@`#sDdyx7B6W%6D*6>jyR_;g}=t!BYky$1ic zP-pTL)6>gNc6a?FY_8n)V4um&oi$k28{T^LIRvRo#nfp$nrmdBr3g7SdHeNjj#+V? zBKgex_=CRwm40~$3)fXUh94s5Ax2*%x++8aY7oXPU*(oRlD=mp@95%_XjX4m1u_)@ zlMC{N;*3>r{(NRqZUs82^#+mGzxLZ!^7)((KSj;jR$En{qSiBYYQIChGUwHYG8VN0 zMwkAjsz|$dvj=Y14cKTCJ{vyS@J}{N%VqOSOMG02R9)vuBfmu7ZIs??fPd36;e5*_ z!`c_#!=aKB2qv4-sNH|`Zb8+g&CHdyBHfzv%jjFVtq*jX=MMsh9abwca!6YuYUrwR zIiJv7iP*Jz?(R{0kO>@-P?b0Ina^EXYNzMHil`|(=-b!xS*sX#-tqYOp^5SG%ubF& zl1`}<<#UB~rB@>Y%ynBF&)bv)tB%SwC(ADSvPaeE)qvh3S6j5v4=jcoawK1!a%GQx z)B>ifn(6Ek^c$P9hmE1e%25(Y@h*{^yG$vC?v3-LIiq3KSD#<&_Li76<%|2t{Nrq@ z#5>9RmlwM~o=vG4dJ$WX%Dj7I+;S9_`>k*o@04M>kfM^<6uz*W6L6u9D&=7la6tDG z_A4_yxTU37vG_~G?fIUW@Szs4f~Ce+BQqCYZImSv6noZWCUB$B@Hx1a?8O6!{DL&s z<*otm0ObWv!zuWBX7y%YkHrmz(Vho<3iaEcNj#UGTFO>67K;n6Sp_Cf=@$N|&U>97 zZ+?|KZf3Y^_YP7Zuc7QtK{9Bfv|@X_bilIZ{5S|}Y^pO4A#7HQ$|+_l1l}U-W^G97 zqCUBQ7_Cmq-&)iE-e!+~i5qcg6mW3Y(?_Eqa9Ge&QT^-^jHBGW*#!RT@+^y??lF)2 zYVHICFa(+r6j4C%FW3Mm0mx4<`Q>%{@tew;jiJqy(wu>a4>#*7&n;h zn7{?+YWd8z#lqmouMfuuO9vf(N>>U}1O<3(?2<2D)Fv>@F8<-bkQ@uZZc` z`(@ze;Op$*Ct~2`d*8v|TiDy*oAecl2SL-sz?4VL!^?s0%yLyR&r(VKQf$F;W+=~M zMauHq3ZCT*;$?ls@|*hQtYL6_A;U>Vd^r4w&C81;`rC=qca4Gi1Qf#g;&vNdwrHbT5*o&ovXFSW0 zoaOw`-*5jbiyT35yIsWAvk={Iw8R|X;O*_~?gTg%K|Tq+cPT^}Z-cH3A>kjdY5v^i ziTUZi9=3ETQ?xbroYAdXd<&bWDA?@6Py=?-`5-fCe>2XGmu9YlbiOC$%D1TJbt6%+ z90oTuY4TK&Z3#{W69H!)bD{j|YVv*>$R5t#mdy?1s-%9*`{MrloyIb?PgxnKO(GF_ z?GFLp(!icWrhYt*#dY@8f~ zpSn9m8|Z1!onb!)qSMk;HzFY+8zLd0AfY-%e9~$J-y&wD03!`mk}^2w0`bPx!Bopp zSC`~E@tTT+f|Q+vDBVej6eUq4<@j}dhx8f=`Cre;NJyfbi3tBLqesk-f7Zlw4D)wR zmP7VW39_La@_(+6fk;T+5w=~48TC_5GXMz*eaCSkO%bH$AR$pE(Nb5r?@ziib^4_n zjYZuI<+a<&j0Qy#=a~&-uI0R=V0?9n^+wLM@S(@b$t1RKslttDuiY-Zn{f!F2jIBP!EWdIyamWfxze&iQ zf{KwxIh2HyjDiZn8&;kD_C2Ti-?zulKj|qO1o8YT!8pppQy?`hll)itwZu0$e-B_BW8qis=8a6Ys4fLUWLz7=vGL$->9&{#Ds2Qk51`#M^&h$tWml zx_{L~5=KWB9!Q?0M*XWfR8JCq*F?raL9ug6KI+`B<}eO%k^jaeNkz3t=TCp-S95p@ z6i)r_yelK)5XU;_-CxZicAWNCO(z}yH4Y~u{@3(9nd-k*@yQxLW+Nxe?H8{-;p)eN zm zXPJ8Wm2f@tlNd#;V;HU(aeJ zUPuEjY*s5BRP$R(!Em276uo9)N;`%6LR$+hqn*OFIyWsA?wULVV*NmV4dcUQzO#x; z9{6LAb7YUI@G98wn&N5cU$9l;#Ze#dc9YQG_Fu2>1R#)UH*{!4d4R7j^@+ zt&8$*G+qjek~wPSK5P}s#vDy_E8#chcgjIWVqd){C#tiLmgnb1KscwV5G0-{d#@vt z(7JJ75t|S=^xn6X;9#}u(CZg6;wQw?JWZK!Xbxuz=f=-)Bgc-%L}t$&kxWqR^Fpp; zLr_>DjC8k5>V~NY48H_}_H`4!*Fk+k9zY@81o_onZYRR72cb6TTLVERqkF)`>R_D7 zV<}`U2qlJX>a|#{3VQ85glY=d-0yQz1hDl!LhXO={U8#uFf}2)jF12&VAlF=>e^Rf zAQ-Mmz8g5h0-Qit+c=WxKjPQ2Wa0OS)jh9ygE zJZ(;Z+zNHi4Zc0 zIGXfB;!*e&lrZ5CL1>Qc445*^?%m45HutW0fDYyt^g;WRE3~NZ-IM#L(A4e6-C5%* zjxvKEp)2KD3B=L&Q*dQT$@i)SaoOpIEZ$3_-q?`@tp*(7-LhWxGff^$tpS@5I~1Ff zXj-hHB#dwXgY}KgcFUthdzB8`m2Mo@4n17R#xHmkYFjK;MBd zNGS7zDLr0Kt!s9`0LOYQeObN&4i5?8mFfH+b_k~xm*Rpbk4K(UdwRW5_WRpaZ)0km zf_|mt1LC)zPxL@)(5!EpPs_#K1VZBq3{u^<7L1YevuTFn$48*MVjDs0k{h?`f=~y@ z-a|D7Zj-63jh9Y0VCyunir9V}K!+2CZCRUou6EJyDY)8YX=RoLwzt`WI%w$?wA_es za4{(N*pe2(*kxOt_5kdLC^Xf(101)V^@GO6K=|V&fV@50v4?~EL3f1tTQ~{)@-x)E zQ>(spOy$}9N>P+U(Q=#{VBD7udBgPegRk62U#D8oJ)h2(`*$K_XX~>Mq?%{wdfU12 z>q+s1V>->VkJNXq-dNIVM}l+=xO^Oa*4%LuJEBs4Z@MQ(Ku06ls}8rB-~eMk_-1+C z5vsggPTUf1;tAc0h52LcJ-EQ#4gGd@c=`|`Ti#h6kuO7QwaI%b`h&~Wr-}~v?X=mH*o}nL;EMJJOI(3 z%)4$n&DKuhiM@07Tlk`Fx%ql(>@)R9gJLjz7BNqG%ufBn#7B4#X-G^@H)Jd;4N&V3 zKc%#pm|%`}MK(kD838@oQ)uch6hB)hnovip^I~ZYeOl#N=@}vzeOldZ1lz3DL5a) z(Xp{P;3~gux3eN@>p>KeOL_ICfpBRe7hnQROi=vc-QInbzNB8<7Tj!VN7BkLqkF~EBdxyUN-EAW{D@y>Yd9Mxp1J6j zb>DKYjRhI9yfu38S_JnxNpWPWveZgJcKLd<3=MQAx~CA>x2k_E%eq+@hq~+bAxJ>@ z%Gm_}j_8D$ifV*M(8PqV6?;~7k>=D=V;LAh?KNdVAhMQ-s@=Y zRouC)i>ptgd=wYvJ%}86t*je3ImcCZu(@r1Rm`aaE%wTs(2msc*BK|EZY90s3S(jRSZzEHbAE@sjT5@5WH63zPzrT>(LJ8p-7x*f>KM` zmJ@E+>S%agAOAyNZ>_AiW1ZqaehjRA5xTkrb;s84MQ86t4=VV!w6nr@=A)(--=KH! zTUZaS;w@9wd>rfd3Hic;Him@t-$HEFdiWGS5|XNKrSlCFL*1xFu7U^$ z-1{3P2l(CwV{dpa;={EqMSg#DZPwVMwI;$YCLPyRMO_F+Zs*ChQ}2m;ftSo-yDl(a zKXd>PG+b=ygAK^2m-lZP>vykhdS+)E0{;w8dW93XhFYgXRUCZ72g>wAID#-PwV8})JP20Y6(L9y>2K>B9w^M8F+nklpO-sGD0Na; zGHc?eYLesH0A0Pxzm9Rm8-mJq21FsfyjSrFfv$)U42K40e5;|qTME|YIEkqguM4o1 zT=U~{qhz{l{Q}Xm4 zAr%mAM+7X#Md5twnx-~2K-(Gz9{af{gxMT*9?l=r(>jTEoeR8x%a!W}>FkBfn*irG zLr=?FXDsaqJyStiR?^x|)o($f19bilooXxgo2^Y)(|9MWhH^fJC<*nCYM zW{LY^vZ{JCJp;Jor3!RKS0&*hYoZ$?>Y_Vvx42G&WV<1rS>i-q6*1Tpg7E`l#9-ei zAh)uu!M(KThl>$T16ZO~=^IpLXf@|0td%i`uB_m0M(xF+>SKRUFE#`_;?0*ZfDqVH zFZW&;w<&rg!xP+nbaBnUd+LMyY1qcr^hEY26oHzvT3;IBrf70dhl}~L?}P*$?aPdT zraX?md#sc_Cw#El|8QiIy>}!(CcP+$3iTgygFX=*d5&P43%De$&$$Z3Hm#{1%8oR^ zqubFOk1(F{X(fGgLBeO*(i#Yd_-TxvA_#yAXaJIL1ZQD<*NZjzkYnC0cexfYPzjliqs=^^9awh#drH1eSeJSe8_pvVq$p+~6I$-hCP4(J1P{_A(8{qBk(1tvJcYs+}A&?W08hFXkeG7>G z78Q{l6`(4%wp3rYk)92$AaqzGyPxfOh6fUkbZ#K72yf|nFhK#-1NBz(*G*(R>y8jd z@-i9X-KS-{UVClAuV6f-Qj z9Wvb6GjF`5PloZ7OehU#Cbr1L87&G0cG`t-z_#RI)lKqG6wv{yir8$#*rdCEJ4czH z+=dsI3!r6hpP->f3sn0{5}hMTBHR7RNoROW%k0N&d&4@J0|}=y7^km#2KWJnqk+{C zn{J3DBd2d4ekuM8CSsmGOSB&LE}rH-qK-~|^(up*k0;klBm{Ou0umcqcegW}d1&>a`?u1NX=35c%>ivhV zs3Vs?1|ZjNf)acUvn5Xo1x@WVCx~zMgSXx@@fTsdA{zQl*w9YqYo~WS74ssn8R7+7 z{aIVYA&Q>C6}7Ro^qX8k0|JWbO4t05DEvfvH{$T9Xu|xPL_(d6OEU`Vfki9)Q9gNFoxZ3qd4gH7z?U1^E#;R?&(~bJhLxG=R)K;AEdq&1sREQBc#? zLG|p`O;JgG#$8yNX**$iLGHBZQh0G0z$0)ek)#-<%)%PrsI&Q2l zBmqH+Q4T`@QQgaEb?#-G}`-KG0jd%Y2KmiEKxJ{u$_Xf9!vuv z$7W`{oH%lgEr5fm3s5-N>H*IzkMH|n8a0LPzOqWt?89}nHmDQF^F3|6EWQFUysyBd zadB?v*n2ZThR^Sw=>F{l=b?mW6(|1k{gQ++GY<9G@%{3l50Hg7hLrqr{7@Jv8}KLm zF0V{OwYXdP+as?@s*)*x=a=JEi7{GX2#CuBbs zamUm0WWxP#HJvQAf0)tmG5F81S&u>$^WyN>O;|jTnQp2i)A#Zv=8eCh$>Y<`Bb&6wv5jSj%lpDq2SZ zC3YJbL664hXwSUwLc&(Pj1iF%y5qD?TP3@yrt8$4?zI zALx1v-ekoK8tOgO%=~!p^84ydi50Qv_N$CiBEX6CtF@JxH7%a3uuxv!Q~c5-{Cv_p zl~x~X2w_tF-*j|ME(trkDIvC?Kv;8~k{%O=^W%(>_f*b&6a5QKG2v4d*zF$!6P_3C z3aup)$r@KFUM?+TcXJn`V(zHPlAONsjM{G8$Ij(}=I6%}Pdb?$z8Uj9Qq!GHyvy`$ zHzL&F!{qC??4r!W-Z_Oaf|k=#*6yX&Z|+>6Q@=1*X?;7+G;ydRrz7r~sDZKJ8yOaW z-G{6jkDV_Y-;JYO5$H2H--U0#&(!(qn_O&>m6z%HGbszXS9U@xUMQ0ZP`PLhoY9O< z(cXs?;voaOM{eQy=Z$7PnD4o9e5eT*DHAiwn3hDjwU|~O&e(In=?(2+1I8ln zwi?|L5;9Jmpa|4JWGVW^<5<%HpSvXoD>RA*Wp2K;ePt6Lq-F*YlDtr)x9T<$Ct*-> zc|_&3knC30QcC6dAYeZdm}zl%ZO99#+hgw}QfZT#qDlAp zllq(DcQtrQy$WkOg{W$+{@X9tr87!4e)7OLD){#b6Gg!smW=-RRjmrPTDh67@3FUx zx~0#EDlj3-_ebB-=NG3k4n$FqM!{=ee+Dx^qlq=#v6!%YxZZv2vQ*g z2Vb8V*Jog`jgw#8Cgau1g<~-VuLDIt++SB@eN9fOVzU-)Ecb@Ai02X+{r~yEC&Gj4A6% zh?@x8CGzla3*d@1j4u_t2;1~1(ELEUj2+PYccycvUl}4NvWW>|`hTzqVqDY^!~Q2^ zqKv?lS1w~OckhQ6CmX$uy1=kYs_N?(bE?U5|160@%L;qvWO{# z6lXBAx-FVd)@=qgxm)HI*xNb<$BcfVF%~fHy2mG^g#u)A{@eh8XQ1cTtysB_j{N%IWClw=YewRC zd}5||nIBBWqgzJkvSpvw!7XVMg}cQmZ&#VGzuc}z(VLt>QkY$hxU*k$k98lu`T2pt zot+EDry(N(-^3zxo2Au?Wa~P1jqJ6xA4nj}YS5mSwI_Ze-5rXhGHiFf1gwJHw+0$ZkP=5@?WP`{R#Nz0Z;z|B+jm1JoNW*Pk$o#Gfez% z1QK-rLh$dP@qeQDGe+@m6#M7?6Z`g`h{b;*_%l54Zv>x+F^~TpEch7m-_-sy(C%-@ z3Lko(주)한글과컴퓨터docxly2004-11-09T15:23:46Z2026-03-08T07:30:22Z2017년 8월 3일 목요일 오후 4:55:39 \ No newline at end of file +ko(주)한글과컴퓨터limchaesung2004-11-09T15:23:46Z2026-03-12T08:56:23Z2017년 8월 3일 목요일 오후 4:55:39 \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/expected/Contents/header.xml b/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/expected/Contents/header.xml index 0d6bf34..d89b7a2 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/expected/Contents/header.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/expected/Contents/header.xml @@ -1 +1 @@ -^1.^2.^3)^4)(^5)(^6)^7 \ No newline at end of file +^1.^2.^3)^4)(^5)(^6)^7 \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/expected/Contents/section0.xml b/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/expected/Contents/section0.xml index 82fba4e..7a93f23 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/expected/Contents/section0.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/expected/Contents/section0.xml @@ -1 +1 @@ -브라우저와 Rust 코어를 공유하는 기본 문단입니다. \ No newline at end of file +브라우저와 Rust 코어를 공유하는 기본 문단입니다. 브라우저와 Rust 코어를 공유하는 기본 문단입니다. 브라우저와 Rust 코어를 공유하는 기본 문단입니다.This is Second Contents. This is Second Contents. This is Second Contents. This is Second Contents. This is Second Contents. \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/expected/Preview/PrvImage.png.sha256 b/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/expected/Preview/PrvImage.png.sha256 index 21d6e53..1da4350 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/expected/Preview/PrvImage.png.sha256 +++ b/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/expected/Preview/PrvImage.png.sha256 @@ -1 +1 @@ -a3cc714d6645caa3b0735b03d85d295b46cc486218b1d676fb74adcf4019df24 \ No newline at end of file +94fc0a7eae273be7384b9e9ffaa8ae826d840ae4c1a3c758cdf3448aee8051cc \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/expected/Preview/PrvText.txt b/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/expected/Preview/PrvText.txt index f9e5270..5537dc4 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/expected/Preview/PrvText.txt +++ b/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/expected/Preview/PrvText.txt @@ -1 +1,2 @@ -브라우저와 Rust 코어를 공유하는 기본 문단입니다. \ No newline at end of file +브라우저와 Rust 코어를 공유하는 기본 문단입니다. 브라우저와 Rust 코어를 공유하는 기본 문단입니다. 브라우저와 Rust 코어를 공유하는 기본 문단입니다. +This is Second Contents. This is Second Contents. This is Second Contents. This is Second Contents. This is Second Contents. \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/expected/settings.xml b/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/expected/settings.xml index 5d51824..978b884 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/expected/settings.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/expected/settings.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/expected/version.xml b/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/expected/version.xml index 549bb34..4ee1300 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/expected/version.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/expected/version.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/fixture.toml b/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/fixture.toml index fa18aa8..5e52e53 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/fixture.toml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/fixture.toml @@ -2,3 +2,5 @@ title = "" author = "Fixture Author" strict_mode = true determinism = true +manual_verified = true +verified_at = "2026-03-11" diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/golden.hwpx b/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/golden.hwpx index 6f4ce45aee697e8ac6e0a1aa09fdf0b505ea5b94..07f3cb9517ed16725e3da5d3aa8531cd0d2f25a7 100644 GIT binary patch literal 37624 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 I9trLJ2lW7R(EtDd literal 27585 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 diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/hash.txt b/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/hash.txt index a878c9d..2deb215 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/hash.txt +++ b/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/hash.txt @@ -1 +1 @@ -8d760c34cce95e8e19d689c04579e66463979ac2877c03e9115e239cb39aea4f \ No newline at end of file +a8aa040c5bd23c3db9b31d78bf0b527096a8d53e1da09cb4181c3584eaac4f7e \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/input.md b/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/input.md index 488ff97..238c49a 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/input.md +++ b/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/input.md @@ -1 +1,3 @@ -브라우저와 Rust 코어를 공유하는 기본 문단입니다. +브라우저와 Rust 코어를 공유하는 기본 문단입니다. 브라우저와 Rust 코어를 공유하는 기본 문단입니다. 브라우저와 Rust 코어를 공유하는 기본 문단입니다. + +This is Second Contents. This is Second Contents. This is Second Contents. This is Second Contents. This is Second Contents. diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/list-basic/expected/Contents/header.xml b/packages/core-rs/tests/fixtures/hwpx/approved/list-basic/expected/Contents/header.xml index 033dcec..7aa7b2a 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/list-basic/expected/Contents/header.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/list-basic/expected/Contents/header.xml @@ -1 +1 @@ -^1.^2.^3)^4)(^5)(^6)^7 \ No newline at end of file +^1.^2.^3)^4)(^5)(^6)^7 \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/list-basic/expected/Contents/section0.xml b/packages/core-rs/tests/fixtures/hwpx/approved/list-basic/expected/Contents/section0.xml index 7439f42..678640c 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/list-basic/expected/Contents/section0.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/list-basic/expected/Contents/section0.xml @@ -1 +1 @@ -AlphaBetaGamma \ No newline at end of file +AlphaBetaGamma \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/list-basic/fixture.toml b/packages/core-rs/tests/fixtures/hwpx/approved/list-basic/fixture.toml index 4ec74c9..ad27fcc 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/list-basic/fixture.toml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/list-basic/fixture.toml @@ -2,3 +2,5 @@ title = "HWPX List Candidate" author = "Fixture Author" strict_mode = false determinism = false +manual_verified = true +verified_at = "2026-03-11" diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/list-basic/golden.hwpx b/packages/core-rs/tests/fixtures/hwpx/approved/list-basic/golden.hwpx index e8d7a72f4661ce3855012990f2c6c011b845d8fc..64e8c95759b2338f83ca940be214a69bf08bf806 100644 GIT binary patch delta 4179 zcmV-Z5UlT|*8#QG0kB5_32=5K$q@?x027l?0VjWm#i729^)6&tj+H2KV99WENid>D zB4difB}egkPg(3`fgajJ0u=2Yx+u_Qg8&Ws0p!sBLn{aU7djs#MKKH|rix`L{e%_$y%_5fPgMtiwS zqufF`kFm??D2MQIpybhNq!a3kP)c7)O2;k=MU16HnEPfT%YidYk6`yQ z6dNxkHHIER?kxw7W*D8}`$aw7Kx62)=nQ`YnZe8z&cwBLusg~R^gOcA7}>f)GwbKnfQfTpUIJbgN=iRo|MfG%08TW8fnQ!bo|Z4!2GDGf&#&MB?mp!EY4$eHw0 z;d(($9os{bj*N+G?_%hg-mVSD$lE14H5u6htWTITcB8d-`KXGFu>zoD3h51-nhbw( zd@S|R$h13?vBElu-BpGd6B`pzpqj&L$JF_6(znF3uP2ZQ&AB{R=AvPn1`59(Vw()1 zjy%OAXv~|eU+7j&tF{q^M~nAucp8K z&D9^DQY4oSz1`~_<`mOwj{9gZKwN(yKd%A2tKs#tx8FZc*N~53O#@6_+tJI!u$1EC-Hj+!SMh`amX-v03>j?L5V2KYN#@&p@WcYxeO)3PG8>4Tp= z3jh$qMd*NA;~8*(FZiWSUjxW(n=EPS9Czq&>AMJ?H;`p@5lc=85)W0O*Ry}1XZWZ^ z{f3M2xxh?KXy6G`IP*at>gV08LKYg5eQv>=)P*@9G;2|JfHEFa?AXFo=rfi71aB2{ zWPGSB!OjOt_Aw!N{DHy)6OOPAEnl#E+-@B#A(@}+=A>TNWUp^suN(UAkoNDsJMj!& zx-e~C;0)6c^=&!{$|(k<>85{w=d&9B$tlos*fhwGpgCpnfSNB0&SClMeU~PYE-@3b zHSsuLO#`uRq3px3D+GN*8uRw>n4AYa@z5bgnSjK=8f_t#v zrpr^&@`;Gja2Y8S0WlGH^+Rx7awNI!>tODs!1 z1UuXxfei}`($-***sFZheb8>#_uI|oP17=&R=D|nRrpmFgNmEfSH(=Gm2P}reg3eN z$)fTGb=Kz(%b9;{tE|DcM(d=}ZXP_!&})?JA6By1RZX=^U4Inj7Z_06bOV680T$$= z8APn;??6v13z2v84skz7I>ec$|9vI4*zkJXWj2JH``g7+$f>%G=}) zRSfK!-%_2Bdil<2x7Rv+z>j+oQ)jpK>)mF%MdwEO@YE#B8>73L<{$tXc9y>n$~KR? zh!7^h3vHIcpV3LyUhZzQdH>Pb@yUU|$=tzq@>ANC_~O{5+mj;qKS#(pGl|imJr~|! zwhAlR5ww4Orf?q(u`7IL(WhrFbf`He&H7y*9t)YSKRrTq&xCf4d>qFZx+Bx(o3Dj1X>e+x=L%(7u(tW z(+-VAvCqr)!xvQH``!!c&=`5Lpo&dhoK__tMNfY>>ZzLphm!eW<}0BKkBMr8Q@M8s zXb2}3k)8=W6FMXx4Z~myK55B95mV=I$|#EAB-1bVuoEfm^D_$p-PlMMRVhQzL3EE> z5}`RkS8?V7lDQ;vEjvCzNtNXFKIatF?QzcL@Rv5vUm(g~+6^PHrb!N41BVs1g2RC1ua`*IOvu{lvT}w%?n}i(z?|rz4Bj}tc7`N8?t1&f7)(0dm*y{ zxo9dke&oi_C(B~9>$&e?dF0ZX`}BhUQ3`%P{PKeEL;a+lQ`lhI%S~TDd;9(KU*0^W zgV7#!_`MGMLWIReIBO+xpgQ`g2)bCy`lQR$&#G9^9<>hGT&qZZVqyK9eQtkZMpZL3 zAQ82lB;YB;@Dvy0DQ3Y_O2Jc1z*CIjDJ{lR%7Ukyf~S;#rxe3e zUW})l1y3afPdNckIfkdQ7*BsC3!Z8Uo=O6qN(@hRF`jA`Jhc=&)dW1%IR7jGsFsNd z)f7av1Vptsq9sBIWFu#gMv7_@E2?o&OT-b#MART;>c&z1Bmi~w@;}P-??1f#ee|_) zpXR2DNXZwx#?=4!HI_)%(-&{1e|a_i>+d3%*!P~^fm4gY0SQjZ#MXb{J#_uu)t~B|=pIQP&P$Faoo zjzR*KOhgS5nfmJWi|J2Km8(DfIDPqZ1XAy~p&W^MVH{aJ11TmT%S73rnt}|(K*jSR z5Kll#32-toHmIe*(PDpa;^`15E&(SKUu$X#oI(svJR1U~#c;Gm43rP^eR1ugVcYb? zERIfv9Xh}7Tj&7mlQEmd5jvGXgw7Y}0_>R2b?E%~*J65L!Am?L4PVH#OmgWr@U9{* zFu?k;Lrfe_vd&F=h^dk6OLbyZpk*7#HHp89B8Vn(&(}Y%h`2IPKc|ivp$4{x=13V@ za6}gxO68zFTNL^7e^5&U1QY-O00;m803iTB;sUgW1ONa54wI}59e>|%;y4igSEAh0 zJ(MIAC`C(Glph6&5>%ko-AiQ>llV5VeYR7|{q;AtlR)>jD^5D~w95+_GNxzIWJg9!f4+O8oujb8nKwrr0;IllZ&qFNQG#OGDdOVJz(iNpKm^;vL|um0Gi5P ziu!J<6z_T7e!p*|2!9yzM#LIlgqQAlo_S3_2t35OTOs&a2#y3w1mfut7x-r&JKsRc zvphml3Z$q}(OIa730yD4NGn`|pq!%;-uiDGL;wLfR89>wbuYWxF^|+J4ca$Qb&U&rK z?R6@9UQnX++F1S6oA?&XxFfX>>4Odn>BSv=w8lw=j$B-nlM^TPtPeqV2rZI~O zEk^HjcTYDJGz{-?hs1ridDXsZH(TA{%5`*=F2!v0fI+X|TDRIE!=>%(-xGW*F#2Mw zh-F1ik9Lklf`4QEXy`>j9W6sm*s<$S(o11nyN<%IYD9!B^1eF_A7;OYi}hd*t~rYE z(71qV41&uRY)9LbV$iEy-}h_t5oLY1b-JPz1-7Tkhg*wNH?f`8b3I^o*bGnypq4>DmQM+W6t z1r!v_qR5S&D{aUSvcC=2Q>(ERn?yFKZDzjlp9QiqRXi_5k}b)IIyfnb@q`Gebi`N~ zK)p6$Ontp}UngYNw@et=Y8pO059Xl%j%3-XT_ZIHf`3al&bd0w3AJo8WqWPFwykw+ z1J3F-BY(Uju#sYY;JB=)f~wntd5(#m^HE7yD)xp=D+QI5!D%2Pq$!?eqw)MV5MIB# zzT3WTH{rZUF-cO8zuOAB9oI<;1({o)^YLW;94-K37;_{NLY=hKu+?lgL5Na4Ygx1j zD#JU?+V-I`Md`vArbOGFPFL%y6w6C6gzeRF8-ItIdja&Ni3-IaOIt6FG9iv;ce;iP ziN(+i6{rk>cjUFQWU;HQU$x0)BnUsLHwqr~>wt)9qJ_DD^0(}(a6TK=g87Z0>EyX- zZ3c9|_~vs$vx2UKv)I{Uwa1w1*r7X{-AhOnXZEe@++moYEqlS%23aZ&^A0tG^*@Ae zpMNbj;b!GJ6z@%41=ah_U;^m*<(Q|wJ01+D&%*eF%)l>Gi zoHxtCa{SOxsed5GG287VR%IgwEq#QQjc5(ii1x8+L}Q&a z;xX>`QyD*w7e7fV;b`=OW#Uy=216bxVY7qgIo={nYK!sHs*F5C2RQTXcsG4E)CY^n zQ^Eyl^;i9PhxkcZTXxT0UcP&YQ8$B(r^xJ8ClHgtw{BP654v5cn_H~@MO@b(77^DS z%JX*v^XwnEo4)~2O9KRx%V`>u^Jx>a)e(GG32=5K$q@?x027m1X*e7|;sUgW1ONa5 z4gdfZ00000000000001_fqM*-unZTI#c3)5G?VaYKmm7?F={yhXp?JdK>=Trt!hI8 doMV&nYCr*plQU~W0={RHcxxvHVrT#W0091G^3MPO delta 4288 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 diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/list-basic/hash.txt b/packages/core-rs/tests/fixtures/hwpx/approved/list-basic/hash.txt index 293d8db..820a15f 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/list-basic/hash.txt +++ b/packages/core-rs/tests/fixtures/hwpx/approved/list-basic/hash.txt @@ -1 +1 @@ -0c54ee85b4061f5c9a9d26869dd932a14c99f48896a4469d42ab7a15ca1ff16b \ No newline at end of file +a696873286394c7ab58c363cfd4d3044e4ca44646083a1fad160faefa12b6ee4 \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/list-nested-depth-2/expected/Contents/header.xml b/packages/core-rs/tests/fixtures/hwpx/approved/list-nested-depth-2/expected/Contents/header.xml index 53c3b1d..2a4e629 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/list-nested-depth-2/expected/Contents/header.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/list-nested-depth-2/expected/Contents/header.xml @@ -1 +1 @@ -^1.^2.^3)^4)(^5)(^6)^7 \ No newline at end of file +^1.^2.^3)^4)(^5)(^6)^7 \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/list-nested-depth-2/expected/Contents/section0.xml b/packages/core-rs/tests/fixtures/hwpx/approved/list-nested-depth-2/expected/Contents/section0.xml index e5ca3bf..3cf1f15 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/list-nested-depth-2/expected/Contents/section0.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/list-nested-depth-2/expected/Contents/section0.xml @@ -1 +1 @@ -ParentChild oneChild twoSibling \ No newline at end of file +ParentChild oneChild twoSibling \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/list-nested-depth-2/fixture.toml b/packages/core-rs/tests/fixtures/hwpx/approved/list-nested-depth-2/fixture.toml index 43b5910..a5245c4 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/list-nested-depth-2/fixture.toml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/list-nested-depth-2/fixture.toml @@ -2,3 +2,5 @@ title = "HWPX Nested List Candidate" author = "Fixture Author" strict_mode = false determinism = false +manual_verified = true +verified_at = "2026-03-11" diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/list-nested-depth-2/golden.hwpx b/packages/core-rs/tests/fixtures/hwpx/approved/list-nested-depth-2/golden.hwpx index 42713c950b244aad19d323076494db699bdc2bbf..ad999b0e4e33b2bc8baa7712a03baaad95444118 100644 GIT binary patch delta 4229 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 bzGjm!V-=HQ|lG3q@LJ?yj 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 V0(WSW5oi~aR%;Lj8)*Ol006M`cCY{d diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/list-nested-depth-2/hash.txt b/packages/core-rs/tests/fixtures/hwpx/approved/list-nested-depth-2/hash.txt index 178768a..5a662f4 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/list-nested-depth-2/hash.txt +++ b/packages/core-rs/tests/fixtures/hwpx/approved/list-nested-depth-2/hash.txt @@ -1 +1 @@ -2afac16b46457a0d36293610a3bcb79e20e0b329fd79d73ff859a8d349f2f8f4 \ No newline at end of file +080e18382b4286616fb791edc269237430396e953ee47b076bd856a75fe4a440 \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/ordered-list-basic/expected/Contents/header.xml b/packages/core-rs/tests/fixtures/hwpx/approved/ordered-list-basic/expected/Contents/header.xml index 27ba8d9..0647ff4 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/ordered-list-basic/expected/Contents/header.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/ordered-list-basic/expected/Contents/header.xml @@ -1 +1 @@ -^1.^2.^3)^4)(^5)(^6)^7^1.^2.^3)^4)(^5)(^6)^7^8 \ No newline at end of file +^1.^2.^3)^4)(^5)(^6)^7^1.^2.^3)^4)(^5)(^6)^7^8 \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/ordered-list-basic/expected/Contents/section0.xml b/packages/core-rs/tests/fixtures/hwpx/approved/ordered-list-basic/expected/Contents/section0.xml index 7439f42..678640c 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/ordered-list-basic/expected/Contents/section0.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/ordered-list-basic/expected/Contents/section0.xml @@ -1 +1 @@ -AlphaBetaGamma \ No newline at end of file +AlphaBetaGamma \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/ordered-list-basic/fixture.toml b/packages/core-rs/tests/fixtures/hwpx/approved/ordered-list-basic/fixture.toml index 4ec74c9..ad27fcc 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/ordered-list-basic/fixture.toml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/ordered-list-basic/fixture.toml @@ -2,3 +2,5 @@ title = "HWPX List Candidate" author = "Fixture Author" strict_mode = false determinism = false +manual_verified = true +verified_at = "2026-03-11" diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/ordered-list-basic/golden.hwpx b/packages/core-rs/tests/fixtures/hwpx/approved/ordered-list-basic/golden.hwpx index e97850a6d3e7b92c6e7651b5b12d45a9e48509ba..11246312da6f3ec3e4737ab2b8258c65a7051a50 100644 GIT binary patch delta 4079 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 delta 4171 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 VliF%60xoEi32P??(r5qx004OX?CSsk diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/ordered-list-basic/hash.txt b/packages/core-rs/tests/fixtures/hwpx/approved/ordered-list-basic/hash.txt index 6109858..e346704 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/ordered-list-basic/hash.txt +++ b/packages/core-rs/tests/fixtures/hwpx/approved/ordered-list-basic/hash.txt @@ -1 +1 @@ -65857b59e5d15ab4b28033d3d49b55ebd2514be107e844bed1e54ac0cd791abc \ No newline at end of file +63fb7d14cb3e71475762e443cd4ac5bced0b0478c7fb3154df878f6a3e374a3f \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/ordered-list-nested-depth-2/expected/Contents/header.xml b/packages/core-rs/tests/fixtures/hwpx/approved/ordered-list-nested-depth-2/expected/Contents/header.xml index 0313b7b..3fc9eb2 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/ordered-list-nested-depth-2/expected/Contents/header.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/ordered-list-nested-depth-2/expected/Contents/header.xml @@ -1 +1 @@ -^1.^2.^3)^4)(^5)(^6)^7^1.^2.^3)^4)(^5)(^6)^7^8^1.^2.^3)^4)(^5)(^6)^7^8 \ No newline at end of file +^1.^2.^3)^4)(^5)(^6)^7^1.^2.^3)^4)(^5)(^6)^7^8^1.^2.^3)^4)(^5)(^6)^7^8 \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/ordered-list-nested-depth-2/expected/Contents/section0.xml b/packages/core-rs/tests/fixtures/hwpx/approved/ordered-list-nested-depth-2/expected/Contents/section0.xml index 150be35..6c26547 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/ordered-list-nested-depth-2/expected/Contents/section0.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/ordered-list-nested-depth-2/expected/Contents/section0.xml @@ -1 +1 @@ -ParentChild oneChild twoSibling \ No newline at end of file +ParentChild oneChild twoSibling \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/ordered-list-nested-depth-2/fixture.toml b/packages/core-rs/tests/fixtures/hwpx/approved/ordered-list-nested-depth-2/fixture.toml index 43b5910..a5245c4 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/ordered-list-nested-depth-2/fixture.toml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/ordered-list-nested-depth-2/fixture.toml @@ -2,3 +2,5 @@ title = "HWPX Nested List Candidate" author = "Fixture Author" strict_mode = false determinism = false +manual_verified = true +verified_at = "2026-03-11" diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/ordered-list-nested-depth-2/golden.hwpx b/packages/core-rs/tests/fixtures/hwpx/approved/ordered-list-nested-depth-2/golden.hwpx index 0994c50c9b7ca9b3efddee5e10d1c94426116da4..7d409ef4e6facc02b7d2756bdfd7a9aaf65af6e3 100644 GIT binary patch delta 4191 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 diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/ordered-list-nested-depth-2/hash.txt b/packages/core-rs/tests/fixtures/hwpx/approved/ordered-list-nested-depth-2/hash.txt index 13b9803..5970a45 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/ordered-list-nested-depth-2/hash.txt +++ b/packages/core-rs/tests/fixtures/hwpx/approved/ordered-list-nested-depth-2/hash.txt @@ -1 +1 @@ -9ad706f92b7c190a34d21cfb264b84658b23f33cb908eeba9f7817f50447d4ca \ No newline at end of file +3c1d7cb45d8758dcb0b636b1713e8edb58c5fd710c92d937de2b5371375910d0 \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/style-brand-color/expected/Contents/content.hpf b/packages/core-rs/tests/fixtures/hwpx/approved/style-brand-color/expected/Contents/content.hpf index f4e1b46..ee895eb 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/style-brand-color/expected/Contents/content.hpf +++ b/packages/core-rs/tests/fixtures/hwpx/approved/style-brand-color/expected/Contents/content.hpf @@ -1 +1 @@ -Brand Color Candidateko(주)한글과컴퓨터docxly2004-11-09T15:23:46Z2026-03-08T07:30:22Z2017년 8월 3일 목요일 오후 4:55:39 \ No newline at end of file +Brand Color Candidateko(주)한글과컴퓨터limchaesung2004-11-09T15:23:46Z2026-03-12T07:38:04Z2017년 8월 3일 목요일 오후 4:55:39 \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/style-brand-color/expected/Contents/header.xml b/packages/core-rs/tests/fixtures/hwpx/approved/style-brand-color/expected/Contents/header.xml index 46e60cb..642631e 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/style-brand-color/expected/Contents/header.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/style-brand-color/expected/Contents/header.xml @@ -1 +1 @@ -^1.^2.^3)^4)(^5)(^6)^7 \ No newline at end of file +^1.^2.^3)^4)(^5)(^6)^7 \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/style-brand-color/expected/Contents/section0.xml b/packages/core-rs/tests/fixtures/hwpx/approved/style-brand-color/expected/Contents/section0.xml index 06a9869..29c8cb5 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/style-brand-color/expected/Contents/section0.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/style-brand-color/expected/Contents/section0.xml @@ -1 +1 @@ -브랜드 제목강조 텍스트코드 그리고 링크 (https://example.com) \ No newline at end of file +브랜드 제목강조 텍스트코드 그리고 0https\://example.com/;1;0;0;https://example.com/HWPHYPERLINK_TYPE_URLHWPHYPERLINK_TARGET_BOOKMARKHWPHYPERLINK_JUMP_CURRENTTAB링크 \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/style-brand-color/expected/Preview/PrvImage.png.sha256 b/packages/core-rs/tests/fixtures/hwpx/approved/style-brand-color/expected/Preview/PrvImage.png.sha256 index 21d6e53..6e95fe0 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/style-brand-color/expected/Preview/PrvImage.png.sha256 +++ b/packages/core-rs/tests/fixtures/hwpx/approved/style-brand-color/expected/Preview/PrvImage.png.sha256 @@ -1 +1 @@ -a3cc714d6645caa3b0735b03d85d295b46cc486218b1d676fb74adcf4019df24 \ No newline at end of file +4c3b2c3fea1d4f7c72a6326588822feaa76edbb0e7fa27c223569d690181603a \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/style-brand-color/expected/Preview/PrvText.txt b/packages/core-rs/tests/fixtures/hwpx/approved/style-brand-color/expected/Preview/PrvText.txt index a971f9f..0b6339b 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/style-brand-color/expected/Preview/PrvText.txt +++ b/packages/core-rs/tests/fixtures/hwpx/approved/style-brand-color/expected/Preview/PrvText.txt @@ -1,2 +1,2 @@ 브랜드 제목 -강조 텍스트 와 코드 그리고 링크 (https://example.com) \ No newline at end of file +강조 텍스트 와 코드 그리고 링크 \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/style-brand-color/expected/settings.xml b/packages/core-rs/tests/fixtures/hwpx/approved/style-brand-color/expected/settings.xml index 5d51824..978b884 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/style-brand-color/expected/settings.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/style-brand-color/expected/settings.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/style-brand-color/expected/version.xml b/packages/core-rs/tests/fixtures/hwpx/approved/style-brand-color/expected/version.xml index 549bb34..4ee1300 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/style-brand-color/expected/version.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/style-brand-color/expected/version.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/style-brand-color/fixture.toml b/packages/core-rs/tests/fixtures/hwpx/approved/style-brand-color/fixture.toml index 7c8e69c..c62217f 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/style-brand-color/fixture.toml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/style-brand-color/fixture.toml @@ -2,6 +2,9 @@ title = "Brand Color Candidate" author = "docxly" strict_mode = true determinism = true +comparison_excludes = ["Contents/content.hpf", "Preview/PrvImage.png", "version.xml"] +manual_verified = true +verified_at = "2026-03-11" [style] body_font = "함초롬바탕" diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/style-brand-color/golden.hwpx b/packages/core-rs/tests/fixtures/hwpx/approved/style-brand-color/golden.hwpx index e747034e51f27bbe1085df2fb908f065ae4978ac..d442a751e90f5a6e6d319b6c98fd32a797aaa466 100644 GIT binary patch literal 31751 zcmeIbXIN89*DwqSf=CsmC{65C>77ud2vVfiAVQ?~8cG6!fOM52U7CPY>Ai{|p(!PF zqy`8OdLW^E;du1i_j#W8yRP@&`-6R5!!DV%_RQLAR-0j#hROxPt9ZYTn|LobWfW;} zH)n+eSM06rL2h6tkeQQ{owd1{o3*0@kCmqrhqtwp#(x36!P9*$hL4ALRxaZJJwPt5 zxXRpK_I45g+#lAzA4szCazA8a1v!{IT39<+O0wx_D{zUhvAVjMIarw4IXZwO*}x!I zHdcU?gq4SwlAMC>FHEdBBo3}(RvwaUR&H)iVmv&ao}S!RW;jIl+~$tlwk~H4v$x~n zd&tYn^Y`1>Sl!GlL2fetv|f@;PhInohNillyq1=_CL620nT?~1B%2`a%G%-Viudfw z+{ICn4cE@+?$&k|YVP*WLBD=+bj4M`J;ycX?{=Mah#v>@9~6>gQ~Ik}tm-dbSet`b zad(#Pc7Ij*2k0R;|3hwG9IOBzAFHa2oRGj1`A23hp4JYge1hD3I3n>#{UR~pUti)T z-s8jQ2R;;dc-%R^zK@)vgB!@f&6URrWM%Sdrs??n) ztl~t8eIF0zGEatGyefEwL5ndsd&F7ofy<=x zz$rJTc&ucsYE;{2xhEbNfBx{nZ+DhBUZ?8)_o0|6^6vQ0TBU6R9CYCc2?-qW0c>I% zcTH;|XcjlgW~{KmmV4VP1vCqjoD@rBce`@wF1!f-DX*C> z`jq<}mh;+pl+0@q zds^^Y3A45wxchoc*!#Sokc4T`A9^uT+%LgX3U?g=wK3oa%XdAEgcKCup0G)hwGV68 znP_9>%6lZ2glfm`yoeplhdtNM`8t{u#oN&n7Je5=#2bIT(7s-ADoP>sk!d9@A_FFO z`10*L%YFp_tIVj=7wEn`ob`21LgxJ*%cARq@)FfGH>$|q5hXE|Mdi7u=rWvUie8M| z3#nJ6?6cJ_xe;bz+h~zAHTmEJx%PnC?4D=8=`msg$pUFQX-AnJFWPFjEY7+%LNo@} zK8#zJzIgYV>@6ev(^%=ERgT$?a`=p|F!)kVO^_)A6H3_B4J^V|Vg((rC!EE9I$*jd zGFKVAe*I+l=BP&vwEH12PqHI4xA7jkaQkX#M&%S{_RX9n8<}IbI>~ZpeONmGJfgGy zUcKUQ`KRYAJ^J3pP<=_AqZ9@}zm#8vXW>%zbP(nvy*K3J0)I~Wlg>V`Rl3#Kq4Am- zi<5wLkq3M1Ka#J1bNswPV<=^gm3>3Xne~mQBkPj;n?u25T0O;1MgRHW{)li=*A#w* z@b@v_SoCtFXhjsB$+IUsG2Ievjv=MnEg?l|21xD|n7ya79DL!T&oN2E)urujEi6~) zpsVLrG>E@GIh(iAsIx_9o9;1DXFVO`BV1V7+8*QS@V?gihW*BMwV{#n`SDqZ%#8{C zE1^JP^y8pq_vM05@J{8P=X-Uacq95PABb@20OG~r508#;eKYNa7$-B%2UhU`AnV=5 zM*A(j0kY0puCM92JM7HniELCa4G~i9I*@1w)eVwp>$WW1RtSDa!!mLiP%-#|pz-!S z#zP-Ff>Qcc@sVx91H-N+GV{ZzZyB{*;$(IBS=SB{@6n!i1yiO!oVg~XkVIZ07EN}_ zmU#L$k>Ai$F(`%!AL$gqUOn|{{emcyzSOgW78)rCw?f>0?JNWLWeZh_iUnh(IDwj; zSF2h(w_fNc-+Ps5^7!+dD7(G{I*hjD>Fk^+H?X0Lbor^FG(iPpWUA<5k0^H4;{l>b zx1}xdiW=X2kaExa2U{=w?$ego-!W0HbKv{1lXJOlbm|RvsCSUb?cFQ&o3RA%nH#cO z?gYf^!*xgRNc-Ji?Ymb0;r$)0`4<4MxKBn%pBpY?xgQmJ8f|PCKSeGC!o8Inf=W4h zIj-Ew0jn0SErrvS=I4%gq_@{-H-Vi1@Vo{Z@;#1k6n_K z*z57}dc9Wf6^5qRyzfb&+}$7y(ORVK@rn6Td^w$%c$)qVPs_A(BjiXal%mLz4esyt znC8+*gn+7KC4OefK2N(ITBvG2j;i3ea?2=b@Di{6Bs1d|qqwXXuLII1OWiQxHyvMr zRszBH9Ezb2Ad$jxkq%u*g{+IYw?4nR(U{&{wB)4iup0&==ojSD(16~*fj3vGBZ`J0{H2F@3 z7W!^tQ+Q~_eRS0|H~&8Ag4b&+}Z ztl)+7%n2#zy2gua(m;~V0ihK?M|7c~co0whRsJxyJ7{5sd-qdE)<2Tu%Wb`pjn&`y zhCPmW<+Nt2qIYmz^iVQb>!;f6JDa_Q^3KlOQJpKmZN*8Z6As_%!^O{(>P9QfUajxZ zqZCEA@>DZ*G1BC5s@ZukNp`2`N~&)hD{Upw)ZP(GDY~A3*WV##l51WIw_oJxDlxfb7fx7bd+{jp{v%Cag zLPy8cGY3OXb#Hv`v>8zk93CtjkzknLJdgjD0ex~MeQ2H6nw3?(^R{#A+|$QO%QUM+ zd_|S!!OHlLwa4=0F?TWHvcCNG>E@S@kI3eoh>GYcV`@fi zex$uEY+bM|OACV(yG9VZg&eu76PhT+{45Ud0&kIdwXnWa;khkvooq{X?u&Y>;T8Iw z_-DxZopQ`oTYeLh_9>B3ap0CzV9eo<^Ls)AH;Q&ek(z@|RxBgk}O1{aR z-OkkS^-=vu*?SjO*@uz#`M}W;{Ulyx_Vp)~JSpVm0zC$dcv&%Ru>@72c(g3Ow?=|& z*58Y|+kUm}1YOVKCoXj-dd8W8CB0J55Ki&zb9)FuX{-4KOR{IM3gskG|C=tV*3Vpt zAFdg2i}@GHFF6xEtEPAc%eLrYZ0WkGKGyOa@CiJ&Ih$F!j8|2R$BF*A#9;fZbe5-= zK2_BJO@NbiO}U7|LlHJEv6uGu36tqwat-oX?~_zLx!cwu^RR}oNsusk9&97~Ie3MYHZEnH_&?W7>?$oadDHNgL zPT&u}U~<*zLn+7NhtmyNxTIfXw%M~ho|+pc;72=g-DSeNhGL*A+4=IYWwT zBE8Jcni&7PB69njG26q45K1K<_*`||wmTx0$L`AH23~6IB80xNnug^QWk`)DO(P~{ z^O|@5{Jrg{^npctuANJBKnXmKMfUh=s`=uFg`WOV(}<9ghfmD(nS*=XW4M`vizxLX zW0~)6^aX{nOYS(%3ZyxFvRO+}DJslQW1v3ggD1lW&%~j+#9KUl zB)VPVlsVO&F^)@R39Jn+`m5ftnr?uw17_2mD@>>cpSp0{$4*CkYO|QXEHN|;R&x2e z>xAOm1of2e+*JW^1$b4!6}I_qu-EL<9l%R|_NIg{yp#L0QYJi26!S&xy_f4Sx22+! ziy6Z6Z>lST9hH-kpv?^@hoG=n$?1mGTR~BsH{?i6w)uE(F4PK0ck%EvLH7`r*GZLY zTqemoinI^n&Bz$?I|t+n-@j+xmhH+SCL$jQbY$?3fig-e@rg=Sl)P7@B)$HDc!qD6 z^;E<=#=^zhDw;K#`Ko|4WfN$b8om}mYjHaWCjMWb3f8<(e{O;`7F*7!=w7u$lP27Yw(4&Iw!(+ej?ta>&KCX-$}aefM$Y5`{5UivI=8S7AZYtQ2t~1D8)VJh6by>4^E; z6Gay6?0Nfhi9Oiq24NI-skiC0wf+7OvqsuQPKVWzfP7m4k1>hR#*b1A>64#U4B3?= z?%fQx)-oN`hD(X~&Zysb;4WAiH5ja(RLAq^$HRzW8~2|y(xDW_9!}{uPS{_|whM8= zSRZFPa=w+Ny)^q}{`OY2N4!Bx+4BawMB37FG7@EJb{D9_O^yWTq~wEFPkfb^jNUy@ z-U-xx*wojXDVxUjmbBaP&X8(UVFjYS3c2mnx@?|i{qS^W_@%sXi$eQ zV2?|`IpD)Lmnusizj}@BPXi{jJOr#?-*%EK{9v{Jay!#nu7*5*z^iZa&fTof^>qQ+ z1Z}q?A6;H*;Vswx6p!?1EVk9pk(;(p*Ak(;kyu5s;t{Io(et2oW+)(m`*On51}4Q= zpR0KMZcPz_J+e`HTEL9V5M%EVK^c}Xj~1Os;l06DFgi;H>N|7+e#zNdrOy9(Md8~E zuy1y^=`|`}B$Wo(c_jp?PK~9u1`&*RIQ>9Hxkqxz(|>VWcxOQqkz-38Il6I`pv_}L zlbawRh_2Q6AXl1{q2SZl%`Y=y_+pp8&$k-OEl+G_xDenY0pd?YQrY%W zF74j_ncu>sl_&AA3!z4Tp>4fC_3@SG(k_s%H3BzMDY!o>N_L&Hk+h;bQnE70?I0}0 zGyk?35I9!pCtu) zu4-$NB*?517;7~zSBW?J)K#^W+paEpj|C&!asv=@7~3eFg5{*@t5mhyW5|?<+(4<( zuE5tM=$G_l+7wLnWQ_V2V_LUBT%Pnlh%)0<^PZ&D##}JSTvR6V?SD^2=5itnE>w{;Rq z9;;jzDZLm==SSPWOupRKTh5XxndqZ=!K`0JEB|(!%((GFwk$AzQ0_f->6PN=_6i+g zKj_gEYZbP4NaBK5hj{lt6~AHdVV&w`w0~$J&-!wEhM>w5`>xd@+m_elTU}XM||jB)?#5M7e^>P zwJ;V|llgkP`s#|uJ-j)R;BXuheNmUMYT0oGz;{MYb$hyg5IsL50_L zw&P`YX3hXhB=cf=^(F1ltM_)Z2DUCI(XA}$s78o1`0fucQbzMMg9}Y0!M=wRC0C^H zr$h=)dp_7BL0E9jmR))r5ES4do8NA3i4rGgmJx{83Y>A8TEb}Z^S{-66mZRhVMbOt zM#kLMYnb;$Y0j{WsV(EBYhhVr1?lS$@AaYHE5_;uqu7a9y;!!uwI6FXwT4xxNkj)e z$2Whj?}t2$mE9aaHZw43o7+4bL={7PKTXAcPw2}k7v>bFb)oSqy- z-|MKdG_Zk+%F*JJBWwb<>AA`ch3q>ozR`yNSdko<23A9fi-FIGJZRmRrL~yY$q;jdU)a$9%Ym(3)0nIOw|o$w|x*wv{yPCq%%e-*LUT3 zAQn-HEjLalUAy9uP_2JerXZJP4kmR>KfS+u^Yz=9wstAL>GEm*S@oCeqfG?1B&l*2 z-Ns>cY*3*_S%a1J&D(o``iWN`HcjNhd)fgI%=M`-=~}RyAzzy zqWfB9@2$NKDe31$?Nr13xd!W-#g^m;fchrJa>A0FQ^h{AJyc73GyZ?;P$y2gdPSV> zl*N^^c)W%S$io`s$)n-op$+nK<973M!tQvlYl;4>YmKpIbyllukRJyN&ox3Lj-lKUvclF z#f9?8aA#MLn;R}v?|P=P-J^Te+8C@gSB@c#qsbWbtNzaZA@{-T*b2Hjw?0*u)1Rg%sC6a1#c;jz=1WOm zMwCk8J7ViiTPo`q8*rD<%fYAF`sVSV6}F0=N${#v!E2Ro@z`m#yD-qMD(K%7!cF0{ooD1rvBq)N3}jl&#w7iO-@1*DpqNKJbVH|5{Wwj8S2(N z;?%z%P_YhBQ{^>M|5K%ciYm|O=UCid_0ItPrPn3@2FKlgc;zoWMbKaT4^l!K)EVl( z-@!#lxJv8gapw;NtA-EdpZ8N)-DK6MZYk&Z2O?nH=U3a#DoJ0)52zzbGr9B!f@Em= z!XM;>h9o3IT$^jke;}kk>@)vIUu2~}I0RVg{nH$RO7x$O;04eU5V&8KsK4?D0@roo z-z_I3Iq&-US3hU?KO^^ite=ayzXa{Mg#W)%Zpgvgn+Gqq2LzirqE%Bhm0f*anO>Yg zn#{^HO8=#qN}pdQGyQCurU%!3c2bxK`brMH&*6w(<8%9xSGAZ{b&SI9tt?K5t8tkN zjaFRx9-SbrE>*WPDW@3u;KurCM%ipKrQbLZ_EzG1MDnTrhQzEZXxBU04Tv%U9Iie$ zMePEB`Y+TQ&0BtyqZ$jWePj&GqZ?t$$@YMs9>3LPe6zoF2EFj|F>JUZwD=4i?jHs>fV=_=(TsoqhK> z&qQ_M-kk8Rcf)j|_eeEtas};CQX*(MdK1$9vbMY)ZSeL+bbqqe#wji$0T+Y7QdJoEEFOCpZQEZ*&TKUVkfqcbe=Dr^wu8} zLoz2fXB_WSo@{0Ed9?Rp$%mrVYV#$W#%fLKV~Jc}B#5HbxhR2$$TF;TbzBhutXVtf zpDGqzQqj=|YNPOpz5Xa9l5wz69UfVSbDHxUps@KJglEY3^&wA@rOXo{qqRDsa!bK9 zmiw2NdXbe=?QoNE`X^ik8{2JVV;QDgdd*01DwQMzwnX`Iy@UsC>Y~P+J;*-^99wHvgKw(9d~$|7SDtb?1Ic-|GxaB(^Gg-`W? zf4~`!wwH!t-=w-{!5!7oy`D4hxhXO5+5L2?olU0Tsy;%AC*Nw6OMX_rdEXh{BngcM z`n0EcCLHV!yiuj>eX}s8<+#4KGfI|17Eu6zRAI(b$+nfO>KP5%9`9_pYp>}7 zq=YzOD>Y8Yk8L!X!Ds}$*vvJ*s#?1w@TPbAbWv@}Tl=I;*lzgHf8i@iq^$7pC7ctB z(0cRBCZrd@5$XfOa2Q%PWLJdlFgUu=Iv@Bm3)+}7x%=%xk{>f_x+G0ba$vg!gdq1) z?v%zpDr!cwX^ENDLAqSyQh}R8X&*&jO1e~5iX*CLFgbEbuBARKv(W6rn5fJV7{WPb}9L(dFVuo_@?}E<9NgF=e&mf+|ttm=_wWf{Tu{e=V+X zie6e}Hi6ljiZ^h+KemETEn=`~mm!nj6JAwHL2c7Acpd1Z@E*`;2fyQvNfqYc5@;NJ z&%KW+eGi@ti%34v4~{mfE!^`vfKphQHS{eQ_bpx&$Nq+!-M(|q!O(YY%zEh42 zJBj@K!I;5Rz2cE7E$J~w3knowp0pgq;CB>*5>Td39mMx<s zZ~n4K;tOyB%lMEO`;KyjF{3B|6`<0;64aW;)Z1{3N%O=`Zjeq!3*hfXp^nuPXN;da zY(x;l8bUUB+)etWrz{cjx)G@rItcb#(UUGZhx0}r1`FSzld0Q9x4J1hPd**8nz6uK{t}03DfRMMK`oWRRM!}Kbxn*xa<*COu9qbmgsV-en zIq~=l3*{bW@Ur?VgPO6UrB$}US@$U;N-Tj`NUmBQfcl zuaj)Vm;z z=ib;B>y6kpg?1s&>&k$zT40^yzNDv+>Boa;EqU50hviV}9AL&mJ}30# z$VZvHK`=sH;^!tGO7PU6_@p1<$xZGxPmPVY2e^(uheg>I!zGa$6PT$NcCG@@H3g$x zu$_K6WL7~g#w_PVA`WzLpd)4~qJ=JkgCTC+ zks!C7-cS?8VsTT4ve;Khy52{ePyp_8H_fCBi>iW>)zg0%w>f$-eD1yuO3s347DM{O zyLVwc%PAKcYY@eHCiwvOL^$K?^qrO&;>n|}TG#HvdI#I42QDrM%5ZN{oRj#s8YLwe z;#e|HtJ2%stV|Hfr7AR1C=q|KD{A{}!x#qk-|;PyQvt~YS)mQvW z80%8u2Y3@Pd?|x4>>bS~LTjtA_m^)$PqYyCSA*8m>mQB^sf!#_BI1xzN8^2mwe`mf zJgKh!>1*zj;O+0KQ^+*Ca$q#id;S6mr*HkRQs;Sb)BUm)8AV}ya4K-3@URUYS4%l< zbh16%p2YD&!uhaaDQ~S9P?tY+YiuucbG3h}Ai~(ax9lqo+^?xvlqq|EQ#--1Jnb%@ z!*-jkK2LGqMMp7`v8e?=qkM< zE?f;6{MimS)?`LGKd#@nXV4R=lT={q7Qa;Ha)BwXD51YV1ulGNWMZ!(tQb^8x30CS z4%Y;HKs)V3zEZ&&*6o+*_2uc&8vs>~w^K2LGn#;(R)0%|iy{8fk|IkLLnSWlegKWt z{<(PX3UBzk5?d*f8XJTSQ}{#`@-s%J4MS7rW`c3>A7otxckhKdH11B7^3IFF8!3Fa zZj!VA9acEv$MMx^gqBHITBg*O>Z?2%K7-ytb`=9Wt?k%M-txH5wgtIX=P{!D)>H!J)AF+=_1 zJG6PG&Br}7#SYv#o;r!A%MtkK0u~3x%8`o~q?#mH@>nz&fB5>$g~sV|6bS0=(({__ zm3?LQ;n`EUE@tzDp?YfsXKVBuWx7rzU@oow?Y4392i)vE%vJGB3_!Kj`}&gTa<_RT zOd1gGaOyU7I{i8~*2ppW%C_5}V={1~ex(Tr;Lq({gA~J>d<|+*U$J#4PX~b+SlzSf zm-tX`$2I1^Bo93!K|66LtOv{9e3(iDyQXI{Nd9oK1jg*v6 z2taoz^{u+jwHNYrv=F|to>0+7Xc(u($&T1TTB(Nm^r*tdFU@EV4Ln>~bO zE77G7Oc#%C+9_JxIYsYnPwc4k+$d~zNVi9Z0Ze@44x3Au^rnyZp*-TKg9u3VA;!5g zL91URccyc-yPp~CHD(=hf?ls3ycZ=_#|Yid2j#?lb+@CCTuxmq!Zv5PVv}biKQkVs zUBHU$O*SLe0Y|fxi!pK(E481n`#6es=jWyw-c?H(ZQOXP!u0ZrlW_kCfSv$t3241&ADc&q0 zve>%ePIePA)?KAxz-~$nL zTg?FFxMQyGPxjgWQWBY$a7tqQ5iM9!k@4gNXuI#bC)T@Ky*ZRQg*u4q1%mRi%ZGz zGeB>!=+AZH1pDggObPR3F1o3s$0$_QghNj7!b!Y|=Tv0`3!tfAF4?}yKy>R14Yts9 z{F=|kR-sFubtihv9ur6zmv910u5Wjn`YGNC$>?#6it^+##jI9$xA!(Pb2@-$chZfx z#Soo-LbXs#;3CMU8T`7*8zBX9!m58P8Lr=0~3A|1oWp|7zQaLtoGkEAJBkHp7 z2J~fg8cNSymx}#s=suIbN|mSgC7*0F$%YkDdP%2$+CBkj)Ue|3nUK*Q9FJ~BuTPoP zGq+Bf)!Wxq7bxC|K3qa4KRoW<{s>1+HJ*HP=y|`Tl&{!<+XmZ^Hi<8yuECQ&~ZN6935c|iu(Ugw-M?jV0Jys7|gMojaWEyiK(00Uu{0WM~Vyn8IO#~U0Q+&7w? z<^zOG*D%GbQZAES9S#VYU!25tW^T-BW5BAEQoBx~NAu}iEQdd_-8IhUKK7scZPDym z3Q_Fugs1c181LNRah8*X9+w5nCT*cqiF{Ygqx4AH^5@E~Vy%S+(e72FUULemtX0b^ zcaGOj(AH?75;Xr}aI%B7?D)a0s&;Cw|VA8!5^J)8wk4iu%2b`i~n3IhGdb$X}GWqa|-=*fziiM`n95av~r z^0X2umxZoA^=5IR;s(^o3TSUw54lH{o9671kBTf_REJM#RM|ug@O<0uy2TH(*E%t* zn(!#-TS5!(=>czlz}X6sxl{Th6QIVj?ym)ILPp@gI9PE1xRZ0e8*{QVJVNu7C>y~y zzrg?+>dxwpG3yoOqWz0@54#94>Oz5quc7-1M(QCVn*e}@$2cTejo!FoM(-5H(LKs* z?p@ThH66V-ag7 zJ==rnrWzjw6^+s0-h%G=Ug&DsbRALv>J@R{oc8l-3D5*xRoz(BPt4%hu=Ukt-1<2E z!co_)=-8Rl6ZCQXgmWhoRx&kbCX!fRbUh8*on*U9F=UF}r|e!zcF-I#v0U#1?vWgC z^iMkblQ!zjxFGEpV<`O^jxdv2;*^u_=pjDqxr>d$-fw-x8$cnDXl3|-gyoA<#qMaI zKIfb?o+;OS#XQ9lu{s2M^Hwro(Z3svaydhJf%wY)pzo8?T0k{z8W;&znAW!aPMlm=g2qEdgaMhz~_@ND1sQk ze(z*?x6hhTkh9sMUs8+(gLIsz+ZabWaGKhxae)7=x?tC<6N;)gd?+?o z3rc`Oqt60ej<)>z{fhfPuY63dq8Nf6Pnh&J%Rc$E=*@*~Ua9m`yRcf>dS$wHEvz7Y zw<1MeCDplwA$2DWNsZ_m7Ef}`VDYiaKp%-V%DKXuh}A`FkT1|ETlI!2MUT?zIx0W*SVs?k68C zm^4TLIV~tpf;O2RwAMCHgZ03@$&{SJrtZ!N5>Lq_qtn`EAi4Pm&}5DCVtYtaJ$81s z*EaE?nJ+^7b$XgBWX$5nmp1bDh?jkZOxB%c0tL?T64xmXa>UP@9J+uv-Jf zzig!T;4Q2!y#x^%m%QVh&Zz-kM~aRsGHuOnhwngjny`CWKJixW^+2%_klX|dDt64OK%?QrK9^r(QSQqwpK&X!2LNoH|CODH_ke@S1a;x z&nZ*m_r()K2nb?gA7yuv%5qeif`yp^J|o9cXZ8;JC9sp4?LNmbM*TU>52tpv(#-0L zp;)8+^`qU%7Y5VC&{)ob9N?@uB9#s*g)Z|hmPZ;N<|4b1rj78-g5;e+fZkVODBiG^ z(NeFG*ZLZkCY6)l_;y)L!$T~QnvztCJC(S|%LWit?LoIBj4dZrh7?J>mpME+Pxx1XSSWyvUc(7lh-@?>qY zPw4df(M^}0?}~zmD!>-8+zz-2!*l7D0vRf0TvHtZUT<<*tH$Tyx)tfU}*Qb~!>iVMAcC|vvox-YkYjOsI;d-fF zHRM`~?9Ws=Zj1N67F`UAT+(z*wv+gLW&s!H7P^_pEIbx=5PC1!viNg)vkv;8#B1!b zPh6g<2H*38(xtHbI1kf)+9sE7*W34lctKH>VLtrbMlGY;fQ%$+zs{sTHBG=;v&22T zesIRUs=mrJr*?XOLG@y>`vMeNYCG76!tl_jYzOofr;b=j4UF!cxT;>$1|Sm)E&m>vG*Ai$we$>3hlpz zUfbgfj@}2THcI~$T?@Eo*m7!HG%$bI8D22spbtnnI9dwVwXteMOk>4ZS_%-fp08Hw z_KNG*Mh_1=)6{2*8i8ALn$Wb3oHTVH^rB&%RIJFryd>PQ?AzU(6V6`EXic7Q5Ulr} zu&FXX*dTVb`s<5#!rXh720S+;Yy(|7;3fB~Pq+8^R*2q-DJJm%_P6`;S!mL$Hz^A% z(?09w1AZ9}oBcQ+mGW&qWUZapv+4a}5lXB=V$yf4dvu8r;qA6%!eZ!^w!>;|Ufa-< zAr~CaRXtiUG&2_em_~RFtr-OSkpY=HD8yREg-dxnP4rl=;CtQ^%C~h$BcAIYVX*#< zr?-aFG~Do)wF@R8vzCg`R`0JMV++6#-BsCkpZv}>OxdWKexfnv^&=RxtO>aT-nG>N z=>DXUlcL5Fs_xxl2CyB8O5p#RZ@{fE*elb}4*c!svKC1gfPBMh_tILp+y;CIlcq(Ar8xL}Jw*2w)|Q7_ygi6DPzri@Q_|J+~41r&Po z1plmxpu#P(P|W-E__vE5qagi3jo&8n&nwTHa7MdxDx6c{clha?7tVR%oEOe@#JQ%# z%@Lp53um_1ITg;SaBe}L`w{2f)R`POr@}cEeoMo1UO4B4b6z-)xShuxaUlcgb1Ixu z;jcvw=e%&H7SE}0PKEQisq-1S^Lfg@<-q?sTRZpVEf5cne(!8Afb*H#^ZDy@t$D6B z&$Z^c);!mm|9@KZTvMKF%5zP5t|`wo<+-Lj*Ocd)@_bG7`O0v7g7fw9XOpq#R5+)? zU(+w=yzuw#73U-3d_XksVSyp5xa|Q-5xG0=V|nb0 z{pzng+ZhZpl)u}&dXOF?G66EuYPw4aZZ)pY{F15(G@7`FH!p&jk1J6nGGpwddPF6k zR#sKfN zdfH6ZhG2>!e~7950h@Kq?z8M$&BNW9WrTuZS0Yheq83K)3!tMccAVTP5;T)tfqbP<#T5ovO2yu-}Jpf@!~*jq*(fBnchZ-1?s=a zNZiQi{u8&EwGS@&$Ju7ue-eM3kr9`zlH1DZMXHXf5j2{k+-A_Vj||TOk7@U3ufo+mN(QROimP zVke#TTuPvjaahxh3ST%rb$4jE^NgehqIFoNov)8)OkkM#`w{>rXq{F5L(W zywW!Ces5~jQ2tV)T5i$%j3?uyAluBx5<0kTFd$S^A7ksE=P8hN2;8v!IG03sK>l8` ztXF{C3!j(Duu!qmbTssuGWoST4v$_3$P4BeIPnVWt{TXz+*pt0Gnerp#p)zDy8(uS zc(V)b3@<^0S0b;bsSQ7v3kfxS?BCtz5ggvc6UnU1J14@FNUP1)PJWG( zNX}j%Rcc6>%I8y^;T4AF!&{vG^jnJoJDr0JThe@-4 zQ|XJ)RPOKw7}9xp{N9PwI-^E&y$Xlp1YqvczN4wB81YU)wVLJ0%6RSZiT%l}{fQ9t z6y)r_;OxSSKjf8d7gy!zSMnFG>{s|Z{FerIZ@!fo@BC`&U7PTP!GWV&-WR~8vJxy>n$b|fjLJyHB&*pTkTs`B{4qUWug zGAi5r8y!SLIxP*KspR(y3~!TkMaLiQ*T4J8WRbdbv1-(@R&J}QL68{wuWkHU-rpIz zgcB?ixblAxEVzyJ{hoevXnRO?d<_O?7s73T6I3aF3}5C*7x=K%5`<0GAT5!U614YB9(Yk`jNXyqs$@Y$cr|^FNN8Rolo^uxnDkf z%otYqRd!$-JW}@{N%Sv6m>5c8-YWq0}JU4hF3;!{6G2PMTU zQ4tDarsKRJm#itgDb+1vVW;`RL{St9wGTSEQZ#;ELM6Vb zn1%fSY$~;zTDo8Y0XL0TZ}iUH_-}FI$nn$I87D@9apf$J&>yL6Z{}eA0_5s;mXts+ zQiZhrAsJB#TLsFI?orB`_se~}uexD3A?$Co#wYIOxMANX!lO}QgDP}9!7T~%iI3lK z(n%1^IKt&R7r59u$36rAR-eod7^P(^#6E8PKJ5>aA_CHd%JXoC>X;Z_@nJ8^sbo(& z8fbnn(>_>=`@r}FbeGS>+u89yvnKy~&Wh(BaCY@Ohw{In{+%-UA1G1W<=+{M&-(Wp z@c)zu_#eO&9FP5xxK@AC0{EA8 zjo{x|CjLP{NdMnCCw@czs_{SbHT(m)i-Y{%vN!z3@SA}D*7g5jz_|PekNubC|2K|* zE4qJh$hiFn4gW*!{Tsu-1;alWv^@XUR{Srl@bB^Q4~`hG|G@E|qeVl7=qzakE=%`i Nyhfa+766~!{eNUq0HOc@ literal 27922 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 Layout Candidateko(주)한글과컴퓨터docxly2004-11-09T15:23:46Z2026-03-08T07:30:22Z2017년 8월 3일 목요일 오후 4:55:39 \ No newline at end of file +Centered Layout Candidateko(주)한글과컴퓨터limchaesung2004-11-09T15:23:46Z2026-03-12T07:39:01Z2017년 8월 3일 목요일 오후 4:55:39 \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/style-centered-layout/expected/Contents/header.xml b/packages/core-rs/tests/fixtures/hwpx/approved/style-centered-layout/expected/Contents/header.xml index 4f7e66c..3991567 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/style-centered-layout/expected/Contents/header.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/style-centered-layout/expected/Contents/header.xml @@ -1 +1 @@ -^1.^2.^3)^4)(^5)(^6)^7 \ No newline at end of file +^1.^2.^3)^4)(^5)(^6)^7 \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/style-centered-layout/expected/Contents/section0.xml b/packages/core-rs/tests/fixtures/hwpx/approved/style-centered-layout/expected/Contents/section0.xml index 40cc1d7..830d9ae 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/style-centered-layout/expected/Contents/section0.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/style-centered-layout/expected/Contents/section0.xml @@ -1 +1 @@ -중앙 정렬중앙 정렬 본문입니다.두 번째 문단입니다. \ No newline at end of file +중앙 정렬중앙 정렬 본문입니다.두 번째 문단입니다. \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/style-centered-layout/expected/Preview/PrvImage.png.sha256 b/packages/core-rs/tests/fixtures/hwpx/approved/style-centered-layout/expected/Preview/PrvImage.png.sha256 index 21d6e53..5838135 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/style-centered-layout/expected/Preview/PrvImage.png.sha256 +++ b/packages/core-rs/tests/fixtures/hwpx/approved/style-centered-layout/expected/Preview/PrvImage.png.sha256 @@ -1 +1 @@ -a3cc714d6645caa3b0735b03d85d295b46cc486218b1d676fb74adcf4019df24 \ No newline at end of file +8fed5270608c6a573ec78ec6f1449bba68f41d50554b5057442ee513de6a91be \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/style-centered-layout/expected/settings.xml b/packages/core-rs/tests/fixtures/hwpx/approved/style-centered-layout/expected/settings.xml index 5d51824..1da4edd 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/style-centered-layout/expected/settings.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/style-centered-layout/expected/settings.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/style-centered-layout/expected/version.xml b/packages/core-rs/tests/fixtures/hwpx/approved/style-centered-layout/expected/version.xml index 549bb34..4ee1300 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/style-centered-layout/expected/version.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/style-centered-layout/expected/version.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/style-centered-layout/fixture.toml b/packages/core-rs/tests/fixtures/hwpx/approved/style-centered-layout/fixture.toml index bd4ded6..aa75215 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/style-centered-layout/fixture.toml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/style-centered-layout/fixture.toml @@ -2,6 +2,9 @@ title = "Centered Layout Candidate" author = "docxly" strict_mode = true determinism = true +comparison_excludes = ["Contents/content.hpf", "Preview/PrvImage.png", "version.xml"] +manual_verified = true +verified_at = "2026-03-11" [style] body_font = "함초롬바탕" diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/style-centered-layout/golden.hwpx b/packages/core-rs/tests/fixtures/hwpx/approved/style-centered-layout/golden.hwpx index 43c6a94aaa56438646b41aa80182a96492744e59..fe582abcd61c428f819f9dffdecaa457c45098bb 100644 GIT binary patch literal 32545 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 Candidateko(주)한글과컴퓨터docxly2004-11-09T15:23:46Z2026-03-08T07:30:22Z2017년 8월 3일 목요일 오후 4:55:39 \ No newline at end of file +Typography Candidateko(주)한글과컴퓨터limchaesung2004-11-09T15:23:46Z2026-03-12T07:36:49Z2017년 8월 3일 목요일 오후 4:55:39 \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/style-typography/expected/Contents/header.xml b/packages/core-rs/tests/fixtures/hwpx/approved/style-typography/expected/Contents/header.xml index 672418b..bd500ff 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/style-typography/expected/Contents/header.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/style-typography/expected/Contents/header.xml @@ -1 +1 @@ -^1.^2.^3)^4)(^5)(^6)^7 \ No newline at end of file +^1.^2.^3)^4)(^5)(^6)^7 \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/style-typography/expected/Contents/section0.xml b/packages/core-rs/tests/fixtures/hwpx/approved/style-typography/expected/Contents/section0.xml index df5eb6f..c49f242 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/style-typography/expected/Contents/section0.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/style-typography/expected/Contents/section0.xml @@ -1 +1 @@ -제목본문 문단입니다.링크 (https://example.com) \ No newline at end of file +제목본문 문단입니다.0https\://example.com/;1;0;0;https://example.com/HWPHYPERLINK_TYPE_URLHWPHYPERLINK_TARGET_BOOKMARKHWPHYPERLINK_JUMP_CURRENTTAB링크 \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/style-typography/expected/Preview/PrvImage.png.sha256 b/packages/core-rs/tests/fixtures/hwpx/approved/style-typography/expected/Preview/PrvImage.png.sha256 index 21d6e53..21786d2 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/style-typography/expected/Preview/PrvImage.png.sha256 +++ b/packages/core-rs/tests/fixtures/hwpx/approved/style-typography/expected/Preview/PrvImage.png.sha256 @@ -1 +1 @@ -a3cc714d6645caa3b0735b03d85d295b46cc486218b1d676fb74adcf4019df24 \ No newline at end of file +735ab1697d7cc3643cd750e5eafeb29fad0d25533dbbc8149f9a8167aef06bf5 \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/style-typography/expected/Preview/PrvText.txt b/packages/core-rs/tests/fixtures/hwpx/approved/style-typography/expected/Preview/PrvText.txt index 07e0cf6..88426ff 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/style-typography/expected/Preview/PrvText.txt +++ b/packages/core-rs/tests/fixtures/hwpx/approved/style-typography/expected/Preview/PrvText.txt @@ -1,3 +1,3 @@ 제목 본문 문단입니다. -링크 (https://example.com) \ No newline at end of file +링크 \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/style-typography/expected/settings.xml b/packages/core-rs/tests/fixtures/hwpx/approved/style-typography/expected/settings.xml index 5d51824..395fe43 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/style-typography/expected/settings.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/style-typography/expected/settings.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/style-typography/expected/version.xml b/packages/core-rs/tests/fixtures/hwpx/approved/style-typography/expected/version.xml index 549bb34..4ee1300 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/style-typography/expected/version.xml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/style-typography/expected/version.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/style-typography/fixture.toml b/packages/core-rs/tests/fixtures/hwpx/approved/style-typography/fixture.toml index 3bd6183..0d2b5ef 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/style-typography/fixture.toml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/style-typography/fixture.toml @@ -2,6 +2,9 @@ title = "Typography Candidate" author = "docxly" strict_mode = true determinism = true +comparison_excludes = ["Contents/content.hpf", "Preview/PrvImage.png", "version.xml"] +manual_verified = true +verified_at = "2026-03-11" [style] body_font = "함초롬바탕" diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/style-typography/golden.hwpx b/packages/core-rs/tests/fixtures/hwpx/approved/style-typography/golden.hwpx index 70f7ec54a524cf191f868e349b4bf41d29f24db9..573864f04a52303e5a5a7192ca963b02913036c5 100644 GIT binary patch literal 28943 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 TNEmfnAs$*iN=}P9TzvaK&xJ0t literal 27838 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.^2.^3)^4)(^5)(^6)^7 \ No newline at end of file +^1.^2.^3)^4)(^5)(^6)^7 \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/table-alignment/fixture.toml b/packages/core-rs/tests/fixtures/hwpx/approved/table-alignment/fixture.toml index 0a2ff7c..2529002 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/table-alignment/fixture.toml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/table-alignment/fixture.toml @@ -2,3 +2,5 @@ title = "HWPX Table Candidate" author = "Fixture Author" strict_mode = false determinism = false +manual_verified = true +verified_at = "2026-03-11" diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/table-alignment/golden.hwpx b/packages/core-rs/tests/fixtures/hwpx/approved/table-alignment/golden.hwpx index f3a675bb9ce795c0d5ff3f2ae37f22758b24f5ca..66d105b2fe054b8246ab46605510bf89b9a22004 100644 GIT binary patch delta 1900 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.^2.^3)^4)(^5)(^6)^7 \ No newline at end of file +^1.^2.^3)^4)(^5)(^6)^7 \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/table-basic/fixture.toml b/packages/core-rs/tests/fixtures/hwpx/approved/table-basic/fixture.toml index 0a2ff7c..2529002 100644 --- a/packages/core-rs/tests/fixtures/hwpx/approved/table-basic/fixture.toml +++ b/packages/core-rs/tests/fixtures/hwpx/approved/table-basic/fixture.toml @@ -2,3 +2,5 @@ title = "HWPX Table Candidate" author = "Fixture Author" strict_mode = false determinism = false +manual_verified = true +verified_at = "2026-03-11" diff --git a/packages/core-rs/tests/fixtures/hwpx/approved/table-basic/golden.hwpx b/packages/core-rs/tests/fixtures/hwpx/approved/table-basic/golden.hwpx index f3a675bb9ce795c0d5ff3f2ae37f22758b24f5ca..66d105b2fe054b8246ab46605510bf89b9a22004 100644 GIT binary patch delta 1900 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 diff --git a/packages/core-rs/tests/fixtures/hwpx/provisional/image-data-uri-basic/input.md b/packages/core-rs/tests/fixtures/hwpx/provisional/image-data-uri-basic/input.md index 251d167..9dcac60 100644 --- a/packages/core-rs/tests/fixtures/hwpx/provisional/image-data-uri-basic/input.md +++ b/packages/core-rs/tests/fixtures/hwpx/provisional/image-data-uri-basic/input.md @@ -1 +1 @@ -![tiny](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Wn0gWQAAAAASUVORK5CYII=) \ No newline at end of file +![tiny](data:image/png;base64,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) \ No newline at end of file diff --git a/packages/core-rs/tests/fixtures/hwpx/provisional/mixed-rich/golden.hwpx b/packages/core-rs/tests/fixtures/hwpx/provisional/mixed-rich/golden.hwpx index 2f51e0e8e1e36218f3866b0bc59c0e06fceb22ae..b894309389084902746f3dfeff290fbc05568d44 100644 GIT binary patch literal 43459 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")); - assert!(section.contains("Beta")); - assert!(section.contains("vertpos=\"0\"")); - assert!(section.contains("vertpos=\"2300\"")); - assert!(section.contains("horzpos=\"1100\" horzsize=\"41420\" flags=\"2490368\"")); - assert!(!section.contains("- Alpha")); - assert!(!section.contains("- Beta")); - assert!(!section.contains('•')); - assert!(!section.contains("paraPrIDRef=\"14\"")); -} - -#[test] -fn nested_unordered_lists_use_second_list_level() { - let markdown = "- Parent\n - Child one\n - Child two\n- Sibling"; - let generated = generate(markdown, HwpxOptions::default()).unwrap(); - - let section = generated.section_xml().unwrap(); - assert!(section.contains("paraPrIDRef=\"19\"")); - assert!(section.contains("paraPrIDRef=\"20\"")); - assert!(section.contains("styleIDRef=\"0\"")); - assert!(section.contains("Parent")); - assert!(section.contains("Child one")); - assert!(section.contains("Child two")); - assert!(section.contains("Sibling")); - assert!(section.contains("vertpos=\"6900\"")); - assert!(section.contains("horzpos=\"1100\" horzsize=\"41420\" flags=\"2490368\"")); - assert!(section.contains("horzpos=\"2200\" horzsize=\"40320\" flags=\"2490368\"")); - assert!(!section.contains("- Parent")); - assert!(!section.contains("- Child one")); - assert!(!section.contains("- Child two")); - assert!(!section.contains("- Sibling")); - assert!(!section.contains('•')); - assert!(!section.contains('◦')); - assert!(!section.contains("paraPrIDRef=\"14\"")); - assert!(!section.contains("paraPrIDRef=\"15\"")); -} - -#[test] -fn header_reuses_existing_list_paragraph_contract() { - let generated = generate("- Alpha\n - Child", HwpxOptions::default()).unwrap(); - let header = generated.text_entry("Contents/header.xml").unwrap(); - - assert!(header.contains("")); - assert!(header.contains("")); - assert!(header.contains("")); - assert!(header.contains("Name")); - assert!(section.contains("Reviewer")); - assert!(!section.contains("Name | Role")); -} - -#[test] -fn table_preview_text_matches_golden_contract() { - let markdown = "| Name | Role |\n| --- | --- |\n| A | Writer |\n| B | Reviewer |"; - let generated = generate(markdown, HwpxOptions::default()).unwrap(); - let preview = generated.text_entry("Preview/PrvText.txt").unwrap(); - let settings = generated.text_entry("settings.xml").unwrap(); - - assert_eq!(preview, "\n\n"); - assert!(settings.contains("pos=\"16\"")); -} - -#[test] -fn tables_add_table_border_fill_to_header() { - let markdown = "| Name | Role |\n| --- | --- |\n| A | Writer |"; - let generated = generate(markdown, HwpxOptions::default()).unwrap(); - let header = generated.text_entry("Contents/header.xml").unwrap(); - - assert!(header.contains("")); - assert!(header.contains("")); - assert!(!header.contains("first")); - assert!(section.contains("second")); - assert!(!section.contains("1. first")); - assert!(!section.contains("2. second")); - assert_eq!(preview, "first\nsecond"); - assert!(settings.contains("paraIDRef=\"0\"")); - assert!(settings.contains("pos=\"5\"")); -} - -#[test] -fn nested_ordered_lists_use_shape_specific_contract_in_non_strict_mode() { - let markdown = "1. Parent\n 1. Child one\n 2. Child two\n2. Sibling"; - let generated = generate( - markdown, - HwpxOptions { - strict_mode: false, - ..HwpxOptions::default() - }, - ) - .unwrap(); - - let section = generated.section_xml().unwrap(); - let preview = generated.text_entry("Preview/PrvText.txt").unwrap(); - let settings = generated.text_entry("settings.xml").unwrap(); - - assert!(section.contains("paraPrIDRef=\"20\"")); - assert!(section.contains("paraPrIDRef=\"19\"")); - assert!(section.contains("Parent")); - assert!(section.contains("Child one")); - assert!(section.contains("Child two")); - assert!(section.contains("Sibling")); - assert!(section.contains("horzpos=\"1100\" horzsize=\"41420\" flags=\"2490368\"")); - assert!(section.contains("horzpos=\"2200\" horzsize=\"40320\" flags=\"2490368\"")); - assert!(!section.contains("1. Parent")); - assert!(!section.contains("1. Child one")); - assert!(!section.contains("2. Child two")); - assert!(!section.contains("2. Sibling")); - assert_eq!(preview, "Parent\nChild one\nChild two\nSibling"); - assert!(settings.contains("paraIDRef=\"3\"")); - assert!(settings.contains("pos=\"7\"")); -} - -#[test] -fn ordered_list_headers_use_numbering_contract() { - let basic = generate( - "1. Alpha\n2. Beta\n3. Gamma", - HwpxOptions { - strict_mode: false, - ..HwpxOptions::default() - }, - ) - .unwrap(); - let basic_header = basic.text_entry("Contents/header.xml").unwrap(); - assert!(basic_header.contains("")); - assert!(basic_header.contains("")); - assert!(basic_header.contains("")); - assert!(basic_header.contains("")); - - let nested = generate( - "1. Parent\n 1. Child\n2. Sibling", - HwpxOptions { - strict_mode: false, - ..HwpxOptions::default() - }, - ) - .unwrap(); - let nested_header = nested.text_entry("Contents/header.xml").unwrap(); - assert!(nested_header.contains("")); - assert!(nested_header.contains("")); - assert!(nested_header.contains("")); - assert!(nested_header.contains("")); - assert!(nested_header.contains("")); - assert!(nested_header.contains("")); -} - -#[test] -fn nested_lists_over_depth_two_fail_in_strict_mode() { - let markdown = "- A\n - B\n - C"; - let error = generate_hwpx(markdown, HwpxOptions::default()).unwrap_err(); - assert!(matches!(error, CoreRsError::UnsupportedFeature(_))); -} - -#[test] -fn nested_lists_over_depth_two_fall_back_in_non_strict_mode() { - let markdown = "- A\n - B\n - C"; - let generated = generate( - markdown, - HwpxOptions { - strict_mode: false, - ..HwpxOptions::default() - }, - ) - .unwrap(); - - let section = generated.section_xml().unwrap(); - assert!(section.contains("A")); - assert!(section.contains("B")); - assert!(section.contains("C")); - assert!(section.contains("paraPrIDRef=\"20\"")); - assert!(!section.contains('•')); - assert!(!section.contains('◦')); -} - -#[test] -fn links_are_rendered_as_visible_text() { - let markdown = "[문서 열기](https://example.com)"; - let generated = generate(markdown, HwpxOptions::default()).unwrap(); - let section = generated.section_xml().unwrap(); - assert!(section.contains("문서 열기 (https://example.com)")); -} - -#[test] -fn empty_link_label_renders_url_only() { - let markdown = "[](https://example.com)"; - let generated = generate(markdown, HwpxOptions::default()).unwrap(); - let section = generated.section_xml().unwrap(); - assert!(section.contains("https://example.com")); -} - -#[test] -fn content_hpf_contains_metadata() { - let generated = generate( - "hello", - HwpxOptions { - title: Some("Fixture Title".to_string()), - author: Some("Fixture Author".to_string()), - strict_mode: true, - ..HwpxOptions::default() - }, - ) - .unwrap(); - - let content_hpf = generated.content_hpf().unwrap(); - assert!(content_hpf.contains("Fixture Title")); - assert!( - content_hpf - .contains("(주)한글과컴퓨터") - ); - assert!(content_hpf.contains("href=\"Contents/header.xml\"")); - assert!(content_hpf.contains("href=\"Contents/section0.xml\"")); - assert!(content_hpf.contains("href=\"settings.xml\"")); - assert!(content_hpf.contains("ko")); - assert!(content_hpf.contains("")); - assert!(content_hpf.contains("")); -} - -#[test] -fn package_contract_paths_are_self_consistent() { - let generated = generate("hello", HwpxOptions::default()).unwrap(); - let content_hpf = generated.content_hpf().unwrap(); - let container = generated.text_entry("META-INF/container.xml").unwrap(); - let container_rdf = generated.text_entry("META-INF/container.rdf").unwrap(); - - assert!(container.contains("full-path=\"Contents/content.hpf\"")); - assert!(container.contains("full-path=\"Preview/PrvText.txt\"")); - assert!(container.contains("full-path=\"META-INF/container.rdf\"")); - assert!(content_hpf.contains("href=\"Contents/header.xml\"")); - assert!(content_hpf.contains("href=\"Contents/section0.xml\"")); - assert!(content_hpf.contains("href=\"settings.xml\"")); - assert!(container_rdf.contains("Contents/header.xml")); - assert!(container_rdf.contains("Contents/section0.xml")); -} - -#[test] -fn headings_render_as_plain_hwpx_paragraphs() { - let generated = generate("# Heading", HwpxOptions::default()).unwrap(); - let section = generated.section_xml().unwrap(); - assert!(section.contains("Heading")); -} - -#[test] -fn hard_breaks_flatten_to_visible_space_in_phase_a() { - let generated = generate("hello \nworld", HwpxOptions::default()).unwrap(); - let section = generated.section_xml().unwrap(); - assert!(section.contains("hello world")); -} - -#[test] -fn hwpx_xml_uses_hancom_2011_namespaces() { - let generated = generate("hello", HwpxOptions::default()).unwrap(); - let section = generated.section_xml().unwrap(); - let content_hpf = generated.content_hpf().unwrap(); - - assert!(section.contains("http://www.hancom.co.kr/hwpml/2011/paragraph")); - assert!(section.contains("Generated Document")); - assert!(section.contains("안녕하세요")); -} - -#[test] -fn preview_text_tracks_visible_paragraph_text() { - let generated = generate( - "# 제목\n\n문단", - HwpxOptions { - strict_mode: true, - ..HwpxOptions::default() - }, - ) - .unwrap(); - let preview = generated.text_entry("Preview/PrvText.txt").unwrap(); - assert!(preview.contains("제목")); - assert!(preview.contains("문단")); - assert_eq!(preview, "제목\n문단"); -} -#[test] -fn headings_use_outline_style_refs_and_heading_char_properties() { - let generated = generate("# Heading", HwpxOptions::default()).unwrap(); - let header = generated.text_entry("Contents/header.xml").unwrap(); - let section = generated.section_xml().unwrap(); - - assert!(header.contains("")); - assert!(header.contains("일반 ")); - assert!(section.contains("굵게")); - assert!(section.contains("기울임")); - assert!(section.contains("코드")); - assert!( - section.contains( - "링크 (https://example.com)" - ) - ); -} - -#[test] -fn custom_style_overrides_header_fonts_colors_and_sizes() { - let generated = generate( - "# Heading\n\n본문 [링크](https://example.com)", - HwpxOptions { - style: HwpxStyleOptions { - body_font: Some("Pretendard".to_string()), - heading_font: Some("MaruBuri".to_string()), - body_font_size: Some(1120), - heading_font_size: Some(1560), - text_color: Some("444444".to_string()), - heading_color: Some("AA2200".to_string()), - link_color: Some("0088CC".to_string()), - paragraph_align: Some(HwpxParagraphAlign::Center), - }, - ..HwpxOptions::default() - }, - ) - .unwrap(); - let header = generated.text_entry("Contents/header.xml").unwrap(); - - assert!(header.contains("face=\"Pretendard\"")); - assert!(header.contains("face=\"MaruBuri\"")); - assert!(header.contains("")); - assert!(header.contains("굵게")); - assert!(section.contains("기울임")); - assert!( - section.contains( - "링크 (https://example.com)" - ) - ); -} - -#[test] -fn custom_heading_font_does_not_leak_into_body_inline_runs() { - let generated = generate( - "본문 **굵게** *기울임* [링크](https://example.com)", - HwpxOptions { - style: HwpxStyleOptions { - heading_font: Some("MaruBuri".to_string()), - ..HwpxStyleOptions::default() - }, - ..HwpxOptions::default() - }, - ) - .unwrap(); - let header = generated.text_entry("Contents/header.xml").unwrap(); - - assert!(header.contains("face=\"함초롬바탕\"")); - assert!(header.contains("face=\"MaruBuri\"")); - assert!(header.contains("")); - - let body_bold = header - .split("").next()) - .unwrap(); - let body_italic = header - .split("").next()) - .unwrap(); - let body_link = header - .split("").next()) - .unwrap(); - - assert!(body_bold.contains("hangul=\"0\"")); - assert!(body_italic.contains("hangul=\"0\"")); - assert!(body_link.contains("hangul=\"0\"")); -} - -#[test] -fn paragraph_alignment_applies_to_heading_paragraph_styles() { - let generated = generate( - "# Heading\n\n본문", - HwpxOptions { - style: HwpxStyleOptions { - paragraph_align: Some(HwpxParagraphAlign::Right), - ..HwpxStyleOptions::default() - }, - ..HwpxOptions::default() - }, - ) - .unwrap(); - let header = generated.text_entry("Contents/header.xml").unwrap(); - - for para_id in [0, 2, 3, 4, 5, 6, 7] { - let marker = format!("").next()) - .unwrap(); - assert!(segment.contains("")); - } -} - -#[test] -fn invalid_hwpx_color_option_fails_fast() { - let error = generate_hwpx( - "본문", - HwpxOptions { - style: HwpxStyleOptions { - text_color: Some("not-a-color".to_string()), - ..HwpxStyleOptions::default() - }, - ..HwpxOptions::default() - }, - ) - .unwrap_err(); - - assert!(matches!(error, CoreRsError::InvalidOption(_))); -} - -#[test] -fn invalid_hwpx_font_name_fails_fast() { - let error = generate_hwpx( - "본문", - HwpxOptions { - style: HwpxStyleOptions { - body_font: Some(" ".to_string()), - ..HwpxStyleOptions::default() - }, - ..HwpxOptions::default() - }, - ) - .unwrap_err(); - - assert!(matches!(error, CoreRsError::InvalidOption(_))); -} - -#[test] -fn invalid_hwpx_font_size_fails_fast() { - let error = generate_hwpx( - "본문", - HwpxOptions { - style: HwpxStyleOptions { - body_font_size: Some(5000), - ..HwpxStyleOptions::default() - }, - ..HwpxOptions::default() - }, - ) - .unwrap_err(); - - assert!(matches!(error, CoreRsError::InvalidOption(_))); -} - -#[test] -fn approved_style_fixtures_preserve_header_style_registry_contract() { - for fixture_name in [ - "core-heading", - "core-inline-style", - "core-link-text", - "style-typography", - "style-centered-layout", - "style-brand-color", - ] { - let fixture = approved_fixture(fixture_name); - let input = crate::read_fixture_input(&fixture).unwrap(); - let generated = generate_hwpx(&input, fixture.options()).unwrap(); - let generated_entries = crate::normalized_entries(&generated).unwrap(); - let expected_entries = read_expected_entries(&fixture.root.join("expected")).unwrap(); - - let generated_header = text_entry(&generated_entries, "Contents/header.xml"); - let expected_header = text_entry(&expected_entries, "Contents/header.xml"); - - assert_eq!( - header_style_contract(generated_header), - header_style_contract(expected_header), - "header style contract drifted for fixture {fixture_name}", - ); - } -} - -#[test] -fn approved_style_fixtures_preserve_section_style_references() { - for fixture_name in [ - "core-heading", - "core-inline-style", - "core-link-text", - "style-typography", - "style-centered-layout", - "style-brand-color", - ] { - let fixture = approved_fixture(fixture_name); - let input = crate::read_fixture_input(&fixture).unwrap(); - let generated = generate_hwpx(&input, fixture.options()).unwrap(); - let generated_entries = crate::normalized_entries(&generated).unwrap(); - let expected_entries = read_expected_entries(&fixture.root.join("expected")).unwrap(); - - let generated_section = text_entry(&generated_entries, "Contents/section0.xml"); - let expected_section = text_entry(&expected_entries, "Contents/section0.xml"); - - assert_eq!( - section_style_refs(generated_section), - section_style_refs(expected_section), - "section style references drifted for fixture {fixture_name}", - ); - } -} - -fn approved_fixture(name: &str) -> crate::hwpx_fixture::HwpxFixture { - discover_fixtures() - .unwrap() - .into_iter() - .find(|fixture| fixture.name == name) - .unwrap_or_else(|| panic!("missing approved HWPX fixture: {name}")) -} - -fn text_entry<'a>( - entries: &'a std::collections::BTreeMap, - path: &str, -) -> &'a str { - match entries - .get(path) - .unwrap_or_else(|| panic!("missing entry: {path}")) - { - crate::NormalizedEntry::Text(text) => text.as_str(), - crate::NormalizedEntry::BinaryHash(_) => panic!("{path} was normalized as binary"), - } -} - -type HeaderStyleContract = (Vec, Vec, Vec<(u32, u32, u32)>); - -fn header_style_contract(xml: &str) -> HeaderStyleContract { - let doc = XmlDocument::parse(xml).unwrap(); - let mut char_ids = Vec::new(); - let mut para_ids = Vec::new(); - let mut style_triplets = Vec::new(); - - for node in doc.descendants().filter(|node| node.is_element()) { - match node.tag_name().name() { - "charPr" if node.tag_name().namespace().is_some() => { - if let Some(id) = node - .attribute("id") - .and_then(|value| value.parse::().ok()) - { - char_ids.push(id); - } - } - "paraPr" if node.tag_name().namespace().is_some() => { - if let Some(id) = node - .attribute("id") - .and_then(|value| value.parse::().ok()) - { - para_ids.push(id); - } - } - "style" if node.tag_name().namespace().is_some() => { - let Some(id) = node - .attribute("id") - .and_then(|value| value.parse::().ok()) - else { - continue; - }; - let Some(para_pr) = node - .attribute("paraPrIDRef") - .and_then(|value| value.parse::().ok()) - else { - continue; - }; - let Some(char_pr) = node - .attribute("charPrIDRef") - .and_then(|value| value.parse::().ok()) - else { - continue; - }; - style_triplets.push((id, para_pr, char_pr)); - } - _ => {} - } - } - - char_ids.sort_unstable(); - char_ids.dedup(); - para_ids.sort_unstable(); - para_ids.dedup(); - style_triplets.sort_unstable(); - - (char_ids, para_ids, style_triplets) -} - -fn section_style_refs(xml: &str) -> Vec<(u32, u32, Vec)> { - let doc = XmlDocument::parse(xml).unwrap(); - let mut paragraphs = Vec::new(); - - for paragraph in doc - .descendants() - .filter(|node| node.is_element() && node.tag_name().name() == "p") - { - let Some(para_pr) = paragraph - .attribute("paraPrIDRef") - .and_then(|value| value.parse::().ok()) - else { - continue; - }; - let Some(style_id) = paragraph - .attribute("styleIDRef") - .and_then(|value| value.parse::().ok()) - else { - continue; - }; - - let mut char_refs = paragraph - .children() - .filter(|node| node.is_element() && node.tag_name().name() == "run") - .filter_map(|run| run.attribute("charPrIDRef")) - .filter_map(|value| value.parse::().ok()) - .collect::>(); - char_refs.sort_unstable(); - char_refs.dedup(); - paragraphs.push((para_pr, style_id, char_refs)); - } - - paragraphs -} diff --git a/packages/core-rs/tests/hwpx/regression/document.rs b/packages/core-rs/tests/hwpx/regression/document.rs new file mode 100644 index 0000000..06bd39b --- /dev/null +++ b/packages/core-rs/tests/hwpx/regression/document.rs @@ -0,0 +1,324 @@ +use super::*; + +#[test] +fn non_table_documents_keep_default_header_and_settings_contract() { + let generated = generate("hello", HwpxOptions::default()).unwrap(); + let header = generated.text_entry("Contents/header.xml").unwrap(); + let settings = generated.text_entry("settings.xml").unwrap(); + + assert!(header.contains("")); + assert!(!header.contains("문서 열기")); + assert!(!section.contains("문서 열기 (https://example.com)")); +} + +#[test] +fn empty_link_label_renders_url_only() { + let markdown = "[](https://example.com)"; + let generated = generate(markdown, HwpxOptions::default()).unwrap(); + let section = generated.section_xml().unwrap(); + assert!(section.contains("https://example.com")); +} + +#[test] +fn legacy_inline_links_render_as_hyperlink_fields() { + let generated = generate( + "# Heading\n\n본문 [링크](https://example.com/path) 입니다.", + HwpxOptions::default(), + ) + .unwrap(); + let section = generated.section_xml().unwrap(); + let preview = generated.text_entry("Preview/PrvText.txt").unwrap(); + + assert!(section.contains("type=\"HYPERLINK\"")); + assert!( + section.contains("https://example.com/path") + ); + assert!(section.contains("링크")); + assert!(!section.contains("링크 (https://example.com/path)")); + assert!(preview.contains("본문 링크 입니다.")); +} + +#[test] +fn legacy_links_use_document_unique_field_ids_across_paragraphs() { + let generated = generate( + "첫 문단 [링크1](https://example.com/one)\n\n둘째 문단 [링크2](https://example.com/two)", + HwpxOptions::default(), + ) + .unwrap(); + let section = generated.section_xml().unwrap(); + + assert!(section.contains("fieldid=\"627600491\"")); + assert!(section.contains("fieldid=\"627600492\"")); + assert_eq!(section.matches("fieldid=\"627600491\"").count(), 2); + assert_eq!(section.matches("fieldid=\"627600492\"").count(), 2); + assert_eq!(section.matches("beginIDRef=\"2107483186\"").count(), 1); + assert_eq!(section.matches("beginIDRef=\"2107483187\"").count(), 1); +} + +#[test] +fn content_hpf_contains_metadata() { + let generated = generate( + "hello", + HwpxOptions { + title: Some("Fixture Title".to_string()), + author: Some("Fixture Author".to_string()), + strict_mode: true, + ..HwpxOptions::default() + }, + ) + .unwrap(); + + let content_hpf = generated.content_hpf().unwrap(); + assert!(content_hpf.contains("Fixture Title")); + assert!( + content_hpf + .contains("(주)한글과컴퓨터") + ); + assert!(content_hpf.contains("href=\"Contents/header.xml\"")); + assert!(content_hpf.contains("href=\"Contents/section0.xml\"")); + assert!(content_hpf.contains("href=\"settings.xml\"")); + assert!(content_hpf.contains("ko")); + assert!(content_hpf.contains("")); + assert!(content_hpf.contains("")); +} + +#[test] +fn package_contract_paths_are_self_consistent() { + let generated = generate("hello", HwpxOptions::default()).unwrap(); + let content_hpf = generated.content_hpf().unwrap(); + let container = generated.text_entry("META-INF/container.xml").unwrap(); + let container_rdf = generated.text_entry("META-INF/container.rdf").unwrap(); + + assert!(container.contains("full-path=\"Contents/content.hpf\"")); + assert!(container.contains("full-path=\"Preview/PrvText.txt\"")); + assert!(container.contains("full-path=\"META-INF/container.rdf\"")); + assert!(content_hpf.contains("href=\"Contents/header.xml\"")); + assert!(content_hpf.contains("href=\"Contents/section0.xml\"")); + assert!(content_hpf.contains("href=\"settings.xml\"")); + assert!(container_rdf.contains("Contents/header.xml")); + assert!(container_rdf.contains("Contents/section0.xml")); +} + +#[test] +fn headings_render_as_plain_hwpx_paragraphs() { + let generated = generate("# Heading", HwpxOptions::default()).unwrap(); + let section = generated.section_xml().unwrap(); + assert!(section.contains("Heading")); +} + +#[test] +fn blockquotes_render_with_quote_paragraph_contract_in_strict_mode() { + let generated = generate( + "> Quoted paragraph\n>\n> Another quoted line", + HwpxOptions::default(), + ) + .unwrap(); + let section = generated.section_xml().unwrap(); + let header = generated.text_entry("Contents/header.xml").unwrap(); + let settings = generated.text_entry("settings.xml").unwrap(); + let preview = generated.text_entry("Preview/PrvText.txt").unwrap(); + + assert!(section.contains("paraPrIDRef=\"19\"")); + assert!(section.contains("charPrIDRef=\"7\"")); + assert!(section.contains("horzpos=\"2000\" horzsize=\"40520\" flags=\"393216\"")); + assert!(section.contains("vertpos=\"1600\"")); + assert!(section.contains("Quoted paragraph")); + assert!(section.contains("Another quoted line")); + assert!(header.contains("")); + assert!(!section.contains("hello world")); +} + +#[test] +fn hwpx_xml_uses_hancom_2011_namespaces() { + let generated = generate("hello", HwpxOptions::default()).unwrap(); + let section = generated.section_xml().unwrap(); + let content_hpf = generated.content_hpf().unwrap(); + + assert!(section.contains("http://www.hancom.co.kr/hwpml/2011/paragraph")); + assert!(section.contains("Generated Document")); + assert!(section.contains("안녕하세요")); +} + +#[test] +fn preview_text_tracks_visible_paragraph_text() { + let generated = generate( + "# 제목\n\n문단", + HwpxOptions { + strict_mode: true, + ..HwpxOptions::default() + }, + ) + .unwrap(); + let preview = generated.text_entry("Preview/PrvText.txt").unwrap(); + assert!(preview.contains("제목")); + assert!(preview.contains("문단")); + assert_eq!(preview, "제목\n문단"); +} + +#[test] +fn legacy_body_paragraphs_omit_linesegarray_to_allow_viewer_reflow() { + let generated = generate( + "# Heading\n\nThis paragraph should be wrapped by the viewer instead of using a fixed single lineseg.", + HwpxOptions { + strict_mode: true, + ..HwpxOptions::default() + }, + ) + .unwrap(); + let section = generated.section_xml().unwrap(); + + assert_eq!(section.matches("").count(), 1); + assert!(section.contains("Heading")); + assert!(section.contains( + "This paragraph should be wrapped by the viewer instead of using a fixed single lineseg." + )); +} + +#[test] +fn legacy_list_items_omit_linesegarray_to_avoid_spacing_compression() { + let generated = generate( + "- 첫 번째 항목은 viewer가 줄바꿈을 다시 계산해야 합니다.\n- 두 번째 항목도 동일한 규칙을 따라야 합니다.", + HwpxOptions { + strict_mode: false, + ..HwpxOptions::default() + }, + ) + .unwrap(); + let section = generated.section_xml().unwrap(); + + assert!(!section.contains("")); + assert!(section.contains("paraPrIDRef=\"19\"")); +} + +#[test] +fn legacy_report_like_documents_keep_hyperlinks_and_viewer_reflow_contract() { + let markdown = "# Adoption Report\n\n\ +이 문서는 [저장소](https://github.com/docxly/core-rs)와 [데모](https://docxly.github.io/core-rs/)를 함께 안내합니다.\n\n\ +- 첫 번째 항목은 [가이드](https://example.com/guide) 링크를 포함합니다.\n\ +- 두 번째 항목도 viewer가 줄바꿈을 다시 계산해야 하는 긴 설명을 유지합니다."; + let generated = generate(markdown, HwpxOptions::default()).unwrap(); + let section = generated.section_xml().unwrap(); + let preview = generated.text_entry("Preview/PrvText.txt").unwrap(); + + assert_eq!(section.matches("type=\"HYPERLINK\"").count(), 3); + assert!(section.contains( + "https://github.com/docxly/core-rs" + )); + assert!(section.contains( + "https://docxly.github.io/core-rs/" + )); + assert!( + section.contains("https://example.com/guide") + ); + assert!(section.contains("저장소")); + assert!(section.contains("데모")); + assert!(section.contains("가이드")); + assert!(!section.contains("저장소 (https://github.com/docxly/core-rs)")); + assert!(!section.contains("데모 (https://docxly.github.io/core-rs/)")); + assert!(!section.contains("가이드 (https://example.com/guide)")); + assert_eq!(section.matches("").count(), 1); + assert_eq!( + preview, + "Adoption Report\n이 문서는 저장소와 데모를 함께 안내합니다.\n첫 번째 항목은 가이드 링크를 포함합니다.\n두 번째 항목도 viewer가 줄바꿈을 다시 계산해야 하는 긴 설명을 유지합니다." + ); +} diff --git a/packages/core-rs/tests/hwpx/regression/errors.rs b/packages/core-rs/tests/hwpx/regression/errors.rs new file mode 100644 index 0000000..9c508d3 --- /dev/null +++ b/packages/core-rs/tests/hwpx/regression/errors.rs @@ -0,0 +1,124 @@ +use super::*; + +#[test] +fn analyze_markdown_reports_hwpx_ordered_list_contract() { + let report = analyze_markdown("1. first\n2. second", ConversionTarget::Hwpx); + assert_eq!(report.unsupported_count, 0); + assert_eq!(report.fallback_count, 0); + assert_eq!(report.issues[0].feature, "HWPX ordered list in strict mode"); + assert_eq!(report.issues[0].severity, IssueSeverity::Warning); +} + +#[test] +fn strict_hwpx_report_failure_promotes_ordered_list_issue() { + let failure = + generate_hwpx_with_report("1. first\n2. second", HwpxOptions::default()).unwrap_err(); + assert_eq!(failure.report.unsupported_count, 1); + assert_eq!( + failure.report.issues[0].feature, + "HWPX ordered list in strict mode" + ); + assert_eq!(failure.report.issues[0].severity, IssueSeverity::Error); + assert_eq!( + failure.report.issues[0].message, + "unsupported feature: HWPX approved baseline does not support ordered list" + ); +} + +#[test] +fn compat_hwpx_report_marks_image_fallback() { + let markdown = "![diagram](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO5W5V8AAAAASUVORK5CYII=)"; + let generated = generate_hwpx_with_report( + markdown, + HwpxOptions { + strict_mode: false, + ..HwpxOptions::default() + }, + ) + .unwrap(); + + assert!(generated.report.degraded); + assert_eq!(generated.report.unsupported_count, 0); + assert_eq!(generated.report.fallback_count, 1); + assert_eq!(generated.report.issues[0].feature, "HWPX image"); +} + +#[test] +fn analyze_markdown_does_not_report_blockquote_as_hwpx_fallback() { + let report = analyze_markdown("> quoted", ConversionTarget::Hwpx); + assert!(report.issues.is_empty()); + assert!(!report.degraded); +} + +#[test] +fn invalid_hwpx_color_option_fails_fast() { + let error = generate_hwpx( + "본문", + HwpxOptions { + style: HwpxStyleOptions { + text_color: Some("not-a-color".to_string()), + ..HwpxStyleOptions::default() + }, + ..HwpxOptions::default() + }, + ) + .unwrap_err(); + + assert!(matches!(error, CoreRsError::InvalidOption(_))); +} + +#[test] +fn invalid_hwpx_font_name_fails_fast() { + let error = generate_hwpx( + "본문", + HwpxOptions { + style: HwpxStyleOptions { + body_font: Some(" ".to_string()), + ..HwpxStyleOptions::default() + }, + ..HwpxOptions::default() + }, + ) + .unwrap_err(); + + assert!(matches!(error, CoreRsError::InvalidOption(_))); +} + +#[test] +fn invalid_hwpx_font_size_fails_fast() { + let error = generate_hwpx( + "본문", + HwpxOptions { + style: HwpxStyleOptions { + body_font_size: Some(5000), + ..HwpxStyleOptions::default() + }, + ..HwpxOptions::default() + }, + ) + .unwrap_err(); + + assert!(matches!(error, CoreRsError::InvalidOption(_))); +} + +#[test] +fn invalid_hwpx_option_is_reported_in_generation_failure() { + let failure = generate_hwpx_with_report( + "본문", + HwpxOptions { + style: HwpxStyleOptions { + text_color: Some("not-a-color".to_string()), + ..HwpxStyleOptions::default() + }, + ..HwpxOptions::default() + }, + ) + .unwrap_err(); + + assert!(matches!(failure.error, CoreRsError::InvalidOption(_))); + assert_eq!(failure.report.unsupported_count, 1); + assert_eq!(failure.report.fallback_count, 0); + assert_eq!(failure.report.issues[0].feature, "invalid option"); + assert_eq!(failure.report.issues[0].severity, IssueSeverity::Error); + assert!(failure.report.issues[0].message.contains("invalid option")); +} diff --git a/packages/core-rs/tests/hwpx/regression/lists.rs b/packages/core-rs/tests/hwpx/regression/lists.rs new file mode 100644 index 0000000..a318310 --- /dev/null +++ b/packages/core-rs/tests/hwpx/regression/lists.rs @@ -0,0 +1,236 @@ +use super::*; + +#[test] +fn lists_fail_in_strict_mode() { + let markdown = "1. item"; + let error = generate_hwpx(markdown, HwpxOptions::default()).unwrap_err(); + assert!(matches!(error, CoreRsError::UnsupportedFeature(_))); +} + +#[test] +fn unordered_lists_use_list_paragraph_contract_in_strict_mode() { + let markdown = "- Alpha\n- Beta"; + let generated = generate(markdown, HwpxOptions::default()).unwrap(); + + let section = generated.section_xml().unwrap(); + assert!(section.contains("paraPrIDRef=\"19\"")); + assert!(section.contains("styleIDRef=\"0\"")); + assert!(section.contains("Alpha")); + assert!(section.contains("Beta")); + assert!(!section.contains("")); + assert!(!section.contains("- Alpha")); + assert!(!section.contains("- Beta")); + assert!(!section.contains('•')); + assert!(!section.contains("paraPrIDRef=\"14\"")); +} + +#[test] +fn nested_unordered_lists_use_second_list_level() { + let markdown = "- Parent\n - Child one\n - Child two\n- Sibling"; + let generated = generate(markdown, HwpxOptions::default()).unwrap(); + + let section = generated.section_xml().unwrap(); + assert!(section.contains("paraPrIDRef=\"19\"")); + assert!(section.contains("paraPrIDRef=\"20\"")); + assert!(section.contains("styleIDRef=\"0\"")); + assert!(section.contains("Parent")); + assert!(section.contains("Child one")); + assert!(section.contains("Child two")); + assert!(section.contains("Sibling")); + assert!(!section.contains("")); + assert!(!section.contains("- Parent")); + assert!(!section.contains("- Child one")); + assert!(!section.contains("- Child two")); + assert!(!section.contains("- Sibling")); + assert!(!section.contains('•')); + assert!(!section.contains('◦')); + assert!(!section.contains("paraPrIDRef=\"14\"")); + assert!(!section.contains("paraPrIDRef=\"15\"")); +} + +#[test] +fn header_reuses_existing_list_paragraph_contract() { + let generated = generate("- Alpha\n - Child", HwpxOptions::default()).unwrap(); + let header = generated.text_entry("Contents/header.xml").unwrap(); + + assert!(header.contains("")); + assert!(header.contains("")); + assert!(header.contains("")); + assert!(header.contains("Name")); + assert!(section.contains("Reviewer")); + assert!(!section.contains("Name | Role")); +} + +#[test] +fn table_preview_text_matches_golden_contract() { + let markdown = "| Name | Role |\n| --- | --- |\n| A | Writer |\n| B | Reviewer |"; + let generated = generate(markdown, HwpxOptions::default()).unwrap(); + let preview = generated.text_entry("Preview/PrvText.txt").unwrap(); + let settings = generated.text_entry("settings.xml").unwrap(); + + assert_eq!(preview, "\n\n"); + assert!(settings.contains("pos=\"16\"")); +} + +#[test] +fn tables_add_table_border_fill_to_header() { + let markdown = "| Name | Role |\n| --- | --- |\n| A | Writer |"; + let generated = generate(markdown, HwpxOptions::default()).unwrap(); + let header = generated.text_entry("Contents/header.xml").unwrap(); + + assert!(header.contains("")); + assert!(header.contains("")); + assert!(section.contains("first")); + assert!(section.contains("second")); + assert!(!section.contains("1. first")); + assert!(!section.contains("2. second")); + assert_eq!(preview, "first\nsecond"); + assert!(settings.contains("paraIDRef=\"0\"")); + assert!(settings.contains("pos=\"5\"")); +} + +#[test] +fn nested_ordered_lists_use_shape_specific_contract_in_non_strict_mode() { + let markdown = "1. Parent\n 1. Child one\n 2. Child two\n2. Sibling"; + let generated = generate( + markdown, + HwpxOptions { + strict_mode: false, + ..HwpxOptions::default() + }, + ) + .unwrap(); + + let section = generated.section_xml().unwrap(); + let preview = generated.text_entry("Preview/PrvText.txt").unwrap(); + let settings = generated.text_entry("settings.xml").unwrap(); + + assert!(section.contains("paraPrIDRef=\"20\"")); + assert!(section.contains("paraPrIDRef=\"19\"")); + assert!(section.contains("Parent")); + assert!(section.contains("Child one")); + assert!(section.contains("Child two")); + assert!(section.contains("Sibling")); + assert!(!section.contains("")); + assert!(!section.contains("1. Parent")); + assert!(!section.contains("1. Child one")); + assert!(!section.contains("2. Child two")); + assert!(!section.contains("2. Sibling")); + assert_eq!(preview, "Parent\nChild one\nChild two\nSibling"); + assert!(settings.contains("paraIDRef=\"3\"")); + assert!(settings.contains("pos=\"7\"")); +} + +#[test] +fn ordered_list_headers_use_numbering_contract() { + let basic = generate( + "1. Alpha\n2. Beta\n3. Gamma", + HwpxOptions { + strict_mode: false, + ..HwpxOptions::default() + }, + ) + .unwrap(); + let basic_header = basic.text_entry("Contents/header.xml").unwrap(); + assert!(basic_header.contains("")); + assert!(basic_header.contains("")); + assert!(basic_header.contains("")); + assert!(basic_header.contains("")); + + let nested = generate( + "1. Parent\n 1. Child\n2. Sibling", + HwpxOptions { + strict_mode: false, + ..HwpxOptions::default() + }, + ) + .unwrap(); + let nested_header = nested.text_entry("Contents/header.xml").unwrap(); + assert!(nested_header.contains("")); + assert!(nested_header.contains("")); + assert!(nested_header.contains("")); + assert!(nested_header.contains("")); + assert!(nested_header.contains("")); + assert!(nested_header.contains("")); +} + +#[test] +fn nested_lists_over_depth_two_fail_in_strict_mode() { + let markdown = "- A\n - B\n - C"; + let error = generate_hwpx(markdown, HwpxOptions::default()).unwrap_err(); + assert!(matches!(error, CoreRsError::UnsupportedFeature(_))); +} + +#[test] +fn nested_lists_over_depth_two_fall_back_in_non_strict_mode() { + let markdown = "- A\n - B\n - C"; + let generated = generate( + markdown, + HwpxOptions { + strict_mode: false, + ..HwpxOptions::default() + }, + ) + .unwrap(); + + let section = generated.section_xml().unwrap(); + assert!(section.contains("A")); + assert!(section.contains("B")); + assert!(section.contains("C")); + assert!(section.contains("paraPrIDRef=\"20\"")); + assert!(!section.contains('•')); + assert!(!section.contains('◦')); +} diff --git a/packages/core-rs/tests/hwpx/regression/mod.rs b/packages/core-rs/tests/hwpx/regression/mod.rs new file mode 100644 index 0000000..5b07f8e --- /dev/null +++ b/packages/core-rs/tests/hwpx/regression/mod.rs @@ -0,0 +1,132 @@ +mod document; +mod errors; +mod lists; +mod styles; + +use std::path::Path; + +use core_rs::{ + ConversionTarget, CoreRsError, HwpxOptions, HwpxParagraphAlign, HwpxStyleOptions, + IssueSeverity, analyze_markdown, generate_hwpx, generate_hwpx_with_report, +}; +use roxmltree::Document as XmlDocument; + +use crate::hwpx_contract::{compare_contracts, snapshot_from_bytes, snapshot_from_expected_tree}; +use crate::hwpx_fixture::{discover_fixtures, read_expected_entries}; +use crate::hwpx_runtime::generate; + +fn approved_fixture(name: &str) -> crate::hwpx_fixture::HwpxFixture { + discover_fixtures() + .unwrap() + .into_iter() + .find(|fixture| fixture.name == name) + .unwrap_or_else(|| panic!("missing approved HWPX fixture: {name}")) +} + +fn text_entry<'a>( + entries: &'a std::collections::BTreeMap, + path: &str, +) -> &'a str { + match entries + .get(path) + .unwrap_or_else(|| panic!("missing entry: {path}")) + { + crate::NormalizedEntry::Text(text) => text.as_str(), + crate::NormalizedEntry::BinaryHash(_) => panic!("{path} was normalized as binary"), + } +} + +type HeaderStyleContract = (Vec, Vec, Vec<(u32, u32, u32)>); + +fn header_style_contract(xml: &str) -> HeaderStyleContract { + let doc = XmlDocument::parse(xml).unwrap(); + let mut char_ids = Vec::new(); + let mut para_ids = Vec::new(); + let mut style_triplets = Vec::new(); + + for node in doc.descendants().filter(|node| node.is_element()) { + match node.tag_name().name() { + "charPr" if node.tag_name().namespace().is_some() => { + if let Some(id) = node + .attribute("id") + .and_then(|value| value.parse::().ok()) + { + char_ids.push(id); + } + } + "paraPr" if node.tag_name().namespace().is_some() => { + if let Some(id) = node + .attribute("id") + .and_then(|value| value.parse::().ok()) + { + para_ids.push(id); + } + } + "style" if node.tag_name().namespace().is_some() => { + let Some(id) = node + .attribute("id") + .and_then(|value| value.parse::().ok()) + else { + continue; + }; + let Some(para_pr) = node + .attribute("paraPrIDRef") + .and_then(|value| value.parse::().ok()) + else { + continue; + }; + let Some(char_pr) = node + .attribute("charPrIDRef") + .and_then(|value| value.parse::().ok()) + else { + continue; + }; + style_triplets.push((id, para_pr, char_pr)); + } + _ => {} + } + } + + char_ids.sort_unstable(); + char_ids.dedup(); + para_ids.sort_unstable(); + para_ids.dedup(); + style_triplets.sort_unstable(); + + (char_ids, para_ids, style_triplets) +} + +fn section_style_refs(xml: &str) -> Vec<(u32, u32, Vec)> { + let doc = XmlDocument::parse(xml).unwrap(); + let mut paragraphs = Vec::new(); + + for paragraph in doc + .descendants() + .filter(|node| node.is_element() && node.tag_name().name() == "p") + { + let Some(para_pr) = paragraph + .attribute("paraPrIDRef") + .and_then(|value| value.parse::().ok()) + else { + continue; + }; + let Some(style_id) = paragraph + .attribute("styleIDRef") + .and_then(|value| value.parse::().ok()) + else { + continue; + }; + + let mut char_refs = paragraph + .children() + .filter(|node| node.is_element() && node.tag_name().name() == "run") + .filter_map(|run| run.attribute("charPrIDRef")) + .filter_map(|value| value.parse::().ok()) + .collect::>(); + char_refs.sort_unstable(); + char_refs.dedup(); + paragraphs.push((para_pr, style_id, char_refs)); + } + + paragraphs +} diff --git a/packages/core-rs/tests/hwpx/regression/styles.rs b/packages/core-rs/tests/hwpx/regression/styles.rs new file mode 100644 index 0000000..2d1aa2b --- /dev/null +++ b/packages/core-rs/tests/hwpx/regression/styles.rs @@ -0,0 +1,232 @@ +use super::*; + +#[test] +fn headings_use_outline_style_refs_and_heading_char_properties() { + let generated = generate("# Heading", HwpxOptions::default()).unwrap(); + let header = generated.text_entry("Contents/header.xml").unwrap(); + let section = generated.section_xml().unwrap(); + + assert!(header.contains("")); + assert!(header.contains("" + )); + assert!(header.contains("일반 ")); + assert!(section.contains("굵게")); + assert!(section.contains("기울임")); + assert!(section.contains("코드")); + assert!(section.contains("type=\"HYPERLINK\"")); + assert!(section.contains("https://example.com")); + assert!(section.contains("링크")); + assert!(!section.contains("링크 (https://example.com)")); +} + +#[test] +fn default_header_registry_does_not_leave_negative_tracking() { + let generated = generate("본문", HwpxOptions::default()).unwrap(); + let header = generated.text_entry("Contents/header.xml").unwrap(); + + assert!(!header.contains("hangul=\"-5\"")); + assert!(header.contains("")); +} + +#[test] +fn custom_style_overrides_header_fonts_colors_and_sizes() { + let generated = generate( + "# Heading\n\n본문 [링크](https://example.com)", + HwpxOptions { + style: HwpxStyleOptions { + body_font: Some("Pretendard".to_string()), + heading_font: Some("MaruBuri".to_string()), + body_font_size: Some(1120), + heading_font_size: Some(1560), + text_color: Some("444444".to_string()), + heading_color: Some("AA2200".to_string()), + link_color: Some("0088CC".to_string()), + paragraph_align: Some(HwpxParagraphAlign::Center), + }, + ..HwpxOptions::default() + }, + ) + .unwrap(); + let header = generated.text_entry("Contents/header.xml").unwrap(); + + assert!(header.contains("face=\"Pretendard\"")); + assert!(header.contains("face=\"MaruBuri\"")); + assert!(header.contains("")); + assert!(header.contains("굵게")); + assert!(section.contains("기울임")); + assert!(section.contains("type=\"HYPERLINK\"")); + assert!(section.contains("https://example.com")); + assert!(section.contains("링크")); + assert!(!section.contains("링크 (https://example.com)")); +} + +#[test] +fn custom_heading_font_does_not_leak_into_body_inline_runs() { + let generated = generate( + "본문 **굵게** *기울임* [링크](https://example.com)", + HwpxOptions { + style: HwpxStyleOptions { + heading_font: Some("MaruBuri".to_string()), + ..HwpxStyleOptions::default() + }, + ..HwpxOptions::default() + }, + ) + .unwrap(); + let header = generated.text_entry("Contents/header.xml").unwrap(); + + assert!(header.contains("face=\"함초롬바탕\"")); + assert!(header.contains("face=\"MaruBuri\"")); + assert!(header.contains("")); + + let body_bold = header + .split("").next()) + .unwrap(); + let body_italic = header + .split("").next()) + .unwrap(); + let body_link = header + .split("").next()) + .unwrap(); + + assert!(body_bold.contains("hangul=\"0\"")); + assert!(body_italic.contains("hangul=\"0\"")); + assert!(body_link.contains("hangul=\"0\"")); +} + +#[test] +fn paragraph_alignment_applies_to_heading_paragraph_styles() { + let generated = generate( + "# Heading\n\n본문", + HwpxOptions { + style: HwpxStyleOptions { + paragraph_align: Some(HwpxParagraphAlign::Right), + ..HwpxStyleOptions::default() + }, + ..HwpxOptions::default() + }, + ) + .unwrap(); + let header = generated.text_entry("Contents/header.xml").unwrap(); + + for para_id in [0, 2, 3, 4, 5, 6, 7] { + let marker = format!("").next()) + .unwrap(); + assert!(segment.contains("")); + } +} + +#[test] +fn approved_style_fixtures_preserve_header_style_registry_contract() { + for fixture_name in [ + "core-heading", + "core-inline-style", + "core-link-text", + "style-typography", + "style-centered-layout", + "style-brand-color", + ] { + let fixture = approved_fixture(fixture_name); + let input = crate::read_fixture_input(&fixture).unwrap(); + let generated = generate_hwpx(&input, fixture.options()).unwrap(); + let generated_entries = crate::normalized_entries(&generated).unwrap(); + let expected_entries = read_expected_entries(&fixture.root.join("expected")).unwrap(); + + let generated_header = text_entry(&generated_entries, "Contents/header.xml"); + let expected_header = text_entry(&expected_entries, "Contents/header.xml"); + + assert_eq!( + header_style_contract(generated_header), + header_style_contract(expected_header), + "header style contract drifted for fixture {fixture_name}", + ); + } +} + +#[test] +fn approved_style_fixtures_preserve_section_style_references() { + for fixture_name in [ + "core-heading", + "core-inline-style", + "core-link-text", + "style-typography", + "style-centered-layout", + "style-brand-color", + ] { + let fixture = approved_fixture(fixture_name); + let input = crate::read_fixture_input(&fixture).unwrap(); + let generated = generate_hwpx(&input, fixture.options()).unwrap(); + let generated_entries = crate::normalized_entries(&generated).unwrap(); + let expected_entries = read_expected_entries(&fixture.root.join("expected")).unwrap(); + + let generated_section = text_entry(&generated_entries, "Contents/section0.xml"); + let expected_section = text_entry(&expected_entries, "Contents/section0.xml"); + + assert_eq!( + section_style_refs(generated_section), + section_style_refs(expected_section), + "section style references drifted for fixture {fixture_name}", + ); + } +} diff --git a/packages/core-rs/tests/hwpx_fixture_refresh_test.rs b/packages/core-rs/tests/hwpx_fixture_refresh_test.rs index 1220453..1d89f74 100644 --- a/packages/core-rs/tests/hwpx_fixture_refresh_test.rs +++ b/packages/core-rs/tests/hwpx_fixture_refresh_test.rs @@ -7,7 +7,8 @@ use std::time::{SystemTime, UNIX_EPOCH}; mod archive_fixture; use archive_fixture::{ - ArchiveEntry, hash_entries, normalized_entries, read_expected_entries, refresh_fixture_metadata, + ArchiveEntry, hash_entries, normalized_archives_equivalent, normalized_entries, + read_expected_entries, refresh_fixture_metadata, }; const AUXILIARY_NAMES: &[&str] = &[".DS_Store", "README.md", "provenance.md"]; @@ -142,6 +143,43 @@ fn cli_fails_for_unknown_fixture_name() { assert!(stderr.contains("unknown HWPX approved fixture: does-not-exist")); } +#[test] +fn cli_prints_equivalent_status_for_matching_generated_output() { + let _guard = GeneratedOutputGuard::backup("core-paragraph"); + let generated = generated_output_root("core-paragraph"); + fs::create_dir_all(generated.parent().unwrap()).unwrap(); + fs::copy( + fixture_root().join("core-paragraph/golden.hwpx"), + &generated, + ) + .unwrap(); + + let output = Command::new(env!("CARGO_BIN_EXE_refresh_hwpx_fixture_metadata")) + .arg("core-paragraph") + .output() + .unwrap(); + + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!(stdout.contains("📦 fixture: core-paragraph")); + assert!(stdout.contains( + "golden: /Users/limchaesung/Github/docxly/core-rs/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/golden.hwpx" + )); + assert!( + stdout.contains( + "compare: ✅ /tmp/hwpx-generator-outputs/core-paragraph/generated.hwpx equivalent /Users/limchaesung/Github/docxly/core-rs/packages/core-rs/tests/fixtures/hwpx/approved/core-paragraph/golden.hwpx" + ) + ); +} + +#[test] +fn normalized_archive_comparison_detects_differences() { + let left = fs::read(fixture_root().join("core-paragraph/golden.hwpx")).unwrap(); + let right = fs::read(fixture_root().join("table-basic/golden.hwpx")).unwrap(); + + assert!(!normalized_archives_equivalent(&left, &right, is_text_entry).unwrap()); +} + fn fixture_root() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")) .join("tests") @@ -205,3 +243,55 @@ impl Drop for TestTempDir { let _ = fs::remove_dir_all(&self.path); } } + +fn generated_output_root(name: &str) -> PathBuf { + Path::new("/tmp") + .join("hwpx-generator-outputs") + .join(name) + .join("generated.hwpx") +} + +struct GeneratedOutputGuard { + target_dir: PathBuf, + backup_dir: Option, +} + +impl GeneratedOutputGuard { + fn backup(name: &str) -> Self { + let target_dir = Path::new("/tmp").join("hwpx-generator-outputs").join(name); + let backup_dir = if target_dir.exists() { + let backup = std::env::temp_dir().join(format!( + "hwpx-generator-outputs-backup-{name}-{}-{}", + std::process::id(), + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + if let Some(parent) = backup.parent() { + fs::create_dir_all(parent).unwrap(); + } + fs::rename(&target_dir, &backup).unwrap(); + Some(backup) + } else { + None + }; + + Self { + target_dir, + backup_dir, + } + } +} + +impl Drop for GeneratedOutputGuard { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.target_dir); + if let Some(backup_dir) = &self.backup_dir { + if let Some(parent) = self.target_dir.parent() { + let _ = fs::create_dir_all(parent); + } + let _ = fs::rename(backup_dir, &self.target_dir); + } + } +} diff --git a/packages/core-rs/tests/hwpx_test.rs b/packages/core-rs/tests/hwpx_test.rs index f120a19..65f03fc 100644 --- a/packages/core-rs/tests/hwpx_test.rs +++ b/packages/core-rs/tests/hwpx_test.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::fs; #[path = "hwpx/fixture_suite.rs"] @@ -9,7 +9,7 @@ mod hwpx_contract; mod hwpx_fixture; #[path = "support/hwpx_runtime.rs"] mod hwpx_runtime; -#[path = "hwpx/regression.rs"] +#[path = "hwpx/regression/mod.rs"] mod regression; use hwpx_fixture::{ @@ -49,7 +49,8 @@ pub(crate) fn assert_fixture_matches_golden(fixture: &HwpxFixture) { ); assert_eq!( - generated_normalized, golden_normalized, + comparable_entries(fixture, &generated_normalized), + comparable_entries(fixture, &golden_normalized), "generated golden mismatch: {}", fixture.name ); @@ -120,3 +121,34 @@ pub(crate) fn assert_xml_is_well_formed(entries: &BTreeMap, +) -> BTreeMap { + let excluded_paths = fixture_comparison_excludes(fixture); + if excluded_paths.is_empty() { + return entries.clone(); + } + + entries + .iter() + .filter(|(path, _)| !excluded_paths.contains(path.as_str())) + .map(|(path, entry)| (path.clone(), entry.clone())) + .collect() +} + +fn fixture_comparison_excludes(fixture: &HwpxFixture) -> BTreeSet<&str> { + let mut excluded_paths = BTreeSet::new(); + if !fixture.determinism { + excluded_paths.extend(NONDETERMINISTIC_ENTRY_EXCLUDES.iter().copied()); + } + excluded_paths.extend(fixture.comparison_excludes.iter().map(String::as_str)); + excluded_paths +} + +const NONDETERMINISTIC_ENTRY_EXCLUDES: &[&str] = &[ + "Contents/content.hpf", + "Preview/PrvImage.png", + "version.xml", +]; diff --git a/packages/core-rs/tests/readme_contract_test.rs b/packages/core-rs/tests/readme_contract_test.rs new file mode 100644 index 0000000..1335b4a --- /dev/null +++ b/packages/core-rs/tests/readme_contract_test.rs @@ -0,0 +1,39 @@ +use std::fs; +use std::path::Path; + +#[test] +fn readmes_include_strict_compat_capability_contract() { + let crate_root = Path::new(env!("CARGO_MANIFEST_DIR")); + let root_readme = fs::read_to_string(crate_root.join("../../README.md")).unwrap(); + let crate_readme = fs::read_to_string(crate_root.join("README.md")).unwrap(); + let npm_readme = fs::read_to_string(crate_root.join("../npm-core-rs/README.md")).unwrap(); + + for readme in [&root_readme, &crate_readme, &npm_readme] { + assert!(readme.contains("| Capability | DOCX | HWPX strict | HWPX compat |")); + } + + assert!(root_readme.contains("Experimental API")); + assert!(crate_readme.contains("generate_docx_with_report")); + assert!(npm_readme.contains("experimental HWPX API")); + assert!(npm_readme.contains("generateHwpxWithReport")); +} + +#[test] +fn benchmark_docs_do_not_freeze_old_summary_numbers() { + let crate_root = Path::new(env!("CARGO_MANIFEST_DIR")); + let root_readme = fs::read_to_string(crate_root.join("../../README.md")).unwrap(); + let ko_readme = fs::read_to_string(crate_root.join("../../docs/ko/README.md")).unwrap(); + let ko_benchmark = fs::read_to_string(crate_root.join("../../docs/ko/benchmark.md")).unwrap(); + let ko_design = fs::read_to_string(crate_root.join("../../docs/ko/design-system.md")).unwrap(); + + for doc in [&root_readme, &ko_readme, &ko_benchmark, &ko_design] { + assert!(!doc.contains("105x")); + assert!(!doc.contains("80 ms cold / 2 ms steady")); + assert!(!doc.contains("284 ms cold / 210 ms steady")); + } + + assert!(root_readme.contains("comparison block below")); + assert!(ko_readme.contains("comparison block")); + assert!(ko_benchmark.contains("comparison-data.json")); + assert!(ko_design.contains("shared benchmark dataset")); +} diff --git a/packages/core-rs/tests/support/archive_fixture.rs b/packages/core-rs/tests/support/archive_fixture.rs index aa8e917..ddec355 100644 --- a/packages/core-rs/tests/support/archive_fixture.rs +++ b/packages/core-rs/tests/support/archive_fixture.rs @@ -6,6 +6,6 @@ mod shared_archive_fixture; pub(crate) use shared_archive_fixture::{ ArchiveEntry, BinaryRepresentation, FixtureResult, NormalizedEntry, archive_entries, archive_entry_names, detect_binary_representations, discover_fixture_dirs, hash_entries, - normalized_entries, normalized_entries_from_archive, read_expected_entries, read_fixture_input, - refresh_fixture_metadata, + normalized_archives_equivalent, normalized_entries, normalized_entries_from_archive, + read_expected_entries, read_fixture_input, refresh_fixture_metadata, }; diff --git a/packages/core-rs/tests/support/docx_fixture.rs b/packages/core-rs/tests/support/docx_fixture.rs index 93d6d7c..be3e12f 100644 --- a/packages/core-rs/tests/support/docx_fixture.rs +++ b/packages/core-rs/tests/support/docx_fixture.rs @@ -106,5 +106,8 @@ pub fn hash_entries(entries: &BTreeMap) -> String { } fn is_text_entry(path: &str) -> bool { - path.ends_with(".xml") || path.ends_with(".rels") || path.ends_with(".txt") || path.ends_with(".md") + path.ends_with(".xml") + || path.ends_with(".rels") + || path.ends_with(".txt") + || path.ends_with(".md") } diff --git a/packages/core-rs/tests/support/docx_runtime.rs b/packages/core-rs/tests/support/docx_runtime.rs index 545c147..d481fb2 100644 --- a/packages/core-rs/tests/support/docx_runtime.rs +++ b/packages/core-rs/tests/support/docx_runtime.rs @@ -6,7 +6,10 @@ pub struct GeneratedDocx { entries: std::collections::BTreeMap, } -fn generate_normalized(markdown: &str, options: DocxOptions) -> FixtureResult> { +fn generate_normalized( + markdown: &str, + options: DocxOptions, +) -> FixtureResult> { let bytes = generate_docx(markdown, options)?; normalized_entries(&bytes) } diff --git a/packages/core-rs/tests/support/hwpx_fixture.rs b/packages/core-rs/tests/support/hwpx_fixture.rs index b1698ff..c09b0eb 100644 --- a/packages/core-rs/tests/support/hwpx_fixture.rs +++ b/packages/core-rs/tests/support/hwpx_fixture.rs @@ -18,6 +18,12 @@ struct FixtureConfig { strict_mode: bool, determinism: bool, #[serde(default)] + comparison_excludes: Vec, + #[serde(default)] + manual_verified: bool, + #[serde(default)] + verified_at: Option, + #[serde(default)] style: FixtureStyleConfig, } @@ -41,6 +47,9 @@ pub struct HwpxFixture { pub author: String, pub strict_mode: bool, pub determinism: bool, + pub comparison_excludes: Vec, + pub manual_verified: bool, + pub verified_at: Option, pub style: HwpxStyleOptions, } @@ -115,6 +124,9 @@ fn discover_fixtures_in(root: PathBuf) -> FixtureResult> { author: config.author, strict_mode: config.strict_mode, determinism: config.determinism, + comparison_excludes: config.comparison_excludes, + manual_verified: config.manual_verified, + verified_at: config.verified_at, style: HwpxStyleOptions { body_font: config.style.body_font, heading_font: config.style.heading_font, diff --git a/packages/npm-core-rs/README.md b/packages/npm-core-rs/README.md index 4df4ebe..fa6f123 100644 --- a/packages/npm-core-rs/README.md +++ b/packages/npm-core-rs/README.md @@ -2,12 +2,13 @@ Language: [English](./README.md) · [한국어 문서](../../docs/ko/README.md) -`@docxly/core-rs` is the npm package for the `docxly` Rust core, an embeddable document generation engine for app integration. Where Pandoc is a general-purpose converter, this package is designed to expose DOCX generation directly inside Node and browser-based runtimes, with the current offline Node benchmark showing an 80 ms cold start and a 2 ms steady median for docxly versus 284 ms cold and 210 ms steady for Pandoc on the summary corpus. +`@docxly/core-rs` is the npm package for the `docxly` Rust core, an embeddable document generation engine for app integration. Where Pandoc is a general-purpose converter, this package is designed to expose DOCX generation directly inside Node and browser-based runtimes, with current benchmark details maintained in the shared comparison dataset and README comparison block. ## Status - DOCX generation: supported -- HWPX generation: not exposed in npm v0.x +- HWPX generation: experimental HWPX API +- report APIs: recommended for user-generated Markdown - API style: async only ## Live Demo @@ -20,38 +21,55 @@ Language: [English](./README.md) · [한국어 문서](../../docs/ko/README.md) npm install @docxly/core-rs ``` +## Release Workflow + +- Contributors add a changeset from the repository root with `npm run changeset:add` +- After that changeset merges into `main`, the `Version Packages` workflow opens or updates a PR that bumps this package version +- The same PR also syncs `packages/core-rs/Cargo.toml` and refreshes this package's `package-lock.json` +- The existing `Release` workflow still publishes npm when a matching `v*.*.*` tag is pushed + ## Usage ### Node ```js import { writeFile } from "node:fs/promises"; -import { generateDocx } from "@docxly/core-rs"; +import { generateDocx, generateHwpxWithReport } from "@docxly/core-rs"; const bytes = await generateDocx("# Hello\n\nThis is **docxly**."); await writeFile("output.docx", bytes); + +const hwpx = await generateHwpxWithReport("1. alpha\n2. beta", { + strictMode: false, +}); +console.log(hwpx.report.issues); ``` ### Browser -Use a bundler/runtime that can resolve package exports and emit the `.wasm` asset. The package is -not meant to be imported directly from a raw `