moxml 0.1.24 → 0.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1c61c5dcecbe10b9f0f2850a134c9a8c5e2ec7d0be5b5b1dbb828dac25019a55
4
- data.tar.gz: f418e1f7a7406bfcc0dd9e57bc2a6b372a332b0e6677821a9e2b7a8173871b45
3
+ metadata.gz: ed30996109d8cfb8057f216677cba8ca3cd3856287c38b6a7edab3251ebb2064
4
+ data.tar.gz: f93d7c13863ef9d437e40403e4a48434ed1e1c5530ea2424cdeff710740243a0
5
5
  SHA512:
6
- metadata.gz: 44ccdeb45ec15bdadb02a470bd8506f051ee22385a0832b78d17b607ef32a192b28f0ec716f7910f2ba22c106b0a48ffec85aaaf1dbe33f7ce4a1f4fd7108695
7
- data.tar.gz: 49ed1641cd66fe4efa02f0e072b07621f441e3936734aba4ebb58a2f9188fab5feae920c989b21476019b01fac1562afc36356059d4c979f9a41b8f3e29686ec
6
+ metadata.gz: 9b09c144a268507bb02a16a4c6ba7caf6d1a2fa003718a6fa0960a8b68ba03a09584a66fb9475b1c23cd2d79071f1cc8c313200749f997d317aa90e61fba3811
7
+ data.tar.gz: 6389858a232d82faad5f6bbfd22d4828801c7a6d45a55a2a0f08d28c44647a588b52d2106649d408777453b571815b40c191f48c374cc49b648c58ad2be1e835
@@ -21,7 +21,7 @@ jobs:
21
21
  runs-on: ubuntu-latest
22
22
  steps:
23
23
  - name: Checkout
24
- uses: actions/checkout@v4
24
+ uses: actions/checkout@v6
25
25
 
26
26
  - name: Setup Ruby
27
27
  uses: ruby/setup-ruby@v1
@@ -21,16 +21,33 @@ jobs:
21
21
  uses: ruby/setup-ruby@v1
22
22
  with:
23
23
  ruby-version: "3.4"
24
- bundler-cache: true
24
+ # NOTE: bundler-cache is disabled because the path-source oga
25
+ # and ruby-ll forks need ragel + ruby-ll outputs generated
26
+ # (rake vendor:prepare) before their C extensions can compile.
27
+ bundler-cache: false
25
28
 
26
29
  - name: Set up Node.js
27
30
  uses: actions/setup-node@v4
28
31
  with:
29
32
  node-version: "18"
30
33
 
34
+ - name: Install ragel
35
+ run: sudo apt-get update && sudo apt-get install -y ragel
36
+
37
+ - name: Install ruby-ll (for rake vendor:prepare)
38
+ run: gem install ruby-ll --no-document
39
+
40
+ - name: Generate ragel / ruby-ll outputs in vendored forks
41
+ # Run before `bundle install`: the path-source oga/ruby-ll forks
42
+ # gitignore their generated .rb/.c files (parser.rb, lexer.rb,
43
+ # ext/c/lexer.c). bundle install compiles the C extensions via
44
+ # each fork's extconf.rb, which requires ext/c/lexer.c to exist.
45
+ run: rake vendor:prepare
46
+
31
47
  - name: Install Opal dependencies
32
48
  run: |
33
49
  npm list -g opal-compiler || npm install -g opal-compiler
50
+ bundle install
34
51
  bundle exec rake opal:generate_entity_data
35
52
 
36
53
  - name: Run Opal tests
@@ -13,14 +13,43 @@ jobs:
13
13
  rake:
14
14
  runs-on: ubuntu-latest
15
15
  steps:
16
- - uses: actions/checkout@v4
17
- - uses: ruby/setup-ruby@v1
16
+ - uses: actions/checkout@v6
17
+ with:
18
+ submodules: "recursive"
19
+
20
+ - name: Set up Ruby
21
+ uses: ruby/setup-ruby@v1
18
22
  with:
19
23
  ruby-version: "3.3"
20
- bundler-cache: true
24
+ # bundler-cache is disabled because the path-source oga
25
+ # and ruby-ll forks need ragel + ruby-ll outputs generated
26
+ # (rake vendor:prepare) before their C extensions can compile.
27
+ bundler-cache: false
28
+
29
+ - name: Install ragel
30
+ run: sudo apt-get update && sudo apt-get install -y ragel
31
+
32
+ - name: Install ruby-ll (for rake vendor:prepare)
33
+ run: gem install ruby-ll --no-document
34
+
35
+ - name: Generate ragel / ruby-ll outputs in vendored forks
36
+ # Run before `bundle install`: the path-source oga/ruby-ll forks
37
+ # gitignore their generated .rb/.c files (parser.rb, lexer.rb,
38
+ # ext/c/lexer.c). bundle install compiles the C extensions via
39
+ # each fork's extconf.rb, which requires ext/c/lexer.c to exist.
40
+ run: rake vendor:prepare
41
+
21
42
  - name: Install dependencies
22
43
  run: bundle install
44
+
45
+ - name: Compile liboga / libll native extensions
46
+ # Bundler does not reliably build path-source gems' native
47
+ # extensions; compile them explicitly so `require "oga"` (which
48
+ # requires libll) resolves under CRuby.
49
+ run: rake vendor:compile
50
+
23
51
  - name: Run fast tests (unit + adapter + integration)
24
52
  run: bundle exec rake spec:fast
53
+
25
54
  - name: Run rubocop
26
55
  run: bundle exec rubocop
@@ -19,11 +19,34 @@ jobs:
19
19
  ruby: [ "3.1", "3.2", "3.3", "3.4", "4.0" ]
20
20
  category: [ metanorma, rfcxml, niso-jats ]
21
21
  steps:
22
- - uses: actions/checkout@v4
23
- - uses: ruby/setup-ruby@v1
22
+ - uses: actions/checkout@v6
23
+ with:
24
+ submodules: "recursive"
25
+
26
+ - name: Set up Ruby
27
+ uses: ruby/setup-ruby@v1
24
28
  with:
25
29
  ruby-version: ${{ matrix.ruby }}
