panda-editor 0.5.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -0
  3. data/CHANGELOG.md +19 -0
  4. data/README.md +68 -0
  5. data/app/javascript/panda/editor/application.js +8 -6
  6. data/app/javascript/panda/editor/controllers/index.js +5 -0
  7. data/app/javascript/panda/editor/editor_js_config.js +7 -5
  8. data/app/javascript/panda/editor/editor_js_initializer.js +10 -3
  9. data/app/javascript/panda/editor/plugins/embed.min.js +2 -0
  10. data/app/javascript/panda/editor/plugins/header.min.js +9 -0
  11. data/app/javascript/panda/editor/plugins/nested-list.min.js +2 -0
  12. data/app/javascript/panda/editor/plugins/paragraph.min.js +9 -0
  13. data/app/javascript/panda/editor/plugins/quote.min.js +2 -0
  14. data/app/javascript/panda/editor/plugins/simple-image.min.js +2 -0
  15. data/app/javascript/panda/editor/plugins/table.min.js +2 -0
  16. data/app/javascript/panda/editor/rich_text_editor.js +2 -3
  17. data/app/services/panda/editor/html_to_editor_js_converter.rb +68 -68
  18. data/app/stylesheets/editor.css +120 -0
  19. data/config/importmap.rb +22 -11
  20. data/docs/FOOTNOTES.md +96 -3
  21. data/lefthook.yml +16 -0
  22. data/lib/panda/editor/asset_loader.rb +27 -27
  23. data/lib/panda/editor/blocks/alert.rb +10 -10
  24. data/lib/panda/editor/blocks/base.rb +1 -1
  25. data/lib/panda/editor/blocks/header.rb +2 -2
  26. data/lib/panda/editor/blocks/image.rb +11 -11
  27. data/lib/panda/editor/blocks/list.rb +25 -6
  28. data/lib/panda/editor/blocks/paragraph.rb +41 -10
  29. data/lib/panda/editor/blocks/quote.rb +6 -6
  30. data/lib/panda/editor/blocks/table.rb +6 -6
  31. data/lib/panda/editor/content.rb +11 -8
  32. data/lib/panda/editor/engine.rb +33 -7
  33. data/lib/panda/editor/footnote_registry.rb +10 -5
  34. data/lib/panda/editor/html_to_editor_js_converter.rb +159 -0
  35. data/lib/panda/editor/markdown_to_editor_js_converter.rb +50 -0
  36. data/lib/panda/editor/renderer.rb +31 -31
  37. data/lib/panda/editor/version.rb +1 -1
  38. data/lib/panda/editor.rb +19 -15
  39. data/lib/panda-editor.rb +5 -0
  40. data/lib/tasks/assets.rake +27 -27
  41. data/mise.toml +2 -0
  42. data/panda-editor.gemspec +25 -23
  43. metadata +49 -3
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+
5
+ module Panda
6
+ module Editor
7
+ # Converts HTML to EditorJS format
8
+ # Parses HTML and converts it to EditorJS blocks
9
+ class HtmlToEditorJsConverter
10
+ def self.convert(html)
11
+ new(html).convert
12
+ end
13
+
14
+ def initialize(html)
15
+ @html = html
16
+ @blocks = []
17
+ end
18
+
19
+ def convert
20
+ doc = Nokogiri::HTML.fragment(@html)
21
+
22
+ doc.children.each do |node|
23
+ block = node_to_block(node)
24
+ @blocks << block if block
25
+ end
26
+
27
+ {
28
+ time: Time.now.to_i * 1000,
29
+ blocks: @blocks,
30
+ version: "2.28.0"
31
+ }
32
+ end
33
+
34
+ private
35
+
36
+ def node_to_block(node)
37
+ return nil if node.text? && node.text.strip.empty?
38
+
39
+ case node.name
40
+ when "h1", "h2", "h3", "h4", "h5", "h6"
41
+ header_block(node)
42
+ when "p"
43
+ paragraph_block(node)
44
+ when "ul", "ol"
45
+ list_block(node)
46
+ when "blockquote"
47
+ quote_block(node)
48
+ when "pre"
49
+ code_block(node)
50
+ when "table"
51
+ table_block(node)
52
+ when "hr"
53
+ delimiter_block
54
+ when "text"
55
+ # Handle text nodes that aren't wrapped in tags
56
+ text = node.text.strip
57
+ text.empty? ? nil : paragraph_block_from_text(text)
58
+ else
59
+ # For any other node, try to extract text content
60
+ text = node.text.strip
61
+ text.empty? ? nil : paragraph_block_from_text(text)
62
+ end
63
+ end
64
+
65
+ def header_block(node)
66
+ level = node.name[1].to_i
67
+ {
68
+ type: "header",
69
+ data: {
70
+ text: node.inner_html.strip,
71
+ level: level
72
+ }
73
+ }
74
+ end
75
+
76
+ def paragraph_block(node)
77
+ text = node.inner_html.strip
78
+ return nil if text.empty?
79
+
80
+ {
81
+ type: "paragraph",
82
+ data: {
83
+ text: text
84
+ }
85
+ }
86
+ end
87
+
88
+ def paragraph_block_from_text(text)
89
+ {
90
+ type: "paragraph",
91
+ data: {
92
+ text: text
93
+ }
94
+ }
95
+ end
96
+
97
+ def list_block(node)
98
+ style = (node.name == "ol") ? "ordered" : "unordered"
99
+ items = node.css("li").map { |li| li.inner_html.strip }
100
+
101
+ {
102
+ type: "list",
103
+ data: {
104
+ style: style,
105
+ items: items
106
+ }
107
+ }
108
+ end
109
+
110
+ def quote_block(node)
111
+ {
112
+ type: "quote",
113
+ data: {
114
+ text: node.inner_html.strip,
115
+ caption: "",
116
+ alignment: "left"
117
+ }
118
+ }
119
+ end
120
+
121
+ def code_block(node)
122
+ code = node.css("code").first
123
+ text = code ? code.text : node.text
124
+
125
+ {
126
+ type: "code",
127
+ data: {
128
+ code: text
129
+ }
130
+ }
131
+ end
132
+
133
+ def table_block(node)
134
+ content = []
135
+
136
+ # Process table rows
137
+ node.css("tr").each do |row|
138
+ cells = row.css("th, td").map { |cell| cell.inner_html.strip }
139
+ content << cells
140
+ end
141
+
142
+ {
143
+ type: "table",
144
+ data: {
145
+ withHeadings: node.css("thead").any? || node.css("th").any?,
146
+ content: content
147
+ }
148
+ }
149
+ end
150
+
151
+ def delimiter_block
152
+ {
153
+ type: "delimiter",
154
+ data: {}
155
+ }
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "redcarpet"
4
+
5
+ module Panda
6
+ module Editor
7
+ # Converts Markdown to EditorJS format
8
+ # Uses Redcarpet to parse markdown to HTML, then converts HTML to EditorJS blocks
9
+ class MarkdownToEditorJsConverter
10
+ def self.convert(markdown)
11
+ new(markdown).convert
12
+ end
13
+
14
+ def initialize(markdown)
15
+ @markdown = markdown
16
+ end
17
+
18
+ def convert
19
+ # Step 1: Convert Markdown to HTML using Redcarpet
20
+ html = markdown_to_html
21
+
22
+ # Step 2: Convert HTML to EditorJS using existing converter
23
+ Panda::Editor::HtmlToEditorJsConverter.convert(html)
24
+ end
25
+
26
+ private
27
+
28
+ def markdown_to_html
29
+ renderer = Redcarpet::Render::HTML.new(
30
+ hard_wrap: true,
31
+ link_attributes: {rel: "noopener noreferrer"}
32
+ )
33
+
34
+ markdown_processor = Redcarpet::Markdown.new(
35
+ renderer,
36
+ autolink: true,
37
+ tables: true,
38
+ fenced_code_blocks: true,
39
+ strikethrough: true,
40
+ superscript: true,
41
+ footnotes: true,
42
+ no_intra_emphasis: true,
43
+ space_after_headers: true
44
+ )
45
+
46
+ markdown_processor.render(@markdown)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'sanitize'
3
+ require "sanitize"
4
4
 
