From 7194cff6e4b8673c80256d6bf4a1f3f27b57c4a6 Mon Sep 17 00:00:00 2001 From: Iktahana <171251543+Iktahana@users.noreply.github.com> Date: Fri, 24 Jul 2026 02:50:14 +0900 Subject: [PATCH] Prepare Python Swift and Rust package releases --- .github/workflows/ci.yml | 20 ++ .github/workflows/python.yml | 73 +++++ .github/workflows/release-pypi.yml | 263 ++++++++++++++++-- .github/workflows/swift-release.yml | 84 ++++-- .gitignore | 3 + Package.swift | 2 +- docs/mdi-2.0-announcement.md | 4 +- docs/src/content/docs/bindings/swift.md | 10 +- docs/src/content/docs/ja/bindings/swift.md | 12 +- docs/src/content/docs/zh-tw/bindings/swift.md | 10 +- mdi-android-jni/Cargo.lock | 2 +- mdi-core/Cargo.lock | 2 +- mdi-core/Cargo.toml | 2 +- python/Cargo.lock | 4 +- python/Cargo.toml | 4 +- python/pyproject.toml | 3 +- python/tests/test_mdi.py | 23 ++ python/tests/test_package_smoke.py | 171 ++++++++++++ scripts/export-swift-coverage.sh | 7 +- scripts/verify-swift-package-formats.sh | 91 ++++++ scripts/write-swift-package-manifest.sh | 29 +- swift/README.md | 8 +- swift/Sources/MDI/MDI.swift | 40 ++- swift/Sources/MDICore/include/mdi_core.h | 8 + swift/Tests/MDITests/MDITests.swift | 234 +++++++++++++++- 25 files changed, 1049 insertions(+), 60 deletions(-) create mode 100644 python/tests/test_package_smoke.py create mode 100755 scripts/verify-swift-package-formats.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b53222..8a6dfa0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -112,10 +112,24 @@ jobs: with: targets: aarch64-apple-darwin,x86_64-apple-darwin,aarch64-apple-ios,aarch64-apple-ios-sim,x86_64-apple-ios + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + + - name: Install the official W3C EPUBCheck validator + run: | + curl --fail --location --silent --show-error \ + https://github.com/w3c/epubcheck/releases/download/v5.3.0/epubcheck-5.3.0.zip \ + --output "$RUNNER_TEMP/epubcheck.zip" + ditto -x -k "$RUNNER_TEMP/epubcheck.zip" "$RUNNER_TEMP" + - name: Build the Apple XCFramework run: scripts/package-swift-xcframework.sh "$RUNNER_TEMP/mdi-swift" - name: Test Swift binding with coverage + env: + MDI_SWIFT_SMOKE_OUTPUT: ${{ runner.temp }}/swift-package-smoke run: | mkdir -p swift/Artifacts ditto "$RUNNER_TEMP/mdi-swift/MDICore.xcframework" swift/Artifacts/MDICore.xcframework @@ -123,6 +137,12 @@ jobs: swift test --enable-code-coverage scripts/export-swift-coverage.sh swift/coverage.lcov + - name: Open and validate every Swift package format + run: >- + scripts/verify-swift-package-formats.sh + "$RUNNER_TEMP/swift-package-smoke" + "$RUNNER_TEMP/epubcheck-5.3.0/epubcheck.jar" + - name: Upload Swift coverage to Codecov uses: codecov/codecov-action@v5 with: diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 699fff5..d7a4a93 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -7,11 +7,13 @@ on: - python/** - mdi-core/** - .github/workflows/python.yml + - .github/workflows/release-pypi.yml pull_request: paths: - python/** - mdi-core/** - .github/workflows/python.yml + - .github/workflows/release-pypi.yml jobs: test: @@ -35,6 +37,22 @@ jobs: cache: pip cache-dependency-path: python/pyproject.toml + - uses: actions/setup-java@v4 + if: matrix.python-version == '3.14' + with: + distribution: temurin + java-version: "21" + + - uses: actions/setup-dotnet@v4 + if: matrix.python-version == '3.14' + with: + dotnet-version: "8.0.x" + + - uses: actions/setup-node@v4 + if: matrix.python-version == '3.14' + with: + node-version: "22" + - name: Build the native extension and install test dependencies run: python -m pip install -e '.[test]' @@ -43,6 +61,61 @@ jobs: - name: Run public API tests with coverage gate run: python -m pytest --cov=mdi --cov-branch --cov-report=term-missing --cov-report=xml + - name: Install authoritative publication-format consumers + if: matrix.python-version == '3.14' + working-directory: ${{ github.workspace }} + run: | + sudo apt-get update + sudo apt-get install --yes libreoffice-writer + curl --fail --location --silent --show-error \ + https://github.com/w3c/epubcheck/releases/download/v5.3.0/epubcheck-5.3.0.zip \ + --output "$RUNNER_TEMP/epubcheck.zip" + unzip -q "$RUNNER_TEMP/epubcheck.zip" -d "$RUNNER_TEMP" + npm install --prefix "$RUNNER_TEMP/vnu" --no-save vnu-jar@25.12.31 + + - name: Generate every public Python format + if: matrix.python-version == '3.14' + working-directory: ${{ github.workspace }} + run: | + python python/tests/test_package_smoke.py \ + --output-dir "$RUNNER_TEMP/python-package-smoke" + + - name: Validate HTML with the Nu Html Checker + if: matrix.python-version == '3.14' + working-directory: ${{ github.workspace }} + run: | + java -jar \ + "$RUNNER_TEMP/vnu/node_modules/vnu-jar/build/dist/vnu.jar" \ + --errors-only "$RUNNER_TEMP/python-package-smoke/smoke.html" + + - name: Validate EPUB with the official W3C EPUBCheck + if: matrix.python-version == '3.14' + working-directory: ${{ github.workspace }} + run: | + java -jar "$RUNNER_TEMP/epubcheck-5.3.0/epubcheck.jar" \ + "$RUNNER_TEMP/python-package-smoke/smoke.epub" + + - name: Validate DOCX with the Open XML SDK + if: matrix.python-version == '3.14' + working-directory: ${{ github.workspace }} + run: | + dotnet run \ + --project nodejs/format-contracts/openxml-validator/OpenXmlValidator.csproj \ + -- "$RUNNER_TEMP/python-package-smoke/smoke.docx" + + - name: Open DOCX with LibreOffice Writer + if: matrix.python-version == '3.14' + working-directory: ${{ github.workspace }} + run: | + mkdir -p "$RUNNER_TEMP/libreoffice-output" + soffice \ + "-env:UserInstallation=file://$RUNNER_TEMP/libreoffice-profile" \ + --headless \ + --convert-to pdf \ + --outdir "$RUNNER_TEMP/libreoffice-output" \ + "$RUNNER_TEMP/python-package-smoke/smoke.docx" + test -s "$RUNNER_TEMP/libreoffice-output/smoke.pdf" + - name: Upload Python coverage if: matrix.python-version == '3.14' uses: actions/upload-artifact@v4 diff --git a/.github/workflows/release-pypi.yml b/.github/workflows/release-pypi.yml index 6064215..2965413 100644 --- a/.github/workflows/release-pypi.yml +++ b/.github/workflows/release-pypi.yml @@ -2,23 +2,98 @@ name: Publish illusion-markdown to PyPI on: workflow_dispatch: + inputs: + version: + description: Package version to publish (for example, 2.0.4) + required: true + type: string + release_sha: + description: Commit on main whose complete CI run succeeded + required: true + type: string push: tags: - "illusion-markdown-v*" +concurrency: + group: release-pypi-${{ inputs.version || github.ref_name }} + cancel-in-progress: false + permissions: actions: read contents: read +env: + EXPECTED_VERSION: ${{ inputs.version || github.ref_name }} + RELEASE_SHA: ${{ inputs.release_sha || github.sha }} + RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && format('illusion-markdown-v{0}', inputs.version) || github.ref_name }} + jobs: - verify-ci: - name: Verify complete CI gate + verify-release: + name: Verify version and complete CI gate runs-on: ubuntu-latest steps: - - env: + - uses: actions/checkout@v4 + with: + ref: ${{ env.RELEASE_SHA }} + + - uses: dtolnay/rust-toolchain@stable + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Resolve the expected version + shell: bash + run: | + if [[ "$GITHUB_EVENT_NAME" == "push" ]]; then + EXPECTED_VERSION=${EXPECTED_VERSION#illusion-markdown-v} + echo "EXPECTED_VERSION=$EXPECTED_VERSION" >> "$GITHUB_ENV" + fi + + if [[ ! "$EXPECTED_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Invalid package version: $EXPECTED_VERSION" >&2 + exit 1 + fi + + - name: Verify Python and Cargo package versions agree + run: | + python - <<'PY' + import os + import tomllib + from pathlib import Path + + expected = os.environ["EXPECTED_VERSION"] + manifests = { + "python/pyproject.toml": "project", + "python/Cargo.toml": "package", + } + for filename, table in manifests.items(): + actual = tomllib.loads(Path(filename).read_text())[table]["version"] + if actual != expected: + raise SystemExit(f"{filename} has version {actual}, expected {expected}") + PY + + cargo_version=$( + cargo metadata --locked --no-deps \ + --manifest-path python/Cargo.toml \ + --format-version 1 | + jq -r '.packages[] | select(.name == "illusion-markdown-python") | .version' + ) + test "$cargo_version" = "$EXPECTED_VERSION" + + - name: Verify the release commit passed the complete CI gate + env: GH_TOKEN: ${{ github.token }} - RELEASE_SHA: ${{ github.sha }} run: | + if [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]]; then + main_sha=$(gh api "repos/$GITHUB_REPOSITORY/git/ref/heads/main" --jq '.object.sha') + test "$RELEASE_SHA" = "$main_sha" + fi + + checked_out_sha=$(git rev-parse HEAD) + test "$checked_out_sha" = "$RELEASE_SHA" + count=$( gh api --method GET \ "repos/$GITHUB_REPOSITORY/actions/workflows/ci.yml/runs" \ @@ -29,45 +104,88 @@ jobs: ) test "$count" -gt 0 + - name: Verify the release tag is not attached to a different commit + env: + GH_TOKEN: ${{ github.token }} + run: | + tag_sha=$( + gh api "repos/$GITHUB_REPOSITORY/commits/$RELEASE_TAG" \ + --jq '.sha' 2>/dev/null || true + ) + if [[ -n "$tag_sha" ]]; then + test "$tag_sha" = "$RELEASE_SHA" + fi + build-wheels: - name: Build ${{ matrix.target }} wheel - needs: verify-ci + name: Build and test ${{ matrix.target }} wheel + needs: verify-release runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - os: ubuntu-latest - target: x86_64 + target: linux-x86_64 manylinux: "2014" - os: macos-15-intel - target: x86_64 + target: macos-x86_64 - os: macos-15 - target: aarch64 + target: macos-aarch64 - os: windows-latest - target: x86_64 + target: windows-x86_64 steps: - uses: actions/checkout@v4 + with: + ref: ${{ env.RELEASE_SHA }} + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Resolve the expected version + if: github.event_name == 'push' + shell: bash + run: echo "EXPECTED_VERSION=${EXPECTED_VERSION#illusion-markdown-v}" >> "$GITHUB_ENV" - name: Build wheel uses: PyO3/maturin-action@v1 with: command: build - args: --release --manifest-path python/Cargo.toml --out dist + args: --release --locked --manifest-path python/Cargo.toml --out dist manylinux: ${{ matrix.manylinux }} + - name: Install and smoke-test the wheel + shell: bash + run: | + python -m pip install --no-index --find-links dist "illusion-markdown==$EXPECTED_VERSION" + python python/tests/test_package_smoke.py \ + --expected-version "$EXPECTED_VERSION" + - uses: actions/upload-artifact@v4 with: - name: wheels-${{ matrix.os }}-${{ matrix.target }} + name: wheel-${{ matrix.target }} path: dist/*.whl if-no-files-found: error build-sdist: - name: Build source distribution - needs: verify-ci + name: Build and test source distribution + needs: verify-release runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + ref: ${{ env.RELEASE_SHA }} + + - uses: dtolnay/rust-toolchain@stable + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Resolve the expected version + if: github.event_name == 'push' + shell: bash + run: echo "EXPECTED_VERSION=${EXPECTED_VERSION#illusion-markdown-v}" >> "$GITHUB_ENV" - name: Build source distribution uses: PyO3/maturin-action@v1 @@ -75,25 +193,138 @@ jobs: command: sdist args: --manifest-path python/Cargo.toml --out dist + - name: Install and smoke-test the source distribution + run: | + python -m pip install dist/*.tar.gz + python python/tests/test_package_smoke.py \ + --expected-version "$EXPECTED_VERSION" + - uses: actions/upload-artifact@v4 with: name: source-distribution path: dist/*.tar.gz if-no-files-found: error + validate-formats: + name: Validate installed-package publication formats + needs: [verify-release, build-wheels] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ env.RELEASE_SHA }} + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "21" + + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: "8.0.x" + + - uses: actions/setup-node@v4 + with: + node-version: "22" + + - name: Resolve the expected version + if: github.event_name == 'push' + shell: bash + run: echo "EXPECTED_VERSION=${EXPECTED_VERSION#illusion-markdown-v}" >> "$GITHUB_ENV" + + - uses: actions/download-artifact@v4 + with: + name: wheel-linux-x86_64 + path: dist + + - name: Install the built wheel and authoritative consumers + run: | + python -m pip install --no-index --find-links dist \ + "illusion-markdown==$EXPECTED_VERSION" + sudo apt-get update + sudo apt-get install --yes libreoffice-writer + curl --fail --location --silent --show-error \ + https://github.com/w3c/epubcheck/releases/download/v5.3.0/epubcheck-5.3.0.zip \ + --output "$RUNNER_TEMP/epubcheck.zip" + unzip -q "$RUNNER_TEMP/epubcheck.zip" -d "$RUNNER_TEMP" + npm install --prefix "$RUNNER_TEMP/vnu" --no-save vnu-jar@25.12.31 + + - name: Generate every public format from the installed wheel + run: | + python python/tests/test_package_smoke.py \ + --expected-version "$EXPECTED_VERSION" \ + --output-dir "$RUNNER_TEMP/python-package-smoke" + + - name: Validate HTML with the Nu Html Checker + run: | + java -jar \ + "$RUNNER_TEMP/vnu/node_modules/vnu-jar/build/dist/vnu.jar" \ + --errors-only "$RUNNER_TEMP/python-package-smoke/smoke.html" + + - name: Validate EPUB with the official W3C EPUBCheck + run: | + java -jar "$RUNNER_TEMP/epubcheck-5.3.0/epubcheck.jar" \ + "$RUNNER_TEMP/python-package-smoke/smoke.epub" + + - name: Validate DOCX with the Open XML SDK + run: | + dotnet run \ + --project nodejs/format-contracts/openxml-validator/OpenXmlValidator.csproj \ + -- "$RUNNER_TEMP/python-package-smoke/smoke.docx" + + - name: Open DOCX with LibreOffice Writer + run: | + mkdir -p "$RUNNER_TEMP/libreoffice-output" + soffice \ + "-env:UserInstallation=file://$RUNNER_TEMP/libreoffice-profile" \ + --headless \ + --convert-to pdf \ + --outdir "$RUNNER_TEMP/libreoffice-output" \ + "$RUNNER_TEMP/python-package-smoke/smoke.docx" + test -s "$RUNNER_TEMP/libreoffice-output/smoke.pdf" + publish: - name: Publish to PyPI - needs: [build-wheels, build-sdist] + name: Publish to PyPI and create GitHub release + needs: [build-wheels, build-sdist, validate-formats] runs-on: ubuntu-latest environment: pypi permissions: + actions: read + contents: write id-token: write steps: + - name: Resolve the expected version + if: github.event_name == 'push' + shell: bash + run: echo "EXPECTED_VERSION=${EXPECTED_VERSION#illusion-markdown-v}" >> "$GITHUB_ENV" + - uses: actions/download-artifact@v4 with: pattern: "*" merge-multiple: true path: dist + - name: Check package distributions + run: | + python -m pip install --disable-pip-version-check twine + python -m twine check dist/* + test "$(find dist -maxdepth 1 -name '*.whl' | wc -l)" -eq 4 + test "$(find dist -maxdepth 1 -name '*.tar.gz' | wc -l)" -eq 1 + - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 + + - name: Create the GitHub release + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "$RELEASE_TAG" \ + --repo "$GITHUB_REPOSITORY" \ + --target "$RELEASE_SHA" \ + --title "illusion-markdown $EXPECTED_VERSION" \ + --generate-notes \ + dist/* diff --git a/.github/workflows/swift-release.yml b/.github/workflows/swift-release.yml index 7978904..2cfd471 100644 --- a/.github/workflows/swift-release.yml +++ b/.github/workflows/swift-release.yml @@ -9,15 +9,15 @@ on: type: choice options: [prepare, publish] version: - description: Complete SemVer version (for example, 2.0.1) + description: Complete SemVer version (for example, 2.0.3) required: true type: string prepare_run_id: description: Required for publish; the successful prepare workflow run ID required: false type: string - force_retag: - description: "Recovery only: replace an existing tag with the approved main commit" + recover_existing_tag: + description: "Recovery only: allow an unpublished tag that already points to the approved main commit" required: false default: false type: boolean @@ -28,7 +28,7 @@ permissions: pull-requests: write concurrency: - group: swift-release-${{ inputs.version }}-${{ inputs.action }} + group: swift-release-${{ inputs.version }} cancel-in-progress: false jobs: @@ -61,25 +61,51 @@ jobs: with: targets: aarch64-apple-darwin,x86_64-apple-darwin,aarch64-apple-ios,aarch64-apple-ios-sim,x86_64-apple-ios + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + - name: Validate version env: + GH_TOKEN: ${{ github.token }} VERSION: ${{ inputs.version }} - FORCE_RETAG: ${{ inputs.force_retag }} run: | [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] if git rev-parse --verify --quiet "refs/tags/$VERSION"; then - [[ "$FORCE_RETAG" == "true" ]] + echo "tag $VERSION already exists" >&2 + exit 1 fi + if gh release view "$VERSION" --repo "$GITHUB_REPOSITORY"; then + echo "release $VERSION already exists" >&2 + exit 1 + fi + + - name: Install the official W3C EPUBCheck validator + run: | + curl --fail --location --silent --show-error \ + https://github.com/w3c/epubcheck/releases/download/v5.3.0/epubcheck-5.3.0.zip \ + --output "$RUNNER_TEMP/epubcheck.zip" + ditto -x -k "$RUNNER_TEMP/epubcheck.zip" "$RUNNER_TEMP" - name: Build the Apple XCFramework run: scripts/package-swift-xcframework.sh "$RUNNER_TEMP/mdi-swift" - - name: Test against the local XCFramework + - name: Test the local XCFramework with the coverage gate + env: + MDI_SWIFT_SMOKE_OUTPUT: ${{ runner.temp }}/swift-package-smoke run: | mkdir -p swift/Artifacts ditto "$RUNNER_TEMP/mdi-swift/MDICore.xcframework" swift/Artifacts/MDICore.xcframework scripts/write-swift-package-manifest.sh local "swift/Artifacts/MDICore.xcframework" - swift test + swift test --enable-code-coverage + scripts/export-swift-coverage.sh "$RUNNER_TEMP/swift-coverage.lcov" + + - name: Open and validate every Swift package format + run: >- + scripts/verify-swift-package-formats.sh + "$RUNNER_TEMP/swift-package-smoke" + "$RUNNER_TEMP/epubcheck-5.3.0/epubcheck.jar" - name: Archive the XCFramework id: artifact @@ -141,15 +167,43 @@ jobs: - name: Validate approved release inputs env: + GH_TOKEN: ${{ github.token }} VERSION: ${{ inputs.version }} PREPARE_RUN_ID: ${{ inputs.prepare_run_id }} - FORCE_RETAG: ${{ inputs.force_retag }} + RECOVER_EXISTING_TAG: ${{ inputs.recover_existing_tag }} run: | [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] - [[ -n "$PREPARE_RUN_ID" ]] + [[ "$PREPARE_RUN_ID" =~ ^[0-9]+$ ]] + release_sha=$(git rev-parse HEAD) if git rev-parse --verify --quiet "refs/tags/$VERSION"; then - [[ "$FORCE_RETAG" == "true" ]] + [[ "$RECOVER_EXISTING_TAG" == "true" ]] + [[ "$(git rev-list -n 1 "$VERSION")" == "$release_sha" ]] + else + [[ "$RECOVER_EXISTING_TAG" == "false" ]] fi + if gh release view "$VERSION" --repo "$GITHUB_REPOSITORY"; then + echo "release $VERSION already exists" >&2 + exit 1 + fi + + - name: Verify the successful prepare run + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ inputs.version }} + PREPARE_RUN_ID: ${{ inputs.prepare_run_id }} + run: | + run_metadata=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$PREPARE_RUN_ID") + [[ "$(jq -r '.path' <<<"$run_metadata")" == ".github/workflows/swift-release.yml" ]] + [[ "$(jq -r '.event' <<<"$run_metadata")" == "workflow_dispatch" ]] + [[ "$(jq -r '.head_branch' <<<"$run_metadata")" == "main" ]] + [[ "$(jq -r '.conclusion' <<<"$run_metadata")" == "success" ]] + prepare_jobs=$( + gh api "repos/$GITHUB_REPOSITORY/actions/runs/$PREPARE_RUN_ID/jobs" \ + -f per_page=100 \ + --method GET \ + --jq "[.jobs[] | select(.name == \"Prepare MDI $VERSION\" and .conclusion == \"success\")] | length" + ) + [[ "$prepare_jobs" == "1" ]] - name: Download the approved XCFramework env: @@ -170,12 +224,8 @@ jobs: - name: Tag the approved main commit env: VERSION: ${{ inputs.version }} - FORCE_RETAG: ${{ inputs.force_retag }} run: | - if [[ "$FORCE_RETAG" == "true" ]]; then - git tag -f "$VERSION" - git push --force origin "refs/tags/$VERSION" - else + if ! git rev-parse --verify --quiet "refs/tags/$VERSION"; then git tag "$VERSION" git push origin "$VERSION" fi @@ -184,4 +234,4 @@ jobs: env: GH_TOKEN: ${{ github.token }} VERSION: ${{ inputs.version }} - run: gh release create "$VERSION" "$RUNNER_TEMP/release/MDICore.xcframework.zip#MDICore.xcframework.zip" --title "MDI $VERSION" --generate-notes + run: gh release create "$VERSION" "$RUNNER_TEMP/release/MDICore.xcframework.zip#MDICore.xcframework.zip" --verify-tag --title "MDI $VERSION" --generate-notes diff --git a/.gitignore b/.gitignore index 3461bf4..f9676ff 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ dist .turbo *.tsbuildinfo coverage +.coverage +/output/ +/tmp/ .DS_Store *.log target/ diff --git a/Package.swift b/Package.swift index 2980203..d302e8e 100644 --- a/Package.swift +++ b/Package.swift @@ -10,6 +10,6 @@ let package = Package( targets: [ .binaryTarget(name: "MDICore", url: "https://github.com/illusions-lab/MDI/releases/download/2.0.2/MDICore.xcframework.zip", checksum: "d1fd0fedd6cb5464edbd193efac60052589dc0a71cef380e64f15197c319fc89"), .target(name: "MDI", dependencies: ["MDICore"], path: "swift/Sources/MDI"), - .testTarget(name: "MDITests", dependencies: ["MDI"], path: "swift/Tests/MDITests"), + .testTarget(name: "MDITests", dependencies: ["MDI", "MDICore"], path: "swift/Tests/MDITests"), ] ) diff --git a/docs/mdi-2.0-announcement.md b/docs/mdi-2.0-announcement.md index 218eb54..fa23c85 100644 --- a/docs/mdi-2.0-announcement.md +++ b/docs/mdi-2.0-announcement.md @@ -144,8 +144,8 @@ MDI 固有の記法は導入せず、GFM / Pandoc の脚注記法をそのまま | フレーバー | 用途 | |-----------|------| -| `plain` | ルビを捨てた素のテキスト | -| `ruby-paren` | ルビを全角括弧で表現: `漢字(かんじ)` | +| `txt` | ルビを捨てた素のテキスト | +| `txt-ruby` | ルビを再解析可能な MDI 形式で保持: `{漢字\|かんじ}` | | `narou` | 小説家になろう投稿用(傍点は圏点ルビで表現) | | `kakuyomu` | カクヨム投稿用(傍点は `《《》》` 記法) | | `aozora` | 青空文庫注記形式 | diff --git a/docs/src/content/docs/bindings/swift.md b/docs/src/content/docs/bindings/swift.md index 56daf0e..1c7d7a9 100644 --- a/docs/src/content/docs/bindings/swift.md +++ b/docs/src/content/docs/bindings/swift.md @@ -21,7 +21,7 @@ that uses it: ```swift dependencies: [ - .package(url: "https://github.com/illusions-lab/MDI.git", from: "2.0.2"), + .package(url: "https://github.com/illusions-lab/MDI.git", from: "2.0.3"), ] // In a target: @@ -55,11 +55,17 @@ All renderers take MDI source text: let html = try MDI.renderHTML("{東京|とうきょう} ^12^") let mdi = try MDI.serialize("{東京|とうきょう} ^12^") let text = try MDI.renderText("# Title") +let note = try MDI.renderTextFormat( + "# Title\n\n{東京|とうきょう}", + format: .note +) let epub: Data = try MDI.renderEPUB("# Chapter") let docx: Data = try MDI.renderDOCX("# Chapter") ``` +`MDITextFormat` exposes the same six Rust-owned conventions as the other +bindings: `plain`, `ruby`, `narou`, `kakuyomu`, `aozora`, and `note`. `renderEPUB` and `renderDOCX` return ZIP-based `Data`; write the data to a file with the appropriate extension. @@ -83,7 +89,7 @@ do { ## Development and releases The repository's `swift/Package.swift` is the local development package. CI -builds an XCFramework, runs XCTest with a 90% line-coverage gate for +builds an XCFramework, runs XCTest with a 95% line-coverage gate for `swift/Sources/MDI`, and uploads the report to Codecov. The release workflow creates a manifest pull request and publishes the approved artifact after that PR is merged. It uses GitHub Actions' built-in token; no PAT or second diff --git a/docs/src/content/docs/ja/bindings/swift.md b/docs/src/content/docs/ja/bindings/swift.md index 33363a4..6d19dcf 100644 --- a/docs/src/content/docs/ja/bindings/swift.md +++ b/docs/src/content/docs/ja/bindings/swift.md @@ -17,7 +17,7 @@ Swift は小さな C ABI を通じて Rust の `mdi-core` に解析とレンダ ```swift dependencies: [ - .package(url: "https://github.com/illusions-lab/MDI.git", from: "2.0.2"), + .package(url: "https://github.com/illusions-lab/MDI.git", from: "2.0.3"), ] // target の dependencies: @@ -43,15 +43,21 @@ print(result.diagnostics) let html = try MDI.renderHTML("{東京|とうきょう} ^12^") let mdi = try MDI.serialize("{東京|とうきょう} ^12^") let text = try MDI.renderText("# Title") +let note = try MDI.renderTextFormat( + "# Title\n\n{東京|とうきょう}", + format: .note +) let epub: Data = try MDI.renderEPUB("# Chapter") let docx: Data = try MDI.renderDOCX("# Chapter") ``` +`MDITextFormat` は他の binding と同じ6種類(`plain`、`ruby`、`narou`、 +`kakuyomu`、`aozora`、`note`)を Rust core から提供します。 EPUB と DOCX は ZIP ベースの `Data` を返すため、対応する拡張子でファイルへ書き出してください。 ## エラー -すべての API は `MDIError` を throw します。`core` は Rust core の失敗、`invalidWireFormat` は無効または未対応の native response を表します。CI は XCFramework をビルドし、XCTest を実行して `swift/Sources/MDI` に 90% の line coverage を要求し、Codecov へ送信します。PAT や別リポジトリは必要ありません。 +すべての API は `MDIError` を throw します。`core` は Rust core の失敗、`invalidWireFormat` は無効または未対応の native response を表します。 ```swift do { @@ -64,4 +70,4 @@ do { ## 開発とリリース -リポジトリの `swift/Package.swift` はローカル開発用パッケージです。CI は XCFramework をビルドし、XCTest を実行して `swift/Sources/MDI` に 90% の line-coverage gate を適用し、レポートを Codecov に送信します。release workflow は manifest 用の pull request を作成し、その PR がマージされた後に承認済み artifact を公開します。GitHub Actions 組み込みの token を使うため、PAT や別リポジトリは不要です。 +リポジトリの `swift/Package.swift` はローカル開発用パッケージです。CI は XCFramework をビルドし、XCTest を実行して `swift/Sources/MDI` に 95% の line-coverage gate を適用し、レポートを Codecov に送信します。release workflow は manifest 用の pull request を作成し、その PR がマージされた後に承認済み artifact を公開します。GitHub Actions 組み込みの token を使うため、PAT や別リポジトリは不要です。 diff --git a/docs/src/content/docs/zh-tw/bindings/swift.md b/docs/src/content/docs/zh-tw/bindings/swift.md index d92bfdb..b97bd43 100644 --- a/docs/src/content/docs/zh-tw/bindings/swift.md +++ b/docs/src/content/docs/zh-tw/bindings/swift.md @@ -17,7 +17,7 @@ Swift 會透過精簡的 C ABI 將解析和轉譯交給 Rust 的 `mdi-core`, ```swift dependencies: [ - .package(url: "https://github.com/illusions-lab/MDI.git", from: "2.0.2"), + .package(url: "https://github.com/illusions-lab/MDI.git", from: "2.0.3"), ] // target 的 dependencies: @@ -43,10 +43,16 @@ print(result.diagnostics) let html = try MDI.renderHTML("{東京|とうきょう} ^12^") let mdi = try MDI.serialize("{東京|とうきょう} ^12^") let text = try MDI.renderText("# Title") +let note = try MDI.renderTextFormat( + "# Title\n\n{東京|とうきょう}", + format: .note +) let epub: Data = try MDI.renderEPUB("# Chapter") let docx: Data = try MDI.renderDOCX("# Chapter") ``` +`MDITextFormat` 提供與其他綁定相同的六種 Rust 格式:`plain`、`ruby`、 +`narou`、`kakuyomu`、`aozora` 與 `note`。 EPUB 與 DOCX 回傳 ZIP 格式的 `Data`,請以對應副檔名寫入檔案。 ## 錯誤 @@ -64,4 +70,4 @@ do { ## 開發與發布 -repository 的 `swift/Package.swift` 是本機開發用 package。CI 會建置 XCFramework、跑 XCTest,並對 `swift/Sources/MDI` 強制 90% line coverage、上傳 Codecov。release workflow 會建立 manifest pull request;該 PR 合併後才發布核准的 artifact。它使用 GitHub Actions 內建 token,不需要 PAT 或第二個 repository。 +repository 的 `swift/Package.swift` 是本機開發用 package。CI 會建置 XCFramework、跑 XCTest,並對 `swift/Sources/MDI` 強制 95% line coverage、上傳 Codecov。release workflow 會建立 manifest pull request;該 PR 合併後才發布核准的 artifact。它使用 GitHub Actions 內建 token,不需要 PAT 或第二個 repository。 diff --git a/mdi-android-jni/Cargo.lock b/mdi-android-jni/Cargo.lock index 139b936..d942171 100644 --- a/mdi-android-jni/Cargo.lock +++ b/mdi-android-jni/Cargo.lock @@ -205,7 +205,7 @@ dependencies = [ [[package]] name = "mdi-core" -version = "2.0.4" +version = "2.0.5" dependencies = [ "markdown", "serde", diff --git a/mdi-core/Cargo.lock b/mdi-core/Cargo.lock index 8817aad..0a47e78 100644 --- a/mdi-core/Cargo.lock +++ b/mdi-core/Cargo.lock @@ -166,7 +166,7 @@ dependencies = [ [[package]] name = "mdi-core" -version = "2.0.4" +version = "2.0.5" dependencies = [ "js-sys", "markdown", diff --git a/mdi-core/Cargo.toml b/mdi-core/Cargo.toml index 7e3dda0..69cd832 100644 --- a/mdi-core/Cargo.toml +++ b/mdi-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mdi-core" -version = "2.0.4" +version = "2.0.5" description = "Language-neutral parser for MDI 2.0 syntax" edition = "2024" rust-version = "1.85" diff --git a/python/Cargo.lock b/python/Cargo.lock index 760f659..a6d45a0 100644 --- a/python/Cargo.lock +++ b/python/Cargo.lock @@ -111,7 +111,7 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "illusion-markdown-python" -version = "2.0.3" +version = "2.0.4" dependencies = [ "mdi-core", "pyo3", @@ -166,7 +166,7 @@ dependencies = [ [[package]] name = "mdi-core" -version = "2.0.4" +version = "2.0.5" dependencies = [ "markdown", "serde", diff --git a/python/Cargo.toml b/python/Cargo.toml index de31c55..9341e48 100644 --- a/python/Cargo.toml +++ b/python/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "illusion-markdown-python" -version = "2.0.3" +version = "2.0.4" description = "PyO3 bindings for illusion Markdown (MDI)" edition = "2024" license = "MIT" @@ -8,6 +8,8 @@ publish = false readme = "README.md" repository = "https://github.com/illusions-lab/MDI" exclude = [ + ".pytest_cache/**", + "coverage*.xml", "src/mdi/_native.abi3.so", "src/**/__pycache__/**", "tests/**/__pycache__/**", diff --git a/python/pyproject.toml b/python/pyproject.toml index 90ac754..a13cbcd 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "illusion-markdown" -version = "2.0.3" +version = "2.0.4" description = "Python bindings for illusion Markdown (MDI), backed by the Rust core." readme = "README.md" requires-python = ">=3.10" @@ -16,6 +16,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Programming Language :: Rust", "Topic :: Text Processing :: Markup", ] diff --git a/python/tests/test_mdi.py b/python/tests/test_mdi.py index faa05af..9c23e34 100644 --- a/python/tests/test_mdi.py +++ b/python/tests/test_mdi.py @@ -82,6 +82,29 @@ def test_returns_recoverable_diagnostics_with_utf8_byte_spans() -> None: assert result["document"]["span"]["endByte"] == len(source.encode()) +@pytest.mark.parametrize( + "source", + [ + "\\{}《《傍点》》\n\n\\[{東京|とう.きょう}", + "👨👩👧 [[em:**強調**]] [^n]\n\n[^n]: 注", + "[[indent:2]]\n{𠮟る|しか.る} [[no-break:^12^]]\n\n[[pagebreak:left]]", + "```mdi\n{東京|とうきょう}\n```\n\n| a | b |\n| - | - |\n| [[em:x]] | ^12^ |", + ], +) +def test_keeps_the_js_adversarial_corpus_inside_the_rust_wire_contract( + source: str, +) -> None: + result = mdi.parse(source) + + assert result["document"]["span"] == { + "startByte": 0, + "endByte": len(source.encode()), + } + assert_valid_spans(result["document"], source) + assert mdi.render_html(source).startswith("") + assert mdi.parse(mdi.serialize_mdi(source))["irVersion"] == mdi.MDI_IR_VERSION + + def test_serializes_and_renders_complete_source_in_rust() -> None: source = "# 題\n\n{東京|とうきょう} ^12^" assert "