26
- bundler-cache: true
30
+ # bundler-cache is disabled because the path-source oga
31
+ # and ruby-ll forks need ragel + ruby-ll outputs generated
32
+ # (rake vendor:prepare) before their C extensions can compile.
33
+ bundler-cache: false
34
+
35
+ - name: Install ragel
36
+ run: sudo apt-get update && sudo apt-get install -y ragel
37
+
38
+ - name: Install ruby-ll (for rake vendor:prepare)
39
+ run: gem install ruby-ll --no-document
40
+
41
+ - name: Generate ragel / ruby-ll outputs in vendored forks
42
+ run: rake vendor:prepare
43
+
44
+ - name: Install dependencies
45
+ run: bundle install
46
+
47
+ - name: Compile liboga / libll native extensions
48
+ run: rake vendor:compile
49
+
27
50
  - name: Run round-trip tests (${{ matrix.category }})
28
51
  run: bundle exec rspec spec/consistency/ --tag round_trip --tag fixture_category:${{ matrix.category }}
29
52
  env:
@@ -39,11 +62,31 @@ jobs:
39
62
  ruby: [ "3.3", "4.0" ]
40
63
  category: [ metanorma, rfcxml, niso-jats ]
41
64
  steps:
42
- - uses: actions/checkout@v4
43
- - uses: ruby/setup-ruby@v1
65
+ - uses: actions/checkout@v6
66
+ with:
67
+ submodules: "recursive"
68
+
69
+ - name: Set up Ruby
70
+ uses: ruby/setup-ruby@v1
44
71
  with:
45
72
  ruby-version: ${{ matrix.ruby }}
46
- bundler-cache: true
73
+ bundler-cache: false
74
+
75
+ - name: Install ragel
76
+ run: sudo apt-get update && sudo apt-get install -y ragel
77
+
78
+ - name: Install ruby-ll (for rake vendor:prepare)
79
+ run: gem install ruby-ll --no-document
80
+
81
+ - name: Generate ragel / ruby-ll outputs in vendored forks
82
+ run: rake vendor:prepare
83
+
84
+ - name: Install dependencies
85
+ run: bundle install
86
+
87
+ - name: Compile liboga / libll native extensions
88
+ run: rake vendor:compile
89
+
47
90
  - name: Run Nokogiri × Ox round-trip tests (${{ matrix.category }})
48
91
  run: bundle exec rspec spec/consistency/ --tag round_trip --tag fixture_category:${{ matrix.category }}
49
92
  env:
@@ -61,11 +104,31 @@ jobs:
61
104
  ruby: [ "3.3", "4.0" ]
62
105
  category: [ metanorma, rfcxml, niso-jats ]
63
106
  steps:
64
- - uses: actions/checkout@v4
65
- - uses: ruby/setup-ruby@v1
107
+ - uses: actions/checkout@v6
108
+ with:
109
+ submodules: "recursive"
110
+
111
+ - name: Set up Ruby
112
+ uses: ruby/setup-ruby@v1
66
113
  with:
67
114
  ruby-version: ${{ matrix.ruby }}
68
- bundler-cache: true
115
+ bundler-cache: false
116
+
117
+ - name: Install ragel
118
+ run: sudo apt-get update && sudo apt-get install -y ragel
119
+
120
+ - name: Install ruby-ll (for rake vendor:prepare)
121
+ run: gem install ruby-ll --no-document
122
+
123
+ - name: Generate ragel / ruby-ll outputs in vendored forks
124
+ run: rake vendor:prepare
125
+
126
+ - name: Install dependencies
127
+ run: bundle install
128
+
129
+ - name: Compile liboga / libll native extensions
130
+ run: rake vendor:compile
131
+
69
132
  - name: Run Nokogiri × REXML round-trip tests (${{ matrix.category }})
70
133
  run: bundle exec rspec spec/consistency/ --tag round_trip --tag fixture_category:${{ matrix.category }}
71
134
  env:
data/.gitmodules ADDED
@@ -0,0 +1,6 @@
1
+ [submodule "opal-oga"]
2
+ path = vendor/opal-oga
3
+ url = git@github.com:lutaml/opal-oga.git
4
+ [submodule "opal-ruby-ll"]
5
+ path = vendor/opal-ruby-ll
6
+ url = git@github.com:lutaml/opal-ruby-ll.git
data/.rubocop_todo.yml CHANGED
@@ -675,6 +675,8 @@ RSpec/SpecFilePathFormat:
675
675
  - '**/spec/routing/**/*'
676
676
  - 'spec/moxml/node_type_map_spec.rb'
677
677
  - 'spec/moxml/opal_rexml_adapter_spec.rb'
678
+ - 'spec/moxml/opal_oga_adapter_spec.rb'
679
+ - 'spec/moxml/adapter/platform_spec.rb'
678
680
  - 'spec/moxml/xpath/ast/node_spec.rb'
679
681
  - 'spec/moxml/xpath/cache_spec.rb'
680
682
  - 'spec/moxml/xpath/compiler_spec.rb'
data/Gemfile CHANGED
@@ -11,11 +11,19 @@ gem "benchmark-ips"
11
11
  gem "get_process_mem"
12
12
  gem "libxml-ruby", "~> 5.0"
13
13
  gem "nokogiri", "~> 1.18"
14
- gem "oga", "~> 3.4"
15
14
  gem "openssl", "~> 3.0"
16
15
  gem "ox", "~> 2.14"
17
16
  gem "rake"
18
17
  gem "rexml"
18
+
19
+ # Opal-compatible forks of oga and ruby-ll. The forks add pure-Ruby lexer
20
+ # and driver fallbacks (under ext/pureruby/) plus an Opal-aware conditional
21
+ # in lib/oga.rb / lib/ll/setup.rb that selects the pure-Ruby implementation
22
+ # when RUBY_PLATFORM == 'opal'. Under CRuby/JRuby the forks behave
23
+ # identically to upstream (the conditional falls through to liboga/libll).
24
+ gem "oga", path: "vendor/opal-oga"
25
+ gem "ruby-ll", path: "vendor/opal-ruby-ll"
26
+
19
27
  gem "rspec"
20
28
  gem "rubocop"
21
29
  gem "rubocop-performance"