5
5
  module Panda
6
6
  module Editor
@@ -20,10 +20,10 @@ module Panda
20
20
  end
21
21
 
22
22
  def render
23
- return '' if content.nil? || content == {}
24
- return content.to_s unless content.is_a?(Hash) && content['blocks'].is_a?(Array)
23
+ return "" if content.nil? || content == {}
24
+ return content.to_s unless content.is_a?(Hash) && content["blocks"].is_a?(Array)
25
25
 
26
- rendered = content['blocks'].map do |block|
26
+ rendered = content["blocks"].map do |block|
27
27
  render_block(block)
28
28
  end.join("\n")
29
29
 
@@ -35,36 +35,36 @@ module Panda
35
35
  rendered = [rendered, sources_section].join("\n")
36
36
  end
37
37
 
38
- rendered.presence || ''
38
+ rendered.presence || ""
39
39
  end
40
40
 
41
41
  def section(blocks)
42
- return '' if blocks.nil? || blocks.empty?
42
+ return "" if blocks.nil? || blocks.empty?
43
43
 
44
- content = { 'blocks' => blocks }
44
+ content = {"blocks" => blocks}
45
45
  rendered = self.class.new(content, options).render
46
46
 
47
47
  "<section class=\"content-section\">#{rendered}</section>"
