From 8081038e3fcdd6d2930b56de85bed8884ceb0c4c Mon Sep 17 00:00:00 2001 From: Paul Mucur Date: Sat, 11 Jan 2025 15:59:06 +0000 Subject: [PATCH] Initial commit of rb-mrml --- .github/workflows/tests.yml | 322 ++++++++++++++++++++ .gitignore | 14 + CHANGELOG.md | 13 + Cargo.lock | 587 ++++++++++++++++++++++++++++++++++++ Cargo.toml | 7 + Gemfile | 9 + LICENSE | 11 + README.md | 48 +++ Rakefile | 17 ++ ext/mrml/Cargo.toml | 15 + ext/mrml/extconf.rb | 6 + ext/mrml/src/lib.rs | 22 ++ lib/mrml.rb | 10 + lib/mrml/version.rb | 5 + lib/rb-mrml.rb | 3 + rb-mrml.gemspec | 39 +++ scripts/test-gem-install | 16 + test/test_mrml.rb | 26 ++ 18 files changed, 1170 insertions(+) create mode 100644 .github/workflows/tests.yml create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 Gemfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 Rakefile create mode 100644 ext/mrml/Cargo.toml create mode 100644 ext/mrml/extconf.rb create mode 100644 ext/mrml/src/lib.rs create mode 100644 lib/mrml.rb create mode 100644 lib/mrml/version.rb create mode 100644 lib/rb-mrml.rb create mode 100644 rb-mrml.gemspec create mode 100755 scripts/test-gem-install create mode 100644 test/test_mrml.rb diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..5f206c7 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,322 @@ +name: Tests + +concurrency: + group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}" + cancel-in-progress: true + +on: + workflow_dispatch: + push: + branches: + - main + pull_request: + schedule: + - cron: "30 4 * * 4" + +jobs: + ruby-gem: + name: Package Ruby gem + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.4' + bundler-cache: true + - uses: actions-rust-lang/setup-rust-toolchain@v1 + - run: bundle exec rake build + - uses: actions/upload-artifact@v4 + with: + name: ruby-gem + path: pkg/*.gem + + test-ruby-gem: + needs: ruby-gem + runs-on: ubuntu-latest + name: Ruby ${{ matrix.ruby }} + strategy: + matrix: + ruby: + - '3.4' + - '3.3' + - '3.2' + - '3.1' + - '3.0' + - '2.7' + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + - uses: actions-rust-lang/setup-rust-toolchain@v1 + - uses: actions/download-artifact@v4 + with: + name: ruby-gem + path: pkg + - run: scripts/test-gem-install default + + cross-gem: + name: Compile native gem for ${{ matrix.platform }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + platform: + - arm-linux + - aarch64-linux + - aarch64-linux-musl + - arm64-darwin + - x64-mingw-ucrt + - x64-mingw32 + - x86_64-darwin + - x86_64-linux + - x86_64-linux-musl + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.4' + bundler-cache: true + - uses: oxidize-rb/actions/cross-gem@v1 + id: cross-gem + with: + platform: ${{ matrix.platform }} + ruby-versions: '2.7,3.0,3.1,3.2,3.3,3.4' + - uses: actions/upload-artifact@v4 + with: + name: cross-gem-${{ matrix.platform }} + path: ${{ steps.cross-gem.outputs.gem-path }} + + test-precompiled-arm-linux: + needs: "cross-gem" + strategy: + fail-fast: false + matrix: + ruby: ["2.7", "3.0", "3.1", "3.2", "3.3", "3.4"] + include: + - { ruby: "2.7", rubygems: "3.4.22" } + - { ruby: "3.0", rubygems: "3.5.23" } + - { ruby: "3.1", rubygems: "default" } + - { ruby: "3.2", rubygems: "default" } + - { ruby: "3.3", rubygems: "default" } + - { ruby: "3.4", rubygems: "default" } + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: cross-gem-arm-linux + path: pkg + - name: enable execution of multi-architecture containers by qemu + run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes + - run: | + docker run --rm -v "$(pwd):/rb-mrml" -w /rb-mrml \ + --platform=linux/arm/v7 \ + ruby:${{ matrix.ruby }} \ + ./scripts/test-gem-install ${{ matrix.rubygems }} + + test-precompiled-aarch64-linux: + needs: "cross-gem" + strategy: + fail-fast: false + matrix: + ruby: ["2.7", "3.0", "3.1", "3.2", "3.3", "3.4"] + include: + - { ruby: "2.7", rubygems: "3.4.22" } + - { ruby: "3.0", rubygems: "3.5.23" } + - { ruby: "3.1", rubygems: "default" } + - { ruby: "3.2", rubygems: "default" } + - { ruby: "3.3", rubygems: "default" } + - { ruby: "3.4", rubygems: "default" } + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: cross-gem-aarch64-linux + path: pkg + - name: enable execution of multi-architecture containers by qemu + run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes + - run: | + docker run --rm -v "$(pwd):/rb-mrml" -w /rb-mrml \ + --platform=linux/arm64 \ + ruby:${{ matrix.ruby }} \ + ./scripts/test-gem-install ${{ matrix.rubygems }} + + test-precompiled-aarch64-linux-musl: + needs: "cross-gem" + strategy: + fail-fast: false + matrix: + ruby: ["2.7", "3.0", "3.1", "3.2", "3.3", "3.4"] + include: + - { ruby: "2.7", rubygems: "3.4.22" } + - { ruby: "3.0", rubygems: "3.5.23" } + - { ruby: "3.1", rubygems: "default" } + - { ruby: "3.2", rubygems: "default" } + - { ruby: "3.3", rubygems: "default" } + - { ruby: "3.4", rubygems: "default" } + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: cross-gem-aarch64-linux-musl + path: pkg + - name: enable execution of multi-architecture containers by qemu + run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes + - run: | + docker run --rm -v "$(pwd):/rb-mrml" -w /rb-mrml \ + --platform=linux/arm64 \ + ruby:${{ matrix.ruby }}-alpine \ + ./scripts/test-gem-install ${{ matrix.rubygems }} + + test-precompiled-arm64-darwin: + needs: "cross-gem" + strategy: + fail-fast: false + matrix: + ruby: ["2.7", "3.0", "3.1", "3.2", "3.3", "3.4"] + include: + - { ruby: "2.7", rubygems: "3.4.22" } + - { ruby: "3.0", rubygems: "3.5.23" } + - { ruby: "3.1", rubygems: "default" } + - { ruby: "3.2", rubygems: "default" } + - { ruby: "3.3", rubygems: "default" } + - { ruby: "3.4", rubygems: "default" } + runs-on: macos-14 + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: "${{ matrix.ruby }}" + rubygems: "${{ matrix.rubygems }}" + bundler-cache: true + - uses: actions/download-artifact@v4 + with: + name: cross-gem-arm64-darwin + path: pkg + - run: ./scripts/test-gem-install default + + test-precompiled-x64-mingw-ucrt: + needs: "cross-gem" + strategy: + fail-fast: false + matrix: + ruby: ["3.1", "3.2", "3.3", "3.4"] + runs-on: windows-2022 + steps: + - uses: actions/checkout@v4 + - uses: MSP-Greg/setup-ruby-pkgs@v1 + with: + ruby-version: "${{ matrix.ruby }}" + bundler-cache: true + - uses: actions/download-artifact@v4 + with: + name: cross-gem-x64-mingw-ucrt + path: pkg + - run: ./scripts/test-gem-install default + shell: bash + + test-precompiled-x64-mingw32: + needs: "cross-gem" + strategy: + fail-fast: false + matrix: + ruby: ["2.7", "3.0"] + include: + - { ruby: "2.7", rubygems: "3.4.22" } + - { ruby: "3.0", rubygems: "3.5.23" } + runs-on: windows-2019 + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: "${{ matrix.ruby }}" + rubygems: "${{ matrix.rubygems }}" + bundler-cache: true + - uses: actions/download-artifact@v4 + with: + name: cross-gem-x64-mingw32 + path: pkg + - run: ./scripts/test-gem-install default + shell: bash + + test-precompiled-x86_64-darwin: + needs: "cross-gem" + strategy: + fail-fast: false + matrix: + ruby: ["2.7", "3.0", "3.1", "3.2", "3.3", "3.4"] + include: + - { ruby: "2.7", rubygems: "3.4.22" } + - { ruby: "3.0", rubygems: "3.5.23" } + - { ruby: "3.1", rubygems: "default" } + - { ruby: "3.2", rubygems: "default" } + - { ruby: "3.3", rubygems: "default" } + - { ruby: "3.4", rubygems: "default" } + runs-on: macos-13 + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: "${{ matrix.ruby }}" + rubygems: "${{ matrix.rubygems }}" + bundler-cache: true + - uses: actions/download-artifact@v4 + with: + name: cross-gem-x86_64-darwin + path: pkg + - run: ./scripts/test-gem-install default + + test-precompiled-x86_64-linux: + needs: "cross-gem" + strategy: + fail-fast: false + matrix: + ruby: ["2.7", "3.0", "3.1", "3.2", "3.3", "3.4"] + include: + - { ruby: "2.7", rubygems: "3.4.22" } + - { ruby: "3.0", rubygems: "3.5.23" } + - { ruby: "3.1", rubygems: "default" } + - { ruby: "3.2", rubygems: "default" } + - { ruby: "3.3", rubygems: "default" } + - { ruby: "3.4", rubygems: "default" } + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: "${{ matrix.ruby }}" + rubygems: "${{ matrix.rubygems }}" + bundler-cache: true + - uses: actions/download-artifact@v4 + with: + name: cross-gem-x86_64-linux + path: pkg + - run: ./scripts/test-gem-install default + + test-precompiled-x86_64-linux-musl: + needs: "cross-gem" + strategy: + fail-fast: false + matrix: + ruby: ["2.7", "3.0", "3.1", "3.2", "3.3", "3.4"] + include: + - { ruby: "2.7", rubygems: "3.4.22" } + - { ruby: "3.0", rubygems: "3.5.23" } + - { ruby: "3.1", rubygems: "default" } + - { ruby: "3.2", rubygems: "default" } + - { ruby: "3.3", rubygems: "default" } + - { ruby: "3.4", rubygems: "default" } + runs-on: ubuntu-latest + container: + image: "ruby:${{ matrix.ruby }}-alpine" + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: cross-gem-x86_64-linux-musl + path: pkg + - run: ./scripts/test-gem-install ${{ matrix.rubygems }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..566e747 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +/Gemfile.lock +/.bundle/ +/.yardoc +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/tmp/ +*.bundle +*.so +*.o +*.a +mkmf.log +target/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a77e1e1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.1.0] - 2025-01-11 + +### Added + +- The initial version of the mrml-ruby gem, providing Ruby bindings to the + reimplementation of the MJML markup language in Rust. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..671ca54 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,587 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "lazy_static", + "lazycell", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "enum_dispatch" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +dependencies = [ + "equivalent", + "hashbrown", + "serde", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "libloading" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +dependencies = [ + "cfg-if", + "windows-targets", +] + +[[package]] +name = "magnus" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d87ae53030f3a22e83879e666cb94e58a7bdf31706878a0ba48752994146dab" +dependencies = [ + "magnus-macros", + "rb-sys", + "rb-sys-env", + "seq-macro", +] + +[[package]] +name = "magnus-macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5968c820e2960565f647819f5928a42d6e874551cab9d88d75e3e0660d7f71e3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mrml" +version = "0.1.0" +dependencies = [ + "magnus", + "mrml 4.0.1", + "serde", +] + +[[package]] +name = "mrml" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a5fb21b06edad8397bd7e0e6dcef12013b33e979e3ae14f126899ba524f491e" +dependencies = [ + "enum-as-inner", + "enum_dispatch", + "indexmap", + "itertools 0.13.0", + "mrml-json-macros", + "rustc-hash 2.1.0", + "serde", + "serde_json", + "thiserror", + "xmlparser", +] + +[[package]] +name = "mrml-json-macros" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40cbf208a374588552a03b4e5763d87b7628294b50b8b93da681f660105414b3" +dependencies = [ + "Inflector", + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "proc-macro2" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rb-sys" +version = "0.9.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56aaf81d9efc195606456e91896297ee5ab2002381539f8ed1ba6b4f2e467f3b" +dependencies = [ + "rb-sys-build", +] + +[[package]] +name = "rb-sys-build" +version = "0.9.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "035b513baded6df2b90a8559efb1973c47ba42e16c21c5f0863dd2aa4dbd6abe" +dependencies = [ + "bindgen", + "lazy_static", + "proc-macro2", + "quote", + "regex", + "shell-words", + "syn", +] + +[[package]] +name = "rb-sys-env" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a35802679f07360454b418a5d1735c89716bde01d35b1560fc953c1415a0b3bb" + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "seq-macro" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3f0bf26fd526d2a95683cd0f87bf103b8539e2ca1ef48ce002d67aad59aa0b4" + +[[package]] +name = "serde" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.135" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d66b100 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +# This Cargo.toml is here to let externals tools (IDEs, etc.) know that this is +# a Rust project. Your extensions dependencies should be added to the Cargo.toml +# in the ext/ directory. + +[workspace] +members = ["./ext/mrml"] +resolver = "2" diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..77bbf97 --- /dev/null +++ b/Gemfile @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +gemspec + +gem "minitest" +gem "rake" +gem "rake-compiler" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..48ee443 --- /dev/null +++ b/LICENSE @@ -0,0 +1,11 @@ +Copyright (c) 2025 Paul Mucur. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b9f6b55 --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +# rb-mrml - Ruby bindings to MRML + +Ruby bindings to [MRML][], a reimplementation of the [MJML][] markup language in Rust. + +[![Build Status](https://github.com/mudge/rb-mrml/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/mudge/rb-mrml/actions) + +**Current version:** 0.1.0 +**Bundled MRML version:** 4.0.1 + +```ruby +MRML.to_html("") +#=> "..." +``` + +## Requirements + +This gem requires any of the following to run: + +* [Ruby](https://www.ruby-lang.org/en/) 2.7 to 3.4 + +### Native gems + +Where possible, a pre-compiled native gem will be provided for the following platforms: + +* Linux + * `aarch64-linux`, `arm-linux`, `x86-linux`, `x86_64-linux` + * `aarch64-linux-musl`, `x86_64-linux-musl` for [musl](https://musl.libc.org/)-based systems such as [Alpine](https://alpinelinux.org) +* macOS `x86_64-darwin` and `arm64-darwin` +* Windows `x64-mingw32` and `x64-mingw-ucrt` + +## Thanks + +* Thanks to [Bytecode Alliance's `wastime-rb`](https://github.com/bytecodealliance/wasmtime-rb/tree/main) as an example of a Ruby Rust extension +* Thanks to [Jonian Guveli's "MRML Ruby"](https://github.com/hardpixel/mrml-ruby) as another implementation of Ruby bindings to MRML for older versions of Ruby + +## Contact + +All issues and suggestions should go to [GitHub +Issues](https://github.com/mudge/rb-mrml/issues). + +## License + +This library is licensed under the BSD 3-Clause License, see `LICENSE`. + +Copyright © 2025, Paul Mucur. + + [MRML]: https://github.com/jdrouet/mrml/tree/main + [MJML]: https://documentation.mjml.io/ diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..e2aaeb8 --- /dev/null +++ b/Rakefile @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require "bundler/gem_tasks" +require "minitest/test_task" +require "rb_sys/extensiontask" + +Minitest::TestTask.create + +task build: :compile + +GEMSPEC = Gem::Specification.load("rb-mrml.gemspec") + +RbSys::ExtensionTask.new("mrml", GEMSPEC) do |ext| + ext.lib_dir = "lib/mrml" +end + +task default: %i[compile test] diff --git a/ext/mrml/Cargo.toml b/ext/mrml/Cargo.toml new file mode 100644 index 0000000..763c299 --- /dev/null +++ b/ext/mrml/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "mrml" +version = "0.1.0" +edition = "2021" +authors = ["Paul Mucur "] +license = "BSD-3-Clause" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +magnus = { version = "0.7" } +mrml = { version = "4.0" } +serde = { version = "1.0", features = ["derive"] } diff --git a/ext/mrml/extconf.rb b/ext/mrml/extconf.rb new file mode 100644 index 0000000..cdf3c56 --- /dev/null +++ b/ext/mrml/extconf.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +require "mkmf" +require "rb_sys/mkmf" + +create_rust_makefile("mrml/mrml") diff --git a/ext/mrml/src/lib.rs b/ext/mrml/src/lib.rs new file mode 100644 index 0000000..361bc5c --- /dev/null +++ b/ext/mrml/src/lib.rs @@ -0,0 +1,22 @@ +use magnus::{function, prelude::*, Error, Ruby}; + +fn to_html(ruby: &Ruby, input: String) -> Result { + let root = match mrml::parse(&input) { + Ok(root) => root, + Err(err) => return Err(Error::new(ruby.exception_arg_error(), err.to_string())), + }; + + let opts = mrml::prelude::render::RenderOptions::default(); + + match root.render(&opts) { + Ok(content) => Ok(content), + Err(err) => Err(Error::new(ruby.exception_standard_error(), err.to_string())), + } +} + +#[magnus::init] +fn init(ruby: &Ruby) -> Result<(), Error> { + let module = ruby.define_module("MRML")?; + module.define_singleton_method("to_html", function!(to_html, 1))?; + Ok(()) +} diff --git a/lib/mrml.rb b/lib/mrml.rb new file mode 100644 index 0000000..661e2a7 --- /dev/null +++ b/lib/mrml.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require_relative "mrml/version" + +begin + RUBY_VERSION =~ /(\d+\.\d+)/ + require "mrml/#{Regexp.last_match(1)}/mrml" +rescue LoadError + require "mrml/mrml" +end diff --git a/lib/mrml/version.rb b/lib/mrml/version.rb new file mode 100644 index 0000000..143f980 --- /dev/null +++ b/lib/mrml/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module MRML + VERSION = "0.1.0" +end diff --git a/lib/rb-mrml.rb b/lib/rb-mrml.rb new file mode 100644 index 0000000..5fe2a86 --- /dev/null +++ b/lib/rb-mrml.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +require_relative "mrml" diff --git a/rb-mrml.gemspec b/rb-mrml.gemspec new file mode 100644 index 0000000..a152a4b --- /dev/null +++ b/rb-mrml.gemspec @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require_relative "lib/mrml/version" + +Gem::Specification.new do |spec| + spec.name = "rb-mrml" + spec.version = MRML::VERSION + spec.license = "BSD-3-Clause" + spec.authors = ["Paul Mucur"] + spec.summary = "Ruby bindings to MRML" + spec.description = "Ruby bindings to MRML, a reimplementation of the MJML markup language in Rust." + spec.homepage = "https://github.com/mudge/rb-mrml" + spec.metadata = { + "bug_tracker_uri" => "https://github.com/mudge/rb-mrml/issues", + "changelog_uri" => "https://github.com/mudge/rb-mrml/blob/main/CHANGELOG.md", + "funding_uri" => "https://github.com/sponsors/mudge", + "homepage_uri" => "https://github.com/mudge/rb-mrml", + "source_code_uri" => "https://github.com/mudge/rb-mrml", + "rubygems_mfa_required" => "true" + } + spec.required_ruby_version = ">= 2.7.0" + spec.files = [ + "Cargo.lock", + "Cargo.toml", + "LICENSE", + "README.md", + "ext/mrml", + "ext/mrml/Cargo.toml", + "ext/mrml/extconf.rb", + "ext/mrml/src", + "ext/mrml/src/lib.rs", + "lib/mrml", + "lib/mrml.rb", + "lib/rb-mrml.rb", + "lib/mrml/version.rb" + ] + spec.extensions = ["ext/mrml/extconf.rb"] + spec.add_dependency("rb_sys", "~> 0.9.107") +end diff --git a/scripts/test-gem-install b/scripts/test-gem-install new file mode 100755 index 0000000..2ce3589 --- /dev/null +++ b/scripts/test-gem-install @@ -0,0 +1,16 @@ +#!/usr/bin/env sh + +set -eu + +rubygems=${1:-default} +shift + +if [ "$rubygems" != "default" ] +then + gem update --system "$rubygems" +fi + +gem install --no-document pkg/*.gem +bundle install +rm -rf lib +ruby test/test_mrml.rb diff --git a/test/test_mrml.rb b/test/test_mrml.rb new file mode 100644 index 0000000..419b5bd --- /dev/null +++ b/test/test_mrml.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require "minitest/autorun" +require "rb-mrml" + +class TestMRML < Minitest::Test + def test_that_it_has_a_version_number + refute_nil ::MRML::VERSION + end + + def test_it_produces_html + assert_includes MRML.to_html(""), "" + end + + def test_it_raises_an_argument_error_given_invalid_mjml + error = assert_raises(ArgumentError) { MRML.to_html("") } + + assert_includes error.message, "unexpected end of stream" + end + + def test_it_raises_an_argument_error_given_an_unexpected_element + error = assert_raises(ArgumentError) { MRML.to_html("") } + + assert_includes error.message, "unexpected element" + end +end