data/README.adoc CHANGED
@@ -889,6 +889,130 @@ performance optimization, and testing strategies, see
889
889
  link:docs/_pages/best-practices.adoc[Best Practices Guide].
890
890
 
891
891
 
892
+ == Running under Opal (JavaScript)
893
+
894
+ Opal compiles Ruby to JavaScript and cannot load C extensions. To run
895
+ Moxml on Opal you need three things: an adapter with no native code, a
896
+ XML library fork that exposes pure-Ruby fallbacks for its lexer and
897
+ parser driver, and an Opal build that puts those pure-Ruby
898
+ implementations on the load path.
899
+
900
+ === Default adapter under Opal
901
+
902
+ Under `RUBY_ENGINE == "opal"`, Moxml's default adapter is `:oga` (see
903
+ <<Default adapter selection>>). Oga is a pure-Ruby XML parser; under
904
+ standard CRuby its lexer and parsers are accelerated by the `liboga`
905
+ and `libll` C extensions. Those extensions cannot be compiled into
906
+ JavaScript, so on Opal they are replaced by the pure-Ruby fallbacks
907
+ shipped in `ext/pureruby/` of the Opal-compatible forks below.
908
+
909
+ The REXML adapter is also available on Opal (`Moxml.new(:rexml)`) but
910
+ relies on a number of compatibility shims because REXML uses Ruby
911
+ regular-expression features (the `/n` flag, `\u{...}` codepoint
912
+ escapes, inline flag groups) and String APIs that do not all transpile
913
+ to JavaScript today. `:oga` is the recommended path for new Opal
914
+ projects.
915
+
916
+ === Required XML library forks
917
+
918
+ Upstream https://github.com/yorickpeterse/oga[oga] and
919
+ https://github.com/yorickpeterse/ruby-ll[ruby-ll] do not currently
920
+ ship Opal-compatible fallbacks. Moxml vendors the following forks:
921
+
922
+ * https://github.com/lutaml/opal-oga[lutaml/opal-oga] — oga with an
923
+ `ext/pureruby/oga/native/lexer.rb` fallback that fires when
924
+ `RUBY_PLATFORM == 'opal'`. Under CRuby/JRuby the fork behaves
925
+ identically to upstream oga.
926
+ * https://github.com/lutaml/opal-ruby-ll[lutaml/opal-ruby-ll] —
927
+ ruby-ll with `ext/pureruby/ll/native/{driver,driver_config}.rb`
928
+ fallbacks selected by the same conditional in `lib/ll/setup.rb`.
929
+
930
+ Both forks are vendored as submodules under `vendor/opal-oga` and
931
+ `vendor/opal-ruby-ll` in the Moxml repository. Moxml's own `Gemfile`
932
+ references them via `path:` source so that any consumer of Moxml under
933
+ Opal resolves to the same fork commits Moxml was tested against.
934
+
935
+ === Wiring the forks in a downstream Gemfile
936
+
937
+ Downstream libraries that run on Opal (for example
938
+ https://github.com/plurimath/plurimath-js[plurimath-js]) must also use
939
+ the Opal-compatible forks rather than upstream `oga` and `ruby-ll`.
940
+ Wire them as `path:` or `git:` sources so the fork's
941
+ `RUBY_PLATFORM == 'opal'` conditional can fire:
942
+
943
+ [source,ruby]
944
+ ----
945
+ # In your downstream gem's Gemfile (or gemspec)
946
+ gem "moxml", "~> 0.1.24" # provides the :oga adapter
947
+ gem "oga", git: "https://github.com/lutaml/opal-oga.git",
948
+ ref: "<commit-sha-moxml-tests-against>"
949
+ gem "ruby-ll", git: "https://github.com/lutaml/opal-ruby-ll.git",
950
+ ref: "<commit-sha-moxml-tests-against>"
951
+ ----
952
+
953
+ Pin the `ref:` to the submodule commits Moxml ships in
954
+ `vendor/opal-oga` and `vendor/opal-ruby-ll` (recorded in
955
+ `.gitmodules`) so your fork and Moxml's fork stay in lockstep. If you
956
+ prefer to vendor the forks yourself, replace the `git:` source with
957
+ `path: "vendor/opal-oga"` and `path: "vendor/opal-ruby-ll"` after
958
+ copying them in.
959
+
960
+ === Opal build configuration
961
+
962
+ Under Opal, the forks' `lib/` directory is on the load path (via the
963
+ gem), but the pure-Ruby implementations live under `ext/pureruby/`.
964
+ That directory must be added to Opal's load path so the
965
+ `if RUBY_PLATFORM == 'opal'` branch in `lib/oga.rb` and
966
+ `lib/ll/setup.rb` resolves to the pure-Ruby files instead of trying to
967
+ load the C extension.
968
+
969
+ For consumers using https://github.com/opal/opal[Opal] directly:
970
+
971
+ [source,ruby]
972
+ ----
973
+ require "opal"
974
+
975
+ # Opal path: put both lib/ and ext/pureruby/ on the load path so the
976
+ # fork's conditional requires resolve to the pure-Ruby fallback.
977
+ %w[opal-oga opal-ruby-ll].each do |fork|
978
+ Opal.append_path File.expand_path("vendor/#{fork}/lib", __dir__)
979
+ Opal.append_path File.expand_path("vendor/#{fork}/ext/pureruby", __dir__)
980
+ end
981
+
982
+ # Moxml's Opal boot file (lib/compat/opal/moxml_boot.rb) explicitly
983
+ # requires every module that would normally be autoloaded under CRuby.
984
+ require "moxml_boot"
985
+ ----
986
+
987
+ For consumers using https://github.com/opal/opal-compiler[Opal via
988
+ Sprockets/Webpack], the equivalent is to add both `lib/` and
989
+ `ext/pureruby/` from each fork to the Opal `paths` array.
990
+
991
+ === Verifying the Opal build
992
+
993
+ Moxml's own Opal test suite exercises the wiring described above. To
994
+ reproduce:
995
+
996
+ [source,shell]
997
+ ----
998
+ # One-time setup: install ragel + ruby-ll so the forks' gitignored
999
+ # generated lexer/parser sources can be produced.
1000
+ gem install ruby-ll --no-document
1001
+ # (Install ragel via your package manager, e.g. apt-get install ragel
1002
+ # or brew install ragel.)
1003
+
1004
+ # Generate the gitignored outputs in the vendored forks.
1005
+ bundle exec rake vendor:prepare
1006
+
1007
+ # Run the Opal (JavaScript) test suite.
1008
+ bundle exec rake spec:opal
1009
+ ----
1010
+
1011
+ Reference implementation: https://github.com/plurimath/plurimath-js[plurimath-js]
1012
+ is the canonical downstream consumer; its `Rakefile` and `env/` wrapper
1013
+ scripts show the full pattern end-to-end.
1014
+
1015
+
892
1016
  == Specific adapter limitations
