diff --git a/.gitlab/ci/gitlab-gems.gitlab-ci.yml b/.gitlab/ci/gitlab-gems.gitlab-ci.yml
index 0c2fc96b02b3efb1a17c8fa8f6a1845c6ddb20e0..f188e1f97f8ad4fc128821281695eeec33715d88 100644
--- a/.gitlab/ci/gitlab-gems.gitlab-ci.yml
+++ b/.gitlab/ci/gitlab-gems.gitlab-ci.yml
@@ -14,3 +14,6 @@ include:
   - local: .gitlab/ci/templates/gem.gitlab-ci.yml
     inputs:
       gem_name: "gitlab-schema-validation"
+  - local: .gitlab/ci/templates/gem.gitlab-ci.yml
+    inputs:
+      gem_name: "gitlab-ipynbdiff"
diff --git a/.gitlab/ci/vendored-gems.gitlab-ci.yml b/.gitlab/ci/vendored-gems.gitlab-ci.yml
index 3c9dadb83322d3e1ee8c4cdba9470ff7c4e3aacd..a06481197e5fb7cf296e24d4b3bc71b625ad79b2 100644
--- a/.gitlab/ci/vendored-gems.gitlab-ci.yml
+++ b/.gitlab/ci/vendored-gems.gitlab-ci.yml
@@ -11,10 +11,6 @@ include:
     inputs:
       gem_name: "microsoft_graph_mailer"
       gem_path_prefix: "vendor/gems/"
-  - local: .gitlab/ci/templates/gem.gitlab-ci.yml
-    inputs:
-      gem_name: "ipynbdiff"
-      gem_path_prefix: "vendor/gems/"
   - local: .gitlab/ci/templates/gem.gitlab-ci.yml
     inputs:
       gem_name: "omniauth-azure-oauth2"
diff --git a/Gemfile b/Gemfile
index 6aeaa32425e14d157e4d9cc08a8917b5de79a966..3a6ce1553f7fcf4cb2adce1e672b49d9e99831cd 100644
--- a/Gemfile
+++ b/Gemfile
@@ -593,7 +593,7 @@ gem 'ipaddress', '~> 0.8.3'
 
 gem 'parslet', '~> 1.8'
 
-gem 'ipynbdiff', path: 'vendor/gems/ipynbdiff'
+gem 'ipynbdiff', path: 'gems/ipynbdiff', require: 'ipynb_diff'
 
 gem 'ed25519', '~> 1.3.0'
 
diff --git a/Gemfile.lock b/Gemfile.lock
index a2a7a3f26aef5bdb06ee33f6bc36d48f510d4604..adbca941778c13dab16b7465f55bdff01e6fc28d 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -34,6 +34,13 @@ PATH
       nokogiri (~> 1.15.2)
       rake (~> 13.0)
 
+PATH
+  remote: gems/ipynbdiff
+  specs:
+    ipynbdiff (0.4.7)
+      diffy (~> 3.4)
+      oj (~> 3.13.16)
+
 PATH
   remote: vendor/gems/attr_encrypted
   specs:
@@ -68,13 +75,6 @@ PATH
     error_tracking_open_api (1.0.0)
       typhoeus (~> 1.0, >= 1.0.1)
 
-PATH
-  remote: vendor/gems/ipynbdiff
-  specs:
-    ipynbdiff (0.4.7)
-      diffy (~> 3.4)
-      oj (~> 3.13.16)
-
 PATH
   remote: vendor/gems/mail-smtp_pool
   specs:
diff --git a/vendor/gems/ipynbdiff/.gitignore b/gems/ipynbdiff/.gitignore
similarity index 60%
rename from vendor/gems/ipynbdiff/.gitignore
rename to gems/ipynbdiff/.gitignore
index 4f284c35a42069bc139e86aa9edc504e0978ebdb..fae8d1c772f887986ef956c473c11551de9a9faa 100644
--- a/vendor/gems/ipynbdiff/.gitignore
+++ b/gems/ipynbdiff/.gitignore
@@ -1,2 +1,3 @@
 *.gem
+coverage
 .bundle
diff --git a/gems/ipynbdiff/.gitlab-ci.yml b/gems/ipynbdiff/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..de5c989a4a44bf3424a4bae0e7b4b885b6470c46
--- /dev/null
+++ b/gems/ipynbdiff/.gitlab-ci.yml
@@ -0,0 +1,4 @@
+include:
+  - local: gems/gem.gitlab-ci.yml
+    inputs:
+      gem_name: "ipynbdiff"
diff --git a/gems/ipynbdiff/.rubocop.yml b/gems/ipynbdiff/.rubocop.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f3a696778a4cc4aeaba2bab20eb07e8e4ec2c0f0
--- /dev/null
+++ b/gems/ipynbdiff/.rubocop.yml
@@ -0,0 +1,11 @@
+inherit_from:
+  - ../config/rubocop.yml
+
+CodeReuse/ActiveRecord:
+  Enabled: false
+
+Naming/FileName:
+  Exclude:
+    - spec/**/*.rb
+    - lib/gitlab/rspec.rb
+    - lib/gitlab/rspec/all.rb
diff --git a/vendor/gems/ipynbdiff/Gemfile b/gems/ipynbdiff/Gemfile
similarity index 100%
rename from vendor/gems/ipynbdiff/Gemfile
rename to gems/ipynbdiff/Gemfile
diff --git a/vendor/gems/ipynbdiff/Gemfile.lock b/gems/ipynbdiff/Gemfile.lock
similarity index 82%
rename from vendor/gems/ipynbdiff/Gemfile.lock
rename to gems/ipynbdiff/Gemfile.lock
index 6a8d375060251881f43b1e53d3ac458a22ae2b50..1c52ff8c8297694f67252477da4d212f26ac4282 100644
--- a/vendor/gems/ipynbdiff/Gemfile.lock
+++ b/gems/ipynbdiff/Gemfile.lock
@@ -1,7 +1,7 @@
 PATH
   remote: .
   specs:
-    ipynbdiff (0.4.7)
+    gitlab-ipynbdiff (0.4.7)
       diffy (~> 3.4)
       oj (~> 3.13.16)
 
@@ -15,9 +15,10 @@ GEM
     coderay (1.1.3)
     diff-lcs (1.5.0)
     diffy (3.4.2)
+    docile (1.4.0)
     memory_profiler (1.0.0)
     method_source (1.0.0)
-    oj (3.13.16)
+    oj (3.13.23)
     parser (3.1.2.0)
       ast (~> 2.4.1)
     proc_to_ast (0.1.0)
@@ -47,6 +48,12 @@ GEM
       rspec (>= 2.13, < 4)
       unparser
     rspec-support (3.11.0)
+    simplecov (0.22.0)
+      docile (~> 1.1)
+      simplecov-html (~> 0.11)
+      simplecov_json_formatter (~> 0.1)
+    simplecov-html (0.12.3)
+    simplecov_json_formatter (0.1.4)
     unparser (0.6.5)
       diff-lcs (~> 1.3)
       parser (>= 3.1.0)
@@ -57,11 +64,12 @@ PLATFORMS
 DEPENDENCIES
   benchmark-memory (~> 0.2.0)
   bundler (~> 2.2)
-  ipynbdiff!
+  gitlab-ipynbdiff!
   pry (~> 0.14)
   rake (~> 13.0)
   rspec (~> 3.10)
   rspec-parameterized (~> 0.5.1)
+  simplecov
 
 BUNDLED WITH
    2.3.16
