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 +4 -4
- data/.github/workflows/docs.yml +1 -1
- data/.github/workflows/opal.yml +18 -1
- data/.github/workflows/rake.yml +32 -3
- data/.github/workflows/round-trip.yml +72 -9
- data/.gitmodules +6 -0
- data/.rubocop_todo.yml +2 -0
- data/Gemfile +9 -1
- data/README.adoc +124 -0
- data/Rakefile +116 -4
- data/lib/compat/opal/moxml_boot.rb +4 -0
- data/lib/moxml/adapter.rb +5 -3
- data/lib/moxml/config.rb +1 -1
- data/lib/moxml/version.rb +1 -1
- data/moxml.gemspec +1 -1
- data/spec/moxml/adapter/platform_spec.rb +64 -46
- data/spec/moxml/opal_oga_adapter_spec.rb +14 -0
- data/spec/moxml/opal_oga_features_spec.rb +212 -0
- data/spec/moxml/opal_oga_smoke_spec.rb +35 -0
- data/spec/moxml/xpath/functions/node_functions_spec.rb +1 -1
- data/spec/spec_helper.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ed30996109d8cfb8057f216677cba8ca3cd3856287c38b6a7edab3251ebb2064
|
|
4
|
+
data.tar.gz: f93d7c13863ef9d437e40403e4a48434ed1e1c5530ea2424cdeff710740243a0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9b09c144a268507bb02a16a4c6ba7caf6d1a2fa003718a6fa0960a8b68ba03a09584a66fb9475b1c23cd2d79071f1cc8c313200749f997d317aa90e61fba3811
|
|
7
|
+
data.tar.gz: 6389858a232d82faad5f6bbfd22d4828801c7a6d45a55a2a0f08d28c44647a588b52d2106649d408777453b571815b40c191f48c374cc49b648c58ad2be1e835
|
data/.github/workflows/docs.yml
CHANGED
data/.github/workflows/opal.yml
CHANGED
|
@@ -21,16 +21,33 @@ jobs:
|
|
|
21
21
|
uses: ruby/setup-ruby@v1
|
|
22
22
|
with:
|
|
23
23
|
ruby-version: "3.4"
|
|
24
|
-
bundler-cache
|
|
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
|
data/.github/workflows/rake.yml
CHANGED
|
@@ -13,14 +13,43 @@ jobs:
|
|
|
13
13
|
rake:
|
|
14
14
|
runs-on: ubuntu-latest
|
|
15
15
|
steps:
|
|
16
|
-
- uses: actions/checkout@
|
|
17
|
-
|
|
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
|
|
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@
|
|
23
|
-
|
|
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
|
|
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@
|
|
43
|
-
|
|
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:
|
|
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@
|
|
65
|
-
|
|
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:
|
|
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
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
15
|
-
#
|
|
16
|
-
|
|
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 = :
|
|
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
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
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
32
|
-
expect(described_class.available?(:nokogiri)).to be
|
|
22
|
+
it "returns true for :nokogiri under MRI" do
|
|
23
|
+
expect(described_class.available?(:nokogiri)).to be true
|
|
33
24
|
end
|
|
34
25
|
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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>&#38;</root>")
|
|
25
|
+
expect(doc.root.text).to eq("&")
|
|
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
|
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" ?
|
|
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.
|
|
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-
|
|
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
|