893
1017
 
894
1018
  === Ox adapter
data/Rakefile CHANGED
@@ -1,9 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler/gem_tasks"
4
- require "rspec/core/rake_task"
5
4
 
6
- RSpec::Core::RakeTask.new(:spec)
5
+ # vendor:prepare must be runnable before `bundle install` (CI runs it
6
+ # first so the path-source oga/ruby-ll forks' gitignored lexer/parser
7
+ # outputs exist before their extconf.rb runs). Guard the rspec/opal
8
+ # requires so the file loads with only `rake` + `bundler` available.
9
+ begin
10
+ require "rspec/core/rake_task"
11
+ RSpec::Core::RakeTask.new(:spec)
12
+ rescue LoadError
13
+ end
7
14
 
8
15
  begin
9
16
  require "opal/rspec/rake_task"
@@ -17,15 +24,110 @@ begin
17
24
  File.exist?(File.join(p, "rexml", "document.rb"))
18
25
  end
19
26
  Opal.append_path rexml_lib if rexml_lib
27
+
28
+ # The Opal-compatible oga and ruby-ll forks (vendored as submodules)
29
+ # expose pure-Ruby implementations under ext/pureruby/. Their top-level
30
+ # lib/oga.rb and lib/ll/setup.rb conditionally require them when
31
+ # RUBY_PLATFORM == 'opal'. Both lib/ and ext/pureruby/ must be on
32
+ # Opal's load path so the conditional resolves correctly.
33
+ %w[opal-oga opal-ruby-ll].each do |fork_name|
34
+ fork_path = File.expand_path("vendor/#{fork_name}", __dir__)
35
+ Opal.append_path File.join(fork_path, "lib")
36
+ Opal.append_path File.join(fork_path, "ext/pureruby")
37
+ end
20
38
  end
21
39
  rescue LoadError
22
40
  # Opal not available or incompatible with current Ruby version
23
41
  end
24
42
 
25
- require "rubocop/rake_task"
43
+ begin
44
+ require "rubocop/rake_task"
45
+ RuboCop::RakeTask.new
46
+ rescue LoadError
47
+ end
48
+
49
+ # Regenerate the ragel / ruby-ll outputs that the Opal-compatible forks
50
+ # (vendored as submodules under vendor/) gitignore. The forks ship the
51
+ # grammar sources (.rl / .rll) but not the generated .rb / .c, since
52
+ # those are large and version-controllable upstream. Both ragel and
53
+ # ruby-ll must be on PATH; the upstream ruby-ll gem is sufficient for
54
+ # generation (the fork is only needed at runtime).
55
+ namespace :vendor do
56
+ desc "Generate ragel / ruby-ll outputs in vendored opal-oga and opal-ruby-ll"
57
+ task :prepare do
58
+ require "fileutils"
59
+
60
+ oga = File.expand_path("vendor/opal-oga", __dir__)
61
+ ruby_ll = File.expand_path("vendor/opal-ruby-ll", __dir__)
62
+
63
+ generators = [
64
+ # oga: ruby-ll grammar → Ruby parser
65
+ ["ruby-ll #{oga}/lib/oga/xml/parser.rll -o #{oga}/lib/oga/xml/parser.rb",
66
+ "#{oga}/lib/oga/xml/parser.rb",
67
+ "#{oga}/lib/oga/xml/parser.rll"],
68
+ ["ruby-ll #{oga}/lib/oga/xpath/parser.rll -o #{oga}/lib/oga/xpath/parser.rb",
69
+ "#{oga}/lib/oga/xpath/parser.rb",
70
+ "#{oga}/lib/oga/xpath/parser.rll"],
71
+ ["ruby-ll #{oga}/lib/oga/css/parser.rll -o #{oga}/lib/oga/css/parser.rb",
72
+ "#{oga}/lib/oga/css/parser.rb",
73
+ "#{oga}/lib/oga/css/parser.rll"],
74
+ # oga: ragel Ruby lexer
75
+ ["ragel -R -F1 #{oga}/lib/oga/xpath/lexer.rl -o #{oga}/lib/oga/xpath/lexer.rb",
76
+ "#{oga}/lib/oga/xpath/lexer.rb",
77
+ "#{oga}/lib/oga/xpath/lexer.rl"],
78
+ ["ragel -R -F1 #{oga}/lib/oga/css/lexer.rl -o #{oga}/lib/oga/css/lexer.rb",
79
+ "#{oga}/lib/oga/css/lexer.rb",
80
+ "#{oga}/lib/oga/css/lexer.rl"],
81
+ # oga: ragel C lexer for liboga
82
+ ["ragel -C -I #{oga}/ext/ragel -G2 #{oga}/ext/c/lexer.rl -o #{oga}/ext/c/lexer.c",
83
+ "#{oga}/ext/c/lexer.c",
84
+ "#{oga}/ext/c/lexer.rl"],
85
+ # ruby-ll: ruby-ll grammar → Ruby parser
86
+ ["ruby-ll #{ruby_ll}/lib/ll/parser.rll -o #{ruby_ll}/lib/ll/parser.rb --no-requires",
87
+ "#{ruby_ll}/lib/ll/parser.rb",
88
+ "#{ruby_ll}/lib/ll/parser.rll"],
89
+ ]
26
90
 
27
- RuboCop::RakeTask.new
91
+ generators.each do |cmd, output, source|
92
+ if File.exist?(output) && File.mtime(output) >= File.mtime(source)
93
+ next
94
+ end
28
95
 