diff --git a/vendor/gems/ipynbdiff/LICENSE b/gems/ipynbdiff/LICENSE
similarity index 100%
rename from vendor/gems/ipynbdiff/LICENSE
rename to gems/ipynbdiff/LICENSE
diff --git a/vendor/gems/ipynbdiff/README.md b/gems/ipynbdiff/README.md
similarity index 100%
rename from vendor/gems/ipynbdiff/README.md
rename to gems/ipynbdiff/README.md
diff --git a/vendor/gems/ipynbdiff/ipynbdiff.gemspec b/gems/ipynbdiff/ipynbdiff.gemspec
similarity index 69%
rename from vendor/gems/ipynbdiff/ipynbdiff.gemspec
rename to gems/ipynbdiff/ipynbdiff.gemspec
index 014005029eff4a1154e5c33d405b09d090683efb..1ec68557a05a247411ac346208537aa72d6b027e 100644
--- a/vendor/gems/ipynbdiff/ipynbdiff.gemspec
+++ b/gems/ipynbdiff/ipynbdiff.gemspec
@@ -1,34 +1,32 @@
 # frozen_string_literal: true
 
-lib = File.expand_path('lib/..', __dir__)
-$LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib)
+$LOAD_PATH.push File.expand_path('lib', __dir__ || '')
 
-require 'lib/version'
+require_relative 'lib/ipynb_diff/version'
 
 Gem::Specification.new do |s|
   s.name        = 'ipynbdiff'
-  s.version     = IpynbDiff::VERSION
+  s.version     = IpynbDiff::Version::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.glob("lib/**/*.*")
-  s.test_files = Dir.glob("spec/**/*.*")
-  s.homepage =
-    'https://gitlab.com/gitlab-org/incubation-engineering/mlops/rb-ipynbdiff'
-  s.license       = 'MIT'
-
-  s.require_paths = ['lib']
+  s.files = Dir['lib/**/*.rb']
+  s.require_paths = ["lib"]
+  s.required_ruby_version = ">= 3.0"
+  s.homepage = 'https://gitlab.com/gitlab-org/gitlab/-/tree/master/gems/ipynbdiff'
+  s.license = 'MIT'
 
   s.add_runtime_dependency 'diffy', '~> 3.4'
   s.add_runtime_dependency 'oj', '~> 3.13.16'
 
+  s.add_development_dependency 'benchmark-memory', '~>0.2.0'
   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'
-  s.add_development_dependency 'benchmark-memory', '~>0.2.0'
+  s.add_development_dependency 'simplecov', '~> 0.12.0'
 end
diff --git a/vendor/gems/ipynbdiff/lib/ipynbdiff.rb b/gems/ipynbdiff/lib/ipynb_diff.rb
similarity index 89%
rename from vendor/gems/ipynbdiff/lib/ipynbdiff.rb
rename to gems/ipynbdiff/lib/ipynb_diff.rb
index 1765e434bf9539eb84a1365ae214c8a4bfb1d7e6..605ff6e4a75e2aa93483ec554ae86ac318eeec18 100644
--- a/vendor/gems/ipynbdiff/lib/ipynbdiff.rb
+++ b/gems/ipynbdiff/lib/ipynb_diff.rb
@@ -1,10 +1,11 @@
 # frozen_string_literal: true
 
+require 'ipynb_diff/transformer'
+require 'ipynb_diff/diff'
+require 'ipynb_diff/symbol_map'
+
 # 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 --git a/vendor/gems/ipynbdiff/lib/diff.rb b/gems/ipynbdiff/lib/ipynb_diff/diff.rb
similarity index 100%
rename from vendor/gems/ipynbdiff/lib/diff.rb
rename to gems/ipynbdiff/lib/ipynb_diff/diff.rb
diff --git a/vendor/gems/ipynbdiff/lib/output_transformer.rb b/gems/ipynbdiff/lib/ipynb_diff/output_transformer.rb
similarity index 83%
rename from vendor/gems/ipynbdiff/lib/output_transformer.rb
rename to gems/ipynbdiff/lib/ipynb_diff/output_transformer.rb
index 57e8a9edce314ddfe037c06ad75ea1d838e1496d..95dbcecf95c1711daee39a292e53f8926dd57ec3 100644
--- a/vendor/gems/ipynbdiff/lib/output_transformer.rb
+++ b/gems/ipynbdiff/lib/ipynb_diff/output_transformer.rb
@@ -1,9 +1,10 @@
 # frozen_string_literal: true
 
+require 'ipynb_diff/symbolized_markdown_helper'
+
 module IpynbDiff
   # Transforms Jupyter output data into markdown
   class OutputTransformer
-    require 'symbolized_markdown_helper'
     include SymbolizedMarkdownHelper
 
     HIDDEN_IMAGE_OUTPUT = '    [Hidden Image Output]'
@@ -32,7 +33,7 @@ def transform(output, symbol)
     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)
+          ___(symbol / idx, l.gsub(/\[[0-9][0-9;]*m/, '').sub("\u001B", '    ').delete("\u001B").rstrip)
         end
       end
     end
@@ -47,22 +48,22 @@ 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)
+        transform_image("#{output_type};base64", output_element, new_symbol)
       when 'image/svg+xml'
-        transform_image(output_type + ';utf8', output_element, new_symbol)
+        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
+      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})")
+      ___(symbol, "    ![](data:#{image_type},#{single_line})")
     end
 
     def transform_text(text_content, symbol)
diff --git a/vendor/gems/ipynbdiff/lib/symbol_map.rb b/gems/ipynbdiff/lib/ipynb_diff/symbol_map.rb
similarity index 94%
rename from vendor/gems/ipynbdiff/lib/symbol_map.rb
rename to gems/ipynbdiff/lib/ipynb_diff/symbol_map.rb
index 89cbccbed1bdc56ac0d40be452f6484e9397f4a2..383f1de5c18e5a2ca031a35526b8f45654b17926 100644
--- a/vendor/gems/ipynbdiff/lib/symbol_map.rb
+++ b/gems/ipynbdiff/lib/ipynb_diff/symbol_map.rb
@@ -36,6 +36,7 @@ module IpynbDiff
   # .obj1.2.obj3.obj4  -> 9
   #
   class SymbolMap
+    # rubocop:disable Lint/UnusedMethodArgument
     class << self
       def handler
         @handler ||= SymbolMap.new
@@ -87,7 +88,7 @@ def key_or_index(key)
       if key.nil? # value in an array
         if @current_path.empty?
           @current_path = ['']
-          return nil
+          return
         end
 
         symbol = @current_array_index.last
@@ -103,5 +104,6 @@ def reset
       @symbols = {}
       @current_array_index = []
     end
+    # rubocop:enable Lint/UnusedMethodArgument
   end
 end
diff --git a/vendor/gems/ipynbdiff/lib/symbolized_markdown_helper.rb b/gems/ipynbdiff/lib/ipynb_diff/symbolized_markdown_helper.rb
similarity index 65%
rename from vendor/gems/ipynbdiff/lib/symbolized_markdown_helper.rb
rename to gems/ipynbdiff/lib/ipynb_diff/symbolized_markdown_helper.rb
index c075aa0d8788b7b30283d73c0e039545d05cf216..991c9e493bc77353f1ed57be2ff4bfb4324ec81d 100644
--- a/vendor/gems/ipynbdiff/lib/symbolized_markdown_helper.rb
+++ b/gems/ipynbdiff/lib/ipynb_diff/symbolized_markdown_helper.rb
@@ -3,16 +3,15 @@
 module IpynbDiff
   # Helper functions
   module SymbolizedMarkdownHelper