48
48
  end
49
49
 
50
50
  def article(blocks, title: nil)
51
- return '' if blocks.nil? || blocks.empty?
51
+ return "" if blocks.nil? || blocks.empty?
52
52
 
53
- content = { 'blocks' => blocks }
53
+ content = {"blocks" => blocks}
54
54
  rendered = self.class.new(content, options).render
55
55
 
56
56
  [
57
- '<article>',
58
- (title ? "<h1>#{title}</h1>" : ''),
57
+ "<article>",
58
+ (title ? "<h1>#{title}</h1>" : ""),
59
59
  rendered,
60
- '</article>'
60
+ "</article>"
61
61
  ].join("\n")
62
62
  end
63
63
 
64
64
  private
65
65
 
66
66
  def validate_html(html)
67
- return '' if html.blank?
67
+ return "" if html.blank?
68
68
 
69
69
  begin
70
70
  # For quote blocks, only allow specific content
@@ -73,35 +73,35 @@ module Panda
73
73
  valid_content = '<figure class="text-left"><blockquote><p>Valid HTML</p></blockquote><figcaption>Valid caption</figcaption></figure>'
74
74
  return html if html.strip == valid_content.strip
75
75
 
76
- return ''
76
+ return ""
77
77
  end
78
78
 
79
79
  # For other HTML, use sanitize
80
80
  config = Sanitize::Config::RELAXED.dup
81
81
  config[:elements] += %w[figure figcaption blockquote pre code mention math]
82
82
  config[:attributes].merge!({
83
- 'figure' => ['class'],
84
- 'blockquote' => ['class'],
85
- 'p' => ['class'],
86
- 'figcaption' => ['class']
87
- })
83
+ "figure" => ["class"],
84
+ "blockquote" => ["class"],
85
+ "p" => ["class"],
86
+ "figcaption" => ["class"]
87
+ })
88
88
 
89
89
  sanitized = Sanitize.fragment(html, config)
90
- sanitized == html ? html : ''
91
- rescue StandardError => e
90
+ (sanitized == html) ? html : ""
91
+ rescue => e
92
92
  Rails.logger.error("HTML validation error: #{e.message}")
93
- ''
93
+ ""
94
94
  end
95
95
  end
96
96
 
97
97
  def render_block_with_cache(block)
98
98
  # Don't cache blocks with footnotes - they need to register with the footnote registry
99
- if block['data']['footnotes'].present?
99
+ if block["data"]["footnotes"].present?
100
100
  renderer = renderer_for(block)
101
101
  return renderer.render
102
102
  end
103
103
 
104
- cache_key = "editor_js_block/#{block['type']}/#{Digest::MD5.hexdigest(block['data'].to_json)}"
104
+ cache_key = "editor_js_block/#{block["type"]}/#{Digest::MD5.hexdigest(block["data"].to_json)}"
105
105
 
106
106
  cache_store.fetch(cache_key) do
107
107
  renderer = renderer_for(block)
@@ -110,28 +110,28 @@ module Panda
110
110
  end
111
111
 
112
112
  def renderer_for(block)
113
- if custom_renderers[block['type']]
114
- custom_renderers[block['type']].new(block['data'], options)
113
+ if custom_renderers[block["type"]]
114
+ custom_renderers[block["type"]].new(block["data"], options)
115
115
  else
116
116
  default_renderer_for(block)
117
117
  end
118
118
  end
119
119
 
120
120
  def default_renderer_for(block)
121
- renderer_class = "Panda::Editor::Blocks::#{block['type'].classify}".constantize
122
- renderer_class.new(block['data'], options)
121
+ renderer_class = "Panda::Editor::Blocks::#{block["type"].classify}".constantize
122
+ renderer_class.new(block["data"], options)
123
123
  rescue NameError
