diff --git a/.editorconfig b/.editorconfig
index ac225590..cffab408 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -11,5 +11,5 @@ indent_size = 2
[*.{rb,js}]
max_line_length = 80
-[README.md]
+[*.md]
max_line_length = unset
diff --git a/.github/workflows/super_diff.yml b/.github/workflows/super_diff.yml
index c225488d..f4339ee5 100644
--- a/.github/workflows/super_diff.yml
+++ b/.github/workflows/super_diff.yml
@@ -7,6 +7,7 @@ on:
pull_request:
types:
- opened
+ - closed
- reopened
- synchronize
concurrency:
@@ -15,6 +16,7 @@ concurrency:
jobs:
analyze:
runs-on: ubuntu-latest
+ if: ${{ github.event_name == 'push' || github.event.action != 'closed' }}
steps:
- uses: actions/checkout@v4
- name: Download actionlint
@@ -88,3 +90,179 @@ jobs:
- test
steps:
- run: echo "Analysis and tests passed. Ready to merge."
+
+ collect-release-info:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ bundler-cache: true
+ - name: Run command
+ id: command
+ run: scripts/collect-release-info.rb
+ outputs:
+ IS_NEW_RELEASE: ${{ steps.command.outputs.IS_NEW_RELEASE }}
+ RELEASE_VERSION: ${{ steps.command.outputs.RELEASE_VERSION }}
+
+ collect-docsite-release-info:
+ runs-on: ubuntu-latest
+ needs:
+ - collect-release-info
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Run command
+ id: command
+ run: |
+ set -x
+
+ if [[ "$IS_NEW_RELEASE" == "true" ]]; then
+ DOCSITE_RELEASE_VERSION="$RELEASE_VERSION"
+ DOCSITE_DESTINATION_PATH="releases/$RELEASE_VERSION"
+ HAS_CHANGES_TO_DOCS="true"
+ else
+ DOCSITE_RELEASE_VERSION="$COMMIT_ID"
+ DOCSITE_DESTINATION_PATH="branches/$BRANCH_NAME/$COMMIT_ID"
+ # Check if there any changes to docs/
+ if git diff --quiet --merge-base "origin/$GITHUB_BASE_REF" -- docs; then
+ HAS_CHANGES_TO_DOCS="false"
+ else
+ HAS_CHANGES_TO_DOCS="true"
+ fi
+ fi
+
+ {
+ echo "DOCSITE_RELEASE_VERSION=$DOCSITE_RELEASE_VERSION"
+ echo "DOCSITE_DESTINATION_PATH=$DOCSITE_DESTINATION_PATH"
+ echo "HAS_CHANGES_TO_DOCS=$HAS_CHANGES_TO_DOCS"
+ } >> "$GITHUB_OUTPUT"
+ env:
+ IS_NEW_RELEASE: ${{ needs.collect-release-info.outputs.IS_NEW_RELEASE }}
+ RELEASE_VERSION: ${{ needs.collect-release-info.outputs.RELEASE_VERSION }}
+ BRANCH_NAME: ${{ github.head_ref }}
+ COMMIT_ID: ${{ github.event.pull_request.head.sha }}
+ outputs:
+ DOCSITE_RELEASE_VERSION: ${{ steps.command.outputs.DOCSITE_RELEASE_VERSION }}
+ DOCSITE_DESTINATION_PATH: ${{ steps.command.outputs.DOCSITE_DESTINATION_PATH }}
+ HAS_CHANGES_TO_DOCS: ${{ steps.command.outputs.HAS_CHANGES_TO_DOCS }}
+
+ build-docsite:
+ runs-on: ubuntu-latest
+ needs:
+ - analyze
+ - collect-release-info
+ - collect-docsite-release-info
+ if: ${{ github.event_name == 'pull_request' && ((needs.collect-release-info.outputs.IS_NEW_RELEASE == 'false' && needs.collect-docsite-release-info.outputs.HAS_CHANGES_TO_DOCS == 'true') || (needs.collect-release-info.outputs.IS_NEW_RELEASE == 'true' && github.event.merged)) }}
+ steps:
+ - uses: actions/checkout@v4
+ - name: Install poetry
+ run: pipx install poetry
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ - name: Install Python dependencies
+ run: poetry install
+ - name: Build docsite
+ run: poetry run mkdocs build
+ - name: Save site/ for later jobs
+ uses: actions/cache/save@v3
+ with:
+ path: site
+ key: docsite-${{ github.sha }}
+
+ publish-docsite:
+ runs-on: ubuntu-latest
+ needs:
+ - collect-release-info
+ - collect-docsite-release-info
+ - build-docsite
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ ref: gh-pages
+ - name: Restore cache from previous job
+ uses: actions/cache/restore@v3
+ with:
+ path: site
+ key: docsite-${{ github.sha }}
+ - name: Update redirect in index (for a release)
+ if: ${{ needs.collect-release-info.outputs.IS_NEW_RELEASE == 'true' }}
+ run: |
+ cat <<-EOT > index.html
+
+
+
+ SuperDiff Documentation
+
+
+
+
+ This page has moved to a different URL.
+ Please click
+
+ this link
+
+ if you are not redirected.
+
+
+
+ EOT
+ - name: Copy site/ to ${{ needs.collect-docsite-release-info.outputs.DOCSITE_DESTINATION_PATH }}
+ run: |
+ mkdir -p "$(dirname "$DOCSITE_DESTINATION_PATH")"
+ mv site "$DOCSITE_DESTINATION_PATH"
+ env:
+ DOCSITE_DESTINATION_PATH: ${{ needs.collect-docsite-release-info.outputs.DOCSITE_DESTINATION_PATH }}
+ - name: Publish new version of docsite
+ run: |
+ git add -A .
+ git config user.name "${GITHUB_ACTOR}"
+ git config user.email "${GITHUB_ACTOR}@users.noreply.github.com"
+ git commit -m "Publish docs at $DOCSITE_DESTINATION_PATH"
+ git push
+ env:
+ DOCSITE_DESTINATION_PATH: ${{ needs.collect-docsite-release-info.outputs.DOCSITE_DESTINATION_PATH }}
+ - name: Announce publishing of docsite as a comment on the PR
+ run: |
+ gh pr comment "$PULL_REQUEST_NUMBER" --body ":book: A new version of the docsite has been published at: "
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ PULL_REQUEST_NUMBER: ${{ github.event.number }}
+ DOCSITE_DESTINATION_PATH: ${{ needs.collect-docsite-release-info.outputs.DOCSITE_DESTINATION_PATH }}
+
+ unpublish_docsite:
+ runs-on: ubuntu-latest
+ needs:
+ - collect-release-info
+ - collect-docsite-release-info
+ if: ${{ github.event_name == 'pull_request' && needs.collect-release-info.outputs.IS_NEW_RELEASE == 'false' && github.event.action == 'closed' }}
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ ref: gh-pages
+ - name: Set DOCSITE_DESTINATION_PARENT_PATH
+ run: |
+ set -x
+ DOCSITE_DESTINATION_PARENT_PATH="$(dirname "$DOCSITE_DESTINATION_PATH")"
+ echo "DOCSITE_DESTINATION_PARENT_PATH=$DOCSITE_DESTINATION_PARENT_PATH" >> "GITHUB_ENV"
+ env:
+ DOCSITE_DESTINATION_PATH: ${{ needs.collect-docsite-release-info.outputs.DOCSITE_DESTINATION_PATH }}
+ - name: Remove ${{ env.DOCSITE_DESTINATION_PARENT_PATH }} on gh-pages
+ run: |
+ set -x
+ if [[ "$DOCSITE_DESTINATION_PARENT_PATH" == "releases" || "$DOCSITE_DESTINATION_PARENT_PATH" == "branches" ]]; then
+ echo "Not removing $DOCSITE_DESTINATION_PARENT_PATH."
+ exit 1
+ fi
+ rm -rf "$DOCSITE_DESTINATION_PARENT_PATH"
+ - name: Re-push docsite if necessary
+ run: |
+ git add -A .
+ if ! git diff --cached --quiet; then
+ git config user.name "${GITHUB_ACTOR}"
+ git config user.email "${GITHUB_ACTOR}@users.noreply.github.com"
+ git commit -m "Remove $DOCSITE_DESTINATION_PARENT_PATH"
+ git push
+ fi
diff --git a/.gitignore b/.gitignore
index 87d7767b..fed4404a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,3 +18,9 @@ zeus.server-start.log
!.yarn/releases
!.yarn/sdks
!.yarn/versions
+
+# Ignore Python stuff
+poetry.lock
+
+# Ignore mkdocs stuff
+site
diff --git a/.prettierignore b/.prettierignore
index 8fe001fd..22e00978 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -1,5 +1,7 @@
.yarn/cache
.yarn/releases
gemfiles
+site
+tmp
vendor/bundle
yarn.lock
diff --git a/.python-version b/.python-version
new file mode 100644
index 00000000..8531a3b7
--- /dev/null
+++ b/.python-version
@@ -0,0 +1 @@
+3.12.2
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
deleted file mode 100644
index 3440b775..00000000
--- a/ARCHITECTURE.md
+++ /dev/null
@@ -1,33 +0,0 @@
-# Architecture
-
-I'll have more later around this,
-but here are some quick hits:
-
-## Basic concepts
-
-- An **object inspector** generates a multi-line textual representation of an object,
- similar to PrettyPrinter in Ruby or AwesomePrint,
- but more appropriate for showing within a diff.
-- An **operation** represents a difference in one value from another
- in the context of a data structure.
- That difference can either be an _delete_, _insert_, or _change_.
-- An **operation tree** is the set of all operations between two data structures,
- where each operation represents the difference between an inner element within the structure
- (value for an array or a key/value pair for a hash).
- Since change operations represent elements that have child elements,
- they also have child operations to represent those child elements.
- Those child operations can themselves have children, etc.
- This descendancy is what forms the tree.
-- An **operation tree builder** makes a comparison between two like data structures
- and generates an operation tree to represent the differences.
-- A **diff formatter** takes an operation tree
- and spits out a textual representation of that tree in the form of a conventional diff.
- Each operation may in fact generate more than one line in the final diff
- because the object that is specific to the operation is run through an object inspector.
-- Finally, a **differ** ties everything together
- and figures out which operation tree builder and diff formatter to use for a particular pair of values
- (where one value is the "expected" and the other is the "actual").
-
-## Code flow diagram
-
-[![code flow](./docs/code-flow-diagram.png)](https://docs.google.com/drawings/d/1nKi4YKXgzzIIM-eY0P4uwjkglmuwlf8nTRFne8QZhBg/edit)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
deleted file mode 100644
index 20094255..00000000
--- a/CONTRIBUTING.md
+++ /dev/null
@@ -1,72 +0,0 @@
-# Contributing
-
-Want to make a change to this library?
-Great! Here's how you do that.
-
-First, create a fork of this repo,
-cloning it to your computer
-and running the following command in the resulting directory
-in order to install dependencies:
-
-```
-bin/setup
-```
-
-After this, you can run all of the tests
-to make sure everything is kosher:
-
-```
-bundle exec rake
-```
-
-Next, make changes to the code as necessary.
-
-Code is linted and formatted using Prettier,
-so [make sure that's set up in your editor first][prettier-editors],
-or you can always fix any lint violations by running:
-
-```
-yarn lint:fix
-```
-
-[prettier-editors]: https://prettier.io/docs/en/editors.html
-
-If you update one of the tests,
-you can run it like so:
-
-```
-bin/rspec spec/integration/...
-bin/rspec spec/unit/...
-```
-
-Finally, submit your PR.
-I'll try to respond as quickly as I can.
-I may have suggestions about code style or your approach,
-but hopefully everything looks good and your changes get merged!
-Now you're a contributor! đ
-
-## Speeding up the integration tests
-
-The integration tests,
-located in `spec/integration`,
-can be quite slow to run.
-If you'd like to speed them up,
-run the following command in a separate tab:
-
-```
-zeus start
-```
-
-Now the next time you run an integration test by saying
-
-```
-bin/rspec spec/integration/...
-```
-
-it should run twice as fast.
-
-## Understanding the codebase
-
-If you want to make a change
-but you're having trouble where to start,
-you might find the [Architecture](./ARCHITECTURE.md) document helpful.
diff --git a/README.md b/README.md
index 7bed3d5e..9bc7347d 100644
--- a/README.md
+++ b/README.md
@@ -8,24 +8,26 @@
[issuehunt-badge]: https://img.shields.io/badge/sponsored_through-IssueHunt-2EC28C
[issuehunt]: https://issuehunt.io/r/mcmire/super_diff
-SuperDiff is a gem that hooks into RSpec
-to intelligently display the differences between two data structures of any type.
+**SuperDiff** is a Ruby gem
+which is designed to display the differences between two objects of any type
+in a familiar and intelligent fashion.
đĸ **[See what's changed in recent versions.][changelog]**
-[changelog]: CHANGELOG.md
+[changelog]: ./CHANGELOG.md
## Introduction
The primary motivation behind this gem
is to vastly improve upon RSpec's built-in diffing capabilities.
-Sometimes, whenever you use a matcher such as `eq`, `match`, `include`, or `have_attributes`,
+The primary motivation behind this gem
+is to vastly improve upon RSpec's built-in diffing capabilities.
+RSpec has many nice features,
+and one of them is that whenever you use a matcher such as `eq`, `match`, `include`, or `have_attributes`,
you will get a diff of the two data structures you are trying to match against.
This is great if all you want to do is compare multi-line strings.
-But if you want to compare other, more "real world" kinds of values,
-such as what you might work with when developing API endpoints
-or testing methods that make database calls and return a set of model objects,
+But if you want to compare other, more "real world" kinds of values such as API or database data,
then you are out of luck.
Since [RSpec merely runs your `expected` and `actual` values through Ruby's PrettyPrinter library][rspec-differ-fail]
and then performs a diff of these strings,
@@ -33,8 +35,7 @@ the output it produces leaves much to be desired.
[rspec-differ-fail]: https://github.com/rspec/rspec-support/blob/c69a231d7369dd165ad7ce4742e1a2e21e3462b5/lib/rspec/support/differ.rb#L178
-For instance,
-let's say you wanted to compare these two hashes:
+For instance, let's say you wanted to compare these two hashes:
```ruby
actual = {
@@ -78,7 +79,7 @@ expect(actual).to eq(expected)
You would get output that looks like this:
-![Before super_diff](docs/before.png)
+![Before super_diff](docs/assets/before.png)
What this library does
is to provide a diff engine
@@ -87,162 +88,14 @@ and display them in a sensible way.
So, using the example above,
you'd get this instead:
-![After super_diff](docs/after.png)
-
-## Installation
-
-There are a few different ways to install `super_diff`
-depending on your type of project.
-
-### Rails apps
-
-If you're developing a Rails app,
-add the following to your Gemfile:
-
-```ruby
-group :test do
- gem "super_diff"
-end
-```
-
-After running `bundle install`,
-add the following to your `rails_helper`:
-
-```ruby
-require "super_diff/rspec-rails"
-```
-
-### Projects using some part of Rails (e.g. ActiveModel)
-
-If you're developing an app using Hanami or Sinatra,
-or merely using a part of Rails such as ActiveModel,
-add the following to your Gemfile where appropriate:
+![After super_diff](docs/assets/after.png)
-```ruby
-gem "super_diff"
-```
-
-After running `bundle install`,
-add the following to your `spec_helper`:
-
-```ruby
-require "super_diff/rspec"
-require "super_diff/active_support"
-```
-
-### Gems
-
-If you're developing a gem,
-add the following to your gemspec:
-
-```ruby
-spec.add_development_dependency "super_diff"
-```
-
-Now add the following to your `spec_helper`:
-
-```ruby
-require "super_diff/rspec"
-```
+## Installation & Usage
-## Configuration
+đ For more on how to install and use SuperDiff,
+[read the user documentation][user-docs].
-You can customize the behavior of the gem
-by adding a configuration block
-to your test helper file
-(`rails_helper` or `spec_helper`)
-which looks something like this:
-
-```ruby
-SuperDiff.configure do |config|
- # ...
-end
-```
-
-### Customizing colors
-
-If you don't like the colors that SuperDiff uses,
-you can change them like this:
-
-```ruby
-SuperDiff.configure do |config|
- config.actual_color = :green
- config.expected_color = :red
- config.border_color = :yellow
- config.header_color = :yellow
-end
-```
-
-See [eight_bit_color.rb](lib/super_diff/csi/eight_bit_color.rb)
-for the list of available colors.
-
-You can also completely disable colorized output.
-
-
-```ruby
-SuperDiff.configure do |config|
- config.color_enabled = false
- end
-```
-
-
-### Disabling the key
-
-You can disable the key by changing the following config (default: true):
-
-
-```ruby
-SuperDiff.configure do |config|
- config.key_enabled = false
-end
-```
-
-
-### Hiding unimportant lines
-
-When looking at a large diff for which many of the lines do not change,
-it can be difficult to locate the lines which do. Text-oriented
-diffs such as those you get from a conventional version control system
-solve this problem by removing those unchanged lines from the diff
-entirely. The same can be done in SuperDiff.
-
-```ruby
-SuperDiff.configure do |config|
- config.diff_elision_enabled = false
- config.diff_elision_maximum = 3
-end
-```
-
-- `diff_elision_enabled` â The elision logic is disabled by default so
- as not to surprise people, so setting this to `true` will turn it on.
-- `diff_elision_maximum` â This number controls what happens to
- unchanged lines (i.e. lines that are neither "insert" lines nor
- "delete" lines) that are in between changed lines. If a section of
- unchanged lines is beyond this number, the gem will elide (a fancy
- word for remove) the data structures within that section as much as
- possible until the limit is reached or it cannot go further. Elided
- lines are replaced with a `# ...` marker.
-
-### Diffing custom objects
-
-If you are comparing two data structures
-that involve a class that is specific to your project,
-the resulting diff may not look as good as diffs involving native or primitive objects.
-This happens because if SuperDiff doesn't recognize a class,
-it will fall back to a generic representation when diffing instances of that class.
-Fortunately, the gem has a pluggable interface
-that allows you to insert your own implementations
-of key pieces involved in the diffing process.
-I'll have more about how that works soon,
-but here is what such a configuration would look like:
-
-```ruby
-SuperDiff.configure do |config|
- config.add_extra_differ_class(YourDiffer)
- config.add_extra_operation_tree_builder_class(YourOperationTreeBuilder)
- config.add_extra_operation_tree_class(YourOperationTree)
-end
-```
+[user-docs]: ./docs/users/getting-started.md
## Support
@@ -257,16 +110,19 @@ I'll try to respond to it as soon as I can!
## Contributing
Any code contributions to improve this library are welcome!
-Please see the [contributing](./CONTRIBUTING.md) document for more on how to do that.
+Please see the [contributing](./docs/contributors/index.md) document
+for more on how to do that.
## Sponsoring
If there's a change you want implemented, you can choose to sponsor that change!
`super_diff` is set up on IssueHunt,
so feel free to search for an existing issue (or make your own)
-and [add a bounty](https://issuehunt.io/r/mcmire/super_diff).
+and [add a bounty][issuehunt].
I'll get notified right away!
+[issuehunt]: https://issuehunt.io/r/mcmire/super_diff
+
## Compatibility
`super_diff` is [tested][gh-actions] to work with
diff --git a/bin/setup b/bin/setup
index 8533cb28..6c9551fa 100755
--- a/bin/setup
+++ b/bin/setup
@@ -19,7 +19,7 @@ provision-project() {
#
# To regenerate this section, install the gem and run:
#
-# generate-setup -p ruby -p node
+# generate-setup -p ruby -p node -p python
#
# --- SETUP --------------------------------------------------------------------
@@ -332,9 +332,12 @@ ensure-project-node-dependencies-installed() {
banner 'Installing Node dependencies'
npm install
elif [[ -f yarn.lock ]]; then
- if ! type yarn &>/dev/null || ! yarn --version &>/dev/null; then
- banner 'Installing Yarn'
- npm install -g yarn
+ if ! has-executable yarn || ! yarn --version &>/dev/null; then
+ banner 'Enabling Yarn'
+ corepack enable
+ if has-executable asdf; then
+ asdf reshim nodejs
+ fi
fi
banner 'Installing Node dependencies'
yarn install
@@ -344,15 +347,88 @@ ensure-project-node-dependencies-installed() {
It doesn't look like you have a package-lock.json or yarn.lock in your project
yet. I'm not sure which package manager you plan on using, so you'll need to run
either \`npm install\` or \`yarn install\` once first. Additionally, if you want
-to use Yarn 2+, then now is the time to switch to that. Then you can re-run this
+to use Yarn 2, then now is the time to switch to that. Then you can re-run this
script."
exit 1
fi
}
+# --- PYTHON -------------------------------------------------------------------
+
+REQUIRED_PYTHON_VERSION=
+
+provision-python() {
+ if [[ -f .tool-versions ]]; then
+ REQUIRED_PYTHON_VERSION=$(cat .tool-versions | grep '^python ' | head -n 1 | sed -Ee 's/^python (.+)$/\1/')
+ elif [[ -f .python-version ]]; then
+ REQUIRED_PYTHON_VERSION=$(cat .python-version | head -n 1 | sed -Ee 's/^python-([[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+)$/\1/')
+ fi
+
+ if [[ -z $REQUIRED_PYTHON_VERSION ]]; then
+ error "Could not determine required Python version for this project."
+ print-wrapped "\
+Your project needs to include either a valid .tool-versions file with a 'python'
+line or a valid .python-version file."
+ exit 1
+ fi
+
+ ensure-python-installed
+ ensure-pipx-installed
+
+ if [[ -f pyproject.toml ]] || [[ -f requirements.txt ]]; then
+ ensure-project-python-dependencies-installed
+ fi
+}
+
+ensure-python-installed() {
+ if has-executable asdf; then
+ if ! (asdf current python | grep $REQUIRED_PYTHON_VERSION'\>' &>/dev/null); then
+ banner "Installing Python $REQUIRED_PYTHON_VERSION with asdf"
+ asdf install python $REQUIRED_PYTHON_VERSION
+ fi
+ elif has-executable pyenv; then
+ if ! (pyenv versions | grep $REQUIRED_PYTHON_VERSION'\>' &>/dev/null); then
+ banner "Installing Python $REQUIRED_PYTHON_VERSION with pyenv"
+ pyenv install --skip-existing "$REQUIRED_PYTHON_VERSION"
+ fi
+ else
+ error "You don't seem to have a Python manager installed."
+ print-wrapped "\
+We recommend using asdf. You can find instructions to install it here:
+
+ https://asdf-vm.com
+
+When you're done, close and re-open this terminal tab and re-run this script."
+ exit 1
+ fi
+}
+
+ensure-pipx-installed() {
+ if ! has-executable pipx; then
+ banner "Installing pipx"
+ pip install --user pipx
+ fi
+}
+
+ensure-project-python-dependencies-installed() {
+ banner 'Installing Python dependencies'
+
+ if [[ -f pyproject.toml ]]; then
+ if ! has-executable poetry; then
+ banner "Installing Poetry"
+ pipx install poetry
+ fi
+
+ poetry install
+ else
+ warning "Did not detect a way to install Python dependencies."
+ fi
+}
+
run-provisions() {
provision-ruby
provision-node
+ provision-python
}
# --- FIN ----------------------------------------------------------------------
diff --git a/docs/after.rb b/docs-support/after.rb
similarity index 100%
rename from docs/after.rb
rename to docs-support/after.rb
diff --git a/docs/before.rb b/docs-support/before.rb
similarity index 100%
rename from docs/before.rb
rename to docs-support/before.rb
diff --git a/docs/carbon-config.json b/docs-support/carbon-config.json
similarity index 100%
rename from docs/carbon-config.json
rename to docs-support/carbon-config.json
diff --git a/docs/carbon.md b/docs-support/carbon.md
similarity index 100%
rename from docs/carbon.md
rename to docs-support/carbon.md
diff --git a/docs/code-flow-diagram.png b/docs-support/code-flow-diagram.png
similarity index 100%
rename from docs/code-flow-diagram.png
rename to docs-support/code-flow-diagram.png
diff --git a/docs/after.png b/docs/assets/after.png
similarity index 100%
rename from docs/after.png
rename to docs/assets/after.png
diff --git a/docs/before.png b/docs/assets/before.png
similarity index 100%
rename from docs/before.png
rename to docs/assets/before.png
diff --git a/docs/contributors/architecture/how-rspec-works.md b/docs/contributors/architecture/how-rspec-works.md
new file mode 100644
index 00000000..e2c7240b
--- /dev/null
+++ b/docs/contributors/architecture/how-rspec-works.md
@@ -0,0 +1,224 @@
+# How RSpec works
+
+In order to understand how the RSpec integration in SuperDiff works,
+it's important to study the pieces in play within RSpec itself.
+
+## Context
+
+Imagine a file such as the following:
+
+```ruby
+# spec/some_spec.rb
+describe "Some tests" do
+ it "does something" do
+ expect([1, 2, 3]).to eq([1, 6, 3])
+ end
+end
+```
+
+Then, imagine that the user runs:
+
+```
+rspec
+```
+
+Without SuperDiff activated,
+this will produce the following output:
+
+```
+Some tests
+ does something (FAILED - 1)
+
+Failures:
+
+ 1) Some tests does something
+ Failure/Error: expect([1, 2, 3]).to eq([1, 6, 3])
+
+ expected: [1, 6, 3]
+ got: [1, 2, 3]
+
+ (compared using ==)
+ # ./spec/some_spec.rb:3:in `block (2 levels) in '
+
+Finished in 0.01186 seconds (files took 0.07765 seconds to load)
+1 example, 1 failure
+
+Failed examples:
+
+rspec ./spec/some_spec.rb:2 # Some tests does something
+```
+
+Now imagine that we want to modify this output
+to replace the "expected:"/"actual:" lines with a diff.
+How would we do this?
+
+## RSpec's cast of characters
+
+First, we will review several concepts in RSpec: [^fn1]
+
+- Since RSpec tests are "just Ruby",
+ parts of tests map to objects
+ which are created when those tests are loaded.
+ `describe`s and `context`s are represented by
+ **example groups**,
+ instances of [`RSpec::Core::ExampleGroup`](https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/example_group.rb),
+ and `it`s and `specify`s are represented by
+ **examples**,
+ instances of [`RSpec::Core::Example`](https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/example.rb).
+- Most notably,
+ within tests themselves,
+ the `expect` method â
+ [mixed into tests via the syntax layer][rspec-exp-syntax] â
+ returns an instance of [`RSpec::Expectations::ExpectationTarget`](https://github.com/rspec/rspec-expectations/blob/v3.13.0/lib/rspec/expectations/expectation_target.rb),
+ and may raise an error if the check it is performing fails.
+- **Configuration** is kept in an instance of [`RSpec::Core::Configuration`](https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/configuration.rb),
+ which is accessible via `RSpec.configuration`
+ and is [initialized the first time it's used][rspec-configuration-init]
+- The **runner**,
+ an instance of [`RSpec::Core::Runner`](https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/runner.rb),
+ is the entrypoint to all of RSpec â
+ [it's called directly by the `rspec` executable][rspec-core-runner-call] â
+ and executes the tests the user has specified.
+- **Formatters** change RSpec's output after running tests.
+ Since the user can specify one formatter when running `rspec`,
+ the collection of registered formatters is managed by the formatter loader,
+ an instance of [`RSpec::Core::Formatters::Loader`](https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/formatters.rb#L96).
+ The default formatter is "progress",
+ [set in the configuration object][rspec-default-formatter-set],
+ which maps to an instance of [`RSpec::Core::Formatters::ProgressFormatter`](https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/formatters/progress_formatter.rb).
+- [Notifications](https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/notifications.rb)
+ represent events that occur while running tests,
+ such as "these tests failed"
+ or "this test was skipped".
+- The **reporter**,
+ an instance of [`RSpec::Core::Reporter`](https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/reporter.rb),
+ acts as sort of the brain of the whole operation.
+ Implementing a publish/subscribe model,
+ it tracks the state of tests as they are run,
+ including errors captured during the process,
+ packaging key moments into notifications
+ and delegating them to all registered formatters (or anything else listening to the reporter).
+ Like the configuration object,
+ it is also global,
+ accessible via the configuration object,
+ and is [initialized the first time it's used][rspec-reporter-init]
+- The **exception presenter**,
+ an instance of [`RSpec::Core::Formatters::ExceptionPresenter`](https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/formatters/exception_presenter.rb),
+ is a special type of formatter
+ which does not respond to events,
+ but is rather responsible for managing all of the logic involved
+ in building all of the output that appears
+ when a test fails.
+
+## What RSpec does
+
+Given the above, RSpec performs the following sequence of events:
+
+1. The developer adds an failing assertion to a test using the following forms
+ (filling in ``, ``, ``, and `` appropriately):
+ - `expect().to ()`
+ - `expect { }.to ()`
+ - `expect().not_to ()`
+ - `expect { }.not_to ()`
+1. The developer runs the test using the `rspec` executable.
+1. The `rspec` executable [calls `RSpec::Core::Runner.invoke`][rspec-core-runner-call].
+1. Skipping a few steps, `RSpec::Core::Runner#run_specs` is called,
+ which [runs all tests by surrounding them in a call to `RSpec::Core::Reporter#report`][rspec-reporter-report-call].
+1. Skipping a few more steps, [`RSpec::Core::Example#run` is called to run the current example][rspec-core-example-run-call].
+1. From here one of two paths is followed
+ depending on whether the assertion is positive (`.to`) or negative (`.not_to`).
+ - If the assertion is positive:
+ 1. Within the test,
+ after `expect` is called to build a `RSpec::Expectations::ExpectationTarget`,
+ [the `to` method calls `RSpec::Expectations::PositiveExpectationHandler.handle_matcher`][rspec-positive-expectation-handler-handle-matcher-call].
+ 1. The matcher is then used to know
+ whether the assertion passes or fails:
+ `PositiveExpectationHandler`
+ [calls the `matches?` method on the matcher][rspec-positive-expectation-handler-matcher-matches].
+ 1. Assuming that `matches?` returns false,
+ `PositiveExpectationHandler` then [calls `RSpec::Expectations::ExpectationHelper.handle_failure`][rspec-expectation-helper-handle-failure-call-positive],
+ telling it to get the positive failure message from the matcher
+ by calling `failure_message`.
+ - If the assertion is negative:
+ 1. Within the test,
+ after `expect` is called to build a `RSpec::Expectations::ExpectationTarget`,
+ [the `not_to` method calls `RSpec::Expectations::NegativeExpectationHandler.handle_matcher`][rspec-negative-expectation-handler-handle-matcher-call].
+ 1. The matcher is then used to know
+ whether the assertion passes or fails:
+ `NegativeExpectationHandler`,
+ [calls the `does_not_match?` method on the matcher][rspec-negative-expectation-handler-matcher-does-not-match].
+ 1. Assuming that `does_not_match?` returns false,
+ `NegativeExpectationHandler` then [calls `RSpec::Expectations::ExpectationHelper.handle_failure`][via `NegativeExpectationHandler`][rspec-expectation-helper-handle-failure-call-negative],
+ telling it to get the negative failure message from the matcher
+ by calling `failure_message_when_negated`.
+1. `RSpec::Expectations::ExpectationHelper.handle_failure` [calls `RSpec::Expectations.fail_with`][rspec-expectations-fail-with-call].
+1. `RSpec::Expectations.fail_with` [creates a diff using `RSpec::Matchers::MultiMatcherDiff`,
+ wraps it in an exception,
+ and feeds the exception to `RSpec::Support.notify_failure`][rspec-support-notify-failure-call].
+1. `RSpec::Support.notify_failure` calls the currently set failure notifier,
+ which by default [raises the given exception][rspec-support-exception-raise].
+1. Returning to `RSpec::Core::Example#run`,
+ this method [rescues the exception][rspec-core-example-run-rescue]
+ and then calls `finish`,
+ which [calls `example_failed` on the reporter][rspec-reporter-example-failed-call].
+1. `RSpec::Core::Reporter#example_failed` uses `RSpec::Core::Notifications::ExampleNotification.for`
+ to [construct a notification][rspec-reporter-construct-failed-example-notification],
+ which in this case is an `RSpec::Core::Notifications::FailedExampleNotification`.
+ `RSpec::Core::Notifications::FailedExampleNotification` in turn
+ [constructs an `RSpec::Core::Formatters::ExceptionPresenter`][rspec-exception-presenter-init].
+1. `RSpec::Core::Reporter#example_failed` then [passes the notification object
+ along with an event of `:example_failed` to the `notify` method][rspec-reporter-example-failed-call].
+ Because `RSpec::Core::Formatters::ProgressFormatter` is a listener on the reporter,
+ [its `example_failed` method gets called][rspec-progress-formatter-example-failed-call],
+ which prints a message `Failure:` to the terminal.
+1. Returning to `RSpec::Core::Reporter#report`,
+ it now [calls `finish` after all tests are run][rspec-reporter-finish-call].
+1. `RSpec::Core::Reporter#finish` [notifies listeners of the `:dump_failures` event][rspec-reporter-notify-dump-failures],
+ this time using an instance of `RSpec::Core::Notifications::ExamplesNotification`.
+ Again, because `RSpec::Core::Formatters::ProgressFormatter` is registered,
+ its `dump_failures` method is called,
+ which is actually defined in `RSpec::Core::Formatters::BaseTextFormatter`.
+1. `RSpec::Core::Formatters::BaseTextFormatter#dump_failures`
+ [calls `RSpec::Core::Notifications::ExamplesNotification#fully_formatted_failed_examples`][rspec-examples-notification-fully-formatted-failed-examples-call].
+1. `RSpec::Core::Notifications::ExamplesNotification#fully_formatted_failed_examples`
+ [formats all of the failed examples][rspec-failed-examples-notification-fully-formatted-call]
+ by wrapping them in `RSpec::Core::Notifications::FailedExampleNotification`s and calling `fully_formatted` on them.
+1. `RSpec::Core::Notifications::FailedExampleNotification#fully_formatted` then [calls `fully_formatted`
+ on its `RSpec::Core::Formatters::ExceptionPresenter`][rspec-exception-presenter-fully-formatted-call].
+1. `RSpec::Core::Formatters::ExceptionPresenter#fully_formatted` then [constructs various pieces
+ of what will eventually be printed to the terminal][rspec-exception-presenter-main],
+ including the name of the test,
+ the line that failed,
+ the error and backtrace,
+ and other pertinent details.
+
+[^fn1]: Note that the analysis of the RSpec source code in this document is accurate as of RSpec v3.13.0, released February 4, 2024.
+
+[rspec-exp-syntax]: https://github.com/rspec/rspec-expectations/blob/v3.13.0/lib/rspec/expectations/syntax.rb#L73
+[rspec-configuration-init]: https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core.rb#L86
+[rspec-core-runner-call]: https://github.com/rspec/rspec-core/blob/v3.13.0/exe/rspec#L4
+[rspec-default-formatter-set]: https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/configuration.rb#L1030
+[rspec-reporter-init]: https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/configuration.rb#L1056
+[rspec-reporter-report-call]: https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/runner.rb#L115
+[rspec-core-example-run-call]: https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/example_group.rb#L646
+[rspec-positive-expectation-handler-handle-matcher-call]: https://github.com/rspec/rspec-expectations/blob/v3.13.0/lib/rspec/expectations/expectation_target.rb#L65
+[rspec-negative-expectation-handler-handle-matcher-call]: https://github.com/rspec/rspec-expectations/blob/v3.13.0/lib/rspec/expectations/expectation_target.rb#L78
+[rspec-positive-expectation-handler-matcher-matches]: https://github.com/rspec/rspec-expectations/blob/v3.13.0/lib/rspec/expectations/handler.rb#L51
+[rspec-negative-expectation-handler-matcher-does-not-match]: https://github.com/rspec/rspec-expectations/blob/v3.13.0/lib/rspec/expectations/handler.rb#L79
+[rspec-expectation-helper-handle-failure-call-positive]: https://github.com/rspec/rspec-expectations/blob/v3.13.0/lib/rspec/expectations/handler.rb#L56
+[rspec-expectation-helper-handle-failure-call-negative]: https://github.com/rspec/rspec-expectations/blob/v3.13.0/lib/rspec/expectations/handler.rb#L84
+[rspec-expectations-fail-with-call]: https://github.com/rspec/rspec-expectations/blob/v3.13.0/lib/rspec/expectations/handler.rb#L37-L41
+[rspec-support-notify-failure-call]: https://github.com/rspec/rspec-expectations/blob/v3.13.0/lib/rspec/expectations/fail_with.rb#L27-L35
+[rspec-support-exception-raise]: https://github.com/rspec/rspec-support/blob/v3.13.0/lib/rspec/support.rb#L110
+[rspec-core-example-run-rescue]: https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/example.rb#L280
+[rspec-reporter-example-failed-call]: https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/example.rb#L484
+[rspec-reporter-construct-failed-example-notification]: https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/notifications.rb#L52
+[rspec-exception-presenter-init]: https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/notifications.rb#L213
+[rspec-reporter-example-failed-call]: https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/reporter.rb#L145
+[rspec-progress-formatter-example-failed-call]: https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/reporter.rb#L209
+[rspec-reporter-finish-call]: https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/reporter.rb#L76
+[rspec-reporter-notify-dump-failures]: https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/reporter.rb#L178
+[rspec-examples-notification-fully-formatted-failed-examples-call]: https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/formatters/base_text_formatter.rb#L32
+[rspec-failed-examples-notification-fully-formatted-call]: https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/notifications.rb#L114
+[rspec-exception-presenter-fully-formatted-call]: https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/notifications.rb#L202
+[rspec-exception-presenter-main]: https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/formatters/exception_presenter.rb#L84-L100
diff --git a/docs/contributors/architecture/how-super-diff-works.md b/docs/contributors/architecture/how-super-diff-works.md
new file mode 100644
index 00000000..6a86ba85
--- /dev/null
+++ b/docs/contributors/architecture/how-super-diff-works.md
@@ -0,0 +1,186 @@
+# How SuperDiff works
+
+## SuperDiff's cast of characters
+
+- An **inspection tree builder** (or, casually, an _inspector_)
+ makes use of an **inspection tree**
+ to generate a multi-line textual representation of an object,
+ similar to PrettyPrinter in Ruby or AwesomePrint,
+ but more appropriate for showing within a diff.
+- An **operation tree builder** makes a comparison between two objects
+ (the "expected" vs. the "actual")
+ and generates an **operation tree** to represent the differences.
+- An operation tree is made up of **operations**,
+ which designate differences in the inner parts of the two objects.
+ Those differences can be of type _delete_, _insert_, or _change_.
+ Since objects can be nested,
+ some operations can have children operations themselves,
+ hence the tree.
+- An **operation tree flattener** takes an operation tree
+ and converts them to a set of **lines**,
+ which will aid in generating a diff.
+ Logic is applied to determine whether to place prefixes, suffixes, or commas.
+ Each operation may in fact generate more than one line
+ because the object that is specific to the operation is run through an inspector.
+- A **diff formatter** takes a set of lines
+ and spits out a textual representation in the form of a conventional diff.
+- A **differ** ties everything together
+ by figuring out which operation tree builder to use for a pair of expected and actual values,
+ building an operation tree,
+ and then converting it to a diff.
+
+## Where SuperDiff integrates into RSpec
+
+As described in ["How RSpec works"](./how-rspec-works.md#what-rspec-does),
+when an assertion in a test fails â
+which happens when a matcher whose `matches?` method returns `false`
+is passed to `expect(...).to`,
+or when a matcher whose `does_not_match?` method returns `true`
+is passed to `expect(...).not_to` â
+RSpec will call the `RSpec::Expectations::ExpectationHelper#handle_failure` method,
+which will call `RSpec::Expectations.fail_with`.
+This method will use `RSpec::Matchers::ExpectedsForMultipleDiffs`
+and the differ object that `RSpec::Expectations.differ` returns
+to generate a diff,
+combining it with the failure message from the matcher,
+obtained by either calling `failure_message` or `failure_messsage_when_negated`,
+and then it will bundle them both into an error object.
+RSpec's runner will eventually capture this error and hand it to the reporter,
+which will display it via `RSpec::Core::Formatters::ExceptionPresenter`.
+
+Given this, there are a few things that SuperDiff needs to do
+in order to integrate fully with RSpec.
+
+1. First,
+ SuperDiff needs to get RSpec to use its differ instead of its own.
+ Unfortunately, while RSpec is very configurable,
+ it does not allow its differ to be substituted,
+ so the gem needs to do some amount of patching in order to achieve this.
+2. Second,
+ the gem needs to provide intelligent diffing
+ for all kinds of built-in matchers.
+ Many matchers in RSpec are marked as non-diffable â
+ their `diffable?` method returns `false` â
+ causing RSpec to not show a diff after the matcher's failure message
+ in the failure output.
+ The `contain_exactly` matcher is one such example.
+ SuperDiff turns this on â
+ but the only way to do this is via patching.
+3. Lastly,
+ SuperDiff also modifies the failure messages for RSpec's built-in matchers
+ so that key words belonging to the "expected" and "actual" values
+ get recolored.
+ Again, the only real way to do this is via patching.
+
+Here are all of the places that SuperDiff patches RSpec:
+
+- `RSpec::Expectations.differ`
+ (to use `SuperDiff::RSpec::Differ` instead of RSpec's own differ)
+- `RSpec::Expectations::ExpectationHelper#handle_failure`
+ (to consult the matcher for the "expected" and "actual" values,
+ under special methods `expected_for_diff` and `actual_for_diff`)
+- `RSpec::Core::Formatters::ConsoleCodes`
+ (to allow for using SuperDiff's colors
+ and to remove the fallback in the absence of a specified color)
+- `RSpec::Core::Formatters::ExceptionPresenter`
+ (to recolor failure output)
+- `RSpec::Core::SyntaxHighlighter`
+ (to turn off syntax highlighting for code,
+ as it interferes with the previous patches)
+- `RSpec::Support::ObjectFormatter`
+ (to use SuperDiff's object inspectors)
+- `RSpec::Matchers::ExpectedsForMultipleDiffs`
+ (to add a key above the diff,
+ add spacing around the diff,
+ and colorize the word "Diff:")
+- `RSpec::Matchers::Builtin::*`
+ (to reword failure messages across various matchers)
+- `RSpec::Matchers`
+ (to reset the `an_array_matching` alias for `match_array`,
+ and to ensure that `match_array` preserves behavior,
+ as it is backed by MatchArray class specific to SuperDiff)
+
+## How SuperDiff's diff engine works
+
+With the internals of RSpec thoroughly explored,
+the internals of SuperDiff can finally be enumerated.
+
+Once a test fails
+and RSpec delegates to SuperDiff's differ,
+this sequence of events occurs:
+
+1. `SuperDiff::Differs::Main.call` is called with a pair of values: `expected` and `actual`.
+ This method looks for a differ that is suitable for the pair
+ among a set of defaults and the list of differs registered via SuperDiff's configuration.
+ It does this by calling `.applies_to?` on each,
+ passing the `expected` and `actual`;
+ the first differ for whom this method returns `true` wins.
+ (This is a common pattern throughout the codebase.)
+ In most cases, if no differs are suitable,
+ then an error is raised,
+ although this is sometimes overridden.
+1. Once a differ is found,
+ its `.call` method is called.
+ Since all differs inherit from `SuperDiff::Differs::Base`,
+ `.call` always builds an operation tree,
+ but the type of operation tree to build
+ â or, more specifically, the operation tree builder subclass â
+ is determined by the differ itself,
+ via the `operation_tree_builder_class` method.
+ For instance,
+ `SuperDiff::Differs::Array` uses a `SuperDiff::OperationTreeBuilder::Array`,
+ `SuperDiff::Differs::Hash` uses a `SuperDiff::OperationTreeBuilder::Hash`,
+ etc.
+1. Once the differ has an operation tree builder,
+ the differ calls `.call` on it
+ to build an operation tree.
+ Different operation tree builders do different things
+ depending on the types of objects,
+ but the end goal is to iterate over both the expected and actual values in tandem,
+ find the differences between them,
+ and represent those differences as operations.
+ An operation may be one of four types:
+ `insert`, `delete`, `change`, or `noop`.
+ In the case of collections â
+ which covers most types of values â
+ the diff is performed recursively.
+ This means that just as collections can have multiple levels,
+ so too can operation trees.
+1. Once the differ has an operation tree,
+ it then calls `to_diff` on it.
+ This method is defined in `SuperDiff::OperationTrees::Base`,
+ and it starts by first flattening the tree.
+1. This means that we need an operation tree flattener class.
+ Like differs,
+ operation trees specify which operation tree flattener they want to use
+ via the `operation_tree_flattener_class` method.
+1. Once the operation tree has a flattener class,
+ it calls `.call` on the class
+ to flatten the tree.
+1. Different types of flatteners also do different things,
+ but most of them operate on collection-based operation trees.
+ Since operation trees can have multiple level,
+ the flattening must be performed recursively.
+ The end result is a list of Line objects.
+1. Once the operation tree has been flattened,
+ then if the user has configured the gem to do so,
+ a step is performed to look for unchanged lines
+ (that is, operations of type `noop`)
+ and _elide_ them â
+ collapse them in such a way that the surrounding context is still visible.
+1. Once a set of elided lines is obtained,
+ the operation tree runs them through a formatter â
+ so called `TieredLinesFormatter` â
+ which will add the `-`s and `+`s along with splashes of color
+ to create the final format you see at the very end.
+
+In summary:
+
+```mermaid
+graph TB
+ DiffersMain["Differs::Main"] -- Differs --> Differ;
+ Differ -- Operation tree builder --> OperationTree[Operation tree];
+ OperationTree -- Operation tree flattener --> Lines;
+ Lines -- Tiered lines elider --> ElidedLines[Elided lines];
+ ElidedLines -- Tiered lines formatter --> FinalDiff[Final diff];
+```
diff --git a/docs/contributors/architecture/introduction.md b/docs/contributors/architecture/introduction.md
new file mode 100644
index 00000000..3ca96d41
--- /dev/null
+++ b/docs/contributors/architecture/introduction.md
@@ -0,0 +1,10 @@
+# Architecture
+
+The SuperDiff codebase is sufficiently complex
+that diving into the source code for the first time may be daunting.
+
+This section aims to point contributors in the right direction.
+Since most of the code in this codebase services the RSpec integration,
+these guides heavily skew toward that topic,
+and you can start with a [guide to how RSpec works if you like](./how-rspec-works.md).
+But you can also find [details around the diff engine](./how-super-diff-works.md).
diff --git a/docs/contributors/how-to-contribute.md b/docs/contributors/how-to-contribute.md
new file mode 100644
index 00000000..e0c1c86f
--- /dev/null
+++ b/docs/contributors/how-to-contribute.md
@@ -0,0 +1,123 @@
+# How to Contribute
+
+Want to make a change to this project?
+Great! Here's how you do that.
+
+## 1. Install dependencies
+
+First, [create a fork of the SuperDiff repo](https://github.com/mcmire/super_diff/fork)
+and clone it to your computer.
+
+Next, run the following command in the resulting directory
+in order to install dependencies.
+This will also install a Git hook
+which ensures all code is formatted whenever a commit is pushed:
+
+```
+bin/setup
+```
+
+## 2. Make a new branch
+
+It's best to follow [GitHub Flow][github-flow] when working on this repo.
+Start by making a new branch to hold your changes:
+
+```
+git checkout -b
+```
+
+[github-flow]: https://docs.github.com/en/get-started/using-github/github-flow
+
+## 3. Understand the codebase
+
+Some architectural documents have been provided
+to aid you in understanding the codebase.
+You might find the guide on [how SuperDiff works](./architecture/how-super-diff-works.md) to be helpful, for example.
+
+## 4. Write and run tests
+
+All code is backed by tests,
+so if you want to submit a pull request,
+make sure to update the existing tests or write new ones as you find necessary.
+
+There are two kinds of tests in this project:
+
+- **Unit tests**, kept in `spec/unit`,
+ exercise individual classes and methods in isolation.
+- **Integration tests**, kept in `spec/integration`,
+ exercise the interaction between SuperDiff, RSpec, and parts of Rails.
+
+It's best to run all of the tests after cloning SuperDiff
+to establish a baseline for any changes you want to make,
+but you can also run them at any time:
+
+```
+bundle exec rake
+```
+
+If you want to run one of the tests, say:
+
+```
+bin/rspec spec/integration/...
+bin/rspec spec/unit/...
+```
+
+Note that the integration tests
+can be quite slow to run.
+If you'd like to speed them up,
+run the following command in a separate terminal session:
+
+```
+zeus start
+```
+
+Now the next time you run an integration test by saying
+
+```
+bin/rspec spec/integration/...
+```
+
+it should run twice as fast.
+
+## 5. Run the linter
+
+Code is linted and formatted using Prettier,
+so [make sure that's set up in your editor][prettier-editors].
+If you don't want to do this,
+you can also fix any lint violations by running:
+
+```
+yarn lint:fix
+```
+
+Provided that you ran `bin/setup` above,
+any code you've changed will also be linted
+whenever you push a commit.
+
+[prettier-editors]: https://prettier.io/docs/en/editors.html
+
+## 6. (Optional) Update the documentation
+
+If there's any part of this documentation that you wish to update,
+then in a free terminal session, run:
+
+```
+poetry run mkdocs serve
+```
+
+Now open `http://localhost:8000` to view a preview of the documentation locally.
+
+The files themselves are located in `docs/`
+and are written in Markdown.
+Thanks to the command above,
+updating any of these files will automatically be reflected in the preview.
+
+## 7. Submit a pull request
+
+When you're done,
+push your branch
+and create a new pull request.
+I'll try to respond as quickly as I can.
+I may have suggestions about code style or your approach,
+but hopefully everything looks good and your changes get merged!
+Now you're a contributor! đ
diff --git a/docs/contributors/index.md b/docs/contributors/index.md
new file mode 100644
index 00000000..61087dfe
--- /dev/null
+++ b/docs/contributors/index.md
@@ -0,0 +1,7 @@
+# Contributor Documentation
+
+If you're looking to contribute to SuperDiff's codebase,
+you're in the right place.
+
+Here you can find [instructions on setting up your development environment](./how-to-contribute.md)
+as well as a [guide to the codebase itself](./architecture/introduction.md).
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 00000000..c0a60f32
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,14 @@
+---
+hide:
+ - navigation
+---
+
+# SuperDiff
+
+This site hosts user- and contributor-facing documentation for SuperDiff,
+a Ruby gem that hooks into RSpec
+to intelligently display the differences between two data structures of any type.
+
+- [Learn how to use SuperDiff â¤](./users/index.md)
+- [Learn how to contribute to SuperDiff and dive into the source code â¤](./contributors/index.md)
+- [File a bug, submit a feature request, or suggest another change â¤](https://github.com/mcmire/super_diff/issues)
diff --git a/docs/users/customization.md b/docs/users/customization.md
new file mode 100644
index 00000000..1cd7a55c
--- /dev/null
+++ b/docs/users/customization.md
@@ -0,0 +1,281 @@
+# Customizing SuperDiff
+
+You can customize the behavior of the gem
+by opening your test helper file
+(`spec/rails_helper.rb` or `spec/spec_helper.rb`)
+and calling `SuperDiff.configure` with a configuration block:
+
+```ruby
+SuperDiff.configure do |config|
+ # ...
+end
+```
+
+The following is a list of options you can set on the configuration object
+along with their defaults:
+
+| name | description | default |
+| ---------------------- | ----------------------------------------------------------------------------- | ----------------------------------------------------------- |
+| `actual_color` | The color used to display "actual" values in diffs | `:yellow` |
+| `border_color` | The color used to display the border in diff keys | `:blue` |
+| `color_enabled` | Whether to colorize output | `true` if `ENV["CI"]` or stdout is a TTY, `false` otherwise |
+| `diff_elision_enabled` | Whether to elide (remove) unchanged lines in diff | `false` |
+| `diff_elision_maximum` | How large a section of consecutive unchanged lines can be before being elided | `0` |
+| `elision_marker_color` | The color used to display the marker substituted for elided lines in a diff | `:cyan` |
+| `expected_color` | The color used to display "expected" values in diffs | `:magenta` |
+| `header_color` | The color used to display the "Diff:" header in failure messages | `:white` |
+| `key_enabled` | Whether to show the key above diffs | `true` |
+
+The following is a list of methods you can call on the configuration object:
+
+| name | description |
+| ------------------------------------------- | ------------------------------------------------------------------- |
+| `add_extra_diff_formatter_classes` | Additional classes with which to format diffs |
+| `add_extra_differ_classes` | Additional classes with which to compute diffs for objects |
+| `add_extra_inspection_tree_builder_classes` | Additional classes used to inspect objects |
+| `add_extra_operation_tree_builder_classes` | Additional classes used to build operation trees for objects |
+| `add_extra_operation_tree_classes` | Additional classes used to hold operations in diffs between objects |
+
+Read on for more information about available kinds of customizations.
+
+### Customizing colors
+
+If you don't like the colors that SuperDiff uses,
+you can change them like so:
+
+```ruby
+SuperDiff.configure do |config|
+ config.actual_color = :green
+ config.expected_color = :red
+ config.border_color = :yellow
+ config.header_color = :yellow
+end
+```
+
+See `CSI::EightBitColor` in the codebase
+for the list of available colors you can use as values here.
+
+You can also completely disable colorized output:
+
+```ruby
+SuperDiff.configure { |config| config.color_enabled = false }
+```
+
+### Disabling the key
+
+By default, when a diff is displayed,
+a key appears above it.
+This key serves to clarify
+which colors and symbols belong to the "expected" and "actual" values.
+However, you can disable the key as follows:
+
+```ruby
+SuperDiff.configure { |config| config.key_enabled = false }
+```
+
+### Hiding unchanged lines
+
+When looking at a large diff made up of many lines that do not change,
+it can be difficult to make out the lines that do.
+Text-oriented diffs,
+such as those you get from a conventional version control system,
+solve this problem by removing or "eliding" those unchanged lines from the diff entirely.
+The same can be done in SuperDiff.
+
+For instance, the following configuration enables diff elision
+and ensures that within a block of unchanged lines,
+a maximum of only 3 lines are displayed:
+
+```ruby
+SuperDiff.configure do |config|
+ config.diff_elision_enabled = true
+ config.diff_elision_maximum = 3
+end
+```
+
+A diff in which some lines are elided may look like this:
+
+```diff
+ [
+ # ...
+ "American Samoa",
+ "Andorra",
+- "Angola",
++ "Anguilla",
+ "Antarctica",
+ "Antigua And Barbuda",
+ # ...
+ ]
+```
+
+as opposed to:
+
+```diff
+ [
+ "Afghanistan",
+ "Aland Islands",
+ "Albania",
+ "Algeria",
+ "American Samoa",
+ "Andorra",
+- "Angola",
++ "Anguilla",
+ "Antarctica",
+ "Antigua And Barbuda",
+ "Argentina",
+ "Armenia",
+ "Aruba",
+ "Australia"
+ ]
+```
+
+### Diffing custom objects
+
+If you are comparing two instances of a class
+which are specific to your project,
+the resulting diff may not look as good
+as diffs involving native or primitive objects.
+This happens because if SuperDiff doesn't recognize a class,
+it will fall back to a generic representation for the diff.
+
+There are two ways to solve this problem.
+
+#### Adding an `attributes_for_super_diff` method
+
+This is the easiest approach.
+If two objects have this method,
+SuperDiff will use the hash that this method returns to compare those objects
+and will compute a diff between them,
+which will show up in the output.
+
+##### Example
+
+For instance, say we have the following classes:
+
+```ruby
+class Http
+ # ...
+end
+
+class Order
+ def initialize(id, number)
+ @id = id
+ @number = number
+ end
+end
+
+class OrderRequestor
+ def initialize(order)
+ @order = order
+ @http_library = Http.new
+ end
+
+ def request
+ @http_library.get("/orders/#{order.id}")
+ end
+end
+
+class OrderTracker
+ def initialize(order)
+ @order = order
+ @requestor = OrderRequestor.new(order)
+ end
+end
+```
+
+and we have two instances of these class as follows:
+
+```ruby
+actual = OrderTracker.new(Order.new(id: 1, number: "1000"))
+expected = OrderTracker.new(Order.new(id: 2, number: "2000"))
+```
+
+If we diff these two objects,
+then we will see something like:
+
+```diff
+ #,
+- @requestor=#,
+- @http_library=#
+- }>
++ @order=#,
++ @requestor=#,
++ @http_library=#
++ }>
+ }>
+```
+
+It is not difficult to see that this diff is fairly noisy.
+It would be good if we could exclude `requestor`,
+since it's a bit redundant,
+and it would help if we could collapse some of the lines as well.
+We also don't need to know the address of each object
+(the `0xXXXXXXXXX` bit).
+
+We can easily solve this
+by adding an `attributes_for_super_diff` method to OrderTracker,
+making sure to exclude `requestor`,
+and by adding a similar method to Order as well.
+
+```diff
+ class Order
+ def initialize(id, number)
+ @id = id
+ @number = number
+ end
++
++ def attributes_for_super_diff
++ { id: @id, number: @number }
++ end
+ end
+
+ class OrderTracker
+ def initialize(order)
+ @order = order
+ @requestor = OrderRequestor.new(order)
+ end
++
++ def attributes_for_super_diff
++ { order: @order }
++ end
+ end
+```
+
+If we performed another diff, we would now get:
+
+```diff
+ #
+ }>
+```
+
+#### Registering new building blocks
+
+This approach is more advanced,
+but also offers the greatest flexibility.
+
+More information will be added here on how to do this,
+but in the meantime,
+the best example is the [RSpec integration](https://github.com/mcmire/super_diff/blob/v0.11.0/lib/super_diff/rspec.rb#L91) in SuperDiff itself.
diff --git a/docs/users/getting-started.md b/docs/users/getting-started.md
new file mode 100644
index 00000000..019b1ec5
--- /dev/null
+++ b/docs/users/getting-started.md
@@ -0,0 +1,118 @@
+# Getting Started
+
+SuperDiff is designed to be used different ways, depending on the type of project.
+
+## Using SuperDiff with RSpec in a Rails app
+
+If you're developing a Rails app,
+run the following command to add SuperDiff to your project:
+
+```bash
+bundle add super_diff --group test
+```
+
+Then add the following toward the top of `spec/rails_helper.rb`:
+
+```ruby
+require "super_diff/rspec-rails"
+```
+
+At this point, you can write tests for parts of your app,
+and SuperDiff will be able to diff Rails-specific objects
+such as ActiveRecord models,
+ActionController response objects,
+instances of HashWithIndifferentAccess, etc.,
+in addition to objects that ship with RSpec,
+such as matchers.
+
+You can now continue on to [customizing SuperDiff](./customization.md).
+
+## Using SuperDiff with RSpec in a project using parts of Rails
+
+If you're developing an app using Hanami or Sinatra,
+or merely using a part of Rails such as ActiveModel,
+run the following command to add SuperDiff to your project:
+
+```bash
+bundle add super_diff
+```
+
+After running `bundle install`,
+add the following toward the top of `spec/spec_helper.rb`:
+
+```ruby
+require "super_diff/rspec"
+```
+
+Then, add one or all of the following lines:
+
+```ruby
+require "super_diff/active_support"
+require "super_diff/active_record"
+```
+
+At this point, you can write tests for parts of your app,
+and SuperDiff will be able to diff objects depending on which path you required.
+For instance, if you required `super_diff/active_support`,
+then SuperDiff will be able to diff objects defined in ActiveSupport,
+such as HashWithIndifferentAccess,
+and if you required `super_diff/active_record`,
+it will be able to diff ActiveRecord models.
+In addition to these,
+it will also be able to diff objects that ship with RSpec,
+such as matchers.
+
+You can now continue on to [customizing SuperDiff](./customization.md).
+
+## Using SuperDiff with RSpec in a Ruby project
+
+If you're developing a library or other project
+that does not depend on any part of Rails,
+run the following command to add SuperDiff to your project:
+
+```bash
+bundle add super_diff
+```
+
+Now add the following toward the top of `spec/spec_helper.rb`:
+
+```ruby
+require "super_diff/rspec"
+```
+
+At this point, you can write tests for parts of your app,
+and SuperDiff will be able to diff objects that ship with RSpec,
+such as matchers.
+
+You can now continue on to [customizing SuperDiff](./customization.md).
+
+## Using parts of SuperDiff directly
+
+Although SuperDiff is primarily designed to integrate with RSpec,
+it can also be used on its own in other kinds of applications.
+
+First, install the gem:
+
+```bash
+bundle add super_diff
+```
+
+Then, require it somewhere:
+
+```ruby
+require "super_diff"
+```
+
+If you want to compare two objects and display a friendly diff,
+you can use the equality matcher interface:
+
+```ruby
+SuperDiff::EqualityMatchers::Main.call(expected, actual)
+```
+
+Or, if you want to compare two objects and get a lower-level list of operations,
+you can use the differ interface:
+
+```ruby
+SuperDiff::Differs::Main.call(expected, actual)
+```
diff --git a/docs/users/index.md b/docs/users/index.md
new file mode 100644
index 00000000..e2624697
--- /dev/null
+++ b/docs/users/index.md
@@ -0,0 +1,74 @@
+# Introduction to SuperDiff
+
+**SuperDiff** is a Ruby gem
+which is designed to display the differences between two objects of any type
+in a familiar and intelligent fashion.
+
+The primary motivation behind this gem
+is to vastly improve upon RSpec's built-in diffing capabilities.
+RSpec has many nice features,
+and one of them is that whenever you use a matcher such as `eq`, `match`, `include`, or `have_attributes`,
+you will get a diff of the two data structures you are trying to match against.
+This is great if all you want to do is compare multi-line strings.
+But if you want to compare other, more "real world" kinds of values such as API or database data,
+then you are out of luck.
+Since [RSpec merely runs your `expected` and `actual` values through Ruby's PrettyPrinter library][rspec-differ-fail]
+and then performs a diff of these strings,
+the output it produces leaves much to be desired.
+
+[rspec-differ-fail]: https://github.com/rspec/rspec-support/blob/c69a231d7369dd165ad7ce4742e1a2e21e3462b5/lib/rspec/support/differ.rb#L178
+
+For instance, let's say you wanted to compare these two hashes:
+
+```ruby
+actual = {
+ customer: {
+ person: SuperDiff::Test::Person.new(name: "Marty McFly, Jr.", age: 17),
+ shipping_address: {
+ line_1: "456 Ponderosa Ct.",
+ city: "Hill Valley",
+ state: "CA",
+ zip: "90382"
+ }
+ },
+ items: [
+ { name: "Fender Stratocaster", cost: 100_000, options: %w[red blue green] },
+ { name: "Mattel Hoverboard" }
+ ]
+}
+
+expected = {
+ customer: {
+ person: SuperDiff::Test::Person.new(name: "Marty McFly", age: 17),
+ shipping_address: {
+ line_1: "123 Main St.",
+ city: "Hill Valley",
+ state: "CA",
+ zip: "90382"
+ }
+ },
+ items: [
+ { name: "Fender Stratocaster", cost: 100_000, options: %w[red blue green] },
+ { name: "Chevy 4x4" }
+ ]
+}
+```
+
+If, somewhere in a test, you were to say:
+
+```ruby
+expect(actual).to eq(expected)
+```
+
+You would get output that looks like this:
+
+![Before super_diff](../assets/before.png)
+
+What this library does
+is to provide a diff engine
+that knows how to figure out the differences between any two data structures
+and display them in a sensible way.
+So, using the example above,
+you'd get this instead:
+
+![After super_diff](../assets/after.png)
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 00000000..e2fb439d
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,48 @@
+site_name: SuperDiff Documentation
+site_description: User- and contributor-facing documentation for SuperDiff
+copyright: Šī¸ Elliot Winkler.
+
+# NOTE: Don't do this for now as it loads the version and stars dynamically
+#repo_url: https://github.com/mcmire/super_diff
+#repo_name: SuperDiff on GitHub
+
+theme:
+ name: material
+ features:
+ - navigation.instant
+ - navigation.instant.progress
+ - navigation.tabs
+ - navigation.tabs.sticky
+ - navigation.path
+ - toc.integrate
+ #plugins:
+ #- search
+
+nav:
+ - Home: index.md
+ - User Documentation:
+ - "Introduction to SuperDiff": "users/index.md"
+ - "Getting Started": "users/getting-started.md"
+ - "Customizing SuperDiff": "users/customization.md"
+ - Contributor Documentation:
+ - "Home": "contributors/index.md"
+ - "How to Contribute": "contributors/how-to-contribute.md"
+ - Architecture:
+ - "Introduction": "contributors/architecture/introduction.md"
+ - "How RSpec works": "contributors/architecture/how-rspec-works.md"
+ - "How SuperDiff works": "contributors/architecture/how-super-diff-works.md"
+
+#plugins:
+#- entangled # this also runs `entangled sync` as a pre-build action
+
+markdown_extensions:
+ - admonition
+ - footnotes
+ - smarty
+ - pymdownx.superfences:
+ custom_fences:
+ - name: mermaid
+ class: mermaid
+ format: !!python/name:pymdownx.superfences.fence_code_format
+ - pymdownx.tabbed:
+ alternate_style: true
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 00000000..94cb9988
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,17 @@
+[tool.poetry]
+name = "super-diff"
+version = "0.0.0"
+description = "A more helpful way to view differences between complex data structures in RSpec"
+authors = ["Elliot Winkler "]
+license = "MIT"
+readme = "README.md"
+
+[tool.poetry.group.dev.dependencies]
+python = "^3.12"
+mkdocs = { version = "^1.5.3", python = ">=3.7" }
+mkdocs-material = { version = "^9.5.9", python = ">=3.8" }
+
+
+[build-system]
+requires = ["poetry-core"]
+build-backend = "poetry.core.masonry.api"
diff --git a/scripts/collect-release-info.rb b/scripts/collect-release-info.rb
new file mode 100755
index 00000000..f74a8179
--- /dev/null
+++ b/scripts/collect-release-info.rb
@@ -0,0 +1,19 @@
+#!/usr/bin/env ruby
+
+github_output = File.open(ENV.fetch("GITHUB_OUTPUT"), "a")
+
+spec = Gem::Specification.load("super_diff.gemspec")
+current_version = spec.version
+
+latest_version = Gem.latest_version_for("super_diff")
+
+puts "Current version is #{current_version}, latest version is #{latest_version}"
+
+if current_version == latest_version
+ puts "This isn't a new release."
+ github_output.puts("IS_NEW_RELEASE=false")
+else
+ puts "Looks like a new release!"
+ github_output.puts("IS_NEW_RELEASE=true")
+ github_output.puts("RELEASE_VERSION=#{current_version}")
+end