96
+ FileUtils.mkdir_p(File.dirname(output))
97
+ sh cmd
98
+ end
99
+ end
100
+
101
+ # Bundler does not reliably compile native extensions for path-source
102
+ # gems. Build liboga/libll explicitly so `require "oga"` resolves.
103
+ desc "Compile liboga / libll native extensions for vendored forks"
104
+ task :compile do
105
+ require "fileutils"
106
+ require "rbconfig"
107
+
108
+ dlext = RbConfig::CONFIG["DLEXT"]
109
+
110
+ {
111
+ "vendor/opal-ruby-ll" => "libll",
112
+ "vendor/opal-oga" => "liboga",
113
+ }.each do |fork_path, ext_name|
114
+ abs_fork = File.expand_path(fork_path, __dir__)
115
+ lib_bundle = File.join(abs_fork, "lib", "#{ext_name}.#{dlext}")
116
+ ext_dir = File.join(abs_fork, "ext", "c")
117
+ extconf = File.join(ext_dir, "extconf.rb")
118
+ lib_dir = File.join(abs_fork, "lib")
119
+ next if File.exist?(lib_bundle)
120
+
121
+ Dir.chdir(ext_dir) do
122
+ sh "ruby #{extconf}"
123
+ sh "make"
124
+ FileUtils.cp("#{ext_name}.#{dlext}", lib_dir)
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ if defined?(RSpec)
29
131
  namespace :spec do
30
132
  if defined?(Opal::RSpec::RakeTask)
31
133
  desc "Run Opal (JavaScript) tests"
@@ -34,12 +136,21 @@ namespace :spec do
34
136
  server.append_path "spec"
35
137
 
36
138
  runner.default_path = "spec"
139
+ # `oga` and `ll/setup` must be required before moxml_boot so that
140
+ # the forks' Opal-aware conditional requires fire (lib/oga.rb calls
141
+ # `require 'oga/native/lexer'` when RUBY_PLATFORM == 'opal'; that
142
+ # resolves against vendor/opal-oga/ext/pureruby/, which the global
143
+ # Opal.append_path calls above add to the load path).
37
144
  runner.requires = %w[rexml_compat rexml/document rexml/xpath
145
+ oga ll/setup
38
146
  moxml_boot spec_helper support/opal]
39
147
  runner.files = Dir.glob("spec/moxml/*opal*_spec.rb") +
40
148
  Dir.glob("spec/moxml/native_attachment/opal_spec.rb") +
41
149
  Dir.glob("spec/moxml/adapter/shared_examples/*.rb")
42
150
  end
151
+
152
+ desc "Alias for spec:opal that also runs vendor:prepare"
153
+ task opal: "vendor:prepare"
43
154
  end
44
155
 
45
156
  desc "Validate XML fixtures are well-formed (requires xmllint)"
@@ -124,6 +235,7 @@ namespace :spec do
124
235
  task all: %i[unit adapter integration consistency examples
125
236
  performance]
126
237
  end
238
+ end
127
239
 
128
240
  namespace :benchmark do
129
241
  desc "Run XPath performance benchmarks"
@@ -35,6 +35,10 @@ require "moxml/sax/element_handler"
35
35
  require "moxml/sax/block_handler"
36
36
  require "moxml/sax/namespace_splitter"
37
37
  require "moxml/adapter/rexml"
38
+ require "moxml/adapter/customized_oga"
39
+ require "moxml/adapter/customized_oga/xml_declaration"
40
+ require "moxml/adapter/customized_oga/xml_generator"
41
+ require "moxml/adapter/oga"
38
42
  require "moxml/document_builder"
39
43
  require "moxml/builder"
40
44
  require "moxml/context"
data/lib/moxml/adapter.rb CHANGED
@@ -11,9 +11,11 @@ module Moxml
11
11
  AVAILABLE_ADAPTERS = %i[nokogiri oga rexml ox headed_ox libxml].freeze
12
12
 
13
13
  # Adapters that work under the Opal (JavaScript) runtime.
14
- # REXML is pure Ruby and Opal reimplements strscan/stringio in its stdlib,
15
- # enabling REXML to compile cleanly to JavaScript.
16
- OPAL_AVAILABLE_ADAPTERS = %i[rexml].freeze
14
+ # Oga is pure Ruby and designed with Opal compatibility in mind — it is the
15
+ # canonical XML parser for the JavaScript runtime. REXML is also pure Ruby
16
+ # but requires extensive runtime compat shims (regex features like /n,
17
+ # \u{...}, (?-mix:...) don't transpile cleanly), so it is opt-in only.
18
+ OPAL_AVAILABLE_ADAPTERS = %i[oga rexml].freeze
17
19
 
18
20
  # Registry mapping adapter names to their class name suffixes.
19
21
  # Special cases (like :headed_ox → "HeadedOx") live here instead of
data/lib/moxml/config.rb CHANGED
@@ -7,7 +7,7 @@ module Moxml
7
7
  VALID_LINE_ENDINGS = [LINE_ENDING_LF, LINE_ENDING_CRLF].freeze
8
8
  VALID_ADAPTERS = %i[nokogiri oga rexml ox headed_ox libxml].freeze
9
9
  DEFAULT_ADAPTER = :nokogiri
10
- OPAL_DEFAULT_ADAPTER = :rexml
10
+ OPAL_DEFAULT_ADAPTER = :oga
11
11
 
12
12
  # Entity loading modes:
13
13
  # - :required - Must load entities, raise error if unavailable (default)
data/lib/moxml/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- VERSION = "0.1.24"
4
+ VERSION = "0.1.25"
5
5
  end
data/moxml.gemspec CHANGED
@@ -27,7 +27,7 @@ Gem::Specification.new do |spec|
27
27
  # RubyGem that have been added into git.
28
28
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
29
29
  `git ls-files -z`.split("\x0").reject do |f|
30
- f.match(%r{^(test|features)/})
30
+ f.match(%r{^(test|features|vendor/)})
31
31
  end
32
32
  end
33
33
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
@@ -2,69 +2,87 @@
2
2
 
3
3
  require "spec_helper"
4
4
 