124
- Panda::Editor::Blocks::Base.new(block['data'], options)
124
+ Panda::Editor::Blocks::Base.new(block["data"], options)
125
125
  end
126
126
 
127
127
  def remove_empty_paragraphs(blocks)
128
128
  blocks.reject do |block|
129
- block['type'] == 'paragraph' && block['data']['text'].blank?
129
+ block["type"] == "paragraph" && block["data"]["text"].blank?
130
130
  end
131
131
  end
132
132
 
133
133
  def empty_paragraph?(block)
134
- block['type'] == 'paragraph' && block['data']['text'].blank?
134
+ block["type"] == "paragraph" && block["data"]["text"].blank?
135
135
  end
136
136
 
137
137
  def render_block(block)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Panda
4
4
  module Editor
5
- VERSION = '0.5.0'
5
+ VERSION = "0.8.2"
6
6
  end
7
7
  end
data/lib/panda/editor.rb CHANGED
@@ -1,13 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry-configurable'
4
- require_relative 'editor/version'
5
- require_relative 'editor/engine'
3
+ require "dry-configurable"
4
+ require_relative "editor/version"
5
+ require_relative "editor/engine"
6
6
 
7
7
  module Panda
8
8
  module Editor
9
9
  extend Dry::Configurable
10
10
 
11
+ mattr_accessor :importmap
12
+
11
13
  # EditorJS configuration
12
14
  setting :editor_js_tools, default: []
13
15
  setting :editor_js_tool_config, default: {}
@@ -17,20 +19,22 @@ module Panda
17
19
 
18
20
  class Error < StandardError; end
19
21
 
20
- # Autoload components
21
- autoload :Renderer, 'panda/editor/renderer'
22
- autoload :Content, 'panda/editor/content'
23
- autoload :FootnoteRegistry, 'panda/editor/footnote_registry'
22
+ # Require components
23
+ require_relative "editor/renderer"
24
+ require_relative "editor/content"
25
+ require_relative "editor/footnote_registry"
26
+ require_relative "editor/markdown_to_editor_js_converter"
27
+ require_relative "editor/html_to_editor_js_converter"
24
28
 
25
29
  module Blocks
26
- autoload :Base, 'panda/editor/blocks/base'
27
- autoload :Alert, 'panda/editor/blocks/alert'
28
- autoload :Header, 'panda/editor/blocks/header'
29
- autoload :Image, 'panda/editor/blocks/image'
30
- autoload :List, 'panda/editor/blocks/list'
31
- autoload :Paragraph, 'panda/editor/blocks/paragraph'
32
- autoload :Quote, 'panda/editor/blocks/quote'
33
- autoload :Table, 'panda/editor/blocks/table'
30
+ autoload :Base, "panda/editor/blocks/base"
31
+ autoload :Alert, "panda/editor/blocks/alert"
32
+ autoload :Header, "panda/editor/blocks/header"
33
+ autoload :Image, "panda/editor/blocks/image"
34
+ autoload :List, "panda/editor/blocks/list"
35
+ autoload :Paragraph, "panda/editor/blocks/paragraph"
36
+ autoload :Quote, "panda/editor/blocks/quote"
37
+ autoload :Table, "panda/editor/blocks/table"
34
38
  end
35
39
  end
36
40
  end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "panda/editor/version"
4
+ require "panda/editor/engine"
5
+ require "panda/editor"
@@ -2,14 +2,14 @@
2
2
 
3
3
  namespace :panda_editor do
4
4
  namespace :assets do
5
- desc 'Compile Panda Editor assets for production'
5
+ desc "Compile Panda Editor assets for production"
6
6
  task compile: :environment do
7
- require 'fileutils'
7
+ require "fileutils"
8
8
 
9
- puts 'Compiling Panda Editor assets...'
9
+ puts "Compiling Panda Editor assets..."
10
10
 
11
11
  # Create temporary directory for assets
12
- tmp_dir = Rails.root.join('tmp', 'panda_editor_assets')
12
+ tmp_dir = Rails.root.join("tmp", "panda_editor_assets")
13
13
  FileUtils.mkdir_p(tmp_dir)
14
14
 
15
15
  # Get version from gem
@@ -22,51 +22,51 @@ namespace :panda_editor do
22
22
  compile_css(tmp_dir, version)
