diff --git a/.changepacks/changepack_log_s_nU5lSdph23oTfBwfu8w.json b/.changepacks/changepack_log_s_nU5lSdph23oTfBwfu8w.json new file mode 100644 index 0000000..5a24225 --- /dev/null +++ b/.changepacks/changepack_log_s_nU5lSdph23oTfBwfu8w.json @@ -0,0 +1 @@ +{"changes":{"packages/dotnet/BraillifyNet/BraillifyNet.csproj":"Major","packages/dotnet/Braillify/Braillify.csproj":"Major"},"note":"Add C#","date":"2026-01-13T04:17:13.750635400Z"} \ No newline at end of file diff --git a/.changepacks/config.json b/.changepacks/config.json index 173694f..be70799 100644 --- a/.changepacks/config.json +++ b/.changepacks/config.json @@ -1,5 +1,5 @@ { - "ignore": ["**", "!packages/python/pyproject.toml", "!packages/node/package.json", "!libs/braillify/Cargo.toml"], + "ignore": ["**", "!packages/python/pyproject.toml", "!packages/dotnet/BraillifyNet/BraillifyNet.csproj", "!packages/dotnet/Braillify/Braillify.csproj", "!packages/node/package.json", "!libs/braillify/Cargo.toml"], "baseBranch": "main", "latestPackage": null, "publish": {} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f14e459..31bd9af 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -430,7 +430,7 @@ jobs: run: cargo build --release --target ${{ matrix.target }} -p braillify - name: Rename binary run: mv target/${{ matrix.target }}/release/braillify${{ matrix.os == 'windows' && '.exe' || '' }} ${{ matrix.binary_name }} - + - name: Upload Asset uses: owjs3901/upload-github-release-asset@main with: @@ -478,3 +478,144 @@ jobs: token: ${{ secrets.WINGET_TOKEN }} version: ${{ steps.version.outputs.version }} + # dotnet + dotnet-build: + name: .NET Build - ${{ matrix.rid }} + runs-on: ${{ matrix.runner }} + if: ${{ contains(needs.changepacks.outputs.changepacks, 'packages/dotnet') }} + needs: + - test + - changepacks + strategy: + fail-fast: false + matrix: + include: + # Windows + - runner: windows-latest + target: x86_64-pc-windows-msvc + rid: win-x64 + lib_name: braillify_native.dll + - runner: windows-latest + target: i686-pc-windows-msvc + rid: win-x86 + lib_name: braillify_native.dll + - runner: windows-latest + target: aarch64-pc-windows-msvc + rid: win-arm64 + lib_name: braillify_native.dll + # Linux + - runner: ubuntu-22.04 + target: x86_64-unknown-linux-gnu + rid: linux-x64 + lib_name: libbraillify_native.so + - runner: ubuntu-22.04 + target: aarch64-unknown-linux-gnu + rid: linux-arm64 + lib_name: libbraillify_native.so + # macOS + - runner: macos-13 + target: x86_64-apple-darwin + rid: osx-x64 + lib_name: libbraillify_native.dylib + - runner: macos-14 + target: aarch64-apple-darwin + rid: osx-arm64 + lib_name: libbraillify_native.dylib + steps: + - uses: actions/checkout@v5 + + - name: Setup Rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.target }} + + - name: Install cross-compilation tools (Linux ARM64) + if: matrix.rid == 'linux-arm64' + run: | + sudo apt-get update + sudo apt-get install -y gcc-aarch64-linux-gnu + + - name: Build native library + run: cargo build --release --target ${{ matrix.target }} -p dotnet + env: + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: dotnet-native-${{ matrix.rid }} + path: target/${{ matrix.target }}/release/${{ matrix.lib_name }} + if-no-files-found: error + + dotnet-publish: + name: .NET Publish + runs-on: ubuntu-latest + if: ${{ contains(needs.changepacks.outputs.changepacks, 'packages/dotnet') }} + needs: + - changepacks + - dotnet-build + permissions: + contents: write + id-token: write + steps: + - uses: actions/checkout@v5 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '9.0.x' + + - name: Download all native artifacts + uses: actions/download-artifact@v4 + with: + path: native-artifacts + pattern: dotnet-native-* + + - name: Prepare native libraries + run: | + # Create runtimes directories + mkdir -p packages/dotnet/BraillifyNet/runtimes/win-x64/native + mkdir -p packages/dotnet/BraillifyNet/runtimes/win-x86/native + mkdir -p packages/dotnet/BraillifyNet/runtimes/win-arm64/native + mkdir -p packages/dotnet/BraillifyNet/runtimes/linux-x64/native + mkdir -p packages/dotnet/BraillifyNet/runtimes/linux-arm64/native + mkdir -p packages/dotnet/BraillifyNet/runtimes/osx-x64/native + mkdir -p packages/dotnet/BraillifyNet/runtimes/osx-arm64/native + + # Copy native libraries to runtimes folders + cp native-artifacts/dotnet-native-win-x64/* packages/dotnet/BraillifyNet/runtimes/win-x64/native/ + cp native-artifacts/dotnet-native-win-x86/* packages/dotnet/BraillifyNet/runtimes/win-x86/native/ + cp native-artifacts/dotnet-native-win-arm64/* packages/dotnet/BraillifyNet/runtimes/win-arm64/native/ + cp native-artifacts/dotnet-native-linux-x64/* packages/dotnet/BraillifyNet/runtimes/linux-x64/native/ + cp native-artifacts/dotnet-native-linux-arm64/* packages/dotnet/BraillifyNet/runtimes/linux-arm64/native/ + cp native-artifacts/dotnet-native-osx-x64/* packages/dotnet/BraillifyNet/runtimes/osx-x64/native/ + cp native-artifacts/dotnet-native-osx-arm64/* packages/dotnet/BraillifyNet/runtimes/osx-arm64/native/ + + # List for verification + echo "=== Native libraries ===" + find packages/dotnet/BraillifyNet/runtimes -type f + + - name: Build and Pack BraillifyNet + run: dotnet pack -c Release -o nupkg + working-directory: packages/dotnet/BraillifyNet + + - name: Build and Pack Braillify CLI + run: dotnet pack -c Release -o nupkg + working-directory: packages/dotnet/Braillify + + - name: List packages + run: | + echo "=== NuGet packages ===" + find packages/dotnet -name "*.nupkg" -type f + + - name: Publish to NuGet + run: | + dotnet nuget push packages/dotnet/BraillifyNet/nupkg/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate + dotnet nuget push packages/dotnet/Braillify/nupkg/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate + + - name: Upload NuGet packages as artifacts + uses: actions/upload-artifact@v4 + with: + name: nuget-packages + path: packages/dotnet/**/nupkg/*.nupkg diff --git a/Cargo.lock b/Cargo.lock index 1b4a71d..5953eb3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -260,6 +260,13 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" +[[package]] +name = "dotnet" +version = "0.1.0" +dependencies = [ + "braillify", +] + [[package]] name = "embed-manifest" version = "1.5.0" diff --git a/bun.lock b/bun.lock index 2595365..e1bceef 100644 --- a/bun.lock +++ b/bun.lock @@ -19,14 +19,14 @@ "version": "0.1.0", "dependencies": { "@devup-ui/components": "^0.1.36", - "@devup-ui/react": "^1.0.27", + "@devup-ui/react": "^1.0.28", "@mdx-js/loader": "^3.1.1", "@mdx-js/react": "^3.1.1", "@next/mdx": "^16.0.3", "@types/mdx": "^2.0.13", "braillify": "workspace:*", "katex": "^0.16.25", - "next": "16.0.3", + "next": "16.0.10", "react": "^19.2.0", "react-dom": "^19.2.0", "react-latex-next": "^3.0.0", @@ -44,7 +44,7 @@ }, "packages/node": { "name": "braillify", - "version": "1.0.2", + "version": "1.0.10", }, }, "overrides": { @@ -251,25 +251,25 @@ "@mdx-js/react": ["@mdx-js/react@3.1.1", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="], - "@next/env": ["@next/env@16.0.3", "", {}, "sha512-IqgtY5Vwsm14mm/nmQaRMmywCU+yyMIYfk3/MHZ2ZTJvwVbBn3usZnjMi1GacrMVzVcAxJShTCpZlPs26EdEjQ=="], + "@next/env": ["@next/env@16.0.10", "", {}, "sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang=="], "@next/mdx": ["@next/mdx@16.0.3", "", { "dependencies": { "source-map": "^0.7.0" }, "peerDependencies": { "@mdx-js/loader": ">=0.15.0", "@mdx-js/react": ">=0.15.0" }, "optionalPeers": ["@mdx-js/loader", "@mdx-js/react"] }, "sha512-uVl2JSEGAjBV+EVnpt1cZN88SK3lJ2n7Fc+iqTsgVx2g9+Y6ru+P6nuUgXd38OHPUIwzL6k2V1u4iV3kwuTySQ=="], - "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MOnbd92+OByu0p6QBAzq1ahVWzF6nyfiH07dQDez4/Nku7G249NjxDVyEfVhz8WkLiOEU+KFVnqtgcsfP2nLXg=="], + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.0.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4XgdKtdVsaflErz+B5XeG0T5PeXKDdruDf3CRpnhN+8UebNa5N2H58+3GDgpn/9GBurrQ1uWW768FfscwYkJRg=="], - "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-i70C4O1VmbTivYdRlk+5lj9xRc2BlK3oUikt3yJeHT1unL4LsNtN7UiOhVanFdc7vDAgZn1tV/9mQwMkWOJvHg=="], + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.0.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-spbEObMvRKkQ3CkYVOME+ocPDFo5UqHb8EMTS78/0mQ+O1nqE8toHJVioZo4TvebATxgA8XMTHHrScPrn68OGw=="], - "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-O88gCZ95sScwD00mn/AtalyCoykhhlokxH/wi1huFK+rmiP5LAYVs/i2ruk7xST6SuXN4NI5y4Xf5vepb2jf6A=="], + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.0.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-uQtWE3X0iGB8apTIskOMi2w/MKONrPOUCi5yLO+v3O8Mb5c7K4Q5KD1jvTpTF5gJKa3VH/ijKjKUq9O9UhwOYw=="], - "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-CEErFt78S/zYXzFIiv18iQCbRbLgBluS8z1TNDQoyPi8/Jr5qhR3e8XHAIxVxPBjDbEMITprqELVc5KTfFj0gg=="], + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.0.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-llA+hiDTrYvyWI21Z0L1GiXwjQaanPVQQwru5peOgtooeJ8qx3tlqRV2P7uH2pKQaUfHxI/WVarvI5oYgGxaTw=="], - "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Tc3i+nwt6mQ+Dwzcri/WNDj56iWdycGVh5YwwklleClzPzz7UpfaMw1ci7bLl6GRYMXhWDBfe707EXNjKtiswQ=="], + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.0.10", "", { "os": "linux", "cpu": "x64" }, "sha512-AK2q5H0+a9nsXbeZ3FZdMtbtu9jxW4R/NgzZ6+lrTm3d6Zb7jYrWcgjcpM1k8uuqlSy4xIyPR2YiuUr+wXsavA=="], - "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-zTh03Z/5PBBPdTurgEtr6nY0vI9KR9Ifp/jZCcHlODzwVOEKcKRBtQIGrkc7izFgOMuXDEJBmirwpGqdM/ZixA=="], + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.0.10", "", { "os": "linux", "cpu": "x64" }, "sha512-1TDG9PDKivNw5550S111gsO4RGennLVl9cipPhtkXIFVwo31YZ73nEbLjNC8qG3SgTz/QZyYyaFYMeY4BKZR/g=="], - "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.0.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-Jc1EHxtZovcJcg5zU43X3tuqzl/sS+CmLgjRP28ZT4vk869Ncm2NoF8qSTaL99gh6uOzgM99Shct06pSO6kA6g=="], + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.0.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-aEZIS4Hh32xdJQbHz121pyuVZniSNoqDVx1yIr2hy+ZwJGipeqnMZBJHyMxv2tiuAXGx6/xpTcQJ6btIiBjgmg=="], - "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-N7EJ6zbxgIYpI/sWNzpVKRMbfEGgsWuOIvzkML7wxAAZhPk1Msxuo/JDu1PKjWGrAoOLaZcIX5s+/pF5LIbBBg=="], + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.0.10", "", { "os": "win32", "cpu": "x64" }, "sha512-E+njfCoFLb01RAFEnGZn6ERoOqhK1Gl3Lfz1Kjnj0Ulfu7oJbuMyvBKNj/bw8XZnenHDASlygTjZICQW+rYW1Q=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -985,7 +985,7 @@ "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - "next": ["next@16.0.3", "", { "dependencies": { "@next/env": "16.0.3", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.0.3", "@next/swc-darwin-x64": "16.0.3", "@next/swc-linux-arm64-gnu": "16.0.3", "@next/swc-linux-arm64-musl": "16.0.3", "@next/swc-linux-x64-gnu": "16.0.3", "@next/swc-linux-x64-musl": "16.0.3", "@next/swc-win32-arm64-msvc": "16.0.3", "@next/swc-win32-x64-msvc": "16.0.3", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-Ka0/iNBblPFcIubTA1Jjh6gvwqfjrGq1Y2MTI5lbjeLIAfmC+p5bQmojpRZqgHHVu5cG4+qdIiwXiBSm/8lZ3w=="], + "next": ["next@16.0.10", "", { "dependencies": { "@next/env": "16.0.10", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.0.10", "@next/swc-darwin-x64": "16.0.10", "@next/swc-linux-arm64-gnu": "16.0.10", "@next/swc-linux-arm64-musl": "16.0.10", "@next/swc-linux-x64-gnu": "16.0.10", "@next/swc-linux-x64-musl": "16.0.10", "@next/swc-win32-arm64-msvc": "16.0.10", "@next/swc-win32-x64-msvc": "16.0.10", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA=="], "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -1339,6 +1339,8 @@ "@devup-ui/components/@devup-ui/react": ["@devup-ui/react@1.0.27", "", { "dependencies": { "csstype-extra": "latest", "react": "^19.2" } }, "sha512-k0puvfD4UNyGGwWSByco36QcsfplqRnL4zxfkIMdj03FSFGop2bRaMAXUczWDL6VbSQ8k9WFO5Sn/vfdaTHoEA=="], + "@devup-ui/next-plugin/next": ["next@16.0.3", "", { "dependencies": { "@next/env": "16.0.3", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.0.3", "@next/swc-darwin-x64": "16.0.3", "@next/swc-linux-arm64-gnu": "16.0.3", "@next/swc-linux-arm64-musl": "16.0.3", "@next/swc-linux-x64-gnu": "16.0.3", "@next/swc-linux-x64-musl": "16.0.3", "@next/swc-win32-arm64-msvc": "16.0.3", "@next/swc-win32-x64-msvc": "16.0.3", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-Ka0/iNBblPFcIubTA1Jjh6gvwqfjrGq1Y2MTI5lbjeLIAfmC+p5bQmojpRZqgHHVu5cG4+qdIiwXiBSm/8lZ3w=="], + "@devup-ui/webpack-plugin/@devup-ui/wasm": ["@devup-ui/wasm@1.0.47", "", {}, "sha512-RPktfdg53bK5BqAyhfs9hA5vzAiH0D63w60S+ACaoIPXpqQaQp2Lh9pl3Mi6E+8KA0Div/hoQCLfYxuAefodrg=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], @@ -1415,6 +1417,24 @@ "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@devup-ui/next-plugin/next/@next/env": ["@next/env@16.0.3", "", {}, "sha512-IqgtY5Vwsm14mm/nmQaRMmywCU+yyMIYfk3/MHZ2ZTJvwVbBn3usZnjMi1GacrMVzVcAxJShTCpZlPs26EdEjQ=="], + + "@devup-ui/next-plugin/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MOnbd92+OByu0p6QBAzq1ahVWzF6nyfiH07dQDez4/Nku7G249NjxDVyEfVhz8WkLiOEU+KFVnqtgcsfP2nLXg=="], + + "@devup-ui/next-plugin/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-i70C4O1VmbTivYdRlk+5lj9xRc2BlK3oUikt3yJeHT1unL4LsNtN7UiOhVanFdc7vDAgZn1tV/9mQwMkWOJvHg=="], + + "@devup-ui/next-plugin/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-O88gCZ95sScwD00mn/AtalyCoykhhlokxH/wi1huFK+rmiP5LAYVs/i2ruk7xST6SuXN4NI5y4Xf5vepb2jf6A=="], + + "@devup-ui/next-plugin/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-CEErFt78S/zYXzFIiv18iQCbRbLgBluS8z1TNDQoyPi8/Jr5qhR3e8XHAIxVxPBjDbEMITprqELVc5KTfFj0gg=="], + + "@devup-ui/next-plugin/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Tc3i+nwt6mQ+Dwzcri/WNDj56iWdycGVh5YwwklleClzPzz7UpfaMw1ci7bLl6GRYMXhWDBfe707EXNjKtiswQ=="], + + "@devup-ui/next-plugin/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-zTh03Z/5PBBPdTurgEtr6nY0vI9KR9Ifp/jZCcHlODzwVOEKcKRBtQIGrkc7izFgOMuXDEJBmirwpGqdM/ZixA=="], + + "@devup-ui/next-plugin/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.0.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-Jc1EHxtZovcJcg5zU43X3tuqzl/sS+CmLgjRP28ZT4vk869Ncm2NoF8qSTaL99gh6uOzgM99Shct06pSO6kA6g=="], + + "@devup-ui/next-plugin/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-N7EJ6zbxgIYpI/sWNzpVKRMbfEGgsWuOIvzkML7wxAAZhPk1Msxuo/JDu1PKjWGrAoOLaZcIX5s+/pF5LIbBBg=="], + "@npmcli/git/which/isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="], "@npmcli/map-workspaces/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], diff --git a/packages/dotnet/.claude/CLAUDE.md b/packages/dotnet/.claude/CLAUDE.md new file mode 100644 index 0000000..6dea4bb --- /dev/null +++ b/packages/dotnet/.claude/CLAUDE.md @@ -0,0 +1,206 @@ +# CLAUDE.md + +이 파일은 Claude Code (claude.ai/code)가 이 저장소의 코드를 작업할 때 참고하는 가이드입니다. + +## 프로젝트 개요 + +Braillify는 2024 한글 점자 규정을 기반으로 한 한국어 점역 라이브러리입니다. 핵심 라이브러리는 Rust로 작성되었으며, Node.js (WebAssembly), Python (PyO3), .NET (FFI/P/Invoke) 바인딩을 제공합니다. + +## 빌드 및 개발 명령어 + +```bash +# 의존성 설치 (uv sync, wasm-pack, maturin 포함) +bun install + +# 모든 패키지 빌드 (Rust, Node.js WASM, Python) +bun run build + +# 랜딩 페이지 빌드 (build + test_by_testcase + Next.js build) +bun run build:landing + +# 모든 테스트 실행 (Rust tarpaulin, Vitest, Python pytest) +bun run test + +# 린트 +bun run lint +bun run lint:fix +``` + +### 개별 패키지 명령어 + +```bash +# Node.js WASM 패키지 +cd packages/node && wasm-pack build --target bundler --out-dir ./pkg --out-name index + +# Python 패키지 +cd packages/python && maturin build --release --out dist + +# Rust 테스트 +cargo test + +# Rust 테스트 (커버리지 포함) +cargo tarpaulin --out xml --out stdout + +# 특정 Rust 테스트 실행 (test_by_testcase는 test_cases/ 내 모든 CSV 파일 읽음) +cargo test test_by_testcase + +# Node.js 테스트 +vitest test --run + +# Python 테스트 +cd py-test && uv run pytest __init__.py +``` + +### .NET 패키지 명령어 + +```bash +# 네이티브 라이브러리 빌드 +bun run build:dotnet-native +# 또는: cargo build --release -p dotnet + +# .NET 패키지 빌드 +bun run build:dotnet +# 또는: cd packages/dotnet && dotnet build -c Release + +# .NET 테스트 (모든 프레임워크) +bun run test:dotnet +# 또는: cd packages/dotnet && dotnet test + +# 특정 프레임워크 테스트 +bun run test:dotnet:net5.0 +bun run test:dotnet:net6.0 +bun run test:dotnet:net7.0 +bun run test:dotnet:net8.0 +bun run test:dotnet:net9.0 +bun run test:dotnet:net10.0 +bun run test:dotnet:net472 +``` + +## 아키텍처 + +``` +braillify/ +├── libs/braillify/ # 핵심 Rust 라이브러리 +│ └── src/ +│ ├── lib.rs # 메인 인코더 (Encoder 구조체, encode/encode_to_unicode) +│ ├── cli.rs # CLI 구현 +│ ├── korean_char.rs # 한글 문자 인코딩 +│ ├── jauem/ # 자음 처리 +│ ├── moeum/ # 모음 처리 +│ ├── english.rs # 영문자 인코딩 +│ ├── number.rs # 숫자 인코딩 +│ ├── symbol_shortcut.rs # 기호 매핑 +│ ├── word_shortcut.rs # 약어 매핑 +│ ├── rule.rs # 한글 점자 규정 +│ ├── rule_en.rs # 영문 점자 규정 +│ └── fraction.rs # 분수 처리 (LaTeX, Unicode) +├── packages/ +│ ├── node/ # Node.js WASM 바인딩 (wasm-bindgen) +│ │ └── src/lib.rs # 노출: encode, translateToUnicode, translateToBrailleFont +│ ├── python/ # Python 바인딩 (PyO3/maturin) +│ │ └── src/lib.rs # 노출: encode, translate_to_unicode, translate_to_braille_font, cli +│ └── dotnet/ # .NET 바인딩 (FFI/P/Invoke) +│ ├── Cargo.toml # Rust FFI 크레이트 설정 +│ ├── src/lib.rs # C ABI 노출 함수 (braillify_encode, braillify_encode_to_unicode 등) +│ ├── BraillifyNet/ # .NET 클래스 라이브러리 (NuGet: BraillifyNet) +│ │ ├── Braillify.cs # 공개 API (Encode, EncodeToUnicode, EncodeToBrailleFont) +│ │ ├── NativeMethods.cs # P/Invoke 선언 (버전별 전처리) +│ │ ├── NativeLibraryLoader.cs # 플랫폼별 네이티브 라이브러리 로딩 +│ │ └── BraillifyException.cs # 예외 클래스 +│ ├── Braillify/ # CLI 도구 (NuGet: Braillify, 명령어: braillify) +│ │ └── Program.cs # System.CommandLine 기반 CLI +│ ├── Braillify.Tests/ # xUnit 테스트 (멀티타겟팅) +│ └── Braillify.Tests.NetFramework/ # MSTest (.NET Framework 4.7.2) +├── apps/landing/ # Next.js 랜딩 페이지 (braillify.com) +├── test_cases/ # CSV 테스트 파일 (rule_1.csv ~ rule_63.csv) +└── py-test/ # Python 테스트 스위트 +``` + +## 핵심 구현 세부사항 + +### 인코더 상태 머신 + +`libs/braillify/src/lib.rs`의 `Encoder` 구조체는 다음 상태를 유지합니다: + +- `is_english`: 현재 영문/로마자 모드 여부 +- `english_indicator`: 한글 텍스트에 로마자 표시자 필요 여부 +- `triple_big_english`: 연속 대문자 단어 시퀀스 추적 +- `parenthesis_stack`: 기호 렌더링을 위한 괄호 컨텍스트 추적 + +### 한글 점자 규정 + +`test_cases/`의 테스트 케이스는 2024 한글 점자 규정에 대응합니다. `test_by_testcase` 테스트는 CSV 파일을 읽어 모든 규정을 검증합니다. CSV 형식: `input,internal_code,expected_code,expected_unicode`. + +### 멀티 플랫폼 지원 + +- **Rust**: CLI 포함 네이티브 라이브러리 (`braillify` 바이너리는 `cli` 피처 필요) +- **Node.js**: wasm-pack을 통한 WebAssembly, `braillify` npm 패키지로 배포 +- **Python**: PyO3/maturin을 통한 네이티브 확장, `braillify` PyPI 패키지로 배포 +- **.NET**: FFI/P/Invoke를 통한 네이티브 라이브러리 + - `BraillifyNet`: 라이브러리 NuGet 패키지 + - `Braillify`: CLI 도구 NuGet 패키지 (`dnx braillify` 또는 `dotnet tool install -g Braillify`) + +## .NET 바인딩 상세 + +### NuGet 패키지 + +| 패키지 | 설명 | 타겟 프레임워크 | +| -------------- | ---------- | --------------------------------------------- | +| `BraillifyNet` | 라이브러리 | netstandard2.0, netcoreapp3.1, net5.0~net10.0 | +| `Braillify` | CLI 도구 | net10.0 | + +### 지원 플랫폼 + +- .NET Standard 2.0 +- .NET Core 3.1 +- .NET 5.0, 6.0, 7.0, 8.0, 9.0, 10.0 +- .NET Framework 4.7.2 (테스트 지원) + +### 버전별 구현 차이 (NativeMethods.cs) + +| 버전 | P/Invoke 방식 | 포인터 타입 | UTF-8 마샬링 | +| ----------------- | --------------------------- | -------------- | ---------------------- | +| .NET 7+ | LibraryImport (소스 생성기) | nint/nuint | StringMarshalling.Utf8 | +| .NET 5-6 | DllImport | nint/nuint | LPUTF8Str | +| .NET Core 3.1 | DllImport | IntPtr/UIntPtr | LPUTF8Str | +| .NET Standard 2.0 | DllImport | IntPtr/UIntPtr | 수동 UTF-8 변환 | + +### 네이티브 라이브러리 로딩 (NativeLibraryLoader.cs) + +- .NET Core 3.1+: `NativeLibrary.SetDllImportResolver` 사용 +- `AppContext.BaseDirectory` 사용 (NativeAOT 호환) +- 런타임 식별자(RID) 기반 경로 탐색: `runtimes/{rid}/native/{lib}` + +### 테스트 패키지 호환성 (Braillify.Tests.csproj) + +| 프레임워크 | Microsoft.NET.Test.Sdk | xunit | xunit.runner.visualstudio | +| ----------------------- | ---------------------- | ----- | ------------------------- | +| .NET Core 3.1, .NET 5.0 | 17.8.0 | 2.4.2 | 2.4.5 | +| .NET 6.0, 7.0 | 17.12.0 | 2.6.6 | 2.5.6 | +| .NET 8.0+ | 18.\* | 2.\* | 3.\* | + +> **주의**: xunit.runner.visualstudio 3.x는 .NET 8.0 이상에서만 지원됩니다. + +### CLI 사용법 + +```bash +# 글로벌 설치 +dotnet tool install -g Braillify + +# 설치 없이 실행 (.NET 10+) +dnx braillify "안녕하세요" + +# 텍스트 변환 +braillify "안녕하세요" +# 출력: ⠣⠒⠉⠻⠚⠠⠝⠬ + +# 파이프 입력 +echo "안녕하세요" | braillify + +# REPL 모드 (인자 없이 실행) +braillify +``` + +## 테스트 + +`cargo test test_by_testcase`를 실행하여 모든 점자 규정을 검증합니다. 이 테스트는 `test_cases/`의 모든 CSV 파일을 읽고 실패 시 컬러 diff와 함께 상세 정보를 출력합니다. diff --git a/packages/dotnet/.claude/settings.local.json b/packages/dotnet/.claude/settings.local.json new file mode 100644 index 0000000..4b1f9e4 --- /dev/null +++ b/packages/dotnet/.claude/settings.local.json @@ -0,0 +1,43 @@ +{ + "permissions": { + "allow": [ + "WebSearch", + "Bash(cat:*)", + "Bash(cargo build:*)", + "Bash(dotnet tool install:*)", + "Bash(dotnet tool update:*)", + "Bash(dotnet tool uninstall:*)", + "Bash(dotnet tool exec:*)", + "Bash(dotnet new:*)", + "Bash(dotnet sln:*)", + "Bash(dotnet build:*)", + "Bash(dotnet restore:*)", + "Bash(git checkout:*)", + "Bash(dotnet test:*)", + "Bash(dotnet nuget:*)", + "Bash(where:*)", + "Bash(where cargo:*)", + "Bash(cargo --version:*)", + "Bash(dotnet --version:*)", + "Bash(rustc:*)", + "Bash($env:USERPROFILE.cargobincargo.exe --version)", + "Bash(C:/Users/vincent/.cargo/bin/cargo.exe:*)", + "Bash(C:/Users/vincent/.cargo/bin/rustup.exe update:*)", + "Bash(for fw in netcoreapp3.1 net5.0 net6.0 net7.0 net8.0 net9.0 net10.0)", + "Bash(do echo \"=== $fw ===\")", + "Bash(done)", + "Bash(dotnet --list-runtimes:*)", + "Bash(dotnet --list-sdks:*)", + "Bash(git add:*)", + "Bash(git commit -m \"$\\(cat <<''EOF''\nAdd .NET bindings for braillify library\n\n- Add Rust FFI crate \\(packages/dotnet/\\) with C ABI exports\n- Add .NET class library with P/Invoke bindings\n - Support for .NET Standard 2.0, .NET Core 3.x, .NET 5-10\n - Platform-specific native library loading\n - LibraryImport for .NET 7+, DllImport for older versions\n- Add xUnit test project with multi-targeting\n - Conditional package references for test SDK compatibility\n - xunit.runner.visualstudio 3.x for .NET 8+, 2.x for older\n- Add MSTest project for .NET Framework 4.7.2\n- Update CI/CD workflow for .NET package build and publish\n- Add build scripts to package.json\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")", + "Bash(dotnet pack:*)", + "Bash(unzip:*)", + "Bash(dotnet run)", + "Bash(dnx:*)", + "Bash(braillify:*)", + "Bash(cmd /c \"dnx --help\")", + "Bash(gh pr create:*)", + "Bash(git rm:*)" + ] + } +} diff --git a/packages/dotnet/.gitignore b/packages/dotnet/.gitignore new file mode 100644 index 0000000..740b758 --- /dev/null +++ b/packages/dotnet/.gitignore @@ -0,0 +1,348 @@ +## User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +## User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +## Mono auto generated files +mono_crash.* + +## Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +## Visual Studio 2015/2017 cache/options directory +.vs/ + +## Visual Studio 2017 auto generated files +Generated\ Files/ + +## MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +## NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +## Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +## Benchmark Results +BenchmarkDotNet.Artifacts/ + +## .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +## StyleCop +StyleCopReport.xml + +## Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +## Chutzpah Test files +_Chutzpah* + +## Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +## Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +## Visual Studio Trace Files +*.e2e + +## TFS 2012 Local Workspace +$tf/ + +## Guidance Automation Toolkit +*.gpState + +## ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +## TeamCity is a build add-in +_TeamCity* + +## DotCover is a Code Coverage Tool +*.dotCover + +## AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +## Coverlet is a Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +## Visual Studio code coverage results +*.coverage +*.coveragexml + +## NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +## MightyMoose +*.mm.* +AutoTest.Net/ + +## Web workbench (Sass) +.sass-cache/ + +## Installshield output folder +[Ee]xpress/ + +## DocProject +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +## Click-Once +publish/ + +## Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +*.pubxml +*.publishproj + +## NuGet +*.nupkg +*.snupkg +**/[Pp]ackages/* +!**/[Pp]ackages/build/ +!**/[Pp]ackages/repositories.config +*.nuget.props +*.nuget.targets + +## NuGet v3 restore +project.assets.json + +## Codeanalysis +/codeAnalysis/ + +## Microsoft Azure Build Output +csx/ +*.build.csdef + +## Microsoft Azure Emulator +ecf/ +rcf/ + +## Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +## Visual Studio cache files +*.[Cc]ache +!?*.[Cc]ache/ + +## Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +## RIA/Silverlight projects +Generated_Code/ + +## SQL Server files +*.mdf +*.ldf +*.ndf + +## Business Intelligence +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +## Microsoft Fakes +FakesAssemblies/ + +## GhostDoc +*.GhostDoc.xml + +## Node.js Tools for Visual Studio +.ntvs_analysis.dat + +## Visual Studio 6 build log +*.plg + +## Visual Studio 6 workspace options file +*.opt + +## Visual Studio 6 auto-generated workspace file +*.vbw + +## Visual Studio 6 auto-generated project file (for VB project) +*.vbp + +## Visual Studio 6 workspace and project file (for C project) +*.dsw +*.dsp + +## Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +## Paket dependency manager +.paket/paket.exe +paket-files/ + +## FAKE - F# Make +.fake/ + +## CodeRush personal settings +.cr/personal + +## Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +## Cake - C# Make +tools/** +!tools/packages.config + +## Tabs Studio +*.tss + +## Telerik's JustMock +*.jmconfig + +## BizTalk +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +## OpenCover UI analysis results +OpenCover/ + +## Azure Stream Analytics local run output +ASALocalRun/ + +## MSBuild Binary and Structured Log +*.binlog + +## NVidia Nsight GPU Debugger +*.nvuser + +## MFractors (Xamarin productivity tool) +.mfractor/ + +## Local History for Visual Studio +.localhistory/ + +## Visual Studio History (Stacker) +.vshistory/ + +## BeatPulse healthcheck temp database +healthchecksdb + +## Backup folder for Package Reference +MigrationBackup/ + +## Ionide (F# VS Code extension) +.ionide/ + +## Fody - auto weaving +FodyWeavers.xsd + +## Local History for Visual Studio Code +.history/ + +## Windows Store Certificate +*.appxcert + +## JetBrains Rider +*.sln.iml + +## Native libraries (built by Cargo) +**/runtimes/*/native/ diff --git a/packages/dotnet/Braillify.DllVerification/Braillify.DllVerification.csproj b/packages/dotnet/Braillify.DllVerification/Braillify.DllVerification.csproj new file mode 100644 index 0000000..3a098d0 --- /dev/null +++ b/packages/dotnet/Braillify.DllVerification/Braillify.DllVerification.csproj @@ -0,0 +1,25 @@ + + + + Exe + net8.0 + enable + enable + + + + + + ..\BraillifyNet\bin\Release\net8.0\BraillifyNet.dll + + + + + + + PreserveNewest + runtimes\win-x64\native\braillify_native.dll + + + + diff --git a/packages/dotnet/Braillify.DllVerification/Program.cs b/packages/dotnet/Braillify.DllVerification/Program.cs new file mode 100644 index 0000000..b08060b --- /dev/null +++ b/packages/dotnet/Braillify.DllVerification/Program.cs @@ -0,0 +1,116 @@ +// Braillify DLL 직접 참조 검증 프로그램 +// Braillify Direct DLL Reference Verification Program + +Console.OutputEncoding = System.Text.Encoding.UTF8; +Console.WriteLine("=== Braillify DLL 직접 참조 검증 ==="); +Console.WriteLine("=== Braillify Direct DLL Reference Verification ===\n"); + +// DLL 경로 확인 +// Check DLL paths +var baseDir = AppContext.BaseDirectory; +Console.WriteLine($"실행 디렉토리 (Base Directory): {baseDir}"); + +var nativeDllPath = Path.Combine(baseDir, "runtimes", "win-x64", "native", "braillify_native.dll"); +Console.WriteLine($"네이티브 DLL 경로 (Native DLL Path): {nativeDllPath}"); +Console.WriteLine($"네이티브 DLL 존재 여부 (Native DLL Exists): {File.Exists(nativeDllPath)}\n"); + +// 테스트 케이스 +// Test cases +var testCases = new (string Input, string ExpectedUnicode)[] +{ + ("안녕하세요", "⠣⠒⠉⠻⠚⠠⠝⠬"), + ("상상이상의", "⠇⠶⠇⠶⠕⠇⠶⠺"), + ("1,000", "⠼⠁⠂⠚⠚⠚"), + ("ATM", "⠠⠠⠁⠞⠍"), + ("한글 점자", null!), // 예상 결과 없이 테스트 / Test without expected result +}; + +var passCount = 0; +var failCount = 0; + +foreach (var (input, expectedUnicode) in testCases) +{ + try + { + Console.WriteLine($"입력 (Input): \"{input}\""); + + // EncodeToUnicode 테스트 + // EncodeToUnicode test + var unicodeResult = Braillify.Braillify.EncodeToUnicode(input); + Console.WriteLine($"유니코드 (Unicode): {unicodeResult}"); + + // Encode 테스트 (바이트 배열) + // Encode test (byte array) + var byteResult = Braillify.Braillify.Encode(input); + Console.WriteLine($"바이트 배열 (Byte array): [{string.Join(", ", byteResult)}]"); + + // EncodeToBrailleFont 테스트 + // EncodeToBrailleFont test + var fontResult = Braillify.Braillify.EncodeToBrailleFont(input); + Console.WriteLine($"폰트 문자열 (Font string): {fontResult}"); + + // 예상 결과와 비교 + // Compare with expected result + if (expectedUnicode != null) + { + if (unicodeResult == expectedUnicode) + { + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine("[PASS] 예상 결과와 일치 / Matches expected result"); + Console.ResetColor(); + passCount++; + } + else + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"[FAIL] 예상: {expectedUnicode}, 실제: {unicodeResult}"); + Console.ResetColor(); + failCount++; + } + } + else + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine("[INFO] 예상 결과 없음 - 출력만 확인 / No expected result - output only"); + Console.ResetColor(); + passCount++; + } + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"[ERROR] 예외 발생: {ex.Message}"); + Console.WriteLine($"[ERROR] 스택 트레이스: {ex.StackTrace}"); + Console.ResetColor(); + failCount++; + } + + Console.WriteLine(); +} + +// 요약 출력 +// Summary output +Console.WriteLine("=== 검증 결과 요약 (Verification Summary) ==="); +Console.ForegroundColor = passCount > 0 && failCount == 0 ? ConsoleColor.Green : ConsoleColor.Yellow; +Console.WriteLine($"통과 (Passed): {passCount}"); +Console.ForegroundColor = failCount > 0 ? ConsoleColor.Red : ConsoleColor.Green; +Console.WriteLine($"실패 (Failed): {failCount}"); +Console.ResetColor(); + +Console.WriteLine(); +if (failCount == 0) +{ + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine("*** 모든 검증 통과! DLL 직접 참조가 정상 동작합니다. ***"); + Console.WriteLine("*** All verifications passed! Direct DLL reference works correctly. ***"); + Console.ResetColor(); + return 0; +} +else +{ + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("*** 일부 검증 실패. 확인이 필요합니다. ***"); + Console.WriteLine("*** Some verifications failed. Review needed. ***"); + Console.ResetColor(); + return 1; +} diff --git a/packages/dotnet/Braillify.Tests.NetFramework/Braillify.Tests.NetFramework.csproj b/packages/dotnet/Braillify.Tests.NetFramework/Braillify.Tests.NetFramework.csproj new file mode 100644 index 0000000..c4b6c04 --- /dev/null +++ b/packages/dotnet/Braillify.Tests.NetFramework/Braillify.Tests.NetFramework.csproj @@ -0,0 +1,31 @@ + + + net472 + latest + enable + false + true + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + + PreserveNewest + false + + + diff --git a/packages/dotnet/Braillify.Tests.NetFramework/BraillifyTests.cs b/packages/dotnet/Braillify.Tests.NetFramework/BraillifyTests.cs new file mode 100644 index 0000000..0077b3a --- /dev/null +++ b/packages/dotnet/Braillify.Tests.NetFramework/BraillifyTests.cs @@ -0,0 +1,94 @@ +namespace Braillify.Tests.NetFramework; + +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +public sealed class BraillifyTests +{ + [TestMethod] + public void EncodeToUnicode_SimpleKorean_ReturnsExpectedBraille() + { + // Arrange + const string input = "안녕하세요"; + const string expected = "⠣⠒⠉⠻⠚⠠⠝⠬"; + + // Act + var result = Braillify.EncodeToUnicode(input); + + // Assert + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void Encode_SimpleKorean_ReturnsNonEmptyByteArray() + { + // Arrange + const string input = "안녕"; + + // Act + var result = Braillify.Encode(input); + + // Assert + Assert.IsTrue(result.Length > 0); + } + + [TestMethod] + public void EncodeToBrailleFont_SimpleKorean_ReturnsNonEmptyString() + { + // Arrange + const string input = "테스트"; + + // Act + var result = Braillify.EncodeToBrailleFont(input); + + // Assert + Assert.IsFalse(string.IsNullOrEmpty(result)); + } + + [TestMethod] + public void EncodeToUnicode_NullInput_ThrowsArgumentNullException() + { + // Act & Assert + Assert.ThrowsExactly(() => Braillify.EncodeToUnicode(null!)); + } + + [TestMethod] + [DataRow("상상이상의", "⠇⠶⠇⠶⠕⠇⠶⠺")] + [DataRow("1,000", "⠼⠁⠂⠚⠚⠚")] + [DataRow("ATM", "⠠⠠⠁⠞⠍")] + public void EncodeToUnicode_VariousInputs_ReturnsExpectedResults(string input, string expected) + { + // Act + var result = Braillify.EncodeToUnicode(input); + + // Assert + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void EncodeToUnicode_EmptyString_ReturnsEmptyString() + { + // Arrange + const string input = ""; + + // Act + var result = Braillify.EncodeToUnicode(input); + + // Assert + Assert.AreEqual(string.Empty, result); + } + + [TestMethod] + public void Encode_EmptyString_ReturnsEmptyArray() + { + // Arrange + const string input = ""; + + // Act + var result = Braillify.Encode(input); + + // Assert + Assert.AreEqual(0, result.Length); + } +} diff --git a/packages/dotnet/Braillify.Tests/Braillify.Tests.csproj b/packages/dotnet/Braillify.Tests/Braillify.Tests.csproj new file mode 100644 index 0000000..d830167 --- /dev/null +++ b/packages/dotnet/Braillify.Tests/Braillify.Tests.csproj @@ -0,0 +1,66 @@ + + + + + netcoreapp3.1;net5.0;net6.0;net7.0;net8.0;net9.0;net10.0 + latest + enable + enable + false + true + + true + true + + $(NoWarn);NU1701;NU1702 + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + diff --git a/packages/dotnet/Braillify.Tests/BraillifyTests.cs b/packages/dotnet/Braillify.Tests/BraillifyTests.cs new file mode 100644 index 0000000..1824b34 --- /dev/null +++ b/packages/dotnet/Braillify.Tests/BraillifyTests.cs @@ -0,0 +1,92 @@ +namespace Braillify.Tests; + +using Xunit; + +public sealed class BraillifyTests +{ + [Fact] + public void EncodeToUnicode_SimpleKorean_ReturnsExpectedBraille() + { + // Arrange + const string input = "안녕하세요"; + const string expected = "⠣⠒⠉⠻⠚⠠⠝⠬"; + + // Act + var result = Braillify.EncodeToUnicode(input); + + // Assert + Assert.Equal(expected, result); + } + + [Fact] + public void Encode_SimpleKorean_ReturnsNonEmptyByteArray() + { + // Arrange + const string input = "안녕"; + + // Act + var result = Braillify.Encode(input); + + // Assert + Assert.NotEmpty(result); + } + + [Fact] + public void EncodeToBrailleFont_SimpleKorean_ReturnsNonEmptyString() + { + // Arrange + const string input = "테스트"; + + // Act + var result = Braillify.EncodeToBrailleFont(input); + + // Assert + Assert.NotEmpty(result); + } + + [Fact] + public void EncodeToUnicode_NullInput_ThrowsArgumentNullException() + { + // Act & Assert + Assert.Throws(() => Braillify.EncodeToUnicode(null!)); + } + + [Theory] + [InlineData("상상이상의", "⠇⠶⠇⠶⠕⠇⠶⠺")] + [InlineData("1,000", "⠼⠁⠂⠚⠚⠚")] + [InlineData("ATM", "⠠⠠⠁⠞⠍")] + public void EncodeToUnicode_VariousInputs_ReturnsExpectedResults(string input, string expected) + { + // Act + var result = Braillify.EncodeToUnicode(input); + + // Assert + Assert.Equal(expected, result); + } + + [Fact] + public void EncodeToUnicode_EmptyString_ReturnsEmptyString() + { + // Arrange + const string input = ""; + + // Act + var result = Braillify.EncodeToUnicode(input); + + // Assert + Assert.Equal(string.Empty, result); + } + + [Fact] + public void Encode_EmptyString_ReturnsEmptyArray() + { + // Arrange + const string input = ""; + + // Act + var result = Braillify.Encode(input); + + // Assert + Assert.Empty(result); + } +} diff --git a/packages/dotnet/Braillify.Verification/Braillify.Verification.csproj b/packages/dotnet/Braillify.Verification/Braillify.Verification.csproj new file mode 100644 index 0000000..64aebc5 --- /dev/null +++ b/packages/dotnet/Braillify.Verification/Braillify.Verification.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + diff --git a/packages/dotnet/Braillify.Verification/Program.cs b/packages/dotnet/Braillify.Verification/Program.cs new file mode 100644 index 0000000..fab5e5f --- /dev/null +++ b/packages/dotnet/Braillify.Verification/Program.cs @@ -0,0 +1,134 @@ +// Braillify NuGet 패키지 검증 프로그램 +// Braillify NuGet Package Verification Program + +Console.OutputEncoding = System.Text.Encoding.UTF8; +Console.WriteLine("=== Braillify NuGet 패키지 검증 ==="); +Console.WriteLine("=== Braillify NuGet Package Verification ===\n"); + +// 테스트 케이스 +// Test cases +var testCases = new (string Input, string ExpectedUnicode)[] +{ + ("안녕하세요", "⠣⠒⠉⠻⠚⠠⠝⠬"), + ("상상이상의", "⠇⠶⠇⠶⠕⠇⠶⠺"), + ("1,000", "⠼⠁⠂⠚⠚⠚"), + ("ATM", "⠠⠠⠁⠞⠍"), + ("한글 점자", null!), // 예상 결과 없이 테스트 / Test without expected result +}; + +var passCount = 0; +var failCount = 0; + +foreach (var (input, expectedUnicode) in testCases) +{ + try + { + Console.WriteLine($"입력 (Input): \"{input}\""); + + // EncodeToUnicode 테스트 + // EncodeToUnicode test + var unicodeResult = Braillify.Braillify.EncodeToUnicode(input); + Console.WriteLine($"유니코드 (Unicode): {unicodeResult}"); + + // Encode 테스트 (바이트 배열) + // Encode test (byte array) + var byteResult = Braillify.Braillify.Encode(input); + Console.WriteLine($"바이트 배열 길이 (Byte array length): {byteResult.Length}"); + + // EncodeToBrailleFont 테스트 + // EncodeToBrailleFont test + var fontResult = Braillify.Braillify.EncodeToBrailleFont(input); + Console.WriteLine($"폰트 문자열 (Font string): {fontResult}"); + + // 예상 결과와 비교 + // Compare with expected result + if (expectedUnicode != null) + { + if (unicodeResult == expectedUnicode) + { + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine("[PASS] 예상 결과와 일치 / Matches expected result"); + Console.ResetColor(); + passCount++; + } + else + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"[FAIL] 예상: {expectedUnicode}, 실제: {unicodeResult}"); + Console.WriteLine($"[FAIL] Expected: {expectedUnicode}, Actual: {unicodeResult}"); + Console.ResetColor(); + failCount++; + } + } + else + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine("[INFO] 예상 결과 없음 - 출력만 확인 / No expected result - output only"); + Console.ResetColor(); + passCount++; + } + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"[ERROR] 예외 발생: {ex.Message}"); + Console.WriteLine($"[ERROR] Exception: {ex.Message}"); + Console.ResetColor(); + failCount++; + } + + Console.WriteLine(); +} + +// 요약 출력 +// Summary output +Console.WriteLine("=== 검증 결과 요약 (Verification Summary) ==="); +Console.ForegroundColor = passCount > 0 && failCount == 0 ? ConsoleColor.Green : ConsoleColor.Yellow; +Console.WriteLine($"통과 (Passed): {passCount}"); +Console.ForegroundColor = failCount > 0 ? ConsoleColor.Red : ConsoleColor.Green; +Console.WriteLine($"실패 (Failed): {failCount}"); +Console.ResetColor(); + +// null 입력 예외 테스트 +// Null input exception test +Console.WriteLine("\n=== Null 입력 예외 테스트 (Null Input Exception Test) ==="); +try +{ + Braillify.Braillify.EncodeToUnicode(null!); + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("[FAIL] ArgumentNullException이 발생하지 않음 / ArgumentNullException not thrown"); + Console.ResetColor(); +} +catch (ArgumentNullException) +{ + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine("[PASS] ArgumentNullException 정상 발생 / ArgumentNullException thrown as expected"); + Console.ResetColor(); +} +catch (Exception ex) +{ + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"[FAIL] 예상치 못한 예외: {ex.GetType().Name}"); + Console.WriteLine($"[FAIL] Unexpected exception: {ex.GetType().Name}"); + Console.ResetColor(); +} + +// 최종 결과 +// Final result +Console.WriteLine(); +if (failCount == 0) +{ + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine("*** 모든 검증 통과! NuGet 패키지가 정상 동작합니다. ***"); + Console.WriteLine("*** All verifications passed! NuGet package works correctly. ***"); + Console.ResetColor(); + return 0; +} +else +{ + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("*** 일부 검증 실패. 확인이 필요합니다. ***"); + Console.WriteLine("*** Some verifications failed. Review needed. ***"); + Console.ResetColor(); + return 1; +} diff --git a/packages/dotnet/Braillify.Verification/nuget.config b/packages/dotnet/Braillify.Verification/nuget.config new file mode 100644 index 0000000..7918181 --- /dev/null +++ b/packages/dotnet/Braillify.Verification/nuget.config @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/dotnet/Braillify.slnx b/packages/dotnet/Braillify.slnx new file mode 100644 index 0000000..7512fc3 --- /dev/null +++ b/packages/dotnet/Braillify.slnx @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/packages/dotnet/Braillify/Braillify.csproj b/packages/dotnet/Braillify/Braillify.csproj new file mode 100644 index 0000000..090fde1 --- /dev/null +++ b/packages/dotnet/Braillify/Braillify.csproj @@ -0,0 +1,37 @@ + + + + Exe + net10.0 + enable + enable + + + true + braillify + + + Braillify + 0.1.0 + DevFive + 한국어 점자 변환 CLI 도구 + Apache-2.0 + https://braillify.kr + https://github.com/dev-five-git/braillify + braille;korean;translation;accessibility;cli + README.md + + + + + + + + + + + + + + + diff --git a/packages/dotnet/Braillify/Program.cs b/packages/dotnet/Braillify/Program.cs new file mode 100644 index 0000000..03f04d8 --- /dev/null +++ b/packages/dotnet/Braillify/Program.cs @@ -0,0 +1,99 @@ +// Braillify CLI - 한국어 점자 변환 CLI 도구 + +using System.CommandLine; + +Console.OutputEncoding = System.Text.Encoding.UTF8; +Console.InputEncoding = System.Text.Encoding.UTF8; + +// 입력 텍스트 인자 정의 +var inputArgument = new Argument("input") +{ + Description = "변환할 텍스트. 없으면 REPL 모드로 진입합니다.", + Arity = ArgumentArity.ZeroOrOne +}; + +// 루트 명령 정의 +var rootCommand = new RootCommand("한국어 점자 변환 CLI") +{ + inputArgument +}; + +rootCommand.SetAction(parseResult => +{ + var input = parseResult.GetValue(inputArgument); + + // stdin 파이프 입력 확인 + if (input is null && Console.IsInputRedirected) + { + input = Console.In.ReadToEnd().TrimEnd('\r', '\n'); + } + + if (input is not null) + { + // One-shot 모드 + RunOneShot(input); + } + else + { + // REPL 모드 + RunRepl(); + } +}); + +return rootCommand.Parse(args).Invoke(); + +static void RunOneShot(string text) +{ + try + { + var result = Braillify.Braillify.EncodeToUnicode(text); + Console.Write(result); + } + catch (Exception ex) + { + Console.Error.WriteLine($"점자 변환 실패: {ex.Message}"); + Environment.ExitCode = 1; + } +} + +static void RunRepl() +{ + Console.WriteLine("braillify REPL - 입력을 점자로 변환합니다. 종료: Ctrl+C 또는 Ctrl+D"); + + while (true) + { + Console.Write("> "); + + string? line; + try + { + line = Console.ReadLine(); + } + catch (Exception) + { + break; + } + + // Ctrl+D (EOF) 또는 null + if (line is null) + { + Console.WriteLine("종료합니다."); + break; + } + + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + + try + { + var result = Braillify.Braillify.EncodeToUnicode(line); + Console.WriteLine(result); + } + catch (Exception ex) + { + Console.WriteLine($"오류: {ex.Message}"); + } + } +} diff --git a/packages/dotnet/Braillify/README.md b/packages/dotnet/Braillify/README.md new file mode 100644 index 0000000..35cf0d1 --- /dev/null +++ b/packages/dotnet/Braillify/README.md @@ -0,0 +1,49 @@ +# Braillify + +한국어 점자 변환 CLI 도구 + +## 설치 + +```bash +# 글로벌 설치 +dotnet tool install -g Braillify + +# 설치 없이 실행 (.NET 10+) +dnx braillify "안녕하세요" +``` + +## 사용법 + +```bash +# 텍스트 변환 +braillify "안녕하세요" +# 출력: ⠣⠒⠉⠻⠚⠠⠝⠬ + +# 파이프 입력 +echo "안녕하세요" | braillify + +# REPL 모드 (인자 없이 실행) +braillify +> 안녕하세요 +⠣⠒⠉⠻⠚⠠⠝⠬ +> 한글 점자 +⠚⠒⠈⠮⠀⠨⠎⠢⠨ +> (Ctrl+C로 종료) +``` + +## 옵션 + +| 옵션 | 설명 | +| -------------- | ----------- | +| `-h`, `--help` | 도움말 출력 | +| `--version` | 버전 출력 | + +## 라이선스 + +Apache-2.0 + +## 관련 링크 + +- [Braillify.Net 라이브러리 (NuGet)](https://www.nuget.org/packages/Braillify.Net) +- [GitHub](https://github.com/dev-five-git/braillify) +- [홈페이지](https://braillify.kr) diff --git a/packages/dotnet/BraillifyNet/Braillify.cs b/packages/dotnet/BraillifyNet/Braillify.cs new file mode 100644 index 0000000..017512f --- /dev/null +++ b/packages/dotnet/BraillifyNet/Braillify.cs @@ -0,0 +1,368 @@ +namespace Braillify; + +using System; +using System.Runtime.InteropServices; +#if !NET6_0_OR_GREATER +using System.Text; +#endif + +/// +/// 한국어 텍스트를 점자로 변환하는 라이브러리입니다. +/// +public static class Braillify +{ +#if NETCOREAPP3_0_OR_GREATER + static Braillify() + { + NativeLibraryLoader.EnsureLoaded(); + } +#endif + + /// + /// 텍스트를 점자 바이트 배열로 인코딩합니다. + /// + /// 변환할 텍스트 + /// 점자 바이트 배열 + /// 텍스트가 null인 경우 + /// 인코딩 실패 시 + public static byte[] Encode(string text) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(text); +#else + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } +#endif + +#if NET5_0_OR_GREATER + nint resultPtr = NativeMethods.braillify_encode(text, out nuint length); + + if (resultPtr == 0) + { + ThrowLastError(); + } + + try + { + var result = new byte[(int)length]; + Marshal.Copy(resultPtr, result, 0, (int)length); + return result; + } + finally + { + NativeMethods.braillify_free_bytes(resultPtr, length); + } +#elif NETCOREAPP3_0_OR_GREATER + IntPtr resultPtr = NativeMethods.braillify_encode(text, out UIntPtr length); + + if (resultPtr == IntPtr.Zero) + { + ThrowLastError(); + } + + try + { + var len = (int)length.ToUInt32(); + var result = new byte[len]; + Marshal.Copy(resultPtr, result, 0, len); + return result; + } + finally + { + NativeMethods.braillify_free_bytes(resultPtr, length); + } +#else + // .NET Standard 2.0: 수동 UTF-8 마샬링 + IntPtr textPtr = StringToUtf8Ptr(text); + try + { + IntPtr resultPtr = NativeMethods.braillify_encode(textPtr, out UIntPtr length); + + if (resultPtr == IntPtr.Zero) + { + ThrowLastError(); + } + + try + { + var len = (int)length.ToUInt32(); + var result = new byte[len]; + Marshal.Copy(resultPtr, result, 0, len); + return result; + } + finally + { + NativeMethods.braillify_free_bytes(resultPtr, length); + } + } + finally + { + Marshal.FreeHGlobal(textPtr); + } +#endif + } + + /// + /// 텍스트를 점자 유니코드 문자열로 인코딩합니다. + /// + /// 변환할 텍스트 + /// 점자 유니코드 문자열 + /// 텍스트가 null인 경우 + /// 인코딩 실패 시 + public static string EncodeToUnicode(string text) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(text); +#else + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } +#endif + +#if NET5_0_OR_GREATER + nint resultPtr = NativeMethods.braillify_encode_to_unicode(text); + + if (resultPtr == 0) + { + ThrowLastError(); + } + + try + { +#if NET6_0_OR_GREATER + return Marshal.PtrToStringUTF8(resultPtr) ?? string.Empty; +#else + return PtrToStringUtf8(resultPtr); +#endif + } + finally + { + NativeMethods.braillify_free_string(resultPtr); + } +#elif NETCOREAPP3_0_OR_GREATER + IntPtr resultPtr = NativeMethods.braillify_encode_to_unicode(text); + + if (resultPtr == IntPtr.Zero) + { + ThrowLastError(); + } + + try + { + return PtrToStringUtf8(resultPtr); + } + finally + { + NativeMethods.braillify_free_string(resultPtr); + } +#else + // .NET Standard 2.0: 수동 UTF-8 마샬링 + IntPtr textPtr = StringToUtf8Ptr(text); + try + { + IntPtr resultPtr = NativeMethods.braillify_encode_to_unicode(textPtr); + + if (resultPtr == IntPtr.Zero) + { + ThrowLastError(); + } + + try + { + return PtrToStringUtf8(resultPtr); + } + finally + { + NativeMethods.braillify_free_string(resultPtr); + } + } + finally + { + Marshal.FreeHGlobal(textPtr); + } +#endif + } + + /// + /// 텍스트를 점자 폰트 문자열로 인코딩합니다. + /// + /// 변환할 텍스트 + /// 점자 폰트 문자열 + /// 텍스트가 null인 경우 + /// 인코딩 실패 시 + public static string EncodeToBrailleFont(string text) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(text); +#else + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } +#endif + +#if NET5_0_OR_GREATER + nint resultPtr = NativeMethods.braillify_encode_to_braille_font(text); + + if (resultPtr == 0) + { + ThrowLastError(); + } + + try + { +#if NET6_0_OR_GREATER + return Marshal.PtrToStringUTF8(resultPtr) ?? string.Empty; +#else + return PtrToStringUtf8(resultPtr); +#endif + } + finally + { + NativeMethods.braillify_free_string(resultPtr); + } +#elif NETCOREAPP3_0_OR_GREATER + IntPtr resultPtr = NativeMethods.braillify_encode_to_braille_font(text); + + if (resultPtr == IntPtr.Zero) + { + ThrowLastError(); + } + + try + { + return PtrToStringUtf8(resultPtr); + } + finally + { + NativeMethods.braillify_free_string(resultPtr); + } +#else + // .NET Standard 2.0: 수동 UTF-8 마샬링 + IntPtr textPtr = StringToUtf8Ptr(text); + try + { + IntPtr resultPtr = NativeMethods.braillify_encode_to_braille_font(textPtr); + + if (resultPtr == IntPtr.Zero) + { + ThrowLastError(); + } + + try + { + return PtrToStringUtf8(resultPtr); + } + finally + { + NativeMethods.braillify_free_string(resultPtr); + } + } + finally + { + Marshal.FreeHGlobal(textPtr); + } +#endif + } + + private static void ThrowLastError() + { +#if NET5_0_OR_GREATER + nint errorPtr = NativeMethods.braillify_get_last_error(); + + if (errorPtr == 0) + { + throw new BraillifyException("알 수 없는 오류가 발생했습니다."); + } + + try + { +#if NET6_0_OR_GREATER + var errorMessage = Marshal.PtrToStringUTF8(errorPtr); +#else + var errorMessage = PtrToStringUtf8(errorPtr); +#endif + throw new BraillifyException(errorMessage ?? "알 수 없는 오류"); + } + finally + { + NativeMethods.braillify_free_string(errorPtr); + } +#else + IntPtr errorPtr = NativeMethods.braillify_get_last_error(); + + if (errorPtr == IntPtr.Zero) + { + throw new BraillifyException("알 수 없는 오류가 발생했습니다."); + } + + try + { + var errorMessage = PtrToStringUtf8(errorPtr); + throw new BraillifyException(errorMessage); + } + finally + { + NativeMethods.braillify_free_string(errorPtr); + } +#endif + } + +#if !NET6_0_OR_GREATER + private static string PtrToStringUtf8( +#if NET5_0_OR_GREATER + nint ptr +#else + IntPtr ptr +#endif + ) + { +#if NET5_0_OR_GREATER + if (ptr == 0) +#else + if (ptr == IntPtr.Zero) +#endif + { + return string.Empty; + } + + // UTF-8 문자열 길이 계산 + var len = 0; + while (Marshal.ReadByte(ptr, len) != 0) + { + len++; + } + + if (len == 0) + { + return string.Empty; + } + + var buffer = new byte[len]; + Marshal.Copy(ptr, buffer, 0, len); + return Encoding.UTF8.GetString(buffer); + } +#endif + +#if !NETCOREAPP3_0_OR_GREATER + private static IntPtr StringToUtf8Ptr(string text) + { + if (string.IsNullOrEmpty(text)) + { + // 빈 문자열의 경우 null-terminator만 포함된 버퍼 반환 + IntPtr emptyPtr = Marshal.AllocHGlobal(1); + Marshal.WriteByte(emptyPtr, 0); + return emptyPtr; + } + + byte[] utf8Bytes = Encoding.UTF8.GetBytes(text); + IntPtr ptr = Marshal.AllocHGlobal(utf8Bytes.Length + 1); + Marshal.Copy(utf8Bytes, 0, ptr, utf8Bytes.Length); + Marshal.WriteByte(ptr, utf8Bytes.Length, 0); // null-terminator + return ptr; + } +#endif +} diff --git a/packages/dotnet/BraillifyNet/BraillifyException.cs b/packages/dotnet/BraillifyNet/BraillifyException.cs new file mode 100644 index 0000000..4fb7aab --- /dev/null +++ b/packages/dotnet/BraillifyNet/BraillifyException.cs @@ -0,0 +1,14 @@ +namespace Braillify; + +using System; + +public sealed class BraillifyException : Exception +{ + public BraillifyException(string message) : base(message) + { + } + + public BraillifyException(string message, Exception innerException) : base(message, innerException) + { + } +} diff --git a/packages/dotnet/BraillifyNet/BraillifyNet.csproj b/packages/dotnet/BraillifyNet/BraillifyNet.csproj new file mode 100644 index 0000000..474d2fa --- /dev/null +++ b/packages/dotnet/BraillifyNet/BraillifyNet.csproj @@ -0,0 +1,28 @@ + + + netstandard2.0;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0;net9.0;net10.0 + latest + enable + true + + BraillifyNet + 0.1.0 + DevFive + Rust 기반 크로스플랫폼 한국어 점역 라이브러리 .NET 바인딩 + Apache-2.0 + https://braillify.kr + https://github.com/dev-five-git/braillify + braille;korean;translation;accessibility + README.md + + + + + + + + + PreserveNewest + + + diff --git a/packages/dotnet/BraillifyNet/NativeLibraryLoader.cs b/packages/dotnet/BraillifyNet/NativeLibraryLoader.cs new file mode 100644 index 0000000..8b696e8 --- /dev/null +++ b/packages/dotnet/BraillifyNet/NativeLibraryLoader.cs @@ -0,0 +1,131 @@ +#if NETCOREAPP3_0_OR_GREATER +// .NET Core 3.0+: NativeLibrary 클래스 사용 가능 + +namespace Braillify; + +using System; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +#if NET9_0_OR_GREATER +using System.Threading; +#endif + +internal static class NativeLibraryLoader +{ + private static bool _isLoaded; +#if NET9_0_OR_GREATER + // .NET 9+: Lock 클래스 사용 - 더 효율적인 동기화 + private static readonly Lock _lock = new(); +#else + private static readonly object _lock = new object(); +#endif + + internal static void EnsureLoaded() + { + if (_isLoaded) + { + return; + } + + lock (_lock) + { + if (_isLoaded) + { + return; + } + + NativeLibrary.SetDllImportResolver(typeof(NativeLibraryLoader).Assembly, DllImportResolver); + _isLoaded = true; + } + } + + private static IntPtr DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) + { + if (libraryName != "braillify_native") + { + return IntPtr.Zero; + } + + var rid = GetRuntimeIdentifier(); + var libraryFileName = GetLibraryFileName(); + + // AppContext.BaseDirectory 사용 (NativeAOT 호환) + var baseDir = AppContext.BaseDirectory; + var paths = new[] + { + // NuGet 패키지 구조: runtimes/{rid}/native/{lib} + Path.Combine(baseDir, "runtimes", rid, "native", libraryFileName), + + // 개발 환경: 직접 경로 + Path.Combine(baseDir, libraryFileName), + + // 상위 디렉토리 탐색 + Path.Combine(baseDir, "..", "runtimes", rid, "native", libraryFileName), + }; + + foreach (var path in paths) + { + if (File.Exists(path) && NativeLibrary.TryLoad(path, out var handle)) + { + return handle; + } + } + + // 기본 로딩 시도 (시스템 PATH 등) + if (NativeLibrary.TryLoad(libraryName, assembly, searchPath, out var defaultHandle)) + { + return defaultHandle; + } + + return IntPtr.Zero; + } + + private static string GetRuntimeIdentifier() + { + var arch = RuntimeInformation.ProcessArchitecture switch + { + Architecture.X64 => "x64", + Architecture.X86 => "x86", + Architecture.Arm64 => "arm64", + Architecture.Arm => "arm", + _ => "x64" + }; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return $"win-{arch}"; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return $"linux-{arch}"; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return $"osx-{arch}"; + } + + // 알 수 없는 플랫폼은 Linux로 가정 + return $"linux-{arch}"; + } + + private static string GetLibraryFileName() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return "braillify_native.dll"; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return "libbraillify_native.dylib"; + } + + // Linux 및 기타 + return "libbraillify_native.so"; + } +} + +#endif diff --git a/packages/dotnet/BraillifyNet/NativeMethods.cs b/packages/dotnet/BraillifyNet/NativeMethods.cs new file mode 100644 index 0000000..e4a61a6 --- /dev/null +++ b/packages/dotnet/BraillifyNet/NativeMethods.cs @@ -0,0 +1,136 @@ +#if NET7_0_OR_GREATER +// .NET 7+: LibraryImport (source generator) 사용 - 최고 성능 + +namespace Braillify; + +using System; +using System.Runtime.InteropServices; + +internal static partial class NativeMethods +{ + private const string LibraryName = "braillify_native"; + + [LibraryImport(LibraryName, StringMarshalling = StringMarshalling.Utf8)] + internal static partial nint braillify_encode(string text, out nuint outLen); + + [LibraryImport(LibraryName, StringMarshalling = StringMarshalling.Utf8)] + internal static partial nint braillify_encode_to_unicode(string text); + + [LibraryImport(LibraryName, StringMarshalling = StringMarshalling.Utf8)] + internal static partial nint braillify_encode_to_braille_font(string text); + + [LibraryImport(LibraryName)] + internal static partial nint braillify_get_last_error(); + + [LibraryImport(LibraryName)] + internal static partial void braillify_free_string(nint ptr); + + [LibraryImport(LibraryName)] + internal static partial void braillify_free_bytes(nint ptr, nuint len); +} + +#elif NET5_0_OR_GREATER +// .NET 5-6: DllImport with nint/nuint + +namespace Braillify; + +using System; +using System.Runtime.InteropServices; + +internal static class NativeMethods +{ + private const string LibraryName = "braillify_native"; + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] + internal static extern nint braillify_encode( + [MarshalAs(UnmanagedType.LPUTF8Str)] string text, + out nuint outLen); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] + internal static extern nint braillify_encode_to_unicode( + [MarshalAs(UnmanagedType.LPUTF8Str)] string text); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] + internal static extern nint braillify_encode_to_braille_font( + [MarshalAs(UnmanagedType.LPUTF8Str)] string text); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern nint braillify_get_last_error(); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void braillify_free_string(nint ptr); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void braillify_free_bytes(nint ptr, nuint len); +} + +#elif NETCOREAPP3_0_OR_GREATER +// .NET Core 3.x: DllImport with IntPtr/UIntPtr and LPUTF8Str + +namespace Braillify; + +using System; +using System.Runtime.InteropServices; + +internal static class NativeMethods +{ + private const string LibraryName = "braillify_native"; + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] + internal static extern IntPtr braillify_encode( + [MarshalAs(UnmanagedType.LPUTF8Str)] string text, + out UIntPtr outLen); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] + internal static extern IntPtr braillify_encode_to_unicode( + [MarshalAs(UnmanagedType.LPUTF8Str)] string text); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] + internal static extern IntPtr braillify_encode_to_braille_font( + [MarshalAs(UnmanagedType.LPUTF8Str)] string text); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern IntPtr braillify_get_last_error(); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void braillify_free_string(IntPtr ptr); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void braillify_free_bytes(IntPtr ptr, UIntPtr len); +} + +#else +// .NET Standard 2.0: 수동 UTF-8 마샬링이 필요한 DllImport (IntPtr/UIntPtr) + +namespace Braillify; + +using System; +using System.Runtime.InteropServices; + +internal static class NativeMethods +{ + private const string LibraryName = "braillify_native"; + + // .NET Standard 2.0에서는 byte[] 포인터를 직접 전달 + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern IntPtr braillify_encode( + IntPtr text, + out UIntPtr outLen); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern IntPtr braillify_encode_to_unicode(IntPtr text); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern IntPtr braillify_encode_to_braille_font(IntPtr text); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern IntPtr braillify_get_last_error(); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void braillify_free_string(IntPtr ptr); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void braillify_free_bytes(IntPtr ptr, UIntPtr len); +} + +#endif diff --git a/packages/dotnet/Cargo.toml b/packages/dotnet/Cargo.toml new file mode 100644 index 0000000..4a512c4 --- /dev/null +++ b/packages/dotnet/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "dotnet" +version = "0.1.0" +edition = "2024" + +[lib] +name = "braillify_native" +crate-type = ["cdylib"] + +[dependencies] +braillify = { path = "../../libs/braillify", default-features = false } diff --git a/packages/dotnet/README.md b/packages/dotnet/README.md new file mode 100644 index 0000000..ab05c9e --- /dev/null +++ b/packages/dotnet/README.md @@ -0,0 +1,77 @@ +# Braillify .NET + +Rust 기반 크로스플랫폼 한국어 점역 라이브러리 .NET 바인딩 + +## 패키지 + +| 패키지 | 설명 | NuGet | +| -------------- | ---------- | ---------------------------------------------------------------------------------------------------- | +| `BraillifyNet` | 라이브러리 | [![NuGet](https://img.shields.io/nuget/v/BraillifyNet)](https://www.nuget.org/packages/BraillifyNet) | +| `Braillify` | CLI 도구 | [![NuGet](https://img.shields.io/nuget/v/Braillify)](https://www.nuget.org/packages/Braillify) | + +## 라이브러리 설치 + +```bash +dotnet add package BraillifyNet +``` + +## 라이브러리 사용법 + +```csharp +using Braillify; + +// 텍스트를 점자 유니코드로 변환 +string braille = Braillify.EncodeToUnicode("안녕하세요"); +// 결과: "⠣⠒⠉⠻⠚⠠⠝⠬" + +// 텍스트를 점자 바이트 배열로 변환 +byte[] bytes = Braillify.Encode("안녕하세요"); + +// 텍스트를 점자 폰트 문자열로 변환 +string font = Braillify.EncodeToBrailleFont("안녕하세요"); +``` + +## CLI 설치 + +```bash +# 글로벌 설치 +dotnet tool install -g Braillify + +# 설치 없이 실행 (.NET 10+) +dnx braillify "안녕하세요" +``` + +## CLI 사용법 + +```bash +# 텍스트 변환 +braillify "안녕하세요" +# 출력: ⠣⠒⠉⠻⠚⠠⠝⠬ + +# 파이프 입력 +echo "안녕하세요" | braillify + +# REPL 모드 +braillify +``` + +## 지원 플랫폼 + +- Windows (x64, x86, arm64) +- Linux (x64, arm64) +- macOS (x64, arm64) + +## 지원 .NET 버전 + +- .NET Standard 2.0 +- .NET Core 3.1 +- .NET 5.0, 6.0, 7.0, 8.0, 9.0, 10.0 + +## 라이선스 + +Apache-2.0 + +## 링크 + +- [GitHub](https://github.com/dev-five-git/braillify) +- [홈페이지](https://braillify.kr) diff --git a/packages/dotnet/src/lib.rs b/packages/dotnet/src/lib.rs new file mode 100644 index 0000000..d751b16 --- /dev/null +++ b/packages/dotnet/src/lib.rs @@ -0,0 +1,159 @@ +use std::cell::RefCell; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; +use std::ptr; + +thread_local! { + static LAST_ERROR: RefCell> = const { RefCell::new(None) }; +} + +fn set_last_error(err: String) { + LAST_ERROR.with(|e| { + *e.borrow_mut() = Some(err); + }); +} + +fn clear_last_error() { + LAST_ERROR.with(|e| { + *e.borrow_mut() = None; + }); +} + +/// 마지막 에러 메시지를 반환합니다. 호출자가 braillify_free_string으로 해제해야 합니다. +/// Returns the last error message. Caller must free with braillify_free_string. +#[unsafe(no_mangle)] +pub extern "C" fn braillify_get_last_error() -> *mut c_char { + LAST_ERROR.with(|e| match e.borrow().as_ref() { + Some(msg) => CString::new(msg.clone()) + .map(|s| s.into_raw()) + .unwrap_or(ptr::null_mut()), + None => ptr::null_mut(), + }) +} + +/// 텍스트를 점자 바이트 배열로 인코딩합니다. +/// Encodes text to braille byte array. +/// 성공 시 바이트 배열 포인터 반환, 실패 시 null 반환. +/// Returns byte array pointer on success, null on failure. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn braillify_encode(text: *const c_char, out_len: *mut usize) -> *mut u8 { + clear_last_error(); + + if text.is_null() || out_len.is_null() { + set_last_error("Null pointer argument".to_string()); + return ptr::null_mut(); + } + + let c_str = unsafe { CStr::from_ptr(text) }; + let text_str = match c_str.to_str() { + Ok(s) => s, + Err(e) => { + set_last_error(format!("Invalid UTF-8: {}", e)); + return ptr::null_mut(); + } + }; + + match braillify::encode(text_str) { + Ok(result) => { + unsafe { *out_len = result.len() }; + let boxed = result.into_boxed_slice(); + Box::into_raw(boxed) as *mut u8 + } + Err(e) => { + set_last_error(e); + ptr::null_mut() + } + } +} + +/// 텍스트를 점자 유니코드 문자열로 인코딩합니다. +/// Encodes text to braille unicode string. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn braillify_encode_to_unicode(text: *const c_char) -> *mut c_char { + clear_last_error(); + + if text.is_null() { + set_last_error("Null pointer argument".to_string()); + return ptr::null_mut(); + } + + let c_str = unsafe { CStr::from_ptr(text) }; + let text_str = match c_str.to_str() { + Ok(s) => s, + Err(e) => { + set_last_error(format!("Invalid UTF-8: {}", e)); + return ptr::null_mut(); + } + }; + + match braillify::encode_to_unicode(text_str) { + Ok(result) => match CString::new(result) { + Ok(c_string) => c_string.into_raw(), + Err(e) => { + set_last_error(format!("CString conversion error: {}", e)); + ptr::null_mut() + } + }, + Err(e) => { + set_last_error(e); + ptr::null_mut() + } + } +} + +/// 텍스트를 점자 폰트 문자열로 인코딩합니다. +/// Encodes text to braille font string. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn braillify_encode_to_braille_font(text: *const c_char) -> *mut c_char { + clear_last_error(); + + if text.is_null() { + set_last_error("Null pointer argument".to_string()); + return ptr::null_mut(); + } + + let c_str = unsafe { CStr::from_ptr(text) }; + let text_str = match c_str.to_str() { + Ok(s) => s, + Err(e) => { + set_last_error(format!("Invalid UTF-8: {}", e)); + return ptr::null_mut(); + } + }; + + match braillify::encode_to_braille_font(text_str) { + Ok(result) => match CString::new(result) { + Ok(c_string) => c_string.into_raw(), + Err(e) => { + set_last_error(format!("CString conversion error: {}", e)); + ptr::null_mut() + } + }, + Err(e) => { + set_last_error(e); + ptr::null_mut() + } + } +} + +/// Rust에서 할당한 문자열을 해제합니다. +/// Frees a string allocated by Rust. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn braillify_free_string(ptr: *mut c_char) { + if !ptr.is_null() { + unsafe { + drop(CString::from_raw(ptr)); + } + } +} + +/// Rust에서 할당한 바이트 배열을 해제합니다. +/// Frees a byte array allocated by Rust. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn braillify_free_bytes(ptr: *mut u8, len: usize) { + if !ptr.is_null() { + unsafe { + let _ = Vec::from_raw_parts(ptr, len, len); + } + } +} diff --git a/uv.lock b/uv.lock index 5caf04a..0672740 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 1 +revision = 3 requires-python = ">=3.13" [manifest] @@ -11,6 +11,7 @@ members = [ [[package]] name = "braillify" +version = "1.0.11" source = { editable = "packages/python" } [[package]] @@ -41,45 +42,45 @@ source = { virtual = "." } name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] name = "iniconfig" version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, ] [[package]] name = "packaging" version = "25.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] [[package]] name = "pluggy" version = "1.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] name = "pygments" version = "2.19.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, ] [[package]] @@ -93,7 +94,7 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/aa/405082ce2749be5398045152251ac69c0f3578c7077efc53431303af97ce/pytest-8.4.0.tar.gz", hash = "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6", size = 1515232 } +sdist = { url = "https://files.pythonhosted.org/packages/fb/aa/405082ce2749be5398045152251ac69c0f3578c7077efc53431303af97ce/pytest-8.4.0.tar.gz", hash = "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6", size = 1515232, upload-time = "2025-06-02T17:36:30.03Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/de/afa024cbe022b1b318a3d224125aa24939e99b4ff6f22e0ba639a2eaee47/pytest-8.4.0-py3-none-any.whl", hash = "sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e", size = 363797 }, + { url = "https://files.pythonhosted.org/packages/2f/de/afa024cbe022b1b318a3d224125aa24939e99b4ff6f22e0ba639a2eaee47/pytest-8.4.0-py3-none-any.whl", hash = "sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e", size = 363797, upload-time = "2025-06-02T17:36:27.859Z" }, ]