-
-    def _(symbol = nil, content = '')
+    def ___(symbol = nil, content = '')
       { symbol: symbol, content: content }
     end
 
-    def symbolize_array(symbol, content, &block)
+    def symbolize_array(symbol, content)
       if content.is_a?(Array)
-        content.map.with_index { |l, idx| _(symbol / idx, block.call(l)) }
+        content.map.with_index { |l, idx| ___(symbol / idx, yield(l)) }
       else
-        content.split("\n").map { |c| _(symbol, c) }
+        content.split("\n").map { |c| ___(symbol, c) }
       end
     end
   end
diff --git a/vendor/gems/ipynbdiff/lib/transformed_notebook.rb b/gems/ipynbdiff/lib/ipynb_diff/transformed_notebook.rb
similarity index 100%
rename from vendor/gems/ipynbdiff/lib/transformed_notebook.rb
rename to gems/ipynbdiff/lib/ipynb_diff/transformed_notebook.rb
diff --git a/vendor/gems/ipynbdiff/lib/transformer.rb b/gems/ipynbdiff/lib/ipynb_diff/transformer.rb
similarity index 79%
rename from vendor/gems/ipynbdiff/lib/transformer.rb
rename to gems/ipynbdiff/lib/ipynb_diff/transformer.rb
index 9e666a20aa557746e89a2e1c1b509fe142c1cba6..2b386168b5d76045147d323fcabd4c564b87aea4 100644
--- a/vendor/gems/ipynbdiff/lib/transformer.rb
+++ b/gems/ipynbdiff/lib/ipynb_diff/transformer.rb
@@ -1,19 +1,18 @@
 # frozen_string_literal: true
 
-module IpynbDiff
-  require 'oj'
+require 'json'
+require 'yaml'
+require 'ipynb_diff/output_transformer'
+require 'ipynb_diff/symbolized_markdown_helper'
+require 'ipynb_diff/symbol_map'
+require 'ipynb_diff/transformed_notebook'
+require 'oj'
 
-  class InvalidNotebookError < StandardError
-  end
+module IpynbDiff
+  InvalidNotebookError = Class.new(StandardError)
 
   # Returns a markdown version of the Jupyter Notebook
   class Transformer
-    require 'json'
-    require 'yaml'
-    require 'output_transformer'
-    require 'symbolized_markdown_helper'
-    require 'symbol_map'
-    require 'transformed_notebook'
     include SymbolizedMarkdownHelper
 
     @include_frontmatter = true
@@ -60,10 +59,10 @@ def decorate_cell(rows, cell, symbol)
       type = cell['cell_type'] || 'raw'
 
       [
-        _(symbol, %(%% Cell type:#{type} id:#{cell['id']} tags:#{tags&.join(',')})),
-        _,
+        ___(symbol, %(%% Cell type:#{type} id:#{cell['id']} tags:#{tags&.join(',')})),
+        ___,
         rows,
-        _
+        ___
       ]
     end
 
@@ -73,9 +72,9 @@ def transform_cell(cell, notebook, symbol)
 
     def transform_code_cell(cell, notebook, symbol)
       [
-        _(symbol / 'source', %(``` #{notebook.dig('metadata', 'kernelspec', 'language') || ''})),
+        ___(symbol / 'source', %(``` #{notebook.dig('metadata', 'kernelspec', 'language') || ''})),
         symbolize_array(symbol / 'source', cell['source'], &:rstrip),
-        _(nil, '```'),
+        ___(nil, '```'),
         transform_outputs(cell['outputs'], symbol)
       ]
     end
@@ -84,10 +83,10 @@ def transform_outputs(outputs, symbol)
       transformed = outputs.map
                            .with_index { |output, i| @out_transformer.transform(output, symbol / ['outputs', i]) }
                            .compact
-                           .map { |el| [_, el] }
+                           .map { |el| [___, el] }
 
       [
-        transformed.empty? ? [] : [_, _(symbol / 'outputs', '%% Output')],
+        transformed.empty? ? [] : [___, ___(symbol / 'outputs', '%% Output')],
         transformed
       ]
     end
@@ -106,7 +105,7 @@ def transform_metadata(notebook_json)
         }
       }.to_yaml
 
-      as_yaml.split("\n").map { |l| _(nil, l) }.append(_(nil, '---'), _)
+      as_yaml.split("\n").map { |l| ___(nil, l) }.append(___(nil, '---'), ___)
     end
   end
 end
diff --git a/vendor/gems/ipynbdiff/lib/version.rb b/gems/ipynbdiff/lib/ipynb_diff/version.rb
similarity index 53%
rename from vendor/gems/ipynbdiff/lib/version.rb
rename to gems/ipynbdiff/lib/ipynb_diff/version.rb
index 1451bb4ef329ee0a563aa79a9cc2af7130333253..1a407f9c0fad7705901472a5a49cc54fd77759a5 100644
--- a/vendor/gems/ipynbdiff/lib/version.rb
+++ b/gems/ipynbdiff/lib/ipynb_diff/version.rb
@@ -1,5 +1,7 @@
 # frozen_string_literal: true
 
 module IpynbDiff
-  VERSION = '0.4.7'
+  module Version
+    VERSION = '0.4.7'
+  end
 end
diff --git a/vendor/gems/ipynbdiff/spec/benchmark.rb b/gems/ipynbdiff/spec/benchmark.rb
similarity index 99%
rename from vendor/gems/ipynbdiff/spec/benchmark.rb
rename to gems/ipynbdiff/spec/benchmark.rb
index 99f088f2056545b9b6eaa481181d3539c4ac484e..514c870018303e8ea4e2f59dea2b3c833b0acf73 100644
--- a/vendor/gems/ipynbdiff/spec/benchmark.rb
+++ b/gems/ipynbdiff/spec/benchmark.rb
@@ -1,8 +1,11 @@
+# frozen_string_literal: true
+
 require 'ipynbdiff'
 require 'benchmark'
 require 'benchmark/memory'
 require_relative 'test_helper'
 
+# rubocop:disable Layout/LineLength
 large_cell = '{
   "cell_type": "code",
   "execution_count": 9,
@@ -26,6 +29,7 @@
     "do_plot(is_sin = False)"
   ]
 },'
+# rubocop:enable Layout/LineLength
 
 base = '{
  "cells": [
diff --git a/gems/ipynbdiff/spec/ipynb_diff/symbol_map_spec.rb b/gems/ipynbdiff/spec/ipynb_diff/symbol_map_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..66fb7af66afebe84db3dd339507a40117c914cc5
--- /dev/null
+++ b/gems/ipynbdiff/spec/ipynb_diff/symbol_map_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require_relative '../test_helper'
+
+describe IpynbDiff::SymbolMap do
+  def res(*cases)
+    cases&.to_h || []
+  end
+
+  describe '.parse' do
+    subject { described_class.parse(JSON.pretty_generate(source)) }
+
+    context 'when object has blank key' do
+      let(:source) { { "": { "": 5 } } }
+
+      it { is_expected.to match_array(res([".", 2], ["..", 3])) }
+    end
+
+    context 'when object is empty' do
+      let(:source) { {} }
+
+      it { is_expected.to be_empty }
+    end
+
+    context 'when object is empty array' do
+      let(:source) { [] }
+
+      it { is_expected.to be_empty }
+    end
+
+    context 'when object has inner object and number' do
+      let(:source) { { obj1: { obj2: 1 } } }
+
+      it { is_expected.to match_array(res(['.obj1', 2], ['.obj1.obj2', 3])) }
+    end
+
+    context 'when object has 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', 2],
+            ['.obj1.obj2', 3],
+            ['.obj1.obj2.0', 4],
+            ['.obj1.obj2.1', 5],
+            ['.obj1.obj2.2', 6],
+            ['.obj1.obj3', 8],
+            ['.obj1.obj4', 9],
+            ['.obj1.obj5', 10],
+            ['.obj1.obj6', 11])
+        )
+      end
+    end
+  end
+end
diff --git a/gems/ipynbdiff/spec/ipynb_diff/transformer_spec.rb b/gems/ipynbdiff/spec/ipynb_diff/transformer_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..214a8192542771869a8b34944930e1d62ce6d88a
--- /dev/null
+++ b/gems/ipynbdiff/spec/ipynb_diff/transformer_spec.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+require_relative '../test_helper'
+
+describe IpynbDiff::Transformer do
+  describe '.transform' do
+    using RSpec::Parameterized::TableSyntax
+
+    let!(:default_config) { { include_frontmatter: false, hide_images: false } }
+
+    let(:test_case) { read_test_case(test_case_name) }
+    let(:notebook) { test_case[:input] || FROM_IPYNB }
+    let(:config) { {} }
+
+    subject { described_class.new(**default_config.merge(config)).transform(notebook) }
+
+    where(:ctx, :test_case_name, :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 }
+      '\n within source lines' | 'source_with_linebreak' | { hide_images: true }
+    end
+
+    with_them do
+      it 'generates the expected markdown' do
+        expect(subject.as_text).to eq test_case[:expected_markdown]
+      end
+
+      it 'marks the lines correctly' do
+        blocks = subject.blocks.map { |b| b[:source_symbol] }.join("\n")
+
+        expect(blocks).to eq test_case[:expected_symbols]
+      end
+    end
+
+    describe 'Source line map' do
+      let(:config) { { include_frontmatter: false } }
+      let(:test_case_name) { 'text_png_output' }
+
+      it 'generates the correct transformed to source line map' do
+        line_numbers = subject.blocks.map { |b| b[:source_line] }.join("\n")
+
+        expect(line_numbers).to eq test_case[:expected_line_numbers]
+      end
+    end
+
+    context 'when json is invalid' do
+      let(:notebook) { 'a' }
+
+      it 'raises error' do
+        expect { subject }.to raise_error(IpynbDiff::InvalidNotebookError)
+      end
+    end
+
+    context 'when it does not have the cell tag' do
+      let(:notebook) { '{"metadata":[]}' }
+
+      it 'raises error' do
+        expect { subject }.to raise_error(IpynbDiff::InvalidNotebookError)
+      end
+    end
+
+    context 'when notebook can not be parsed' do
+      let(:notebook) { '{"cells":[]}' }
+
+      before do
+        allow(Oj::Parser.usual).to receive(:parse).and_return(nil)
+      end
+
+      it 'raises error' do
+        expect { subject }.to raise_error(IpynbDiff::InvalidNotebookError)
+      end
+    end
+  end
+end
diff --git a/vendor/gems/ipynbdiff/spec/ipynbdiff_spec.rb b/gems/ipynbdiff/spec/ipynb_diff_spec.rb
similarity index 51%
rename from vendor/gems/ipynbdiff/spec/ipynbdiff_spec.rb
rename to gems/ipynbdiff/spec/ipynb_diff_spec.rb
index 1c2a2188edf7f1ab987e8aadcd6d2cf36787eb42..44fcd99f13131ad66dd8ccc07bfe8b98e4948fcd 100644
--- a/vendor/gems/ipynbdiff/spec/ipynbdiff_spec.rb
+++ b/gems/ipynbdiff/spec/ipynb_diff_spec.rb
@@ -1,59 +1,55 @@
 # frozen_string_literal: true
 