23
23
 
24
24
  # Copy to public directory
25
- public_dir = Rails.root.join('public', 'panda-editor-assets')
25
+ public_dir = Rails.root.join("public", "panda-editor-assets")
26
26
  FileUtils.mkdir_p(public_dir)
27
- FileUtils.cp_r(Dir.glob(tmp_dir.join('*')), public_dir)
27
+ FileUtils.cp_r(Dir.glob(tmp_dir.join("*")), public_dir)
28
28
 
29
- puts '✅ Assets compiled successfully'
29
+ puts "✅ Assets compiled successfully"
30
30
  end
31
31
 
32
- desc 'Download Panda Editor assets from GitHub'
32
+ desc "Download Panda Editor assets from GitHub"
33
33
  task download: :environment do
34
- require 'panda/editor/asset_loader'
34
+ require "panda/editor/asset_loader"
35
35
 
36
- puts 'Downloading Panda Editor assets from GitHub...'
36
+ puts "Downloading Panda Editor assets from GitHub..."
37
37
  Panda::Editor::AssetLoader.send(:download_assets_from_github)
38
- puts '✅ Assets downloaded successfully'
38
+ puts "✅ Assets downloaded successfully"
39
39
  end
40
40
 
41
- desc 'Upload compiled assets to GitHub release'
41
+ desc "Upload compiled assets to GitHub release"
42
42
  task upload: :environment do
43
- require 'net/http'
44
- require 'json'
43
+ require "net/http"
44
+ require "json"
45
45
 
46
- puts 'Uploading Panda Editor assets to GitHub release...'
46
+ puts "Uploading Panda Editor assets to GitHub release..."
47
47
 
48
48
  # This task would be run in CI to upload compiled assets
49
49
  # to the GitHub release when a new version is tagged
50
50
 
51
- version = ENV['GITHUB_REF_NAME'] || "v#{Panda::Editor::VERSION}"
52
- token = ENV['GITHUB_TOKEN']
51
+ version = ENV["GITHUB_REF_NAME"] || "v#{Panda::Editor::VERSION}"
52
+ token = ENV["GITHUB_TOKEN"]
53
53
 
54
54
  unless token
55
- puts '❌ GITHUB_TOKEN environment variable required'
55
+ puts "❌ GITHUB_TOKEN environment variable required"
56
56
  exit 1
57
57
  end
58
58
 
59
59
  # Find compiled assets
60
- assets_dir = Rails.root.join('public', 'panda-editor-assets')
61
- js_file = Dir.glob(assets_dir.join('panda-editor-*.js')).first
62
- css_file = Dir.glob(assets_dir.join('panda-editor-*.css')).first
60
+ assets_dir = Rails.root.join("public", "panda-editor-assets")
61
+ js_file = Dir.glob(assets_dir.join("panda-editor-*.js")).first
62
+ css_file = Dir.glob(assets_dir.join("panda-editor-*.css")).first
63
63
 
64
64
  if js_file && css_file
65
65
  upload_to_release(js_file, version, token)
66
66
  upload_to_release(css_file, version, token)
67
- puts '✅ Assets uploaded successfully'
67
+ puts "✅ Assets uploaded successfully"
68
68
  else
69
- puts '❌ Compiled assets not found'
69
+ puts "❌ Compiled assets not found"
70
70
  exit 1
71
71
  end
72
72
  end
@@ -74,9 +74,9 @@ namespace :panda_editor do
74
74
  private
75
75
 
76
76
  def compile_javascript(tmp_dir, version)
77
- puts ' Compiling JavaScript...'
77
+ puts " Compiling JavaScript..."
78
78
 
79
- js_files = Dir.glob(Panda::Editor::Engine.root.join('app/javascript/panda/editor/**/*.js'))
79
+ js_files = Dir.glob(Panda::Editor::Engine.root.join("app/javascript/panda/editor/**/*.js"))
80
80
 
81
81
  output = js_files.map { |file| File.read(file) }.join("\n\n")
82
82
 
@@ -88,9 +88,9 @@ namespace :panda_editor do
88
88
  end
89
89
 
90
90
  def compile_css(tmp_dir, version)
91
- puts ' Compiling CSS...'
91
+ puts " Compiling CSS..."
92
92
 
