diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..b18fd293
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,6 @@
+version: 2
+updates:
+ - package-ecosystem: 'github-actions'
+ directory: '/'
+ schedule:
+ interval: 'weekly'
diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml
new file mode 100644
index 00000000..52349b44
--- /dev/null
+++ b/.github/workflows/benchmark.yml
@@ -0,0 +1,29 @@
+name: Benchmark
+
+on:
+ - push
+ - pull_request
+
+jobs:
+ benchmark:
+ name: "Benchmark: Ruby ${{ matrix.ruby-version }}: ${{ matrix.runs-on }}"
+ strategy:
+ fail-fast: false
+ matrix:
+ ruby-version:
+ - '3.3'
+ runs-on:
+ - ubuntu-latest
+ runs-on: ${{ matrix.runs-on }}
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: ${{ matrix.ruby-version }}
+ - name: Install dependencies
+ run: |
+ bundle install
+ gem install rexml -v 3.2.6
+ - name: Benchmark
+ run: |
+ rake benchmark
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 00000000..20ff87e7
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,30 @@
+name: Release
+on:
+ push:
+ tags:
+ - "*"
+jobs:
+ github:
+ name: GitHub
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ steps:
+ - uses: actions/checkout@v4
+ - name: Extract release note
+ run: |
+ ruby \
+ -e 'print("## REXML "); \
+ puts(ARGF.read.split(/^## /)[1]. \
+ gsub(/ {.+?}/, ""). \
+ gsub(/\[(.+?)\]\[.+?\]/) {$1})' \
+ NEWS.md > release-note.md
+ - name: Upload to release
+ run: |
+ title=$(head -n1 release-note.md | sed -e 's/^## //')
+ tail -n +2 release-note.md > release-note-without-version.md
+ gh release create ${GITHUB_REF_NAME} \
+ --discussion-category Announcements \
+ --notes-file release-note-without-version.md \
+ --title "${title}"
+ env:
+ GH_TOKEN: ${{ github.token }}
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 00000000..fd26b9ab
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,121 @@
+name: Test
+on:
+ - push
+ - pull_request
+jobs:
+ ruby-versions:
+ uses: ruby/actions/.github/workflows/ruby_versions.yml@master
+ with:
+ engine: cruby-jruby
+ min_version: 2.5
+
+ inplace:
+ needs: ruby-versions
+ name: "Inplace: ${{ matrix.ruby-version }} on ${{ matrix.runs-on }}"
+ runs-on: ${{ matrix.runs-on }}
+ strategy:
+ fail-fast: false
+ matrix:
+ runs-on:
+ - ubuntu-latest
+ - macos-latest
+ - windows-latest
+ ruby-version: ${{ fromJson(needs.ruby-versions.outputs.versions) }}
+ exclude:
+ - {runs-on: macos-latest, ruby-version: 2.5}
+ # include:
+ # - runs-on: ubuntu-latest
+ # ruby-version: truffleruby
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: ${{ matrix.ruby-version }}
+ bundler-cache: true
+ - name: Test
+ run: bundle exec rake test
+
+ frozen-string-literal:
+ name: frozen-string-literal
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: ruby
+ bundler-cache: true
+ - name: Test
+ run: bundle exec rake test RUBYOPT="--enable-frozen-string-literal"
+
+ gem:
+ name: "Gem: ${{ matrix.ruby-version }} on ${{ matrix.runs-on }}"
+ runs-on: ${{ matrix.runs-on }}
+ strategy:
+ fail-fast: false
+ matrix:
+ runs-on:
+ - ubuntu-latest
+ - macos-latest
+ - windows-latest
+ ruby-version:
+ - "3.0"
+ - head
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: ${{ matrix.ruby-version }}
+ - name: Install as gem
+ env:
+ BUNDLE_PATH__SYSTEM: "true"
+ BUNDLE_WITHOUT: "benchmark:development"
+ run: |
+ rake install
+ bundle install
+ - name: Test
+ run: |
+ ruby -run -e mkdir -- tmp
+ ruby -run -e cp -- -p -r test tmp
+ cd tmp
+ ruby test/run.rb
+
+ document:
+ name: "Document"
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 2.7
+ - name: Install dependencies
+ run: |
+ bundle install
+ - name: Build document
+ run: |
+ bundle exec rake warning:error rdoc
+ - uses: actions/checkout@v4
+ if: |
+ github.event_name == 'push'
+ with:
+ ref: gh-pages
+ path: gh-pages
+ - name: Deploy
+ if: |
+ github.event_name == 'push'
+ run: |
+ rm html/created.rid
+ touch html/.nojekyll
+ cd gh-pages
+ rsync \
+ -a \
+ --delete \
+ --exclude "/.git/" \
+ ../html/ \
+ ./
+ if [ "$(git status --porcelain)" != "" ]; then
+ git add --all
+ git config user.name github-actions
+ git config user.email github-actions@github.com
+ git commit -m "Generate (${GITHUB_SHA})"
+ git push
+ fi
diff --git a/.gitignore b/.gitignore
index 4ea57987..aeae5f29 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,7 +2,7 @@
/.yardoc
/_yardoc/
/coverage/
-/doc/
+/html/
/pkg/
/spec/reports/
/tmp/
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index b2e241a5..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,24 +0,0 @@
-notifications:
- webhooks:
- - https://webhook.commit-email.info/
-matrix:
- include:
- - name: "2.3"
- rvm: 2.3
- - name: "2.4"
- rvm: 2.4.5
- - name: "2.5"
- rvm: 2.5.2
- - name: "2.6"
- rvm: 2.6.0-rc2
- - name: "trunk"
- rvm: ruby-head
- - name: "gem"
- rvm: 2.6
- install:
- - rake install
- script:
- - mkdir -p tmp
- - cd tmp
- - cp -a ../test/ ./
- - ../run-test.rb
diff --git a/Gemfile b/Gemfile
index 54da2c0c..67f21dfb 100644
--- a/Gemfile
+++ b/Gemfile
@@ -4,3 +4,17 @@ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
# Specify your gem's dependencies in rexml.gemspec
gemspec
+
+group :development do
+ gem "bundler"
+ gem "rake"
+end
+
+group :benchmark do
+ gem "benchmark_driver"
+end
+
+group :test do
+ gem "test-unit"
+ gem "test-unit-ruby-core"
+end
diff --git a/NEWS.md b/NEWS.md
index 57a3d9a8..013409e6 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -1,5 +1,206 @@
# News
+## 3.2.8 - 2024-05-16 {#version-3-2-8}
+
+### Fixes
+
+ * Suppressed a warning
+
+## 3.2.7 - 2024-05-16 {#version-3-2-7}
+
+### Improvements
+
+ * Improve parse performance by using `StringScanner`.
+
+ * GH-106
+ * GH-107
+ * GH-108
+ * GH-109
+ * GH-112
+ * GH-113
+ * GH-114
+ * GH-115
+ * GH-116
+ * GH-117
+ * GH-118
+ * GH-119
+ * GH-121
+
+ * Patch by NAITOH Jun.
+
+ * Improved parse performance when an attribute has many `<`s.
+
+ * GH-124
+
+### Fixes
+
+ * XPath: Fixed a bug of `normalize_space(array)`.
+
+ * GH-110
+ * GH-111
+
+ * Patch by flatisland.
+
+ * XPath: Fixed a bug that wrong position is used with nested path.
+
+ * GH-110
+ * GH-122
+
+ * Reported by jcavalieri.
+ * Patch by NAITOH Jun.
+
+ * Fixed a bug that an exception message can't be generated for
+ invalid encoding XML.
+
+ * GH-29
+ * GH-123
+
+ * Reported by DuKewu.
+ * Patch by NAITOH Jun.
+
+### Thanks
+
+ * NAITOH Jun
+ * flatisland
+ * jcavalieri
+ * DuKewu
+
+
+## 3.2.6 - 2023-07-27 {#version-3-2-6}
+
+### Improvements
+
+ * Required Ruby 2.5 or later explicitly.
+ [GH-69][gh-69]
+ [Patch by Ivo Anjo]
+
+ * Added documentation for maintenance cycle.
+ [GH-71][gh-71]
+ [Patch by Ivo Anjo]
+
+ * Added tutorial.
+ [GH-77][gh-77]
+ [GH-78][gh-78]
+ [Patch by Burdette Lamar]
+
+ * Improved performance and memory usage.
+ [GH-94][gh-94]
+ [Patch by fatkodima]
+
+ * `REXML::Parsers::XPathParser#abbreviate`: Added support for
+ function arguments.
+ [GH-95][gh-95]
+ [Reported by pulver]
+
+ * `REXML::Parsers::XPathParser#abbreviate`: Added support for string
+ literal that contains double-quote.
+ [GH-96][gh-96]
+ [Patch by pulver]
+
+ * `REXML::Parsers::XPathParser#abbreviate`: Added missing `/` to
+ `:descendant_or_self/:self/:parent`.
+ [GH-97][gh-97]
+ [Reported by pulver]
+
+ * `REXML::Parsers::XPathParser#abbreviate`: Added support for more patterns.
+ [GH-97][gh-97]
+ [Reported by pulver]
+
+### Fixes
+
+ * Fixed a typo in NEWS.
+ [GH-72][gh-72]
+ [Patch by Spencer Goodman]
+
+ * Fixed a typo in NEWS.
+ [GH-75][gh-75]
+ [Patch by Andrew Bromwich]
+
+ * Fixed documents.
+ [GH-87][gh-87]
+ [Patch by Alexander Ilyin]
+
+ * Fixed a bug that `Attriute` convert `'` and `'` even when
+ `attribute_quote: :quote` is used.
+ [GH-92][gh-92]
+ [Reported by Edouard Brière]
+
+ * Fixed links in tutorial.
+ [GH-99][gh-99]
+ [Patch by gemmaro]
+
+
+### Thanks
+
+ * Ivo Anjo
+
+ * Spencer Goodman
+
+ * Andrew Bromwich
+
+ * Burdette Lamar
+
+ * Alexander Ilyin
+
+ * Edouard Brière
+
+ * fatkodima
+
+ * pulver
+
+ * gemmaro
+
+[gh-69]:https://github.com/ruby/rexml/issues/69
+[gh-71]:https://github.com/ruby/rexml/issues/71
+[gh-72]:https://github.com/ruby/rexml/issues/72
+[gh-75]:https://github.com/ruby/rexml/issues/75
+[gh-77]:https://github.com/ruby/rexml/issues/77
+[gh-87]:https://github.com/ruby/rexml/issues/87
+[gh-92]:https://github.com/ruby/rexml/issues/92
+[gh-94]:https://github.com/ruby/rexml/issues/94
+[gh-95]:https://github.com/ruby/rexml/issues/95
+[gh-96]:https://github.com/ruby/rexml/issues/96
+[gh-97]:https://github.com/ruby/rexml/issues/97
+[gh-98]:https://github.com/ruby/rexml/issues/98
+[gh-99]:https://github.com/ruby/rexml/issues/99
+
+## 3.2.5 - 2021-04-05 {#version-3-2-5}
+
+### Improvements
+
+ * Add more validations to XPath parser.
+
+ * `require "rexml/document"` by default.
+ [GitHub#36][Patch by Koichi ITO]
+
+ * Don't add `#dclone` method to core classes globally.
+ [GitHub#37][Patch by Akira Matsuda]
+
+ * Add more documentations.
+ [Patch by Burdette Lamar]
+
+ * Added `REXML::Elements#parent`.
+ [GitHub#52][Patch by Burdette Lamar]
+
+### Fixes
+
+ * Fixed a bug that `REXML::DocType#clone` doesn't copy external ID
+ information.
+
+ * Fixed round-trip vulnerability bugs.
+ See also: https://www.ruby-lang.org/en/news/2021/04/05/xml-round-trip-vulnerability-in-rexml-cve-2021-28965/
+ [HackerOne#1104077][CVE-2021-28965][Reported by Juho Nurminen]
+
+### Thanks
+
+ * Koichi ITO
+
+ * Akira Matsuda
+
+ * Burdette Lamar
+
+ * Juho Nurminen
+
## 3.2.4 - 2020-01-31 {#version-3-2-4}
### Improvements
diff --git a/README.md b/README.md
index da38f36f..e8ab5082 100644
--- a/README.md
+++ b/README.md
@@ -4,21 +4,9 @@ REXML was inspired by the Electric XML library for Java, which features an easy-
REXML supports both tree and stream document parsing. Stream parsing is faster (about 1.5 times as fast). However, with stream parsing, you don't get access to features such as XPath.
-## Installation
+## API
-Add this line to your application's Gemfile:
-
-```ruby
-gem 'rexml'
-```
-
-And then execute:
-
- $ bundle
-
-Or install it yourself as:
-
- $ gem install rexml
+See the [API documentation](https://ruby.github.io/rexml/).
## Usage
@@ -45,6 +33,15 @@ doc = Document.new string
So parsing a string is just as easy as parsing a file.
+## Support
+
+REXML support follows the same maintenance cycle as Ruby releases, as shown on .
+
+If you are running on an end-of-life Ruby, do not expect modern REXML releases to be compatible with it; in fact, it's recommended that you DO NOT use this gem, and instead use the REXML version that came bundled with your end-of-life Ruby version.
+
+The `required_ruby_version` on the gemspec is kept updated on a [best-effort basis](https://github.com/ruby/rexml/pull/70) by the community.
+Up to version 3.2.5, this information was not set. That version [is known broken with at least Ruby < 2.3](https://github.com/ruby/rexml/issues/69).
+
## Development
After checking out the repo, run `rake test` to run the tests.
diff --git a/Rakefile b/Rakefile
index 9da51d61..76a56296 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1,8 +1,69 @@
+require "rdoc/task"
+
require "bundler/gem_tasks"
+spec = Bundler::GemHelper.gemspec
+
desc "Run test"
task :test do
- ruby("run-test.rb")
+ ruby("test/run.rb")
end
task :default => :test
+
+namespace :warning do
+ desc "Treat warning as error"
+ task :error do
+ def Warning.warn(*message)
+ super
+ raise "Treat warning as error:\n" + message.join("\n")
+ end
+ end
+end
+
+RDoc::Task.new do |rdoc|
+ rdoc.options = spec.rdoc_options
+ rdoc.rdoc_files.include(*spec.source_paths)
+ rdoc.rdoc_files.include(*spec.extra_rdoc_files)
+end
+
+load "#{__dir__}/tasks/tocs.rake"
+
+benchmark_tasks = []
+namespace :benchmark do
+ Dir.glob("benchmark/*.yaml").sort.each do |yaml|
+ name = File.basename(yaml, ".*")
+ env = {
+ "RUBYLIB" => nil,
+ "BUNDLER_ORIG_RUBYLIB" => nil,
+ }
+ command_line = [
+ RbConfig.ruby, "-v", "-S", "benchmark-driver", File.expand_path(yaml),
+ ]
+
+ desc "Run #{name} benchmark"
+ task name do
+ puts("```")
+ sh(env, *command_line)
+ puts("```")
+ end
+ benchmark_tasks << "benchmark:#{name}"
+
+ case name
+ when /\Aparse/
+ namespace name do
+ desc "Run #{name} benchmark: small"
+ task :small do
+ puts("```")
+ sh(env.merge("N_ELEMENTS" => "500", "N_ATTRIBUTES" => "1"),
+ *command_line)
+ puts("```")
+ end
+ benchmark_tasks << "benchmark:#{name}:small"
+ end
+ end
+ end
+end
+
+desc "Run all benchmarks"
+task :benchmark => benchmark_tasks
diff --git a/benchmark/parse.yaml b/benchmark/parse.yaml
new file mode 100644
index 00000000..e7066fcb
--- /dev/null
+++ b/benchmark/parse.yaml
@@ -0,0 +1,57 @@
+loop_count: 100
+contexts:
+ - gems:
+ rexml: 3.2.6
+ require: false
+ prelude: require 'rexml'
+ - name: master
+ prelude: |
+ $LOAD_PATH.unshift(File.expand_path("lib"))
+ require 'rexml'
+ - name: 3.2.6(YJIT)
+ gems:
+ rexml: 3.2.6
+ require: false
+ prelude: |
+ require 'rexml'
+ RubyVM::YJIT.enable
+ - name: master(YJIT)
+ prelude: |
+ $LOAD_PATH.unshift(File.expand_path("lib"))
+ require 'rexml'
+ RubyVM::YJIT.enable
+
+prelude: |
+ require 'rexml/document'
+ require 'rexml/parsers/sax2parser'
+ require 'rexml/parsers/pullparser'
+ require 'rexml/parsers/streamparser'
+ require 'rexml/streamlistener'
+
+ n_elements = Integer(ENV.fetch("N_ELEMENTS", "5000"), 10)
+ n_attributes = Integer(ENV.fetch("N_ATTRIBUTES", "2"), 10)
+
+ def build_xml(n_elements, n_attributes)
+ xml = ''
+ n_elements.times do |i|
+ xml << ''
+ end
+ xml << ''
+ end
+ xml = build_xml(n_elements, n_attributes)
+
+ class Listener
+ include REXML::StreamListener
+ end
+
+benchmark:
+ 'dom' : REXML::Document.new(xml).elements.each("root/child") {|_|}
+ 'sax' : REXML::Parsers::SAX2Parser.new(xml).parse
+ 'pull' : |
+ parser = REXML::Parsers::PullParser.new(xml)
+ while parser.has_next?
+ parser.pull
+ end
+ 'stream' : REXML::Parsers::StreamParser.new(xml, Listener.new).parse
diff --git a/doc/rexml/context.rdoc b/doc/rexml/context.rdoc
new file mode 100644
index 00000000..7ef01f7b
--- /dev/null
+++ b/doc/rexml/context.rdoc
@@ -0,0 +1,143 @@
+== Element Context
+
+Notes:
+- All code on this page presupposes that the following has been executed:
+
+ require 'rexml/document'
+
+- For convenience, examples on this page use +REXML::Document.new+, not +REXML::Element.new+.
+ This is completely valid, because REXML::Document is a subclass of REXML::Element.
+
+The context for an element is a hash of processing directives
+that influence the way \XML is read, stored, and written.
+The context entries are:
+
+- +:respect_whitespace+: controls treatment of whitespace.
+- +:compress_whitespace+: determines whether whitespace is compressed.
+- +:ignore_whitespace_nodes+: determines whether whitespace-only nodes are to be ignored.
+- +:raw+: controls treatment of special characters and entities.
+
+The default context for a new element is {}.
+You can set the context at element-creation time:
+
+ d = REXML::Document.new('', {compress_whitespace: :all, raw: :all})
+ d.context # => {:compress_whitespace=>:all, :raw=>:all}
+
+You can reset the entire context by assigning a new hash:
+
+ d.context = {ignore_whitespace_nodes: :all}
+ d.context # => {:ignore_whitespace_nodes=>:all}
+
+Or you can create or modify an individual entry:
+
+ d.context[:raw] = :all
+ d.context # => {:ignore_whitespace_nodes=>:all, :raw=>:all}
+
+=== +:respect_whitespace+
+
+Affects: +REXML::Element.new+, +REXML::Element.text=+.
+
+By default, all parsed whitespace is respected (that is, stored whitespace not compressed):
+
+ xml_string = 'a bc de f'
+ d = REXML::Document.new(xml_string)
+ d.to_s # => "a bc de f"
+
+Use +:respect_whitespace+ with an array of element names
+to specify the elements that _are_ to have their whitespace respected;
+other elements' whitespace, and whitespace between elements, will be compressed.
+
+In this example: +foo+ and +baz+ will have their whitespace respected;
++bar+ and the space between elements will have their whitespace compressed:
+
+ d = REXML::Document.new(xml_string, {respect_whitespace: ['foo', 'baz']})
+ d.to_s # => "a bc de f"
+ bar = d.root[2] # => ... >
+ bar.text = 'X Y'
+ d.to_s # => "a bX Ye f"
+
+=== +:compress_whitespace+
+
+Affects: +REXML::Element.new+, +REXML::Element.text=+.
+
+Use compress_whitespace: :all
+to compress whitespace both within and between elements:
+
+ xml_string = 'a bc de f'
+ d = REXML::Document.new(xml_string, {compress_whitespace: :all})
+ d.to_s # => "a bc de f"
+
+Use +:compress_whitespace+ with an array of element names
+to compress whitespace in those elements,
+but not in other elements nor between elements.
+
+In this example, +foo+ and +baz+ will have their whitespace compressed;
++bar+ and the space between elements will not:
+
+ d = REXML::Document.new(xml_string, {compress_whitespace: ['foo', 'baz']})
+ d.to_s # => "a bc de f"
+ foo = d.root[0] # => ... >
+ foo.text= 'X Y'
+ d.to_s # => "X Yc de f"
+
+=== +:ignore_whitespace_nodes+
+
+Affects: +REXML::Element.new+.
+
+Use ignore_whitespace_nodes: :all to omit all whitespace-only elements.
+
+In this example, +bar+ has a text node, while nodes +foo+ and +baz+ do not:
+
+ xml_string = ' BAR '
+ d = REXML::Document.new(xml_string, {ignore_whitespace_nodes: :all})
+ d.to_s # => " FOO BAZ "
+ root = d.root # => ... >
+ foo = root[0] # =>
+ bar = root[1] # => ... >
+ baz = root[2] # =>
+ foo.first.class # => NilClass
+ bar.first.class # => REXML::Text
+ baz.first.class # => NilClass
+
+Use +:ignore_whitespace_nodes+ with an array of element names
+to specify the elements that are to have whitespace nodes ignored.
+
+In this example, +bar+ and +baz+ have text nodes, while node +foo+ does not.
+
+ xml_string = ' BAR '
+ d = REXML::Document.new(xml_string, {ignore_whitespace_nodes: ['foo']})
+ d.to_s # => " BAR "
+ root = d.root # => ... >
+ foo = root[0] # =>
+ bar = root[1] # => ... >
+ baz = root[2] # => ... >
+ foo.first.class # => NilClass
+ bar.first.class # => REXML::Text
+ baz.first.class # => REXML::Text
+
+=== +:raw+
+
+Affects: +Element.text=+, +Element.add_text+, +Text.to_s+.
+
+Parsing of +a+ elements is not affected by +raw+:
+
+ xml_string = '0 < 11 > 0'
+ d = REXML::Document.new(xml_string, {:raw => ['a']})
+ d.root.to_s # => "0 < 11 > 0"
+ a, b = *d.root.elements
+ a.to_s # => "0 < 1"
+ b.to_s # => "1 > 0"
+
+But Element#text= is affected:
+
+ a.text = '0 < 1'
+ b.text = '1 > 0'
+ a.to_s # => "0 < 1"
+ b.to_s # => "1 > 0"
+
+As is Element.add_text:
+
+ a.add_text(' so 1 > 0')
+ b.add_text(' so 0 < 1')
+ a.to_s # => "0 < 1 so 1 > 0"
+ b.to_s # => "1 > 0 so 0 < 1"
diff --git a/doc/rexml/tasks/rdoc/child.rdoc b/doc/rexml/tasks/rdoc/child.rdoc
new file mode 100644
index 00000000..89536381
--- /dev/null
+++ b/doc/rexml/tasks/rdoc/child.rdoc
@@ -0,0 +1,87 @@
+== Class Child
+
+Class Child includes module Node;
+see {Tasks for Node}[node_rdoc.html].
+
+:include: ../tocs/child_toc.rdoc
+
+=== Relationships
+
+==== Task: Set the Parent
+
+Use method {Child#parent=}[../../../../REXML/Parent.html#method-i-parent-3D]
+to set the parent:
+
+ e0 = REXML::Element.new('foo')
+ e1 = REXML::Element.new('bar')
+ e1.parent # => nil
+ e1.parent = e0
+ e1.parent # =>
+
+==== Task: Insert Previous Sibling
+
+Use method {Child#previous_sibling=}[../../../../REXML/Parent.html#method-i-previous_sibling-3D]
+to insert a previous sibling:
+
+ xml_string = ''
+ d = REXML::Document.new(xml_string)
+ d.root.to_a # => [, ]
+ c = d.root[1] # =>
+ b = REXML::Element.new('b')
+ c.previous_sibling = b
+ d.root.to_a # => [, , ]
+
+==== Task: Insert Next Sibling
+
+Use method {Child#next_sibling=}[../../../../REXML/Parent.html#method-i-next-sibling-3D]
+to insert a previous sibling:
+
+ xml_string = ''
+ d = REXML::Document.new(xml_string)
+ d.root.to_a # => [, ]
+ a = d.root[0] # =>
+ b = REXML::Element.new('b')
+ a.next_sibling = b
+ d.root.to_a # => [, , ]
+
+=== Removal or Replacement
+
+==== Task: Remove Child from Parent
+
+Use method {Child#remove}[../../../../REXML/Parent.html#method-i-remove]
+to remove a child from its parent; returns the removed child:
+
+ xml_string = ''
+ d = REXML::Document.new(xml_string)
+ d.root.to_a # => [, , ]
+ b = d.root[1] # =>
+ b.remove # =>
+ d.root.to_a # => [, ]
+
+==== Task: Replace Child
+
+Use method {Child#replace_with}[../../../../REXML/Parent.html#method-i-replace]
+to replace a child;
+returns the replaced child:
+
+ xml_string = ''
+ d = REXML::Document.new(xml_string)
+ d.root.to_a # => [, , ]
+ b = d.root[1] # =>
+ d = REXML::Element.new('d')
+ b.replace_with(d) # =>
+ d.root.to_a # => [, , ]
+
+=== Document
+
+==== Task: Get the Document
+
+Use method {Child#document}[../../../../REXML/Parent.html#method-i-document]
+to get the document for the child:
+
+ xml_string = ''
+ d = REXML::Document.new(xml_string)
+ d.root.to_a # => [, , ]
+ b = d.root[1] # =>
+ b.document == d # => true
+ REXML::Child.new.document # => nil
diff --git a/doc/rexml/tasks/rdoc/document.rdoc b/doc/rexml/tasks/rdoc/document.rdoc
new file mode 100644
index 00000000..96d03351
--- /dev/null
+++ b/doc/rexml/tasks/rdoc/document.rdoc
@@ -0,0 +1,276 @@
+== Class Document
+
+Class Document has methods from its superclasses and included modules;
+see:
+
+- {Tasks for Element}[element_rdoc.html].
+- {Tasks for Parent}[parent_rdoc.html].
+- {Tasks for Child}[child_rdoc.html].
+- {Tasks for Node}[node_rdoc.html].
+- {Module Enumerable}[https://docs.ruby-lang.org/en/master/Enumerable.html].
+
+:include: ../tocs/document_toc.rdoc
+
+=== New Document
+
+==== Task: Create an Empty Document
+
+Use method {Document::new}[../../../../REXML/Document.html#method-c-new]
+to create an empty document.
+
+ d = REXML::Document.new
+
+==== Task: Parse a \String into a New Document
+
+Use method {Document::new}[../../../../REXML/Document.html#method-c-new]
+to parse an XML string into a new document:
+
+ xml_string = 'textmore'
+ d = REXML::Document.new(xml_string)
+ d.root # => ... >
+
+==== Task: Parse an \IO Stream into a New Document
+
+Use method {Document::new}[../../../../REXML/Document.html#method-c-new]
+to parse an XML \IO stream into a new document:
+
+ xml_string = 'textmore'
+ File.write('t.xml', xml_string)
+ d = File.open('t.xml', 'r') do |file|
+ REXML::Document.new(file)
+ end
+ d.root # => ... >
+
+==== Task: Create a Document from an Existing Document
+
+Use method {Document::new}[../../../../REXML/Document.html#method-c-new]
+to create a document from an existing document.
+The context and attributes are copied to the new document,
+but not the children:
+
+ xml_string = 'textmore'
+ d = REXML::Document.new(xml_string)
+ d.children # => [ ... >]
+ d.context = {raw: :all, compress_whitespace: :all}
+ d.add_attributes({'bar' => 0, 'baz' => 1})
+ d1 = REXML::Document.new(d)
+ d1.context # => {:raw=>:all, :compress_whitespace=>:all}
+ d1.attributes # => {"bar"=>bar='0', "baz"=>baz='1'}
+ d1.children # => []
+
+==== Task: Clone a Document
+
+Use method {Document#clone}[../../../../REXML/Document.html#method-i-clone]
+to clone a document.
+The context and attributes are copied to the new document,
+but not the children:
+
+ xml_string = 'textmore'
+ d = REXML::Document.new(xml_string)
+ d.children # => [ ... >]
+ d.context = {raw: :all, compress_whitespace: :all}
+ d.add_attributes({'bar' => 0, 'baz' => 1})
+ d1 = d.clone # => < bar='0' baz='1'/>
+ d1.context # => {:raw=>:all, :compress_whitespace=>:all}
+ d1.attributes # => {"bar"=>bar='0', "baz"=>baz='1'}
+ d1.children # => []
+
+=== Document Type
+
+==== Task: Get the Document Type
+
+Use method {Document#doctype}[../../../../REXML/Document.html#method-i-doctype]
+to get the document type:
+
+ d = REXML::Document.new('')
+ d.doctype.class # => REXML::DocType
+ d = REXML::Document.new('')
+ d.doctype.class # => nil
+
+==== Task: Set the Document Type
+
+Use method {document#add}[../../../../REXML/Document.html#method-i-add]
+to add or replace the document type:
+
+ d = REXML::Document.new('')
+ d.doctype.class # => nil
+ d.add(REXML::DocType.new('foo'))
+ d.doctype.class # => REXML::DocType
+
+=== XML Declaration
+
+==== Task: Get the XML Declaration
+
+Use method {document#xml_decl}[../../../../REXML/Document.html#method-i-xml_decl]
+to get the XML declaration:
+
+ d = REXML::Document.new('')
+ d.xml_decl.class # => REXML::XMLDecl
+ d.xml_decl # =>
+ d = REXML::Document.new('')
+ d.xml_decl.class # => REXML::XMLDecl
+ d.xml_decl # =>
+
+==== Task: Set the XML Declaration
+
+Use method {document#add}[../../../../REXML/Document.html#method-i-add]
+to replace the XML declaration:
+
+ d = REXML::Document.new('')
+ d.add(REXML::XMLDecl.new)
+
+=== Children
+
+==== Task: Add an Element Child
+
+Use method
+{document#add_element}[../../../../REXML/Document.html#method-i-add_element]
+to add an element to the document:
+
+ d = REXML::Document.new('')
+ d.add_element(REXML::Element.new('root'))
+ d.children # => []
+
+==== Task: Add a Non-Element Child
+
+Use method
+{document#add}[../../../../REXML/Document.html#method-i-add]
+to add a non-element to the document:
+
+ xml_string = 'textmore'
+ d = REXML::Document.new(xml_string)
+ d.add(REXML::Text.new('foo'))
+ d.children # => [ ... >, "foo"]
+
+=== Writing
+
+==== Task: Write to $stdout
+
+Use method
+{document#write}[../../../../REXML/Document.html#method-i-write]
+to write the document to $stdout:
+
+ xml_string = 'textmore'
+ d = REXML::Document.new(xml_string)
+ d.write
+
+Output:
+
+ textmore
+
+==== Task: Write to IO Stream
+
+Use method
+{document#write}[../../../../REXML/Document.html#method-i-write]
+to write the document to $stdout:
+
+ xml_string = 'textmore'
+ d = REXML::Document.new(xml_string)
+ File.open('t.xml', 'w') do |file|
+ d.write(file)
+ end
+ p File.read('t.xml')
+
+Output:
+
+ "textmore"
+
+==== Task: Write with No Indentation
+
+Use method
+{document#write}[../../../../REXML/Document.html#method-i-write]
+to write the document with no indentation:
+
+ xml_string = ''
+ d = REXML::Document.new(xml_string)
+ d.write({indent: 0})
+
+Output:
+
+
+
+
+
+
+
+
+
+==== Task: Write with Specified Indentation
+
+Use method
+{document#write}[../../../../REXML/Document.html#method-i-write]
+to write the document with a specified indentation:
+
+ xml_string = ''
+ d = REXML::Document.new(xml_string)
+ d.write({indent: 2})
+
+Output:
+
+
+
+
+
+
+
+
+
+=== Querying
+
+==== Task: Get the Document
+
+Use method
+{document#document}[../../../../REXML/Document.html#method-i-document]
+to get the document (+self+); overrides Element#document:
+
+ xml_string = ''
+ d = REXML::Document.new(xml_string)
+ d.document == d # => true
+
+==== Task: Get the Encoding
+
+Use method
+{document#document}[../../../../REXML/Document.html#method-i-document]
+to get the document (+self+); overrides Element#document:
+
+ xml_string = ''
+ d = REXML::Document.new(xml_string)
+ d.encoding # => "UTF-8"
+
+==== Task: Get the Node Type
+
+Use method
+{document#node_type}[../../../../REXML/Document.html#method-i-node_type]
+to get the node type (+:document+); overrides Element#node_type:
+
+ xml_string = ''
+ d = REXML::Document.new(xml_string)
+ d.node_type # => :document
+
+==== Task: Get the Root Element
+
+Use method
+{document#root}[../../../../REXML/Document.html#method-i-root]
+to get the root element:
+
+ xml_string = ''
+ d = REXML::Document.new(xml_string)
+ d.root # => ... >
+
+==== Task: Determine Whether Stand-Alone
+
+Use method
+{document#stand_alone?}[../../../../REXML/Document.html#method-i-stand_alone-3F]
+to get the stand-alone value:
+
+ d = REXML::Document.new('')
+ d.stand_alone? # => "yes"
+
+==== Task: Get the Version
+
+Use method
+{document#version}[../../../../REXML/Document.html#method-i-version]
+to get the version:
+
+ d = REXML::Document.new('')
+ d.version # => "2.0"
diff --git a/doc/rexml/tasks/rdoc/element.rdoc b/doc/rexml/tasks/rdoc/element.rdoc
new file mode 100644
index 00000000..4b3609b0
--- /dev/null
+++ b/doc/rexml/tasks/rdoc/element.rdoc
@@ -0,0 +1,602 @@
+== Class Element
+
+Class Element has methods from its superclasses and included modules;
+see:
+
+- {Tasks for Parent}[parent_rdoc.html].
+- {Tasks for Child}[child_rdoc.html].
+- {Tasks for Node}[node_rdoc.html].
+- {Module Enumerable}[https://docs.ruby-lang.org/en/master/Enumerable.html].
+
+:include: ../tocs/element_toc.rdoc
+
+=== New Element
+
+==== Task: Create a Default Element
+
+Use method
+{Element::new}[../../../../REXML/Element.html#method-c-new]
+with no arguments to create a default element:
+
+ e = REXML::Element.new
+ e.name # => "UNDEFINED"
+ e.parent # => nil
+ e.context # => nil
+
+==== Task: Create a Named Element
+
+Use method
+{Element::new}[../../../../REXML/Element.html#method-c-new]
+with a string name argument
+to create a named element:
+
+ e = REXML::Element.new('foo')
+ e.name # => "foo"
+ e.parent # => nil
+ e.context # => nil
+
+==== Task: Create an Element with Name and Parent
+
+Use method
+{Element::new}[../../../../REXML/Element.html#method-c-new]
+with name and parent arguments
+to create an element with name and parent:
+
+ p = REXML::Parent.new
+ e = REXML::Element.new('foo', p)
+ e.name # => "foo"
+ e.parent # => #]>
+ e.context # => nil
+
+==== Task: Create an Element with Name, Parent, and Context
+
+Use method
+{Element::new}[../../../../REXML/Element.html#method-c-new]
+with name, parent, and context arguments
+to create an element with name, parent, and context:
+
+ p = REXML::Parent.new
+ e = REXML::Element.new('foo', p, {compress_whitespace: :all})
+ e.name # => "foo"
+ e.parent # => #]>
+ e.context # => {:compress_whitespace=>:all}
+
+==== Task: Create a Shallow Clone
+
+Use method
+{Element#clone}[../../../../REXML/Element.html#method-i-clone]
+to create a shallow clone of an element,
+copying only the name, attributes, and context:
+
+ e0 = REXML::Element.new('foo', nil, {compress_whitespace: :all})
+ e0.add_attribute(REXML::Attribute.new('bar', 'baz'))
+ e0.context = {compress_whitespace: :all}
+ e1 = e0.clone # =>
+ e1.name # => "foo"
+ e1.context # => {:compress_whitespace=>:all}
+
+=== Attributes
+
+==== Task: Create and Add an Attribute
+
+Use method
+{Element#add_attribute}[../../../../REXML/Element.html#method-i-add_attribute]
+to create and add an attribute:
+
+ e = REXML::Element.new
+ e.add_attribute('attr', 'value') # => "value"
+ e['attr'] # => "value"
+ e.add_attribute('attr', 'VALUE') # => "VALUE"
+ e['attr'] # => "VALUE"
+
+==== Task: Add an Existing Attribute
+
+Use method
+{Element#add_attribute}[../../../../REXML/Element.html#method-i-add_attribute]
+to add an existing attribute:
+
+ e = REXML::Element.new
+ a = REXML::Attribute.new('attr', 'value')
+ e.add_attribute(a)
+ e['attr'] # => "value"
+ a = REXML::Attribute.new('attr', 'VALUE')
+ e.add_attribute(a)
+ e['attr'] # => "VALUE"
+
+==== Task: Add Multiple Attributes from a Hash
+
+Use method
+{Element#add_attributes}[../../../../REXML/Element.html#method-i-add_attributes]
+to add multiple attributes from a hash:
+
+ e = REXML::Element.new
+ h = {'foo' => 0, 'bar' => 1}
+ e.add_attributes(h)
+ e['foo'] # => "0"
+ e['bar'] # => "1"
+
+==== Task: Add Multiple Attributes from an Array
+
+Use method
+{Element#add_attributes}[../../../../REXML/Element.html#method-i-add_attributes]
+to add multiple attributes from an array:
+
+ e = REXML::Element.new
+ a = [['foo', 0], ['bar', 1]]
+ e.add_attributes(a)
+ e['foo'] # => "0"
+ e['bar'] # => "1"
+
+==== Task: Retrieve the Value for an Attribute Name
+
+Use method
+{Element#[]}[../../../../REXML/Element.html#method-i-5B-5D]
+to retrieve the value for an attribute name:
+
+ e = REXML::Element.new
+ e.add_attribute('attr', 'value') # => "value"
+ e['attr'] # => "value"
+
+==== Task: Retrieve the Attribute Value for a Name and Namespace
+
+Use method
+{Element#attribute}[../../../../REXML/Element.html#method-i-attribute]
+to retrieve the value for an attribute name:
+
+ xml_string = ""
+ d = REXML::Document.new(xml_string)
+ e = d.root
+ e.attribute("x") # => x='x'
+ e.attribute("x", "a") # => a:x='a:x'
+
+==== Task: Delete an Attribute
+
+Use method
+{Element#delete_attribute}[../../../../REXML/Element.html#method-i-delete_attribute]
+to remove an attribute:
+
+ e = REXML::Element.new('foo')
+ e.add_attribute('bar', 'baz')
+ e.delete_attribute('bar')
+ e.delete_attribute('bar')
+ e['bar'] # => nil
+
+==== Task: Determine Whether the Element Has Attributes
+
+Use method
+{Element#has_attributes?}[../../../../REXML/Element.html#method-i-has_attributes-3F]
+to determine whether the element has attributes:
+
+ e = REXML::Element.new('foo')
+ e.has_attributes? # => false
+ e.add_attribute('bar', 'baz')
+ e.has_attributes? # => true
+
+=== Children
+
+Element Children
+
+==== Task: Create and Add an Element
+
+Use method
+{Element#add_element}[../../../../REXML/Element.html#method-i-add_element]
+to create a new element and add it to this element:
+
+ e0 = REXML::Element.new('foo')
+ e0.add_element('bar')
+ e0.children # => []
+
+==== Task: Add an Existing Element
+
+Use method
+{Element#add_element}[../../../../REXML/Element.html#method-i-add_element]
+to add an element to this element:
+
+ e0 = REXML::Element.new('foo')
+ e1 = REXML::Element.new('bar')
+ e0.add_element(e1)
+ e0.children # => []
+
+==== Task: Create and Add an Element with Attributes
+
+Use method
+{Element#add_element}[../../../../REXML/Element.html#method-i-add_element]
+to create a new element with attributes, and add it to this element:
+
+ e0 = REXML::Element.new('foo')
+ e0.add_element('bar', {'name' => 'value'})
+ e0.children # => []
+
+==== Task: Add an Existing Element with Added Attributes
+
+Use method
+{Element#add_element}[../../../../REXML/Element.html#method-i-add_element]
+to add an element to this element:
+
+ e0 = REXML::Element.new('foo')
+ e1 = REXML::Element.new('bar')
+ e0.add_element(e1, {'name' => 'value'})
+ e0.children # => []
+
+==== Task: Delete a Specified Element
+
+Use method
+{Element#delete_element}[../../../../REXML/Element.html#method-i-delete_element]
+to remove a specified element from this element:
+
+ e0 = REXML::Element.new('foo')
+ e1 = REXML::Element.new('bar')
+ e0.add_element(e1)
+ e0.children # => []
+ e0.delete_element(e1)
+ e0.children # => []
+
+==== Task: Delete an Element by Index
+
+Use method
+{Element#delete_element}[../../../../REXML/Element.html#method-i-delete_element]
+to remove an element from this element by index:
+
+ e0 = REXML::Element.new('foo')
+ e1 = REXML::Element.new('bar')
+ e0.add_element(e1)
+ e0.children # => []
+ e0.delete_element(1)
+ e0.children # => []
+
+==== Task: Delete an Element by XPath
+
+Use method
+{Element#delete_element}[../../../../REXML/Element.html#method-i-delete_element]
+to remove an element from this element by XPath:
+
+ e0 = REXML::Element.new('foo')
+ e1 = REXML::Element.new('bar')
+ e0.add_element(e1)
+ e0.children # => []
+ e0.delete_element('//bar/')
+ e0.children # => []
+
+==== Task: Determine Whether Element Children
+
+Use method
+{Element#has_elements?}[../../../../REXML/Element.html#method-i-has_elements-3F]
+to determine whether the element has element children:
+
+ e0 = REXML::Element.new('foo')
+ e0.has_elements? # => false
+ e0.add_element(REXML::Element.new('bar'))
+ e0.has_elements? # => true
+
+==== Task: Get Element Descendants by XPath
+
+Use method
+{Element#get_elements}[../../../../REXML/Element.html#method-i-get_elements]
+to fetch all element descendant children by XPath:
+
+ xml_string = <<-EOT
+
+
+
+
+
+ EOT
+ d = REXML::Document.new(xml_string)
+ d.root.get_elements('//a') # => [ ... >, ]
+
+==== Task: Get Next Element Sibling
+
+Use method
+{Element#next_element}[../../../../REXML/Element.html#method-i-next_element]
+to retrieve the next element sibling:
+
+ d = REXML::Document.new 'text'
+ d.root.elements['b'].next_element #->
+ d.root.elements['c'].next_element #-> nil
+
+==== Task: Get Previous Element Sibling
+
+Use method
+{Element#previous_element}[../../../../REXML/Element.html#method-i-previous_element]
+to retrieve the previous element sibling:
+
+ d = REXML::Document.new 'text'
+ d.root.elements['c'].previous_element #->
+ d.root.elements['b'].previous_element #-> nil
+
+Text Children
+
+==== Task: Add a Text Node
+
+Use method
+{Element#add_text}[../../../../REXML/Element.html#method-i-add_text]
+to add a text node to the element:
+
+ d = REXML::Document.new('foobar')
+ e = d.root
+ e.add_text(REXML::Text.new('baz'))
+ e.to_a # => ["foo", , "bar", "baz"]
+ e.add_text(REXML::Text.new('baz'))
+ e.to_a # => ["foo", , "bar", "baz", "baz"]
+
+==== Task: Replace the First Text Node
+
+Use method
+{Element#text=}[../../../../REXML/Element.html#method-i-text-3D]
+to replace the first text node in the element:
+
+ d = REXML::Document.new('textmore')
+ e = d.root
+ e.to_a # => [, "text", , "more", ]
+ e.text = 'oops'
+ e.to_a # => [, "oops", , "more", ]
+
+==== Task: Remove the First Text Node
+
+Use method
+{Element#text=}[../../../../REXML/Element.html#method-i-text-3D]
+to remove the first text node in the element:
+
+ d = REXML::Document.new('textmore')
+ e = d.root
+ e.to_a # => [, "text", , "more", ]
+ e.text = nil
+ e.to_a # => [, , "more", ]
+
+==== Task: Retrieve the First Text Node
+
+Use method
+{Element#get_text}[../../../../REXML/Element.html#method-i-get_text]
+to retrieve the first text node in the element:
+
+ d = REXML::Document.new('textmore')
+ e = d.root
+ e.to_a # => [, "text", , "more", ]
+ e.get_text # => "text"
+
+==== Task: Retrieve a Specific Text Node
+
+Use method
+{Element#get_text}[../../../../REXML/Element.html#method-i-get_text]
+to retrieve the first text node in a specified element:
+
+ d = REXML::Document.new "some text this is bold! more text"
+ e = d.root
+ e.get_text('//root') # => "some text "
+ e.get_text('//b') # => "this is bold!"
+
+==== Task: Determine Whether the Element has Text Nodes
+
+Use method
+{Element#has_text?}[../../../../REXML/Element.html#method-i-has_text-3F]
+to determine whether the element has text:
+
+ e = REXML::Element.new('foo')
+ e.has_text? # => false
+ e.add_text('bar')
+ e.has_text? # => true
+
+Other Children
+
+==== Task: Get the Child at a Given Index
+
+Use method
+{Element#[]}[../../../../REXML/Element.html#method-i-5B-5D]
+to retrieve the child at a given index:
+
+ d = REXML::Document.new '>textmore'
+ e = d.root
+ e[0] # =>
+ e[1] # => "text"
+ e[2] # =>
+
+==== Task: Get All CDATA Children
+
+Use method
+{Element#cdatas}[../../../../REXML/Element.html#method-i-cdatas]
+to retrieve all CDATA children:
+
+ xml_string = <<-EOT
+
+
+
+
+ EOT
+ d = REXML::Document.new(xml_string)
+ d.root.cdatas # => ["foo", "bar"]
+
+==== Task: Get All Comment Children
+
+Use method
+{Element#comments}[../../../../REXML/Element.html#method-i-comments]
+to retrieve all comment children:
+
+ xml_string = <<-EOT
+
+
+
+
+ EOT
+ d = REXML::Document.new(xml_string)
+ d.root.comments.map {|comment| comment.to_s } # => ["foo", "bar"]
+
+==== Task: Get All Processing Instruction Children
+
+Use method
+{Element#instructions}[../../../../REXML/Element.html#method-i-instructions]
+to retrieve all processing instruction children:
+
+ xml_string = <<-EOT
+
+
+
+
+ EOT
+ d = REXML::Document.new(xml_string)
+ instructions = d.root.instructions.map {|instruction| instruction.to_s }
+ instructions # => ["", ""]
+
+==== Task: Get All Text Children
+
+Use method
+{Element#texts}[../../../../REXML/Element.html#method-i-texts]
+to retrieve all text children:
+
+ xml_string = 'textmore'
+ d = REXML::Document.new(xml_string)
+ d.root.texts # => ["text", "more"]
+
+=== Namespaces
+
+==== Task: Add a Namespace
+
+Use method
+{Element#add_namespace}[../../../../REXML/Element.html#method-i-add_namespace]
+to add a namespace to the element:
+
+ e = REXML::Element.new('foo')
+ e.add_namespace('bar')
+ e.namespaces # => {"xmlns"=>"bar"}
+
+==== Task: Delete the Default Namespace
+
+Use method
+{Element#delete_namespace}[../../../../REXML/Element.html#method-i-delete_namespace]
+to remove the default namespace from the element:
+
+ d = REXML::Document.new ""
+ d.to_s # => ""
+ d.root.delete_namespace # =>
+ d.to_s # => ""
+
+==== Task: Delete a Specific Namespace
+
+Use method
+{Element#delete_namespace}[../../../../REXML/Element.html#method-i-delete_namespace]
+to remove a specific namespace from the element:
+
+ d = REXML::Document.new ""
+ d.to_s # => ""
+ d.root.delete_namespace # =>
+ d.to_s # => ""
+ d.root.delete_namespace('foo')
+ d.to_s # => ""
+
+==== Task: Get a Namespace URI
+
+Use method
+{Element#namespace}[../../../../REXML/Element.html#method-i-namespace]
+to retrieve a specific namespace URI for the element:
+
+ xml_string = <<-EOT
+
+
+
+
+
+
+ EOT
+ d = REXML::Document.new(xml_string)
+ b = d.elements['//b']
+ b.namespace # => "1"
+ b.namespace('y') # => "2"
+
+==== Task: Retrieve Namespaces
+
+Use method
+{Element#namespaces}[../../../../REXML/Element.html#method-i-namespaces]
+to retrieve all namespaces for the element:
+
+ xml_string = ''
+ d = REXML::Document.new(xml_string)
+ d.root.attributes.namespaces # => {"xmlns"=>"foo", "x"=>"bar", "y"=>"twee"}
+
+==== Task: Retrieve Namespace Prefixes
+
+Use method
+{Element#prefixes}[../../../../REXML/Element.html#method-i-prefixes]
+to retrieve all prefixes (namespace names) for the element:
+
+ xml_string = <<-EOT
+
+
+
+
+
+
+ EOT
+ d = REXML::Document.new(xml_string, {compress_whitespace: :all})
+ d.elements['//a'].prefixes # => ["x", "y"]
+ d.elements['//b'].prefixes # => ["x", "y"]
+ d.elements['//c'].prefixes # => ["x", "y", "z"]
+
+=== Iteration
+
+==== Task: Iterate Over Elements
+
+Use method
+{Element#each_element}[../../../../REXML/Element.html#method-i-each_element]
+to iterate over element children:
+
+ d = REXML::Document.new 'bbd'
+ d.root.each_element {|e| p e }
+
+Output:
+
+ ... >
+ ... >
+ ... >
+
+
+==== Task: Iterate Over Elements Having a Specified Attribute
+
+Use method
+{Element#each_element_with_attribute}[../../../../REXML/Element.html#method-i-each_element_with_attribute]
+to iterate over element children that have a specified attribute:
+
+ d = REXML::Document.new ''
+ a = d.root
+ a.each_element_with_attribute('id') {|e| p e }
+
+Output:
+
+
+
+
+
+==== Task: Iterate Over Elements Having a Specified Attribute and Value
+
+Use method
+{Element#each_element_with_attribute}[../../../../REXML/Element.html#method-i-each_element_with_attribute]
+to iterate over element children that have a specified attribute and value:
+
+ d = REXML::Document.new ''
+ a = d.root
+ a.each_element_with_attribute('id', '1') {|e| p e }
+
+Output:
+
+
+
+
+==== Task: Iterate Over Elements Having Specified Text
+
+Use method
+{Element#each_element_with_text}[../../../../REXML/Element.html#method-i-each_element_with_text]
+to iterate over element children that have specified text:
+
+
+=== Context
+
+#whitespace
+#ignore_whitespace_nodes
+#raw
+
+=== Other Getters
+
+#document
+#root
+#root_node
+#node_type
+#xpath
+#inspect
diff --git a/doc/rexml/tasks/rdoc/node.rdoc b/doc/rexml/tasks/rdoc/node.rdoc
new file mode 100644
index 00000000..d5d2e12a
--- /dev/null
+++ b/doc/rexml/tasks/rdoc/node.rdoc
@@ -0,0 +1,97 @@
+== Module Node
+
+:include: ../tocs/node_toc.rdoc
+
+=== Siblings
+
+==== Task: Find Previous Sibling
+
+Use method
+{Node.previous_sibling_node}[../../../../REXML/Node.html#method-i-previous_sibling]
+to retrieve the previous sibling:
+
+ d = REXML::Document.new('')
+ b = d.root[1] # =>
+ b.previous_sibling_node # =>
+
+==== Task: Find Next Sibling
+
+Use method
+{Node.next_sibling_node}[../../../../REXML/Node.html#method-i-next_sibling]
+to retrieve the next sibling:
+
+ d = REXML::Document.new('')
+ b = d.root[1] # =>
+ b.next_sibling_node # =>
+
+=== Position
+
+==== Task: Find Own Index Among Siblings
+
+Use method
+{Node.index_in_parent}[../../../../REXML/Node.html#method-i-index_in_parent]
+to retrieve the 1-based index of this node among its siblings:
+
+ d = REXML::Document.new('')
+ b = d.root[1] # =>
+ b.index_in_parent # => 2
+
+=== Recursive Traversal
+
+==== Task: Traverse Each Recursively
+
+Use method
+{Node.each_recursive}[../../../../REXML/Node.html#method-i-each_recursive]
+to traverse a tree of nodes recursively:
+
+ xml_string = ''
+ d = REXML::Document.new(xml_string)
+ d.root.each_recursive {|node| p node }
+
+Output:
+
+ ... >
+ ... >
+
+ ... >
+
+
+=== Recursive Search
+
+==== Task: Traverse Each Recursively
+
+Use method
+{Node.find_first_recursive}[../../../../REXML/Node.html#method-i-find_first_recursive]
+to search a tree of nodes recursively:
+
+ xml_string = ''
+ d = REXML::Document.new(xml_string)
+ d.root.find_first_recursive {|node| node.name == 'c' } # =>
+
+=== Representation
+
+==== Task: Represent a String
+
+Use method {Node.to_s}[../../../../REXML/Node.html#method-i-to_s]
+to represent the node as a string:
+
+ xml_string = ''
+ d = REXML::Document.new(xml_string)
+ d.root.to_s # => ""
+
+=== Parent?
+
+==== Task: Determine Whether the Node is a Parent
+
+Use method {Node.parent?}[../../../../REXML/Node.html#method-i-parent-3F]
+to determine whether the node is a parent;
+class Text derives from Node:
+
+ d = REXML::Document.new('textmore')
+ t = d.root[1] # => "text"
+ t.parent? # => false
+
+Class Parent also derives from Node, but overrides this method:
+
+ p = REXML::Parent.new
+ p.parent? # => true
diff --git a/doc/rexml/tasks/rdoc/parent.rdoc b/doc/rexml/tasks/rdoc/parent.rdoc
new file mode 100644
index 00000000..54f1dbe3
--- /dev/null
+++ b/doc/rexml/tasks/rdoc/parent.rdoc
@@ -0,0 +1,267 @@
+== Class Parent
+
+Class Parent has methods from its superclasses and included modules;
+see:
+
+- {Tasks for Child}[child_rdoc.html].
+- {Tasks for Node}[node_rdoc.html].
+- {Module Enumerable}[https://docs.ruby-lang.org/en/master/Enumerable.html].
+
+:include: ../tocs/parent_toc.rdoc
+
+=== Queries
+
+==== Task: Get the Count of Children
+
+Use method {Parent#size}[../../../../REXML/Parent.html#method-i-size]
+(or its alias +length+) to get the count of the parent's children:
+
+ p = REXML::Parent.new
+ p.size # => 0
+ xml_string = ''
+ d = REXML::Document.new(xml_string)
+ d.root.size # => 3
+
+==== Task: Get the Child at a Given Index
+
+Use method {Parent#[]}[../../../../REXML/Parent.html#method-i-5B-5D]
+to get the child at a given index:
+
+ xml_string = ''
+ d = REXML::Document.new(xml_string)
+ d.root[1] # =>
+ d.root[-1] # =>
+ d.root[50] # => nil
+
+==== Task: Get the Index of a Given Child
+
+Use method {Parent#index}[../../../../REXML/Parent.html#method-i-index]
+to get the index (0-based offset) of a child:
+
+ d = REXML::Document.new('')
+ root = d.root
+ e0 = REXML::Element.new('foo')
+ e1 = REXML::Element.new('bar')
+ root.add(e0) # =>
+ root.add(e1) # =>
+ root.add(e0) # =>
+ root.add(e1) # =>
+ root.index(e0) # => 0
+ root.index(e1) # => 1
+
+==== Task: Get the Children
+
+Use method {Parent#children}[../../../../REXML/Parent.html#method-i-children]
+(or its alias +to_a+) to get the parent's children:
+
+ xml_string = ''
+ d = REXML::Document.new(xml_string)
+ d.root.children # => [, , ]
+
+==== Task: Determine Whether the Node is a Parent
+
+Use method {Parent#parent?}[../../../../REXML/Parent.html#method-i-parent-3F]
+to determine whether the node is a parent;
+class Text derives from Node:
+
+ d = REXML::Document.new('textmore')
+ t = d.root[1] # => "text"
+ t.parent? # => false
+
+Class Parent also derives from Node, but overrides this method:
+
+ p = REXML::Parent.new
+ p.parent? # => true
+
+=== Additions
+
+==== Task: Add a Child at the Beginning
+
+Use method {Parent#unshift}[../../../../REXML/Parent.html#method-i-unshift]
+to add a child as at the beginning of the children:
+
+ xml_string = ''
+ d = REXML::Document.new(xml_string)
+ d.root.children # => [, , ]
+ d.root.unshift REXML::Element.new('d')
+ d.root.children # => [, , , ]
+
+==== Task: Add a Child at the End
+
+Use method {Parent#<<}[../../../../REXML/Parent.html#method-i-3C-3C]
+(or an alias +push+ or +add+) to add a child as at the end of the children:
+
+ xml_string = ''
+ d = REXML::Document.new(xml_string)
+ d.root.children # => [, , ]
+ d.root << REXML::Element.new('d')
+ d.root.children # => [, , , ]
+
+==== Task: Replace a Child with Another Child
+
+Use method {Parent#replace}[../../../../REXML/Parent.html#method-i-replace]
+
+ xml_string = ''
+ d = REXML::Document.new(xml_string)
+ d.root.children # => [, , ]
+ b = d.root[1] # =>
+ d.replace_child(b, REXML::Element.new('d'))
+ d.root.children # => [, ]
+
+==== Task: Replace Multiple Children with Another Child
+
+Use method {Parent#[]=}[../../../../REXML/Parent.html#method-i-parent-5B-5D-3D]
+to replace multiple consecutive children with another child:
+
+ xml_string = ''
+ d = REXML::Document.new(xml_string)
+ d.root.children # => [, , , ]
+ d.root[1, 2] = REXML::Element.new('x')
+ d.root.children # => [, , ]
+ d.root[1, 5] = REXML::Element.new('x')
+ d.root.children # => [, ] # BUG?
+
+==== Task: Insert Child Before a Given Child
+
+Use method {Parent#insert_before}[../../../../REXML/Parent.html#method-i-insert_before]
+to insert a child immediately before a given child:
+
+ xml_string = ''
+ d = REXML::Document.new(xml_string)
+ d.root.children # => [, , ]
+ b = d.root[1] # =>
+ x = REXML::Element.new('x')
+ d.root.insert_before(b, x)
+ d.root.children # => [, , , ]
+
+==== Task: Insert Child After a Given Child
+
+Use method {Parent#insert_after}[../../../../REXML/Parent.html#method-i-insert_after]
+to insert a child immediately after a given child:
+
+ xml_string = ''
+ d = REXML::Document.new(xml_string)
+ d.root.children # => [, , ]
+ b = d.root[1] # =>
+ x = REXML::Element.new('x')
+ d.root.insert_after(b, x)
+ d.root.children # => [, , , ]
+
+=== Deletions
+
+==== Task: Remove a Given Child
+
+Use method {Parent#delete}[../../../../REXML/Parent.html#method-i-delete]
+to remove all occurrences of a given child:
+
+ d = REXML::Document.new('')
+ a = REXML::Element.new('a')
+ b = REXML::Element.new('b')
+ d.root.add(a)
+ d.root.add(b)
+ d.root.add(a)
+ d.root.add(b)
+ d.root.children # => [, , , ]
+ d.root.delete(b)
+ d.root.children # => [, ]
+
+==== Task: Remove the Child at a Specified Offset
+
+Use method {Parent#delete_at}[../../../../REXML/Parent.html#method-i-delete_at]
+to remove the child at a specified offset:
+
+ d = REXML::Document.new('')
+ a = REXML::Element.new('a')
+ b = REXML::Element.new('b')
+ d.root.add(a)
+ d.root.add(b)
+ d.root.add(a)
+ d.root.add(b)
+ d.root.children # => [, , , ]
+ d.root.delete_at(2)
+ d.root.children # => [, , ]
+
+==== Task: Remove Children That Meet Specified Criteria
+
+Use method {Parent#delete_if}[../../../../REXML/Parent.html#method-i-delete_if]
+to remove children that meet criteria specified in the given block:
+
+ d = REXML::Document.new('')
+ d.root.add(REXML::Element.new('x'))
+ d.root.add(REXML::Element.new('xx'))
+ d.root.add(REXML::Element.new('xxx'))
+ d.root.add(REXML::Element.new('xxxx'))
+ d.root.children # => [, , , ]
+ d.root.delete_if {|child| child.name.size.odd? }
+ d.root.children # => [, ]
+
+=== Iterations
+
+==== Task: Iterate Over Children
+
+Use method {Parent#each_child}[../../../../REXML/Parent.html#method-i-each_child]
+(or its alias +each+) to iterate over all children:
+
+ xml_string = ''
+ d = REXML::Document.new(xml_string)
+ d.root.children # => [, , ]
+ d.root.each_child {|child| p child }
+
+Output:
+
+
+
+
+
+==== Task: Iterate Over Child Indexes
+
+Use method {Parent#each_index}[../../../../REXML/Parent.html#method-i-each_index]
+to iterate over all child indexes:
+
+ xml_string = ''
+ d = REXML::Document.new(xml_string)
+ d.root.children # => [, , ]
+ d.root.each_index {|child| p child }
+
+Output:
+
+ 0
+ 1
+ 2
+
+=== Clones
+
+==== Task: Clone Deeply
+
+Use method {Parent#deep_clone}[../../../../REXML/Parent.html#method-i-deep_clone]
+to clone deeply; that is, to clone every nested node that is a Parent object:
+
+ xml_string = <<-EOT
+
+
+
+ Everyday Italian
+ Giada De Laurentiis
+ 2005
+ 30.00
+
+
+ Harry Potter
+ J K. Rowling
+ 2005
+ 29.99
+
+
+ Learning XML
+ Erik T. Ray
+ 2003
+ 39.95
+
+
+ EOT
+ d = REXML::Document.new(xml_string)
+ root = d.root
+ shallow = root.clone
+ deep = root.deep_clone
+ shallow.to_s.size # => 12
+ deep.to_s.size # => 590
diff --git a/doc/rexml/tasks/tocs/child_toc.rdoc b/doc/rexml/tasks/tocs/child_toc.rdoc
new file mode 100644
index 00000000..a2083a09
--- /dev/null
+++ b/doc/rexml/tasks/tocs/child_toc.rdoc
@@ -0,0 +1,12 @@
+Tasks on this page:
+
+- {Relationships}[#label-Relationships]
+ - {Task: Set the Parent}[#label-Task-3A+Set+the+Parent]
+ - {Task: Insert Previous Sibling}[#label-Task-3A+Insert+Previous+Sibling]
+ - {Task: Insert Next Sibling}[#label-Task-3A+Insert+Next+Sibling]
+- {Removal or Replacement}[#label-Removal+or+Replacement]
+ - {Task: Remove Child from Parent}[#label-Task-3A+Remove+Child+from+Parent]
+ - {Task: Replace Child}[#label-Task-3A+Replace+Child]
+- {Document}[#label-Document]
+ - {Task: Get the Document}[#label-Task-3A+Get+the+Document]
+
diff --git a/doc/rexml/tasks/tocs/document_toc.rdoc b/doc/rexml/tasks/tocs/document_toc.rdoc
new file mode 100644
index 00000000..5db055ff
--- /dev/null
+++ b/doc/rexml/tasks/tocs/document_toc.rdoc
@@ -0,0 +1,30 @@
+Tasks on this page:
+
+- {New Document}[#label-New+Document]
+ - {Task: Create an Empty Document}[#label-Task-3A+Create+an+Empty+Document]
+ - {Task: Parse a String into a New Document}[#label-Task-3A+Parse+a+String+into+a+New+Document]
+ - {Task: Parse an IO Stream into a New Document}[#label-Task-3A+Parse+an+IO+Stream+into+a+New+Document]
+ - {Task: Create a Document from an Existing Document}[#label-Task-3A+Create+a+Document+from+an+Existing+Document]
+ - {Task: Clone a Document}[#label-Task-3A+Clone+a+Document]
+- {Document Type}[#label-Document+Type]
+ - {Task: Get the Document Type}[#label-Task-3A+Get+the+Document+Type]
+ - {Task: Set the Document Type}[#label-Task-3A+Set+the+Document+Type]
+- {XML Declaration}[#label-XML+Declaration]
+ - {Task: Get the XML Declaration}[#label-Task-3A+Get+the+XML+Declaration]
+ - {Task: Set the XML Declaration}[#label-Task-3A+Set+the+XML+Declaration]
+- {Children}[#label-Children]
+ - {Task: Add an Element Child}[#label-Task-3A+Add+an+Element+Child]
+ - {Task: Add a Non-Element Child}[#label-Task-3A+Add+a+Non-Element+Child]
+- {Writing}[#label-Writing]
+ - {Task: Write to $stdout}[#label-Task-3A+Write+to+-24stdout]
+ - {Task: Write to IO Stream}[#label-Task-3A+Write+to+IO+Stream]
+ - {Task: Write with No Indentation}[#label-Task-3A+Write+with+No+Indentation]
+ - {Task: Write with Specified Indentation}[#label-Task-3A+Write+with+Specified+Indentation]
+- {Querying}[#label-Querying]
+ - {Task: Get the Document}[#label-Task-3A+Get+the+Document]
+ - {Task: Get the Encoding}[#label-Task-3A+Get+the+Encoding]
+ - {Task: Get the Node Type}[#label-Task-3A+Get+the+Node+Type]
+ - {Task: Get the Root Element}[#label-Task-3A+Get+the+Root+Element]
+ - {Task: Determine Whether Stand-Alone}[#label-Task-3A+Determine+Whether+Stand-Alone]
+ - {Task: Get the Version}[#label-Task-3A+Get+the+Version]
+
diff --git a/doc/rexml/tasks/tocs/element_toc.rdoc b/doc/rexml/tasks/tocs/element_toc.rdoc
new file mode 100644
index 00000000..60a504a5
--- /dev/null
+++ b/doc/rexml/tasks/tocs/element_toc.rdoc
@@ -0,0 +1,55 @@
+Tasks on this page:
+
+- {New Element}[#label-New+Element]
+ - {Task: Create a Default Element}[#label-Task-3A+Create+a+Default+Element]
+ - {Task: Create a Named Element}[#label-Task-3A+Create+a+Named+Element]
+ - {Task: Create an Element with Name and Parent}[#label-Task-3A+Create+an+Element+with+Name+and+Parent]
+ - {Task: Create an Element with Name, Parent, and Context}[#label-Task-3A+Create+an+Element+with+Name-2C+Parent-2C+and+Context]
+ - {Task: Create a Shallow Clone}[#label-Task-3A+Create+a+Shallow+Clone]
+- {Attributes}[#label-Attributes]
+ - {Task: Create and Add an Attribute}[#label-Task-3A+Create+and+Add+an+Attribute]
+ - {Task: Add an Existing Attribute}[#label-Task-3A+Add+an+Existing+Attribute]
+ - {Task: Add Multiple Attributes from a Hash}[#label-Task-3A+Add+Multiple+Attributes+from+a+Hash]
+ - {Task: Add Multiple Attributes from an Array}[#label-Task-3A+Add+Multiple+Attributes+from+an+Array]
+ - {Task: Retrieve the Value for an Attribute Name}[#label-Task-3A+Retrieve+the+Value+for+an+Attribute+Name]
+ - {Task: Retrieve the Attribute Value for a Name and Namespace}[#label-Task-3A+Retrieve+the+Attribute+Value+for+a+Name+and+Namespace]
+ - {Task: Delete an Attribute}[#label-Task-3A+Delete+an+Attribute]
+ - {Task: Determine Whether the Element Has Attributes}[#label-Task-3A+Determine+Whether+the+Element+Has+Attributes]
+- {Children}[#label-Children]
+ - {Task: Create and Add an Element}[#label-Task-3A+Create+and+Add+an+Element]
+ - {Task: Add an Existing Element}[#label-Task-3A+Add+an+Existing+Element]
+ - {Task: Create and Add an Element with Attributes}[#label-Task-3A+Create+and+Add+an+Element+with+Attributes]
+ - {Task: Add an Existing Element with Added Attributes}[#label-Task-3A+Add+an+Existing+Element+with+Added+Attributes]
+ - {Task: Delete a Specified Element}[#label-Task-3A+Delete+a+Specified+Element]
+ - {Task: Delete an Element by Index}[#label-Task-3A+Delete+an+Element+by+Index]
+ - {Task: Delete an Element by XPath}[#label-Task-3A+Delete+an+Element+by+XPath]
+ - {Task: Determine Whether Element Children}[#label-Task-3A+Determine+Whether+Element+Children]
+ - {Task: Get Element Descendants by XPath}[#label-Task-3A+Get+Element+Descendants+by+XPath]
+ - {Task: Get Next Element Sibling}[#label-Task-3A+Get+Next+Element+Sibling]
+ - {Task: Get Previous Element Sibling}[#label-Task-3A+Get+Previous+Element+Sibling]
+ - {Task: Add a Text Node}[#label-Task-3A+Add+a+Text+Node]
+ - {Task: Replace the First Text Node}[#label-Task-3A+Replace+the+First+Text+Node]
+ - {Task: Remove the First Text Node}[#label-Task-3A+Remove+the+First+Text+Node]
+ - {Task: Retrieve the First Text Node}[#label-Task-3A+Retrieve+the+First+Text+Node]
+ - {Task: Retrieve a Specific Text Node}[#label-Task-3A+Retrieve+a+Specific+Text+Node]
+ - {Task: Determine Whether the Element has Text Nodes}[#label-Task-3A+Determine+Whether+the+Element+has+Text+Nodes]
+ - {Task: Get the Child at a Given Index}[#label-Task-3A+Get+the+Child+at+a+Given+Index]
+ - {Task: Get All CDATA Children}[#label-Task-3A+Get+All+CDATA+Children]
+ - {Task: Get All Comment Children}[#label-Task-3A+Get+All+Comment+Children]
+ - {Task: Get All Processing Instruction Children}[#label-Task-3A+Get+All+Processing+Instruction+Children]
+ - {Task: Get All Text Children}[#label-Task-3A+Get+All+Text+Children]
+- {Namespaces}[#label-Namespaces]
+ - {Task: Add a Namespace}[#label-Task-3A+Add+a+Namespace]
+ - {Task: Delete the Default Namespace}[#label-Task-3A+Delete+the+Default+Namespace]
+ - {Task: Delete a Specific Namespace}[#label-Task-3A+Delete+a+Specific+Namespace]
+ - {Task: Get a Namespace URI}[#label-Task-3A+Get+a+Namespace+URI]
+ - {Task: Retrieve Namespaces}[#label-Task-3A+Retrieve+Namespaces]
+ - {Task: Retrieve Namespace Prefixes}[#label-Task-3A+Retrieve+Namespace+Prefixes]
+- {Iteration}[#label-Iteration]
+ - {Task: Iterate Over Elements}[#label-Task-3A+Iterate+Over+Elements]
+ - {Task: Iterate Over Elements Having a Specified Attribute}[#label-Task-3A+Iterate+Over+Elements+Having+a+Specified+Attribute]
+ - {Task: Iterate Over Elements Having a Specified Attribute and Value}[#label-Task-3A+Iterate+Over+Elements+Having+a+Specified+Attribute+and+Value]
+ - {Task: Iterate Over Elements Having Specified Text}[#label-Task-3A+Iterate+Over+Elements+Having+Specified+Text]
+- {Context}[#label-Context]
+- {Other Getters}[#label-Other+Getters]
+
diff --git a/doc/rexml/tasks/tocs/master_toc.rdoc b/doc/rexml/tasks/tocs/master_toc.rdoc
new file mode 100644
index 00000000..0214f6b2
--- /dev/null
+++ b/doc/rexml/tasks/tocs/master_toc.rdoc
@@ -0,0 +1,135 @@
+== Tasks
+
+=== {Child}[../../tasks/rdoc/child_rdoc.html]
+- {Relationships}[../../tasks/rdoc/child_rdoc.html#label-Relationships]
+ - {Task: Set the Parent}[../../tasks/rdoc/child_rdoc.html#label-Task-3A+Set+the+Parent]
+ - {Task: Insert Previous Sibling}[../../tasks/rdoc/child_rdoc.html#label-Task-3A+Insert+Previous+Sibling]
+ - {Task: Insert Next Sibling}[../../tasks/rdoc/child_rdoc.html#label-Task-3A+Insert+Next+Sibling]
+- {Removal or Replacement}[../../tasks/rdoc/child_rdoc.html#label-Removal+or+Replacement]
+ - {Task: Remove Child from Parent}[../../tasks/rdoc/child_rdoc.html#label-Task-3A+Remove+Child+from+Parent]
+ - {Task: Replace Child}[../../tasks/rdoc/child_rdoc.html#label-Task-3A+Replace+Child]
+- {Document}[../../tasks/rdoc/child_rdoc.html#label-Document]
+ - {Task: Get the Document}[../../tasks/rdoc/child_rdoc.html#label-Task-3A+Get+the+Document]
+
+=== {Document}[../../tasks/rdoc/document_rdoc.html]
+- {New Document}[../../tasks/rdoc/document_rdoc.html#label-New+Document]
+ - {Task: Create an Empty Document}[../../tasks/rdoc/document_rdoc.html#label-Task-3A+Create+an+Empty+Document]
+ - {Task: Parse a String into a New Document}[../../tasks/rdoc/document_rdoc.html#label-Task-3A+Parse+a+String+into+a+New+Document]
+ - {Task: Parse an IO Stream into a New Document}[../../tasks/rdoc/document_rdoc.html#label-Task-3A+Parse+an+IO+Stream+into+a+New+Document]
+ - {Task: Create a Document from an Existing Document}[../../tasks/rdoc/document_rdoc.html#label-Task-3A+Create+a+Document+from+an+Existing+Document]
+ - {Task: Clone a Document}[../../tasks/rdoc/document_rdoc.html#label-Task-3A+Clone+a+Document]
+- {Document Type}[../../tasks/rdoc/document_rdoc.html#label-Document+Type]
+ - {Task: Get the Document Type}[../../tasks/rdoc/document_rdoc.html#label-Task-3A+Get+the+Document+Type]
+ - {Task: Set the Document Type}[../../tasks/rdoc/document_rdoc.html#label-Task-3A+Set+the+Document+Type]
+- {XML Declaration}[../../tasks/rdoc/document_rdoc.html#label-XML+Declaration]
+ - {Task: Get the XML Declaration}[../../tasks/rdoc/document_rdoc.html#label-Task-3A+Get+the+XML+Declaration]
+ - {Task: Set the XML Declaration}[../../tasks/rdoc/document_rdoc.html#label-Task-3A+Set+the+XML+Declaration]
+- {Children}[../../tasks/rdoc/document_rdoc.html#label-Children]
+ - {Task: Add an Element Child}[../../tasks/rdoc/document_rdoc.html#label-Task-3A+Add+an+Element+Child]
+ - {Task: Add a Non-Element Child}[../../tasks/rdoc/document_rdoc.html#label-Task-3A+Add+a+Non-Element+Child]
+- {Writing}[../../tasks/rdoc/document_rdoc.html#label-Writing]
+ - {Task: Write to $stdout}[../../tasks/rdoc/document_rdoc.html#label-Task-3A+Write+to+-24stdout]
+ - {Task: Write to IO Stream}[../../tasks/rdoc/document_rdoc.html#label-Task-3A+Write+to+IO+Stream]
+ - {Task: Write with No Indentation}[../../tasks/rdoc/document_rdoc.html#label-Task-3A+Write+with+No+Indentation]
+ - {Task: Write with Specified Indentation}[../../tasks/rdoc/document_rdoc.html#label-Task-3A+Write+with+Specified+Indentation]
+- {Querying}[../../tasks/rdoc/document_rdoc.html#label-Querying]
+ - {Task: Get the Document}[../../tasks/rdoc/document_rdoc.html#label-Task-3A+Get+the+Document]
+ - {Task: Get the Encoding}[../../tasks/rdoc/document_rdoc.html#label-Task-3A+Get+the+Encoding]
+ - {Task: Get the Node Type}[../../tasks/rdoc/document_rdoc.html#label-Task-3A+Get+the+Node+Type]
+ - {Task: Get the Root Element}[../../tasks/rdoc/document_rdoc.html#label-Task-3A+Get+the+Root+Element]
+ - {Task: Determine Whether Stand-Alone}[../../tasks/rdoc/document_rdoc.html#label-Task-3A+Determine+Whether+Stand-Alone]
+ - {Task: Get the Version}[../../tasks/rdoc/document_rdoc.html#label-Task-3A+Get+the+Version]
+
+=== {Element}[../../tasks/rdoc/element_rdoc.html]
+- {New Element}[../../tasks/rdoc/element_rdoc.html#label-New+Element]
+ - {Task: Create a Default Element}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Create+a+Default+Element]
+ - {Task: Create a Named Element}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Create+a+Named+Element]
+ - {Task: Create an Element with Name and Parent}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Create+an+Element+with+Name+and+Parent]
+ - {Task: Create an Element with Name, Parent, and Context}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Create+an+Element+with+Name-2C+Parent-2C+and+Context]
+ - {Task: Create a Shallow Clone}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Create+a+Shallow+Clone]
+- {Attributes}[../../tasks/rdoc/element_rdoc.html#label-Attributes]
+ - {Task: Create and Add an Attribute}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Create+and+Add+an+Attribute]
+ - {Task: Add an Existing Attribute}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Add+an+Existing+Attribute]
+ - {Task: Add Multiple Attributes from a Hash}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Add+Multiple+Attributes+from+a+Hash]
+ - {Task: Add Multiple Attributes from an Array}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Add+Multiple+Attributes+from+an+Array]
+ - {Task: Retrieve the Value for an Attribute Name}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Retrieve+the+Value+for+an+Attribute+Name]
+ - {Task: Retrieve the Attribute Value for a Name and Namespace}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Retrieve+the+Attribute+Value+for+a+Name+and+Namespace]
+ - {Task: Delete an Attribute}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Delete+an+Attribute]
+ - {Task: Determine Whether the Element Has Attributes}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Determine+Whether+the+Element+Has+Attributes]
+- {Children}[../../tasks/rdoc/element_rdoc.html#label-Children]
+ - {Task: Create and Add an Element}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Create+and+Add+an+Element]
+ - {Task: Add an Existing Element}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Add+an+Existing+Element]
+ - {Task: Create and Add an Element with Attributes}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Create+and+Add+an+Element+with+Attributes]
+ - {Task: Add an Existing Element with Added Attributes}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Add+an+Existing+Element+with+Added+Attributes]
+ - {Task: Delete a Specified Element}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Delete+a+Specified+Element]
+ - {Task: Delete an Element by Index}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Delete+an+Element+by+Index]
+ - {Task: Delete an Element by XPath}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Delete+an+Element+by+XPath]
+ - {Task: Determine Whether Element Children}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Determine+Whether+Element+Children]
+ - {Task: Get Element Descendants by XPath}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Get+Element+Descendants+by+XPath]
+ - {Task: Get Next Element Sibling}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Get+Next+Element+Sibling]
+ - {Task: Get Previous Element Sibling}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Get+Previous+Element+Sibling]
+ - {Task: Add a Text Node}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Add+a+Text+Node]
+ - {Task: Replace the First Text Node}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Replace+the+First+Text+Node]
+ - {Task: Remove the First Text Node}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Remove+the+First+Text+Node]
+ - {Task: Retrieve the First Text Node}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Retrieve+the+First+Text+Node]
+ - {Task: Retrieve a Specific Text Node}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Retrieve+a+Specific+Text+Node]
+ - {Task: Determine Whether the Element has Text Nodes}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Determine+Whether+the+Element+has+Text+Nodes]
+ - {Task: Get the Child at a Given Index}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Get+the+Child+at+a+Given+Index]
+ - {Task: Get All CDATA Children}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Get+All+CDATA+Children]
+ - {Task: Get All Comment Children}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Get+All+Comment+Children]
+ - {Task: Get All Processing Instruction Children}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Get+All+Processing+Instruction+Children]
+ - {Task: Get All Text Children}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Get+All+Text+Children]
+- {Namespaces}[../../tasks/rdoc/element_rdoc.html#label-Namespaces]
+ - {Task: Add a Namespace}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Add+a+Namespace]
+ - {Task: Delete the Default Namespace}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Delete+the+Default+Namespace]
+ - {Task: Delete a Specific Namespace}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Delete+a+Specific+Namespace]
+ - {Task: Get a Namespace URI}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Get+a+Namespace+URI]
+ - {Task: Retrieve Namespaces}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Retrieve+Namespaces]
+ - {Task: Retrieve Namespace Prefixes}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Retrieve+Namespace+Prefixes]
+- {Iteration}[../../tasks/rdoc/element_rdoc.html#label-Iteration]
+ - {Task: Iterate Over Elements}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Iterate+Over+Elements]
+ - {Task: Iterate Over Elements Having a Specified Attribute}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Iterate+Over+Elements+Having+a+Specified+Attribute]
+ - {Task: Iterate Over Elements Having a Specified Attribute and Value}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Iterate+Over+Elements+Having+a+Specified+Attribute+and+Value]
+ - {Task: Iterate Over Elements Having Specified Text}[../../tasks/rdoc/element_rdoc.html#label-Task-3A+Iterate+Over+Elements+Having+Specified+Text]
+- {Context}[../../tasks/rdoc/element_rdoc.html#label-Context]
+- {Other Getters}[../../tasks/rdoc/element_rdoc.html#label-Other+Getters]
+
+=== {Node}[../../tasks/rdoc/node_rdoc.html]
+- {Siblings}[../../tasks/rdoc/node_rdoc.html#label-Siblings]
+ - {Task: Find Previous Sibling}[../../tasks/rdoc/node_rdoc.html#label-Task-3A+Find+Previous+Sibling]
+ - {Task: Find Next Sibling}[../../tasks/rdoc/node_rdoc.html#label-Task-3A+Find+Next+Sibling]
+- {Position}[../../tasks/rdoc/node_rdoc.html#label-Position]
+ - {Task: Find Own Index Among Siblings}[../../tasks/rdoc/node_rdoc.html#label-Task-3A+Find+Own+Index+Among+Siblings]
+- {Recursive Traversal}[../../tasks/rdoc/node_rdoc.html#label-Recursive+Traversal]
+ - {Task: Traverse Each Recursively}[../../tasks/rdoc/node_rdoc.html#label-Task-3A+Traverse+Each+Recursively]
+- {Recursive Search}[../../tasks/rdoc/node_rdoc.html#label-Recursive+Search]
+ - {Task: Traverse Each Recursively}[../../tasks/rdoc/node_rdoc.html#label-Task-3A+Traverse+Each+Recursively]
+- {Representation}[../../tasks/rdoc/node_rdoc.html#label-Representation]
+ - {Task: Represent a String}[../../tasks/rdoc/node_rdoc.html#label-Task-3A+Represent+a+String]
+- {Parent?}[../../tasks/rdoc/node_rdoc.html#label-Parent-3F]
+ - {Task: Determine Whether the Node is a Parent}[../../tasks/rdoc/node_rdoc.html#label-Task-3A+Determine+Whether+the+Node+is+a+Parent]
+
+=== {Parent}[../../tasks/rdoc/parent_rdoc.html]
+- {Queries}[../../tasks/rdoc/parent_rdoc.html#label-Queries]
+ - {Task: Get the Count of Children}[../../tasks/rdoc/parent_rdoc.html#label-Task-3A+Get+the+Count+of+Children]
+ - {Task: Get the Child at a Given Index}[../../tasks/rdoc/parent_rdoc.html#label-Task-3A+Get+the+Child+at+a+Given+Index]
+ - {Task: Get the Index of a Given Child}[../../tasks/rdoc/parent_rdoc.html#label-Task-3A+Get+the+Index+of+a+Given+Child]
+ - {Task: Get the Children}[../../tasks/rdoc/parent_rdoc.html#label-Task-3A+Get+the+Children]
+ - {Task: Determine Whether the Node is a Parent}[../../tasks/rdoc/parent_rdoc.html#label-Task-3A+Determine+Whether+the+Node+is+a+Parent]
+- {Additions}[../../tasks/rdoc/parent_rdoc.html#label-Additions]
+ - {Task: Add a Child at the Beginning}[../../tasks/rdoc/parent_rdoc.html#label-Task-3A+Add+a+Child+at+the+Beginning]
+ - {Task: Add a Child at the End}[../../tasks/rdoc/parent_rdoc.html#label-Task-3A+Add+a+Child+at+the+End]
+ - {Task: Replace a Child with Another Child}[../../tasks/rdoc/parent_rdoc.html#label-Task-3A+Replace+a+Child+with+Another+Child]
+ - {Task: Replace Multiple Children with Another Child}[../../tasks/rdoc/parent_rdoc.html#label-Task-3A+Replace+Multiple+Children+with+Another+Child]
+ - {Task: Insert Child Before a Given Child}[../../tasks/rdoc/parent_rdoc.html#label-Task-3A+Insert+Child+Before+a+Given+Child]
+ - {Task: Insert Child After a Given Child}[../../tasks/rdoc/parent_rdoc.html#label-Task-3A+Insert+Child+After+a+Given+Child]
+- {Deletions}[../../tasks/rdoc/parent_rdoc.html#label-Deletions]
+ - {Task: Remove a Given Child}[../../tasks/rdoc/parent_rdoc.html#label-Task-3A+Remove+a+Given+Child]
+ - {Task: Remove the Child at a Specified Offset}[../../tasks/rdoc/parent_rdoc.html#label-Task-3A+Remove+the+Child+at+a+Specified+Offset]
+ - {Task: Remove Children That Meet Specified Criteria}[../../tasks/rdoc/parent_rdoc.html#label-Task-3A+Remove+Children+That+Meet+Specified+Criteria]
+- {Iterations}[../../tasks/rdoc/parent_rdoc.html#label-Iterations]
+ - {Task: Iterate Over Children}[../../tasks/rdoc/parent_rdoc.html#label-Task-3A+Iterate+Over+Children]
+ - {Task: Iterate Over Child Indexes}[../../tasks/rdoc/parent_rdoc.html#label-Task-3A+Iterate+Over+Child+Indexes]
+- {Clones}[../../tasks/rdoc/parent_rdoc.html#label-Clones]
+ - {Task: Clone Deeply}[../../tasks/rdoc/parent_rdoc.html#label-Task-3A+Clone+Deeply]
+
diff --git a/doc/rexml/tasks/tocs/node_toc.rdoc b/doc/rexml/tasks/tocs/node_toc.rdoc
new file mode 100644
index 00000000..d9114faf
--- /dev/null
+++ b/doc/rexml/tasks/tocs/node_toc.rdoc
@@ -0,0 +1,16 @@
+Tasks on this page:
+
+- {Siblings}[#label-Siblings]
+ - {Task: Find Previous Sibling}[#label-Task-3A+Find+Previous+Sibling]
+ - {Task: Find Next Sibling}[#label-Task-3A+Find+Next+Sibling]
+- {Position}[#label-Position]
+ - {Task: Find Own Index Among Siblings}[#label-Task-3A+Find+Own+Index+Among+Siblings]
+- {Recursive Traversal}[#label-Recursive+Traversal]
+ - {Task: Traverse Each Recursively}[#label-Task-3A+Traverse+Each+Recursively]
+- {Recursive Search}[#label-Recursive+Search]
+ - {Task: Traverse Each Recursively}[#label-Task-3A+Traverse+Each+Recursively]
+- {Representation}[#label-Representation]
+ - {Task: Represent a String}[#label-Task-3A+Represent+a+String]
+- {Parent?}[#label-Parent-3F]
+ - {Task: Determine Whether the Node is a Parent}[#label-Task-3A+Determine+Whether+the+Node+is+a+Parent]
+
diff --git a/doc/rexml/tasks/tocs/parent_toc.rdoc b/doc/rexml/tasks/tocs/parent_toc.rdoc
new file mode 100644
index 00000000..68fc0b70
--- /dev/null
+++ b/doc/rexml/tasks/tocs/parent_toc.rdoc
@@ -0,0 +1,25 @@
+Tasks on this page:
+
+- {Queries}[#label-Queries]
+ - {Task: Get the Count of Children}[#label-Task-3A+Get+the+Count+of+Children]
+ - {Task: Get the Child at a Given Index}[#label-Task-3A+Get+the+Child+at+a+Given+Index]
+ - {Task: Get the Index of a Given Child}[#label-Task-3A+Get+the+Index+of+a+Given+Child]
+ - {Task: Get the Children}[#label-Task-3A+Get+the+Children]
+ - {Task: Determine Whether the Node is a Parent}[#label-Task-3A+Determine+Whether+the+Node+is+a+Parent]
+- {Additions}[#label-Additions]
+ - {Task: Add a Child at the Beginning}[#label-Task-3A+Add+a+Child+at+the+Beginning]
+ - {Task: Add a Child at the End}[#label-Task-3A+Add+a+Child+at+the+End]
+ - {Task: Replace a Child with Another Child}[#label-Task-3A+Replace+a+Child+with+Another+Child]
+ - {Task: Replace Multiple Children with Another Child}[#label-Task-3A+Replace+Multiple+Children+with+Another+Child]
+ - {Task: Insert Child Before a Given Child}[#label-Task-3A+Insert+Child+Before+a+Given+Child]
+ - {Task: Insert Child After a Given Child}[#label-Task-3A+Insert+Child+After+a+Given+Child]
+- {Deletions}[#label-Deletions]
+ - {Task: Remove a Given Child}[#label-Task-3A+Remove+a+Given+Child]
+ - {Task: Remove the Child at a Specified Offset}[#label-Task-3A+Remove+the+Child+at+a+Specified+Offset]
+ - {Task: Remove Children That Meet Specified Criteria}[#label-Task-3A+Remove+Children+That+Meet+Specified+Criteria]
+- {Iterations}[#label-Iterations]
+ - {Task: Iterate Over Children}[#label-Task-3A+Iterate+Over+Children]
+ - {Task: Iterate Over Child Indexes}[#label-Task-3A+Iterate+Over+Child+Indexes]
+- {Clones}[#label-Clones]
+ - {Task: Clone Deeply}[#label-Task-3A+Clone+Deeply]
+
diff --git a/doc/rexml/tutorial.rdoc b/doc/rexml/tutorial.rdoc
new file mode 100644
index 00000000..c85a70d0
--- /dev/null
+++ b/doc/rexml/tutorial.rdoc
@@ -0,0 +1,1358 @@
+= \REXML Tutorial
+
+== Why \REXML?
+
+- Ruby's \REXML library is part of the Ruby distribution,
+ so using it requires no gem installations.
+- \REXML is fully maintained.
+- \REXML is mature, having been in use for long years.
+
+== To Include, or Not to Include?
+
+REXML is a module.
+To use it, you must require it:
+
+ require 'rexml' # => true
+
+If you do not also include it, you must fully qualify references to REXML:
+
+ REXML::Document # => REXML::Document
+
+If you also include the module, you may optionally omit REXML:::
+
+ include REXML
+ Document # => REXML::Document
+ REXML::Document # => REXML::Document
+
+== Preliminaries
+
+All examples here assume that the following code has been executed:
+
+ require 'rexml'
+ include REXML
+
+The source XML for many examples here is from file
+{books.xml}[https://www.w3schools.com/xml/books.xml] at w3schools.com.
+You may find it convenient to open that page in a new tab
+(Ctrl-click in some browsers).
+
+Note that your browser may display the XML with modified whitespace
+and without the XML declaration, which in this case is:
+
+
+
+For convenience, we capture the XML into a string variable:
+
+ require 'open-uri'
+ source_string = URI.open('https://www.w3schools.com/xml/books.xml').read
+
+And into a file:
+
+ File.write('source_file.xml', source_string)
+
+Throughout these examples, variable +doc+ will hold only the document
+derived from these sources:
+
+ doc = Document.new(source_string)
+
+== Parsing \XML \Source
+
+=== Parsing a Document
+
+Use method REXML::Document::new to parse XML source.
+
+The source may be a string:
+
+ doc = Document.new(source_string)
+
+Or an \IO stream:
+
+ doc = File.open('source_file.xml', 'r') do |io|
+ Document.new(io)
+ end
+
+Method URI.open returns a StringIO object,
+so the source can be from a web page:
+
+ require 'open-uri'
+ io = URI.open("https://www.w3schools.com/xml/books.xml")
+ io.class # => StringIO
+ doc = Document.new(io)
+
+For any of these sources, the returned object is an REXML::Document:
+
+ doc # => ... >
+ doc.class # => REXML::Document
+
+Note: 'UNDEFINED' is the "name" displayed for a document,
+even though doc.name returns an empty string "".
+
+A parsed document may produce \REXML objects of many classes,
+but the two that are likely to be of greatest interest are
+REXML::Document and REXML::Element.
+These two classes are covered in great detail in this tutorial.
+
+=== Context (Parsing Options)
+
+The context for parsing a document is a hash that influences
+the way the XML is read and stored.
+
+The context entries are:
+
+- +:respect_whitespace+: controls treatment of whitespace.
+- +:compress_whitespace+: determines whether whitespace is compressed.
+- +:ignore_whitespace_nodes+: determines whether whitespace-only nodes are to be ignored.
+- +:raw+: controls treatment of special characters and entities.
+
+See {Element Context}[../context_rdoc.html].
+
+== Exploring the Document
+
+An REXML::Document object represents an XML document.
+
+The object inherits from its ancestor classes:
+
+- REXML::Child (includes module REXML::Node)
+ - REXML::Parent (includes module {Enumerable}[rdoc-ref:Enumerable]).
+ - REXML::Element (includes module REXML::Namespace).
+ - REXML::Document
+
+This section covers only those properties and methods that are unique to a document
+(that is, not inherited or included).
+
+=== Document Properties
+
+A document has several properties (other than its children);
+
+- Document type.
+- Node type.
+- Name.
+- Document.
+- XPath
+
+[Document Type]
+
+ A document may have a document type:
+
+ my_xml = ''
+ my_doc = Document.new(my_xml)
+ doc_type = my_doc.doctype
+ doc_type.class # => REXML::DocType
+ doc_type.to_s # => ""
+
+[Node Type]
+
+ A document also has a node type (always +:document+):
+
+ doc.node_type # => :document
+
+[Name]
+
+ A document has a name (always an empty string):
+
+ doc.name # => ""
+
+[Document]
+
+ \Method REXML::Document#document returns +self+:
+
+ doc.document == doc # => true
+
+ An object of a different class (\REXML::Element or \REXML::Child)
+ may have a document, which is the document to which the object belongs;
+ if so, that document will be an \REXML::Document object.
+
+ doc.root.document.class # => REXML::Document
+
+[XPath]
+
+ \method REXML::Element#xpath returns the string xpath to the element,
+ relative to its most distant ancestor:
+
+ doc.root.class # => REXML::Element
+ doc.root.xpath # => "/bookstore"
+ doc.root.texts.first # => "\n\n"
+ doc.root.texts.first.xpath # => "/bookstore/text()"
+
+ If there is no ancestor, returns the expanded name of the element:
+
+ Element.new('foo').xpath # => "foo"
+
+=== Document Children
+
+A document may have children of these types:
+
+- XML declaration.
+- Root element.
+- Text.
+- Processing instructions.
+- Comments.
+- CDATA.
+
+[XML Declaration]
+
+ A document may an XML declaration, which is stored as an REXML::XMLDecl object:
+
+ doc.xml_decl # =>
+ doc.xml_decl.class # => REXML::XMLDecl
+
+ Document.new('').xml_decl # =>
+
+ my_xml = '"'
+ my_doc = Document.new(my_xml)
+ xml_decl = my_doc.xml_decl
+ xml_decl.to_s # => ""
+
+ The version, encoding, and stand-alone values may be retrieved separately:
+
+ my_doc.version # => "1.0"
+ my_doc.encoding # => "UTF-8"
+ my_doc.stand_alone? # => "yes"
+
+[Root Element]
+
+ A document may have a single element child, called the _root_ _element_,
+ which is stored as an REXML::Element object;
+ it may be retrieved with method +root+:
+
+ doc.root # => ... >
+ doc.root.class # => REXML::Element
+
+ Document.new('').root # => nil
+
+[Text]
+
+ A document may have text passages, each of which is stored
+ as an REXML::Text object:
+
+ doc.texts.each {|t| p [t.class, t] }
+
+ Output:
+
+ [REXML::Text, "\n"]
+
+[Processing Instructions]
+
+ A document may have processing instructions, which are stored
+ as REXML::Instruction objects:
+
+
+
+ Output:
+
+ [REXML::Instruction, ]
+ [REXML::Instruction, ]
+
+[Comments]
+
+ A document may have comments, which are stored
+ as REXML::Comment objects:
+
+ my_xml = <<-EOT
+
+
+ EOT
+ my_doc = Document.new(my_xml)
+ my_doc.comments.each {|c| p [c.class, c] }
+
+ Output:
+
+ [REXML::Comment, # ... >, @string="foo">]
+ [REXML::Comment, # ... >, @string="bar">]
+
+[CDATA]
+
+ A document may have CDATA entries, which are stored
+ as REXML::CData objects:
+
+ my_xml = <<-EOT
+
+
+ EOT
+ my_doc = Document.new(my_xml)
+ my_doc.cdatas.each {|cd| p [cd.class, cd] }
+
+ Output:
+
+ [REXML::CData, "foo"]
+ [REXML::CData, "bar"]
+
+The payload of a document is a tree of nodes, descending from the root element:
+
+ doc.root.children.each do |child|
+ p [child, child.class]
+ end
+
+Output:
+
+ [REXML::Text, "\n\n"]
+ [REXML::Element, ... >]
+ [REXML::Text, "\n\n"]
+ [REXML::Element, ... >]
+ [REXML::Text, "\n\n"]
+ [REXML::Element, ... >]
+ [REXML::Text, "\n\n"]
+ [REXML::Element, ... >]
+ [REXML::Text, "\n\n"]
+
+== Exploring an Element
+
+An REXML::Element object represents an XML element.
+
+The object inherits from its ancestor classes:
+
+- REXML::Child (includes module REXML::Node)
+ - REXML::Parent (includes module {Enumerable}[rdoc-ref:Enumerable]).
+ - REXML::Element (includes module REXML::Namespace).
+
+This section covers methods:
+
+- Defined in REXML::Element itself.
+- Inherited from REXML::Parent and REXML::Child.
+- Included from REXML::Node.
+
+=== Inside the Element
+
+[Brief String Representation]
+
+ Use method REXML::Element#inspect to retrieve a brief string representation.
+
+ doc.root.inspect # => " ... >"
+
+ The ellipsis (...) indicates that the element has children.
+ When there are no children, the ellipsis is omitted:
+
+ Element.new('foo').inspect # => ""
+
+ If the element has attributes, those are also included:
+
+ doc.root.elements.first.inspect # => " ... >"
+
+[Extended String Representation]
+
+ Use inherited method REXML::Child.bytes to retrieve an extended
+ string representation.
+
+ doc.root.bytes # => "\n\n\n Everyday Italian\n Giada De Laurentiis\n 2005\n 30.00\n\n\n\n Harry Potter\n J K. Rowling\n 2005\n 29.99\n\n\n\n XQuery Kick Start\n James McGovern\n Per Bothner\n Kurt Cagle\n James Linn\n Vaidyanathan Nagarajan\n 2003\n 49.99\n\n\n\n Learning XML\n Erik T. Ray\n 2003\n 39.95\n\n\n"
+
+[Node Type]
+
+ Use method REXML::Element#node_type to retrieve the node type (always +:element+):
+
+ doc.root.node_type # => :element
+
+[Raw Mode]
+
+ Use method REXML::Element#raw to retrieve whether (+true+ or +nil+)
+ raw mode is set.
+
+ doc.root.raw # => nil
+
+[Context]
+
+ Use method REXML::Element#context to retrieve the context hash
+ (see {Element Context}[../context_rdoc.html]):
+
+ doc.root.context # => {}
+
+=== Relationships
+
+An element may have:
+
+- Ancestors.
+- Siblings.
+- Children.
+
+==== Ancestors
+
+[Containing Document]
+
+ Use method REXML::Element#document to retrieve the containing document, if any:
+
+ ele = doc.root.elements.first # => ... >
+ ele.document # => ... >
+ ele = Element.new('foo') # =>
+ ele.document # => nil
+
+[Root Element]
+
+ Use method REXML::Element#root to retrieve the root element:
+
+ ele = doc.root.elements.first # => ... >
+ ele.root # => ... >
+ ele = Element.new('foo') # =>
+ ele.root # =>
+
+[Root Node]
+
+ Use method REXML::Element#root_node to retrieve the most distant ancestor,
+ which is the containing document, if any, otherwise the root element:
+
+ ele = doc.root.elements.first # => ... >
+ ele.root_node # => ... >
+ ele = Element.new('foo') # =>
+ ele.root_node # =>
+
+[Parent]
+
+ Use inherited method REXML::Child#parent to retrieve the parent
+
+ ele = doc.root # => ... >
+ ele.parent # => ... >
+ ele = doc.root.elements.first # => ... >
+ ele.parent # => ... >
+
+ Use included method REXML::Node#index_in_parent to retrieve the index
+ of the element among all of its parents children (not just the element children).
+ Note that while the index for doc.root.elements[n] is 1-based,
+ the returned index is 0-based.
+
+ doc.root.children # =>
+ # ["\n\n",
+ # ... >,
+ # "\n\n",
+ # ... >,
+ # "\n\n",
+ # ... >,
+ # "\n\n",
+ # ... >,
+ # "\n\n"]
+ ele = doc.root.elements[1] # => ... >
+ ele.index_in_parent # => 2
+ ele = doc.root.elements[2] # => ... >
+ ele.index_in_parent# => 4
+
+==== Siblings
+
+[Next Element]
+
+ Use method REXML::Element#next_element to retrieve the first following
+ sibling that is itself an element (+nil+ if there is none):
+
+ ele = doc.root.elements[1]
+ while ele do
+ p [ele.class, ele]
+ ele = ele.next_element
+ end
+ p ele
+
+ Output:
+
+ [REXML::Element, ... >]
+ [REXML::Element, ... >]
+ [REXML::Element, ... >]
+ [REXML::Element, ... >]
+
+[Previous Element]
+
+ Use method REXML::Element#previous_element to retrieve the first preceding
+ sibling that is itself an element (+nil+ if there is none):
+
+ ele = doc.root.elements[4]
+ while ele do
+ p [ele.class, ele]
+ ele = ele.previous_element
+ end
+ p ele
+
+ Output:
+
+ [REXML::Element, ... >]
+ [REXML::Element, ... >]
+ [REXML::Element, ... >]
+ [REXML::Element, ... >]
+
+[Next Node]
+
+ Use included method REXML::Node.next_sibling_node
+ (or its alias next_sibling) to retrieve the first following node
+ regardless of its class:
+
+ node = doc.root.children[0]
+ while node do
+ p [node.class, node]
+ node = node.next_sibling
+ end
+ p node
+
+ Output:
+
+ [REXML::Text, "\n\n"]
+ [REXML::Element, ... >]
+ [REXML::Text, "\n\n"]
+ [REXML::Element, ... >]
+ [REXML::Text, "\n\n"]
+ [REXML::Element, ... >]
+ [REXML::Text, "\n\n"]
+ [REXML::Element, ... >]
+ [REXML::Text, "\n\n"]
+
+[Previous Node]
+
+ Use included method REXML::Node.previous_sibling_node
+ (or its alias previous_sibling) to retrieve the first preceding node
+ regardless of its class:
+
+ node = doc.root.children[-1]
+ while node do
+ p [node.class, node]
+ node = node.previous_sibling
+ end
+ p node
+
+ Output:
+
+ [REXML::Text, "\n\n"]
+ [REXML::Element, ... >]
+ [REXML::Text, "\n\n"]
+ [REXML::Element, ... >]
+ [REXML::Text, "\n\n"]
+ [REXML::Element, ... >]
+ [REXML::Text, "\n\n"]
+ [REXML::Element, ... >]
+ [REXML::Text, "\n\n"]
+
+==== Children
+
+[Child Count]
+
+ Use inherited method REXML::Parent.size to retrieve the count
+ of nodes (of all types) in the element:
+
+ doc.root.size # => 9
+
+[Child Nodes]
+
+ Use inherited method REXML::Parent.children to retrieve an array
+ of the child nodes (of all types):
+
+ doc.root.children # =>
+ # ["\n\n",
+ # ... >,
+ # "\n\n",
+ # ... >,
+ # "\n\n",
+ # ... >,
+ # "\n\n",
+ # ... >,
+ # "\n\n"]
+
+[Child at Index]
+
+ Use method REXML::Element#[] to retrieve the child at a given numerical index,
+ or +nil+ if there is no such child:
+
+ doc.root[0] # => "\n\n"
+ doc.root[1] # => ... >
+ doc.root[7] # => ... >
+ doc.root[8] # => "\n\n"
+
+ doc.root[-1] # => "\n\n"
+ doc.root[-2] # => ... >
+
+ doc.root[50] # => nil
+
+[Index of Child]
+
+ Use method REXML::Parent#index to retrieve the zero-based child index
+ of the given object, or #size - 1 if there is no such child:
+
+ ele = doc.root # => ... >
+ ele.index(ele[0]) # => 0
+ ele.index(ele[1]) # => 1
+ ele.index(ele[7]) # => 7
+ ele.index(ele[8]) # => 8
+
+ ele.index(ele[-1]) # => 8
+ ele.index(ele[-2]) # => 7
+
+ ele.index(ele[50]) # => 8
+
+[Element Children]
+
+ Use method REXML::Element#has_elements? to retrieve whether the element
+ has element children:
+
+ doc.root.has_elements? # => true
+ REXML::Element.new('foo').has_elements? # => false
+
+ Use method REXML::Element#elements to retrieve the REXML::Elements object
+ containing the element children:
+
+ eles = doc.root.elements
+ eles # => # ... >>
+ eles.size # => 4
+ eles.each {|e| p [e.class], e }
+
+ Output:
+
+ [ ... >,
+ ... >,
+ ... >,
+ ... >
+ ]
+
+Note that while in this example, all the element children of the root element are
+elements of the same name, 'book', that is not true of all documents;
+a root element (or any other element) may have any mixture of child elements.
+
+[CDATA Children]
+
+ Use method REXML::Element#cdatas to retrieve a frozen array of CDATA children:
+
+ my_xml = <<-EOT
+
+
+
+
+ EOT
+ my_doc = REXML::Document.new(my_xml)
+ cdatas my_doc.root.cdatas
+ cdatas.frozen? # => true
+ cdatas.map {|cd| cd.class } # => [REXML::CData, REXML::CData]
+
+[Comment Children]
+
+ Use method REXML::Element#comments to retrieve a frozen array of comment children:
+
+ my_xml = <<-EOT
+
+
+
+
+ EOT
+ my_doc = REXML::Document.new(my_xml)
+ comments = my_doc.root.comments
+ comments.frozen? # => true
+ comments.map {|c| c.class } # => [REXML::Comment, REXML::Comment]
+ comments.map {|c| c.to_s } # => ["foo", "bar"]
+
+[Processing Instruction Children]
+
+ Use method REXML::Element#instructions to retrieve a frozen array
+ of processing instruction children:
+
+ my_xml = <<-EOT
+
+
+
+
+ EOT
+ my_doc = REXML::Document.new(my_xml)
+ instrs = my_doc.root.instructions
+ instrs.frozen? # => true
+ instrs.map {|i| i.class } # => [REXML::Instruction, REXML::Instruction]
+ instrs.map {|i| i.to_s } # => ["", ""]
+
+[Text Children]
+
+ Use method REXML::Element#has_text? to retrieve whether the element
+ has text children:
+
+ doc.root.has_text? # => true
+ REXML::Element.new('foo').has_text? # => false
+
+ Use method REXML::Element#texts to retrieve a frozen array of text children:
+
+ my_xml = 'textmore'
+ my_doc = REXML::Document.new(my_xml)
+ texts = my_doc.root.texts
+ texts.frozen? # => true
+ texts.map {|t| t.class } # => [REXML::Text, REXML::Text]
+ texts.map {|t| t.to_s } # => ["text", "more"]
+
+[Parenthood]
+
+ Use inherited method REXML::Parent.parent? to retrieve whether the element is a parent;
+ always returns +true+; only REXML::Child#parent returns +false+.
+
+ doc.root.parent? # => true
+
+=== Element Attributes
+
+Use method REXML::Element#has_attributes? to return whether the element
+has attributes:
+
+ ele = doc.root # => ... >
+ ele.has_attributes? # => false
+ ele = ele.elements.first # => ... >
+ ele.has_attributes? # => true
+
+Use method REXML::Element#attributes to return the hash
+containing the attributes for the element.
+Each hash key is a string attribute name;
+each hash value is an REXML::Attribute object.
+
+ ele = doc.root # => ... >
+ attrs = ele.attributes # => {}
+
+ ele = ele.elements.first # => ... >
+ attrs = ele.attributes # => {"category"=>category='cooking'}
+ attrs.size # => 1
+ attr_name = attrs.keys.first # => "category"
+ attr_name.class # => String
+ attr_value = attrs.values.first # => category='cooking'
+ attr_value.class # => REXML::Attribute
+
+Use method REXML::Element#[] to retrieve the string value for a given attribute,
+which may be given as either a string or a symbol:
+
+ ele = doc.root.elements.first # => ... >
+ attr_value = ele['category'] # => "cooking"
+ attr_value.class # => String
+ ele['nosuch'] # => nil
+
+Use method REXML::Element#attribute to retrieve the value of a named attribute:
+
+ my_xml = ""
+ my_doc = REXML::Document.new(my_xml)
+ my_doc.root.attribute("x") # => x='x'
+ my_doc.root.attribute("x", "a") # => a:x='a:x'
+
+== Whitespace
+
+Use method REXML::Element#ignore_whitespace_nodes to determine whether
+whitespace nodes were ignored when the XML was parsed;
+returns +true+ if so, +nil+ otherwise.
+
+Use method REXML::Element#whitespace to determine whether whitespace
+is respected for the element; returns +true+ if so, +false+ otherwise.
+
+== Namespaces
+
+Use method REXML::Element#namespace to retrieve the string namespace URI
+for the element, which may derive from one of its ancestors:
+
+ xml_string = <<-EOT
+
+
+
+
+
+
+ EOT
+ d = Document.new(xml_string)
+ b = d.elements['//b']
+ b.namespace # => "1"
+ b.namespace('y') # => "2"
+ b.namespace('nosuch') # => nil
+
+Use method REXML::Element#namespaces to retrieve a hash of all defined namespaces
+in the element and its ancestors:
+
+ xml_string = <<-EOT
+
+
+
+
+
+
+ EOT
+ d = Document.new(xml_string)
+ d.elements['//a'].namespaces # => {"x"=>"1", "y"=>"2"}
+ d.elements['//b'].namespaces # => {"x"=>"1", "y"=>"2"}
+ d.elements['//c'].namespaces # => {"x"=>"1", "y"=>"2", "z"=>"3"}
+
+Use method REXML::Element#prefixes to retrieve an array of the string prefixes (names)
+of all defined namespaces in the element and its ancestors:
+
+ xml_string = <<-EOT
+
+
+
+
+
+
+ EOT
+ d = Document.new(xml_string, {compress_whitespace: :all})
+ d.elements['//a'].prefixes # => ["x", "y"]
+ d.elements['//b'].prefixes # => ["x", "y"]
+ d.elements['//c'].prefixes # => ["x", "y", "z"]
+
+== Traversing
+
+You can use certain methods to traverse children of the element.
+Each child that meets given criteria is yielded to the given block.
+
+[Traverse All Children]
+
+ Use inherited method REXML::Parent#each (or its alias #each_child) to traverse
+ all children of the element:
+
+ doc.root.each {|child| p [child.class, child] }
+
+ Output:
+
+ [REXML::Text, "\n\n"]
+ [REXML::Element, ... >]
+ [REXML::Text, "\n\n"]
+ [REXML::Element, ... >]
+ [REXML::Text, "\n\n"]
+ [REXML::Element, ... >]
+ [REXML::Text, "\n\n"]
+ [REXML::Element, ... >]
+ [REXML::Text, "\n\n"]
+
+[Traverse Element Children]
+
+ Use method REXML::Element#each_element to traverse only the element children
+ of the element:
+
+ doc.root.each_element {|e| p [e.class, e] }
+
+ Output:
+
+ [REXML::Element, ... >]
+ [REXML::Element, ... >]
+ [REXML::Element, ... >]
+ [REXML::Element, ... >]
+
+[Traverse Element Children with Attribute]
+
+ Use method REXML::Element#each_element_with_attribute with the single argument
+ +attr_name+ to traverse each element child that has the given attribute:
+
+ my_doc = Document.new ''
+ my_doc.root.each_element_with_attribute('id') {|e| p [e.class, e] }
+
+ Output:
+
+ [REXML::Element, ]
+ [REXML::Element, ]
+ [REXML::Element, ]
+
+ Use the same method with a second argument +value+ to traverse
+ each element child element that has the given attribute and value:
+
+ my_doc.root.each_element_with_attribute('id', '1') {|e| p [e.class, e] }
+
+ Output:
+
+ [REXML::Element, ]
+ [REXML::Element, ]
+
+ Use the same method with a third argument +max+ to traverse
+ no more than the given number of element children:
+
+ my_doc.root.each_element_with_attribute('id', '1', 1) {|e| p [e.class, e] }
+
+ Output:
+
+ [REXML::Element, ]
+
+ Use the same method with a fourth argument +xpath+ to traverse
+ only those element children that match the given xpath:
+
+ my_doc.root.each_element_with_attribute('id', '1', 2, '//d') {|e| p [e.class, e] }
+
+ Output:
+
+ [REXML::Element, ]
+
+[Traverse Element Children with Text]
+
+ Use method REXML::Element#each_element_with_text with no arguments
+ to traverse those element children that have text:
+
+ my_doc = Document.new 'bbd'
+ my_doc.root.each_element_with_text {|e| p [e.class, e] }
+
+ Output:
+
+ [REXML::Element, ... >]
+ [REXML::Element, ... >]
+ [REXML::Element, ... >]
+
+ Use the same method with the single argument +text+ to traverse
+ those element children that have exactly that text:
+
+ my_doc.root.each_element_with_text('b') {|e| p [e.class, e] }
+
+ Output:
+
+ [REXML::Element, ... >]
+ [REXML::Element, ... >]
+
+ Use the same method with additional second argument +max+ to traverse
+ no more than the given number of element children:
+
+ my_doc.root.each_element_with_text('b', 1) {|e| p [e.class, e] }
+
+ Output:
+
+ [REXML::Element, ... >]
+
+ Use the same method with additional third argument +xpath+ to traverse
+ only those element children that also match the given xpath:
+
+ my_doc.root.each_element_with_text('b', 2, '//c') {|e| p [e.class, e] }
+
+ Output:
+
+ [REXML::Element, ... >]
+
+[Traverse Element Children's Indexes]
+
+ Use inherited method REXML::Parent#each_index to traverse all children's indexes
+ (not just those of element children):
+
+ doc.root.each_index {|i| print i }
+
+ Output:
+
+ 012345678
+
+[Traverse Children Recursively]
+
+ Use included method REXML::Node#each_recursive to traverse all children recursively:
+
+ doc.root.each_recursive {|child| p [child.class, child] }
+
+ Output:
+
+ [REXML::Element, ... >]
+ [REXML::Element, ... >]
+ [REXML::Element, ... >]
+ [REXML::Element, ... >]
+ [REXML::Element, ... >]
+ [REXML::Element, ... >]
+ [REXML::Element, ... >]
+ [REXML::Element, ... >]
+ [REXML::Element, ... >]
+ [REXML::Element, ... >]
+ [REXML::Element, ... >]
+ [REXML::Element, ... >]
+ [REXML::Element, ... >]
+ [REXML::Element, ... >]
+ [REXML::Element, ... >]
+ [REXML::Element, ... >]
+ [REXML::Element, ... >]
+ [REXML::Element, ... >]
+ [REXML::Element, ... >]
+ [REXML::Element, ... >]
+ [REXML::Element, ... >]
+ [REXML::Element, ... >]
+ [REXML::Element, ... >]
+ [REXML::Element, ... >]
+
+== Searching
+
+You can use certain methods to search among the descendants of an element.
+
+Use method REXML::Element#get_elements to retrieve all element children of the element
+that match the given +xpath+:
+
+ xml_string = <<-EOT
+
+
+
+
+
+ EOT
+ d = Document.new(xml_string)
+ d.root.get_elements('//a') # => [ ... >, ]
+
+Use method REXML::Element#get_text with no argument to retrieve the first text node
+in the first child:
+
+ my_doc = Document.new "
some text this is bold! more text
"
+ text_node = my_doc.root.get_text
+ text_node.class # => REXML::Text
+ text_node.to_s # => "some text "
+
+Use the same method with argument +xpath+ to retrieve the first text node
+in the first child that matches the xpath:
+
+ my_doc.root.get_text(1) # => "this is bold!"
+
+Use method REXML::Element#text with no argument to retrieve the text
+from the first text node in the first child:
+
+ my_doc = Document.new "
some text this is bold! more text
"
+ text_node = my_doc.root.text
+ text_node.class # => String
+ text_node # => "some text "
+
+Use the same method with argument +xpath+ to retrieve the text from the first text node
+in the first child that matches the xpath:
+
+ my_doc.root.text(1) # => "this is bold!"
+
+Use included method REXML::Node#find_first_recursive
+to retrieve the first descendant element
+for which the given block returns a truthy value, or +nil+ if none:
+
+ doc.root.find_first_recursive do |ele|
+ ele.name == 'price'
+ end # => ... >
+ doc.root.find_first_recursive do |ele|
+ ele.name == 'nosuch'
+ end # => nil
+
+== Editing
+
+=== Editing a Document
+
+[Creating a Document]
+
+ Create a new document with method REXML::Document::new:
+
+ doc = Document.new(source_string)
+ empty_doc = REXML::Document.new
+
+[Adding to the Document]
+
+ Add an XML declaration with method REXML::Document#add
+ and an argument of type REXML::XMLDecl:
+
+ my_doc = Document.new
+ my_doc.xml_decl.to_s # => ""
+ my_doc.add(XMLDecl.new('2.0'))
+ my_doc.xml_decl.to_s # => ""
+
+ Add a document type with method REXML::Document#add
+ and an argument of type REXML::DocType:
+
+ my_doc = Document.new
+ my_doc.doctype.to_s # => ""
+ my_doc.add(DocType.new('foo'))
+ my_doc.doctype.to_s # => ""
+
+ Add a node of any other REXML type with method REXML::Document#add and an argument
+ that is not of type REXML::XMLDecl or REXML::DocType:
+
+ my_doc = Document.new
+ my_doc.add(Element.new('foo'))
+ my_doc.to_s # => ""
+
+ Add an existing element as the root element with method REXML::Document#add_element:
+
+ ele = Element.new('foo')
+ my_doc = Document.new
+ my_doc.add_element(ele)
+ my_doc.root # =>
+
+ Create and add an element as the root element with method REXML::Document#add_element:
+
+ my_doc = Document.new
+ my_doc.add_element('foo')
+ my_doc.root # =>
+
+=== Editing an Element
+
+==== Creating an Element
+
+Create a new element with method REXML::Element::new:
+
+ ele = Element.new('foo') # =>
+
+==== Setting Element Properties
+
+Set the context for an element with method REXML::Element#context=
+(see {Element Context}[../context_rdoc.html]):
+
+ ele.context # => nil
+ ele.context = {ignore_whitespace_nodes: :all}
+ ele.context # => {:ignore_whitespace_nodes=>:all}
+
+Set the parent for an element with inherited method REXML::Child#parent=
+
+ ele.parent # => nil
+ ele.parent = Element.new('bar')
+ ele.parent # =>
+
+Set the text for an element with method REXML::Element#text=:
+
+ ele.text # => nil
+ ele.text = 'bar'
+ ele.text # => "bar"
+
+==== Adding to an Element
+
+Add a node as the last child with inherited method REXML::Parent#add (or its alias #push):
+
+ ele = Element.new('foo') # =>
+ ele.push(Text.new('bar'))
+ ele.push(Element.new('baz'))
+ ele.children # => ["bar", ]
+
+Add a node as the first child with inherited method REXML::Parent#unshift:
+
+ ele = Element.new('foo') # =>
+ ele.unshift(Element.new('bar'))
+ ele.unshift(Text.new('baz'))
+ ele.children # => ["bar", ]
+
+Add an element as the last child with method REXML::Element#add_element:
+
+ ele = Element.new('foo') # =>
+ ele.add_element('bar')
+ ele.add_element(Element.new('baz'))
+ ele.children # => [, ]
+
+Add a text node as the last child with method REXML::Element#add_text:
+
+ ele = Element.new('foo') # =>
+ ele.add_text('bar')
+ ele.add_text(Text.new('baz'))
+ ele.children # => ["bar", "baz"]
+
+Insert a node before a given node with method REXML::Parent#insert_before:
+
+ ele = Element.new('foo') # =>
+ ele.add_text('bar')
+ ele.add_text(Text.new('baz'))
+ ele.children # => ["bar", "baz"]
+ target = ele[1] # => "baz"
+ ele.insert_before(target, Text.new('bat'))
+ ele.children # => ["bar", "bat", "baz"]
+
+Insert a node after a given node with method REXML::Parent#insert_after:
+
+ ele = Element.new('foo') # =>
+ ele.add_text('bar')
+ ele.add_text(Text.new('baz'))
+ ele.children # => ["bar", "baz"]
+ target = ele[0] # => "bar"
+ ele.insert_after(target, Text.new('bat'))
+ ele.children # => ["bar", "bat", "baz"]
+
+Add an attribute with method REXML::Element#add_attribute:
+
+ ele = Element.new('foo') # =>
+ ele.add_attribute('bar', 'baz')
+ ele.add_attribute(Attribute.new('bat', 'bam'))
+ ele.attributes # => {"bar"=>bar='baz', "bat"=>bat='bam'}
+
+Add multiple attributes with method REXML::Element#add_attributes:
+
+ ele = Element.new('foo') # =>
+ ele.add_attributes({'bar' => 'baz', 'bat' => 'bam'})
+ ele.add_attributes([['ban', 'bap'], ['bah', 'bad']])
+ ele.attributes # => {"bar"=>bar='baz', "bat"=>bat='bam', "ban"=>ban='bap', "bah"=>bah='bad'}
+
+Add a namespace with method REXML::Element#add_namespace:
+
+ ele = Element.new('foo') # =>
+ ele.add_namespace('bar')
+ ele.add_namespace('baz', 'bat')
+ ele.namespaces # => {"xmlns"=>"bar", "baz"=>"bat"}
+
+==== Deleting from an Element
+
+Delete a specific child object with inherited method REXML::Parent#delete:
+
+ ele = Element.new('foo') # =>
+ ele.add_element('bar')
+ ele.add_text('baz')
+ ele.children # => [, "baz"]
+ target = ele[1] # => "baz"
+ ele.delete(target) # => "baz"
+ ele.children # => []
+ target = ele[0] # =>
+ ele.delete(target) # =>
+ ele.children # => []
+
+Delete a child at a specific index with inherited method REXML::Parent#delete_at:
+
+ ele = Element.new('foo') # =>
+ ele.add_element('bar')
+ ele.add_text('baz')
+ ele.children # => [, "baz"]
+ ele.delete_at(1)
+ ele.children # => []
+ ele.delete_at(0)
+ ele.children # => []
+
+Delete all children meeting a specified criterion with inherited method
+REXML::Parent#delete_if:
+
+ ele = Element.new('foo') # =>
+ ele.add_element('bar')
+ ele.add_text('baz')
+ ele.add_element('bat')
+ ele.add_text('bam')
+ ele.children # => [, "baz", , "bam"]
+ ele.delete_if {|child| child.instance_of?(Text) }
+ ele.children # => [, ]
+
+Delete an element at a specific 1-based index with method REXML::Element#delete_element:
+
+ ele = Element.new('foo') # =>
+ ele.add_element('bar')
+ ele.add_text('baz')
+ ele.add_element('bat')
+ ele.add_text('bam')
+ ele.children # => [, "baz", , "bam"]
+ ele.delete_element(2) # =>
+ ele.children # => [, "baz", "bam"]
+ ele.delete_element(1) # =>
+ ele.children # => ["baz", "bam"]
+
+Delete a specific element with the same method:
+
+ ele = Element.new('foo') # =>
+ ele.add_element('bar')
+ ele.add_text('baz')
+ ele.add_element('bat')
+ ele.add_text('bam')
+ ele.children # => [, "baz", , "bam"]
+ target = ele.elements[2] # =>
+ ele.delete_element(target) # =>
+ ele.children # => [, "baz", "bam"]
+
+Delete an element matching an xpath using the same method:
+
+ ele = Element.new('foo') # =>
+ ele.add_element('bar')
+ ele.add_text('baz')
+ ele.add_element('bat')
+ ele.add_text('bam')
+ ele.children # => [, "baz", , "bam"]
+ ele.delete_element('./bat') # =>
+ ele.children # => [, "baz", "bam"]
+ ele.delete_element('./bar') # =>
+ ele.children # => ["baz", "bam"]
+
+Delete an attribute by name with method REXML::Element#delete_attribute:
+
+ ele = Element.new('foo') # =>
+ ele.add_attributes({'bar' => 'baz', 'bam' => 'bat'})
+ ele.attributes # => {"bar"=>bar='baz', "bam"=>bam='bat'}
+ ele.delete_attribute('bam')
+ ele.attributes # => {"bar"=>bar='baz'}
+
+Delete a namespace with method REXML::Element#delete_namespace:
+
+ ele = Element.new('foo') # =>
+ ele.add_namespace('bar')
+ ele.add_namespace('baz', 'bat')
+ ele.namespaces # => {"xmlns"=>"bar", "baz"=>"bat"}
+ ele.delete_namespace('xmlns')
+ ele.namespaces # => {} # => {"baz"=>"bat"}
+ ele.delete_namespace('baz')
+ ele.namespaces # => {} # => {}
+
+Remove an element from its parent with inherited method REXML::Child#remove:
+
+ ele = Element.new('foo') # =>
+ parent = Element.new('bar') # =>
+ parent.add_element(ele) # =>
+ parent.children.size # => 1
+ ele.remove # =>
+ parent.children.size # => 0
+
+==== Replacing Nodes
+
+Replace the node at a given 0-based index with inherited method REXML::Parent#[]=:
+
+ ele = Element.new('foo') # =>
+ ele.add_element('bar')
+ ele.add_text('baz')
+ ele.add_element('bat')
+ ele.add_text('bam')
+ ele.children # => [, "baz", , "bam"]
+ ele[2] = Text.new('bad') # => "bad"
+ ele.children # => [, "baz", "bad", "bam"]
+
+Replace a given node with another node with inherited method REXML::Parent#replace_child:
+
+ ele = Element.new('foo') # =>
+ ele.add_element('bar')
+ ele.add_text('baz')
+ ele.add_element('bat')
+ ele.add_text('bam')
+ ele.children # => [, "baz", , "bam"]
+ target = ele[2] # =>
+ ele.replace_child(target, Text.new('bah'))
+ ele.children # => [, "baz", "bah", "bam"]
+
+Replace +self+ with a given node with inherited method REXML::Child#replace_with:
+
+ ele = Element.new('foo') # =>
+ ele.add_element('bar')
+ ele.add_text('baz')
+ ele.add_element('bat')
+ ele.add_text('bam')
+ ele.children # => [, "baz", , "bam"]
+ target = ele[2] # =>
+ target.replace_with(Text.new('bah'))
+ ele.children # => [, "baz", "bah", "bam"]
+
+=== Cloning
+
+Create a shallow clone of an element with method REXML::Element#clone.
+The clone contains the name and attributes, but not the parent or children:
+
+ ele = Element.new('foo')
+ ele.add_attributes({'bar' => 0, 'baz' => 1})
+ ele.clone # =>
+
+Create a shallow clone of a document with method REXML::Document#clone.
+The XML declaration is copied; the document type and root element are not cloned:
+
+ my_xml = ''
+ my_doc = Document.new(my_xml)
+ clone_doc = my_doc.clone
+
+ my_doc.xml_decl # =>
+ clone_doc.xml_decl # =>
+
+ my_doc.doctype.to_s # => ""
+ clone_doc.doctype.to_s # => ""
+
+ my_doc.root # =>
+ clone_doc.root # => nil
+
+Create a deep clone of an element with inherited method REXML::Parent#deep_clone.
+All nodes and attributes are copied:
+
+ doc.to_s.size # => 825
+ clone = doc.deep_clone
+ clone.to_s.size # => 825
+
+== Writing the Document
+
+Write a document to an \IO stream (defaults to $stdout)
+with method REXML::Document#write:
+
+ doc.write
+
+Output:
+
+
+
+
+
+ Everyday Italian
+ Giada De Laurentiis
+ 2005
+ 30.00
+
+
+
+ Harry Potter
+ J K. Rowling
+ 2005
+ 29.99
+
+
+
+ XQuery Kick Start
+ James McGovern
+ Per Bothner
+ Kurt Cagle
+ James Linn
+ Vaidyanathan Nagarajan
+ 2003
+ 49.99
+
+
+
+ Learning XML
+ Erik T. Ray
+ 2003
+ 39.95
+
+
+
diff --git a/lib/rexml.rb b/lib/rexml.rb
new file mode 100644
index 00000000..eee246e4
--- /dev/null
+++ b/lib/rexml.rb
@@ -0,0 +1,3 @@
+# frozen_string_literal: true
+
+require_relative "rexml/document"
diff --git a/lib/rexml/attribute.rb b/lib/rexml/attribute.rb
index 8933a013..11893a95 100644
--- a/lib/rexml/attribute.rb
+++ b/lib/rexml/attribute.rb
@@ -1,4 +1,4 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
require_relative "namespace"
require_relative 'text'
@@ -13,9 +13,6 @@ class Attribute
# The element to which this attribute belongs
attr_reader :element
- # The normalized value of this attribute. That is, the attribute with
- # entities intact.
- attr_writer :normalized
PATTERN = /\s*(#{NAME_STR})\s*=\s*(["'])(.*?)\2/um
NEEDS_A_SECOND_CHECK = /(<|&((#{Entity::NAME});|(#0*((?:\d+)|(?:x[a-fA-F0-9]+)));)?)/um
@@ -122,10 +119,13 @@ def hash
# b = Attribute.new( "ns:x", "y" )
# b.to_string # -> "ns:x='y'"
def to_string
+ value = to_s
if @element and @element.context and @element.context[:attribute_quote] == :quote
- %Q^#@expanded_name="#{to_s().gsub(/"/, '"')}"^
+ value = value.gsub('"', '"') if value.include?('"')
+ %Q^#@expanded_name="#{value}"^
else
- "#@expanded_name='#{to_s().gsub(/'/, ''')}'"
+ value = value.gsub("'", ''') if value.include?("'")
+ "#@expanded_name='#{value}'"
end
end
@@ -141,7 +141,6 @@ def to_s
return @normalized if @normalized
@normalized = Text::normalize( @unnormalized, doctype )
- @unnormalized = nil
@normalized
end
@@ -150,10 +149,16 @@ def to_s
def value
return @unnormalized if @unnormalized
@unnormalized = Text::unnormalize( @normalized, doctype )
- @normalized = nil
@unnormalized
end
+ # The normalized value of this attribute. That is, the attribute with
+ # entities intact.
+ def normalized=(new_normalized)
+ @normalized = new_normalized
+ @unnormalized = nil
+ end
+
# Returns a copy of this attribute
def clone
Attribute.new self
@@ -190,7 +195,7 @@ def node_type
end
def inspect
- rv = ""
+ rv = +""
write( rv )
rv
end
diff --git a/lib/rexml/doctype.rb b/lib/rexml/doctype.rb
index 757b6396..f3590484 100644
--- a/lib/rexml/doctype.rb
+++ b/lib/rexml/doctype.rb
@@ -7,6 +7,44 @@
require_relative 'xmltokens'
module REXML
+ class ReferenceWriter
+ def initialize(id_type,
+ public_id_literal,
+ system_literal,
+ context=nil)
+ @id_type = id_type
+ @public_id_literal = public_id_literal
+ @system_literal = system_literal
+ if context and context[:prologue_quote] == :apostrophe
+ @default_quote = "'"
+ else
+ @default_quote = "\""
+ end
+ end
+
+ def write(output)
+ output << " #{@id_type}"
+ if @public_id_literal
+ if @public_id_literal.include?("'")
+ quote = "\""
+ else
+ quote = @default_quote
+ end
+ output << " #{quote}#{@public_id_literal}#{quote}"
+ end
+ if @system_literal
+ if @system_literal.include?("'")
+ quote = "\""
+ elsif @system_literal.include?("\"")
+ quote = "'"
+ else
+ quote = @default_quote
+ end
+ output << " #{quote}#{@system_literal}#{quote}"
+ end
+ end
+ end
+
# Represents an XML DOCTYPE declaration; that is, the contents of . DOCTYPES can be used to declare the DTD of a document, as well as
# being used to declare entities used in the document.
@@ -50,6 +88,8 @@ def initialize( first, parent=nil )
super( parent )
@name = first.name
@external_id = first.external_id
+ @long_name = first.instance_variable_get(:@long_name)
+ @uri = first.instance_variable_get(:@uri)
elsif first.kind_of? Array
super( parent )
@name = first[0]
@@ -108,19 +148,17 @@ def clone
# Ignored
def write( output, indent=0, transitive=false, ie_hack=false )
f = REXML::Formatters::Default.new
- c = context
- if c and c[:prologue_quote] == :apostrophe
- quote = "'"
- else
- quote = "\""
- end
indent( output, indent )
output << START
output << ' '
output << @name
- output << " #{@external_id}" if @external_id
- output << " #{quote}#{@long_name}#{quote}" if @long_name
- output << " #{quote}#{@uri}#{quote}" if @uri
+ if @external_id
+ reference_writer = ReferenceWriter.new(@external_id,
+ @long_name,
+ @uri,
+ context)
+ reference_writer.write(output)
+ end
unless @children.empty?
output << ' ['
@children.each { |child|
@@ -159,7 +197,7 @@ def public
when "SYSTEM"
nil
when "PUBLIC"
- strip_quotes(@long_name)
+ @long_name
end
end
@@ -169,9 +207,9 @@ def public
def system
case @external_id
when "SYSTEM"
- strip_quotes(@long_name)
+ @long_name
when "PUBLIC"
- @uri.kind_of?(String) ? strip_quotes(@uri) : nil
+ @uri.kind_of?(String) ? @uri : nil
end
end
@@ -193,15 +231,6 @@ def notation(name)
notation_decl.name == name
}
end
-
- private
-
- # Method contributed by Henrik Martensson
- def strip_quotes(quoted_string)
- quoted_string =~ /^[\'\"].*[\'\"]$/ ?
- quoted_string[1, quoted_string.length-2] :
- quoted_string
- end
end
# We don't really handle any of these since we're not a validating
@@ -259,16 +288,11 @@ def initialize name, middle, pub, sys
end
def to_s
- c = nil
- c = parent.context if parent
- if c and c[:prologue_quote] == :apostrophe
- quote = "'"
- else
- quote = "\""
- end
- notation = ""
notation
end
diff --git a/lib/rexml/document.rb b/lib/rexml/document.rb
index adec2930..b1caa020 100644
--- a/lib/rexml/document.rb
+++ b/lib/rexml/document.rb
@@ -14,25 +14,81 @@
require_relative "parsers/treeparser"
module REXML
- # Represents a full XML document, including PIs, a doctype, etc. A
- # Document has a single child that can be accessed by root().
- # Note that if you want to have an XML declaration written for a document
- # you create, you must add one; REXML documents do not write a default
- # declaration for you. See |DECLARATION| and |write|.
+ # Represents an XML document.
+ #
+ # A document may have:
+ #
+ # - A single child that may be accessed via method #root.
+ # - An XML declaration.
+ # - A document type.
+ # - Processing instructions.
+ #
+ # == In a Hurry?
+ #
+ # If you're somewhat familiar with XML
+ # and have a particular task in mind,
+ # you may want to see the
+ # {tasks pages}[../doc/rexml/tasks/tocs/master_toc_rdoc.html],
+ # and in particular, the
+ # {tasks page for documents}[../doc/rexml/tasks/tocs/document_toc_rdoc.html].
+ #
class Document < Element
- # A convenient default XML declaration. If you want an XML declaration,
- # the easiest way to add one is mydoc << Document::DECLARATION
- # +DEPRECATED+
- # Use: mydoc << XMLDecl.default
+ # A convenient default XML declaration. Use:
+ #
+ # mydoc << XMLDecl.default
+ #
DECLARATION = XMLDecl.default
- # Constructor
- # @param source if supplied, must be a Document, String, or IO.
- # Documents have their context and Element attributes cloned.
- # Strings are expected to be valid XML documents. IOs are expected
- # to be sources of valid XML documents.
- # @param context if supplied, contains the context of the document;
- # this should be a Hash.
+ # :call-seq:
+ # new(string = nil, context = {}) -> new_document
+ # new(io_stream = nil, context = {}) -> new_document
+ # new(document = nil, context = {}) -> new_document
+ #
+ # Returns a new \REXML::Document object.
+ #
+ # When no arguments are given,
+ # returns an empty document:
+ #
+ # d = REXML::Document.new
+ # d.to_s # => ""
+ #
+ # When argument +string+ is given, it must be a string
+ # containing a valid XML document:
+ #
+ # xml_string = 'FooBar'
+ # d = REXML::Document.new(xml_string)
+ # d.to_s # => "FooBar"
+ #
+ # When argument +io_stream+ is given, it must be an \IO object
+ # that is opened for reading, and when read must return a valid XML document:
+ #
+ # File.write('t.xml', xml_string)
+ # d = File.open('t.xml', 'r') do |io|
+ # REXML::Document.new(io)
+ # end
+ # d.to_s # => "FooBar"
+ #
+ # When argument +document+ is given, it must be an existing
+ # document object, whose context and attributes (but not children)
+ # are cloned into the new document:
+ #
+ # d = REXML::Document.new(xml_string)
+ # d.children # => [ ... >]
+ # d.context = {raw: :all, compress_whitespace: :all}
+ # d.add_attributes({'bar' => 0, 'baz' => 1})
+ # d1 = REXML::Document.new(d)
+ # d1.children # => []
+ # d1.context # => {:raw=>:all, :compress_whitespace=>:all}
+ # d1.attributes # => {"bar"=>bar='0', "baz"=>baz='1'}
+ #
+ # When argument +context+ is given, it must be a hash
+ # containing context entries for the document;
+ # see {Element Context}[../doc/rexml/context_rdoc.html]:
+ #
+ # context = {raw: :all, compress_whitespace: :all}
+ # d = REXML::Document.new(xml_string, context)
+ # d.context # => {:raw=>:all, :compress_whitespace=>:all}
+ #
def initialize( source = nil, context = {} )
@entity_expansion_count = 0
super()
@@ -46,26 +102,71 @@ def initialize( source = nil, context = {} )
end
end
+ # :call-seq:
+ # node_type -> :document
+ #
+ # Returns the symbol +:document+.
+ #
def node_type
:document
end
- # Should be obvious
+ # :call-seq:
+ # clone -> new_document
+ #
+ # Returns the new document resulting from executing
+ # Document.new(self). See Document.new.
+ #
def clone
Document.new self
end
- # According to the XML spec, a root node has no expanded name
+ # :call-seq:
+ # expanded_name -> empty_string
+ #
+ # Returns an empty string.
+ #
def expanded_name
''
#d = doc_type
#d ? d.name : "UNDEFINED"
end
-
alias :name :expanded_name
- # We override this, because XMLDecls and DocTypes must go at the start
- # of the document
+ # :call-seq:
+ # add(xml_decl) -> self
+ # add(doc_type) -> self
+ # add(object) -> self
+ #
+ # Adds an object to the document; returns +self+.
+ #
+ # When argument +xml_decl+ is given,
+ # it must be an REXML::XMLDecl object,
+ # which becomes the XML declaration for the document,
+ # replacing the previous XML declaration if any:
+ #
+ # d = REXML::Document.new
+ # d.xml_decl.to_s # => ""
+ # d.add(REXML::XMLDecl.new('2.0'))
+ # d.xml_decl.to_s # => ""
+ #
+ # When argument +doc_type+ is given,
+ # it must be an REXML::DocType object,
+ # which becomes the document type for the document,
+ # replacing the previous document type, if any:
+ #
+ # d = REXML::Document.new
+ # d.doctype.to_s # => ""
+ # d.add(REXML::DocType.new('foo'))
+ # d.doctype.to_s # => ""
+ #
+ # When argument +object+ (not an REXML::XMLDecl or REXML::DocType object)
+ # is given it is added as the last child:
+ #
+ # d = REXML::Document.new
+ # d.add(REXML::Element.new('foo'))
+ # d.to_s # => ""
+ #
def add( child )
if child.kind_of? XMLDecl
if @children[0].kind_of? XMLDecl
@@ -99,49 +200,108 @@ def add( child )
end
alias :<< :add
+ # :call-seq:
+ # add_element(name_or_element = nil, attributes = nil) -> new_element
+ #
+ # Adds an element to the document by calling REXML::Element.add_element:
+ #
+ # REXML::Element.add_element(name_or_element, attributes)
def add_element(arg=nil, arg2=nil)
rv = super
raise "attempted adding second root element to document" if @elements.size > 1
rv
end
- # @return the root Element of the document, or nil if this document
- # has no children.
+ # :call-seq:
+ # root -> root_element or nil
+ #
+ # Returns the root element of the document, if it exists, otherwise +nil+:
+ #
+ # d = REXML::Document.new('')
+ # d.root # =>
+ # d = REXML::Document.new('')
+ # d.root # => nil
+ #
def root
elements[1]
#self
#@children.find { |item| item.kind_of? Element }
end
- # @return the DocType child of the document, if one exists,
- # and nil otherwise.
+ # :call-seq:
+ # doctype -> doc_type or nil
+ #
+ # Returns the DocType object for the document, if it exists, otherwise +nil+:
+ #
+ # d = REXML::Document.new('')
+ # d.doctype.class # => REXML::DocType
+ # d = REXML::Document.new('')
+ # d.doctype.class # => nil
+ #
def doctype
@children.find { |item| item.kind_of? DocType }
end
- # @return the XMLDecl of this document; if no XMLDecl has been
- # set, the default declaration is returned.
+ # :call-seq:
+ # xml_decl -> xml_decl
+ #
+ # Returns the XMLDecl object for the document, if it exists,
+ # otherwise the default XMLDecl object:
+ #
+ # d = REXML::Document.new('')
+ # d.xml_decl.class # => REXML::XMLDecl
+ # d.xml_decl.to_s # => ""
+ # d = REXML::Document.new('')
+ # d.xml_decl.class # => REXML::XMLDecl
+ # d.xml_decl.to_s # => ""
+ #
def xml_decl
rv = @children[0]
return rv if rv.kind_of? XMLDecl
@children.unshift(XMLDecl.default)[0]
end
- # @return the XMLDecl version of this document as a String.
- # If no XMLDecl has been set, returns the default version.
+ # :call-seq:
+ # version -> version_string
+ #
+ # Returns the XMLDecl version of this document as a string,
+ # if it has been set, otherwise the default version:
+ #
+ # d = REXML::Document.new('')
+ # d.version # => "2.0"
+ # d = REXML::Document.new('')
+ # d.version # => "1.0"
+ #
def version
xml_decl().version
end
- # @return the XMLDecl encoding of this document as an
- # Encoding object.
- # If no XMLDecl has been set, returns the default encoding.
+ # :call-seq:
+ # encoding -> encoding_string
+ #
+ # Returns the XMLDecl encoding of the document,
+ # if it has been set, otherwise the default encoding:
+ #
+ # d = REXML::Document.new('')
+ # d.encoding # => "UTF-16"
+ # d = REXML::Document.new('')
+ # d.encoding # => "UTF-8"
+ #
def encoding
xml_decl().encoding
end
- # @return the XMLDecl standalone value of this document as a String.
- # If no XMLDecl has been set, returns the default setting.
+ # :call-seq:
+ # stand_alone?
+ #
+ # Returns the XMLDecl standalone value of the document as a string,
+ # if it has been set, otherwise the default standalone value:
+ #
+ # d = REXML::Document.new('')
+ # d.stand_alone? # => "yes"
+ # d = REXML::Document.new('')
+ # d.stand_alone? # => nil
+ #
def stand_alone?
xml_decl().stand_alone?
end
diff --git a/lib/rexml/element.rb b/lib/rexml/element.rb
index c706a7c2..bf913a82 100644
--- a/lib/rexml/element.rb
+++ b/lib/rexml/element.rb
@@ -15,9 +15,267 @@ module REXML
# context node and convert it back when we write it.
@@namespaces = {}
- # Represents a tagged XML element. Elements are characterized by
- # having children, attributes, and names, and can themselves be
- # children.
+ # An \REXML::Element object represents an XML element.
+ #
+ # An element:
+ #
+ # - Has a name (string).
+ # - May have a parent (another element).
+ # - Has zero or more children
+ # (other elements, text, CDATA, processing instructions, and comments).
+ # - Has zero or more siblings
+ # (other elements, text, CDATA, processing instructions, and comments).
+ # - Has zero or more named attributes.
+ #
+ # == In a Hurry?
+ #
+ # If you're somewhat familiar with XML
+ # and have a particular task in mind,
+ # you may want to see the
+ # {tasks pages}[../doc/rexml/tasks/tocs/master_toc_rdoc.html],
+ # and in particular, the
+ # {tasks page for elements}[../doc/rexml/tasks/tocs/element_toc_rdoc.html].
+ #
+ # === Name
+ #
+ # An element has a name, which is initially set when the element is created:
+ #
+ # e = REXML::Element.new('foo')
+ # e.name # => "foo"
+ #
+ # The name may be changed:
+ #
+ # e.name = 'bar'
+ # e.name # => "bar"
+ #
+ #
+ # === \Parent
+ #
+ # An element may have a parent.
+ #
+ # Its parent may be assigned explicitly when the element is created:
+ #
+ # e0 = REXML::Element.new('foo')
+ # e1 = REXML::Element.new('bar', e0)
+ # e1.parent # => ... >
+ #
+ # Note: the representation of an element always shows the element's name.
+ # If the element has children, the representation indicates that
+ # by including an ellipsis (...).
+ #
+ # The parent may be assigned explicitly at any time:
+ #
+ # e2 = REXML::Element.new('baz')
+ # e1.parent = e2
+ # e1.parent # =>
+ #
+ # When an element is added as a child, its parent is set automatically:
+ #
+ # e1.add_element(e0)
+ # e0.parent # => ... >
+ #
+ # For an element that has no parent, method +parent+ returns +nil+.
+ #
+ # === Children
+ #
+ # An element has zero or more children.
+ # The children are an ordered collection
+ # of all objects whose parent is the element itself.
+ #
+ # The children may include any combination of elements, text, comments,
+ # processing instructions, and CDATA.
+ # (This example keeps things clean by controlling whitespace
+ # via a +context+ setting.)
+ #
+ # xml_string = <<-EOT
+ #
+ #
+ # text 0
+ #
+ #
+ #
+ #
+ # text 1
+ #
+ #
+ #
+ #
+ # EOT
+ # context = {ignore_whitespace_nodes: :all, compress_whitespace: :all}
+ # d = REXML::Document.new(xml_string, context)
+ # root = d.root
+ # root.children.size # => 10
+ # root.each {|child| p "#{child.class}: #{child}" }
+ #
+ # Output:
+ #
+ # "REXML::Element: "
+ # "REXML::Text: \n text 0\n "
+ # "REXML::Comment: comment 0"
+ # "REXML::Instruction: "
+ # "REXML::CData: cdata 0"
+ # "REXML::Element: "
+ # "REXML::Text: \n text 1\n "
+ # "REXML::Comment: comment 1"
+ # "REXML::Instruction: "
+ # "REXML::CData: cdata 1"
+ #
+ # A child may be added using inherited methods
+ # Parent#insert_before or Parent#insert_after:
+ #
+ # xml_string = ''
+ # d = REXML::Document.new(xml_string)
+ # root = d.root
+ # c = d.root[1] # =>
+ # root.insert_before(c, REXML::Element.new('b'))
+ # root.to_a # => [, , , ]
+ #
+ # A child may be replaced using Parent#replace_child:
+ #
+ # root.replace_child(c, REXML::Element.new('x'))
+ # root.to_a # => [, , , ]
+ #
+ # A child may be removed using Parent#delete:
+ #
+ # x = root[2] # =>
+ # root.delete(x)
+ # root.to_a # => [, , ]
+ #
+ # === Siblings
+ #
+ # An element has zero or more siblings,
+ # which are the other children of the element's parent.
+ #
+ # In the example above, element +ele_1+ is between a CDATA sibling
+ # and a text sibling:
+ #
+ # ele_1 = root[5] # =>
+ # ele_1.previous_sibling # => "cdata 0"
+ # ele_1.next_sibling # => "\n text 1\n "
+ #
+ # === \Attributes
+ #
+ # An element has zero or more named attributes.
+ #
+ # A new element has no attributes:
+ #
+ # e = REXML::Element.new('foo')
+ # e.attributes # => {}
+ #
+ # Attributes may be added:
+ #
+ # e.add_attribute('bar', 'baz')
+ # e.add_attribute('bat', 'bam')
+ # e.attributes.size # => 2
+ # e['bar'] # => "baz"
+ # e['bat'] # => "bam"
+ #
+ # An existing attribute may be modified:
+ #
+ # e.add_attribute('bar', 'bad')
+ # e.attributes.size # => 2
+ # e['bar'] # => "bad"
+ #
+ # An existing attribute may be deleted:
+ #
+ # e.delete_attribute('bar')
+ # e.attributes.size # => 1
+ # e['bar'] # => nil
+ #
+ # == What's Here
+ #
+ # To begin with, what's elsewhere?
+ #
+ # \Class \REXML::Element inherits from its ancestor classes:
+ #
+ # - REXML::Child
+ # - REXML::Parent
+ #
+ # \REXML::Element itself and its ancestors also include modules:
+ #
+ # - {Enumerable}[https://docs.ruby-lang.org/en/master/Enumerable.html]
+ # - REXML::Namespace
+ # - REXML::Node
+ # - REXML::XMLTokens
+ #
+ # === Methods for Creating an \Element
+ #
+ # ::new:: Returns a new empty element.
+ # #clone:: Returns a clone of another element.
+ #
+ # === Methods for Attributes
+ #
+ # {[attribute_name]}[#method-i-5B-5D]:: Returns an attribute value.
+ # #add_attribute:: Adds a new attribute.
+ # #add_attributes:: Adds multiple new attributes.
+ # #attribute:: Returns the attribute value for a given name and optional namespace.
+ # #delete_attribute:: Removes an attribute.
+ #
+ # === Methods for Children
+ #
+ # {[index]}[#method-i-5B-5D]:: Returns the child at the given offset.
+ # #add_element:: Adds an element as the last child.
+ # #delete_element:: Deletes a child element.
+ # #each_element:: Calls the given block with each child element.
+ # #each_element_with_attribute:: Calls the given block with each child element
+ # that meets given criteria,
+ # which can include the attribute name.
+ # #each_element_with_text:: Calls the given block with each child element
+ # that meets given criteria,
+ # which can include text.
+ # #get_elements:: Returns an array of element children that match a given xpath.
+ #
+ # === Methods for \Text Children
+ #
+ # #add_text:: Adds a text node to the element.
+ # #get_text:: Returns a text node that meets specified criteria.
+ # #text:: Returns the text string from the first node that meets specified criteria.
+ # #texts:: Returns an array of the text children of the element.
+ # #text=:: Adds, removes, or replaces the first text child of the element
+ #
+ # === Methods for Other Children
+ #
+ # #cdatas:: Returns an array of the cdata children of the element.
+ # #comments:: Returns an array of the comment children of the element.
+ # #instructions:: Returns an array of the instruction children of the element.
+ #
+ # === Methods for Namespaces
+ #
+ # #add_namespace:: Adds a namespace to the element.
+ # #delete_namespace:: Removes a namespace from the element.
+ # #namespace:: Returns the string namespace URI for the element.
+ # #namespaces:: Returns a hash of all defined namespaces in the element.
+ # #prefixes:: Returns an array of the string prefixes (names)
+ # of all defined namespaces in the element
+ #
+ # === Methods for Querying
+ #
+ # #document:: Returns the document, if any, that the element belongs to.
+ # #root:: Returns the most distant element (not document) ancestor of the element.
+ # #root_node:: Returns the most distant ancestor of the element.
+ # #xpath:: Returns the string xpath to the element
+ # relative to the most distant parent
+ # #has_attributes?:: Returns whether the element has attributes.
+ # #has_elements?:: Returns whether the element has elements.
+ # #has_text?:: Returns whether the element has text.
+ # #next_element:: Returns the next sibling that is an element.
+ # #previous_element:: Returns the previous sibling that is an element.
+ # #raw:: Returns whether raw mode is set for the element.
+ # #whitespace:: Returns whether whitespace is respected for the element.
+ # #ignore_whitespace_nodes:: Returns whether whitespace nodes
+ # are to be ignored for the element.
+ # #node_type:: Returns symbol :element.
+ #
+ # === One More Method
+ #
+ # #inspect:: Returns a string representation of the element.
+ #
+ # === Accessors
+ #
+ # #elements:: Returns the REXML::Elements object for the element.
+ # #attributes:: Returns the REXML::Attributes object for the element.
+ # #context:: Returns or sets the context hash for the element.
+ #
class Element < Parent
include Namespace
@@ -30,32 +288,42 @@ class Element < Parent
# whitespace handling.
attr_accessor :context
- # Constructor
- # arg::
- # if not supplied, will be set to the default value.
- # If a String, the name of this object will be set to the argument.
- # If an Element, the object will be shallowly cloned; name,
- # attributes, and namespaces will be copied. Children will +not+ be
- # copied.
- # parent::
- # if supplied, must be a Parent, and will be used as
- # the parent of this object.
- # context::
- # If supplied, must be a hash containing context items. Context items
- # include:
- # * :respect_whitespace the value of this is :+all+ or an array of
- # strings being the names of the elements to respect
- # whitespace for. Defaults to :+all+.
- # * :compress_whitespace the value can be :+all+ or an array of
- # strings being the names of the elements to ignore whitespace on.
- # Overrides :+respect_whitespace+.
- # * :ignore_whitespace_nodes the value can be :+all+ or an array
- # of strings being the names of the elements in which to ignore
- # whitespace-only nodes. If this is set, Text nodes which contain only
- # whitespace will not be added to the document tree.
- # * :raw can be :+all+, or an array of strings being the names of
- # the elements to process in raw mode. In raw mode, special
- # characters in text is not converted to or from entities.
+ # :call-seq:
+ # Element.new(name = 'UNDEFINED', parent = nil, context = nil) -> new_element
+ # Element.new(element, parent = nil, context = nil) -> new_element
+ #
+ # Returns a new \REXML::Element object.
+ #
+ # When no arguments are given,
+ # returns an element with name 'UNDEFINED':
+ #
+ # e = REXML::Element.new # =>
+ # e.class # => REXML::Element
+ # e.name # => "UNDEFINED"
+ #
+ # When only argument +name+ is given,
+ # returns an element of the given name:
+ #
+ # REXML::Element.new('foo') # =>
+ #
+ # When only argument +element+ is given, it must be an \REXML::Element object;
+ # returns a shallow copy of the given element:
+ #
+ # e0 = REXML::Element.new('foo')
+ # e1 = REXML::Element.new(e0) # =>
+ #
+ # When argument +parent+ is also given, it must be an REXML::Parent object:
+ #
+ # e = REXML::Element.new('foo', REXML::Parent.new)
+ # e.parent # => #]>
+ #
+ # When argument +context+ is also given, it must be a hash
+ # representing the context for the element;
+ # see {Element Context}[../doc/rexml/context_rdoc.html]:
+ #
+ # e = REXML::Element.new('foo', nil, {raw: :all})
+ # e.context # => {:raw=>:all}
+ #
def initialize( arg = UNDEFINED, parent=nil, context=nil )
super(parent)
@@ -74,6 +342,27 @@ def initialize( arg = UNDEFINED, parent=nil, context=nil )
end
end
+ # :call-seq:
+ # inspect -> string
+ #
+ # Returns a string representation of the element.
+ #
+ # For an element with no attributes and no children, shows the element name:
+ #
+ # REXML::Element.new.inspect # => ""
+ #
+ # Shows attributes, if any:
+ #
+ # e = REXML::Element.new('foo')
+ # e.add_attributes({'bar' => 0, 'baz' => 1})
+ # e.inspect # => ""
+ #
+ # Shows an ellipsis (...), if there are child elements:
+ #
+ # e.add_element(REXML::Element.new('bar'))
+ # e.add_element(REXML::Element.new('baz'))
+ # e.inspect # => " ... >"
+ #
def inspect
rv = "<#@expanded_name"
@@ -89,60 +378,118 @@ def inspect
end
end
-
- # Creates a shallow copy of self.
- # d = Document.new ""
- # new_a = d.root.clone
- # puts new_a # => ""
+ # :call-seq:
+ # clone -> new_element
+ #
+ # Returns a shallow copy of the element, containing the name and attributes,
+ # but not the parent or children:
+ #
+ # e = REXML::Element.new('foo')
+ # e.add_attributes({'bar' => 0, 'baz' => 1})
+ # e.clone # =>
+ #
def clone
self.class.new self
end
- # Evaluates to the root node of the document that this element
- # belongs to. If this element doesn't belong to a document, but does
- # belong to another Element, the parent's root will be returned, until the
- # earliest ancestor is found.
- #
- # Note that this is not the same as the document element.
- # In the following example, is the document element, and the root
- # node is the parent node of the document element. You may ask yourself
- # why the root node is useful: consider the doctype and XML declaration,
- # and any processing instructions before the document element... they
- # are children of the root node, or siblings of the document element.
- # The only time this isn't true is when an Element is created that is
- # not part of any Document. In this case, the ancestor that has no
- # parent acts as the root node.
- # d = Document.new ''
- # a = d[1] ; c = a[1][1]
- # d.root_node == d # TRUE
- # a.root_node # namely, d
- # c.root_node # again, d
+ # :call-seq:
+ # root_node -> document or element
+ #
+ # Returns the most distant ancestor of +self+.
+ #
+ # When the element is part of a document,
+ # returns the root node of the document.
+ # Note that the root node is different from the document element;
+ # in this example +a+ is document element and the root node is its parent:
+ #
+ # d = REXML::Document.new('')
+ # top_element = d.first # => ... >
+ # child = top_element.first # => ... >
+ # d.root_node == d # => true
+ # top_element.root_node == d # => true
+ # child.root_node == d # => true
+ #
+ # When the element is not part of a document, but does have ancestor elements,
+ # returns the most distant ancestor element:
+ #
+ # e0 = REXML::Element.new('foo')
+ # e1 = REXML::Element.new('bar')
+ # e1.parent = e0
+ # e2 = REXML::Element.new('baz')
+ # e2.parent = e1
+ # e2.root_node == e0 # => true
+ #
+ # When the element has no ancestor elements,
+ # returns +self+:
+ #
+ # e = REXML::Element.new('foo')
+ # e.root_node == e # => true
+ #
+ # Related: #root, #document.
+ #
def root_node
parent.nil? ? self : parent.root_node
end
+ # :call-seq:
+ # root -> element
+ #
+ # Returns the most distant _element_ (not document) ancestor of the element:
+ #
+ # d = REXML::Document.new('')
+ # top_element = d.first
+ # child = top_element.first
+ # top_element.root == top_element # => true
+ # child.root == top_element # => true
+ #
+ # For a document, returns the topmost element:
+ #
+ # d.root == top_element # => true
+ #
+ # Related: #root_node, #document.
+ #
def root
return elements[1] if self.kind_of? Document
return self if parent.kind_of? Document or parent.nil?
return parent.root
end
- # Evaluates to the document to which this element belongs, or nil if this
- # element doesn't belong to a document.
+ # :call-seq:
+ # document -> document or nil
+ #
+ # If the element is part of a document, returns that document:
+ #
+ # d = REXML::Document.new('')
+ # top_element = d.first
+ # child = top_element.first
+ # top_element.document == d # => true
+ # child.document == d # => true
+ #
+ # If the element is not part of a document, returns +nil+:
+ #
+ # REXML::Element.new.document # => nil
+ #
+ # For a document, returns +self+:
+ #
+ # d.document == d # => true
+ #
+ # Related: #root, #root_node.
+ #
def document
rt = root
rt.parent if rt
end
- # Evaluates to +true+ if whitespace is respected for this element. This
- # is the case if:
- # 1. Neither :+respect_whitespace+ nor :+compress_whitespace+ has any value
- # 2. The context has :+respect_whitespace+ set to :+all+ or
- # an array containing the name of this element, and
- # :+compress_whitespace+ isn't set to :+all+ or an array containing the
- # name of this element.
- # The evaluation is tested against +expanded_name+, and so is namespace
- # sensitive.
+ # :call-seq:
+ # whitespace
+ #
+ # Returns +true+ if whitespace is respected for this element,
+ # +false+ otherwise.
+ #
+ # See {Element Context}[../doc/rexml/context_rdoc.html].
+ #
+ # The evaluation is tested against the element's +expanded_name+,
+ # and so is namespace-sensitive.
def whitespace
@whitespace = nil
if @context
@@ -159,6 +506,13 @@ def whitespace
@whitespace
end
+ # :call-seq:
+ # ignore_whitespace_nodes
+ #
+ # Returns +true+ if whitespace nodes are ignored for the element.
+ #
+ # See {Element Context}[../doc/rexml/context_rdoc.html].
+ #
def ignore_whitespace_nodes
@ignore_whitespace_nodes = false
if @context
@@ -170,9 +524,12 @@ def ignore_whitespace_nodes
end
end
- # Evaluates to +true+ if raw mode is set for this element. This
- # is the case if the context has :+raw+ set to :+all+ or
- # an array containing the name of this element.
+ # :call-seq:
+ # raw
+ #
+ # Returns +true+ if raw mode is set for the element.
+ #
+ # See {Element Context}[../doc/rexml/context_rdoc.html].
#
# The evaluation is tested against +expanded_name+, and so is namespace
# sensitive.
@@ -180,7 +537,7 @@ def raw
@raw = (@context and @context[:raw] and
(@context[:raw] == :all or
@context[:raw].include? expanded_name))
- @raw
+ @raw
end
#once :whitespace, :raw, :ignore_whitespace_nodes
@@ -189,10 +546,25 @@ def raw
# Namespaces #
#################################################
- # Evaluates to an +Array+ containing the prefixes (names) of all defined
- # namespaces at this context node.
- # doc = Document.new("")
- # doc.elements['//b'].prefixes # -> ['x', 'y']
+ # :call-seq:
+ # prefixes -> array_of_namespace_prefixes
+ #
+ # Returns an array of the string prefixes (names) of all defined namespaces
+ # in the element and its ancestors:
+ #
+ # xml_string = <<-EOT
+ #
+ #
+ #
+ #
+ #
+ #
+ # EOT
+ # d = REXML::Document.new(xml_string, {compress_whitespace: :all})
+ # d.elements['//a'].prefixes # => ["x", "y"]
+ # d.elements['//b'].prefixes # => ["x", "y"]
+ # d.elements['//c'].prefixes # => ["x", "y", "z"]
+ #
def prefixes
prefixes = []
prefixes = parent.prefixes if parent
@@ -200,6 +572,25 @@ def prefixes
return prefixes
end
+ # :call-seq:
+ # namespaces -> array_of_namespace_names
+ #
+ # Returns a hash of all defined namespaces
+ # in the element and its ancestors:
+ #
+ # xml_string = <<-EOT
+ #
+ #
+ #
+ #
+ #
+ #
+ # EOT
+ # d = REXML::Document.new(xml_string)
+ # d.elements['//a'].namespaces # => {"x"=>"1", "y"=>"2"}
+ # d.elements['//b'].namespaces # => {"x"=>"1", "y"=>"2"}
+ # d.elements['//c'].namespaces # => {"x"=>"1", "y"=>"2", "z"=>"3"}
+ #
def namespaces
namespaces = {}
namespaces = parent.namespaces if parent
@@ -207,19 +598,26 @@ def namespaces
return namespaces
end
- # Evaluates to the URI for a prefix, or the empty string if no such
- # namespace is declared for this element. Evaluates recursively for
- # ancestors. Returns the default namespace, if there is one.
- # prefix::
- # the prefix to search for. If not supplied, returns the default
- # namespace if one exists
- # Returns::
- # the namespace URI as a String, or nil if no such namespace
- # exists. If the namespace is undefined, returns an empty string
- # doc = Document.new("")
- # b = doc.elements['//b']
- # b.namespace # -> '1'
- # b.namespace("y") # -> '2'
+ # :call-seq:
+ # namespace(prefix = nil) -> string_uri or nil
+ #
+ # Returns the string namespace URI for the element,
+ # possibly deriving from one of its ancestors.
+ #
+ # xml_string = <<-EOT
+ #
+ #
+ #
+ #
+ #
+ #
+ # EOT
+ # d = REXML::Document.new(xml_string)
+ # b = d.elements['//b']
+ # b.namespace # => "1"
+ # b.namespace('y') # => "2"
+ # b.namespace('nosuch') # => nil
+ #
def namespace(prefix=nil)
if prefix.nil?
prefix = prefix()
@@ -235,19 +633,24 @@ def namespace(prefix=nil)
return ns
end
- # Adds a namespace to this element.
- # prefix::
- # the prefix string, or the namespace URI if +uri+ is not
- # supplied
- # uri::
- # the namespace URI. May be nil, in which +prefix+ is used as
- # the URI
- # Evaluates to: this Element
- # a = Element.new("a")
- # a.add_namespace("xmlns:foo", "bar" )
- # a.add_namespace("foo", "bar") # shorthand for previous line
- # a.add_namespace("twiddle")
- # puts a #->
+ # :call-seq:
+ # add_namespace(prefix, uri = nil) -> self
+ #
+ # Adds a namespace to the element; returns +self+.
+ #
+ # With the single argument +prefix+,
+ # adds a namespace using the given +prefix+ and the namespace URI:
+ #
+ # e = REXML::Element.new('foo')
+ # e.add_namespace('bar')
+ # e.namespaces # => {"xmlns"=>"bar"}
+ #
+ # With both arguments +prefix+ and +uri+ given,
+ # adds a namespace using both arguments:
+ #
+ # e.add_namespace('baz', 'bat')
+ # e.namespaces # => {"xmlns"=>"bar", "baz"=>"bat"}
+ #
def add_namespace( prefix, uri=nil )
unless uri
@attributes["xmlns"] = prefix
@@ -258,16 +661,28 @@ def add_namespace( prefix, uri=nil )
self
end
- # Removes a namespace from this node. This only works if the namespace is
- # actually declared in this node. If no argument is passed, deletes the
- # default namespace.
+ # :call-seq:
+ # delete_namespace(namespace = 'xmlns') -> self
+ #
+ # Removes a namespace from the element.
+ #
+ # With no argument, removes the default namespace:
+ #
+ # d = REXML::Document.new ""
+ # d.to_s # => ""
+ # d.root.delete_namespace # =>
+ # d.to_s # => ""
+ #
+ # With argument +namespace+, removes the specified namespace:
+ #
+ # d.root.delete_namespace('foo')
+ # d.to_s # => ""
+ #
+ # Does nothing if no such namespace is found:
+ #
+ # d.root.delete_namespace('nosuch')
+ # d.to_s # => ""
#
- # Evaluates to: this element
- # doc = Document.new ""
- # doc.root.delete_namespace
- # puts doc # ->
- # doc.root.delete_namespace 'foo'
- # puts doc # ->
def delete_namespace namespace="xmlns"
namespace = "xmlns:#{namespace}" unless namespace == 'xmlns'
attribute = attributes.get_attribute(namespace)
@@ -279,20 +694,40 @@ def delete_namespace namespace="xmlns"
# Elements #
#################################################
- # Adds a child to this element, optionally setting attributes in
- # the element.
- # element::
- # optional. If Element, the element is added.
- # Otherwise, a new Element is constructed with the argument (see
- # Element.initialize).
- # attrs::
- # If supplied, must be a Hash containing String name,value
- # pairs, which will be used to set the attributes of the new Element.
- # Returns:: the Element that was added
- # el = doc.add_element 'my-tag'
- # el = doc.add_element 'my-tag', {'attr1'=>'val1', 'attr2'=>'val2'}
- # el = Element.new 'my-tag'
- # doc.add_element el
+ # :call-seq:
+ # add_element(name, attributes = nil) -> new_element
+ # add_element(element, attributes = nil) -> element
+ #
+ # Adds a child element, optionally setting attributes
+ # on the added element; returns the added element.
+ #
+ # With string argument +name+, creates a new element with that name
+ # and adds the new element as a child:
+ #
+ # e0 = REXML::Element.new('foo')
+ # e0.add_element('bar')
+ # e0[0] # =>
+ #
+ #
+ # With argument +name+ and hash argument +attributes+,
+ # sets attributes on the new element:
+ #
+ # e0.add_element('baz', {'bat' => '0', 'bam' => '1'})
+ # e0[1] # =>
+ #
+ # With element argument +element+, adds that element as a child:
+ #
+ # e0 = REXML::Element.new('foo')
+ # e1 = REXML::Element.new('bar')
+ # e0.add_element(e1)
+ # e0[0] # =>
+ #
+ # With argument +element+ and hash argument +attributes+,
+ # sets attributes on the added element:
+ #
+ # e0.add_element(e1, {'bat' => '0', 'bam' => '1'})
+ # e0[1] # =>
+ #
def add_element element, attrs=nil
raise "First argument must be either an element name, or an Element object" if element.nil?
el = @elements.add(element)
@@ -302,52 +737,112 @@ def add_element element, attrs=nil
el
end
+ # :call-seq:
+ # delete_element(index) -> removed_element or nil
+ # delete_element(element) -> removed_element or nil
+ # delete_element(xpath) -> removed_element or nil
+ #
# Deletes a child element.
- # element::
- # Must be an +Element+, +String+, or +Integer+. If Element,
- # the element is removed. If String, the element is found (via XPath)
- # and removed. This means that any parent can remove any
- # descendant. If Integer, the Element indexed by that number will be
- # removed.
- # Returns:: the element that was removed.
- # doc.delete_element "/a/b/c[@id='4']"
- # doc.delete_element doc.elements["//k"]
- # doc.delete_element 1
+ #
+ # When 1-based integer argument +index+ is given,
+ # removes and returns the child element at that offset if it exists;
+ # indexing does not include text nodes;
+ # returns +nil+ if the element does not exist:
+ #
+ # d = REXML::Document.new 'text'
+ # a = d.root # => ... >
+ # a.delete_element(1) # =>
+ # a.delete_element(1) # =>
+ # a.delete_element(1) # => nil
+ #
+ # When element argument +element+ is given,
+ # removes and returns that child element if it exists,
+ # otherwise returns +nil+:
+ #
+ # d = REXML::Document.new 'text'
+ # a = d.root # => ... >
+ # c = a[2] # =>
+ # a.delete_element(c) # =>
+ # a.delete_element(c) # => nil
+ #
+ # When xpath argument +xpath+ is given,
+ # removes and returns the element at xpath if it exists,
+ # otherwise returns +nil+:
+ #
+ # d = REXML::Document.new 'text'
+ # a = d.root # => ... >
+ # a.delete_element('//c') # =>
+ # a.delete_element('//c') # => nil
+ #
def delete_element element
@elements.delete element
end
- # Evaluates to +true+ if this element has at least one child Element
- # doc = Document.new "Text"
- # doc.root.has_elements # -> true
- # doc.elements["/a/b"].has_elements # -> false
- # doc.elements["/a/c"].has_elements # -> false
+ # :call-seq:
+ # has_elements?
+ #
+ # Returns +true+ if the element has one or more element children,
+ # +false+ otherwise:
+ #
+ # d = REXML::Document.new 'text'
+ # a = d.root # => ... >
+ # a.has_elements? # => true
+ # b = a[0] # =>
+ # b.has_elements? # => false
+ #
def has_elements?
!@elements.empty?
end
- # Iterates through the child elements, yielding for each Element that
- # has a particular attribute set.
- # key::
- # the name of the attribute to search for
- # value::
- # the value of the attribute
- # max::
- # (optional) causes this method to return after yielding
- # for this number of matching children
- # name::
- # (optional) if supplied, this is an XPath that filters
- # the children to check.
- #
- # doc = Document.new ""
- # # Yields b, c, d
- # doc.root.each_element_with_attribute( 'id' ) {|e| p e}
- # # Yields b, d
- # doc.root.each_element_with_attribute( 'id', '1' ) {|e| p e}
- # # Yields b
- # doc.root.each_element_with_attribute( 'id', '1', 1 ) {|e| p e}
- # # Yields d
- # doc.root.each_element_with_attribute( 'id', '1', 0, 'd' ) {|e| p e}
+ # :call-seq:
+ # each_element_with_attribute(attr_name, value = nil, max = 0, xpath = nil) {|e| ... }
+ #
+ # Calls the given block with each child element that meets given criteria.
+ #
+ # When only string argument +attr_name+ is given,
+ # calls the block with each child element that has that attribute:
+ #
+ # d = REXML::Document.new ''
+ # a = d.root
+ # a.each_element_with_attribute('id') {|e| p e }
+ #
+ # Output:
+ #
+ #
+ #
+ #
+ #
+ # With argument +attr_name+ and string argument +value+ given,
+ # calls the block with each child element that has that attribute
+ # with that value:
+ #
+ # a.each_element_with_attribute('id', '1') {|e| p e }
+ #
+ # Output:
+ #
+ #
+ #
+ #
+ # With arguments +attr_name+, +value+, and integer argument +max+ given,
+ # calls the block with at most +max+ child elements:
+ #
+ # a.each_element_with_attribute('id', '1', 1) {|e| p e }
+ #
+ # Output:
+ #
+ #
+ #
+ # With all arguments given, including +xpath+,
+ # calls the block with only those child elements
+ # that meet the first three criteria,
+ # and also match the given +xpath+:
+ #
+ # a.each_element_with_attribute('id', '1', 2, '//d') {|e| p e }
+ #
+ # Output:
+ #
+ #
+ #
def each_element_with_attribute( key, value=nil, max=0, name=nil, &block ) # :yields: Element
each_with_something( proc {|child|
if value.nil?
@@ -358,27 +853,53 @@ def each_element_with_attribute( key, value=nil, max=0, name=nil, &block ) # :yi
}, max, name, &block )
end
- # Iterates through the children, yielding for each Element that
- # has a particular text set.
- # text::
- # the text to search for. If nil, or not supplied, will iterate
- # over all +Element+ children that contain at least one +Text+ node.
- # max::
- # (optional) causes this method to return after yielding
- # for this number of matching children
- # name::
- # (optional) if supplied, this is an XPath that filters
- # the children to check.
- #
- # doc = Document.new 'bbd'
- # # Yields b, c, d
- # doc.each_element_with_text {|e|p e}
- # # Yields b, c
- # doc.each_element_with_text('b'){|e|p e}
- # # Yields b
- # doc.each_element_with_text('b', 1){|e|p e}
- # # Yields d
- # doc.each_element_with_text(nil, 0, 'd'){|e|p e}
+ # :call-seq:
+ # each_element_with_text(text = nil, max = 0, xpath = nil) {|e| ... }
+ #
+ # Calls the given block with each child element that meets given criteria.
+ #
+ # With no arguments, calls the block with each child element that has text:
+ #
+ # d = REXML::Document.new 'bbd'
+ # a = d.root
+ # a.each_element_with_text {|e| p e }
+ #
+ # Output:
+ #
+ # ... >
+ # ... >
+ # ... >
+ #
+ # With the single string argument +text+,
+ # calls the block with each element that has exactly that text:
+ #
+ # a.each_element_with_text('b') {|e| p e }
+ #
+ # Output:
+ #
+ # ... >
+ # ... >
+ #
+ # With argument +text+ and integer argument +max+,
+ # calls the block with at most +max+ elements:
+ #
+ # a.each_element_with_text('b', 1) {|e| p e }
+ #
+ # Output:
+ #
+ # ... >
+ #
+ # With all arguments given, including +xpath+,
+ # calls the block with only those child elements
+ # that meet the first two criteria,
+ # and also match the given +xpath+:
+ #
+ # a.each_element_with_text('b', 2, '//c') {|e| p e }
+ #
+ # Output:
+ #
+ # ... >
+ #
def each_element_with_text( text=nil, max=0, name=nil, &block ) # :yields: Element
each_with_something( proc {|child|
if text.nil?
@@ -389,35 +910,71 @@ def each_element_with_text( text=nil, max=0, name=nil, &block ) # :yields: Eleme
}, max, name, &block )
end
- # Synonym for Element.elements.each
+ # :call-seq:
+ # each_element {|e| ... }
+ #
+ # Calls the given block with each child element:
+ #
+ # d = REXML::Document.new 'bbd'
+ # a = d.root
+ # a.each_element {|e| p e }
+ #
+ # Output:
+ #
+ # ... >
+ # ... >
+ # ... >
+ #
+ #
def each_element( xpath=nil, &block ) # :yields: Element
@elements.each( xpath, &block )
end
- # Synonym for Element.to_a
- # This is a little slower than calling elements.each directly.
- # xpath:: any XPath by which to search for elements in the tree
- # Returns:: an array of Elements that match the supplied path
+ # :call-seq:
+ # get_elements(xpath)
+ #
+ # Returns an array of the elements that match the given +xpath+:
+ #
+ # xml_string = <<-EOT
+ #
+ #
+ #
+ #
+ #
+ # EOT
+ # d = REXML::Document.new(xml_string)
+ # d.root.get_elements('//a') # => [ ... >, ]
+ #
def get_elements( xpath )
@elements.to_a( xpath )
end
- # Returns the next sibling that is an element, or nil if there is
- # no Element sibling after this one
- # doc = Document.new 'text'
- # doc.root.elements['b'].next_element #->
- # doc.root.elements['c'].next_element #-> nil
+ # :call-seq:
+ # next_element
+ #
+ # Returns the next sibling that is an element if it exists,
+ # +niL+ otherwise:
+ #
+ # d = REXML::Document.new 'text'
+ # d.root.elements['b'].next_element #->
+ # d.root.elements['c'].next_element #-> nil
+ #
def next_element
element = next_sibling
element = element.next_sibling until element.nil? or element.kind_of? Element
return element
end
- # Returns the previous sibling that is an element, or nil if there is
- # no Element sibling prior to this one
- # doc = Document.new 'text'
- # doc.root.elements['c'].previous_element #->
- # doc.root.elements['b'].previous_element #-> nil
+ # :call-seq:
+ # previous_element
+ #
+ # Returns the previous sibling that is an element if it exists,
+ # +niL+ otherwise:
+ #
+ # d = REXML::Document.new 'text'
+ # d.root.elements['c'].previous_element #->
+ # d.root.elements['b'].previous_element #-> nil
+ #
def previous_element
element = previous_sibling
element = element.previous_sibling until element.nil? or element.kind_of? Element
@@ -429,36 +986,69 @@ def previous_element
# Text #
#################################################
- # Evaluates to +true+ if this element has at least one Text child
+ # :call-seq:
+ # has_text? -> true or false
+ #
+ # Returns +true+ if the element has one or more text noded,
+ # +false+ otherwise:
+ #
+ # d = REXML::Document.new 'text'
+ # a = d.root
+ # a.has_text? # => true
+ # b = a[0]
+ # b.has_text? # => false
+ #
def has_text?
not text().nil?
end
- # A convenience method which returns the String value of the _first_
- # child text element, if one exists, and +nil+ otherwise.
+ # :call-seq:
+ # text(xpath = nil) -> text_string or nil
+ #
+ # Returns the text string from the first text node child
+ # in a specified element, if it exists, +nil+ otherwise.
+ #
+ # With no argument, returns the text from the first text node in +self+:
+ #
+ # d = REXML::Document.new "
some text this is bold! more text
"
+ # d.root.text.class # => String
+ # d.root.text # => "some text "
+ #
+ # With argument +xpath+, returns text from the first text node
+ # in the element that matches +xpath+:
+ #
+ # d.root.text(1) # => "this is bold!"
#
- # Note that an element may have multiple Text elements, perhaps
- # separated by other children. Be aware that this method only returns
- # the first Text node.
+ # Note that an element may have multiple text nodes,
+ # possibly separated by other non-text children, as above.
+ # Even so, the returned value is the string text from the first such node.
#
- # This method returns the +value+ of the first text child node, which
- # ignores the +raw+ setting, so always returns normalized text. See
- # the Text::value documentation.
+ # Note also that the text note is retrieved by method get_text,
+ # and so is always normalized text.
#
- # doc = Document.new "
some text this is bold! more text
"
- # # The element 'p' has two text elements, "some text " and " more text".
- # doc.root.text #-> "some text "
def text( path = nil )
rv = get_text(path)
return rv.value unless rv.nil?
nil
end
- # Returns the first child Text node, if any, or +nil+ otherwise.
- # This method returns the actual +Text+ node, rather than the String content.
- # doc = Document.new "
some text this is bold! more text
"
- # # The element 'p' has two text elements, "some text " and " more text".
- # doc.root.get_text.value #-> "some text "
+ # :call-seq:
+ # get_text(xpath = nil) -> text_node or nil
+ #
+ # Returns the first text node child in a specified element, if it exists,
+ # +nil+ otherwise.
+ #
+ # With no argument, returns the first text node from +self+:
+ #
+ # d = REXML::Document.new "
')
+ in_toc = true
+ next
+ end
+ end
+ if in_toc
+ break if line.include?('
')
+ toc_lis.push(line.chomp)
+ end
+ end
+ end
+ key = html_file_path.sub('_rdoc.html', '')
+ lis_by_name[key] = toc_lis
+ end
+ end
+ end
+ lis_by_name
+ end
+
+ def generate_files(lis_by_name)
+ File.open('tocs/master_toc.rdoc', 'w') do |master_toc_file|
+ master_toc_file.write("== Tasks\n\n")
+ cd('tocs') do
+ entries = Dir.entries('.')
+ entries.delete_if {|entry| entry.start_with?('.') }
+ entries.delete_if {|entry| entry == 'master_toc.rdoc' }
+ lis_by_name.keys.sort.each do |name|
+ lis = lis_by_name[name]
+ toc_file_name = name + '_toc.rdoc'
+ entries.delete(toc_file_name)
+ File.open(toc_file_name, 'w') do |class_file|
+ class_file.write("Tasks on this page:\n\n")
+ lis.each_with_index do |li, i|
+ _, temp = li.split('"', 2)
+ link, temp = temp.split('">', 2)
+ text = temp.sub('', '')
+ indentation = text.start_with?('Task') ? ' ' : ''
+ toc_entry = "#{indentation}- {#{text}}[#{link}]\n"
+ if i == 0
+ text = text.split(' ')[1]
+ link = "../../tasks/rdoc/#{text.downcase}_rdoc.html"
+ master_toc_file.write("=== {#{text}}[#{link}]\n")
+ next
+ end
+ master_link = "../../tasks/rdoc/#{toc_file_name.sub('_toc.rdoc', '_rdoc.html')}#{link}"
+ master_toc_entry = "#{indentation}- {#{text}}[#{master_link}]\n"
+ master_toc_file.write(master_toc_entry)
+ class_file.write(toc_entry)
+ end
+ master_toc_file.write("\n")
+ class_file.write("\n")
+ end
+ end
+ unless entries.empty?
+ message = "Some entries not updated: #{entries}"
+ raise message
+ end
+ end
+ end
+ end
+end
+
+namespace :tocs do
+ desc "Generate TOCs"
+ task :generate do
+ generator = TOCsGenerator.new
+ generator.generate
+ end
+end
diff --git a/test/rexml/data/LostineRiver.kml.gz b/test/data/LostineRiver.kml.gz
similarity index 100%
rename from test/rexml/data/LostineRiver.kml.gz
rename to test/data/LostineRiver.kml.gz
diff --git a/test/rexml/data/ProductionSupport.xml b/test/data/ProductionSupport.xml
similarity index 100%
rename from test/rexml/data/ProductionSupport.xml
rename to test/data/ProductionSupport.xml
diff --git a/test/rexml/data/axis.xml b/test/data/axis.xml
similarity index 100%
rename from test/rexml/data/axis.xml
rename to test/data/axis.xml
diff --git a/test/rexml/data/bad.xml b/test/data/bad.xml
similarity index 100%
rename from test/rexml/data/bad.xml
rename to test/data/bad.xml
diff --git a/test/rexml/data/basic.xml b/test/data/basic.xml
similarity index 100%
rename from test/rexml/data/basic.xml
rename to test/data/basic.xml
diff --git a/test/rexml/data/basicupdate.xml b/test/data/basicupdate.xml
similarity index 100%
rename from test/rexml/data/basicupdate.xml
rename to test/data/basicupdate.xml
diff --git a/test/rexml/data/broken.rss b/test/data/broken.rss
similarity index 100%
rename from test/rexml/data/broken.rss
rename to test/data/broken.rss
diff --git a/test/rexml/data/contents.xml b/test/data/contents.xml
similarity index 100%
rename from test/rexml/data/contents.xml
rename to test/data/contents.xml
diff --git a/test/rexml/data/dash.xml b/test/data/dash.xml
similarity index 100%
rename from test/rexml/data/dash.xml
rename to test/data/dash.xml
diff --git a/test/rexml/data/defaultNamespace.xml b/test/data/defaultNamespace.xml
similarity index 100%
rename from test/rexml/data/defaultNamespace.xml
rename to test/data/defaultNamespace.xml
diff --git a/test/rexml/data/doctype_test.xml b/test/data/doctype_test.xml
similarity index 100%
rename from test/rexml/data/doctype_test.xml
rename to test/data/doctype_test.xml
diff --git a/test/rexml/data/documentation.xml b/test/data/documentation.xml
similarity index 100%
rename from test/rexml/data/documentation.xml
rename to test/data/documentation.xml
diff --git a/test/rexml/data/euc.xml b/test/data/euc.xml
similarity index 100%
rename from test/rexml/data/euc.xml
rename to test/data/euc.xml
diff --git a/test/rexml/data/evaluate.xml b/test/data/evaluate.xml
similarity index 100%
rename from test/rexml/data/evaluate.xml
rename to test/data/evaluate.xml
diff --git a/test/rexml/data/fibo.xml b/test/data/fibo.xml
similarity index 100%
rename from test/rexml/data/fibo.xml
rename to test/data/fibo.xml
diff --git a/test/rexml/data/foo.xml b/test/data/foo.xml
similarity index 100%
rename from test/rexml/data/foo.xml
rename to test/data/foo.xml
diff --git a/test/rexml/data/google.2.xml b/test/data/google.2.xml
similarity index 100%
rename from test/rexml/data/google.2.xml
rename to test/data/google.2.xml
diff --git a/test/rexml/data/id.xml b/test/data/id.xml
similarity index 100%
rename from test/rexml/data/id.xml
rename to test/data/id.xml
diff --git a/test/rexml/data/iso8859-1.xml b/test/data/iso8859-1.xml
similarity index 100%
rename from test/rexml/data/iso8859-1.xml
rename to test/data/iso8859-1.xml
diff --git a/test/rexml/data/jaxen24.xml b/test/data/jaxen24.xml
similarity index 100%
rename from test/rexml/data/jaxen24.xml
rename to test/data/jaxen24.xml
diff --git a/test/rexml/data/jaxen3.xml b/test/data/jaxen3.xml
similarity index 100%
rename from test/rexml/data/jaxen3.xml
rename to test/data/jaxen3.xml
diff --git a/test/rexml/data/lang.xml b/test/data/lang.xml
similarity index 100%
rename from test/rexml/data/lang.xml
rename to test/data/lang.xml
diff --git a/test/rexml/data/lang0.xml b/test/data/lang0.xml
similarity index 100%
rename from test/rexml/data/lang0.xml
rename to test/data/lang0.xml
diff --git a/test/rexml/data/message.xml b/test/data/message.xml
similarity index 100%
rename from test/rexml/data/message.xml
rename to test/data/message.xml
diff --git a/test/rexml/data/moreover.xml b/test/data/moreover.xml
similarity index 100%
rename from test/rexml/data/moreover.xml
rename to test/data/moreover.xml
diff --git a/test/rexml/data/much_ado.xml b/test/data/much_ado.xml
similarity index 99%
rename from test/rexml/data/much_ado.xml
rename to test/data/much_ado.xml
index f008fadb..0040088c 100644
--- a/test/rexml/data/much_ado.xml
+++ b/test/data/much_ado.xml
@@ -4735,7 +4735,7 @@ CLAUDIO, BENEDICK, HERO, BEATRICE, and Attendants
But they shall find, awaked in such a kind,Both strength of limb and policy of mind,Ability in means and choice of friends,
-To quit me of them throughly.
+To quit me of them thoroughly.
diff --git a/test/rexml/data/namespaces.xml b/test/data/namespaces.xml
similarity index 100%
rename from test/rexml/data/namespaces.xml
rename to test/data/namespaces.xml
diff --git a/test/rexml/data/nitf.xml b/test/data/nitf.xml
similarity index 100%
rename from test/rexml/data/nitf.xml
rename to test/data/nitf.xml
diff --git a/test/rexml/data/numbers.xml b/test/data/numbers.xml
similarity index 100%
rename from test/rexml/data/numbers.xml
rename to test/data/numbers.xml
diff --git a/test/rexml/data/ofbiz-issues-full-177.xml b/test/data/ofbiz-issues-full-177.xml
similarity index 99%
rename from test/rexml/data/ofbiz-issues-full-177.xml
rename to test/data/ofbiz-issues-full-177.xml
index bfff771d..e1f7bdfd 100644
--- a/test/rexml/data/ofbiz-issues-full-177.xml
+++ b/test/data/ofbiz-issues-full-177.xml
@@ -152,8 +152,8 @@
-
-
+
+
diff --git a/test/rexml/data/pi.xml b/test/data/pi.xml
similarity index 100%
rename from test/rexml/data/pi.xml
rename to test/data/pi.xml
diff --git a/test/rexml/data/pi2.xml b/test/data/pi2.xml
similarity index 100%
rename from test/rexml/data/pi2.xml
rename to test/data/pi2.xml
diff --git a/test/rexml/data/project.xml b/test/data/project.xml
similarity index 100%
rename from test/rexml/data/project.xml
rename to test/data/project.xml
diff --git a/test/rexml/data/simple.xml b/test/data/simple.xml
similarity index 100%
rename from test/rexml/data/simple.xml
rename to test/data/simple.xml
diff --git a/test/rexml/data/stream_accents.xml b/test/data/stream_accents.xml
similarity index 100%
rename from test/rexml/data/stream_accents.xml
rename to test/data/stream_accents.xml
diff --git a/test/rexml/data/t63-1.xml b/test/data/t63-1.xml
similarity index 100%
rename from test/rexml/data/t63-1.xml
rename to test/data/t63-1.xml
diff --git a/test/rexml/data/t63-2.svg b/test/data/t63-2.svg
similarity index 100%
rename from test/rexml/data/t63-2.svg
rename to test/data/t63-2.svg
diff --git a/test/rexml/data/t75.xml b/test/data/t75.xml
similarity index 100%
rename from test/rexml/data/t75.xml
rename to test/data/t75.xml
diff --git a/test/rexml/data/test/tests.xml b/test/data/test/tests.xml
similarity index 99%
rename from test/rexml/data/test/tests.xml
rename to test/data/test/tests.xml
index cf03b42b..fd415679 100644
--- a/test/rexml/data/test/tests.xml
+++ b/test/data/test/tests.xml
@@ -299,7 +299,7 @@
-
+
web-appweb-appweb-app
@@ -318,7 +318,7 @@
-
+
web-appweb-appweb-app
diff --git a/test/rexml/data/test/tests.xsl b/test/data/test/tests.xsl
similarity index 100%
rename from test/rexml/data/test/tests.xsl
rename to test/data/test/tests.xsl
diff --git a/test/rexml/data/testNamespaces.xml b/test/data/testNamespaces.xml
similarity index 100%
rename from test/rexml/data/testNamespaces.xml
rename to test/data/testNamespaces.xml
diff --git a/test/rexml/data/testsrc.xml b/test/data/testsrc.xml
similarity index 100%
rename from test/rexml/data/testsrc.xml
rename to test/data/testsrc.xml
diff --git a/test/rexml/data/text.xml b/test/data/text.xml
similarity index 100%
rename from test/rexml/data/text.xml
rename to test/data/text.xml
diff --git a/test/rexml/data/ticket_61.xml b/test/data/ticket_61.xml
similarity index 100%
rename from test/rexml/data/ticket_61.xml
rename to test/data/ticket_61.xml
diff --git a/test/rexml/data/ticket_68.xml b/test/data/ticket_68.xml
similarity index 100%
rename from test/rexml/data/ticket_68.xml
rename to test/data/ticket_68.xml
diff --git a/test/rexml/data/tutorial.xml b/test/data/tutorial.xml
similarity index 99%
rename from test/rexml/data/tutorial.xml
rename to test/data/tutorial.xml
index bf5783d0..9c4639b9 100644
--- a/test/rexml/data/tutorial.xml
+++ b/test/data/tutorial.xml
@@ -286,7 +286,7 @@ el1 << Text.new(" cruel world")
strings.
I can't emphasize this enough, because people do have problems with
- this. REXML can't possibly alway guess correctly how your text is
+ this. REXML can't possibly always guess correctly how your text is
encoded, so it always assumes the text is UTF-8. It also does not warn
you when you try to add text which isn't properly encoded, for the
same reason. You must make sure that you are adding UTF-8 text.
diff --git a/test/rexml/data/underscore.xml b/test/data/underscore.xml
similarity index 100%
rename from test/rexml/data/underscore.xml
rename to test/data/underscore.xml
diff --git a/test/rexml/data/utf16.xml b/test/data/utf16.xml
similarity index 100%
rename from test/rexml/data/utf16.xml
rename to test/data/utf16.xml
diff --git a/test/rexml/data/web.xml b/test/data/web.xml
similarity index 100%
rename from test/rexml/data/web.xml
rename to test/data/web.xml
diff --git a/test/rexml/data/web2.xml b/test/data/web2.xml
similarity index 100%
rename from test/rexml/data/web2.xml
rename to test/data/web2.xml
diff --git a/test/rexml/data/working.rss b/test/data/working.rss
similarity index 100%
rename from test/rexml/data/working.rss
rename to test/data/working.rss
diff --git a/test/rexml/data/xmlfile-bug.xml b/test/data/xmlfile-bug.xml
similarity index 100%
rename from test/rexml/data/xmlfile-bug.xml
rename to test/data/xmlfile-bug.xml
diff --git a/test/rexml/data/xp.tst b/test/data/xp.tst
similarity index 100%
rename from test/rexml/data/xp.tst
rename to test/data/xp.tst
diff --git a/test/rexml/data/yahoo.xml b/test/data/yahoo.xml
similarity index 100%
rename from test/rexml/data/yahoo.xml
rename to test/data/yahoo.xml
diff --git a/test/rexml/formatter/test_default.rb b/test/formatter/test_default.rb
similarity index 87%
rename from test/rexml/formatter/test_default.rb
rename to test/formatter/test_default.rb
index b5b13172..aa403dbe 100644
--- a/test/rexml/formatter/test_default.rb
+++ b/test/formatter/test_default.rb
@@ -1,10 +1,8 @@
-require_relative "../rexml_test_utils"
-
module REXMLTests
class DefaultFormatterTest < Test::Unit::TestCase
def format(node)
formatter = REXML::Formatters::Default.new
- output = ""
+ output = +""
formatter.write(node, output)
output
end
diff --git a/test/rexml/functions/test_base.rb b/test/functions/test_base.rb
similarity index 93%
rename from test/rexml/functions/test_base.rb
rename to test/functions/test_base.rb
index 74dc1a31..daa38156 100644
--- a/test/rexml/functions/test_base.rb
+++ b/test/functions/test_base.rb
@@ -229,8 +229,30 @@ def test_normalize_space
assert_equal( [REXML::Comment.new("COMMENT A")], m )
end
+ def test_normalize_space_strings
+ source = <<-XML
+breakfast boosts\t\t
+
+concentration
+Coffee beans
+ aroma
+
+
+
+ Dessert
+ \t\t after dinner
+ XML
+ normalized_texts = REXML::XPath.each(REXML::Document.new(source), "normalize-space(//text())").to_a
+ assert_equal([
+ "breakfast boosts concentration",
+ "Coffee beans aroma",
+ "Dessert after dinner",
+ ],
+ normalized_texts)
+ end
+
def test_string_nil_without_context
- doc = REXML::Document.new(<<-XML)
+ doc = REXML::Document.new(<<~XML)
diff --git a/test/rexml/functions/test_boolean.rb b/test/functions/test_boolean.rb
similarity index 100%
rename from test/rexml/functions/test_boolean.rb
rename to test/functions/test_boolean.rb
diff --git a/test/rexml/functions/test_local_name.rb b/test/functions/test_local_name.rb
similarity index 100%
rename from test/rexml/functions/test_local_name.rb
rename to test/functions/test_local_name.rb
diff --git a/test/rexml/functions/test_number.rb b/test/functions/test_number.rb
similarity index 100%
rename from test/rexml/functions/test_number.rb
rename to test/functions/test_number.rb
diff --git a/test/helper.rb b/test/helper.rb
new file mode 100644
index 00000000..3de13276
--- /dev/null
+++ b/test/helper.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: false
+
+require "test-unit"
+
+require "rexml/document"
+
+module Helper
+ module Fixture
+ def fixture_path(*components)
+ File.join(__dir__, "data", *components)
+ end
+ end
+
+ module Global
+ def suppress_warning
+ verbose = $VERBOSE
+ begin
+ $VERBOSE = nil
+ yield
+ ensure
+ $VERBOSE = verbose
+ end
+ end
+
+ def with_default_internal(encoding)
+ default_internal = Encoding.default_internal
+ begin
+ suppress_warning {Encoding.default_internal = encoding}
+ yield
+ ensure
+ suppress_warning {Encoding.default_internal = default_internal}
+ end
+ end
+ end
+end
diff --git a/test/lib/envutil.rb b/test/lib/envutil.rb
deleted file mode 100644
index 5d3bce99..00000000
--- a/test/lib/envutil.rb
+++ /dev/null
@@ -1,298 +0,0 @@
-# -*- coding: us-ascii -*-
-# frozen_string_literal: true
-require "open3"
-require "timeout"
-require_relative "find_executable"
-begin
- require 'rbconfig'
-rescue LoadError
-end
-begin
- require "rbconfig/sizeof"
-rescue LoadError
-end
-
-module EnvUtil
- def rubybin
- if ruby = ENV["RUBY"]
- return ruby
- end
- ruby = "ruby"
- exeext = RbConfig::CONFIG["EXEEXT"]
- rubyexe = (ruby + exeext if exeext and !exeext.empty?)
- 3.times do
- if File.exist? ruby and File.executable? ruby and !File.directory? ruby
- return File.expand_path(ruby)
- end
- if rubyexe and File.exist? rubyexe and File.executable? rubyexe
- return File.expand_path(rubyexe)
- end
- ruby = File.join("..", ruby)
- end
- if defined?(RbConfig.ruby)
- RbConfig.ruby
- else
- "ruby"
- end
- end
- module_function :rubybin
-
- LANG_ENVS = %w"LANG LC_ALL LC_CTYPE"
-
- DEFAULT_SIGNALS = Signal.list
- DEFAULT_SIGNALS.delete("TERM") if /mswin|mingw/ =~ RUBY_PLATFORM
-
- RUBYLIB = ENV["RUBYLIB"]
-
- class << self
- attr_accessor :subprocess_timeout_scale
- attr_reader :original_internal_encoding, :original_external_encoding,
- :original_verbose
-
- def capture_global_values
- @original_internal_encoding = Encoding.default_internal
- @original_external_encoding = Encoding.default_external
- @original_verbose = $VERBOSE
- end
- end
-
- def apply_timeout_scale(t)
- if scale = EnvUtil.subprocess_timeout_scale
- t * scale
- else
- t
- end
- end
- module_function :apply_timeout_scale
-
- def invoke_ruby(args, stdin_data = "", capture_stdout = false, capture_stderr = false,
- encoding: nil, timeout: 10, reprieve: 1, timeout_error: Timeout::Error,
- stdout_filter: nil, stderr_filter: nil,
- signal: :TERM,
- rubybin: EnvUtil.rubybin, precommand: nil,
- **opt)
- timeout = apply_timeout_scale(timeout)
- reprieve = apply_timeout_scale(reprieve) if reprieve
-
- in_c, in_p = IO.pipe
- out_p, out_c = IO.pipe if capture_stdout
- err_p, err_c = IO.pipe if capture_stderr && capture_stderr != :merge_to_stdout
- opt[:in] = in_c
- opt[:out] = out_c if capture_stdout
- opt[:err] = capture_stderr == :merge_to_stdout ? out_c : err_c if capture_stderr
- if encoding
- out_p.set_encoding(encoding) if out_p
- err_p.set_encoding(encoding) if err_p
- end
- c = "C"
- child_env = {}
- LANG_ENVS.each {|lc| child_env[lc] = c}
- if Array === args and Hash === args.first
- child_env.update(args.shift)
- end
- if RUBYLIB and lib = child_env["RUBYLIB"]
- child_env["RUBYLIB"] = [lib, RUBYLIB].join(File::PATH_SEPARATOR)
- end
- args = [args] if args.kind_of?(String)
- pid = spawn(child_env, *precommand, rubybin, *args, **opt)
- in_c.close
- out_c.close if capture_stdout
- err_c.close if capture_stderr && capture_stderr != :merge_to_stdout
- if block_given?
- return yield in_p, out_p, err_p, pid
- else
- th_stdout = Thread.new { out_p.read } if capture_stdout
- th_stderr = Thread.new { err_p.read } if capture_stderr && capture_stderr != :merge_to_stdout
- in_p.write stdin_data.to_str unless stdin_data.empty?
- in_p.close
- if (!th_stdout || th_stdout.join(timeout)) && (!th_stderr || th_stderr.join(timeout))
- timeout_error = nil
- else
- signals = Array(signal).select do |sig|
- DEFAULT_SIGNALS[sig.to_s] or
- DEFAULT_SIGNALS[Signal.signame(sig)] rescue false
- end
- signals |= [:ABRT, :KILL]
- case pgroup = opt[:pgroup]
- when 0, true
- pgroup = -pid
- when nil, false
- pgroup = pid
- end
- while signal = signals.shift
- begin
- Process.kill signal, pgroup
- rescue Errno::EINVAL
- next
- rescue Errno::ESRCH
- break
- end
- if signals.empty? or !reprieve
- Process.wait(pid)
- else
- begin
- Timeout.timeout(reprieve) {Process.wait(pid)}
- rescue Timeout::Error
- end
- end
- end
- status = $?
- end
- stdout = th_stdout.value if capture_stdout
- stderr = th_stderr.value if capture_stderr && capture_stderr != :merge_to_stdout
- out_p.close if capture_stdout
- err_p.close if capture_stderr && capture_stderr != :merge_to_stdout
- status ||= Process.wait2(pid)[1]
- stdout = stdout_filter.call(stdout) if stdout_filter
- stderr = stderr_filter.call(stderr) if stderr_filter
- if timeout_error
- bt = caller_locations
- msg = "execution of #{bt.shift.label} expired timeout (#{timeout} sec)"
- msg = Test::Unit::Assertions::FailDesc[status, msg, [stdout, stderr].join("\n")].()
- raise timeout_error, msg, bt.map(&:to_s)
- end
- return stdout, stderr, status
- end
- ensure
- [th_stdout, th_stderr].each do |th|
- th.kill if th
- end
- [in_c, in_p, out_c, out_p, err_c, err_p].each do |io|
- io&.close
- end
- [th_stdout, th_stderr].each do |th|
- th.join if th
- end
- end
- module_function :invoke_ruby
-
- alias rubyexec invoke_ruby
- class << self
- alias rubyexec invoke_ruby
- end
-
- def verbose_warning
- class << (stderr = "".dup)
- alias write concat
- def flush; end
- end
- stderr, $stderr = $stderr, stderr
- $VERBOSE = true
- yield stderr
- return $stderr
- ensure
- stderr, $stderr = $stderr, stderr
- $VERBOSE = EnvUtil.original_verbose
- end
- module_function :verbose_warning
-
- def default_warning
- $VERBOSE = false
- yield
- ensure
- $VERBOSE = EnvUtil.original_verbose
- end
- module_function :default_warning
-
- def suppress_warning
- $VERBOSE = nil
- yield
- ensure
- $VERBOSE = EnvUtil.original_verbose
- end
- module_function :suppress_warning
-
- def under_gc_stress(stress = true)
- stress, GC.stress = GC.stress, stress
- yield
- ensure
- GC.stress = stress
- end
- module_function :under_gc_stress
-
- def with_default_external(enc)
- suppress_warning { Encoding.default_external = enc }
- yield
- ensure
- suppress_warning { Encoding.default_external = EnvUtil.original_external_encoding }
- end
- module_function :with_default_external
-
- def with_default_internal(enc)
- suppress_warning { Encoding.default_internal = enc }
- yield
- ensure
- suppress_warning { Encoding.default_internal = EnvUtil.original_internal_encoding }
- end
- module_function :with_default_internal
-
- def labeled_module(name, &block)
- Module.new do
- singleton_class.class_eval {define_method(:to_s) {name}; alias inspect to_s}
- class_eval(&block) if block
- end
- end
- module_function :labeled_module
-
- def labeled_class(name, superclass = Object, &block)
- Class.new(superclass) do
- singleton_class.class_eval {define_method(:to_s) {name}; alias inspect to_s}
- class_eval(&block) if block
- end
- end
- module_function :labeled_class
-
- if /darwin/ =~ RUBY_PLATFORM
- DIAGNOSTIC_REPORTS_PATH = File.expand_path("~/Library/Logs/DiagnosticReports")
- DIAGNOSTIC_REPORTS_TIMEFORMAT = '%Y-%m-%d-%H%M%S'
- @ruby_install_name = RbConfig::CONFIG['RUBY_INSTALL_NAME']
-
- def self.diagnostic_reports(signame, pid, now)
- return unless %w[ABRT QUIT SEGV ILL TRAP].include?(signame)
- cmd = File.basename(rubybin)
- cmd = @ruby_install_name if "ruby-runner#{RbConfig::CONFIG["EXEEXT"]}" == cmd
- path = DIAGNOSTIC_REPORTS_PATH
- timeformat = DIAGNOSTIC_REPORTS_TIMEFORMAT
- pat = "#{path}/#{cmd}_#{now.strftime(timeformat)}[-_]*.crash"
- first = true
- 30.times do
- first ? (first = false) : sleep(0.1)
- Dir.glob(pat) do |name|
- log = File.read(name) rescue next
- if /\AProcess:\s+#{cmd} \[#{pid}\]$/ =~ log
- File.unlink(name)
- File.unlink("#{path}/.#{File.basename(name)}.plist") rescue nil
- return log
- end
- end
- end
- nil
- end
- else
- def self.diagnostic_reports(signame, pid, now)
- end
- end
-
- def self.gc_stress_to_class?
- unless defined?(@gc_stress_to_class)
- _, _, status = invoke_ruby(["-e""exit GC.respond_to?(:add_stress_to_class)"])
- @gc_stress_to_class = status.success?
- end
- @gc_stress_to_class
- end
-end
-
-if defined?(RbConfig)
- module RbConfig
- @ruby = EnvUtil.rubybin
- class << self
- undef ruby if method_defined?(:ruby)
- attr_reader :ruby
- end
- dir = File.dirname(ruby)
- CONFIG['bindir'] = dir
- Gem::ConfigMap[:bindir] = dir if defined?(Gem::ConfigMap)
- end
-end
-
-EnvUtil.capture_global_values
diff --git a/test/lib/find_executable.rb b/test/lib/find_executable.rb
deleted file mode 100644
index 89c6fb8f..00000000
--- a/test/lib/find_executable.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-require "rbconfig"
-
-module EnvUtil
- def find_executable(cmd, *args)
- exts = RbConfig::CONFIG["EXECUTABLE_EXTS"].split | [RbConfig::CONFIG["EXEEXT"]]
- ENV["PATH"].split(File::PATH_SEPARATOR).each do |path|
- next if path.empty?
- path = File.join(path, cmd)
- exts.each do |ext|
- cmdline = [path + ext, *args]
- begin
- return cmdline if yield(IO.popen(cmdline, "r", err: [:child, :out], &:read))
- rescue
- next
- end
- end
- end
- nil
- end
- module_function :find_executable
-end
diff --git a/test/lib/iseq_loader_checker.rb b/test/lib/iseq_loader_checker.rb
deleted file mode 100644
index 1a1a6948..00000000
--- a/test/lib/iseq_loader_checker.rb
+++ /dev/null
@@ -1,75 +0,0 @@
-# frozen_string_literal: true
-
-begin
- require '-test-/iseq_load/iseq_load'
-rescue LoadError
-end
-require 'tempfile'
-
-class RubyVM::InstructionSequence
- def disasm_if_possible
- begin
- self.disasm
- rescue Encoding::CompatibilityError, EncodingError, SecurityError
- nil
- end
- end
-
- def self.compare_dump_and_load i1, dumper, loader
- dump = dumper.call(i1)
- return i1 unless dump
- i2 = loader.call(dump)
-
- # compare disassembled result
- d1 = i1.disasm_if_possible
- d2 = i2.disasm_if_possible
-
- if d1 != d2
- STDERR.puts "expected:"
- STDERR.puts d1
- STDERR.puts "actual:"
- STDERR.puts d2
-
- t1 = Tempfile.new("expected"); t1.puts d1; t1.close
- t2 = Tempfile.new("actual"); t2.puts d2; t2.close
- system("diff -u #{t1.path} #{t2.path}") # use diff if available
- exit(1)
- end
- i2
- end
-
- CHECK_TO_A = ENV['RUBY_ISEQ_DUMP_DEBUG'] == 'to_a'
- CHECK_TO_BINARY = ENV['RUBY_ISEQ_DUMP_DEBUG'] == 'to_binary'
-
- def self.translate i1
- # check to_a/load_iseq
- compare_dump_and_load(i1,
- proc{|iseq|
- ary = iseq.to_a
- ary[9] == :top ? ary : nil
- },
- proc{|ary|
- RubyVM::InstructionSequence.iseq_load(ary)
- }) if CHECK_TO_A && defined?(RubyVM::InstructionSequence.iseq_load)
-
- # check to_binary
- i2_bin = compare_dump_and_load(i1,
- proc{|iseq|
- begin
- iseq.to_binary
- rescue RuntimeError # not a toplevel
- # STDERR.puts [:failed, $!, iseq].inspect
- nil
- end
- },
- proc{|bin|
- iseq = RubyVM::InstructionSequence.load_from_binary(bin)
- # STDERR.puts iseq.inspect
- iseq
- }) if CHECK_TO_BINARY
- # return value
- i2_bin if CHECK_TO_BINARY
- end if CHECK_TO_A || CHECK_TO_BINARY
-end
-
-#require_relative 'x'; exit(1)
diff --git a/test/lib/jit_support.rb b/test/lib/jit_support.rb
deleted file mode 100644
index 0759a664..00000000
--- a/test/lib/jit_support.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-module JITSupport
- JIT_TIMEOUT = 600 # 10min for each...
- JIT_SUCCESS_PREFIX = 'JIT success \(\d+\.\dms\)'
- SUPPORTED_COMPILERS = [
- 'gcc',
- 'clang',
- ]
-
- def self.check_support
- # Experimental. If you want to ensure JIT is working with this test, please set this for now.
- if ENV.key?('RUBY_FORCE_TEST_JIT')
- return true
- end
-
- # Very pessimistic check. With this check, we can't ensure JIT is working.
- begin
- _, err = JITSupport.eval_with_jit('proc {}.call', verbose: 1, min_calls: 1, timeout: 10)
- rescue Timeout::Error
- $stderr.puts "TestJIT: #jit_supported? check timed out"
- false
- else
- err.match?(JIT_SUCCESS_PREFIX).tap do |success|
- unless success
- $stderr.puts "TestJIT.check_support stderr:\n```\n#{err}\n```\n"
- end
- end
- end
- end
-
- module_function
- def eval_with_jit(env = nil, script, verbose: 0, min_calls: 5, save_temps: false, timeout: JIT_TIMEOUT)
- args = ['--disable-gems', '--jit-wait', "--jit-verbose=#{verbose}", "--jit-min-calls=#{min_calls}"]
- args << '--jit-save-temps' if save_temps
- args << '-e' << script
- args.unshift(env) if env
- EnvUtil.invoke_ruby(args,
- '', true, true, timeout: timeout,
- )
- end
-
- def supported?
- return @supported if defined?(@supported)
- @supported = JITSupport.check_support.tap do |supported|
- unless supported
- warn "JIT tests are skiped since JIT seems not working. Set RUBY_FORCE_TEST_JIT=1 to let it fail.", uplevel: 1
- end
- end
- end
-
- def remove_mjit_logs(stderr)
- if RubyVM::MJIT.enabled?
- stderr.gsub(/^MJIT warning: Skipped to compile unsupported instruction: \w+\n/m, '')
- else
- stderr
- end
- end
-end
diff --git a/test/lib/leakchecker.rb b/test/lib/leakchecker.rb
deleted file mode 100644
index af9200bf..00000000
--- a/test/lib/leakchecker.rb
+++ /dev/null
@@ -1,240 +0,0 @@
-# frozen_string_literal: true
-class LeakChecker
- def initialize
- @fd_info = find_fds
- @tempfile_info = find_tempfiles
- @thread_info = find_threads
- @env_info = find_env
- @encoding_info = find_encodings
- @old_verbose = $VERBOSE
- end
-
- def check(test_name)
- leaks = [
- check_fd_leak(test_name),
- check_thread_leak(test_name),
- check_tempfile_leak(test_name),
- check_env(test_name),
- check_encodings(test_name),
- check_safe(test_name),
- check_verbose(test_name),
- ]
- GC.start if leaks.any?
- end
-
- def check_safe test_name
- puts "#{test_name}: $SAFE == #{$SAFE}" unless $SAFE == 0
- end
-
- def check_verbose test_name
- puts "#{test_name}: $VERBOSE == #{$VERBOSE}" unless @old_verbose == $VERBOSE
- end
-
- def find_fds
- if IO.respond_to?(:console) and (m = IO.method(:console)).arity.nonzero?
- m[:close]
- end
- fd_dir = "/proc/self/fd"
- if File.directory?(fd_dir)
- fds = Dir.open(fd_dir) {|d|
- a = d.grep(/\A\d+\z/, &:to_i)
- if d.respond_to? :fileno
- a -= [d.fileno]
- end
- a
- }
- fds.sort
- else
- []
- end
- end
-
- def check_fd_leak(test_name)
- leaked = false
- live1 = @fd_info
- live2 = find_fds
- fd_closed = live1 - live2
- if !fd_closed.empty?
- fd_closed.each {|fd|
- puts "Closed file descriptor: #{test_name}: #{fd}"
- }
- end
- fd_leaked = live2 - live1
- if !fd_leaked.empty?
- leaked = true
- h = {}
- ObjectSpace.each_object(IO) {|io|
- inspect = io.inspect
- begin
- autoclose = io.autoclose?
- fd = io.fileno
- rescue IOError # closed IO object
- next
- end
- (h[fd] ||= []) << [io, autoclose, inspect]
- }
- fd_leaked.each {|fd|
- str = ''.dup
- if h[fd]
- str << ' :'
- h[fd].map {|io, autoclose, inspect|
- s = ' ' + inspect
- s << "(not-autoclose)" if !autoclose
- s
- }.sort.each {|s|
- str << s
- }
- end
- puts "Leaked file descriptor: #{test_name}: #{fd}#{str}"
- }
- #system("lsof -p #$$") if !fd_leaked.empty?
- h.each {|fd, list|
- next if list.length <= 1
- if 1 < list.count {|io, autoclose, inspect| autoclose }
- str = list.map {|io, autoclose, inspect| " #{inspect}" + (autoclose ? "(autoclose)" : "") }.sort.join
- puts "Multiple autoclose IO object for a file descriptor:#{str}"
- end
- }
- end
- @fd_info = live2
- return leaked
- end
-
- def extend_tempfile_counter
- return if defined? LeakChecker::TempfileCounter
- m = Module.new {
- @count = 0
- class << self
- attr_accessor :count
- end
-
- def new(data)
- LeakChecker::TempfileCounter.count += 1
- super(data)
- end
- }
- LeakChecker.const_set(:TempfileCounter, m)
-
- class << Tempfile::Remover
- prepend LeakChecker::TempfileCounter
- end
- end
-
- def find_tempfiles(prev_count=-1)
- return [prev_count, []] unless defined? Tempfile
- extend_tempfile_counter
- count = TempfileCounter.count
- if prev_count == count
- [prev_count, []]
- else
- tempfiles = ObjectSpace.each_object(Tempfile).find_all {|t|
- t.instance_variable_defined?(:@tmpfile) and t.path
- }
- [count, tempfiles]
- end
- end
-
- def check_tempfile_leak(test_name)
- return false unless defined? Tempfile
- count1, initial_tempfiles = @tempfile_info
- count2, current_tempfiles = find_tempfiles(count1)
- leaked = false
- tempfiles_leaked = current_tempfiles - initial_tempfiles
- if !tempfiles_leaked.empty?
- leaked = true
- list = tempfiles_leaked.map {|t| t.inspect }.sort
- list.each {|str|
- puts "Leaked tempfile: #{test_name}: #{str}"
- }
- tempfiles_leaked.each {|t| t.close! }
- end
- @tempfile_info = [count2, initial_tempfiles]
- return leaked
- end
-
- def find_threads
- Thread.list.find_all {|t|
- t != Thread.current && t.alive?
- }
- end
-
- def check_thread_leak(test_name)
- live1 = @thread_info
- live2 = find_threads
- thread_finished = live1 - live2
- leaked = false
- if !thread_finished.empty?
- list = thread_finished.map {|t| t.inspect }.sort
- list.each {|str|
- puts "Finished thread: #{test_name}: #{str}"
- }
- end
- thread_leaked = live2 - live1
- if !thread_leaked.empty?
- leaked = true
- list = thread_leaked.map {|t| t.inspect }.sort
- list.each {|str|
- puts "Leaked thread: #{test_name}: #{str}"
- }
- end
- @thread_info = live2
- return leaked
- end
-
- def find_env
- ENV.to_h
- end
-
- def check_env(test_name)
- old_env = @env_info
- new_env = ENV.to_h
- return false if old_env == new_env
- (old_env.keys | new_env.keys).sort.each {|k|
- if old_env.has_key?(k)
- if new_env.has_key?(k)
- if old_env[k] != new_env[k]
- puts "Environment variable changed: #{test_name} : #{k.inspect} changed : #{old_env[k].inspect} -> #{new_env[k].inspect}"
- end
- else
- puts "Environment variable changed: #{test_name} : #{k.inspect} deleted"
- end
- else
- if new_env.has_key?(k)
- puts "Environment variable changed: #{test_name} : #{k.inspect} added"
- else
- flunk "unreachable"
- end
- end
- }
- @env_info = new_env
- return true
- end
-
- def find_encodings
- [Encoding.default_internal, Encoding.default_external]
- end
-
- def check_encodings(test_name)
- old_internal, old_external = @encoding_info
- new_internal, new_external = find_encodings
- leaked = false
- if new_internal != old_internal
- leaked = true
- puts "Encoding.default_internal changed: #{test_name} : #{old_internal.inspect} to #{new_internal.inspect}"
- end
- if new_external != old_external
- leaked = true
- puts "Encoding.default_external changed: #{test_name} : #{old_external.inspect} to #{new_external.inspect}"
- end
- @encoding_info = [new_internal, new_external]
- return leaked
- end
-
- def puts(*a)
- output = MiniTest::Unit.output
- if defined?(output.set_encoding)
- output.set_encoding(nil, nil)
- end
- output.puts(*a)
- end
-end
diff --git a/test/lib/memory_status.rb b/test/lib/memory_status.rb
deleted file mode 100644
index ad002b2d..00000000
--- a/test/lib/memory_status.rb
+++ /dev/null
@@ -1,149 +0,0 @@
-# frozen_string_literal: true
-begin
- require '-test-/memory_status.so'
-rescue LoadError
-end
-
-module Memory
- keys = []
-
- case
- when File.exist?(procfile = "/proc/self/status") && (pat = /^Vm(\w+):\s+(\d+)/) =~ (data = File.binread(procfile))
- PROC_FILE = procfile
- VM_PAT = pat
- def self.read_status
- IO.foreach(PROC_FILE, encoding: Encoding::ASCII_8BIT) do |l|
- yield($1.downcase.intern, $2.to_i * 1024) if VM_PAT =~ l
- end
- end
-
- data.scan(pat) {|k, v| keys << k.downcase.intern}
-
- when /mswin|mingw/ =~ RUBY_PLATFORM
- require 'fiddle/import'
- require 'fiddle/types'
-
- module Win32
- extend Fiddle::Importer
- dlload "kernel32.dll", "psapi.dll"
- include Fiddle::Win32Types
- typealias "SIZE_T", "size_t"
-
- PROCESS_MEMORY_COUNTERS = struct [
- "DWORD cb",
- "DWORD PageFaultCount",
- "SIZE_T PeakWorkingSetSize",
- "SIZE_T WorkingSetSize",
- "SIZE_T QuotaPeakPagedPoolUsage",
- "SIZE_T QuotaPagedPoolUsage",
- "SIZE_T QuotaPeakNonPagedPoolUsage",
- "SIZE_T QuotaNonPagedPoolUsage",
- "SIZE_T PagefileUsage",
- "SIZE_T PeakPagefileUsage",
- ]
-
- typealias "PPROCESS_MEMORY_COUNTERS", "PROCESS_MEMORY_COUNTERS*"
-
- extern "HANDLE GetCurrentProcess()", :stdcall
- extern "BOOL GetProcessMemoryInfo(HANDLE, PPROCESS_MEMORY_COUNTERS, DWORD)", :stdcall
-
- module_function
- def memory_info
- size = PROCESS_MEMORY_COUNTERS.size
- data = PROCESS_MEMORY_COUNTERS.malloc
- data.cb = size
- data if GetProcessMemoryInfo(GetCurrentProcess(), data, size)
- end
- end
-
- keys << :peak << :size
- def self.read_status
- if info = Win32.memory_info
- yield :peak, info.PeakPagefileUsage
- yield :size, info.PagefileUsage
- end
- end
- when (require_relative 'find_executable'
- pat = /^\s*(\d+)\s+(\d+)$/
- pscmd = EnvUtil.find_executable("ps", "-ovsz=", "-orss=", "-p", $$.to_s) {|out| pat =~ out})
- pscmd.pop
- PAT = pat
- PSCMD = pscmd
-
- keys << :size << :rss
- def self.read_status
- if PAT =~ IO.popen(PSCMD + [$$.to_s], "r", err: [:child, :out], &:read)
- yield :size, $1.to_i*1024
- yield :rss, $2.to_i*1024
- end
- end
- else
- def self.read_status
- raise NotImplementedError, "unsupported platform"
- end
- end
-
- if !keys.empty?
- Status = Struct.new(*keys)
- end
-end unless defined?(Memory::Status)
-
-if defined?(Memory::Status)
- class Memory::Status
- def _update
- Memory.read_status do |key, val|
- self[key] = val
- end
- end unless method_defined?(:_update)
-
- Header = members.map {|k| k.to_s.upcase.rjust(6)}.join('')
- Format = "%6d"
-
- def initialize
- _update
- end
-
- def to_s
- status = each_pair.map {|n,v|
- "#{n}:#{v}"
- }
- "{#{status.join(",")}}"
- end
-
- def self.parse(str)
- status = allocate
- str.scan(/(?:\A\{|\G,)(#{members.join('|')}):(\d+)(?=,|\}\z)/) do
- status[$1] = $2.to_i
- end
- status
- end
- end
-
- # On some platforms (e.g. Solaris), libc malloc does not return
- # freed memory to OS because of efficiency, and linking with extra
- # malloc library is needed to detect memory leaks.
- #
- case RUBY_PLATFORM
- when /solaris2\.(?:9|[1-9][0-9])/i # Solaris 9, 10, 11,...
- bits = [nil].pack('p').size == 8 ? 64 : 32
- if ENV['LD_PRELOAD'].to_s.empty? &&
- ENV["LD_PRELOAD_#{bits}"].to_s.empty? &&
- (ENV['UMEM_OPTIONS'].to_s.empty? ||
- ENV['UMEM_OPTIONS'] == 'backend=mmap') then
- envs = {
- 'LD_PRELOAD' => 'libumem.so',
- 'UMEM_OPTIONS' => 'backend=mmap'
- }
- args = [
- envs,
- "--disable=gems",
- "-v", "-",
- ]
- _, err, status = EnvUtil.invoke_ruby(args, "exit(0)", true, true)
- if status.exitstatus == 0 && err.to_s.empty? then
- Memory::NO_MEMORY_LEAK_ENVS = envs
- end
- end
- end #case RUBY_PLATFORM
-
-end
diff --git a/test/lib/minitest/README.txt b/test/lib/minitest/README.txt
deleted file mode 100644
index 368cc3aa..00000000
--- a/test/lib/minitest/README.txt
+++ /dev/null
@@ -1,457 +0,0 @@
-= minitest/{unit,spec,mock,benchmark}
-
-home :: https://github.com/seattlerb/minitest
-rdoc :: http://docs.seattlerb.org/minitest
-vim :: https://github.com/sunaku/vim-ruby-minitest
-
-== DESCRIPTION:
-
-minitest provides a complete suite of testing facilities supporting
-TDD, BDD, mocking, and benchmarking.
-
- "I had a class with Jim Weirich on testing last week and we were
- allowed to choose our testing frameworks. Kirk Haines and I were
- paired up and we cracked open the code for a few test
- frameworks...
-
- I MUST say that minitest is *very* readable / understandable
- compared to the 'other two' options we looked at. Nicely done and
- thank you for helping us keep our mental sanity."
-
- -- Wayne E. Seguin
-
-minitest/unit is a small and incredibly fast unit testing framework.
-It provides a rich set of assertions to make your tests clean and
-readable.
-
-minitest/spec is a functionally complete spec engine. It hooks onto
-minitest/unit and seamlessly bridges test assertions over to spec
-expectations.
-
-minitest/benchmark is an awesome way to assert the performance of your
-algorithms in a repeatable manner. Now you can assert that your newb
-co-worker doesn't replace your linear algorithm with an exponential
-one!
-
-minitest/mock by Steven Baker, is a beautifully tiny mock (and stub)
-object framework.
-
-minitest/pride shows pride in testing and adds coloring to your test
-output. I guess it is an example of how to write IO pipes too. :P
-
-minitest/unit is meant to have a clean implementation for language
-implementors that need a minimal set of methods to bootstrap a working
-test suite. For example, there is no magic involved for test-case
-discovery.
-
- "Again, I can't praise enough the idea of a testing/specing
- framework that I can actually read in full in one sitting!"
-
- -- Piotr Szotkowski
-
-Comparing to rspec:
-
- rspec is a testing DSL. minitest is ruby.
-
- -- Adam Hawkins, "Bow Before MiniTest"
-
-minitest doesn't reinvent anything that ruby already provides, like:
-classes, modules, inheritance, methods. This means you only have to
-learn ruby to use minitest and all of your regular OO practices like
-extract-method refactorings still apply.
-
-== FEATURES/PROBLEMS:
-
-* minitest/autorun - the easy and explicit way to run all your tests.
-* minitest/unit - a very fast, simple, and clean test system.
-* minitest/spec - a very fast, simple, and clean spec system.
-* minitest/mock - a simple and clean mock/stub system.
-* minitest/benchmark - an awesome way to assert your algorithm's performance.
-* minitest/pride - show your pride in testing!
-* Incredibly small and fast runner, but no bells and whistles.
-
-== RATIONALE:
-
-See design_rationale.rb to see how specs and tests work in minitest.
-
-== SYNOPSIS:
-
-Given that you'd like to test the following class:
-
- class Meme
- def i_can_has_cheezburger?
- "OHAI!"
- end
-
- def will_it_blend?
- "YES!"
- end
- end
-
-=== Unit tests
-
- require 'minitest/autorun'
-
- class TestMeme < MiniTest::Unit::TestCase
- def setup
- @meme = Meme.new
- end
-
- def test_that_kitty_can_eat
- assert_equal "OHAI!", @meme.i_can_has_cheezburger?
- end
-
- def test_that_it_will_not_blend
- refute_match /^no/i, @meme.will_it_blend?
- end
-
- def test_that_will_be_skipped
- skip "test this later"
- end
- end
-
-=== Specs
-
- require 'minitest/autorun'
-
- describe Meme do
- before do
- @meme = Meme.new
- end
-
- describe "when asked about cheeseburgers" do
- it "must respond positively" do
- @meme.i_can_has_cheezburger?.must_equal "OHAI!"
- end
- end
-
- describe "when asked about blending possibilities" do
- it "won't say no" do
- @meme.will_it_blend?.wont_match /^no/i
- end
- end
- end
-
-For matchers support check out:
-
-https://github.com/zenspider/minitest-matchers
-
-=== Benchmarks
-
-Add benchmarks to your regular unit tests. If the unit tests fail, the
-benchmarks won't run.
-
- # optionally run benchmarks, good for CI-only work!
- require 'minitest/benchmark' if ENV["BENCH"]
-
- class TestMeme < MiniTest::Unit::TestCase
- # Override self.bench_range or default range is [1, 10, 100, 1_000, 10_000]
- def bench_my_algorithm
- assert_performance_linear 0.9999 do |n| # n is a range value
- @obj.my_algorithm(n)
- end
- end
- end
-
-Or add them to your specs. If you make benchmarks optional, you'll
-need to wrap your benchmarks in a conditional since the methods won't
-be defined.
-
- describe Meme do
- if ENV["BENCH"] then
- bench_performance_linear "my_algorithm", 0.9999 do |n|
- 100.times do
- @obj.my_algorithm(n)
- end
- end
- end
- end
-
-outputs something like:
-
- # Running benchmarks:
-
- TestBlah 100 1000 10000
- bench_my_algorithm 0.006167 0.079279 0.786993
- bench_other_algorithm 0.061679 0.792797 7.869932
-
-Output is tab-delimited to make it easy to paste into a spreadsheet.
-
-=== Mocks
-
- class MemeAsker
- def initialize(meme)
- @meme = meme
- end
-
- def ask(question)
- method = question.tr(" ","_") + "?"
- @meme.__send__(method)
- end
- end
-
- require 'minitest/autorun'
-
- describe MemeAsker do
- before do
- @meme = MiniTest::Mock.new
- @meme_asker = MemeAsker.new @meme
- end
-
- describe "#ask" do
- describe "when passed an unpunctuated question" do
- it "should invoke the appropriate predicate method on the meme" do
- @meme.expect :will_it_blend?, :return_value
- @meme_asker.ask "will it blend"
- @meme.verify
- end
- end
- end
- end
-
-=== Stubs
-
- def test_stale_eh
- obj_under_test = Something.new
-
- refute obj_under_test.stale?
-
- Time.stub :now, Time.at(0) do # stub goes away once the block is done
- assert obj_under_test.stale?
- end
- end
-
-A note on stubbing: In order to stub a method, the method must
-actually exist prior to stubbing. Use a singleton method to create a
-new non-existing method:
-
- def obj_under_test.fake_method
- ...
- end
-
-=== Customizable Test Runner Types:
-
-MiniTest::Unit.runner=(runner) provides an easy way of creating custom
-test runners for specialized needs. Justin Weiss provides the
-following real-world example to create an alternative to regular
-fixture loading:
-
- class MiniTestWithHooks::Unit < MiniTest::Unit
- def before_suites
- end
-
- def after_suites
- end
-
- def _run_suites(suites, type)
- begin
- before_suites
- super(suites, type)
- ensure
- after_suites
- end
- end
-
- def _run_suite(suite, type)
- begin
- suite.before_suite
- super(suite, type)
- ensure
- suite.after_suite
- end
- end
- end
-
- module MiniTestWithTransactions
- class Unit < MiniTestWithHooks::Unit
- include TestSetupHelper
-
- def before_suites
- super
- setup_nested_transactions
- # load any data we want available for all tests
- end
-
- def after_suites
- teardown_nested_transactions
- super
- end
- end
- end
-
- MiniTest::Unit.runner = MiniTestWithTransactions::Unit.new
-
-== FAQ
-
-=== How to test SimpleDelegates?
-
-The following implementation and test:
-
- class Worker < SimpleDelegator
- def work
- end
- end
-
- describe Worker do
- before do
- @worker = Worker.new(Object.new)
- end
-
- it "must respond to work" do
- @worker.must_respond_to :work
- end
- end
-
-outputs a failure:
-
- 1) Failure:
- Worker#test_0001_must respond to work [bug11.rb:16]:
- Expected # (Object) to respond to #work.
-
-Worker is a SimpleDelegate which in 1.9+ is a subclass of BasicObject.
-Expectations are put on Object (one level down) so the Worker
-(SimpleDelegate) hits `method_missing` and delegates down to the
-`Object.new` instance. That object doesn't respond to work so the test
-fails.
-
-You can bypass `SimpleDelegate#method_missing` by extending the worker
-with `MiniTest::Expectations`. You can either do that in your setup at
-the instance level, like:
-
- before do
- @worker = Worker.new(Object.new)
- @worker.extend MiniTest::Expectations
- end
-
-or you can extend the Worker class (within the test file!), like:
-
- class Worker
- include ::MiniTest::Expectations
- end
-
-== Known Extensions:
-
-capybara_minitest_spec :: Bridge between Capybara RSpec matchers and MiniTest::Spec expectations (e.g. page.must_have_content('Title')).
-minispec-metadata :: Metadata for describe/it blocks
- (e.g. `it 'requires JS driver', js: true do`)
-minitest-ansi :: Colorize minitest output with ANSI colors.
-minitest-around :: Around block for minitest. An alternative to setup/teardown dance.
-minitest-capistrano :: Assertions and expectations for testing Capistrano recipes
-minitest-capybara :: Capybara matchers support for minitest unit and spec
-minitest-chef-handler :: Run Minitest suites as Chef report handlers
-minitest-ci :: CI reporter plugin for MiniTest.
-minitest-colorize :: Colorize MiniTest output and show failing tests instantly.
-minitest-context :: Defines contexts for code reuse in MiniTest
- specs that share common expectations.
-minitest-debugger :: Wraps assert so failed assertions drop into
- the ruby debugger.
-minitest-display :: Patches MiniTest to allow for an easily configurable output.
-minitest-emoji :: Print out emoji for your test passes, fails, and skips.
-minitest-english :: Semantically symmetric aliases for assertions and expectations.
-minitest-excludes :: Clean API for excluding certain tests you
- don't want to run under certain conditions.
-minitest-firemock :: Makes your MiniTest mocks more resilient.
-minitest-great_expectations :: Generally useful additions to minitest's assertions and expectations
-minitest-growl :: Test notifier for minitest via growl.
-minitest-implicit-subject :: Implicit declaration of the test subject.
-minitest-instrument :: Instrument ActiveSupport::Notifications when
- test method is executed
-minitest-instrument-db :: Store information about speed of test
- execution provided by minitest-instrument in database
-minitest-libnotify :: Test notifier for minitest via libnotify.
-minitest-macruby :: Provides extensions to minitest for macruby UI testing.
-minitest-matchers :: Adds support for RSpec-style matchers to minitest.
-minitest-metadata :: Annotate tests with metadata (key-value).
-minitest-mongoid :: Mongoid assertion matchers for MiniTest
-minitest-must_not :: Provides must_not as an alias for wont in MiniTest
-minitest-nc :: Test notifier for minitest via Mountain Lion's Notification Center
-minitest-predicates :: Adds support for .predicate? methods
-minitest-rails :: MiniTest integration for Rails 3.x
-minitest-rails-capybara :: Capybara integration for MiniTest::Rails
-minitest-reporters :: Create customizable MiniTest output formats
-minitest-should_syntax :: RSpec-style +x.should == y+ assertions for MiniTest
-minitest-shouldify :: Adding all manner of shoulds to MiniTest (bad idea)
-minitest-spec-context :: Provides rspec-ish context method to MiniTest::Spec
-minitest-spec-magic :: Minitest::Spec extensions for Rails and beyond
-minitest-spec-rails :: Drop in MiniTest::Spec superclass for ActiveSupport::TestCase.
-minitest-stub-const :: Stub constants for the duration of a block
-minitest-tags :: add tags for minitest
-minitest-wscolor :: Yet another test colorizer.
-minitest_owrapper :: Get tests results as a TestResult object.
-minitest_should :: Shoulda style syntax for minitest test::unit.
-minitest_tu_shim :: minitest_tu_shim bridges between test/unit and minitest.
-mongoid-minitest :: MiniTest matchers for Mongoid.
-pry-rescue :: A pry plugin w/ minitest support. See pry-rescue/minitest.rb.
-
-== Unknown Extensions:
-
-Authors... Please send me a pull request with a description of your minitest extension.
-
-* assay-minitest
-* detroit-minitest
-* em-minitest-spec
-* flexmock-minitest
-* guard-minitest
-* guard-minitest-decisiv
-* minitest-activemodel
-* minitest-ar-assertions
-* minitest-capybara-unit
-* minitest-colorer
-* minitest-deluxe
-* minitest-extra-assertions
-* minitest-rails-shoulda
-* minitest-spec
-* minitest-spec-should
-* minitest-sugar
-* minitest_should
-* mongoid-minitest
-* spork-minitest
-
-== REQUIREMENTS:
-
-* Ruby 1.8, maybe even 1.6 or lower. No magic is involved.
-
-== INSTALL:
-
- sudo gem install minitest
-
-On 1.9, you already have it. To get newer candy you can still install
-the gem, but you'll need to activate the gem explicitly to use it:
-
- require 'rubygems'
- gem 'minitest' # ensures you're using the gem, and not the built in MT
- require 'minitest/autorun'
-
- # ... usual testing stuffs ...
-
-DO NOTE: There is a serious problem with the way that ruby 1.9/2.0
-packages their own gems. They install a gem specification file, but
-don't install the gem contents in the gem path. This messes up
-Gem.find_files and many other things (gem which, gem contents, etc).
-
-Just install minitest as a gem for real and you'll be happier.
-
-== LICENSE:
-
-(The MIT License)
-
-Copyright (c) Ryan Davis, seattle.rb
-
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of this software and associated documentation files (the
-'Software'), to deal in the Software without restriction, including
-without limitation the rights to use, copy, modify, merge, publish,
-distribute, sublicense, and/or sell copies of the Software, and to
-permit persons to whom the Software is furnished to do so, subject to
-the following conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
-IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
-CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
-TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/test/lib/minitest/autorun.rb b/test/lib/minitest/autorun.rb
deleted file mode 100644
index 84409662..00000000
--- a/test/lib/minitest/autorun.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# encoding: utf-8
-# frozen_string_literal: true
-
-begin
- require 'rubygems'
- gem 'minitest'
-rescue Gem::LoadError
- # do nothing
-end
-
-require 'minitest/unit'
-require 'minitest/mock'
-
-MiniTest::Unit.autorun
diff --git a/test/lib/minitest/benchmark.rb b/test/lib/minitest/benchmark.rb
deleted file mode 100644
index b3f2bc28..00000000
--- a/test/lib/minitest/benchmark.rb
+++ /dev/null
@@ -1,418 +0,0 @@
-# encoding: utf-8
-# frozen_string_literal: true
-
-require 'minitest/unit'
-
-class MiniTest::Unit # :nodoc:
- def run_benchmarks # :nodoc:
- _run_anything :benchmark
- end
-
- def benchmark_suite_header suite # :nodoc:
- "\n#{suite}\t#{suite.bench_range.join("\t")}"
- end
-
- class TestCase
- ##
- # Returns a set of ranges stepped exponentially from +min+ to
- # +max+ by powers of +base+. Eg:
- #
- # bench_exp(2, 16, 2) # => [2, 4, 8, 16]
-
- def self.bench_exp min, max, base = 10
- min = (Math.log10(min) / Math.log10(base)).to_i
- max = (Math.log10(max) / Math.log10(base)).to_i
-
- (min..max).map { |m| base ** m }.to_a
- end
-
- ##
- # Returns a set of ranges stepped linearly from +min+ to +max+ by
- # +step+. Eg:
- #
- # bench_linear(20, 40, 10) # => [20, 30, 40]
-
- def self.bench_linear min, max, step = 10
- (min..max).step(step).to_a
- rescue LocalJumpError # 1.8.6
- r = []; (min..max).step(step) { |n| r << n }; r
- end
-
- ##
- # Returns the benchmark methods (methods that start with bench_)
- # for that class.
-
- def self.benchmark_methods # :nodoc:
- public_instance_methods(true).grep(/^bench_/).map { |m| m.to_s }.sort
- end
-
- ##
- # Returns all test suites that have benchmark methods.
-
- def self.benchmark_suites
- TestCase.test_suites.reject { |s| s.benchmark_methods.empty? }
- end
-
- ##
- # Specifies the ranges used for benchmarking for that class.
- # Defaults to exponential growth from 1 to 10k by powers of 10.
- # Override if you need different ranges for your benchmarks.
- #
- # See also: ::bench_exp and ::bench_linear.
-
- def self.bench_range
- bench_exp 1, 10_000
- end
-
- ##
- # Runs the given +work+, gathering the times of each run. Range
- # and times are then passed to a given +validation+ proc. Outputs
- # the benchmark name and times in tab-separated format, making it
- # easy to paste into a spreadsheet for graphing or further
- # analysis.
- #
- # Ranges are specified by ::bench_range.
- #
- # Eg:
- #
- # def bench_algorithm
- # validation = proc { |x, y| ... }
- # assert_performance validation do |n|
- # @obj.algorithm(n)
- # end
- # end
-
- def assert_performance validation, &work
- range = self.class.bench_range
-
- io.print "#{__name__}"
-
- times = []
-
- range.each do |x|
- GC.start
- t0 = Time.now
- instance_exec(x, &work)
- t = Time.now - t0
-
- io.print "\t%9.6f" % t
- times << t
- end
- io.puts
-
- validation[range, times]
- end
-
- ##
- # Runs the given +work+ and asserts that the times gathered fit to
- # match a constant rate (eg, linear slope == 0) within a given
- # +threshold+. Note: because we're testing for a slope of 0, R^2
- # is not a good determining factor for the fit, so the threshold
- # is applied against the slope itself. As such, you probably want
- # to tighten it from the default.
- #
- # See http://www.graphpad.com/curvefit/goodness_of_fit.htm for
- # more details.
- #
- # Fit is calculated by #fit_linear.
- #
- # Ranges are specified by ::bench_range.
- #
- # Eg:
- #
- # def bench_algorithm
- # assert_performance_constant 0.9999 do |n|
- # @obj.algorithm(n)
- # end
- # end
-
- def assert_performance_constant threshold = 0.99, &work
- validation = proc do |range, times|
- a, b, rr = fit_linear range, times
- assert_in_delta 0, b, 1 - threshold
- [a, b, rr]
- end
-
- assert_performance validation, &work
- end
-
- ##
- # Runs the given +work+ and asserts that the times gathered fit to
- # match a exponential curve within a given error +threshold+.
- #
- # Fit is calculated by #fit_exponential.
- #
- # Ranges are specified by ::bench_range.
- #
- # Eg:
- #
- # def bench_algorithm
- # assert_performance_exponential 0.9999 do |n|
- # @obj.algorithm(n)
- # end
- # end
-
- def assert_performance_exponential threshold = 0.99, &work
- assert_performance validation_for_fit(:exponential, threshold), &work
- end
-
- ##
- # Runs the given +work+ and asserts that the times gathered fit to
- # match a logarithmic curve within a given error +threshold+.
- #
- # Fit is calculated by #fit_logarithmic.
- #
- # Ranges are specified by ::bench_range.
- #
- # Eg:
- #
- # def bench_algorithm
- # assert_performance_logarithmic 0.9999 do |n|
- # @obj.algorithm(n)
- # end
- # end
-
- def assert_performance_logarithmic threshold = 0.99, &work
- assert_performance validation_for_fit(:logarithmic, threshold), &work
- end
-
- ##
- # Runs the given +work+ and asserts that the times gathered fit to
- # match a straight line within a given error +threshold+.
- #
- # Fit is calculated by #fit_linear.
- #
- # Ranges are specified by ::bench_range.
- #
- # Eg:
- #
- # def bench_algorithm
- # assert_performance_linear 0.9999 do |n|
- # @obj.algorithm(n)
- # end
- # end
-
- def assert_performance_linear threshold = 0.99, &work
- assert_performance validation_for_fit(:linear, threshold), &work
- end
-
- ##
- # Runs the given +work+ and asserts that the times gathered curve
- # fit to match a power curve within a given error +threshold+.
- #
- # Fit is calculated by #fit_power.
- #
- # Ranges are specified by ::bench_range.
- #
- # Eg:
- #
- # def bench_algorithm
- # assert_performance_power 0.9999 do |x|
- # @obj.algorithm
- # end
- # end
-
- def assert_performance_power threshold = 0.99, &work
- assert_performance validation_for_fit(:power, threshold), &work
- end
-
- ##
- # Takes an array of x/y pairs and calculates the general R^2 value.
- #
- # See: http://en.wikipedia.org/wiki/Coefficient_of_determination
-
- def fit_error xys
- y_bar = sigma(xys) { |x, y| y } / xys.size.to_f
- ss_tot = sigma(xys) { |x, y| (y - y_bar) ** 2 }
- ss_err = sigma(xys) { |x, y| (yield(x) - y) ** 2 }
-
- 1 - (ss_err / ss_tot)
- end
-
- ##
- # To fit a functional form: y = ae^(bx).
- #
- # Takes x and y values and returns [a, b, r^2].
- #
- # See: http://mathworld.wolfram.com/LeastSquaresFittingExponential.html
-
- def fit_exponential xs, ys
- n = xs.size
- xys = xs.zip(ys)
- sxlny = sigma(xys) { |x,y| x * Math.log(y) }
- slny = sigma(xys) { |x,y| Math.log(y) }
- sx2 = sigma(xys) { |x,y| x * x }
- sx = sigma xs
-
- c = n * sx2 - sx ** 2
- a = (slny * sx2 - sx * sxlny) / c
- b = ( n * sxlny - sx * slny ) / c
-
- return Math.exp(a), b, fit_error(xys) { |x| Math.exp(a + b * x) }
- end
-
- ##
- # To fit a functional form: y = a + b*ln(x).
- #
- # Takes x and y values and returns [a, b, r^2].
- #
- # See: http://mathworld.wolfram.com/LeastSquaresFittingLogarithmic.html
-
- def fit_logarithmic xs, ys
- n = xs.size
- xys = xs.zip(ys)
- slnx2 = sigma(xys) { |x,y| Math.log(x) ** 2 }
- slnx = sigma(xys) { |x,y| Math.log(x) }
- sylnx = sigma(xys) { |x,y| y * Math.log(x) }
- sy = sigma(xys) { |x,y| y }
-
- c = n * slnx2 - slnx ** 2
- b = ( n * sylnx - sy * slnx ) / c
- a = (sy - b * slnx) / n
-
- return a, b, fit_error(xys) { |x| a + b * Math.log(x) }
- end
-
-
- ##
- # Fits the functional form: a + bx.
- #
- # Takes x and y values and returns [a, b, r^2].
- #
- # See: http://mathworld.wolfram.com/LeastSquaresFitting.html
-
- def fit_linear xs, ys
- n = xs.size
- xys = xs.zip(ys)
- sx = sigma xs
- sy = sigma ys
- sx2 = sigma(xs) { |x| x ** 2 }
- sxy = sigma(xys) { |x,y| x * y }
-
- c = n * sx2 - sx**2
- a = (sy * sx2 - sx * sxy) / c
- b = ( n * sxy - sx * sy ) / c
-
- return a, b, fit_error(xys) { |x| a + b * x }
- end
-
- ##
- # To fit a functional form: y = ax^b.
- #
- # Takes x and y values and returns [a, b, r^2].
- #
- # See: http://mathworld.wolfram.com/LeastSquaresFittingPowerLaw.html
-
- def fit_power xs, ys
- n = xs.size
- xys = xs.zip(ys)
- slnxlny = sigma(xys) { |x, y| Math.log(x) * Math.log(y) }
- slnx = sigma(xs) { |x | Math.log(x) }
- slny = sigma(ys) { | y| Math.log(y) }
- slnx2 = sigma(xs) { |x | Math.log(x) ** 2 }
-
- b = (n * slnxlny - slnx * slny) / (n * slnx2 - slnx ** 2);
- a = (slny - b * slnx) / n
-
- return Math.exp(a), b, fit_error(xys) { |x| (Math.exp(a) * (x ** b)) }
- end
-
- ##
- # Enumerates over +enum+ mapping +block+ if given, returning the
- # sum of the result. Eg:
- #
- # sigma([1, 2, 3]) # => 1 + 2 + 3 => 7
- # sigma([1, 2, 3]) { |n| n ** 2 } # => 1 + 4 + 9 => 14
-
- def sigma enum, &block
- enum = enum.map(&block) if block
- enum.inject { |sum, n| sum + n }
- end
-
- ##
- # Returns a proc that calls the specified fit method and asserts
- # that the error is within a tolerable threshold.
-
- def validation_for_fit msg, threshold
- proc do |range, times|
- a, b, rr = send "fit_#{msg}", range, times
- assert_operator rr, :>=, threshold
- [a, b, rr]
- end
- end
- end
-end
-
-class MiniTest::Spec
- ##
- # This is used to define a new benchmark method. You usually don't
- # use this directly and is intended for those needing to write new
- # performance curve fits (eg: you need a specific polynomial fit).
- #
- # See ::bench_performance_linear for an example of how to use this.
-
- def self.bench name, &block
- define_method "bench_#{name.gsub(/\W+/, '_')}", &block
- end
-
- ##
- # Specifies the ranges used for benchmarking for that class.
- #
- # bench_range do
- # bench_exp(2, 16, 2)
- # end
- #
- # See Unit::TestCase.bench_range for more details.
-
- def self.bench_range &block
- return super unless block
-
- meta = (class << self; self; end)
- meta.send :define_method, "bench_range", &block
- end
-
- ##
- # Create a benchmark that verifies that the performance is linear.
- #
- # describe "my class" do
- # bench_performance_linear "fast_algorithm", 0.9999 do |n|
- # @obj.fast_algorithm(n)
- # end
- # end
-
- def self.bench_performance_linear name, threshold = 0.99, &work
- bench name do
- assert_performance_linear threshold, &work
- end
- end
-
- ##
- # Create a benchmark that verifies that the performance is constant.
- #
- # describe "my class" do
- # bench_performance_constant "zoom_algorithm!" do |n|
- # @obj.zoom_algorithm!(n)
- # end
- # end
-
- def self.bench_performance_constant name, threshold = 0.99, &work
- bench name do
- assert_performance_constant threshold, &work
- end
- end
-
- ##
- # Create a benchmark that verifies that the performance is exponential.
- #
- # describe "my class" do
- # bench_performance_exponential "algorithm" do |n|
- # @obj.algorithm(n)
- # end
- # end
-
- def self.bench_performance_exponential name, threshold = 0.99, &work
- bench name do
- assert_performance_exponential threshold, &work
- end
- end
-end
diff --git a/test/lib/minitest/mock.rb b/test/lib/minitest/mock.rb
deleted file mode 100644
index 224b06cb..00000000
--- a/test/lib/minitest/mock.rb
+++ /dev/null
@@ -1,196 +0,0 @@
-# encoding: utf-8
-# frozen_string_literal: true
-
-class MockExpectationError < StandardError; end # :nodoc:
-
-##
-# A simple and clean mock object framework.
-
-module MiniTest # :nodoc:
-
- ##
- # All mock objects are an instance of Mock
-
- class Mock
- alias :__respond_to? :respond_to?
-
- skip_methods = %w(object_id respond_to_missing? inspect === to_s)
-
- instance_methods.each do |m|
- undef_method m unless skip_methods.include?(m.to_s) || m =~ /^__/
- end
-
- def initialize # :nodoc:
- @expected_calls = Hash.new { |calls, name| calls[name] = [] }
- @actual_calls = Hash.new { |calls, name| calls[name] = [] }
- end
-
- ##
- # Expect that method +name+ is called, optionally with +args+ or a
- # +blk+, and returns +retval+.
- #
- # @mock.expect(:meaning_of_life, 42)
- # @mock.meaning_of_life # => 42
- #
- # @mock.expect(:do_something_with, true, [some_obj, true])
- # @mock.do_something_with(some_obj, true) # => true
- #
- # @mock.expect(:do_something_else, true) do |a1, a2|
- # a1 == "buggs" && a2 == :bunny
- # end
- #
- # +args+ is compared to the expected args using case equality (ie, the
- # '===' operator), allowing for less specific expectations.
- #
- # @mock.expect(:uses_any_string, true, [String])
- # @mock.uses_any_string("foo") # => true
- # @mock.verify # => true
- #
- # @mock.expect(:uses_one_string, true, ["foo"]
- # @mock.uses_one_string("bar") # => true
- # @mock.verify # => raises MockExpectationError
-
- def expect(name, retval, args=[], &blk)
- if block_given?
- raise ArgumentError, "args ignored when block given" unless args.empty?
- @expected_calls[name] << { :retval => retval, :block => blk }
- else
- raise ArgumentError, "args must be an array" unless Array === args
- @expected_calls[name] << { :retval => retval, :args => args }
- end
- self
- end
-
- def __call name, data # :nodoc:
- case data
- when Hash then
- "#{name}(#{data[:args].inspect[1..-2]}) => #{data[:retval].inspect}"
- else
- data.map { |d| __call name, d }.join ", "
- end
- end
-
- ##
- # Verify that all methods were called as expected. Raises
- # +MockExpectationError+ if the mock object was not called as
- # expected.
-
- def verify
- @expected_calls.each do |name, calls|
- calls.each do |expected|
- msg1 = "expected #{__call name, expected}"
- msg2 = "#{msg1}, got [#{__call name, @actual_calls[name]}]"
-
- raise MockExpectationError, msg2 if
- @actual_calls.has_key?(name) and
- not @actual_calls[name].include?(expected)
-
- raise MockExpectationError, msg1 unless
- @actual_calls.has_key?(name) and
- @actual_calls[name].include?(expected)
- end
- end
- true
- end
-
- def method_missing(sym, *args) # :nodoc:
- unless @expected_calls.has_key?(sym) then
- raise NoMethodError, "unmocked method %p, expected one of %p" %
- [sym, @expected_calls.keys.sort_by(&:to_s)]
- end
-
- index = @actual_calls[sym].length
- expected_call = @expected_calls[sym][index]
-
- unless expected_call then
- raise MockExpectationError, "No more expects available for %p: %p" %
- [sym, args]
- end
-
- expected_args, retval, val_block =
- expected_call.values_at(:args, :retval, :block)
-
- if val_block then
- raise MockExpectationError, "mocked method %p failed block w/ %p" %
- [sym, args] unless val_block.call(args)
-
- # keep "verify" happy
- @actual_calls[sym] << expected_call
- return retval
- end
-
- if expected_args.size != args.size then
- raise ArgumentError, "mocked method %p expects %d arguments, got %d" %
- [sym, expected_args.size, args.size]
- end
-
- fully_matched = expected_args.zip(args).all? { |mod, a|
- mod === a or mod == a
- }
-
- unless fully_matched then
- raise MockExpectationError, "mocked method %p called with unexpected arguments %p" %
- [sym, args]
- end
-
- @actual_calls[sym] << {
- :retval => retval,
- :args => expected_args.zip(args).map { |mod, a| mod === a ? mod : a }
- }
-
- retval
- end
-
- def respond_to?(sym, include_private = false) # :nodoc:
- return true if @expected_calls.has_key?(sym.to_sym)
- return __respond_to?(sym, include_private)
- end
- end
-end
-
-class Object # :nodoc:
-
- ##
- # Add a temporary stubbed method replacing +name+ for the duration
- # of the +block+. If +val_or_callable+ responds to #call, then it
- # returns the result of calling it, otherwise returns the value
- # as-is. Cleans up the stub at the end of the +block+. The method
- # +name+ must exist before stubbing.
- #
- # def test_stale_eh
- # obj_under_test = Something.new
- # refute obj_under_test.stale?
- #
- # Time.stub :now, Time.at(0) do
- # assert obj_under_test.stale?
- # end
- # end
-
- def stub name, val_or_callable, &block
- new_name = "__minitest_stub__#{name}"
-
- metaclass = class << self; self; end
-
- if respond_to? name and not methods.map(&:to_s).include? name.to_s then
- metaclass.send :define_method, name do |*args|
- super(*args)
- end
- end
-
- metaclass.send :alias_method, new_name, name
-
- metaclass.send :define_method, name do |*args|
- if val_or_callable.respond_to? :call then
- val_or_callable.call(*args)
- else
- val_or_callable
- end
- end
-
- yield self
- ensure
- metaclass.send :undef_method, name
- metaclass.send :alias_method, name, new_name
- metaclass.send :undef_method, new_name
- end
-end
diff --git a/test/lib/minitest/unit.rb b/test/lib/minitest/unit.rb
deleted file mode 100644
index 88daaafc..00000000
--- a/test/lib/minitest/unit.rb
+++ /dev/null
@@ -1,1416 +0,0 @@
-# encoding: utf-8
-# frozen_string_literal: true
-
-require "optparse"
-require "rbconfig"
-require "leakchecker"
-
-##
-# Minimal (mostly drop-in) replacement for test-unit.
-#
-# :include: README.txt
-
-module MiniTest
-
- def self.const_missing name # :nodoc:
- case name
- when :MINI_DIR then
- msg = "MiniTest::MINI_DIR was removed. Don't violate other's internals."
- warn "WAR\NING: #{msg}"
- warn "WAR\NING: Used by #{caller.first}."
- const_set :MINI_DIR, "bad value"
- else
- super
- end
- end
-
- ##
- # Assertion base class
-
- class Assertion < Exception; end
-
- ##
- # Assertion raised when skipping a test
-
- class Skip < Assertion; end
-
- class << self
- ##
- # Filter object for backtraces.
-
- attr_accessor :backtrace_filter
- end
-
- class BacktraceFilter # :nodoc:
- def filter bt
- return ["No backtrace"] unless bt
-
- new_bt = []
-
- unless $DEBUG then
- bt.each do |line|
- break if line =~ /lib\/minitest/
- new_bt << line
- end
-
- new_bt = bt.reject { |line| line =~ /lib\/minitest/ } if new_bt.empty?
- new_bt = bt.dup if new_bt.empty?
- else
- new_bt = bt.dup
- end
-
- new_bt
- end
- end
-
- self.backtrace_filter = BacktraceFilter.new
-
- def self.filter_backtrace bt # :nodoc:
- backtrace_filter.filter bt
- end
-
- ##
- # MiniTest Assertions. All assertion methods accept a +msg+ which is
- # printed if the assertion fails.
-
- module Assertions
- ##
- # Returns the diff command to use in #diff. Tries to intelligently
- # figure out what diff to use.
-
- def self.diff
- @diff = if (RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ &&
- system("diff.exe", __FILE__, __FILE__)) then
- "diff.exe -u"
- elsif Minitest::Unit::Guard.maglev? then # HACK
- "diff -u"
- elsif system("gdiff", __FILE__, __FILE__)
- "gdiff -u" # solaris and kin suck
- elsif system("diff", __FILE__, __FILE__)
- "diff -u"
- else
- nil
- end unless defined? @diff
-
- @diff
- end
-
- ##
- # Set the diff command to use in #diff.
-
- def self.diff= o
- @diff = o
- end
-
- ##
- # Returns a diff between +exp+ and +act+. If there is no known
- # diff command or if it doesn't make sense to diff the output
- # (single line, short output), then it simply returns a basic
- # comparison between the two.
-
- def diff exp, act
- require "tempfile"
-
- expect = mu_pp_for_diff exp
- butwas = mu_pp_for_diff act
- result = nil
-
- need_to_diff =
- MiniTest::Assertions.diff &&
- (expect.include?("\n") ||
- butwas.include?("\n") ||
- expect.size > 30 ||
- butwas.size > 30 ||
- expect == butwas)
-
- return "Expected: #{mu_pp exp}\n Actual: #{mu_pp act}" unless
- need_to_diff
-
- tempfile_a = nil
- tempfile_b = nil
-
- Tempfile.open("expect") do |a|
- tempfile_a = a
- a.puts expect
- a.flush
-
- Tempfile.open("butwas") do |b|
- tempfile_b = b
- b.puts butwas
- b.flush
-
- result = `#{MiniTest::Assertions.diff} #{a.path} #{b.path}`
- result.sub!(/^\-\-\- .+/, "--- expected")
- result.sub!(/^\+\+\+ .+/, "+++ actual")
-
- if result.empty? then
- klass = exp.class
- result = [
- "No visible difference in the #{klass}#inspect output.\n",
- "You should look at the implementation of #== on ",
- "#{klass} or its members.\n",
- expect,
- ].join
- end
- end
- end
-
- result
- ensure
- tempfile_a.close! if tempfile_a
- tempfile_b.close! if tempfile_b
- end
-
- ##
- # This returns a human-readable version of +obj+. By default
- # #inspect is called. You can override this to use #pretty_print
- # if you want.
-
- def mu_pp obj
- s = obj.inspect
- s = s.encode Encoding.default_external if defined? Encoding
- s
- end
-
- ##
- # This returns a diff-able human-readable version of +obj+. This
- # differs from the regular mu_pp because it expands escaped
- # newlines and makes hex-values generic (like object_ids). This
- # uses mu_pp to do the first pass and then cleans it up.
-
- def mu_pp_for_diff obj
- mu_pp(obj).gsub(/\\n/, "\n").gsub(/:0x[a-fA-F0-9]{4,}/m, ':0xXXXXXX')
- end
-
- def _assertions= n # :nodoc:
- @_assertions = n
- end
-
- def _assertions # :nodoc:
- @_assertions ||= 0
- end
-
- ##
- # Fails unless +test+ is a true value.
-
- def assert test, msg = nil
- msg ||= "Failed assertion, no message given."
- self._assertions += 1
- unless test then
- msg = msg.call if Proc === msg
- raise MiniTest::Assertion, msg
- end
- true
- end
-
- ##
- # Fails unless +obj+ is empty.
-
- def assert_empty obj, msg = nil
- msg = message(msg) { "Expected #{mu_pp(obj)} to be empty" }
- assert_respond_to obj, :empty?
- assert obj.empty?, msg
- end
-
- ##
- # Fails unless exp == act printing the difference between
- # the two, if possible.
- #
- # If there is no visible difference but the assertion fails, you
- # should suspect that your #== is buggy, or your inspect output is
- # missing crucial details.
- #
- # For floats use assert_in_delta.
- #
- # See also: MiniTest::Assertions.diff
-
- def assert_equal exp, act, msg = nil
- msg = message(msg, "") { diff exp, act }
- assert exp == act, msg
- end
-
- ##
- # For comparing Floats. Fails unless +exp+ and +act+ are within +delta+
- # of each other.
- #
- # assert_in_delta Math::PI, (22.0 / 7.0), 0.01
-
- def assert_in_delta exp, act, delta = 0.001, msg = nil
- n = (exp - act).abs
- msg = message(msg) {
- "Expected |#{exp} - #{act}| (#{n}) to be <= #{delta}"
- }
- assert delta >= n, msg
- end
-
- ##
- # For comparing Floats. Fails unless +exp+ and +act+ have a relative
- # error less than +epsilon+.
-
- def assert_in_epsilon a, b, epsilon = 0.001, msg = nil
- assert_in_delta a, b, [a.abs, b.abs].min * epsilon, msg
- end
-
- ##
- # Fails unless +collection+ includes +obj+.
-
- def assert_includes collection, obj, msg = nil
- msg = message(msg) {
- "Expected #{mu_pp(collection)} to include #{mu_pp(obj)}"
- }
- assert_respond_to collection, :include?
- assert collection.include?(obj), msg
- end
-
- ##
- # Fails unless +obj+ is an instance of +cls+.
-
- def assert_instance_of cls, obj, msg = nil
- msg = message(msg) {
- "Expected #{mu_pp(obj)} to be an instance of #{cls}, not #{obj.class}"
- }
-
- assert obj.instance_of?(cls), msg
- end
-
- ##
- # Fails unless +obj+ is a kind of +cls+.
-
- def assert_kind_of cls, obj, msg = nil # TODO: merge with instance_of
- msg = message(msg) {
- "Expected #{mu_pp(obj)} to be a kind of #{cls}, not #{obj.class}" }
-
- assert obj.kind_of?(cls), msg
- end
-
- ##
- # Fails unless +matcher+ =~ +obj+.
-
- def assert_match matcher, obj, msg = nil
- msg = message(msg) { "Expected #{mu_pp matcher} to match #{mu_pp obj}" }
- assert_respond_to matcher, :"=~"
- matcher = Regexp.new Regexp.escape matcher if String === matcher
- assert matcher =~ obj, msg
- end
-
- ##
- # Fails unless +obj+ is nil
-
- def assert_nil obj, msg = nil
- msg = message(msg) { "Expected #{mu_pp(obj)} to be nil" }
- assert obj.nil?, msg
- end
-
- ##
- # For testing with binary operators.
- #
- # assert_operator 5, :<=, 4
-
- def assert_operator o1, op, o2 = (predicate = true; nil), msg = nil
- return assert_predicate o1, op, msg if predicate
- msg = message(msg) { "Expected #{mu_pp(o1)} to be #{op} #{mu_pp(o2)}" }
- assert o1.__send__(op, o2), msg
- end
-
- ##
- # Fails if stdout or stderr do not output the expected results.
- # Pass in nil if you don't care about that streams output. Pass in
- # "" if you require it to be silent. Pass in a regexp if you want
- # to pattern match.
- #
- # NOTE: this uses #capture_io, not #capture_subprocess_io.
- #
- # See also: #assert_silent
-
- def assert_output stdout = nil, stderr = nil
- out, err = capture_io do
- yield
- end
-
- err_msg = Regexp === stderr ? :assert_match : :assert_equal if stderr
- out_msg = Regexp === stdout ? :assert_match : :assert_equal if stdout
-
- y = send err_msg, stderr, err, "In stderr" if err_msg
- x = send out_msg, stdout, out, "In stdout" if out_msg
-
- (!stdout || x) && (!stderr || y)
- end
-
- ##
- # For testing with predicates.
- #
- # assert_predicate str, :empty?
- #
- # This is really meant for specs and is front-ended by assert_operator:
- #
- # str.must_be :empty?
-
- def assert_predicate o1, op, msg = nil
- msg = message(msg) { "Expected #{mu_pp(o1)} to be #{op}" }
- assert o1.__send__(op), msg
- end
-
- ##
- # Fails unless the block raises one of +exp+. Returns the
- # exception matched so you can check the message, attributes, etc.
-
- def assert_raises *exp
- msg = "#{exp.pop}.\n" if String === exp.last
-
- begin
- yield
- rescue MiniTest::Skip => e
- return e if exp.include? MiniTest::Skip
- raise e
- rescue Exception => e
- expected = exp.any? { |ex|
- if ex.instance_of? Module then
- e.kind_of? ex
- else
- e.instance_of? ex
- end
- }
-
- assert expected, proc {
- exception_details(e, "#{msg}#{mu_pp(exp)} exception expected, not")
- }
-
- return e
- end
-
- exp = exp.first if exp.size == 1
-
- flunk "#{msg}#{mu_pp(exp)} expected but nothing was raised."
- end
-
- ##
- # Fails unless +obj+ responds to +meth+.
-
- def assert_respond_to obj, meth, msg = nil
- msg = message(msg) {
- "Expected #{mu_pp(obj)} (#{obj.class}) to respond to ##{meth}"
- }
- assert obj.respond_to?(meth), msg
- end
-
- ##
- # Fails unless +exp+ and +act+ are #equal?
-
- def assert_same exp, act, msg = nil
- msg = message(msg) {
- data = [mu_pp(act), act.object_id, mu_pp(exp), exp.object_id]
- "Expected %s (oid=%d) to be the same as %s (oid=%d)" % data
- }
- assert exp.equal?(act), msg
- end
-
- ##
- # +send_ary+ is a receiver, message and arguments.
- #
- # Fails unless the call returns a true value
- # TODO: I should prolly remove this from specs
-
- def assert_send send_ary, m = nil
- recv, msg, *args = send_ary
- m = message(m) {
- "Expected #{mu_pp(recv)}.#{msg}(*#{mu_pp(args)}) to return true" }
- assert recv.__send__(msg, *args), m
- end
-
- ##
- # Fails if the block outputs anything to stderr or stdout.
- #
- # See also: #assert_output
-
- def assert_silent
- assert_output "", "" do
- yield
- end
- end
-
- ##
- # Fails unless the block throws +sym+
-
- def assert_throws sym, msg = nil
- default = "Expected #{mu_pp(sym)} to have been thrown"
- caught = true
- catch(sym) do
- begin
- yield
- rescue ThreadError => e # wtf?!? 1.8 + threads == suck
- default += ", not \:#{e.message[/uncaught throw \`(\w+?)\'/, 1]}"
- rescue ArgumentError => e # 1.9 exception
- default += ", not #{e.message.split(/ /).last}"
- rescue NameError => e # 1.8 exception
- default += ", not #{e.name.inspect}"
- end
- caught = false
- end
-
- assert caught, message(msg) { default }
- end
-
- ##
- # Captures $stdout and $stderr into strings:
- #
- # out, err = capture_io do
- # puts "Some info"
- # warn "You did a bad thing"
- # end
- #
- # assert_match %r%info%, out
- # assert_match %r%bad%, err
- #
- # NOTE: For efficiency, this method uses StringIO and does not
- # capture IO for subprocesses. Use #capture_subprocess_io for
- # that.
-
- def capture_io
- require 'stringio'
-
- captured_stdout, captured_stderr = StringIO.new, StringIO.new
-
- synchronize do
- orig_stdout, orig_stderr = $stdout, $stderr
- $stdout, $stderr = captured_stdout, captured_stderr
-
- begin
- yield
- ensure
- $stdout = orig_stdout
- $stderr = orig_stderr
- end
- end
-
- return captured_stdout.string, captured_stderr.string
- end
-
- ##
- # Captures $stdout and $stderr into strings, using Tempfile to
- # ensure that subprocess IO is captured as well.
- #
- # out, err = capture_subprocess_io do
- # system "echo Some info"
- # system "echo You did a bad thing 1>&2"
- # end
- #
- # assert_match %r%info%, out
- # assert_match %r%bad%, err
- #
- # NOTE: This method is approximately 10x slower than #capture_io so
- # only use it when you need to test the output of a subprocess.
-
- def capture_subprocess_io
- require 'tempfile'
-
- captured_stdout, captured_stderr = Tempfile.new("out"), Tempfile.new("err")
-
- synchronize do
- orig_stdout, orig_stderr = $stdout.dup, $stderr.dup
- $stdout.reopen captured_stdout
- $stderr.reopen captured_stderr
-
- begin
- yield
-
- $stdout.rewind
- $stderr.rewind
-
- [captured_stdout.read, captured_stderr.read]
- ensure
- $stdout.reopen orig_stdout
- $stderr.reopen orig_stderr
- orig_stdout.close
- orig_stderr.close
- captured_stdout.close!
- captured_stderr.close!
- end
- end
- end
-
- ##
- # Returns details for exception +e+
-
- def exception_details e, msg
- [
- "#{msg}",
- "Class: <#{e.class}>",
- "Message: <#{e.message.inspect}>",
- "---Backtrace---",
- "#{MiniTest::filter_backtrace(e.backtrace).join("\n")}",
- "---------------",
- ].join "\n"
- end
-
- ##
- # Fails with +msg+
-
- def flunk msg = nil
- msg ||= "Epic Fail!"
- assert false, msg
- end
-
- ##
- # Returns a proc that will output +msg+ along with the default message.
-
- def message msg = nil, ending = ".", &default
- proc {
- msg = msg.call.chomp(".") if Proc === msg
- custom_message = "#{msg}.\n" unless msg.nil? or msg.to_s.empty?
- "#{custom_message}#{default.call}#{ending}"
- }
- end
-
- ##
- # used for counting assertions
-
- def pass msg = nil
- assert true
- end
-
- ##
- # Fails if +test+ is a true value
-
- def refute test, msg = nil
- msg ||= "Failed refutation, no message given"
- not assert(! test, msg)
- end
-
- ##
- # Fails if +obj+ is empty.
-
- def refute_empty obj, msg = nil
- msg = message(msg) { "Expected #{mu_pp(obj)} to not be empty" }
- assert_respond_to obj, :empty?
- refute obj.empty?, msg
- end
-
- ##
- # Fails if exp == act.
- #
- # For floats use refute_in_delta.
-
- def refute_equal exp, act, msg = nil
- msg = message(msg) {
- "Expected #{mu_pp(act)} to not be equal to #{mu_pp(exp)}"
- }
- refute exp == act, msg
- end
-
- ##
- # For comparing Floats. Fails if +exp+ is within +delta+ of +act+.
- #
- # refute_in_delta Math::PI, (22.0 / 7.0)
-
- def refute_in_delta exp, act, delta = 0.001, msg = nil
- n = (exp - act).abs
- msg = message(msg) {
- "Expected |#{exp} - #{act}| (#{n}) to not be <= #{delta}"
- }
- refute delta >= n, msg
- end
-
- ##
- # For comparing Floats. Fails if +exp+ and +act+ have a relative error
- # less than +epsilon+.
-
- def refute_in_epsilon a, b, epsilon = 0.001, msg = nil
- refute_in_delta a, b, a * epsilon, msg
- end
-
- ##
- # Fails if +collection+ includes +obj+.
-
- def refute_includes collection, obj, msg = nil
- msg = message(msg) {
- "Expected #{mu_pp(collection)} to not include #{mu_pp(obj)}"
- }
- assert_respond_to collection, :include?
- refute collection.include?(obj), msg
- end
-
- ##
- # Fails if +obj+ is an instance of +cls+.
-
- def refute_instance_of cls, obj, msg = nil
- msg = message(msg) {
- "Expected #{mu_pp(obj)} to not be an instance of #{cls}"
- }
- refute obj.instance_of?(cls), msg
- end
-
- ##
- # Fails if +obj+ is a kind of +cls+.
-
- def refute_kind_of cls, obj, msg = nil # TODO: merge with instance_of
- msg = message(msg) { "Expected #{mu_pp(obj)} to not be a kind of #{cls}" }
- refute obj.kind_of?(cls), msg
- end
-
- ##
- # Fails if +matcher+ =~ +obj+.
-
- def refute_match matcher, obj, msg = nil
- msg = message(msg) {"Expected #{mu_pp matcher} to not match #{mu_pp obj}"}
- assert_respond_to matcher, :"=~"
- matcher = Regexp.new Regexp.escape matcher if String === matcher
- refute matcher =~ obj, msg
- end
-
- ##
- # Fails if +obj+ is nil.
-
- def refute_nil obj, msg = nil
- msg = message(msg) { "Expected #{mu_pp(obj)} to not be nil" }
- refute obj.nil?, msg
- end
-
- ##
- # Fails if +o1+ is not +op+ +o2+. Eg:
- #
- # refute_operator 1, :>, 2 #=> pass
- # refute_operator 1, :<, 2 #=> fail
-
- def refute_operator o1, op, o2 = (predicate = true; nil), msg = nil
- return refute_predicate o1, op, msg if predicate
- msg = message(msg) { "Expected #{mu_pp(o1)} to not be #{op} #{mu_pp(o2)}"}
- refute o1.__send__(op, o2), msg
- end
-
- ##
- # For testing with predicates.
- #
- # refute_predicate str, :empty?
- #
- # This is really meant for specs and is front-ended by refute_operator:
- #
- # str.wont_be :empty?
-
- def refute_predicate o1, op, msg = nil
- msg = message(msg) { "Expected #{mu_pp(o1)} to not be #{op}" }
- refute o1.__send__(op), msg
- end
-
- ##
- # Fails if +obj+ responds to the message +meth+.
-
- def refute_respond_to obj, meth, msg = nil
- msg = message(msg) { "Expected #{mu_pp(obj)} to not respond to #{meth}" }
-
- refute obj.respond_to?(meth), msg
- end
-
- ##
- # Fails if +exp+ is the same (by object identity) as +act+.
-
- def refute_same exp, act, msg = nil
- msg = message(msg) {
- data = [mu_pp(act), act.object_id, mu_pp(exp), exp.object_id]
- "Expected %s (oid=%d) to not be the same as %s (oid=%d)" % data
- }
- refute exp.equal?(act), msg
- end
-
- ##
- # Skips the current test. Gets listed at the end of the run but
- # doesn't cause a failure exit code.
-
- def skip msg = nil, bt = caller
- msg ||= "Skipped, no message given"
- @skip = true
- raise MiniTest::Skip, msg, bt
- end
-
- ##
- # Was this testcase skipped? Meant for #teardown.
-
- def skipped?
- defined?(@skip) and @skip
- end
-
- ##
- # Takes a block and wraps it with the runner's shared mutex.
-
- def synchronize
- Minitest::Unit.runner.synchronize do
- yield
- end
- end
- end
-
- class Unit # :nodoc:
- VERSION = "4.7.5" # :nodoc:
-
- attr_accessor :report, :failures, :errors, :skips # :nodoc:
- attr_accessor :assertion_count # :nodoc:
- attr_writer :test_count # :nodoc:
- attr_accessor :start_time # :nodoc:
- attr_accessor :help # :nodoc:
- attr_accessor :verbose # :nodoc:
- attr_writer :options # :nodoc:
-
- ##
- # :attr:
- #
- # if true, installs an "INFO" signal handler (only available to BSD and
- # OS X users) which prints diagnostic information about the test run.
- #
- # This is auto-detected by default but may be overridden by custom
- # runners.
-
- attr_accessor :info_signal
-
- ##
- # Lazy accessor for options.
-
- def options
- @options ||= {}
- end
-
- @@installed_at_exit ||= false
- @@out = $stdout
- @@after_tests = []
-
- ##
- # A simple hook allowing you to run a block of code after _all_ of
- # the tests are done. Eg:
- #
- # MiniTest::Unit.after_tests { p $debugging_info }
-
- def self.after_tests &block
- @@after_tests << block
- end
-
- ##
- # Registers MiniTest::Unit to run tests at process exit
-
- def self.autorun
- at_exit {
- # don't run if there was a non-exit exception
- next if $! and not $!.kind_of? SystemExit
-
- # the order here is important. The at_exit handler must be
- # installed before anyone else gets a chance to install their
- # own, that way we can be assured that our exit will be last
- # to run (at_exit stacks).
- exit_code = nil
-
- at_exit {
- @@after_tests.reverse_each(&:call)
- exit false if exit_code && exit_code != 0
- }
-
- exit_code = MiniTest::Unit.new.run ARGV
- } unless @@installed_at_exit
- @@installed_at_exit = true
- end
-
- ##
- # Returns the stream to use for output.
-
- def self.output
- @@out
- end
-
- ##
- # Sets MiniTest::Unit to write output to +stream+. $stdout is the default
- # output
-
- def self.output= stream
- @@out = stream
- end
-
- ##
- # Tells MiniTest::Unit to delegate to +runner+, an instance of a
- # MiniTest::Unit subclass, when MiniTest::Unit#run is called.
-
- def self.runner= runner
- @@runner = runner
- end
-
- ##
- # Returns the MiniTest::Unit subclass instance that will be used
- # to run the tests. A MiniTest::Unit instance is the default
- # runner.
-
- def self.runner
- @@runner ||= self.new
- end
-
- ##
- # Return all plugins' run methods (methods that start with "run_").
-
- def self.plugins
- @@plugins ||= (["run_tests"] +
- public_instance_methods(false).
- grep(/^run_/).map { |s| s.to_s }).uniq
- end
-
- ##
- # Return the IO for output.
-
- def output
- self.class.output
- end
-
- def puts *a # :nodoc:
- output.puts(*a)
- end
-
- def print *a # :nodoc:
- output.print(*a)
- end
-
- def test_count # :nodoc:
- @test_count ||= 0
- end
-
- ##
- # Runner for a given +type+ (eg, test vs bench).
-
- def _run_anything type
- suites = TestCase.send "#{type}_suites"
- return if suites.empty?
-
- puts
- puts "# Running #{type}s:"
- puts
-
- @test_count, @assertion_count = 0, 0
- test_count = assertion_count = 0
- sync = output.respond_to? :"sync=" # stupid emacs
- old_sync, output.sync = output.sync, true if sync
-
- count = 0
- begin
- start = Time.now
-
- results = _run_suites suites, type
-
- @test_count = results.inject(0) { |sum, (tc, _)| sum + tc }
- @assertion_count = results.inject(0) { |sum, (_, ac)| sum + ac }
- test_count += @test_count
- assertion_count += @assertion_count
- t = Time.now - start
- count += 1
- unless @repeat_count
- puts
- puts
- end
- puts "Finished%s %ss in %.6fs, %.4f tests/s, %.4f assertions/s.\n" %
- [(@repeat_count ? "(#{count}/#{@repeat_count}) " : ""), type,
- t, @test_count.fdiv(t), @assertion_count.fdiv(t)]
- end while @repeat_count && count < @repeat_count &&
- report.empty? && failures.zero? && errors.zero?
-
- output.sync = old_sync if sync
-
- report.each_with_index do |msg, i|
- puts "\n%3d) %s" % [i + 1, msg]
- end
-
- puts
- @test_count = test_count
- @assertion_count = assertion_count
-
- status
- end
-
- ##
- # Runs all the +suites+ for a given +type+.
- #
-
- def _run_suites suites, type
- suites.map { |suite| _run_suite suite, type }
- end
-
- ##
- # Run a single +suite+ for a given +type+.
-
- def _run_suite suite, type
- header = "#{type}_suite_header"
- puts send(header, suite) if respond_to? header
-
- filter = options[:filter] || '/./'
- filter = Regexp.new $1 if filter =~ /\/(.*)\//
-
- all_test_methods = suite.send "#{type}_methods"
-
- filtered_test_methods = all_test_methods.find_all { |m|
- filter === m || filter === "#{suite}##{m}"
- }
-
- leakchecker = LeakChecker.new
-
- assertions = filtered_test_methods.map { |method|
- inst = suite.new method
- inst._assertions = 0
-
- print "#{suite}##{method} = " if @verbose
-
- start_time = Time.now if @verbose
- result = inst.run self
-
- print "%.2f s = " % (Time.now - start_time) if @verbose
- print result
- puts if @verbose
- $stdout.flush
-
- if !(defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled?) # compiler process is wrongly considered as leaked
- leakchecker.check("#{inst.class}\##{inst.__name__}")
- end
-
- inst._assertions
- }
-
- return assertions.size, assertions.inject(0) { |sum, n| sum + n }
- end
-
- ##
- # Record the result of a single test. Makes it very easy to gather
- # information. Eg:
- #
- # class StatisticsRecorder < MiniTest::Unit
- # def record suite, method, assertions, time, error
- # # ... record the results somewhere ...
- # end
- # end
- #
- # MiniTest::Unit.runner = StatisticsRecorder.new
- #
- # NOTE: record might be sent more than once per test. It will be
- # sent once with the results from the test itself. If there is a
- # failure or error in teardown, it will be sent again with the
- # error or failure.
-
- def record suite, method, assertions, time, error
- end
-
- def location e # :nodoc:
- last_before_assertion = ""
- e.backtrace.reverse_each do |s|
- break if s =~ /in .(assert|refute|flunk|pass|fail|raise|must|wont)/
- last_before_assertion = s
- end
- last_before_assertion.sub(/:in .*$/, '')
- end
-
- ##
- # Writes status for failed test +meth+ in +klass+ which finished with
- # exception +e+
-
- def puke klass, meth, e
- e = case e
- when MiniTest::Skip then
- @skips += 1
- return "S" unless @verbose
- "Skipped:\n#{klass}##{meth} [#{location e}]:\n#{e.message}\n"
- when MiniTest::Assertion then
- @failures += 1
- "Failure:\n#{klass}##{meth} [#{location e}]:\n#{e.message}\n"
- else
- @errors += 1
- bt = MiniTest::filter_backtrace(e.backtrace).join "\n "
- "Error:\n#{klass}##{meth}:\n#{e.class}: #{e.message.b}\n #{bt}\n"
- end
- @report << e
- e[0, 1]
- end
-
- def initialize # :nodoc:
- @report = []
- @errors = @failures = @skips = 0
- @verbose = false
- @mutex = Thread::Mutex.new
- @info_signal = Signal.list['INFO']
- @repeat_count = nil
- end
-
- def synchronize # :nodoc:
- if @mutex then
- @mutex.synchronize { yield }
- else
- yield
- end
- end
-
- def process_args args = [] # :nodoc:
- options = {}
- orig_args = args.dup
-
- OptionParser.new do |opts|
- opts.banner = 'minitest options:'
- opts.version = MiniTest::Unit::VERSION
-
- opts.on '-h', '--help', 'Display this help.' do
- puts opts
- exit
- end
-
- opts.on '-s', '--seed SEED', Integer, "Sets random seed" do |m|
- options[:seed] = m.to_i
- end
-
- opts.on '-v', '--verbose', "Verbose. Show progress processing files." do
- options[:verbose] = true
- end
-
- opts.on '-n', '--name PATTERN', "Filter test names on pattern (e.g. /foo/)" do |a|
- options[:filter] = a
- end
-
- opts.parse! args
- orig_args -= args
- end
-
- unless options[:seed] then
- srand
- options[:seed] = srand % 0xFFFF
- orig_args << "--seed" << options[:seed].to_s
- end
-
- srand options[:seed]
-
- self.verbose = options[:verbose]
- @help = orig_args.map { |s| s =~ /[\s|&<>$()]/ ? s.inspect : s }.join " "
-
- options
- end
-
- ##
- # Begins the full test run. Delegates to +runner+'s #_run method.
-
- def run args = []
- self.class.runner._run(args)
- end
-
- ##
- # Top level driver, controls all output and filtering.
-
- def _run args = []
- args = process_args args # ARGH!! blame test/unit process_args
- self.options.merge! args
-
- puts "Run options: #{help}"
-
- self.class.plugins.each do |plugin|
- send plugin
- break unless report.empty?
- end
-
- return failures + errors if self.test_count > 0 # or return nil...
- rescue Interrupt
- abort 'Interrupted'
- end
-
- ##
- # Runs test suites matching +filter+.
-
- def run_tests
- _run_anything :test
- end
-
- ##
- # Writes status to +io+
-
- def status io = self.output
- format = "%d tests, %d assertions, %d failures, %d errors, %d skips"
- io.puts format % [test_count, assertion_count, failures, errors, skips]
- end
-
- ##
- # Provides a simple set of guards that you can use in your tests
- # to skip execution if it is not applicable. These methods are
- # mixed into TestCase as both instance and class methods so you
- # can use them inside or outside of the test methods.
- #
- # def test_something_for_mri
- # skip "bug 1234" if jruby?
- # # ...
- # end
- #
- # if windows? then
- # # ... lots of test methods ...
- # end
-
- module Guard
-
- ##
- # Is this running on jruby?
-
- def jruby? platform = RUBY_PLATFORM
- "java" == platform
- end
-
- ##
- # Is this running on mri?
-
- def maglev? platform = defined?(RUBY_ENGINE) && RUBY_ENGINE
- "maglev" == platform
- end
-
- module_function :maglev?
-
- ##
- # Is this running on mri?
-
- def mri? platform = RUBY_DESCRIPTION
- /^ruby/ =~ platform
- end
-
- ##
- # Is this running on rubinius?
-
- def rubinius? platform = defined?(RUBY_ENGINE) && RUBY_ENGINE
- "rbx" == platform
- end
-
- ##
- # Is this running on windows?
-
- def windows? platform = RUBY_PLATFORM
- /mswin|mingw/ =~ platform
- end
- end
-
- ##
- # Provides before/after hooks for setup and teardown. These are
- # meant for library writers, NOT for regular test authors. See
- # #before_setup for an example.
-
- module LifecycleHooks
- ##
- # Runs before every test, after setup. This hook is meant for
- # libraries to extend minitest. It is not meant to be used by
- # test developers.
- #
- # See #before_setup for an example.
-
- def after_setup; end
-
- ##
- # Runs before every test, before setup. This hook is meant for
- # libraries to extend minitest. It is not meant to be used by
- # test developers.
- #
- # As a simplistic example:
- #
- # module MyMinitestPlugin
- # def before_setup
- # super
- # # ... stuff to do before setup is run
- # end
- #
- # def after_setup
- # # ... stuff to do after setup is run
- # super
- # end
- #
- # def before_teardown
- # super
- # # ... stuff to do before teardown is run
- # end
- #
- # def after_teardown
- # # ... stuff to do after teardown is run
- # super
- # end
- # end
- #
- # class MiniTest::Unit::TestCase
- # include MyMinitestPlugin
- # end
-
- def before_setup; end
-
- ##
- # Runs after every test, before teardown. This hook is meant for
- # libraries to extend minitest. It is not meant to be used by
- # test developers.
- #
- # See #before_setup for an example.
-
- def before_teardown; end
-
- ##
- # Runs after every test, after teardown. This hook is meant for
- # libraries to extend minitest. It is not meant to be used by
- # test developers.
- #
- # See #before_setup for an example.
-
- def after_teardown; end
- end
-
- ##
- # Subclass TestCase to create your own tests. Typically you'll want a
- # TestCase subclass per implementation class.
- #
- # See MiniTest::Assertions
-
- class TestCase
- include LifecycleHooks
- include Guard
- extend Guard
-
- attr_reader :__name__ # :nodoc:
-
- PASSTHROUGH_EXCEPTIONS = [NoMemoryError, SignalException,
- Interrupt, SystemExit] # :nodoc:
-
- ##
- # Runs the tests reporting the status to +runner+
-
- def run runner
- trap "INFO" do
- runner.report.each_with_index do |msg, i|
- warn "\n%3d) %s" % [i + 1, msg]
- end
- warn ''
- time = runner.start_time ? Time.now - runner.start_time : 0
- warn "Current Test: %s#%s %.2fs" % [self.class, self.__name__, time]
- runner.status $stderr
- end if runner.info_signal
-
- start_time = Time.now
-
- result = ""
- begin
- @passed = nil
- self.before_setup
- self.setup
- self.after_setup
- self.run_test self.__name__
- result = "." unless io?
- time = Time.now - start_time
- runner.record self.class, self.__name__, self._assertions, time, nil
- @passed = true
- rescue *PASSTHROUGH_EXCEPTIONS
- raise
- rescue Exception => e
- @passed = Skip === e
- time = Time.now - start_time
- runner.record self.class, self.__name__, self._assertions, time, e
- result = runner.puke self.class, self.__name__, e
- ensure
- %w{ before_teardown teardown after_teardown }.each do |hook|
- begin
- self.send hook
- rescue *PASSTHROUGH_EXCEPTIONS
- raise
- rescue Exception => e
- @passed = false
- runner.record self.class, self.__name__, self._assertions, time, e
- result = runner.puke self.class, self.__name__, e
- end
- end
- trap 'INFO', 'DEFAULT' if runner.info_signal
- end
- result
- end
-
- alias :run_test :__send__
-
- def initialize name # :nodoc:
- @__name__ = name
- @__io__ = nil
- @passed = nil
- @@current = self # FIX: make thread local
- end
-
- def self.current # :nodoc:
- @@current # FIX: make thread local
- end
-
- ##
- # Return the output IO object
-
- def io
- @__io__ = true
- MiniTest::Unit.output
- end
-
- ##
- # Have we hooked up the IO yet?
-
- def io?
- @__io__
- end
-
- def self.reset # :nodoc:
- @@test_suites = {}
- end
-
- reset
-
- ##
- # Make diffs for this TestCase use #pretty_inspect so that diff
- # in assert_equal can be more details. NOTE: this is much slower
- # than the regular inspect but much more usable for complex
- # objects.
-
- def self.make_my_diffs_pretty!
- require 'pp'
-
- define_method :mu_pp do |o|
- o.pretty_inspect
- end
- end
-
- def self.inherited klass # :nodoc:
- @@test_suites[klass] = true
- super
- end
-
- def self.test_order # :nodoc:
- :random
- end
-
- def self.test_suites # :nodoc:
- @@test_suites.keys.sort_by { |ts| ts.name.to_s }
- end
-
- def self.test_methods # :nodoc:
- methods = public_instance_methods(true).grep(/^test/).map { |m| m.to_s }
-
- case self.test_order
- when :parallel
- max = methods.size
- ParallelEach.new methods.sort.sort_by { rand max }
- when :random then
- max = methods.size
- methods.sort.sort_by { rand max }
- when :alpha, :sorted then
- methods.sort
- else
- raise "Unknown test_order: #{self.test_order.inspect}"
- end
- end
-
- ##
- # Returns true if the test passed.
-
- def passed?
- @passed
- end
-
- ##
- # Runs before every test. Use this to set up before each test
- # run.
-
- def setup; end
-
- ##
- # Runs after every test. Use this to clean up after each test
- # run.
-
- def teardown; end
-
- include MiniTest::Assertions
- end # class TestCase
- end # class Unit
-
- Test = Unit::TestCase
-end # module MiniTest
-
-Minitest = MiniTest # :nodoc: because ugh... I typo this all the time
diff --git a/test/lib/profile_test_all.rb b/test/lib/profile_test_all.rb
deleted file mode 100644
index 4771b72a..00000000
--- a/test/lib/profile_test_all.rb
+++ /dev/null
@@ -1,91 +0,0 @@
-# frozen_string_literal: true
-#
-# purpose:
-# Profile memory usage of each tests.
-#
-# usage:
-# RUBY_TEST_ALL_PROFILE=[file] make test-all
-#
-# output:
-# [file] specified by RUBY_TEST_ALL_PROFILE
-# If [file] is 'true', then it is ./test_all_profile
-#
-# collected information:
-# - ObjectSpace.memsize_of_all
-# - GC.stat
-# - /proc/meminfo (some fields, if exists)
-# - /proc/self/status (some fields, if exists)
-# - /proc/self/statm (if exists)
-#
-
-require 'objspace'
-
-class MiniTest::Unit::TestCase
- alias orig_run run
-
- file = ENV['RUBY_TEST_ALL_PROFILE']
- file = 'test-all-profile-result' if file == 'true'
- TEST_ALL_PROFILE_OUT = open(file, 'w')
- TEST_ALL_PROFILE_GC_STAT_HASH = {}
- TEST_ALL_PROFILE_BANNER = ['name']
- TEST_ALL_PROFILE_PROCS = []
-
- def self.add *name, &b
- TEST_ALL_PROFILE_BANNER.concat name
- TEST_ALL_PROFILE_PROCS << b
- end
-
- add 'failed?' do |result, tc|
- result << (tc.passed? ? 0 : 1)
- end
-
- add 'memsize_of_all' do |result, *|
- result << ObjectSpace.memsize_of_all
- end
-
- add *GC.stat.keys do |result, *|
- GC.stat(TEST_ALL_PROFILE_GC_STAT_HASH)
- result.concat TEST_ALL_PROFILE_GC_STAT_HASH.values
- end
-
- def self.add_proc_meminfo file, fields
- return unless FileTest.exist?(file)
- regexp = /(#{fields.join("|")}):\s*(\d+) kB/
- # check = {}; fields.each{|e| check[e] = true}
- add *fields do |result, *|
- text = File.read(file)
- text.scan(regexp){
- # check.delete $1
- result << $2
- ''
- }
- # raise check.inspect unless check.empty?
- end
- end
-
- add_proc_meminfo '/proc/meminfo', %w(MemTotal MemFree)
- add_proc_meminfo '/proc/self/status', %w(VmPeak VmSize VmHWM VmRSS)
-
- if FileTest.exist?('/proc/self/statm')
- add *%w(size resident share text lib data dt) do |result, *|
- result.concat File.read('/proc/self/statm').split(/\s+/)
- end
- end
-
- def memprofile_test_all_result_result
- result = ["#{self.class}\##{self.__name__.to_s.gsub(/\s+/, '')}"]
- TEST_ALL_PROFILE_PROCS.each{|proc|
- proc.call(result, self)
- }
- result.join("\t")
- end
-
- def run runner
- result = orig_run(runner)
- TEST_ALL_PROFILE_OUT.puts memprofile_test_all_result_result
- TEST_ALL_PROFILE_OUT.flush
- result
- end
-
- TEST_ALL_PROFILE_OUT.puts TEST_ALL_PROFILE_BANNER.join("\t")
-end
diff --git a/test/lib/test/unit.rb b/test/lib/test/unit.rb
deleted file mode 100644
index 51c8960c..00000000
--- a/test/lib/test/unit.rb
+++ /dev/null
@@ -1,1175 +0,0 @@
-# frozen_string_literal: true
-begin
- gem 'minitest', '< 5.0.0' if defined? Gem
-rescue Gem::LoadError
-end
-require 'minitest/unit'
-require 'test/unit/assertions'
-require_relative '../envutil'
-require 'test/unit/testcase'
-require 'optparse'
-
-# See Test::Unit
-module Test
- ##
- # Test::Unit is an implementation of the xUnit testing framework for Ruby.
- #
- # If you are writing new test code, please use MiniTest instead of Test::Unit.
- #
- # Test::Unit has been left in the standard library to support legacy test
- # suites.
- module Unit
- TEST_UNIT_IMPLEMENTATION = 'test/unit compatibility layer using minitest' # :nodoc:
-
- module RunCount # :nodoc: all
- @@run_count = 0
-
- def self.have_run?
- @@run_count.nonzero?
- end
-
- def run(*)
- @@run_count += 1
- super
- end
-
- def run_once
- return if have_run?
- return if $! # don't run if there was an exception
- yield
- end
- module_function :run_once
- end
-
- module Options # :nodoc: all
- def initialize(*, &block)
- @init_hook = block
- @options = nil
- super(&nil)
- end
-
- def option_parser
- @option_parser ||= OptionParser.new
- end
-
- def process_args(args = [])
- return @options if @options
- orig_args = args.dup
- options = {}
- opts = option_parser
- setup_options(opts, options)
- opts.parse!(args)
- orig_args -= args
- args = @init_hook.call(args, options) if @init_hook
- non_options(args, options)
- @run_options = orig_args
- @help = orig_args.map { |s| s =~ /[\s|&<>$()]/ ? s.inspect : s }.join " "
- @options = options
- end
-
- private
- def setup_options(opts, options)
- opts.separator 'minitest options:'
- opts.version = MiniTest::Unit::VERSION
-
- opts.on '-h', '--help', 'Display this help.' do
- puts opts
- exit
- end
-
- opts.on '-s', '--seed SEED', Integer, "Sets random seed" do |m|
- options[:seed] = m
- end
-
- opts.on '-v', '--verbose', "Verbose. Show progress processing files." do
- options[:verbose] = true
- self.verbose = options[:verbose]
- end
-
- opts.on '-n', '--name PATTERN', "Filter test method names on pattern: /REGEXP/, !/REGEXP/ or STRING" do |a|
- (options[:filter] ||= []) << a
- end
-
- opts.on '--test-order=random|alpha|sorted', [:random, :alpha, :sorted] do |a|
- MiniTest::Unit::TestCase.test_order = a
- end
- end
-
- def non_options(files, options)
- filter = options[:filter]
- if filter
- pos_pat = /\A\/(.*)\/\z/
- neg_pat = /\A!\/(.*)\/\z/
- negative, positive = filter.partition {|s| neg_pat =~ s}
- if positive.empty?
- filter = nil
- elsif negative.empty? and positive.size == 1 and pos_pat !~ positive[0]
- filter = positive[0]
- else
- filter = Regexp.union(*positive.map! {|s| Regexp.new(s[pos_pat, 1] || "\\A#{Regexp.quote(s)}\\z")})
- end
- unless negative.empty?
- negative = Regexp.union(*negative.map! {|s| Regexp.new(s[neg_pat, 1])})
- filter = /\A(?=.*#{filter})(?!.*#{negative})/
- end
- if Regexp === filter
- # bypass conversion in minitest
- def filter.=~(other) # :nodoc:
- super unless Regexp === other
- end
- end
- options[:filter] = filter
- end
- true
- end
- end
-
- module Parallel # :nodoc: all
- def process_args(args = [])
- return @options if @options
- options = super
- if @options[:parallel]
- @files = args
- end
- options
- end
-
- def non_options(files, options)
- @jobserver = nil
- if !options[:parallel] and
- /(?:\A|\s)--jobserver-(?:auth|fds)=(\d+),(\d+)/ =~ ENV["MAKEFLAGS"]
- begin
- r = IO.for_fd($1.to_i(10), "rb", autoclose: false)
- w = IO.for_fd($2.to_i(10), "wb", autoclose: false)
- rescue
- r.close if r
- nil
- else
- @jobserver = [r, w]
- options[:parallel] ||= 1
- end
- end
- super
- end
-
- def status(*args)
- result = super
- raise @interrupt if @interrupt
- result
- end
-
- private
- def setup_options(opts, options)
- super
-
- opts.separator "parallel test options:"
-
- options[:retry] = true
-
- opts.on '-j N', '--jobs N', /\A(t)?(\d+)\z/, "Allow run tests with N jobs at once" do |_, t, a|
- options[:testing] = true & t # For testing
- options[:parallel] = a.to_i
- end
-
- opts.on '--separate', "Restart job process after one testcase has done" do
- options[:parallel] ||= 1
- options[:separate] = true
- end
-
- opts.on '--retry', "Retry running testcase when --jobs specified" do
- options[:retry] = true
- end
-
- opts.on '--no-retry', "Disable --retry" do
- options[:retry] = false
- end
-
- opts.on '--ruby VAL', "Path to ruby which is used at -j option" do |a|
- options[:ruby] = a.split(/ /).reject(&:empty?)
- end
- end
-
- class Worker
- def self.launch(ruby,args=[])
- io = IO.popen([*ruby, "-W1",
- "#{File.dirname(__FILE__)}/unit/parallel.rb",
- *args], "rb+")
- new(io, io.pid, :waiting)
- end
-
- attr_reader :quit_called
-
- def initialize(io, pid, status)
- @io = io
- @pid = pid
- @status = status
- @file = nil
- @real_file = nil
- @loadpath = []
- @hooks = {}
- @quit_called = false
- end
-
- def puts(*args)
- @io.puts(*args)
- end
-
- def run(task,type)
- @file = File.basename(task, ".rb")
- @real_file = task
- begin
- puts "loadpath #{[Marshal.dump($:-@loadpath)].pack("m0")}"
- @loadpath = $:.dup
- puts "run #{task} #{type}"
- @status = :prepare
- rescue Errno::EPIPE
- died
- rescue IOError
- raise unless /stream closed|closed stream/ =~ $!.message
- died
- end
- end
-
- def hook(id,&block)
- @hooks[id] ||= []
- @hooks[id] << block
- self
- end
-
- def read
- res = (@status == :quit) ? @io.read : @io.gets
- res && res.chomp
- end
-
- def close
- @io.close unless @io.closed?
- self
- rescue IOError
- end
-
- def quit
- return if @io.closed?
- @quit_called = true
- @io.puts "quit"
- end
-
- def kill
- Process.kill(:KILL, @pid)
- rescue Errno::ESRCH
- end
-
- def died(*additional)
- @status = :quit
- @io.close
- status = $?
- if status and status.signaled?
- additional[0] ||= SignalException.new(status.termsig)
- end
-
- call_hook(:dead,*additional)
- end
-
- def to_s
- if @file and @status != :ready
- "#{@pid}=#{@file}"
- else
- "#{@pid}:#{@status.to_s.ljust(7)}"
- end
- end
-
- attr_reader :io, :pid
- attr_accessor :status, :file, :real_file, :loadpath
-
- private
-
- def call_hook(id,*additional)
- @hooks[id] ||= []
- @hooks[id].each{|hook| hook[self,additional] }
- self
- end
-
- end
-
- def flush_job_tokens
- if @jobserver
- r, w = @jobserver.shift(2)
- @jobserver = nil
- w << @job_tokens.slice!(0..-1)
- r.close
- w.close
- end
- end
-
- def after_worker_down(worker, e=nil, c=false)
- return unless @options[:parallel]
- return if @interrupt
- flush_job_tokens
- warn e if e
- real_file = worker.real_file and warn "running file: #{real_file}"
- @need_quit = true
- warn ""
- warn "Some worker was crashed. It seems ruby interpreter's bug"
- warn "or, a bug of test/unit/parallel.rb. try again without -j"
- warn "option."
- warn ""
- STDERR.flush
- exit c
- end
-
- def after_worker_quit(worker)
- return unless @options[:parallel]
- return if @interrupt
- worker.close
- if @jobserver and (token = @job_tokens.slice!(0))
- @jobserver[1] << token
- end
- @workers.delete(worker)
- @dead_workers << worker
- @ios = @workers.map(&:io)
- end
-
- def launch_worker
- begin
- worker = Worker.launch(@options[:ruby], @run_options)
- rescue => e
- abort "ERROR: Failed to launch job process - #{e.class}: #{e.message}"
- end
- worker.hook(:dead) do |w,info|
- after_worker_quit w
- after_worker_down w, *info if !info.empty? && !worker.quit_called
- end
- @workers << worker
- @ios << worker.io
- @workers_hash[worker.io] = worker
- worker
- end
-
- def delete_worker(worker)
- @workers_hash.delete worker.io
- @workers.delete worker
- @ios.delete worker.io
- end
-
- def quit_workers
- return if @workers.empty?
- @workers.reject! do |worker|
- begin
- Timeout.timeout(1) do
- worker.quit
- end
- rescue Errno::EPIPE
- rescue Timeout::Error
- end
- worker.close
- end
-
- return if @workers.empty?
- begin
- Timeout.timeout(0.2 * @workers.size) do
- Process.waitall
- end
- rescue Timeout::Error
- @workers.each do |worker|
- worker.kill
- end
- @worker.clear
- end
- end
-
- FakeClass = Struct.new(:name)
- def fake_class(name)
- (@fake_classes ||= {})[name] ||= FakeClass.new(name)
- end
-
- def deal(io, type, result, rep, shutting_down = false)
- worker = @workers_hash[io]
- cmd = worker.read
- cmd.sub!(/\A\.+/, '') if cmd # read may return nil
- case cmd
- when ''
- # just only dots, ignore
- when /^okay$/
- worker.status = :running
- when /^ready(!)?$/
- bang = $1
- worker.status = :ready
-
- unless task = @tasks.shift
- worker.quit
- return nil
- end
- if @options[:separate] and not bang
- worker.quit
- worker = add_worker
- end
- worker.run(task, type)
- @test_count += 1
-
- jobs_status(worker)
- when /^done (.+?)$/
- begin
- r = Marshal.load($1.unpack("m")[0])
- rescue
- print "unknown object: #{$1.unpack("m")[0].dump}"
- return true
- end
- result << r[0..1] unless r[0..1] == [nil,nil]
- rep << {file: worker.real_file, report: r[2], result: r[3], testcase: r[5]}
- $:.push(*r[4]).uniq!
- jobs_status(worker) if @options[:job_status] == :replace
- return true
- when /^record (.+?)$/
- begin
- r = Marshal.load($1.unpack("m")[0])
- rescue => e
- print "unknown record: #{e.message} #{$1.unpack("m")[0].dump}"
- return true
- end
- record(fake_class(r[0]), *r[1..-1])
- when /^p (.+?)$/
- del_jobs_status
- print $1.unpack("m")[0]
- jobs_status(worker) if @options[:job_status] == :replace
- when /^after (.+?)$/
- @warnings << Marshal.load($1.unpack("m")[0])
- when /^bye (.+?)$/
- after_worker_down worker, Marshal.load($1.unpack("m")[0])
- when /^bye$/, nil
- if shutting_down || worker.quit_called
- after_worker_quit worker
- else
- after_worker_down worker
- end
- else
- print "unknown command: #{cmd.dump}\n"
- end
- return false
- end
-
- def _run_parallel suites, type, result
- if @options[:parallel] < 1
- warn "Error: parameter of -j option should be greater than 0."
- return
- end
-
- # Require needed thing for parallel running
- require 'timeout'
- @tasks = @files.dup # Array of filenames.
- @need_quit = false
- @dead_workers = [] # Array of dead workers.
- @warnings = []
- @total_tests = @tasks.size.to_s(10)
- rep = [] # FIXME: more good naming
-
- @workers = [] # Array of workers.
- @workers_hash = {} # out-IO => worker
- @ios = [] # Array of worker IOs
- @job_tokens = String.new(encoding: Encoding::ASCII_8BIT) if @jobserver
- begin
- [@tasks.size, @options[:parallel]].min.times {launch_worker}
-
- while _io = IO.select(@ios)[0]
- break if _io.any? do |io|
- @need_quit or
- (deal(io, type, result, rep).nil? and
- !@workers.any? {|x| [:running, :prepare].include? x.status})
- end
- if @jobserver and @job_tokens and !@tasks.empty? and !@workers.any? {|x| x.status == :ready}
- t = @jobserver[0].read_nonblock([@tasks.size, @options[:parallel]].min, exception: false)
- if String === t
- @job_tokens << t
- t.size.times {launch_worker}
- end
- end
- end
- rescue Interrupt => ex
- @interrupt = ex
- return result
- ensure
- if @interrupt
- @ios.select!{|x| @workers_hash[x].status == :running }
- while !@ios.empty? && (__io = IO.select(@ios,[],[],10))
- __io[0].reject! {|io| deal(io, type, result, rep, true)}
- end
- end
-
- quit_workers
- flush_job_tokens
-
- unless @interrupt || !@options[:retry] || @need_quit
- parallel = @options[:parallel]
- @options[:parallel] = false
- suites, rep = rep.partition {|r| r[:testcase] && r[:file] && r[:report].any? {|e| !e[2].is_a?(MiniTest::Skip)}}
- suites.map {|r| r[:file]}.uniq.each {|file| require file}
- suites.map! {|r| eval("::"+r[:testcase])}
- del_status_line or puts
- unless suites.empty?
- puts "\n""Retrying..."
- _run_suites(suites, type)
- end
- @options[:parallel] = parallel
- end
- unless @options[:retry]
- del_status_line or puts
- end
- unless rep.empty?
- rep.each do |r|
- r[:report].each do |f|
- puke(*f) if f
- end
- end
- if @options[:retry]
- @errors += rep.map{|x| x[:result][0] }.inject(:+)
- @failures += rep.map{|x| x[:result][1] }.inject(:+)
- @skips += rep.map{|x| x[:result][2] }.inject(:+)
- end
- end
- unless @warnings.empty?
- warn ""
- @warnings.uniq! {|w| w[1].message}
- @warnings.each do |w|
- warn "#{w[0]}: #{w[1].message} (#{w[1].class})"
- end
- warn ""
- end
- end
- end
-
- def _run_suites suites, type
- _prepare_run(suites, type)
- @interrupt = nil
- result = []
- GC.start
- if @options[:parallel]
- _run_parallel suites, type, result
- else
- suites.each {|suite|
- begin
- result << _run_suite(suite, type)
- rescue Interrupt => e
- @interrupt = e
- break
- end
- }
- end
- del_status_line
- result
- end
- end
-
- module Skipping # :nodoc: all
- def failed(s)
- super if !s or @options[:hide_skip]
- end
-
- private
- def setup_options(opts, options)
- super
-
- opts.separator "skipping options:"
-
- options[:hide_skip] = true
-
- opts.on '-q', '--hide-skip', 'Hide skipped tests' do
- options[:hide_skip] = true
- end
-
- opts.on '--show-skip', 'Show skipped tests' do
- options[:hide_skip] = false
- end
- end
-
- def _run_suites(suites, type)
- result = super
- report.reject!{|r| r.start_with? "Skipped:" } if @options[:hide_skip]
- report.sort_by!{|r| r.start_with?("Skipped:") ? 0 : \
- (r.start_with?("Failure:") ? 1 : 2) }
- failed(nil)
- result
- end
- end
-
- module Statistics
- def update_list(list, rec, max)
- if i = list.empty? ? 0 : list.bsearch_index {|*a| yield(*a)}
- list[i, 0] = [rec]
- list[max..-1] = [] if list.size >= max
- end
- end
-
- def record(suite, method, assertions, time, error)
- if @options.values_at(:longest, :most_asserted).any?
- @tops ||= {}
- rec = [suite.name, method, assertions, time, error]
- if max = @options[:longest]
- update_list(@tops[:longest] ||= [], rec, max) {|_,_,_,t,_|t