-require 'ipynbdiff'
-require 'rspec'
-require 'rspec-parameterized'
-
-BASE_PATH = File.join(File.expand_path(File.dirname(__FILE__)),  'testdata')
+require_relative 'test_helper'
 
 describe IpynbDiff do
   def diff_signs(diff)
-    diff.to_s(:text).scan(/.*\n/).map { |l| l[0] }.join('')
+    diff.to_s(:text).scan(/.*\n/).map { |l| l[0] }.join('') # rubocop:disable Rails/Pluck
   end
 
-  describe 'diff' do
-    let(:from_path) { File.join(BASE_PATH, 'from.ipynb') }
-    let(:to_path) { File.join(BASE_PATH,'to.ipynb') }
+  describe '.diff' do
+    let(:from_path) { FROM_PATH }
+    let(:to_path) { TO_PATH }
     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) }
+    subject { described_class.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>')
+        is_expected.not_to include('<td>')
       end
     end
 
     context 'when to is nil' do
       let(:to) { nil }
-      let(:from_path) { File.join(BASE_PATH, 'only_md', 'input.ipynb') }
+      let(:from_path) { test_case_input_path('only_md') }
 
       it 'all lines are removals' do
         expect(diff_signs(subject)).to eq('-----')
       end
     end
 
-    context 'when to is nil' do
+    context 'when from is nil' do
       let(:from) { nil }
-      let(:to_path) { File.join(BASE_PATH, 'only_md', 'input.ipynb') }
+      let(:to_path) { test_case_input_path('only_md') }
 
       it 'all lines are additions' do
         expect(diff_signs(subject)).to eq('+++++')
       end
     end
 
-    context 'When include_frontmatter is true' do
+    context 'when include_frontmatter is true' do
       let(:include_frontmatter) { true }
 
-      it 'should show changes metadata in the metadata' do
+      it 'shows 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
+    context 'when hide_images is true' do
       let(:hide_images) { true }
 
       it 'hides images' do
@@ -61,9 +57,9 @@ def diff_signs(diff)
       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)')
+    context 'when include_frontmatter is false' do
+      it 'drops metadata from the diff' do
+        expect(subject.to_s(:text)).not_to include('+    display_name: New Python 3 (ipykernel)')
       end
     end
 
@@ -83,40 +79,47 @@ def diff_signs(diff)
     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
+  describe '.transform' do
+    let(:notebook) { FROM_IPYNB }
+    let(:include_frontmatter) { false }
+    let(:hide_images) { false }
+
+    subject do
+      described_class.transform(notebook,
+        include_frontmatter: include_frontmatter,
+        hide_images: hide_images)
     end
 
-    context 'options' do
-      let(:include_frontmatter) { false }
-      let(:hide_images) { false }
+    describe 'error cases' do
+      using RSpec::Parameterized::TableSyntax
+
+      where(:ctx, :notebook) do
+        'notebook is nil' | nil
+        'notebook is invalid' | 'a'
+        'notebook does not have cell' | '{"metadata":[]}'
+      end
 
-      subject do
-        IpynbDiff.transform(File.read(File.join(BASE_PATH, 'from.ipynb')),
-                            include_frontmatter: include_frontmatter,
-                            hide_images: hide_images)
+      with_them do
+        it { is_expected.to be_nil }
       end
+    end
 
-      context 'include_frontmatter is false' do
-        it { is_expected.to_not include('display_name: Python 3 (ipykernel)') }
+    describe 'options' do
+      context 'when include_frontmatter is false' do
+        it { is_expected.not_to include('display_name: Python 3 (ipykernel)') }
       end
 
