diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml
index 4aacf61112336fd3d0a23ea3485a7f1650dd1b37..16862992b5672e4b5235584bb6ac23cf22f7e0e6 100644
--- a/.gitlab/ci/rules.gitlab-ci.yml
+++ b/.gitlab/ci/rules.gitlab-ci.yml
@@ -1490,6 +1490,12 @@
       changes: ["vendor/gems/mail-smtp_pool/**/*"]
     - <<: *if-merge-request-labels-run-all-rspec
 
+.vendor:rules:ipynbdiff:
+  rules:
+    - <<: *if-merge-request
+      changes: ["vendor/gems/ipynbdiff/**/*"]
+    - <<: *if-merge-request-labels-run-all-rspec
+
 ##################
 # Releases rules #
 ##################
diff --git a/.gitlab/ci/vendored-gems.gitlab-ci.yml b/.gitlab/ci/vendored-gems.gitlab-ci.yml
index a39c4307c13e095a9673b6a0eb370e98f2f8bf48..ce71820100f991fb7731708b6e3a834b24fc66d3 100644
--- a/.gitlab/ci/vendored-gems.gitlab-ci.yml
+++ b/.gitlab/ci/vendored-gems.gitlab-ci.yml
@@ -5,3 +5,10 @@ vendor mail-smtp_pool:
   trigger:
     include: vendor/gems/mail-smtp_pool/.gitlab-ci.yml
     strategy: depend
+vendor ipynbdiff:
+  extends:
+    - .vendor:rules:ipynbdiff
+  needs: []
+  trigger:
+    include: vendor/gems/ipynbdiff/.gitlab-ci.yml
+    strategy: depend
diff --git a/Gemfile b/Gemfile
index c554d509d0efb04ee3a32145b32c46347618c902..4e320d223e244bf23fb390dde2d9061d3a4fa962 100644
--- a/Gemfile
+++ b/Gemfile
@@ -546,6 +546,6 @@ gem 'ipaddress', '~> 0.8.3'
 
 gem 'parslet', '~> 1.8'
 
-gem 'ipynbdiff', '0.4.7'
+gem 'ipynbdiff', path: 'vendor/gems/ipynbdiff'
 
 gem 'ed25519', '~> 1.3.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index dd2b7196115a32a38599a0b1c685fdea292393dd..7c84d4842506912df9cfc6e2a7ebeaabcf196508 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,3 +1,10 @@
+PATH
+  remote: vendor/gems/ipynbdiff
+  specs:
+    ipynbdiff (0.4.7)
+      diffy (~> 3.3)
+      json (~> 2.5, >= 2.5.1)
+
 PATH
   remote: vendor/gems/mail-smtp_pool
   specs:
@@ -667,9 +674,6 @@ GEM
     invisible_captcha (1.1.0)
       rails (>= 4.2)
     ipaddress (0.8.3)
-    ipynbdiff (0.4.7)
-      diffy (~> 3.3)
-      json (~> 2.5, >= 2.5.1)
     jaeger-client (1.1.0)
       opentracing (~> 0.3)
       thrift
@@ -1575,7 +1579,7 @@ DEPENDENCIES
   icalendar
   invisible_captcha (~> 1.1.0)
   ipaddress (~> 0.8.3)
-  ipynbdiff (= 0.4.7)
+  ipynbdiff!
   jira-ruby (~> 2.1.4)
   js_regex (~> 3.7)
   json (~> 2.5.1)