93
- css_files = Dir.glob(Panda::Editor::Engine.root.join('app/assets/stylesheets/panda/editor/**/*.css'))
93
+ css_files = Dir.glob(Panda::Editor::Engine.root.join("app/assets/stylesheets/panda/editor/**/*.css"))
94
94
 
95
95
  output = css_files.map { |file| File.read(file) }.join("\n\n")
96
96
 
data/mise.toml ADDED
@@ -0,0 +1,2 @@
1
+ [tools]
2
+ ruby = "4.0.0"
data/panda-editor.gemspec CHANGED
@@ -1,43 +1,45 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'lib/panda/editor/version'
3
+ require_relative "lib/panda/editor/version"
4
4
 
5
5
  Gem::Specification.new do |spec|
6
- spec.name = 'panda-editor'
6
+ spec.name = "panda-editor"
7
7
  spec.version = Panda::Editor::VERSION
8
- spec.authors = ['Otaina Limited', 'James Inman']
9
- spec.email = ['james@otaina.co.uk']
8
+ spec.authors = ["Otaina Limited", "James Inman"]
9
+ spec.email = ["james@otaina.co.uk"]
10
10
 
11
- spec.summary = 'EditorJS integration for Rails applications'
12
- spec.description = 'A modular, extensible rich text editor using EditorJS for Rails applications. Extracted from Panda CMS.'
13
- spec.homepage = 'https://github.com/tastybamboo/panda-editor'
14
- spec.license = 'BSD-3-Clause'
15
- spec.required_ruby_version = '>= 3.2.0'
11
+ spec.summary = "EditorJS integration for Rails applications"
12
+ spec.description = "A modular, extensible rich text editor using EditorJS for Rails applications. Extracted from Panda CMS."
13
+ spec.homepage = "https://github.com/tastybamboo/panda-editor"
14
+ spec.license = "BSD-3-Clause"
15
+ spec.required_ruby_version = ">= 3.2.0"
16
16
 
17
- spec.metadata['allowed_push_host'] = 'https://rubygems.org'
18
- spec.metadata['homepage_uri'] = spec.homepage
19
- spec.metadata['source_code_uri'] = 'https://github.com/tastybamboo/panda-editor'
20
- spec.metadata['changelog_uri'] = 'https://github.com/tastybamboo/panda-editor/blob/main/CHANGELOG.md'
17
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
18
+ spec.metadata["homepage_uri"] = spec.homepage
19
+ spec.metadata["source_code_uri"] = "https://github.com/tastybamboo/panda-editor"
20
+ spec.metadata["changelog_uri"] = "https://github.com/tastybamboo/panda-editor/blob/main/CHANGELOG.md"
21
21
 
22
22
  spec.files = Dir.chdir(__dir__) do
23
23
  `git ls-files -z`.split("\x0").reject do |f|
24
24
  (File.expand_path(f) == __FILE__) ||
25
25
  f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile]) ||
26
- f.end_with?('.gem')
26
+ f.end_with?(".gem")
27
27
  end
28
28
  end
29
- spec.bindir = 'exe'
29
+ spec.bindir = "exe"
30
30
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
31
- spec.require_paths = ['lib']
31
+ spec.require_paths = ["lib"]
32
32
 
33
33
  # Rails dependencies
34
- spec.add_dependency 'dry-configurable', '~> 1.0'
35
- spec.add_dependency 'rails', '>= 7.1'
36
- spec.add_dependency 'redcarpet', '~> 3.6'
37
- spec.add_dependency 'sanitize', '~> 6.0'
34
+ spec.add_dependency "dry-configurable", "~> 1.0"
35
+ spec.add_dependency "nokogiri", "~> 1.15"
36
+ spec.add_dependency "panda-core" # Must load before panda-editor for ModuleRegistry
37
+ spec.add_dependency "rails", ">= 7.1"
38
+ spec.add_dependency "redcarpet", "~> 3.6"
39
+ spec.add_dependency "sanitize", "~> 6.0"
38
40
 
39
41
  # Development dependencies
40
- spec.add_development_dependency 'factory_bot_rails', '~> 6.2'
41
- spec.add_development_dependency 'rspec-rails', '~> 6.0'
42
- spec.add_development_dependency 'standard', '>= 1.35.1'
42
+ spec.add_development_dependency "factory_bot_rails", "~> 6.2"
43
+ spec.add_development_dependency "rspec-rails", "~> 6.0"
44
+ spec.add_development_dependency "standard", ">= 1.35.1"
43
45
  end