diff --git a/.github/workflows/npm.yml b/.github/workflows/npm.yml deleted file mode 100644 index 27873fa..0000000 --- a/.github/workflows/npm.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: NodeJS - -on: - push: - branches: [ "master" ] - pull_request: - branches: [ "master" ] - -jobs: - build: - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - node-version: [16.x, 18.x, 20.x] - - steps: - - uses: actions/checkout@v3 - - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }} - - - name: Build - run: | - npm run test:js diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..7ae27c3 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,41 @@ +name: test + +on: + push: + branches: + - main + pull_request: + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + node-version: [16, 18, 20, 22] + + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Install dependencies + run: npm install + + - name: Build bundle + run: | + npm run podman:build + npm run podman:bundle + + - name: Run JavaScript tests + run: npx jest + + - name: Run native Ruby tests + run: npm run test:rb diff --git a/Gemfile b/Gemfile index b5ef61a..d081a1b 100644 --- a/Gemfile +++ b/Gemfile @@ -6,10 +6,10 @@ gem 'equivalent-xml', path: 'vendor/equivalent-xml' gem 'ruby-ll', path: 'vendor/ruby-ll' gem 'htmlentities', path: 'vendor/htmlentities' gem 'opal', path: 'vendor/opal' +gem 'plurimath', path: 'vendor/plurimath' gem 'opal-rspec', '>= 1.1.0a' -gem 'nokogiri' gem 'rake' gem 'rspec' gem 'ae' @@ -22,6 +22,7 @@ gem 'rack' gem 'rackup' gem 'simplecov' gem 'ox' -gem 'unitsml' gem 'monitor' -gem 'mml' +gem 'unitsml', git: 'https://github.com/unitsml/unitsml-ruby.git', branch: 'fix/opal-database-payload-loader' +gem 'lutaml-model', github: "lutaml/lutaml-model", branch: "fix/opal-runtime-compatibility-changes" +gem 'omml', github: "plurimath/omml", branch: "fix/opal-runtime-compatibility-changes" diff --git a/build.sh b/build.sh index 7e395a2..73f71b9 100755 --- a/build.sh +++ b/build.sh @@ -1,17 +1,28 @@ #!/bin/sh +set -e + rm -rf tmp/ mkdir -p tmp/ cp src/* tmp/ +echo "** Generating UnitsML Opal payload" +bundle exec ruby scripts/generate_unitsml_opal_payload.rb tmp/unitsml_opal_payload.rb + echo "** Compiling Plurimath-Opal from Ruby with Opal" bundle exec opal --esm -sjruby \ -qerb \ -rcorelib/array/pack \ -ropal-parser \ + -qoga/setup_opal \ + -qll/setup_opal \ + -Itmp/ \ + -runitsml_opal_payload \ + -gplurimath \ -gunitsml \ -ghtmlentities \ -gparslet \ -gmonitor \ + -snokogiri \ -sox \ -sox/ox \ -sox.so \ diff --git a/demo.html b/demo.html index e64d57d..ce7e257 100644 --- a/demo.html +++ b/demo.html @@ -20,6 +20,7 @@ + diff --git a/package.json b/package.json index e647e9e..b28f1be 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,12 @@ "scripts": { "test": "npm run test:js && npm run test:rb", "test:js": "npm run build && jest", - "test:rb": "podman run --rm -it -v $(pwd):/srv:z plurimath-js /bin/bash -c './setup.sh && env/plurimath'", + "test:rb": "npm run test:rb:native", + "test:rb:opal": "podman run --rm -i -e OPAL_PREFORK_DISABLE=1 -v .:/srv:z plurimath-js /bin/bash -c './setup.sh && env/plurimath'", + "test:rb:native": "podman run --rm -i -e OPAL_PREFORK_DISABLE=1 -v .:/srv:z plurimath-js /bin/bash -c './setup.sh && bundle exec rspec spec/unitsml_opal_payload_generator_spec.rb'", "podman:build": "podman build -t plurimath-js .", - "podman:shell": "podman run --rm -it -v $(pwd):/srv:z plurimath-js", - "podman:bundle": "podman run --rm -it -v $(pwd):/srv:z plurimath-js /bin/bash -c './setup.sh && ./build.sh'", + "podman:shell": "podman run --rm -it -e OPAL_PREFORK_DISABLE=1 -v .:/srv:z plurimath-js", + "podman:bundle": "podman run --rm -i -e OPAL_PREFORK_DISABLE=1 -v .:/srv:z plurimath-js /bin/bash -c './setup.sh && ./build.sh'", "build": "npm run podman:build && npm run podman:bundle", "submodule:init": "git submodule init && npm run submodule:update", "submodule:update": "git submodule update" diff --git a/scripts/generate_unitsml_opal_payload.rb b/scripts/generate_unitsml_opal_payload.rb new file mode 100644 index 0000000..438e88e --- /dev/null +++ b/scripts/generate_unitsml_opal_payload.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require "ox" +require "unitsdb" + +module PlurimathJs + module UnitsmlOpalPayloadGenerator + module_function + + def build_payload_hash(database_path: nil) + source_path = database_path || Unitsdb.data_dir + ::Unitsdb::Database.from_db(source_path).to_hash + end + + def to_ruby_source(database_hash) + rendered_hash = database_hash.inspect + + <<~RUBY + # frozen_string_literal: true + + require "unitsml" + require "unitsml/unitsdb" + + Unitsml::Unitsdb::Database.load_opal_payload( + #{rendered_hash}.freeze, + ) + RUBY + end + + def write_to(file_path, database_hash: nil, database_path: nil) + payload_hash = database_hash || build_payload_hash(database_path: database_path) + absolute_path = File.expand_path(file_path) + + File.write(absolute_path, to_ruby_source(payload_hash)) + absolute_path + end + end +end + +if $PROGRAM_NAME == __FILE__ + output_path = ARGV[0] + database_path = ARGV[1] + + abort "Usage: ruby scripts/generate_unitsml_opal_payload.rb OUTPUT_PATH [DATABASE_PATH]" unless output_path + + PlurimathJs::UnitsmlOpalPayloadGenerator.write_to( + output_path, + database_path: database_path, + ) +end diff --git a/setup.sh b/setup.sh index bbaa05f..618b368 100755 --- a/setup.sh +++ b/setup.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -e git submodule init git submodule update @@ -14,4 +15,3 @@ env/oga rake rm -rf vendor/*/tmp/* rm -rf vendor/ruby-ll/lib/libll.so vendor/oga/lib/liboga.so - diff --git a/spec/to-mathml-intent.spec.js b/spec/to-mathml-intent.spec.js index ba8cfcb..f107423 100644 --- a/spec/to-mathml-intent.spec.js +++ b/spec/to-mathml-intent.spec.js @@ -1,9 +1,20 @@ import Plurimath from "../dist" describe('to mathml with intent', () => { - it('', () => { + it('adds MathML intent metadata when requested', () => { const math = new Plurimath('∑_𝑥^𝑦 𝑧', 'unicode') + const mathml = math.toMathml(true).trim() - expect(math.toMathml(true).trim()).toBe('\n \n \n \n \n 𝑥\n 𝑦\n \n \n 𝑧\n \n \n \n') + expect(mathml).toContain('intent=":sum(𝑥,𝑦,$naryand)"') + expect(mathml).toContain('') + expect(mathml).toMatch(/(∑|∑)<\/mo>/) + expect(mathml).toMatch(/\s*(𝑧|𝑧)<\/mi>\s*<\/mrow>/) + }) + + it('omits MathML intent metadata by default', () => { + const math = new Plurimath('∑_𝑥^𝑦 𝑧', 'unicode') + + expect(math.toMathml()).not.toContain(' intent=') + expect(math.toMathml()).not.toBe(math.toMathml(true)) }) }) diff --git a/spec/to-unicodemath.spec.js b/spec/to-unicodemath.spec.js index 5de7b7f..6eaef3a 100644 --- a/spec/to-unicodemath.spec.js +++ b/spec/to-unicodemath.spec.js @@ -1,9 +1,21 @@ import Plurimath from "../dist" describe('to unicodemath', () => { - it('', () => { + it('matches conversion of UnicodeMath equation to UnicodeMath', () => { const math = new Plurimath('∑_𝑥^𝑦 𝑧', 'unicode') expect(math.toUnicodemath().trim()).toBe('∑_(𝑥)^(𝑦)▒〖𝑧〗') }) + + it('matches conversion of LaTeX equation to UnicodeMath', () => { + const math = new Plurimath('M \\= \\begin{bmatrix} \\-\\sin λ_0 \\& \\cos λ_0 \\& 0 \\\\ \\-\\sin φ_0 \\cos λ_0 \\& \\-\\sin φ_0 \\sin λ_0 \\& \\cos φ_0 \\\\ \\cos φ_0 \\cos λ_0 \\& \\cos φ_0 \\sin λ_0 \\& \\sin φ_0 \\end{bmatrix}', 'latex') + + expect(math.toUnicodemath()).toBe('M = ⓢ(− sin⁡λ_(0)&cos⁡λ_(0)&0@− sin⁡φ_(0)cos⁡λ_(0)&− sin⁡φ_(0)sin⁡λ_(0)&cos⁡φ_(0)@cos⁡φ_(0)cos⁡λ_(0)&cos⁡φ_(0)sin⁡λ_(0)&sin⁡φ_(0))') + }) + + it('tests basic equation performing plus operation', () => { + const math = new Plurimath('M \\= a \\+ b', 'latex') + + expect(math.toUnicodemath()).toBe('M = a + b') + }) }) diff --git a/spec/unitsml.spec.js b/spec/unitsml.spec.js new file mode 100644 index 0000000..c73e54c --- /dev/null +++ b/spec/unitsml.spec.js @@ -0,0 +1,53 @@ +import Plurimath from "../dist" + +describe('unitsml asciimath', () => { + it('converts simple unit: kg', () => { + const math = new Plurimath('"unitsml(kg)"', 'asciimath') + + expect(math.toAsciimath()).toBe('rm(kg)') + expect(math.toLatex()).toBe('\\mathrm{kg}') + expect(math.toMathml()).toMatch(/\s*kg<\/mi>\s*<\/mstyle>/) + }) + + it('converts unit with value: 9.8 m/s^2', () => { + const math = new Plurimath('9.8 "unitsml(m/s^2)"', 'asciimath') + + expect(math.toAsciimath()).toBe('9.8 rm(m) * rm(s)^(- 2)') + expect(math.toLatex()).toBe('9.8 \\mathrm{m} \\cdot \\mathrm{s}^{- 2}') + }) + + it('converts frequency unit: Hz', () => { + const math = new Plurimath('"unitsml(Hz)"', 'asciimath') + + expect(math.toAsciimath()).toBe('rm(Hz)') + expect(math.toLatex()).toBe('\\mathrm{Hz}') + }) + + it('converts prefixed unit: MHz', () => { + const math = new Plurimath('"unitsml(MHz)"', 'asciimath') + + expect(math.toAsciimath()).toBe('rm(MHz)') + expect(math.toLatex()).toBe('\\mathrm{MHz}') + }) + + it('converts compound unit: g/mol', () => { + const math = new Plurimath('"unitsml(g/mol)"', 'asciimath') + + expect(math.toAsciimath()).toBe('rm(g) * rm(mol)^(- 1)') + expect(math.toLatex()).toBe('\\mathrm{g} \\cdot \\mathrm{mol}^{- 1}') + }) + + it('converts unit in expression context', () => { + const math = new Plurimath('1 "unitsml(A)"', 'asciimath') + + expect(math.toAsciimath()).toBe('1 rm(A)') + expect(math.toLatex()).toBe('1 \\mathrm{A}') + }) + + it('converts Hz in complex expression', () => { + const math = new Plurimath('540 xx 10^(12) "unitsml(Hz)"', 'asciimath') + + expect(math.toAsciimath()).toContain('rm(Hz)') + expect(math.toLatex()).toContain('\\mathrm{Hz}') + }) +}) diff --git a/spec/unitsml_opal_payload_generator_spec.rb b/spec/unitsml_opal_payload_generator_spec.rb new file mode 100644 index 0000000..31445fc --- /dev/null +++ b/spec/unitsml_opal_payload_generator_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require "tmpdir" +require_relative "../scripts/generate_unitsml_opal_payload" + +RSpec.describe PlurimathJs::UnitsmlOpalPayloadGenerator do + let(:database_hash) do + { + "schema_version" => "2.0.0", + "units" => [], + "prefixes" => [], + "quantities" => [], + "dimensions" => [], + "unit_systems" => [], + } + end + + describe ".to_ruby_source" do + it "renders a Ruby payload file that loads the Opal database payload" do + source = described_class.to_ruby_source(database_hash) + + expect(source).to include('require "unitsml"') + expect(source).to include("Unitsml::Unitsdb::Database.load_opal_payload(") + expect(source).not_to include("class OpalDatabase") + expect(source).not_to include("def self.from_db") + expect(source).to match(/"schema_version"\s*=>\s*"2\.0\.0"/) + expect(source).to include(".freeze") + end + + it "renders unit symbols as a Ruby literal" do + source = described_class.to_ruby_source( + database_hash.merge( + "units" => [ + { + "id" => "ohm", + "symbols" => ["Ω", "µΩ"], + }, + ], + ), + ) + + rendered_hash = source.match(/load_opal_payload\(\n (.*)\.freeze,/m)[1] + payload = eval(rendered_hash) # rubocop:disable Security/Eval + + expect(payload["units"].first["symbols"]).to eq(["Ω", "µΩ"]) + end + end + + describe ".write_to" do + it "writes the generated payload file to the requested path" do + file_path = nil + + Dir.mktmpdir do |dir| + file_path = described_class.write_to( + File.join(dir, "unitsml_opal_payload.rb"), + database_hash: database_hash, + ) + + generated_source = File.read(file_path) + expect(generated_source).to eq(described_class.to_ruby_source(database_hash)) + end + + expect(file_path).to end_with("unitsml_opal_payload.rb") + end + end +end diff --git a/src/index.ts b/src/index.ts index 7149e27..e9410a5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,4 @@ import "./plurimath-opal.js"; - Opal.require("plurimath"); export default class Plurimath { diff --git a/vendor/plurimath b/vendor/plurimath index 1474143..68564b2 160000 --- a/vendor/plurimath +++ b/vendor/plurimath @@ -1 +1 @@ -Subproject commit 1474143d3cb06f1744b7d79a61de1d346157f937 +Subproject commit 68564b20de4ade7c7ea60e6c3d62352489931df0