diff --git a/vendor/gems/ipynbdiff/.gitignore b/vendor/gems/ipynbdiff/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..4f284c35a42069bc139e86aa9edc504e0978ebdb
--- /dev/null
+++ b/vendor/gems/ipynbdiff/.gitignore
@@ -0,0 +1,2 @@
+*.gem
+.bundle
diff --git a/vendor/gems/ipynbdiff/.gitlab-ci.yml b/vendor/gems/ipynbdiff/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..7b0c9df6cd9fde227031dba7a331c5d29f430334
--- /dev/null
+++ b/vendor/gems/ipynbdiff/.gitlab-ci.yml
@@ -0,0 +1,32 @@
+# You can override the included template(s) by including variable overrides
+# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
+# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
+# Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings
+# Note that environment variables can be set in several places
+# See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
+workflow:
+  rules:
+    - if: $CI_MERGE_REQUEST_ID
+
+.rspec:
+  cache:
+    key: ipynbdiff
+    paths:
+      - vendor/gems/ipynbdiff/vendor/ruby
+  before_script:
+    - cd vendor/gems/ipynbdiff
+    - ruby -v                                   # Print out ruby version for debugging
+    - gem install bundler --no-document         # Bundler is not installed with the image
+    - bundle config set --local path 'vendor'   # Install dependencies into ./vendor/ruby
+    - bundle config set with 'development'
+    - bundle install -j $(nproc)
+  script:
+    - bundle exec rspec
+
+rspec-2.7:
+  image: "ruby:2.7"
+  extends: .rspec
+
+rspec-3.0:
+  image: "ruby:3.0"
+  extends: .rspec
diff --git a/vendor/gems/ipynbdiff/Gemfile b/vendor/gems/ipynbdiff/Gemfile
new file mode 100644
index 0000000000000000000000000000000000000000..7f4f5e950d1572e1ce5607ca237375aa2e30d662
--- /dev/null
+++ b/vendor/gems/ipynbdiff/Gemfile
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+source 'https://rubygems.org'
+
+gemspec
diff --git a/vendor/gems/ipynbdiff/Gemfile.lock b/vendor/gems/ipynbdiff/Gemfile.lock
new file mode 100644
index 0000000000000000000000000000000000000000..a5e8e3e4e86bb90231e5dd2c2d2e46f074de5643
--- /dev/null
+++ b/vendor/gems/ipynbdiff/Gemfile.lock
@@ -0,0 +1,64 @@
+PATH
+  remote: .
+  specs:
+    ipynbdiff (0.4.7)
+      diffy (~> 3.3)
+      json (~> 2.5, >= 2.5.1)
+
+GEM
+  remote: https://rubygems.org/
+  specs:
+    ast (2.4.2)
+    binding_ninja (0.2.3)
+    coderay (1.1.3)
+    diff-lcs (1.5.0)
+    diffy (3.4.2)
+    json (2.6.2)
+    method_source (1.0.0)
+    parser (3.1.2.0)
+      ast (~> 2.4.1)
+    proc_to_ast (0.1.0)
+      coderay
+      parser
+      unparser
+    pry (0.14.1)
+      coderay (~> 1.1)
+      method_source (~> 1.0)
+    rake (13.0.6)
+    rspec (3.11.0)
+      rspec-core (~> 3.11.0)
+      rspec-expectations (~> 3.11.0)
+      rspec-mocks (~> 3.11.0)
+    rspec-core (3.11.0)
+      rspec-support (~> 3.11.0)
+    rspec-expectations (3.11.0)
+      diff-lcs (>= 1.2.0, < 2.0)
+      rspec-support (~> 3.11.0)
+    rspec-mocks (3.11.1)
+      diff-lcs (>= 1.2.0, < 2.0)
+      rspec-support (~> 3.11.0)
+    rspec-parameterized (0.5.1)
+      binding_ninja (>= 0.2.3)
+      parser
+      proc_to_ast
+      rspec (>= 2.13, < 4)
+      unparser
+    rspec-support (3.11.0)
+    unparser (0.6.5)
+      diff-lcs (~> 1.3)
+      parser (>= 3.1.0)
+
+PLATFORMS
+  x86_64-darwin-20
+  x86_64-linux
+
+DEPENDENCIES
+  bundler (~> 2.2)
+  ipynbdiff!
+  pry (~> 0.14)
+  rake (~> 13.0)
+  rspec (~> 3.10)
+  rspec-parameterized (~> 0.5.1)
+
+BUNDLED WITH
+   2.3.16
diff --git a/vendor/gems/ipynbdiff/LICENSE b/vendor/gems/ipynbdiff/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..e6de2f908640470f7423b29f745cf07679425688
--- /dev/null
+++ b/vendor/gems/ipynbdiff/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016-2021 GitLab B.V.
+
+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/vendor/gems/ipynbdiff/README.md b/vendor/gems/ipynbdiff/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..f046f678a4d168a35838256a9e31ea155c918863
--- /dev/null
+++ b/vendor/gems/ipynbdiff/README.md
@@ -0,0 +1,56 @@
+# IpynbDiff: Better diff for Jupyter Notebooks
+
+This is a simple diff tool that cleans up Jupyter notebooks, transforming each [notebook](example/1/from.ipynb)
+into a [readable markdown file](example/1/from_html.md), keeping the output of cells, and running the
+diff after. Markdowns are generated using an opinionated Jupyter to Markdown conversion. This means
+that the entire file is readable on the diff.
+
+The result are diffs that are much easier to read:
+
+| Diff                                | IpynbDiff                                             |
+| ----------------------------------- | ----------------------------------------------------- |
+| [Diff text](example/diff.txt)       | [IpynbDiff text](example/ipynbdiff_percent.txt)       |
+| ![Diff image](example/img/diff.png) | ![IpynbDiff image](example/img/ipynbdiff_percent.png) |
+
+This started as a port of [ipynbdiff](https://gitlab.com/gitlab-org/incubation-engineering/mlops/poc/ipynbdiff),
+but now has extended functionality although not working as git driver.
+
+## Usage
+
+### Generating diffs
+
+```ruby
+IpynbDiff.diff(from_path, to_path, options)
+```
+
+Options:
+
+```ruby
+@default_transform_options = {
+  preprocess_input: true, # Whether the input should be transformed
+  write_output_to: nil, # Pass a path to save the output to a file
+  format: :text, # These are the formats Diffy accepts https://github.com/samg/diffy
+  sources_are_files: false, # Weather to use the from/to as string or path to a file
+  raise_if_invalid_notebook: false, # Raises an error if the notebooks are invalid, otherwise returns nil
+  transform_options: @default_transform_options, # See below for transform options
+  diff_opts: {
+    include_diff_info: false # These are passed to Diffy https://github.com/samg/diffy
+  }
+}
+```
+
+### Transforming the notebooks
+
+It might be necessary to have the transformed files in addition to the diff.
+
+```ruby
+IpynbDiff.transform(notebook, options)
+```
+
+Options:
+
+```ruby
+@default_transform_options = {
+    include_frontmatter: false, # Whether to include or not the notebook metadata (kernel, language, etc)
+}
+```
diff --git a/vendor/gems/ipynbdiff/ipynbdiff.gemspec b/vendor/gems/ipynbdiff/ipynbdiff.gemspec
new file mode 100644
index 0000000000000000000000000000000000000000..9ace051b4966f135d1bca9e6f7b91934252313d6
--- /dev/null
+++ b/vendor/gems/ipynbdiff/ipynbdiff.gemspec
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+lib = File.expand_path('lib/..', __dir__)
+$LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib)
+
+require 'lib/version'
+
+Gem::Specification.new do |s|
+  s.name        = 'ipynbdiff'
+  s.version     = IpynbDiff::VERSION
+  s.summary     = 'Human Readable diffs for Jupyter Notebooks'
+  s.description = 'Better diff for Jupyter Notebooks by first preprocessing them and removing clutter'
+  s.authors     = ['Eduardo Bonet']
+  s.email       = 'ebonet@gitlab.com'
+  # Specify which files should be added to the gem when it is released.
+  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
+  s.files = Dir.chdir(File.expand_path(__dir__)) do
+    `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec|example)/}) }
+  end
+  s.homepage =
+    'https://gitlab.com/gitlab-org/incubation-engineering/mlops/rb-ipynbdiff'
+  s.license       = 'MIT'
+
+  s.require_paths = ['lib']
+
+  s.add_runtime_dependency 'diffy', '~> 3.3'
+  s.add_runtime_dependency 'json', '~> 2.5', '>= 2.5.1'
+
+  s.add_development_dependency 'bundler', '~> 2.2'
+  s.add_development_dependency 'pry', '~> 0.14'
+  s.add_development_dependency 'rake', '~> 13.0'
+  s.add_development_dependency 'rspec', '~> 3.10'
+  s.add_development_dependency 'rspec-parameterized', '~> 0.5.1'
+end
diff --git a/vendor/gems/ipynbdiff/lib/diff.rb b/vendor/gems/ipynbdiff/lib/diff.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3554ac55d9936caba849bc2c2fa729e3d4946ba4
--- /dev/null
+++ b/vendor/gems/ipynbdiff/lib/diff.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+# Custom differ for Jupyter Notebooks
+module IpynbDiff
+  require 'delegate'
+
+  # The result of a diff object
+  class Diff < SimpleDelegator
+    require 'diffy'
+
+    attr_reader :from, :to
+
+    def initialize(from, to, diffy_opts)
+      super(Diffy::Diff.new(from.as_text, to.as_text, **diffy_opts))
+
+      @from = from
+      @to = to
+    end
+  end
+end
diff --git a/vendor/gems/ipynbdiff/lib/ipynb_symbol_map.rb b/vendor/gems/ipynbdiff/lib/ipynb_symbol_map.rb
new file mode 100644
index 0000000000000000000000000000000000000000..33e06aa8d189d925ae8eb67607a6e7aaf7db643d
--- /dev/null
+++ b/vendor/gems/ipynbdiff/lib/ipynb_symbol_map.rb
@@ -0,0 +1,218 @@
+# frozen_string_literal: true
+
+module IpynbDiff
+  class InvalidTokenError < StandardError
+  end
+
+  # Creates a symbol map for a ipynb file (JSON format)
+  class IpynbSymbolMap
+    class << self
+      def parse(notebook, objects_to_ignore = [])
+        IpynbSymbolMap.new(notebook, objects_to_ignore).parse('')
+      end
+    end
+
+    attr_reader :current_line, :char_idx, :results
+
+    WHITESPACE_CHARS = ["\t", "\r", ' ', "\n"].freeze
+
+    VALUE_STOPPERS = [',', '[', ']', '{', '}', *WHITESPACE_CHARS].freeze
+
+    def initialize(notebook, objects_to_ignore = [])
+      @chars = notebook.chars
+      @current_line = 0
+      @char_idx = 0
+      @results = {}
+      @objects_to_ignore = objects_to_ignore
+    end
+
+    def parse(prefix = '.')
+      raise_if_file_ended
+
+      skip_whitespaces
+
+      if (c = current_char) == '"'
+        parse_string
+      elsif c == '['
+        parse_array(prefix)
+      elsif c == '{'
+        parse_object(prefix)
+      else
+        parse_value
+      end
+
+      results
+    end
+
+    def parse_array(prefix)
+      # [1, 2, {"some": "object"}, [1]]
+
+      i = 0
+
+      current_should_be '['
+
+      loop do
+        raise_if_file_ended
+
+        break if skip_beginning(']')
+
+        new_prefix = "#{prefix}.#{i}"
+
+        add_result(new_prefix, current_line)
+
+        parse(new_prefix)
+
+        i += 1
+      end
+    end
+
+    def parse_object(prefix)
+      # {"name":"value", "another_name": [1, 2, 3]}
+
+      current_should_be '{'
+
+      loop do
+        raise_if_file_ended
+
+        break if skip_beginning('}')
+
+        prop_name = parse_string(return_value: true)
+
+        next_and_skip_whitespaces
+
+        current_should_be ':'
+
+        next_and_skip_whitespaces
+
+        if @objects_to_ignore.include? prop_name
+          skip
+        else
+          new_prefix = "#{prefix}.#{prop_name}"
+
+          add_result(new_prefix, current_line)
+
+          parse(new_prefix)
+        end
+      end
+    end
+
+    def parse_string(return_value: false)
+      current_should_be '"'
+      init_idx = @char_idx
+
+      loop do
+        increment_char_index
+
+        raise_if_file_ended
+
+        if current_char == '"' && !prev_backslash?
+          init_idx += 1
+          break
+        end
+      end
+
+      @chars[init_idx...@char_idx].join if return_value
+    end
+
+    def add_result(key, line_number)
+      @results[key] = line_number
+    end
+
+    def parse_value
+      increment_char_index until raise_if_file_ended || VALUE_STOPPERS.include?(current_char)
+    end
+
+    def skip_whitespaces
+      while WHITESPACE_CHARS.include?(current_char)
+        raise_if_file_ended
+        check_for_new_line
+        increment_char_index
+      end
+    end
+
+    def increment_char_index
+      @char_idx += 1
+    end
+
+    def next_and_skip_whitespaces
+      increment_char_index
+      skip_whitespaces
+    end
+
+    def current_char
+      raise_if_file_ended
+
+      @chars[@char_idx]
+    end
+
+    def prev_backslash?
+      @chars[@char_idx - 1] == '\\' && @chars[@char_idx - 2] != '\\'
+    end
+
+    def current_should_be(another_char)
+      raise InvalidTokenError unless current_char == another_char
+    end
+
+    def check_for_new_line
+      @current_line += 1 if current_char == "\n"
+    end
+
+    def raise_if_file_ended
+      @char_idx >= @chars.size && raise(InvalidTokenError)
+    end
+
+    def skip
+      raise_if_file_ended
+
+      skip_whitespaces
+
+      if (c = current_char) == '"'
+        parse_string
+      elsif c == '['
+        skip_array
+      elsif c == '{'
+        skip_object
+      else
+        parse_value
+      end
+    end
+
+    def skip_array
+      loop do
+        raise_if_file_ended
+
+        break if skip_beginning(']')
+
+        skip
+      end
+    end
+
+    def skip_object
+      loop do
+        raise_if_file_ended
+
+        break if skip_beginning('}')
+
+        parse_string
+
+        next_and_skip_whitespaces
+
+        current_should_be ':'
+
+        next_and_skip_whitespaces
+
+        skip
+      end
+    end
+
+    def skip_beginning(closing_char)
+      check_for_new_line
+
+      next_and_skip_whitespaces
+
+      return true if current_char == closing_char
+
+      next_and_skip_whitespaces if current_char == ','
+    end
+  end
+end
diff --git a/vendor/gems/ipynbdiff/lib/ipynbdiff.rb b/vendor/gems/ipynbdiff/lib/ipynbdiff.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1765e434bf9539eb84a1365ae214c8a4bfb1d7e6
--- /dev/null
+++ b/vendor/gems/ipynbdiff/lib/ipynbdiff.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+# Human Readable Jupyter Diffs
+module IpynbDiff
+  require 'transformer'
+  require 'diff'
+
+  def self.diff(from, to, raise_if_invalid_nb: false, include_frontmatter: false, hide_images: false, diffy_opts: {})
+    transformer = Transformer.new(include_frontmatter: include_frontmatter, hide_images: hide_images)
+
+    Diff.new(transformer.transform(from), transformer.transform(to), diffy_opts)
+  rescue InvalidNotebookError
+    raise if raise_if_invalid_nb
+  end
+
+  def self.transform(notebook, raise_errors: false, include_frontmatter: true, hide_images: false)
+    return unless notebook
+
+    Transformer.new(include_frontmatter: include_frontmatter, hide_images: hide_images).transform(notebook).as_text
+  rescue InvalidNotebookError
+    raise if raise_errors
+  end
+end
diff --git a/vendor/gems/ipynbdiff/lib/output_transformer.rb b/vendor/gems/ipynbdiff/lib/output_transformer.rb
new file mode 100644
index 0000000000000000000000000000000000000000..88728df2f173e3f188ede525986dbc073f809636
--- /dev/null
+++ b/vendor/gems/ipynbdiff/lib/output_transformer.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+module IpynbDiff
+  # Transforms Jupyter output data into markdown
+  class OutputTransformer
+    require 'symbolized_markdown_helper'
+    include SymbolizedMarkdownHelper
+
+    HIDDEN_IMAGE_OUTPUT = '    [Hidden Image Output]'
+
+    ORDERED_KEYS = {
+      'execute_result' => %w[image/png image/svg+xml image/jpeg text/markdown text/latex text/plain],
+      'display_data' => %w[image/png image/svg+xml image/jpeg text/markdown text/latex],
+      'stream' => %w[text]
+    }.freeze
+
+    def initialize(hide_images: false)
+      @hide_images = hide_images
+    end
+
+    def transform(output, symbol)
+      transformed = case (output_type = output['output_type'])
+                    when 'error'
+                      transform_error(output['traceback'], symbol / 'traceback')
+                    when 'execute_result', 'display_data'
+                      transform_non_error(ORDERED_KEYS[output_type], output['data'], symbol / 'data')
+                    when 'stream'
+                      transform_element('text', output['text'], symbol)
+                    end
+
+      transformed ? decorate_output(transformed, output, symbol) : []
+    end
+
+    def decorate_output(output_rows, output, symbol)
+      [
+        _,
+        _(symbol, %(%%%% Output: #{output['output_type']})),
+        _,
+        *output_rows
+      ]
+    end
+
+    def transform_error(traceback, symbol)
+      traceback.map.with_index do |t, idx|
+        t.split("\n").map do |l|
+          _(symbol / idx, l.gsub(/\[[0-9][0-9;]*m/, '').sub("\u001B", '    ').gsub(/\u001B/, '').rstrip)
+        end
+      end
+    end
+
+    def transform_non_error(accepted_keys, elements, symbol)
+      accepted_keys.filter { |key| elements.key?(key) }.map do |key|
+        transform_element(key, elements[key], symbol)
+      end
+    end
+
+    def transform_element(output_type, output_element, symbol_prefix)
+      new_symbol = symbol_prefix / output_type
+      case output_type
+      when 'image/png', 'image/jpeg'
+        transform_image(output_type + ';base64', output_element, new_symbol)
+      when 'image/svg+xml'
+        transform_image(output_type + ';utf8', output_element, new_symbol)
+      when 'text/markdown', 'text/latex', 'text/plain', 'text'
+        transform_text(output_element, new_symbol)
+      end
+    end
+
+    def transform_image(image_type, image_content, symbol)
+      return _(nil, HIDDEN_IMAGE_OUTPUT) if @hide_images
+
+      lines = image_content.is_a?(Array) ? image_content : [image_content]
+
+      single_line = lines.map(&:strip).join.gsub(/\s+/, ' ')
+
+      _(symbol, "    ![](data:#{image_type},#{single_line})")
+    end
+
+    def transform_text(text_content, symbol)
+      symbolize_array(symbol, text_content) { |l| "    #{l.rstrip}" }
+    end
+  end
+end
diff --git a/vendor/gems/ipynbdiff/lib/symbolized_markdown_helper.rb b/vendor/gems/ipynbdiff/lib/symbolized_markdown_helper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..918666ed899d920296d025d74e7d566ad17200f1
--- /dev/null
+++ b/vendor/gems/ipynbdiff/lib/symbolized_markdown_helper.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module IpynbDiff
+  # Helper functions
+  module SymbolizedMarkdownHelper
+
+    def _(symbol = nil, content = '')
+      { symbol: symbol, content: content }
+    end
+
+    def symbolize_array(symbol, content, &block)
+      if content.is_a?(Array)
+        content.map.with_index { |l, idx| _(symbol / idx, block.call(l)) }
+      else
+        _(symbol, content)
+      end
+    end
+  end
+
+  # Simple wrapper for a string
+  class JsonSymbol < String
+    def /(other)
+      JsonSymbol.new((other.is_a?(Array) ? [self, *other] : [self, other]).join('.'))
+    end
+  end
+end
diff --git a/vendor/gems/ipynbdiff/lib/transformed_notebook.rb b/vendor/gems/ipynbdiff/lib/transformed_notebook.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7a8edf7c22f16ed2a38a7bd6bb47aae4a531c6eb
--- /dev/null
+++ b/vendor/gems/ipynbdiff/lib/transformed_notebook.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module IpynbDiff
+  # Notebook that was transformed into md, including location of source cells
+  class TransformedNotebook
+    attr_reader :blocks
+
+    def as_text
+      @blocks.map { |b| b[:content] }.join("\n")
+    end
+
+    private
+
+    def initialize(lines = [], symbol_map = {})
+      @blocks = lines.map do |line|
+        { content: line[:content], source_symbol: (symbol = line[:symbol]), source_line: symbol && symbol_map[symbol] }
+      end
+    end
+  end
+end
diff --git a/vendor/gems/ipynbdiff/lib/transformer.rb b/vendor/gems/ipynbdiff/lib/transformer.rb
new file mode 100644
index 0000000000000000000000000000000000000000..153d821db2797df57858210eb8d820744ff8ca99
--- /dev/null
+++ b/vendor/gems/ipynbdiff/lib/transformer.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+
+module IpynbDiff
+  class InvalidNotebookError < StandardError
+  end
+
+  # Returns a markdown version of the Jupyter Notebook
+  class Transformer
+    require 'json'
+    require 'yaml'
+    require 'output_transformer'
+    require 'symbolized_markdown_helper'
+    require 'ipynb_symbol_map'
+    require 'transformed_notebook'
+    include SymbolizedMarkdownHelper
+
+    @include_frontmatter = true
+    @objects_to_ignore = ['application/javascript', 'application/vnd.holoviews_load.v0+json']
+
+    def initialize(include_frontmatter: true, hide_images: false)
+      @include_frontmatter = include_frontmatter
+      @hide_images = hide_images
+      @output_transformer = OutputTransformer.new(hide_images: hide_images)
+    end
+
+    def validate_notebook(notebook)
+      notebook_json = JSON.parse(notebook)
+
+      return notebook_json if notebook_json.key?('cells')
+
+      raise InvalidNotebookError
+    rescue JSON::ParserError
+      raise InvalidNotebookError
+    end
+
+    def transform(notebook)
+      return TransformedNotebook.new unless notebook
+
+      notebook_json = validate_notebook(notebook)
+      transformed = transform_document(notebook_json)
+      symbol_map = IpynbSymbolMap.parse(notebook)
+
+      TransformedNotebook.new(transformed, symbol_map)
+    end
+
+    def transform_document(notebook)
+      symbol = JsonSymbol.new('.cells')
+
+      transformed_blocks = notebook['cells'].map.with_index do |cell, idx|
+        decorate_cell(transform_cell(cell, notebook, symbol / idx), cell, symbol / idx)
+      end
+
+      transformed_blocks.prepend(transform_metadata(notebook)) if @include_frontmatter
+      transformed_blocks.flatten
+    end
+
+    def decorate_cell(rows, cell, symbol)
+      tags = cell['metadata']&.fetch('tags', [])
+      type = cell['cell_type'] || 'raw'
+
+      [
+        _(symbol, %(%% Cell type:#{type} id:#{cell['id']} tags:#{tags&.join(',')})),
+        _,
+        rows,
+        _
+      ]
+    end
+
+    def transform_cell(cell, notebook, symbol)
+      cell['cell_type'] == 'code' ? transform_code_cell(cell, notebook, symbol) : transform_text_cell(cell, symbol)
+    end
+
+    def transform_code_cell(cell, notebook, symbol)
+      [
+        _(symbol / 'source', %(``` #{notebook.dig('metadata', 'kernelspec', 'language') || ''})),
+        symbolize_array(symbol / 'source', cell['source'], &:rstrip),
+        _(nil, '```'),
+        cell['outputs'].map.with_index do |output, idx|
+          @output_transformer.transform(output, symbol / ['outputs', idx])
+        end
+      ]
+    end
+
+    def transform_text_cell(cell, symbol)
+      symbolize_array(symbol / 'source', cell['source'], &:rstrip)
+    end
+
+    def transform_metadata(notebook_json)
+      as_yaml = {
+        'jupyter' => {
+          'kernelspec' => notebook_json['metadata']['kernelspec'],
+          'language_info' => notebook_json['metadata']['language_info'],
+          'nbformat' => notebook_json['nbformat'],
+          'nbformat_minor' => notebook_json['nbformat_minor']
+        }
+      }.to_yaml
+
+      as_yaml.split("\n").map { |l| _(nil, l) }.append(_(nil, '---'), _)
+    end
+  end
+end
diff --git a/vendor/gems/ipynbdiff/lib/version.rb b/vendor/gems/ipynbdiff/lib/version.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1451bb4ef329ee0a563aa79a9cc2af7130333253
--- /dev/null
+++ b/vendor/gems/ipynbdiff/lib/version.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+module IpynbDiff
+  VERSION = '0.4.7'
+end
diff --git a/vendor/gems/ipynbdiff/spec/ipynb_symbol_map_spec.rb b/vendor/gems/ipynbdiff/spec/ipynb_symbol_map_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a002fc370f546dc1e34b73a8e7ee14617a2d4a4e
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/ipynb_symbol_map_spec.rb
@@ -0,0 +1,165 @@
+# frozen_string_literal: true
+
+require 'rspec'
+require 'json'
+require 'rspec-parameterized'
+require 'ipynb_symbol_map'
+
+describe IpynbDiff::IpynbSymbolMap do
+  def res(*cases)
+    cases&.to_h || []
+  end
+
+  describe '#parse_string' do
+    using RSpec::Parameterized::TableSyntax
+
+    let(:mapper) { IpynbDiff::IpynbSymbolMap.new(input) }
+
+    where(:input, :result) do
+      # Empty string
+      '""' | ''
+      # Some string with quotes
+      '"he\nll\"o"' | 'he\nll\"o'
+    end
+
+    with_them do
+      it { expect(mapper.parse_string(return_value: true)).to eq(result) }
+      it { expect(mapper.parse_string).to be_nil }
+      it { expect(mapper.results).to be_empty }
+    end
+
+    it 'raises if invalid string' do
+      mapper = IpynbDiff::IpynbSymbolMap.new('"')
+
+      expect { mapper.parse_string }.to raise_error(IpynbDiff::InvalidTokenError)
+    end
+
+  end
+
+  describe '#parse_object' do
+    using RSpec::Parameterized::TableSyntax
+
+    let(:mapper) { IpynbDiff::IpynbSymbolMap.new(notebook, objects_to_ignore) }
+
+    before do
+      mapper.parse_object('')
+    end
+
+    where(:notebook, :objects_to_ignore, :result) do
+      # Empty object
+      '{   }' | [] | res
+      # Object with string
+      '{   "hello"  : "world"   }' | [] | res(['.hello', 0])
+      # Object with boolean
+      '{   "hello"  : true   }' | [] | res(['.hello', 0])
+      # Object with integer
+      '{   "hello"  : 1   }' | [] | res(['.hello', 0])
+      # Object with 2 properties in the same line
+      '{   "hello"  : "world" , "my" : "bad"   }' | [] | res(['.hello', 0], ['.my', 0])
+      # Object with 2 properties in the different lines line
+      "{   \"hello\"  : \"world\" , \n \n \"my\" : \"bad\"   }" | [] | res(['.hello', 0], ['.my', 2])
+      # Object with 2 properties, but one is ignored
+      "{   \"hello\"  : \"world\" , \n \n \"my\" : \"bad\"   }" | ['hello'] | res(['.my', 2])
+    end
+
+    with_them do
+      it { expect(mapper.results).to include(result) }
+    end
+  end
+
+  describe '#parse_array' do
+    using RSpec::Parameterized::TableSyntax
+
+    where(:notebook, :result) do
+      # Empty Array
+      '[]' | res
+      # Array with string value
+      '["a"]' | res(['.0', 0])
+      # Array with boolean
+      '[  true  ]' | res(['.0', 0])
+      # Array with integer
+      '[  1 ]' | res(['.0', 0])
+      # Two values on the same line
+      '["a", "b"]' | res(['.0', 0], ['.1', 0])
+      # With line breaks'
+      "[\n  \"a\"  \n , \n \"b\"  ]" | res(['.0', 1], ['.1', 3])
+    end
+
+    let(:mapper) { IpynbDiff::IpynbSymbolMap.new(notebook) }
+
+    before do
+      mapper.parse_array('')
+    end
+
+    with_them do
+      it { expect(mapper.results).to match_array(result) }
+    end
+  end
+
+  describe '#skip_object' do
+    subject { IpynbDiff::IpynbSymbolMap.parse(JSON.pretty_generate(source)) }
+  end
+
+  describe '#parse' do
+
+    let(:objects_to_ignore) { [] }
+
+    subject { IpynbDiff::IpynbSymbolMap.parse(JSON.pretty_generate(source), objects_to_ignore) }
+
+    context 'Empty object' do
+      let(:source) { {} }
+
+      it { is_expected.to be_empty }
+    end
+
+    context 'Object with inner object and number' do
+      let(:source) { { obj1: { obj2: 1 } } }
+
+      it { is_expected.to match_array(res(['.obj1', 1], ['.obj1.obj2', 2])) }
+    end
+
+    context 'Object with inner object and number, string and array with object' do
+      let(:source) { { obj1: { obj2: [123, 2, true], obj3: "hel\nlo", obj4: true, obj5: 123, obj6: 'a' } } }
+
+      it do
+        is_expected.to match_array(
+          res(['.obj1', 1],
+              ['.obj1.obj2', 2],
+              ['.obj1.obj2.0', 3],
+              ['.obj1.obj2.1', 4],
+              ['.obj1.obj2.2', 5],
+              ['.obj1.obj3', 7],
+              ['.obj1.obj4', 8],
+              ['.obj1.obj5', 9],
+              ['.obj1.obj6', 10])
+        )
+      end
+    end
+
+    context 'When index is exceeded because of failure' do
+      it 'raises an exception' do
+        source = '{"\\a": "a\""}'
+
+        mapper = IpynbDiff::IpynbSymbolMap.new(source)
+
+        expect(mapper).to receive(:prev_backslash?).at_least(1).time.and_return(false)
+
+        expect { mapper.parse('') }.to raise_error(IpynbDiff::InvalidTokenError)
+      end
+    end
+
+    context 'Object with inner object and number, string and array with object' do
+      let(:source) { { obj1: { obj2: [123, 2, true], obj3: "hel\nlo", obj4: true, obj5: 123, obj6: { obj7: 'a' } } } }
+      let(:objects_to_ignore) { %w(obj2 obj6) }
+      it do
+        is_expected.to match_array(
+                         res(['.obj1', 1],
+                             ['.obj1.obj3', 7],
+                             ['.obj1.obj4', 8],
+                             ['.obj1.obj5', 9],
+                       )
+                       )
+      end
+    end
+  end
+end
diff --git a/vendor/gems/ipynbdiff/spec/ipynbdiff_spec.rb b/vendor/gems/ipynbdiff/spec/ipynbdiff_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1c2a2188edf7f1ab987e8aadcd6d2cf36787eb42
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/ipynbdiff_spec.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+
+require 'ipynbdiff'
+require 'rspec'
+require 'rspec-parameterized'
+
+BASE_PATH = File.join(File.expand_path(File.dirname(__FILE__)),  'testdata')
+
+describe IpynbDiff do
+  def diff_signs(diff)
+    diff.to_s(:text).scan(/.*\n/).map { |l| l[0] }.join('')
+  end
+
+  describe 'diff' do
+    let(:from_path) { File.join(BASE_PATH, 'from.ipynb') }
+    let(:to_path) { File.join(BASE_PATH,'to.ipynb') }
+    let(:from) { File.read(from_path) }
+    let(:to) { File.read(to_path) }
+    let(:include_frontmatter) { false }
+    let(:hide_images) { false }
+
+    subject { IpynbDiff.diff(from, to, include_frontmatter: include_frontmatter, hide_images: hide_images) }
+
+    context 'if preprocessing is active' do
+      it 'html tables are stripped' do
+        is_expected.to_not include('<td>')
+      end
+    end
+
+    context 'when to is nil' do
+      let(:to) { nil }
+      let(:from_path) { File.join(BASE_PATH, 'only_md', 'input.ipynb') }
+
+      it 'all lines are removals' do
+        expect(diff_signs(subject)).to eq('-----')
+      end
+    end
+
+    context 'when to is nil' do
+      let(:from) { nil }
+      let(:to_path) { File.join(BASE_PATH, 'only_md', 'input.ipynb') }
+
+      it 'all lines are additions' do
+        expect(diff_signs(subject)).to eq('+++++')
+      end
+    end
+
+    context 'When include_frontmatter is true' do
+      let(:include_frontmatter) { true }
+
+      it 'should show changes metadata in the metadata' do
+        expect(subject.to_s(:text)).to include('+    display_name: New Python 3 (ipykernel)')
+      end
+    end
+
+    context 'When hide_images is true' do
+      let(:hide_images) { true }
+
+      it 'hides images' do
+        expect(subject.to_s(:text)).to include('     [Hidden Image Output]')
+      end
+    end
+
+    context 'When include_frontmatter is false' do
+      it 'should drop metadata from the diff' do
+        expect(subject.to_s(:text)).to_not include('+    display_name: New Python 3 (ipykernel)')
+      end
+    end
+
+    context 'when either notebook can not be processed' do
+      using RSpec::Parameterized::TableSyntax
+
+      where(:ctx, :from, :to) do
+        'because from is invalid'                 | 'a' | nil
+        'because from does not have the cell tag' | '{"metadata":[]}' | nil
+        'because to is invalid'                   | nil | 'a'
+        'because to does not have the cell tag'   | nil | '{"metadata":[]}'
+      end
+
+      with_them do
+        it { is_expected.to be_nil }
+      end
+    end
+  end
+
+  describe 'transform' do
+    [nil, 'a', '{"metadata":[]}'].each do |invalid_nb|
+      context "when json is invalid (#{invalid_nb || 'nil'})" do
+        it 'is nil' do
+          expect(IpynbDiff.transform(invalid_nb)).to be_nil
+        end
+      end
+    end
+
+    context 'options' do
+      let(:include_frontmatter) { false }
+      let(:hide_images) { false }
+
+      subject do
+        IpynbDiff.transform(File.read(File.join(BASE_PATH, 'from.ipynb')),
+                            include_frontmatter: include_frontmatter,
+                            hide_images: hide_images)
+      end
+
+      context 'include_frontmatter is false' do
+        it { is_expected.to_not include('display_name: Python 3 (ipykernel)') }
+      end
+
+      context 'include_frontmatter is true' do
+        let(:include_frontmatter) { true }
+
+        it { is_expected.to include('display_name: Python 3 (ipykernel)') }
+      end
+
+      context 'hide_images is false' do
+        it { is_expected.not_to include('[Hidden Image Output]') }
+      end
+
+      context 'hide_images is true' do
+        let(:hide_images) { true }
+
+        it { is_expected.to include('    [Hidden Image Output]') }
+      end
+    end
+  end
+end
diff --git a/vendor/gems/ipynbdiff/spec/testdata/backslash_as_last_char/expected.md b/vendor/gems/ipynbdiff/spec/testdata/backslash_as_last_char/expected.md
new file mode 100644
index 0000000000000000000000000000000000000000..299e286c679960fc4b6a00507f3d74d9463b50f6
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/backslash_as_last_char/expected.md
@@ -0,0 +1,7 @@
+%% Cell type:markdown id: tags:
+
+\
+
+%% Cell type:markdown id: tags:
+
+a
diff --git a/vendor/gems/ipynbdiff/spec/testdata/backslash_as_last_char/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/backslash_as_last_char/expected_symbols.txt
new file mode 100644
index 0000000000000000000000000000000000000000..6fa29ae28de0d1d8161dc6050c6259f29999120c
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/backslash_as_last_char/expected_symbols.txt
@@ -0,0 +1,7 @@
+.cells.0
+
+.cells.0.source.0
+
+.cells.1
+
+.cells.1.source.0
diff --git a/vendor/gems/ipynbdiff/spec/testdata/backslash_as_last_char/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/backslash_as_last_char/input.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..0714044e3aeb7690e884565283484c0fa336b5ab
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/backslash_as_last_char/input.ipynb
@@ -0,0 +1,16 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "source": [
+    "\\"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "a"
+   ]
+  }
+ ]
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/error_output/expected.md b/vendor/gems/ipynbdiff/spec/testdata/error_output/expected.md
new file mode 100644
index 0000000000000000000000000000000000000000..5be645de9c9718b055de5f93cc709c37a9f8063c
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/error_output/expected.md
@@ -0,0 +1,16 @@
+%% Cell type:code id:5 tags:
+
+``` python
+# A cell that has an error
+y = sin(x)
+```
+
+%%%% Output: error
+
+    ---------------------------------------------------------------------------
+    NameError                                 Traceback (most recent call last)
+    /var/folders/cq/l637k4x13gx6y9p_gfs4c_gc0000gn/T/ipykernel_72857/3962062127.py in <module>
+          1 # A cell that has an error
+    ----> 2 y = sin(x)
+
+    NameError: name 'sin' is not defined
diff --git a/vendor/gems/ipynbdiff/spec/testdata/error_output/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/error_output/expected_symbols.txt
new file mode 100644
index 0000000000000000000000000000000000000000..75e35d123d058bd14291150fefd1b198c9216653
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/error_output/expected_symbols.txt
@@ -0,0 +1,16 @@
+.cells.0
+
+.cells.0.source
+.cells.0.source.0
+.cells.0.source.1
+
+
+.cells.0.outputs.0
+
+.cells.0.outputs.0.traceback.0
+.cells.0.outputs.0.traceback.1
+.cells.0.outputs.0.traceback.2
+.cells.0.outputs.0.traceback.2
+.cells.0.outputs.0.traceback.2
+.cells.0.outputs.0.traceback.2
+.cells.0.outputs.0.traceback.3
diff --git a/vendor/gems/ipynbdiff/spec/testdata/error_output/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/error_output/input.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..45ee81a0e2df7686c3998d337e8eb2e11d0c3a93
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/error_output/input.ipynb
@@ -0,0 +1,32 @@
+{
+ "cells": [
+ {
+    "cell_type": "code",
+    "execution_count": 7,
+    "id": "5",
+    "metadata": {},
+    "outputs": [
+     {
+      "ename": "NameError",
+      "evalue": "name 'sin' is not defined",
+      "output_type": "error",
+      "traceback": [
+       "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
+       "\u001b[0;31mNameError\u001b[0m                                 Traceback (most recent call last)",
+       "\u001b[0;32m/var/folders/cq/l637k4x13gx6y9p_gfs4c_gc0000gn/T/ipykernel_72857/3962062127.py\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[1;32m      1\u001b[0m \u001b[0;31m# A cell that has an error\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0my\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0msin\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mx\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
+       "\u001b[0;31mNameError\u001b[0m: name 'sin' is not defined"
+      ]
+     }
+    ],
+    "source": [
+     "# A cell that has an error\n",
+     "y = sin(x)"
+    ]
+   }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "language": "python"
+  }
+ }
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/from.ipynb b/vendor/gems/ipynbdiff/spec/testdata/from.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..a731c9bfffdfcbf8c3700f3518f5986728cc35d3
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/from.ipynb
@@ -0,0 +1,198 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "id": "0aac5da7-745c-4eda-847a-3d0d07a1bb9b",
+   "metadata": {
+    "tags": []
+   },
+   "source": [
+    "# This is a markdown cell\n",
+    "\n",
+    "This paragraph has\n",
+    "With\n",
+    "Many\n",
+    "Lines. How we will he handle MR notes?\n",
+    "\n",
+    "But I can add another paragraph"
+   ]
+  },
+  {
+   "cell_type": "raw",
+   "id": "faecea5b-de0a-49fa-9a3a-61c2add652da",
+   "metadata": {},
+   "source": [
+    "This is a raw cell\n",
+    "With\n",
+    "Multiple lines"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "id": "893ca2c0-ab75-4276-9dad-be1c40e16e8a",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import pandas as pd\n",
+    "import numpy as np\n",
+    "import matplotlib.pyplot as plt"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "id": "0d707fb5-226f-46d6-80bd-489ebfb8905c",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "np.random.seed(42)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "id": "35467fcf-28b1-4c7b-bb09-4cb192c35293",
+   "metadata": {
+    "tags": [
+     "senoid"
+    ]
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "[<matplotlib.lines.Line2D at 0x123e39370>]"
+      ]
+     },
+     "execution_count": 3,
+     "metadata": {},
+     "output_type": "execute_result"
+    },
+    {
+     "data": {
+      "image/png": "some_invalid_base64_image_here\n",
+      "text/plain": [
+       "<Figure size 432x288 with 1 Axes>"
+      ]
+     },
+     "metadata": {
+      "needs_background": "light"
+     },
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "x = np.linspace(0, 4*np.pi,50)\n",
+    "y = np.sin(x)\n",
+    "\n",
+    "plt.plot(x, y)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "id": "dc1178cd-c46d-4da3-9ab5-08f000699884",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "df = pd.DataFrame({\"x\": x, \"y\": y})"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "id": "6e749b4f-b409-4700-870f-f68c39462490",
+   "metadata": {
+    "tags": [
+     "some-table"
+    ]
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>x</th>\n",
+       "      <th>y</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>0.000000</td>\n",
+       "      <td>0.000000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>0.256457</td>\n",
+       "      <td>0.253655</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "          x         y\n",
+       "0  0.000000  0.000000\n",
+       "1  0.256457  0.253655"
+      ]
+     },
+     "execution_count": 5,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "df[:2]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "0ddef5ef-94a3-4afd-9c70-ddee9694f512",
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.9.7"
+  },
+  "toc-showtags": true
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/hide_images/expected.md b/vendor/gems/ipynbdiff/spec/testdata/hide_images/expected.md
new file mode 100644
index 0000000000000000000000000000000000000000..89a812740a6d46d283ed415b166a631aa404e0c3
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/hide_images/expected.md
@@ -0,0 +1,12 @@
+%% Cell type:code id:5 tags:senoid
+
+``` python
+```
+
+%%%% Output: display_data
+
+    [Hidden Image Output]
+
+%%%% Output: display_data
+
+    [Hidden Image Output]
diff --git a/vendor/gems/ipynbdiff/spec/testdata/hide_images/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/hide_images/expected_symbols.txt
new file mode 100644
index 0000000000000000000000000000000000000000..b94e9538f58d879f6d90fd5e8e77e1cd79c06f0d
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/hide_images/expected_symbols.txt
@@ -0,0 +1,12 @@
+.cells.0
+
+.cells.0.source
+
+
+.cells.0.outputs.0
+
+
+
+.cells.0.outputs.1
+
+
diff --git a/vendor/gems/ipynbdiff/spec/testdata/hide_images/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/hide_images/input.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..dab0e5bb9cf517485c65696cb9430156694ee6da
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/hide_images/input.ipynb
@@ -0,0 +1,45 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "id": "5",
+   "metadata": {
+    "tags": [
+     "senoid"
+    ]
+   },
+   "outputs": [
+    {
+     "data": {
+      "image/png": "this_is_an_invalid_hash_for_testing_purposes\n",
+      "text/plain": [
+       "<Figure size 432x288 with 1 Axes>"
+      ]
+     },
+     "metadata": {
+      "needs_background": "light"
+     },
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "image/svg+xml": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\"><circle cx=\"50\" cy=\"50\" r=\"50\"/></svg>",
+      "text/plain": [
+       "<IPython.core.display.SVG object>"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "language": "python"
+  }
+ }
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/ignore_html_output/expected.md b/vendor/gems/ipynbdiff/spec/testdata/ignore_html_output/expected.md
new file mode 100644
index 0000000000000000000000000000000000000000..456224f3afff830d605dba31ed72436ba02ac685
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/ignore_html_output/expected.md
@@ -0,0 +1,11 @@
+%% Cell type:code id:5 tags:some-table
+
+``` python
+df[:2]
+```
+
+%%%% Output: execute_result
+
+              x         y
+    0  0.000000  0.000000
+    1  0.256457  0.507309
diff --git a/vendor/gems/ipynbdiff/spec/testdata/ignore_html_output/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/ignore_html_output/expected_symbols.txt
new file mode 100644
index 0000000000000000000000000000000000000000..fa9d412c6dce5812ce27bd3df909625538b34d6a
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/ignore_html_output/expected_symbols.txt
@@ -0,0 +1,11 @@
+.cells.0
+
+.cells.0.source
+.cells.0.source.0
+
+
+.cells.0.outputs.0
+
+.cells.0.outputs.0.data.text/plain.0
+.cells.0.outputs.0.data.text/plain.1
+.cells.0.outputs.0.data.text/plain.2
diff --git a/vendor/gems/ipynbdiff/spec/testdata/ignore_html_output/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/ignore_html_output/input.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..26117a7893413bbdf7eb57b613c1bec4a07d5b40
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/ignore_html_output/input.ipynb
@@ -0,0 +1,74 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "id": "5",
+   "metadata": {
+    "tags": [
+     "some-table"
+    ]
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>x</th>\n",
+       "      <th>y</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>0.000000</td>\n",
+       "      <td>0.000000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>0.256457</td>\n",
+       "      <td>0.507309</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "          x         y\n",
+       "0  0.000000  0.000000\n",
+       "1  0.256457  0.507309"
+      ]
+     },
+     "execution_count": 5,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "df[:2]"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "language": "python"
+  }
+ }
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/latex_output/expected.md b/vendor/gems/ipynbdiff/spec/testdata/latex_output/expected.md
new file mode 100644
index 0000000000000000000000000000000000000000..add84ed26a03a089e69fdf16ce6552caf818d9f6
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/latex_output/expected.md
@@ -0,0 +1,10 @@
+%% Cell type:code id:5 tags:
+
+``` python
+from IPython.display import display, Math
+display(Math(r'Dims: {}x{}m \\ Area: {}m^2 \\ Volume: {}m^3'.format(1, round(2,2), 3, 4)))
+```
+
+%%%% Output: display_data
+
+    $\displaystyle Dims: 1x2m \\ Area: 3m^2 \\ Volume: 4m^3$
diff --git a/vendor/gems/ipynbdiff/spec/testdata/latex_output/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/latex_output/expected_symbols.txt
new file mode 100644
index 0000000000000000000000000000000000000000..9407e6db7025d0f37d77bd6a910ea28827888a19
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/latex_output/expected_symbols.txt
@@ -0,0 +1,10 @@
+.cells.0
+
+.cells.0.source
+.cells.0.source.0
+.cells.0.source.1
+
+
+.cells.0.outputs.0
+
+.cells.0.outputs.0.data.text/latex.0
diff --git a/vendor/gems/ipynbdiff/spec/testdata/latex_output/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/latex_output/input.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..f8ff3e72beb4d0e2104c8a1d81fd561e92fa2d38
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/latex_output/input.ipynb
@@ -0,0 +1,34 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "id": "5",
+   "outputs": [
+    {
+     "data": {
+      "text/latex": [
+       "$\\displaystyle Dims: 1x2m \\\\ Area: 3m^2 \\\\ Volume: 4m^3$"
+      ],
+      "text/plain": [
+       "<IPython.core.display.Math object>"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "from IPython.display import display, Math\n",
+    "display(Math(r'Dims: {}x{}m \\\\ Area: {}m^2 \\\\ Volume: {}m^3'.format(1, round(2,2), 3, 4)))"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  }
+ }
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/multiline_png_output/expected.md b/vendor/gems/ipynbdiff/spec/testdata/multiline_png_output/expected.md
new file mode 100644
index 0000000000000000000000000000000000000000..4a880d8ce18e672c787ca7070fe9d8aa5813d952
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/multiline_png_output/expected.md
@@ -0,0 +1,9 @@
+%% Cell type:code id:5 tags:
+
+``` 
+Some Image
+```
+
+%%%% Output: display_data
+
+    ![](data:image/png;base64,this_is_an_invalid_hash_for_testing_purposes)
diff --git a/vendor/gems/ipynbdiff/spec/testdata/multiline_png_output/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/multiline_png_output/expected_symbols.txt
new file mode 100644
index 0000000000000000000000000000000000000000..26e11781ec11a3d64fa8963a39e1665296f5912b
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/multiline_png_output/expected_symbols.txt
@@ -0,0 +1,9 @@
+.cells.0
+
+.cells.0.source
+.cells.0.source.0
+
+
+.cells.0.outputs.0
+
+.cells.0.outputs.0.data.image/png
diff --git a/vendor/gems/ipynbdiff/spec/testdata/multiline_png_output/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/multiline_png_output/input.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..4d19a504553104c7d9406d2492d1bb3cb3d852a1
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/multiline_png_output/input.ipynb
@@ -0,0 +1,25 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "id": "5",
+   "metadata": {
+   },
+   "outputs": [
+    {
+     "data": {
+      "image/png": [
+        "this_is_an_invalid_hash_for_testing_purposes"
+      ]
+     },
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "Some Image"
+   ]
+  }
+ ],
+ "metadata": {
+ }
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/no_cells/expected.md b/vendor/gems/ipynbdiff/spec/testdata/no_cells/expected.md
new file mode 100644
index 0000000000000000000000000000000000000000..b7c09c51fb86baa7c4f095f0ba048a6bac316564
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/no_cells/expected.md
@@ -0,0 +1,19 @@
+---
+jupyter:
+  kernelspec:
+    display_name: Python 3 (ipykernel)
+    language: python
+    name: python3
+  language_info:
+    codemirror_mode:
+      name: ipython
+      version: 3
+    file_extension: ".py"
+    mimetype: text/x-python
+    name: python
+    nbconvert_exporter: python
+    pygments_lexer: ipython3
+    version: 3.9.7
+  nbformat: 4
+  nbformat_minor: 5
+---
diff --git a/vendor/gems/ipynbdiff/spec/testdata/no_cells/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/no_cells/expected_symbols.txt
new file mode 100644
index 0000000000000000000000000000000000000000..a60f3032882aa8c0946871f1d556aaafa6424bc3
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/no_cells/expected_symbols.txt
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vendor/gems/ipynbdiff/spec/testdata/no_cells/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/no_cells/input.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..c2ba0ebf50ab974e432adf68b6a907daeabf5e83
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/no_cells/input.ipynb
@@ -0,0 +1,25 @@
+{
+ "cells": [],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.9.7"
+  },
+  "toc-showtags": true
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/no_cells_no_metadata/expected.md b/vendor/gems/ipynbdiff/spec/testdata/no_cells_no_metadata/expected.md
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/vendor/gems/ipynbdiff/spec/testdata/no_cells_no_metadata/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/no_cells_no_metadata/expected_symbols.txt
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/vendor/gems/ipynbdiff/spec/testdata/no_cells_no_metadata/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/no_cells_no_metadata/input.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..c2ba0ebf50ab974e432adf68b6a907daeabf5e83
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/no_cells_no_metadata/input.ipynb
@@ -0,0 +1,25 @@
+{
+ "cells": [],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.9.7"
+  },
+  "toc-showtags": true
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/no_metadata_on_cell/expected.md b/vendor/gems/ipynbdiff/spec/testdata/no_metadata_on_cell/expected.md
new file mode 100644
index 0000000000000000000000000000000000000000..d9d72bf8f769f8eb2e940ff16cb13aad28fd1255
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/no_metadata_on_cell/expected.md
@@ -0,0 +1,13 @@
+%% Cell type:markdown id:1 tags:
+
+# A
+
+B
+
+%% Cell type:code id:3 tags:
+
+``` python
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+```
diff --git a/vendor/gems/ipynbdiff/spec/testdata/no_metadata_on_cell/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/no_metadata_on_cell/expected_symbols.txt
new file mode 100644
index 0000000000000000000000000000000000000000..a7000494a1b81dde43693be07b964c24cc665625
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/no_metadata_on_cell/expected_symbols.txt
@@ -0,0 +1,13 @@
+.cells.0
+
+.cells.0.source.0
+.cells.0.source.1
+.cells.0.source.2
+
+.cells.1
+
+.cells.1.source
+.cells.1.source.0
+.cells.1.source.1
+.cells.1.source.2
+
diff --git a/vendor/gems/ipynbdiff/spec/testdata/no_metadata_on_cell/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/no_metadata_on_cell/input.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..62060124a2ab805561f4ea68e7858507fd81d51e
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/no_metadata_on_cell/input.ipynb
@@ -0,0 +1,29 @@
+{
+ "cells": [
+   {
+    "cell_type": "markdown",
+    "id": "1",
+    "source": [
+     "# A\n",
+     "\n",
+     "B"
+    ]
+   },
+   {
+    "cell_type": "code",
+    "execution_count": 1,
+    "id": "3",
+    "outputs": [],
+    "source": [
+     "import pandas as pd\n",
+     "import numpy as np\n",
+     "import matplotlib.pyplot as plt"
+    ]
+   }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "language": "python"
+  }
+ }
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_code/expected.md b/vendor/gems/ipynbdiff/spec/testdata/only_code/expected.md
new file mode 100644
index 0000000000000000000000000000000000000000..124b8217a6a87fe51f423ba50b9a22ffb6076002
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/only_code/expected.md
@@ -0,0 +1,7 @@
+%% Cell type:code id:3 tags:
+
+``` python
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+```
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_code/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/only_code/expected_symbols.txt
new file mode 100644
index 0000000000000000000000000000000000000000..59b111031950bc0eaa45dad59c27ade9f89ceaec
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/only_code/expected_symbols.txt
@@ -0,0 +1,7 @@
+.cells.0
+
+.cells.0.source
+.cells.0.source.0
+.cells.0.source.1
+.cells.0.source.2
+
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_code/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/only_code/input.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..a93108dccb8c70fcba3764f404326a0278a04fbd
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/only_code/input.ipynb
@@ -0,0 +1,21 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "id": "3",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import pandas as pd\n",
+    "import numpy as np\n",
+    "import matplotlib.pyplot as plt"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "language": "python"
+  }
+ }
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_code_no_kernelspec/expected.md b/vendor/gems/ipynbdiff/spec/testdata/only_code_no_kernelspec/expected.md
new file mode 100644
index 0000000000000000000000000000000000000000..9100045e0f56cb00f37f083eb88a4db095d21839
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/only_code_no_kernelspec/expected.md
@@ -0,0 +1,5 @@
+%% Cell type:code id:3 tags:
+
+``` 
+
+```
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_code_no_kernelspec/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/only_code_no_kernelspec/expected_symbols.txt
new file mode 100644
index 0000000000000000000000000000000000000000..5f9ad320b8c1e74e792166de5310cbe454cef67c
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/only_code_no_kernelspec/expected_symbols.txt
@@ -0,0 +1,5 @@
+.cells.0
+
+.cells.0.source
+.cells.0.source
+
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_code_no_kernelspec/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/only_code_no_kernelspec/input.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..c3ff71057a61c8bda1c097ee4bcef4edba6150b1
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/only_code_no_kernelspec/input.ipynb
@@ -0,0 +1,12 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "id": "3",
+   "source": "",
+   "outputs": []
+  }
+ ],
+ "metadata": {}
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_code_no_language/expected.md b/vendor/gems/ipynbdiff/spec/testdata/only_code_no_language/expected.md
new file mode 100644
index 0000000000000000000000000000000000000000..9100045e0f56cb00f37f083eb88a4db095d21839
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/only_code_no_language/expected.md
@@ -0,0 +1,5 @@
+%% Cell type:code id:3 tags:
+
+``` 
+
+```
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_code_no_language/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/only_code_no_language/expected_symbols.txt
new file mode 100644
index 0000000000000000000000000000000000000000..5f9ad320b8c1e74e792166de5310cbe454cef67c
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/only_code_no_language/expected_symbols.txt
@@ -0,0 +1,5 @@
+.cells.0
+
+.cells.0.source
+.cells.0.source
+
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_code_no_language/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/only_code_no_language/input.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..fb16b106cbe30ab817b258553051d6ddcc86c7dc
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/only_code_no_language/input.ipynb
@@ -0,0 +1,14 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "id": "3",
+   "source": "",
+   "outputs": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {}
+ }
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_code_no_metadata/expected.md b/vendor/gems/ipynbdiff/spec/testdata/only_code_no_metadata/expected.md
new file mode 100644
index 0000000000000000000000000000000000000000..9100045e0f56cb00f37f083eb88a4db095d21839
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/only_code_no_metadata/expected.md
@@ -0,0 +1,5 @@
+%% Cell type:code id:3 tags:
+
+``` 
+
+```
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_code_no_metadata/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/only_code_no_metadata/expected_symbols.txt
new file mode 100644
index 0000000000000000000000000000000000000000..5f9ad320b8c1e74e792166de5310cbe454cef67c
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/only_code_no_metadata/expected_symbols.txt
@@ -0,0 +1,5 @@
+.cells.0
+
+.cells.0.source
+.cells.0.source
+
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_code_no_metadata/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/only_code_no_metadata/input.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..364c080168ba9d4e9d36ee2cadf8d4f4e333920e
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/only_code_no_metadata/input.ipynb
@@ -0,0 +1,11 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "id": "3",
+   "source": "",
+   "outputs": []
+  }
+ ]
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_md/expected.md b/vendor/gems/ipynbdiff/spec/testdata/only_md/expected.md
new file mode 100644
index 0000000000000000000000000000000000000000..bdf4db5aea59e141c35e46f0b9d60eecce2bd8ae
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/only_md/expected.md
@@ -0,0 +1,5 @@
+%% Cell type:markdown id:1 tags:hello,world
+
+# A
+
+B
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_md/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/only_md/expected_symbols.txt
new file mode 100644
index 0000000000000000000000000000000000000000..d3d6d526fc3a350d96f326e102cd26f549d43a40
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/only_md/expected_symbols.txt
@@ -0,0 +1,5 @@
+.cells.0
+
+.cells.0.source.0
+.cells.0.source.1
+.cells.0.source.2
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_md/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/only_md/input.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..9d6b550af31b7575fafd18827ac0751cb9140f17
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/only_md/input.ipynb
@@ -0,0 +1,21 @@
+{
+ "cells": [
+   {
+    "cell_type": "markdown",
+    "id": "1",
+    "metadata": {
+     "tags": [
+        "hello",
+        "world"
+     ]
+    },
+    "source": [
+     "# A\n",
+     "\n",
+     "B"
+    ]
+   }
+ ],
+ "metadata": {
+ }
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_raw/expected.md b/vendor/gems/ipynbdiff/spec/testdata/only_raw/expected.md
new file mode 100644
index 0000000000000000000000000000000000000000..91c476e843ba67985be50c57139bdc08293f7da2
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/only_raw/expected.md
@@ -0,0 +1,4 @@
+%% Cell type:raw id:2 tags:
+
+A
+B
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_raw/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/only_raw/expected_symbols.txt
new file mode 100644
index 0000000000000000000000000000000000000000..bceaf355c2f68cbd018165d1ca26a8dda78397e4
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/only_raw/expected_symbols.txt
@@ -0,0 +1,4 @@
+.cells.0
+
+.cells.0.source.0
+.cells.0.source.1
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_raw/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/only_raw/input.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..750e1bba6150d541f89d2309a50b8e99050faa1f
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/only_raw/input.ipynb
@@ -0,0 +1,15 @@
+{
+ "cells": [
+  {
+   "cell_type": "raw",
+   "id": "2",
+   "metadata": {},
+   "source": [
+    "A\n",
+    "B"
+   ]
+  }
+ ],
+ "metadata": {
+ }
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/percent_decorator/expected.md b/vendor/gems/ipynbdiff/spec/testdata/percent_decorator/expected.md
new file mode 100644
index 0000000000000000000000000000000000000000..ecb0029f256065491ef3c7b8b767837f1e87a59f
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/percent_decorator/expected.md
@@ -0,0 +1,70 @@
+%% Cell type:markdown id:0aac5da7-745c-4eda-847a-3d0d07a1bb9b tags:
+
+# This is a markdown cell
+
+This paragraph has
+With
+Many
+Lines. How we will he handle MR notes?
+
+But I can add another paragraph
+
+%% Cell type:raw id:faecea5b-de0a-49fa-9a3a-61c2add652da tags:
+
+This is a raw cell
+With
+Multiple lines
+
+%% Cell type:code id:893ca2c0-ab75-4276-9dad-be1c40e16e8a tags:
+
+``` python
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+```
+
+%% Cell type:code id:0d707fb5-226f-46d6-80bd-489ebfb8905c tags:
+
+``` python
+np.random.seed(42)
+```
+
+%% Cell type:code id:35467fcf-28b1-4c7b-bb09-4cb192c35293 tags:senoid
+
+``` python
+x = np.linspace(0, 4*np.pi,50)
+y = np.sin(x)
+
+plt.plot(x, y)
+```
+
+%%%% Output: execute_result
+
+    [<matplotlib.lines.Line2D at 0x123e39370>]
+
+%%%% Output: display_data
+
+    ![](data:image/png;base64,some_invalid_base64_image_here)
+
+%% Cell type:code id:dc1178cd-c46d-4da3-9ab5-08f000699884 tags:
+
+``` python
+df = pd.DataFrame({"x": x, "y": y})
+```
+
+%% Cell type:code id:6e749b4f-b409-4700-870f-f68c39462490 tags:some-table
+
+``` python
+df[:2]
+```
+
+%%%% Output: execute_result
+
+              x         y
+    0  0.000000  0.000000
+    1  0.256457  0.253655
+
+%% Cell type:code id:0ddef5ef-94a3-4afd-9c70-ddee9694f512 tags:
+
+``` python
+```
diff --git a/vendor/gems/ipynbdiff/spec/testdata/percent_decorator/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/percent_decorator/expected_symbols.txt
new file mode 100644
index 0000000000000000000000000000000000000000..ab70e7bc908b702cb2983aa4f8430ff615065716
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/percent_decorator/expected_symbols.txt
@@ -0,0 +1,70 @@
+.cells.0
+
+.cells.0.source.0
+.cells.0.source.1
+.cells.0.source.2
+.cells.0.source.3
+.cells.0.source.4
+.cells.0.source.5
+.cells.0.source.6
+.cells.0.source.7
+
+.cells.1
+
+.cells.1.source.0
+.cells.1.source.1
+.cells.1.source.2
+
+.cells.2
+
+.cells.2.source
+.cells.2.source.0
+.cells.2.source.1
+.cells.2.source.2
+
+
+.cells.3
+
+.cells.3.source
+.cells.3.source.0
+
+
+.cells.4
+
+.cells.4.source
+.cells.4.source.0
+.cells.4.source.1
+.cells.4.source.2
+.cells.4.source.3
+
+
+.cells.4.outputs.0
+
+.cells.4.outputs.0.data.text/plain.0
+
+.cells.4.outputs.1
+
+.cells.4.outputs.1.data.image/png
+
+.cells.5
+
+.cells.5.source
+.cells.5.source.0
+
+
+.cells.6
+
+.cells.6.source
+.cells.6.source.0
+
+
+.cells.6.outputs.0
+
+.cells.6.outputs.0.data.text/plain.0
+.cells.6.outputs.0.data.text/plain.1
+.cells.6.outputs.0.data.text/plain.2
+
+.cells.7
+
+.cells.7.source
+
diff --git a/vendor/gems/ipynbdiff/spec/testdata/single_line_md/expected.md b/vendor/gems/ipynbdiff/spec/testdata/single_line_md/expected.md
new file mode 100644
index 0000000000000000000000000000000000000000..392a5048f59c24c0bc328770e4acaa37946c0bab
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/single_line_md/expected.md
@@ -0,0 +1,3 @@
+%% Cell type:markdown id:1 tags:hello,world
+
+A
diff --git a/vendor/gems/ipynbdiff/spec/testdata/single_line_md/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/single_line_md/expected_symbols.txt
new file mode 100644
index 0000000000000000000000000000000000000000..86a7f6b3960bb924828ee6221c516f1802c7712a
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/single_line_md/expected_symbols.txt
@@ -0,0 +1,3 @@
+.cells.0
+
+.cells.0.source
diff --git a/vendor/gems/ipynbdiff/spec/testdata/single_line_md/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/single_line_md/input.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..5ebd41adbfa9120388621ec13b2a6d524e1f6ca7
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/single_line_md/input.ipynb
@@ -0,0 +1,17 @@
+{
+ "cells": [
+   {
+    "cell_type": "markdown",
+    "id": "1",
+    "metadata": {
+     "tags": [
+        "hello",
+        "world"
+     ]
+    },
+    "source": "A"
+   }
+ ],
+ "metadata": {
+ }
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/stream_text/expected.md b/vendor/gems/ipynbdiff/spec/testdata/stream_text/expected.md
new file mode 100644
index 0000000000000000000000000000000000000000..fb862cbb6364e387cbdddc37a1457406bb3ff5e2
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/stream_text/expected.md
@@ -0,0 +1,9 @@
+%% Cell type:code id:123 tags:
+
+``` python
+print("G'bye")
+```
+
+%%%% Output: stream
+
+    G'bye
diff --git a/vendor/gems/ipynbdiff/spec/testdata/stream_text/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/stream_text/expected_symbols.txt
new file mode 100644
index 0000000000000000000000000000000000000000..ed4a8a075d3bdf51a675456e08098168d6fe80e1
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/stream_text/expected_symbols.txt
@@ -0,0 +1,9 @@
+.cells.0
+
+.cells.0.source
+.cells.0.source.0
+
+
+.cells.0.outputs.0
+
+.cells.0.outputs.0.text.0
diff --git a/vendor/gems/ipynbdiff/spec/testdata/stream_text/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/stream_text/input.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..14601fe35e5edf2682ce096072822087e41d4c36
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/stream_text/input.ipynb
@@ -0,0 +1,27 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "id": "123",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "G'bye\n"
+     ]
+    }
+   ],
+   "source": [
+    "print(\"G'bye\")"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "language": "python"
+  }
+ }
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/svg/expected.md b/vendor/gems/ipynbdiff/spec/testdata/svg/expected.md
new file mode 100644
index 0000000000000000000000000000000000000000..37269446f5a899f5e47c4d7f077e24177bb3fe22
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/svg/expected.md
@@ -0,0 +1,19 @@
+%% Cell type:code id:5 tags:
+
+``` python
+from IPython.display import SVG, display
+
+svg = """<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+  <circle cx="50" cy="50" r="50"/>
+</svg>"""
+
+display(SVG(svg))
+```
+
+%%%% Output: display_data
+
+    ![](data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="50"/></svg>)
+
+%%%% Output: display_data
+
+    ![](data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="50"/></svg>)
diff --git a/vendor/gems/ipynbdiff/spec/testdata/svg/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/svg/expected_symbols.txt
new file mode 100644
index 0000000000000000000000000000000000000000..dd2e412302d8d3f6730b447aa3a0b0739994e534
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/svg/expected_symbols.txt
@@ -0,0 +1,19 @@
+.cells.0
+
+.cells.0.source
+.cells.0.source.0
+.cells.0.source.1
+.cells.0.source.2
+.cells.0.source.3
+.cells.0.source.4
+.cells.0.source.5
+.cells.0.source.6
+
+
+.cells.0.outputs.0
+
+.cells.0.outputs.0.data.image/svg+xml
+
+.cells.0.outputs.1
+
+.cells.0.outputs.1.data.image/svg+xml
diff --git a/vendor/gems/ipynbdiff/spec/testdata/svg/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/svg/input.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..a02d01f7bf21e0b3a63e1a3eb2b29b1ad8581090
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/svg/input.ipynb
@@ -0,0 +1,66 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 10,
+   "id": "5",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "image/svg+xml": [
+       "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\">\n",
+       "  <circle cx=\"50\" cy=\"50\" r=\"50\"/>\n",
+       "</svg>"
+      ],
+      "text/plain": [
+       "<IPython.core.display.SVG object>"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "image/svg+xml": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\"><circle cx=\"50\" cy=\"50\" r=\"50\"/></svg>",
+      "text/plain": [
+       "<IPython.core.display.SVG object>"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "from IPython.display import SVG, display\n",
+    "\n",
+    "svg = \"\"\"<svg viewBox=\"0 0 100 100\" xmlns=\"http://www.w3.org/2000/svg\">\n",
+    "  <circle cx=\"50\" cy=\"50\" r=\"50\"/>\n",
+    "</svg>\"\"\"\n",
+    "\n",
+    "display(SVG(svg))"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.8.12"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/text_output/expected.md b/vendor/gems/ipynbdiff/spec/testdata/text_output/expected.md
new file mode 100644
index 0000000000000000000000000000000000000000..924f4939f54022e82ddeebcdcf6ac365918cac8d
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/text_output/expected.md
@@ -0,0 +1,9 @@
+%% Cell type:code id:5 tags:senoid
+
+``` python
+plt.plot(x, y)
+```
+
+%%%% Output: execute_result
+
+    [<matplotlib.lines.Line2D at 0x12a4e43d0>]
diff --git a/vendor/gems/ipynbdiff/spec/testdata/text_output/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/text_output/expected_symbols.txt
new file mode 100644
index 0000000000000000000000000000000000000000..179b30098a13c13e45b6055ab3987e20d90615d1
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/text_output/expected_symbols.txt
@@ -0,0 +1,9 @@
+.cells.0
+
+.cells.0.source
+.cells.0.source.0
+
+
+.cells.0.outputs.0
+
+.cells.0.outputs.0.data.text/plain.0
diff --git a/vendor/gems/ipynbdiff/spec/testdata/text_output/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/text_output/input.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..b1b387bb99de9ff87decf44e2f5dbbef1df02ac3
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/text_output/input.ipynb
@@ -0,0 +1,31 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "id": "5",
+   "metadata": {
+    "tags": [
+     "senoid"
+    ]
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "[<matplotlib.lines.Line2D at 0x12a4e43d0>]"
+      ]
+     },
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "plt.plot(x, y)"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "language": "python"
+  }
+ }
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/text_png_output/expected.md b/vendor/gems/ipynbdiff/spec/testdata/text_png_output/expected.md
new file mode 100644
index 0000000000000000000000000000000000000000..b1dda235951666d7925f5807b85044185af42f69
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/text_png_output/expected.md
@@ -0,0 +1,16 @@
+%% Cell type:code id:5 tags:senoid
+
+``` python
+x = np.linspace(0, 4*np.pi,50)
+y = 2 * np.sin(x)
+
+plt.plot(x, y)
+```
+
+%%%% Output: execute_result
+
+    [<matplotlib.lines.Line2D at 0x12a4e43d0>]
+
+%%%% Output: display_data
+
+    ![](data:image/png;base64,this_is_an_invalid_hash_for_testing_purposes)
diff --git a/vendor/gems/ipynbdiff/spec/testdata/text_png_output/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/text_png_output/expected_symbols.txt
new file mode 100644
index 0000000000000000000000000000000000000000..5a86e4daa675cf5523fbacbc21e922a048095e41
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/text_png_output/expected_symbols.txt
@@ -0,0 +1,16 @@
+.cells.0
+
+.cells.0.source
+.cells.0.source.0
+.cells.0.source.1
+.cells.0.source.2
+.cells.0.source.3
+
+
+.cells.0.outputs.0
+
+.cells.0.outputs.0.data.text/plain.0
+
+.cells.0.outputs.1
+
+.cells.0.outputs.1.data.image/png
diff --git a/vendor/gems/ipynbdiff/spec/testdata/text_png_output/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/text_png_output/input.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..3728b129d26db7d19ee0e62ef4b44b4df8a205c7
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/text_png_output/input.ipynb
@@ -0,0 +1,49 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "id": "5",
+   "metadata": {
+    "tags": [
+     "senoid"
+    ]
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "[<matplotlib.lines.Line2D at 0x12a4e43d0>]"
+      ]
+     },
+     "execution_count": 3,
+     "metadata": {},
+     "output_type": "execute_result"
+    },
+    {
+     "data": {
+      "image/png": "this_is_an_invalid_hash_for_testing_purposes\n",
+      "text/plain": [
+       "<Figure size 432x288 with 1 Axes>"
+      ]
+     },
+     "metadata": {
+      "needs_background": "light"
+     },
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "x = np.linspace(0, 4*np.pi,50)\n",
+    "y = 2 * np.sin(x)\n",
+    "\n",
+    "plt.plot(x, y)"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "language": "python"
+  }
+ }
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/to.ipynb b/vendor/gems/ipynbdiff/spec/testdata/to.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..99b51f3b85767c68aa6e2db3ea76e8b23feb49d9
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/to.ipynb
@@ -0,0 +1,200 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "id": "0aac5da7-745c-4eda-847a-3d0d07a1bb9b",
+   "metadata": {
+    "tags": []
+   },
+   "source": [
+    "# This is a markdown cell\n",
+    "\n",
+    "This paragraph has\n",
+    "With\n",
+    "Many\n",
+    "Lines. How we will he handle MR notes?\n",
+    "\n",
+    "But I can add another paragraph\n",
+    "\n",
+    "Another paragraph added"
+   ]
+  },
+  {
+   "cell_type": "raw",
+   "id": "faecea5b-de0a-49fa-9a3a-61c2add652da",
+   "metadata": {},
+   "source": [
+    "This is a raw cell\n",
+    "With\n",
+    "Multiple lines"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "id": "893ca2c0-ab75-4276-9dad-be1c40e16e8a",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import pandas as pd\n",
+    "import numpy as np\n",
+    "import matplotlib.pyplot as plt"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "id": "0d707fb5-226f-46d6-80bd-489ebfb8905c",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "np.random.seed(42)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "id": "35467fcf-28b1-4c7b-bb09-4cb192c35293",
+   "metadata": {
+    "tags": [
+     "senoid"
+    ]
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "[<matplotlib.lines.Line2D at 0x12a4e43d0>]"
+      ]
+     },
+     "execution_count": 3,
+     "metadata": {},
+     "output_type": "execute_result"
+    },
+    {
+     "data": {
+      "image/png": "another_invalid_base64_image_here\n",
+      "text/plain": [
+       "<Figure size 432x288 with 1 Axes>"
+      ]
+     },
+     "metadata": {
+      "needs_background": "light"
+     },
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "x = np.linspace(0, 4*np.pi,50)\n",
+    "y = 2 * np.sin(x)\n",
+    "\n",
+    "plt.plot(x, y)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "id": "dc1178cd-c46d-4da3-9ab5-08f000699884",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "df = pd.DataFrame({\"x\": x, \"y\": y})"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "id": "6e749b4f-b409-4700-870f-f68c39462490",
+   "metadata": {
+    "tags": [
+     "some-table"
+    ]
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>x</th>\n",
+       "      <th>y</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>0.000000</td>\n",
+       "      <td>0.000000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>0.256457</td>\n",
+       "      <td>0.507309</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "          x         y\n",
+       "0  0.000000  0.000000\n",
+       "1  0.256457  0.507309"
+      ]
+     },
+     "execution_count": 5,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "df[:2]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "0ddef5ef-94a3-4afd-9c70-ddee9694f512",
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "New Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.9.7"
+  },
+  "toc-showtags": true
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/unknown_output_type/expected.md b/vendor/gems/ipynbdiff/spec/testdata/unknown_output_type/expected.md
new file mode 100644
index 0000000000000000000000000000000000000000..af34d6eb8c3046f5e7c13c61c76c36b960322f70
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/unknown_output_type/expected.md
@@ -0,0 +1,5 @@
+%% Cell type:code id:123 tags:
+
+``` python
+print("G'bye")
+```
diff --git a/vendor/gems/ipynbdiff/spec/testdata/unknown_output_type/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/unknown_output_type/expected_symbols.txt
new file mode 100644
index 0000000000000000000000000000000000000000..cb35f88c8970cd733a6ddff1b3e6ef385d2865d0
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/unknown_output_type/expected_symbols.txt
@@ -0,0 +1,5 @@
+.cells.0
+
+.cells.0.source
+.cells.0.source.0
+
diff --git a/vendor/gems/ipynbdiff/spec/testdata/unknown_output_type/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/unknown_output_type/input.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..42f4b39b365d33efa2a7af513bb403e078cfac1a
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/unknown_output_type/input.ipynb
@@ -0,0 +1,27 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "id": "123",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "unknown_output",
+     "text": [
+      "G'bye\n"
+     ]
+    }
+   ],
+   "source": [
+    "print(\"G'bye\")"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "language": "python"
+  }
+ }
+}
diff --git a/vendor/gems/ipynbdiff/spec/transformer_spec.rb b/vendor/gems/ipynbdiff/spec/transformer_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8f9527847fa68b1c29d98c7f91e1ace65a68d23b
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/transformer_spec.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+require 'rspec'
+require 'ipynbdiff'
+require 'json'
+require 'rspec-parameterized'
+
+BASE_PATH = File.join(File.expand_path(File.dirname(__FILE__)),  'testdata')
+
+def read_file(*paths)
+  File.read(File.join(BASE_PATH, *paths))
+end
+
+def default_config
+  @default_config ||= {
+    include_frontmatter: false,
+    hide_images: false
+  }
+end
+
+def from_ipynb
+  @from_ipynb ||= read_file('from.ipynb')
+end
+
+def read_notebook(input_path)
+  read_file(input_path, 'input.ipynb')
+rescue Errno::ENOENT
+  from_ipynb
+end
+
+describe IpynbDiff::Transformer do
+  describe 'When notebook is valid' do
+    using RSpec::Parameterized::TableSyntax
+
+    where(:ctx, :test_case, :config) do
+      'renders metadata' | 'no_cells' | { include_frontmatter: true }
+      'is empty for no cells, but metadata is false' | 'no_cells_no_metadata' | {}
+      'adds markdown cell' | 'only_md' | {}
+      'adds block with only one line of markdown' | 'single_line_md' | {}
+      'adds raw block' | 'only_raw' | {}
+      'code cell, but no output' | 'only_code' | {}
+      'code cell, but no language' | 'only_code_no_language' | {}
+      'code cell, but no kernelspec' | 'only_code_no_kernelspec' | {}
+      'code cell, but no nb metadata' | 'only_code_no_metadata' | {}
+      'text output' | 'text_output' | {}
+      'ignores html output' | 'ignore_html_output' | {}
+      'extracts png output along with text' | 'text_png_output' | {}
+      'embeds svg as image' | 'svg' | {}
+      'extracts latex output' | 'latex_output'  | {}
+      'extracts error output' | 'error_output'  | {}
+      'does not fetch tags if there is no cell metadata' | 'no_metadata_on_cell' | {}
+      'generates :percent decorator' | 'percent_decorator' | {}
+      'parses stream output' | 'stream_text' | {}
+      'ignores unknown output type' | 'unknown_output_type' | {}
+      'handles backslash correctly' | 'backslash_as_last_char' | {}
+      'multiline png output' | 'multiline_png_output' | {}
+      'hides images when option passed' | 'hide_images' | { hide_images: true }
+    end
+
+    with_them do
+      let(:expected_md) { read_file(test_case, 'expected.md') }
+      let(:expected_symbols) { read_file(test_case, 'expected_symbols.txt') }
+      let(:input) { read_notebook(test_case) }
+      let(:transformed) { IpynbDiff::Transformer.new(**default_config.merge(config)).transform(input) }
+
+      it 'generates the expected markdown' do
+        expect(transformed.as_text).to eq expected_md
+      end
+
+      it 'generates the expected symbol map' do
+        expect(transformed.blocks.map { |b| b[:source_symbol] }.join("\n")).to eq expected_symbols
+      end
+    end
+  end
+
+  context 'When the notebook is invalid' do
+    [
+      ['because the json is invalid', 'a'],
+      ['because it doesnt have the cell tag', '{"metadata":[]}']
+    ].each do |ctx, notebook|
+      context ctx do
+        it 'raises error' do
+          expect do
+            IpynbDiff::Transformer.new.transform(notebook)
+          end.to raise_error(IpynbDiff::InvalidNotebookError)
+        end
+      end
+    end
+  end
+end