-      context 'include_frontmatter is true' do
+      context 'when 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
+      context 'when hide_images is false' do
         it { is_expected.not_to include('[Hidden Image Output]') }
       end
 
-      context 'hide_images is true' do
+      context 'when hide_images is true' do
         let(:hide_images) { true }
 
         it { is_expected.to include('    [Hidden Image Output]') }
diff --git a/gems/ipynbdiff/spec/test_helper.rb b/gems/ipynbdiff/spec/test_helper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..626b72b99f0bff7f2ae9f410419bbd9dbbe69d54
--- /dev/null
+++ b/gems/ipynbdiff/spec/test_helper.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'simplecov'
+SimpleCov.start
+
+require 'ipynb_diff'
+require 'rspec'
+require 'rspec-parameterized'
+require 'json'
+
+BASE_PATH = File.join(__dir__ || '', 'testdata')
+
+FROM_PATH = File.join(BASE_PATH, 'from.ipynb')
+TO_PATH = File.join(BASE_PATH, 'to.ipynb')
+
+FROM_IPYNB = File.read(FROM_PATH)
+TO_IPYNB = File.read(TO_PATH)
+
+def test_case_input_path(test_case)
+  File.join(BASE_PATH, test_case, 'input.ipynb')
+end
+
+def test_case_symbols_path(test_case)
+  File.join(BASE_PATH, test_case, 'expected_symbols.txt')
+end
+
+def test_case_md_path(test_case)
+  File.join(BASE_PATH, test_case, 'expected.md')
+end
+
+def test_case_line_numbers_path(test_case)
+  File.join(BASE_PATH, test_case, 'expected_line_numbers.txt')
+end
+
+def read_file_if_exists(path)
+  File.read(path) if File.file?(path)
+end
+
+def read_test_case(test_case_name)
+  {
+    input: read_file_if_exists(test_case_input_path(test_case_name)),
+    expected_markdown: read_file_if_exists(test_case_md_path(test_case_name)),
+    expected_symbols: read_file_if_exists(test_case_symbols_path(test_case_name)),
+    expected_line_numbers: read_file_if_exists(test_case_line_numbers_path(test_case_name))
+  }
+end
diff --git a/vendor/gems/ipynbdiff/spec/testdata/backslash_as_last_char/expected.md b/gems/ipynbdiff/spec/testdata/backslash_as_last_char/expected.md
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/backslash_as_last_char/expected.md
rename to gems/ipynbdiff/spec/testdata/backslash_as_last_char/expected.md
diff --git a/vendor/gems/ipynbdiff/spec/testdata/backslash_as_last_char/expected_symbols.txt b/gems/ipynbdiff/spec/testdata/backslash_as_last_char/expected_symbols.txt
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/backslash_as_last_char/expected_symbols.txt
rename to gems/ipynbdiff/spec/testdata/backslash_as_last_char/expected_symbols.txt
diff --git a/vendor/gems/ipynbdiff/spec/testdata/backslash_as_last_char/input.ipynb b/gems/ipynbdiff/spec/testdata/backslash_as_last_char/input.ipynb
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/backslash_as_last_char/input.ipynb
rename to gems/ipynbdiff/spec/testdata/backslash_as_last_char/input.ipynb
diff --git a/vendor/gems/ipynbdiff/spec/testdata/error_output/expected.md b/gems/ipynbdiff/spec/testdata/error_output/expected.md
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/error_output/expected.md
rename to gems/ipynbdiff/spec/testdata/error_output/expected.md
diff --git a/vendor/gems/ipynbdiff/spec/testdata/error_output/expected_symbols.txt b/gems/ipynbdiff/spec/testdata/error_output/expected_symbols.txt
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/error_output/expected_symbols.txt
rename to gems/ipynbdiff/spec/testdata/error_output/expected_symbols.txt
diff --git a/vendor/gems/ipynbdiff/spec/testdata/error_output/input.ipynb b/gems/ipynbdiff/spec/testdata/error_output/input.ipynb
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/error_output/input.ipynb
rename to gems/ipynbdiff/spec/testdata/error_output/input.ipynb
diff --git a/vendor/gems/ipynbdiff/spec/testdata/from.ipynb b/gems/ipynbdiff/spec/testdata/from.ipynb
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/from.ipynb
rename to gems/ipynbdiff/spec/testdata/from.ipynb
diff --git a/vendor/gems/ipynbdiff/spec/testdata/hide_images/expected.md b/gems/ipynbdiff/spec/testdata/hide_images/expected.md
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/hide_images/expected.md
rename to gems/ipynbdiff/spec/testdata/hide_images/expected.md
diff --git a/vendor/gems/ipynbdiff/spec/testdata/hide_images/expected_symbols.txt b/gems/ipynbdiff/spec/testdata/hide_images/expected_symbols.txt
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/hide_images/expected_symbols.txt
rename to gems/ipynbdiff/spec/testdata/hide_images/expected_symbols.txt
diff --git a/vendor/gems/ipynbdiff/spec/testdata/hide_images/input.ipynb b/gems/ipynbdiff/spec/testdata/hide_images/input.ipynb
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/hide_images/input.ipynb
rename to gems/ipynbdiff/spec/testdata/hide_images/input.ipynb
diff --git a/vendor/gems/ipynbdiff/spec/testdata/ignore_html_output/expected.md b/gems/ipynbdiff/spec/testdata/ignore_html_output/expected.md
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/ignore_html_output/expected.md
rename to gems/ipynbdiff/spec/testdata/ignore_html_output/expected.md
diff --git a/vendor/gems/ipynbdiff/spec/testdata/ignore_html_output/expected_symbols.txt b/gems/ipynbdiff/spec/testdata/ignore_html_output/expected_symbols.txt
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/ignore_html_output/expected_symbols.txt
rename to gems/ipynbdiff/spec/testdata/ignore_html_output/expected_symbols.txt
diff --git a/vendor/gems/ipynbdiff/spec/testdata/ignore_html_output/input.ipynb b/gems/ipynbdiff/spec/testdata/ignore_html_output/input.ipynb
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/ignore_html_output/input.ipynb
rename to gems/ipynbdiff/spec/testdata/ignore_html_output/input.ipynb
diff --git a/vendor/gems/ipynbdiff/spec/testdata/latex_output/expected.md b/gems/ipynbdiff/spec/testdata/latex_output/expected.md
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/latex_output/expected.md
rename to gems/ipynbdiff/spec/testdata/latex_output/expected.md
diff --git a/vendor/gems/ipynbdiff/spec/testdata/latex_output/expected_symbols.txt b/gems/ipynbdiff/spec/testdata/latex_output/expected_symbols.txt
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/latex_output/expected_symbols.txt
rename to gems/ipynbdiff/spec/testdata/latex_output/expected_symbols.txt
diff --git a/vendor/gems/ipynbdiff/spec/testdata/latex_output/input.ipynb b/gems/ipynbdiff/spec/testdata/latex_output/input.ipynb
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/latex_output/input.ipynb
rename to gems/ipynbdiff/spec/testdata/latex_output/input.ipynb
diff --git a/vendor/gems/ipynbdiff/spec/testdata/multiline_png_output/expected.md b/gems/ipynbdiff/spec/testdata/multiline_png_output/expected.md
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/multiline_png_output/expected.md
rename to gems/ipynbdiff/spec/testdata/multiline_png_output/expected.md
diff --git a/vendor/gems/ipynbdiff/spec/testdata/multiline_png_output/expected_symbols.txt b/gems/ipynbdiff/spec/testdata/multiline_png_output/expected_symbols.txt
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/multiline_png_output/expected_symbols.txt
rename to gems/ipynbdiff/spec/testdata/multiline_png_output/expected_symbols.txt
diff --git a/vendor/gems/ipynbdiff/spec/testdata/multiline_png_output/input.ipynb b/gems/ipynbdiff/spec/testdata/multiline_png_output/input.ipynb
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/multiline_png_output/input.ipynb
rename to gems/ipynbdiff/spec/testdata/multiline_png_output/input.ipynb
diff --git a/vendor/gems/ipynbdiff/spec/testdata/no_cells/expected.md b/gems/ipynbdiff/spec/testdata/no_cells/expected.md
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/no_cells/expected.md
rename to gems/ipynbdiff/spec/testdata/no_cells/expected.md
diff --git a/vendor/gems/ipynbdiff/spec/testdata/no_cells/expected_symbols.txt b/gems/ipynbdiff/spec/testdata/no_cells/expected_symbols.txt
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/no_cells/expected_symbols.txt
rename to gems/ipynbdiff/spec/testdata/no_cells/expected_symbols.txt
diff --git a/vendor/gems/ipynbdiff/spec/testdata/no_cells/input.ipynb b/gems/ipynbdiff/spec/testdata/no_cells/input.ipynb
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/no_cells/input.ipynb
rename to gems/ipynbdiff/spec/testdata/no_cells/input.ipynb
diff --git a/vendor/gems/ipynbdiff/spec/testdata/no_cells_no_metadata/expected.md b/gems/ipynbdiff/spec/testdata/no_cells_no_metadata/expected.md
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/no_cells_no_metadata/expected.md
rename to gems/ipynbdiff/spec/testdata/no_cells_no_metadata/expected.md
diff --git a/vendor/gems/ipynbdiff/spec/testdata/no_cells_no_metadata/expected_symbols.txt b/gems/ipynbdiff/spec/testdata/no_cells_no_metadata/expected_symbols.txt
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/no_cells_no_metadata/expected_symbols.txt
rename to gems/ipynbdiff/spec/testdata/no_cells_no_metadata/expected_symbols.txt
diff --git a/vendor/gems/ipynbdiff/spec/testdata/no_cells_no_metadata/input.ipynb b/gems/ipynbdiff/spec/testdata/no_cells_no_metadata/input.ipynb
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/no_cells_no_metadata/input.ipynb
rename to gems/ipynbdiff/spec/testdata/no_cells_no_metadata/input.ipynb
diff --git a/vendor/gems/ipynbdiff/spec/testdata/no_metadata_on_cell/expected.md b/gems/ipynbdiff/spec/testdata/no_metadata_on_cell/expected.md
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/no_metadata_on_cell/expected.md
rename to gems/ipynbdiff/spec/testdata/no_metadata_on_cell/expected.md
diff --git a/vendor/gems/ipynbdiff/spec/testdata/no_metadata_on_cell/expected_symbols.txt b/gems/ipynbdiff/spec/testdata/no_metadata_on_cell/expected_symbols.txt
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/no_metadata_on_cell/expected_symbols.txt
rename to gems/ipynbdiff/spec/testdata/no_metadata_on_cell/expected_symbols.txt
diff --git a/vendor/gems/ipynbdiff/spec/testdata/no_metadata_on_cell/input.ipynb b/gems/ipynbdiff/spec/testdata/no_metadata_on_cell/input.ipynb
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/no_metadata_on_cell/input.ipynb
rename to gems/ipynbdiff/spec/testdata/no_metadata_on_cell/input.ipynb
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_code/expected.md b/gems/ipynbdiff/spec/testdata/only_code/expected.md
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/only_code/expected.md
rename to gems/ipynbdiff/spec/testdata/only_code/expected.md
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_code/expected_symbols.txt b/gems/ipynbdiff/spec/testdata/only_code/expected_symbols.txt
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/only_code/expected_symbols.txt
rename to gems/ipynbdiff/spec/testdata/only_code/expected_symbols.txt
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_code/input.ipynb b/gems/ipynbdiff/spec/testdata/only_code/input.ipynb
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/only_code/input.ipynb
rename to gems/ipynbdiff/spec/testdata/only_code/input.ipynb
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_code_no_kernelspec/expected.md b/gems/ipynbdiff/spec/testdata/only_code_no_kernelspec/expected.md
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/only_code_no_kernelspec/expected.md
rename to gems/ipynbdiff/spec/testdata/only_code_no_kernelspec/expected.md
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_code_no_kernelspec/expected_symbols.txt b/gems/ipynbdiff/spec/testdata/only_code_no_kernelspec/expected_symbols.txt
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/only_code_no_kernelspec/expected_symbols.txt
rename to gems/ipynbdiff/spec/testdata/only_code_no_kernelspec/expected_symbols.txt
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_code_no_kernelspec/input.ipynb b/gems/ipynbdiff/spec/testdata/only_code_no_kernelspec/input.ipynb
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/only_code_no_kernelspec/input.ipynb
rename to gems/ipynbdiff/spec/testdata/only_code_no_kernelspec/input.ipynb
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_code_no_language/expected.md b/gems/ipynbdiff/spec/testdata/only_code_no_language/expected.md
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/only_code_no_language/expected.md
rename to gems/ipynbdiff/spec/testdata/only_code_no_language/expected.md
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_code_no_language/expected_symbols.txt b/gems/ipynbdiff/spec/testdata/only_code_no_language/expected_symbols.txt
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/only_code_no_language/expected_symbols.txt
rename to gems/ipynbdiff/spec/testdata/only_code_no_language/expected_symbols.txt
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_code_no_language/input.ipynb b/gems/ipynbdiff/spec/testdata/only_code_no_language/input.ipynb
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/only_code_no_language/input.ipynb
rename to gems/ipynbdiff/spec/testdata/only_code_no_language/input.ipynb
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_code_no_metadata/expected.md b/gems/ipynbdiff/spec/testdata/only_code_no_metadata/expected.md
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/only_code_no_metadata/expected.md
rename to gems/ipynbdiff/spec/testdata/only_code_no_metadata/expected.md
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_code_no_metadata/expected_symbols.txt b/gems/ipynbdiff/spec/testdata/only_code_no_metadata/expected_symbols.txt
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/only_code_no_metadata/expected_symbols.txt
rename to gems/ipynbdiff/spec/testdata/only_code_no_metadata/expected_symbols.txt
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_code_no_metadata/input.ipynb b/gems/ipynbdiff/spec/testdata/only_code_no_metadata/input.ipynb
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/only_code_no_metadata/input.ipynb
rename to gems/ipynbdiff/spec/testdata/only_code_no_metadata/input.ipynb
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_md/expected.md b/gems/ipynbdiff/spec/testdata/only_md/expected.md
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/only_md/expected.md
rename to gems/ipynbdiff/spec/testdata/only_md/expected.md
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_md/expected_symbols.txt b/gems/ipynbdiff/spec/testdata/only_md/expected_symbols.txt
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/only_md/expected_symbols.txt
rename to gems/ipynbdiff/spec/testdata/only_md/expected_symbols.txt
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_md/input.ipynb b/gems/ipynbdiff/spec/testdata/only_md/input.ipynb
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/only_md/input.ipynb
rename to gems/ipynbdiff/spec/testdata/only_md/input.ipynb
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_raw/expected.md b/gems/ipynbdiff/spec/testdata/only_raw/expected.md
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/only_raw/expected.md
rename to gems/ipynbdiff/spec/testdata/only_raw/expected.md
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_raw/expected_symbols.txt b/gems/ipynbdiff/spec/testdata/only_raw/expected_symbols.txt
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/only_raw/expected_symbols.txt
rename to gems/ipynbdiff/spec/testdata/only_raw/expected_symbols.txt
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_raw/input.ipynb b/gems/ipynbdiff/spec/testdata/only_raw/input.ipynb
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/only_raw/input.ipynb
rename to gems/ipynbdiff/spec/testdata/only_raw/input.ipynb
diff --git a/vendor/gems/ipynbdiff/spec/testdata/percent_decorator/expected.md b/gems/ipynbdiff/spec/testdata/percent_decorator/expected.md
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/percent_decorator/expected.md
rename to gems/ipynbdiff/spec/testdata/percent_decorator/expected.md
diff --git a/vendor/gems/ipynbdiff/spec/testdata/percent_decorator/expected_symbols.txt b/gems/ipynbdiff/spec/testdata/percent_decorator/expected_symbols.txt
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/percent_decorator/expected_symbols.txt
rename to gems/ipynbdiff/spec/testdata/percent_decorator/expected_symbols.txt
diff --git a/vendor/gems/ipynbdiff/spec/testdata/single_line_md/expected.md b/gems/ipynbdiff/spec/testdata/single_line_md/expected.md
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/single_line_md/expected.md
rename to gems/ipynbdiff/spec/testdata/single_line_md/expected.md
diff --git a/vendor/gems/ipynbdiff/spec/testdata/single_line_md/expected_symbols.txt b/gems/ipynbdiff/spec/testdata/single_line_md/expected_symbols.txt
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/single_line_md/expected_symbols.txt
rename to gems/ipynbdiff/spec/testdata/single_line_md/expected_symbols.txt
diff --git a/vendor/gems/ipynbdiff/spec/testdata/single_line_md/input.ipynb b/gems/ipynbdiff/spec/testdata/single_line_md/input.ipynb
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/single_line_md/input.ipynb
rename to gems/ipynbdiff/spec/testdata/single_line_md/input.ipynb
diff --git a/vendor/gems/ipynbdiff/spec/testdata/source_with_linebreak/expected.md b/gems/ipynbdiff/spec/testdata/source_with_linebreak/expected.md
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/source_with_linebreak/expected.md
rename to gems/ipynbdiff/spec/testdata/source_with_linebreak/expected.md
diff --git a/vendor/gems/ipynbdiff/spec/testdata/source_with_linebreak/expected_symbols.txt b/gems/ipynbdiff/spec/testdata/source_with_linebreak/expected_symbols.txt
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/source_with_linebreak/expected_symbols.txt
rename to gems/ipynbdiff/spec/testdata/source_with_linebreak/expected_symbols.txt
diff --git a/vendor/gems/ipynbdiff/spec/testdata/source_with_linebreak/input.ipynb b/gems/ipynbdiff/spec/testdata/source_with_linebreak/input.ipynb
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/source_with_linebreak/input.ipynb
rename to gems/ipynbdiff/spec/testdata/source_with_linebreak/input.ipynb
diff --git a/vendor/gems/ipynbdiff/spec/testdata/stream_text/expected.md b/gems/ipynbdiff/spec/testdata/stream_text/expected.md
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/stream_text/expected.md
rename to gems/ipynbdiff/spec/testdata/stream_text/expected.md
diff --git a/vendor/gems/ipynbdiff/spec/testdata/stream_text/expected_symbols.txt b/gems/ipynbdiff/spec/testdata/stream_text/expected_symbols.txt
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/stream_text/expected_symbols.txt
rename to gems/ipynbdiff/spec/testdata/stream_text/expected_symbols.txt
diff --git a/vendor/gems/ipynbdiff/spec/testdata/stream_text/input.ipynb b/gems/ipynbdiff/spec/testdata/stream_text/input.ipynb
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/stream_text/input.ipynb
rename to gems/ipynbdiff/spec/testdata/stream_text/input.ipynb
diff --git a/vendor/gems/ipynbdiff/spec/testdata/svg/expected.md b/gems/ipynbdiff/spec/testdata/svg/expected.md
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/svg/expected.md
rename to gems/ipynbdiff/spec/testdata/svg/expected.md
diff --git a/vendor/gems/ipynbdiff/spec/testdata/svg/expected_symbols.txt b/gems/ipynbdiff/spec/testdata/svg/expected_symbols.txt
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/svg/expected_symbols.txt
rename to gems/ipynbdiff/spec/testdata/svg/expected_symbols.txt
diff --git a/vendor/gems/ipynbdiff/spec/testdata/svg/input.ipynb b/gems/ipynbdiff/spec/testdata/svg/input.ipynb
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/svg/input.ipynb
rename to gems/ipynbdiff/spec/testdata/svg/input.ipynb
diff --git a/vendor/gems/ipynbdiff/spec/testdata/text_output/expected.md b/gems/ipynbdiff/spec/testdata/text_output/expected.md
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/text_output/expected.md
rename to gems/ipynbdiff/spec/testdata/text_output/expected.md
diff --git a/vendor/gems/ipynbdiff/spec/testdata/text_output/expected_symbols.txt b/gems/ipynbdiff/spec/testdata/text_output/expected_symbols.txt
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/text_output/expected_symbols.txt
rename to gems/ipynbdiff/spec/testdata/text_output/expected_symbols.txt
diff --git a/vendor/gems/ipynbdiff/spec/testdata/text_output/input.ipynb b/gems/ipynbdiff/spec/testdata/text_output/input.ipynb
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/text_output/input.ipynb
rename to gems/ipynbdiff/spec/testdata/text_output/input.ipynb
diff --git a/vendor/gems/ipynbdiff/spec/testdata/text_png_output/expected.md b/gems/ipynbdiff/spec/testdata/text_png_output/expected.md
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/text_png_output/expected.md
rename to gems/ipynbdiff/spec/testdata/text_png_output/expected.md
diff --git a/vendor/gems/ipynbdiff/spec/testdata/text_png_output/expected_line_numbers.txt b/gems/ipynbdiff/spec/testdata/text_png_output/expected_line_numbers.txt
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/text_png_output/expected_line_numbers.txt
rename to gems/ipynbdiff/spec/testdata/text_png_output/expected_line_numbers.txt
diff --git a/vendor/gems/ipynbdiff/spec/testdata/text_png_output/expected_symbols.txt b/gems/ipynbdiff/spec/testdata/text_png_output/expected_symbols.txt
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/text_png_output/expected_symbols.txt
rename to gems/ipynbdiff/spec/testdata/text_png_output/expected_symbols.txt
diff --git a/vendor/gems/ipynbdiff/spec/testdata/text_png_output/input.ipynb b/gems/ipynbdiff/spec/testdata/text_png_output/input.ipynb
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/text_png_output/input.ipynb
rename to gems/ipynbdiff/spec/testdata/text_png_output/input.ipynb
diff --git a/vendor/gems/ipynbdiff/spec/testdata/to.ipynb b/gems/ipynbdiff/spec/testdata/to.ipynb
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/to.ipynb
rename to gems/ipynbdiff/spec/testdata/to.ipynb
diff --git a/vendor/gems/ipynbdiff/spec/testdata/unknown_output_type/expected.md b/gems/ipynbdiff/spec/testdata/unknown_output_type/expected.md
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/unknown_output_type/expected.md
rename to gems/ipynbdiff/spec/testdata/unknown_output_type/expected.md
diff --git a/vendor/gems/ipynbdiff/spec/testdata/unknown_output_type/expected_symbols.txt b/gems/ipynbdiff/spec/testdata/unknown_output_type/expected_symbols.txt
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/unknown_output_type/expected_symbols.txt
rename to gems/ipynbdiff/spec/testdata/unknown_output_type/expected_symbols.txt
diff --git a/vendor/gems/ipynbdiff/spec/testdata/unknown_output_type/input.ipynb b/gems/ipynbdiff/spec/testdata/unknown_output_type/input.ipynb
similarity index 100%
rename from vendor/gems/ipynbdiff/spec/testdata/unknown_output_type/input.ipynb
rename to gems/ipynbdiff/spec/testdata/unknown_output_type/input.ipynb
diff --git a/spec/lib/gitlab/diff/rendered/notebook/diff_file_spec.rb b/spec/lib/gitlab/diff/rendered/notebook/diff_file_spec.rb
index e1135f4d5460aec3dfda3d82d6fb2e77e41889aa..4b2bb6cbb028dbe3146041702a6de2f9ccdc6087 100644
--- a/spec/lib/gitlab/diff/rendered/notebook/diff_file_spec.rb
+++ b/spec/lib/gitlab/diff/rendered/notebook/diff_file_spec.rb
@@ -2,7 +2,7 @@
 
 require 'spec_helper'
 