5
- RSpec.describe Moxml::Adapter, ".platform_adapters" do
6
- it "includes all known adapters under MRI" do
7
- expect(described_class.platform_adapters).to include(:nokogiri, :oga,
8
- :rexml, :ox)
9
- end
10
-
11
- it "uses the AVAILABLE_ADAPTERS constant under MRI" do
12
- expect(described_class.platform_adapters).to eq(Moxml::Adapter::AVAILABLE_ADAPTERS)
13
- end
14
- end
15
-
16
- RSpec.describe Moxml::Adapter, ".available?" do
17
- it "returns true for :oga" do
18
- expect(described_class.available?(:oga)).to be true
19
- end
5
+ RSpec.describe Moxml::Adapter do
6
+ describe ".platform_adapters" do
7
+ it "includes all known adapters under MRI" do
8
+ expect(described_class.platform_adapters).to include(:nokogiri, :oga,
9
+ :rexml, :ox)
10
+ end
20
11
 
21
- it "returns true for :nokogiri under MRI" do
22
- expect(described_class.available?(:nokogiri)).to be true
12
+ it "uses the AVAILABLE_ADAPTERS constant under MRI" do
13
+ expect(described_class.platform_adapters).to eq(Moxml::Adapter::AVAILABLE_ADAPTERS)
14
+ end
23
15
  end
24
16
 
25
- context "when Opal platform adapters are in effect" do
26
- before do
27
- allow(described_class).to receive(:platform_adapters)
28
- .and_return(Moxml::Adapter::OPAL_AVAILABLE_ADAPTERS)
17
+ describe ".available?" do
18
+ it "returns true for :oga" do
19
+ expect(described_class.available?(:oga)).to be true
29
20
  end
30
21
 
31
- it "returns false for :nokogiri" do
32
- expect(described_class.available?(:nokogiri)).to be false
22
+ it "returns true for :nokogiri under MRI" do
23
+ expect(described_class.available?(:nokogiri)).to be true
33
24
  end
34
25
 
35
- it "returns true for :rexml" do
36
- expect(described_class.available?(:rexml)).to be true
26
+ context "when Opal platform adapters are in effect" do
27
+ before do
28
+ allow(described_class).to receive(:platform_adapters)
29
+ .and_return(Moxml::Adapter::OPAL_AVAILABLE_ADAPTERS)
30
+ end
31
+
32
+ it "returns false for :nokogiri" do
33
+ expect(described_class.available?(:nokogiri)).to be false
34
+ end
35
+
36
+ it "returns true for :oga" do
37
+ expect(described_class.available?(:oga)).to be true
38
+ end
39
+
40
+ it "returns true for :rexml" do
41
+ expect(described_class.available?(:rexml)).to be true
42
+ end
37
43
  end
38
44
  end
39
- end
40
45
 
41
- RSpec.describe Moxml::Adapter, ".load" do
42
- context "when Opal platform adapters are in effect" do
43
- before do
44
- allow(described_class).to receive(:platform_adapters)
45
- .and_return(Moxml::Adapter::OPAL_AVAILABLE_ADAPTERS)
46
- end
46
+ describe ".load" do
47
+ context "when Opal platform adapters are in effect" do
48
+ before do
49
+ allow(described_class).to receive(:platform_adapters)
50
+ .and_return(Moxml::Adapter::OPAL_AVAILABLE_ADAPTERS)
51
+ end
47
52
 
48
- it "raises AdapterError for :nokogiri" do
49
- expect { described_class.load(:nokogiri) }.to raise_error(
50
- Moxml::AdapterError, /not available on this platform/
51
- )
53
+ it "raises AdapterError for :nokogiri" do
54
+ expect { described_class.load(:nokogiri) }.to raise_error(
55
+ Moxml::AdapterError, /not available on this platform/
56
+ )
57
+ end
52
58
  end
53
59
  end
54
- end
55
60
 
56
- RSpec.describe "Moxml::Adapter::OPAL_AVAILABLE_ADAPTERS" do
57
- it "contains only :rexml" do
58
- expect(Moxml::Adapter::OPAL_AVAILABLE_ADAPTERS).to eq(%i[rexml])
61
+ describe "OPAL_AVAILABLE_ADAPTERS" do
62
+ it "lists :oga as the primary Opal adapter, with :rexml as opt-in" do
63
+ expect(Moxml::Adapter::OPAL_AVAILABLE_ADAPTERS).to eq(%i[oga rexml])
64
+ end
59
65
  end
60
- end
61
66
 
62
- RSpec.describe "Moxml::Adapter::CONST_NAME_MAP" do
63
- it "maps :headed_ox to HeadedOx" do
64
- expect(Moxml::Adapter::CONST_NAME_MAP[:headed_ox]).to eq("HeadedOx")
67
+ describe "CONST_NAME_MAP" do
68
+ it "maps :headed_ox to HeadedOx" do
69
+ expect(Moxml::Adapter::CONST_NAME_MAP[:headed_ox]).to eq("HeadedOx")
70
+ end
71
+
72
+ it "falls back to capitalize for unmapped adapters" do
73
+ expect(Moxml::Adapter::CONST_NAME_MAP[:nokogiri]).to be_nil
74
+ end
65
75
  end
66
76
 
67
- it "falls back to capitalize for unmapped adapters" do
68
- expect(Moxml::Adapter::CONST_NAME_MAP[:nokogiri]).to be_nil
77
+ describe "default adapter / available list invariants" do
78
+ it "DEFAULT_ADAPTER is a member of AVAILABLE_ADAPTERS" do
79
+ expect(Moxml::Adapter::AVAILABLE_ADAPTERS)
80
+ .to include(Moxml::Config::DEFAULT_ADAPTER)
81
+ end
82
+
83
+ it "OPAL_DEFAULT_ADAPTER is a member of OPAL_AVAILABLE_ADAPTERS" do
84
+ expect(Moxml::Adapter::OPAL_AVAILABLE_ADAPTERS)
85
+ .to include(Moxml::Config::OPAL_DEFAULT_ADAPTER)
86
+ end
69
87
  end
70
88
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "moxml/adapter/shared_examples/adapter_contract"
5
+
6
+ RSpec.describe Moxml::Adapter::Oga, if: RUBY_ENGINE == "opal" do
7
+ around do |example|
8
+ Moxml.with_config(:oga, true, "UTF-8") do
9
+ example.run
10
+ end
11
+ end
12
+
13
+ it_behaves_like "xml adapter"
14
+ end
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # Exercises feature areas under Opal that are not covered by the shared
6
+ # adapter contract (opal_oga_adapter_spec.rb) or the basic smoke spec
7
+ # (opal_oga_smoke_spec.rb). Targets the default Opal adapter (:oga).
8
+ # Uses an explicit :oga context so leaked global config from other specs
9
+ # cannot flip the adapter under test.
10
+ RSpec.describe "Moxml Opal oga feature coverage", if: RUBY_ENGINE == "opal" do
11
+ let(:context) { Moxml.new(:oga) }
12
+
13
+ describe "builder DSL" do
14
+ it "builds a document with the block DSL" do
15
+ doc = Moxml::Builder.new(context).build do
16
+ root do
17
+ child "text"
18
+ end
19
+ end
20
+
21
+ expect(doc.root.name).to eq("root")
22
+ expect(doc.root.children.first.name).to eq("child")
23
+ expect(doc.root.children.first.text).to eq("text")
24
+ end
25
+
26
+ it "creates nested elements via blocks" do
27
+ doc = Moxml::Builder.new(context).build do
28
+ library do
29
+ book(id: "1") { title "A" }
30
+ book(id: "2") { title "B" }
31
+ end
32
+ end
33
+
34
+ books = doc.root.children.grep(Moxml::Element)
35
+ expect(books.length).to eq(2)
36
+ expect(books.map { |b| b["id"] }).to eq(%w[1 2])
37
+ end
38
+
39
+ it "supports method_missing DSL with attributes and text" do
40
+ doc = Moxml::Builder.new(context).build do
41
+ person(name: "Alice", age: "30") { email "alice@example.com" }
42
+ end
43
+
44
+ person = doc.root
45
+ expect(person.name).to eq("person")
46
+ expect(person["name"]).to eq("Alice")
47
+ expect(person["age"]).to eq("30")
48
+ expect(person.children.first.name).to eq("email")
49
+ expect(person.children.first.text).to eq("alice@example.com")
50
+ end
51
+
52
+ it "strips trailing underscore for reserved-name tags" do
53
+ doc = Moxml::Builder.new(context).build do
54
+ class_ { name "Foo" }
55
+ end
56
+
57
+ expect(doc.root.name).to eq("class")
58
+ end
59
+
60
+ it "attaches namespace declarations" do
61
+ builder = Moxml::Builder.new(context)
62
+ doc = builder.build do
63
+ root("xmlns:dc": "http://purl.org/dc/elements/1.1/") do
64
+ builder.element("dc:title") { builder.text "Hello" }
65
+ end
66
+ end
67
+
68
+ title = doc.root.children.find { |c| c.is_a?(Moxml::Element) }
69
+ expect(title.name).to eq("dc:title")
70
+ expect(doc.to_xml).to include("xmlns:dc")
71
+ expect(doc.to_xml).to include("dc:title")
72
+ end
73
+ end
74
+
75
+ describe "XML declaration preservation" do
76
+ it "does not add a declaration when the input has none" do
77
+ doc = context.parse("<root><child/></root>")
78
+ output = doc.to_xml
79
+
80
+ expect(output).not_to include("<?xml")
81
+ expect(doc.has_xml_declaration).to be false
82
+ end
83
+
84
+ it "preserves the declaration when the input has one" do
85
+ xml = '<?xml version="1.0" encoding="UTF-8"?><root/>'
86
+ doc = context.parse(xml)
87
+
88
+ expect(doc.has_xml_declaration).to be true
89
+ expect(doc.to_xml).to include("<?xml")
90
+ expect(doc.to_xml).to include('version="1.0"')
91
+ end
92
+
93
+ it "forces the declaration on with declaration: true" do
94
+ doc = context.parse("<root/>")
95
+ expect(doc.to_xml(declaration: true)).to include("<?xml")
96
+ end
97
+
98
+ it "forces the declaration off with declaration: false" do
99
+ doc = context.parse('<?xml version="1.0"?><root/>')
100
+ expect(doc.to_xml(declaration: false)).not_to include("<?xml")
101
+ end
102
+
103
+ it "preserves standalone attribute" do
104
+ xml = '<?xml version="1.0" standalone="yes"?><root/>'
105
+ doc = context.parse(xml)
106
+ expect(doc.to_xml).to include("standalone")
107
+ end
108
+ end
109
+
110
+ describe "doctype handling" do
111
+ it "parses a SIMPLE doctype" do
112
+ doc = context.parse("<!DOCTYPE root><root/>")
113
+ doctype = doc.children.find { |c| c.is_a?(Moxml::Doctype) }
114
+ expect(doctype).not_to be_nil
115
+ expect(doctype.name).to eq("root")
116
+ end
117
+
118
+ it "parses a PUBLIC doctype with external and system ids" do
119
+ xml = '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"><html/>'
120
+ doc = context.parse(xml)
121
+ doctype = doc.children.find { |c| c.is_a?(Moxml::Doctype) }
122
+
123
+ expect(doctype).not_to be_nil
124
+ expect(doctype.name).to eq("html")
125
+ expect(doctype.external_id).to eq("-//W3C//DTD HTML 4.01//EN")
126
+ expect(doctype.system_id).to eq("http://www.w3.org/TR/html4/strict.dtd")
127
+ end
128
+
129
+ it "round-trips the doctype through serialization" do
130
+ xml = "<!DOCTYPE root><root/>"
131
+ doc = context.parse(xml)
132
+ expect(doc.to_xml).to include("DOCTYPE")
133
+ expect(doc.to_xml).to include("root")
134
+ end
135
+ end
136
+
137
+ describe "round-trip stability" do
138
+ it "round-trips a document with mixed content" do
139
+ xml = <<~XML.strip
140
+ <root attr="value">
141
+ <child>text</child>
142
+ <!-- comment -->
143
+ <nested><deep>data</deep></nested>
144
+ </root>
145
+ XML
146
+
147
+ doc1 = context.parse(xml)
148
+ serialized1 = doc1.to_xml
149
+ doc2 = context.parse(serialized1)
150
+ serialized2 = doc2.to_xml
151
+
152
+ expect(serialized2).to eq(serialized1)
153
+ end
154
+
155
+ it "round-trips a document with namespaces" do
156
+ xml = '<root xmlns:ns="http://example.com/ns"><ns:child>content</ns:child></root>'
157
+ doc1 = context.parse(xml)
158
+ serialized1 = doc1.to_xml
159
+
160
+ doc2 = context.parse(serialized1)
161
+ child = doc2.root.children.find { |c| c.is_a?(Moxml::Element) }
162
+ expect(child.name).to eq("child")
163
+ expect(child.namespace_prefix).to eq("ns")
164
+ expect(doc2.to_xml).to include("xmlns:ns")
165
+ end
166
+
167
+ it "round-trips a document with CDATA" do
168
+ xml = "<root><![CDATA[<unparsed>content</unparsed>]]></root>"
169
+ doc1 = context.parse(xml)
170
+ serialized1 = doc1.to_xml
171
+
172
+ expect(serialized1).to include("<![CDATA[")
173
+ doc2 = context.parse(serialized1)
174
+ expect(doc2.to_xml).to include("<![CDATA[")
175
+ end
176
+ end
177
+
178
+ describe "XPath under Opal" do
179
+ it "evaluates element-only XPath" do
180
+ doc = context.parse("<root><a><b>1</b></a><a><b>2</b></a></root>")
181
+ results = doc.xpath("//a/b")
182
+ expect(results.length).to eq(2)
183
+ expect(results.map(&:text)).to eq(%w[1 2])
184
+ end
185
+
186
+ it "supports attribute predicates" do
187
+ doc = context.parse('<root><item id="x">1</item><item id="y">2</item></root>')
188
+ found = doc.xpath("//item").find { |i| i["id"] == "y" }
189
+ expect(found.text).to eq("2")
190
+ end
191
+
192
+ it "supports at_xpath" do
193
+ doc = context.parse("<root><a>1</a><a>2</a></root>")
194
+ first = doc.at_xpath("//a")
195
+ expect(first.text).to eq("1")
196
+ end
197
+ end
198
+
199
+ describe "comment and processing instruction handling" do
200
+ it "preserves comments through parse/serialize" do
201
+ xml = "<root><!-- my comment --></root>"
202
+ doc = context.parse(xml)
203
+ expect(doc.to_xml).to include("my comment")
204
+ end
205
+
206
+ it "preserves processing instructions through parse/serialize" do
207
+ xml = "<?pi-target pi-data?><root/>"
208
+ doc = context.parse(xml)
209
+ expect(doc.to_xml).to include("pi-target")
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # Verifies that the Opal default adapter (:oga, set in Moxml::Config)
6
+ # loads correctly via the Opal-compatible oga fork. The fork's
7
+ # lib/oga.rb dispatches `require 'liboga'` to `require 'oga/native/lexer'`
8
+ # under Opal, which resolves to the pure-Ruby lexer vendored at
9
+ # vendor/opal-oga/ext/pureruby/.
10
+ RSpec.describe "Moxml Opal oga default", if: RUBY_ENGINE == "opal" do
11
+ let(:context) { Moxml.new }
12
+
13
+ it "defaults to :oga under Opal" do
14
+ expect(Moxml::Config::OPAL_DEFAULT_ADAPTER).to eq(:oga)
15
+ end
16
+
17
+ it "parses XML through the oga adapter" do
18
+ doc = context.parse("<root><child>text</child></root>")
19
+ expect(doc.root.name).to eq("root")
20
+ expect(doc.root.children.first.name).to eq("child")
21
+ end
22
+
23
+ it "decodes entities single-pass" do
24
+ doc = context.parse("<root>&amp;#38;</root>")
25
+ expect(doc.root.text).to eq("&#38;")
26
+ end
27
+
28
+ it "serializes back to XML" do
29
+ xml = '<person name="Alice"><age>30</age></person>'
30
+ doc = context.parse(xml)
31
+ serialized = doc.to_xml
32
+ expect(serialized).to include("person")
33
+ expect(serialized).to include("Alice")
34
+ end
35
+ end
@@ -57,7 +57,7 @@ RSpec.describe "XPath Node Functions" do
57
57
  result = proc.call(doc)