-RSpec.describe Gitlab::Diff::Rendered::Notebook::DiffFile do
+RSpec.describe Gitlab::Diff::Rendered::Notebook::DiffFile, feature_category: :mlops do
   include RepoHelpers
 
   let_it_be(:project) { create(:project, :repository) }
diff --git a/vendor/gems/ipynbdiff/.gitlab-ci.yml b/vendor/gems/ipynbdiff/.gitlab-ci.yml
deleted file mode 100644
index dd8f61050bb82aa26a44c5c7a606df2d9ff8044a..0000000000000000000000000000000000000000
--- a/vendor/gems/ipynbdiff/.gitlab-ci.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-include:
-  - local: gems/gem.gitlab-ci.yml
-    inputs:
-      gem_name: "ipynbdiff"
-      gem_path_prefix: "vendor/gems/"
-
-rspec:
-  parallel:
-    matrix:
-      - RUBY_VERSION: ["3.0", "3.1"] # 3.2 isn't supported yet
diff --git a/vendor/gems/ipynbdiff/spec/symbol_map_spec.rb b/vendor/gems/ipynbdiff/spec/symbol_map_spec.rb
deleted file mode 100644
index 5fba47c85afdda9c4d31e01b2b65f10730329512..0000000000000000000000000000000000000000
--- a/vendor/gems/ipynbdiff/spec/symbol_map_spec.rb
+++ /dev/null
@@ -1,58 +0,0 @@
-# frozen_string_literal: true
-
-require 'rspec'
-require 'json'
-require 'rspec-parameterized'
-require 'symbol_map'
-
-describe IpynbDiff::SymbolMap do
-  def res(*cases)
-    cases&.to_h || []
-  end
-
-  describe '#parse' do
-    subject { IpynbDiff::SymbolMap.parse(JSON.pretty_generate(source)) }
-
-    context 'Object with blank key' do
-      let(:source) { { "": { "": 5 } }}
-
-      it { is_expected.to match_array(res([".", 2], ["..", 3])) }
-    end
-
-    context 'Empty object' do
-      let(:source) { {} }
-
-      it { is_expected.to be_empty }
-    end
-
-    context 'Empty array' 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', 2], ['.obj1.obj2', 3])) }
-    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', 2],
-              ['.obj1.obj2', 3],
-              ['.obj1.obj2.0', 4],
-              ['.obj1.obj2.1', 5],
-              ['.obj1.obj2.2', 6],
-              ['.obj1.obj3', 8],
-              ['.obj1.obj4', 9],
-              ['.obj1.obj5', 10],
-              ['.obj1.obj6', 11])
-        )
-      end
-    end
-  end
-end
diff --git a/vendor/gems/ipynbdiff/spec/test_helper.rb b/vendor/gems/ipynbdiff/spec/test_helper.rb
deleted file mode 100644
index f9c416885a1220a04148a02c66ed6d1a47aacb0a..0000000000000000000000000000000000000000
--- a/vendor/gems/ipynbdiff/spec/test_helper.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-BASE_PATH = File.join(File.expand_path(File.dirname(__FILE__)),  'testdata')
-
-FROM_PATH = File.join(BASE_PATH, 'from.ipynb')
-TO_PATH = File.join(BASE_PATH, 'to.ipynb')
-
-FROM_IPYNB = File.read(FROM_PATH)
-TO_IPYNB = File.read(TO_PATH)
-
-def input_for_test(test_case)
-  File.join(BASE_PATH, test_case, 'input.ipynb')
-end
-
-def expected_symbols(test_case)
-  File.join(BASE_PATH, test_case, 'expected_symbols.txt')
-end
-
-def expected_md(test_case)
-  File.join(BASE_PATH, test_case, 'expected.md')
-end
-
-def expected_line_numbers(test_case)
-  File.join(BASE_PATH, test_case, 'expected_line_numbers.txt')
-end
diff --git a/vendor/gems/ipynbdiff/spec/transformer_spec.rb b/vendor/gems/ipynbdiff/spec/transformer_spec.rb
deleted file mode 100644
index 660d0a2bd79737abf17387288c55b15d59cd213a..0000000000000000000000000000000000000000
--- a/vendor/gems/ipynbdiff/spec/transformer_spec.rb
+++ /dev/null
@@ -1,117 +0,0 @@
-# frozen_string_literal: true
-
-require 'rspec'
-require 'ipynbdiff'
-require 'json'
-require 'rspec-parameterized'
-
-TRANSFORMER_BASE_PATH = File.join(File.expand_path(File.dirname(__FILE__)),  'testdata')
-
-def read_file(*paths)
-  File.read(File.join(TRANSFORMER_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 }
-      '\n within source lines' | 'source_with_linebreak' | { 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 'marks the lines correctly' do
-        blocks = transformed.blocks.map { |b| b[:source_symbol] }.join("\n")
-        result = expected_symbols
-
-        expect(blocks).to eq result
-      end
-    end
-  end
-
-  it 'generates the correct transformed to source line map' do
-    input = read_file('text_png_output', 'input.ipynb' )
-    expected_line_numbers = read_file('text_png_output', 'expected_line_numbers.txt' )
-
-    transformed = IpynbDiff::Transformer.new(**{ include_frontmatter: false }).transform(input)
-
-    line_numbers = transformed.blocks.map { |b| b[:source_line] }.join("\n")
-
-    expect(line_numbers).to eq(expected_line_numbers)
-
-  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
-
-    context 'when notebook can not be parsed' do
-      it 'raises error' do
-        notebook = '{"cells":[]}'
-        allow(Oj::Parser.usual).to receive(:parse).and_return(nil)
-
-        expect do
-          IpynbDiff::Transformer.new.transform(notebook)
-        end.to raise_error(IpynbDiff::InvalidNotebookError)
-      end
-    end
-  end
-end