58
58
 
59
59
  # Depending on adapter, may include ns: prefix
60
- expect(result).to match(/item/)
60
+ expect(result).to include("item")
61
61
  end
62
62
 
63
63
  it "returns empty string when no node matched" do
data/spec/spec_helper.rb CHANGED
@@ -101,7 +101,7 @@ RSpec.configure do |config|
101
101
  end
102
102
 
103
103
  Moxml.configure do |config|
104
- config.adapter = RUBY_ENGINE == "opal" ? :rexml : :nokogiri
104
+ config.adapter = RUBY_ENGINE == "opal" ? Moxml::Config::OPAL_DEFAULT_ADAPTER : Moxml::Config::DEFAULT_ADAPTER
105
105
  config.strict_parsing = true
106
106
  config.default_encoding = "UTF-8"
107
107
  config.entity_load_mode = :optional if RUBY_ENGINE == "opal"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: moxml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.24
4
+ version: 0.1.25
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-06-02 00:00:00.000000000 Z
11
+ date: 2026-06-26 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |
14
14
  Moxml is a unified XML manipulation library that provides a common API
@@ -28,6 +28,7 @@ files:
28
28
  - ".github/workflows/release.yml"
29
29
  - ".github/workflows/round-trip.yml"
30
30
  - ".gitignore"
31
+ - ".gitmodules"
31
32
  - ".rspec"
32
33
  - ".rspec-opal"
33
34
  - ".rubocop.yml"
@@ -352,6 +353,9 @@ files:
352
353
  - spec/moxml/node_set_spec.rb
353
354
  - spec/moxml/node_spec.rb
354
355
  - spec/moxml/node_type_map_spec.rb
356
+ - spec/moxml/opal_oga_adapter_spec.rb
357
+ - spec/moxml/opal_oga_features_spec.rb
358
+ - spec/moxml/opal_oga_smoke_spec.rb
355
359
  - spec/moxml/opal_rexml_adapter_spec.rb
356
360
  - spec/moxml/opal_smoke_spec.rb
357
361
  - spec/moxml/processing_instruction_spec.rb