panda-editor 0.6.0 → 0.8.3

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -0
  3. data/app/javascript/panda/editor/application.js +8 -6
  4. data/app/javascript/panda/editor/controllers/index.js +5 -0
  5. data/app/javascript/panda/editor/editor_js_config.js +17 -7
  6. data/app/javascript/panda/editor/editor_js_initializer.js +10 -3
  7. data/app/javascript/panda/editor/encoding.js +42 -0
  8. data/app/javascript/panda/editor/plugins/embed.min.js +2 -0
  9. data/app/javascript/panda/editor/plugins/header.min.js +9 -0
  10. data/app/javascript/panda/editor/plugins/nested-list.min.js +2 -0
  11. data/app/javascript/panda/editor/plugins/paragraph.min.js +9 -0
  12. data/app/javascript/panda/editor/plugins/quote.min.js +2 -0
  13. data/app/javascript/panda/editor/plugins/simple-image.min.js +2 -0
  14. data/app/javascript/panda/editor/plugins/table.min.js +2 -0
  15. data/app/javascript/panda/editor/rich_text_editor.js +2 -3
  16. data/app/services/panda/editor/html_to_editor_js_converter.rb +68 -68
  17. data/app/stylesheets/editor.css +120 -0
  18. data/config/importmap.rb +23 -11
  19. data/docs/FOOTNOTES.md +96 -3
  20. data/lefthook.yml +16 -0
  21. data/lib/panda/editor/asset_loader.rb +27 -27
  22. data/lib/panda/editor/blocks/alert.rb +10 -10
  23. data/lib/panda/editor/blocks/base.rb +1 -1
  24. data/lib/panda/editor/blocks/header.rb +2 -2
  25. data/lib/panda/editor/blocks/image.rb +11 -11
  26. data/lib/panda/editor/blocks/list.rb +25 -6
  27. data/lib/panda/editor/blocks/paragraph.rb +41 -10
  28. data/lib/panda/editor/blocks/quote.rb +6 -6
  29. data/lib/panda/editor/blocks/table.rb +6 -6
  30. data/lib/panda/editor/content.rb +11 -8
  31. data/lib/panda/editor/engine.rb +29 -9
  32. data/lib/panda/editor/footnote_registry.rb +10 -5
  33. data/lib/panda/editor/html_to_editor_js_converter.rb +1 -1
  34. data/lib/panda/editor/renderer.rb +31 -31
  35. data/lib/panda/editor/version.rb +1 -1
  36. data/lib/panda/editor.rb +18 -16
  37. data/lib/tasks/assets.rake +27 -27
  38. data/mise.toml +2 -0
  39. data/panda-editor.gemspec +25 -24
  40. metadata +33 -3
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'net/http'
4
- require 'json'
3
+ require "net/http"
4
+ require "json"
5
5
 
6
6
  module Panda
7
7
  module Editor
8
8
  class AssetLoader
9
- GITHUB_RELEASES_URL = 'https://api.github.com/repos/tastybamboo/panda-editor/releases/latest'
10
- ASSET_CACHE_DIR = Rails.root.join('tmp', 'panda_editor_assets')
9
+ GITHUB_RELEASES_URL = "https://api.github.com/repos/tastybamboo/panda-editor/releases/latest"
10
+ ASSET_CACHE_DIR = Rails.root.join("tmp", "panda_editor_assets")
11
11
 
12
12
  class << self
13
13
  def load_assets
@@ -22,7 +22,7 @@ module Panda
22
22
  if use_compiled_assets?
23
23
  compiled_javascript_url
24
24
  else
25
- '/assets/panda/editor/application.js'
25
+ "/assets/panda/editor/application.js"
26
26
  end
27
27
  end
28
28
 
@@ -30,7 +30,7 @@ module Panda
30
30
  if use_compiled_assets?
31
31
  compiled_stylesheet_url
32
32
  else
33
- '/assets/panda/editor/application.css'
33
+ "/assets/panda/editor/application.css"
34
34
  end
35
35
  end
36
36
 
@@ -39,7 +39,7 @@ module Panda
39
39
  def use_compiled_assets?
40
40
  Rails.env.production? ||
41
41
  Rails.env.test? ||
42
- ENV['PANDA_EDITOR_USE_COMPILED_ASSETS'] == 'true'
42
+ ENV["PANDA_EDITOR_USE_COMPILED_ASSETS"] == "true"
43
43
  end
44
44
 
45
45
  def load_compiled_assets
@@ -52,23 +52,23 @@ module Panda
52
52
 
53
53
  def load_development_assets
54
54
  {
55
- javascript: '/assets/panda/editor/application.js',
56
- stylesheet: '/assets/panda/editor/application.css'
55
+ javascript: "/assets/panda/editor/application.js",
56
+ stylesheet: "/assets/panda/editor/application.css"
57
57
  }
58
58
  end
59
59
 
60
60
  def compiled_javascript_url
61
- asset_path = find_latest_asset('js')
61
+ asset_path = find_latest_asset("js")
62
62
  asset_path ? "/panda-editor-assets/#{File.basename(asset_path)}" : nil
63
63
  end
64
64
 
65
65
  def compiled_stylesheet_url
66
- asset_path = find_latest_asset('css')
66
+ asset_path = find_latest_asset("css")
67
67
  asset_path ? "/panda-editor-assets/#{File.basename(asset_path)}" : nil
68
68
  end
69
69
 
70
70
  def find_latest_asset(extension)
71
- pattern = Rails.root.join('public', 'panda-editor-assets', "panda-editor-*.#{extension}")
71
+ pattern = Rails.root.join("public", "panda-editor-assets", "panda-editor-*.#{extension}")
72
72
  Dir.glob(pattern).max_by { |f| File.mtime(f) }
73
73
  end
74
74
 
@@ -79,18 +79,18 @@ module Panda
79
79
  end
80
80
 
81
81
  def assets_exist?
82
- js_exists = Dir.glob(Rails.root.join('public', 'panda-editor-assets', 'panda-editor-*.js')).any?
83
- css_exists = Dir.glob(Rails.root.join('public', 'panda-editor-assets', 'panda-editor-*.css')).any?
82
+ js_exists = Dir.glob(Rails.root.join("public", "panda-editor-assets", "panda-editor-*.js")).any?
83
+ css_exists = Dir.glob(Rails.root.join("public", "panda-editor-assets", "panda-editor-*.css")).any?
84
84
  js_exists && css_exists
85
85
  end
86
86
 
87
87
  def download_assets_from_github
88
- Rails.logger.info '[Panda Editor] Downloading assets from GitHub releases...'
88
+ Rails.logger.info "[Panda Editor] Downloading assets from GitHub releases..."
89
89
 
90
90
  begin
91
91
  release_data = fetch_latest_release
92
- download_release_assets(release_data['assets'])
93
- rescue StandardError => e
92
+ download_release_assets(release_data["assets"])
93
+ rescue => e
94
94
  Rails.logger.error "[Panda Editor] Failed to download assets: #{e.message}"
95
95
  use_fallback_assets
96
96
  end
@@ -100,30 +100,30 @@ module Panda
100
100
  uri = URI(GITHUB_RELEASES_URL)
101
101
  response = Net::HTTP.get_response(uri)
102
102
 
103
- raise "GitHub API returned #{response.code}" unless response.code == '200'
103
+ raise "GitHub API returned #{response.code}" unless response.code == "200"
104
104
 
105
105
  JSON.parse(response.body)
106
106
  end
107
107
 
108
108
  def download_release_assets(assets)
109
109
  assets.each do |asset|
110
- next unless asset['name'].match?(/panda-editor.*\.(js|css)$/)
110
+ next unless asset["name"].match?(/panda-editor.*\.(js|css)$/)
111
111
 
112
112
  download_asset(asset)
113
113
  end
114
114
  end
115
115
 
116
116
  def download_asset(asset)
117
- uri = URI(asset['browser_download_url'])
117
+ uri = URI(asset["browser_download_url"])
118
118
  response = Net::HTTP.get_response(uri)
119
119
 
120
- return unless response.code == '200'
120
+ return unless response.code == "200"
121
121
 
122
- save_asset(asset['name'], response.body)
122
+ save_asset(asset["name"], response.body)
123
123
  end
124
124
 
125
125
  def save_asset(filename, content)
126
- dir = Rails.root.join('public', 'panda-editor-assets')
126
+ dir = Rails.root.join("public", "panda-editor-assets")
127
127
  FileUtils.mkdir_p(dir)
128
128
 
129
129
  File.write(dir.join(filename), content)
@@ -131,17 +131,17 @@ module Panda
131
131
  end
132
132
 
133
133
  def use_fallback_assets
134
- Rails.logger.warn '[Panda Editor] Using fallback embedded assets'
134
+ Rails.logger.warn "[Panda Editor] Using fallback embedded assets"
135
135
  # Copy embedded assets from gem to public directory
136
136
  copy_embedded_assets
137
137
  end
138
138
 
139
139
  def copy_embedded_assets
140
- source_dir = Panda::Editor::Engine.root.join('public', 'panda-editor-assets')
141
- dest_dir = Rails.root.join('public', 'panda-editor-assets')
140
+ source_dir = Panda::Editor::Engine.root.join("public", "panda-editor-assets")
141
+ dest_dir = Rails.root.join("public", "panda-editor-assets")
142
142
 
143
143
  FileUtils.mkdir_p(dest_dir)
144
- FileUtils.cp_r(Dir.glob(source_dir.join('*')), dest_dir)
144
+ FileUtils.cp_r(Dir.glob(source_dir.join("*")), dest_dir)
145
145
  end
146
146
  end
147
147
  end
@@ -5,13 +5,13 @@ module Panda
5
5
  module Blocks
6
6
  class Alert < Base
7
7
  def render
8
- message = sanitize(data['message'])
9
- type = data['type'] || 'primary'
8
+ message = sanitize(data["message"])
9
+ type = data["type"] || "primary"
10
10
 
11
11
  html_safe(
12
12
  "<div class=\"#{alert_classes(type)} p-4 mb-4 rounded-lg\">" \
13
13
  "#{message}" \
14
- '</div>'
14
+ "</div>"
15
15
  )
16
16
  end
17
17
 
@@ -19,13 +19,13 @@ module Panda
19
19
 
20
20
  def alert_classes(type)
21
21
  case type
22
- when 'primary' then 'bg-blue-100 text-blue-800'
23
- when 'secondary' then 'bg-gray-100 text-gray-800'
24
- when 'success' then 'bg-green-100 text-green-800'
25
- when 'danger' then 'bg-red-100 text-red-800'
26
- when 'warning' then 'bg-yellow-100 text-yellow-800'
27
- when 'info' then 'bg-indigo-100 text-indigo-800'
28
- else 'bg-blue-100 text-blue-800'
22
+ when "primary" then "bg-blue-100 text-blue-800"
23
+ when "secondary" then "bg-gray-100 text-gray-800"
24
+ when "success" then "bg-green-100 text-green-800"
25
+ when "danger" then "bg-red-100 text-red-800"
26
+ when "warning" then "bg-yellow-100 text-yellow-800"
27
+ when "info" then "bg-sky-100 text-sky-800"
28
+ else "bg-blue-100 text-blue-800"
29
29
  end
30
30
  end
31
31
  end
@@ -15,7 +15,7 @@ module Panda
15
15
  end
16
16
 
17
17
  def render
18
- ''
18
+ ""
19
19
  end
20
20
 
21
21
  protected
@@ -5,8 +5,8 @@ module Panda
5
5
  module Blocks
6
6
  class Header < Base
7
7
  def render
8
- content = sanitize(data['text'])
9
- level = data['level'] || 2
8
+ content = sanitize(data["text"])
9
+ level = data["level"] || 2
10
10
  html_safe("<h#{level}>#{content}</h#{level}>")
11
11
  end
12
12
  end
@@ -5,19 +5,19 @@ module Panda
5
5
  module Blocks
6
6
  class Image < Base
7
7
  def render
8
- url = data['url']
9
- caption = sanitize(data['caption'])
10
- with_border = data['withBorder']
11
- with_background = data['withBackground']
12
- stretched = data['stretched']
8
+ url = data["url"]
9
+ caption = sanitize(data["caption"])
10
+ with_border = data["withBorder"]
11
+ with_background = data["withBackground"]
12
+ stretched = data["stretched"]
13
13
 
14
- css_classes = ['prose']
15
- css_classes << 'border' if with_border
16
- css_classes << 'bg-gray-100' if with_background
17
- css_classes << 'w-full' if stretched
14
+ css_classes = ["prose"]
15
+ css_classes << "border" if with_border
16
+ css_classes << "bg-gray-100" if with_background
17
+ css_classes << "w-full" if stretched
18
18
 
19
19
  html_safe(<<~HTML)
20
- <figure class="#{css_classes.join(' ')}">
20
+ <figure class="#{css_classes.join(" ")}">
21
21
  <img src="#{url}" alt="#{caption}" />
22
22
  #{caption_element(caption)}
23
23
  </figure>
@@ -27,7 +27,7 @@ module Panda
27
27
  private
28
28
 
29
29
  def caption_element(caption)
30
- return '' if caption.blank?
30
+ return "" if caption.blank?
31
31
 
32
32
  "<figcaption>#{caption}</figcaption>"
33
33
  end
@@ -5,10 +5,11 @@ module Panda
5
5
  module Blocks
6
6
  class List < Base
7
7
  def render
8
- list_type = data['style'] == 'ordered' ? 'ol' : 'ul'
8
+ style = data["style"] || data[:style]
9
+ list_type = (style == "ordered") ? "ol" : "ul"
9
10
  html_safe(
10
11
  "<#{list_type}>" \
11
- "#{render_items(data['items'])}" \
12
+ "#{render_items(data["items"] || data[:items] || [])}" \
12
13
  "</#{list_type}>"
13
14
  )
14
15
  end
@@ -16,15 +17,33 @@ module Panda
16
17
  private
17
18
 
18
19
  def render_items(items)
20
+ return "" unless items.is_a?(Array)
21
+
19
22
  items.map do |item|
20
- content = item.is_a?(Hash) ? item['content'] : item
21
- nested = item.is_a?(Hash) && item['items'].present? ? render_nested(item['items']) : ''
22
- "<li>#{sanitize(content)}#{nested}</li>"
23
+ content = extract_content(item)
24
+ nested_items = extract_nested_items(item)
25
+ nested = nested_items.present? ? render_nested(nested_items) : ""
26
+ "<li>#{sanitize(content.to_s)}#{nested}</li>"
23
27
  end.join
24
28
  end
25
29
 
30
+ def extract_content(item)
31
+ return item unless item.is_a?(Hash)
32
+
33
+ # Handle both string and symbol keys
34
+ item["content"] || item[:content] || ""
35
+ end
36
+
37
+ def extract_nested_items(item)
38
+ return [] unless item.is_a?(Hash)
39
+
40
+ # Handle both string and symbol keys
41
+ item["items"] || item[:items] || []
42
+ end
43
+
26
44
  def render_nested(items)
27
- self.class.new({ 'items' => items, 'style' => data['style'] }).render
45
+ style = data["style"] || data[:style]
46
+ self.class.new({"items" => items, "style" => style}).render
28
47
  end
29
48
  end
30
49
  end
@@ -1,14 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "cgi"
4
+
3
5
  module Panda
4
6
  module Editor
5
7
  module Blocks
6
8
  class Paragraph < Base
7
9
  def render
8
- content = sanitize(data['text'])
9
- return '' if content.blank?
10
+ content = sanitize(data["text"])
11
+ return "" if content.blank?
10
12
 
11
- content = inject_footnotes(content) if data['footnotes'].present?
13
+ content = inject_footnotes(content) if data["footnotes"].present?
12
14
 
13
15
  html_safe("<p>#{content}</p>")
14
16
  end
@@ -16,13 +18,13 @@ module Panda
16
18
  private
17
19
 
18
20
  def inject_footnotes(text)
19
- return text unless data['footnotes'].is_a?(Array)
21
+ return text unless data["footnotes"].is_a?(Array)
20
22
 
21
23
  # Sort footnotes by position in descending order to avoid position shifts
22
- footnotes = data['footnotes'].sort_by { |fn| -fn['position'].to_i }
24
+ footnotes = data["footnotes"].sort_by { |fn| -fn["position"].to_i }
23
25
 
24
26
  footnotes.each do |footnote|
25
- position = footnote['position'].to_i
27
+ position = footnote["position"].to_i
26
28
  # Skip if position is beyond text length
27
29
  next if position.negative? || position > text.length
28
30
 
@@ -30,8 +32,11 @@ module Panda
30
32
  footnote_number = register_footnote(footnote)
31
33
  next unless footnote_number
32
34
 
33
- # Create footnote marker
34
- marker = "<sup id=\"fnref:#{footnote_number}\"><a href=\"#fn:#{footnote_number}\" class=\"footnote\">#{footnote_number}</a></sup>"
35
+ # Get processed content for tooltip
36
+ tooltip_content = get_tooltip_content(footnote["id"])
37
+
38
+ # Create footnote marker with tooltip support
39
+ marker = create_footnote_marker(footnote_number, tooltip_content)
35
40
 
36
41
  # Insert marker at position
37
42
  text.insert(position, marker)
@@ -44,10 +49,36 @@ module Panda
44
49
  return nil unless options[:footnote_registry]
45
50
 
46
51
  options[:footnote_registry].add(
47
- id: footnote['id'],
48
- content: footnote['content']
52
+ id: footnote["id"],
53
+ content: footnote["content"]
49
54
  )
50
55
  end
56
+
57
+ def get_tooltip_content(footnote_id)
58
+ return nil unless options[:footnote_registry]
59
+
60
+ options[:footnote_registry].get_content(footnote_id)
61
+ end
62
+
63
+ def create_footnote_marker(number, tooltip_content)
64
+ # Strip HTML tags for title attribute (simple tooltip fallback)
65
+ plain_content = tooltip_content ? strip_html(tooltip_content) : nil
66
+
67
+ # Build marker with tooltip support
68
+ if tooltip_content
69
+ # Include both title attribute (native browser tooltip) and data attribute (for custom tooltips)
70
+ escaped_content = CGI.escapeHTML(tooltip_content)
71
+ escaped_title = CGI.escapeHTML(plain_content || "")
72
+ %(<sup id="fnref:#{number}" class="footnote-ref" data-footnote-content="#{escaped_content}" title="#{escaped_title}"><a href="#fn:#{number}" class="footnote">#{number}</a></sup>)
73
+ else
74
+ # Fallback without tooltip
75
+ %(<sup id="fnref:#{number}"><a href="#fn:#{number}" class="footnote">#{number}</a></sup>)
76
+ end
77
+ end
78
+
79
+ def strip_html(html)
80
+ html.gsub(/<\/?[^>]*>/, "")
81
+ end
51
82
  end
52
83
  end
53
84
  end
@@ -5,15 +5,15 @@ module Panda
5
5
  module Blocks
6
6
  class Quote < Base
7
7
  def render
8
- text = data['text']
9
- caption = data['caption']
10
- alignment = data['alignment'] || 'left'
8
+ text = data["text"]
9
+ caption = data["caption"]
10
+ alignment = data["alignment"] || "left"
11
11
 
12
12
  # Build the HTML structure
13
13
  html = "<figure class=\"text-#{alignment}\">" \
14
14
  "<blockquote>#{wrap_text_in_p(text)}</blockquote>" \
15
15
  "#{caption_element(caption)}" \
16
- '</figure>'
16
+ "</figure>"
17
17
 
18
18
  # Return raw HTML - validation will be handled by the main renderer if enabled
19
19
  html_safe(html)
@@ -24,7 +24,7 @@ module Panda
24
24
  def wrap_text_in_p(text)
25
25
  # Only wrap in <p> if it's not already wrapped
26
26
  text = sanitize(text)
27
- if text.start_with?('<p>') && text.end_with?('</p>')
27
+ if text.start_with?("<p>") && text.end_with?("</p>")
28
28
  text
29
29
  else
30
30
  "<p>#{text}</p>"
@@ -32,7 +32,7 @@ module Panda
32
32
  end
33
33
 
34
34
  def caption_element(caption)
35
- return '' if caption.blank?
35
+ return "" if caption.blank?
36
36
 
37
37
  "<figcaption>#{sanitize(caption)}</figcaption>"
38
38
  end
@@ -5,8 +5,8 @@ module Panda
5
5
  module Blocks
6
6
  class Table < Base
7
7
  def render
8
- content = data['content']
9
- with_headings = data['withHeadings']
8
+ content = data["content"]
9
+ with_headings = data["withHeadings"]
10
10
 
11
11
  html_safe(<<~HTML)
12
12
  <div class="overflow-x-auto">
@@ -25,10 +25,10 @@ module Panda
25
25
 
26
26
  while index < content.length
27
27
  rows << if index.zero? && with_headings
28
- render_header_row(content[index])
29
- else
30
- render_data_row(content[index])
31
- end
28
+ render_header_row(content[index])
29
+ else
30
+ render_data_row(content[index])
31
+ end
32
32
  index += 1
33
33
  end
34
34
 
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
3
+ require "json"
4
4
 
5
5
  module Panda
6
6
  module Editor
@@ -36,21 +36,24 @@ module Panda
36
36
  end
37
37
 
38
38
  def generate_cached_content
39
- renderer_options = { autolink_urls: true }
39
+ renderer_options = {
40
+ autolink_urls: true,
41
+ custom_renderers: Panda::Editor.config.custom_renderers
42
+ }
40
43
 
41
44
  if content.is_a?(String)
42
45
  begin
43
46
  parsed_content = JSON.parse(content)
44
- self.cached_content = if parsed_content.is_a?(Hash) && parsed_content['blocks'].present?
45
- Panda::Editor::Renderer.new(parsed_content, renderer_options).render
46
- else
47
- content
48
- end
47
+ self.cached_content = if parsed_content.is_a?(Hash) && parsed_content["blocks"].present?
48
+ Panda::Editor::Renderer.new(parsed_content, renderer_options).render
49
+ else
50
+ content
51
+ end
49
52
  rescue JSON::ParserError
50
53
  # If it's not JSON, treat it as plain text
51
54
  self.cached_content = content
52
55
  end
53
- elsif content.is_a?(Hash) && content['blocks'].present?
56
+ elsif content.is_a?(Hash) && content["blocks"].present?
54
57
  # Process EditorJS content
55
58
  self.cached_content = Panda::Editor::Renderer.new(content, renderer_options).render
56
59
  else
@@ -1,7 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rails'
4
- require 'sanitize'
3
+ require "rails"
4
+ require "sanitize"
5
+
6
+ # Ensure panda-core is loaded first (provides ModuleRegistry)
7
+ require "panda/core"
8
+ require "panda/core/engine" if defined?(Rails)
5
9
 
6
10
  module Panda
7
11
  module Editor
@@ -14,21 +18,37 @@ module Panda
14
18
 
15
19
  # Eager load converter classes
16
20
  config.to_prepare do
17
- require 'panda/editor/markdown_to_editor_js_converter'
18
- require 'panda/editor/html_to_editor_js_converter'
21
+ require "panda/editor/markdown_to_editor_js_converter"
22
+ require "panda/editor/html_to_editor_js_converter"
19
23
  end
20
24
 
21
- initializer 'panda_editor.assets' do |app|
25
+ initializer "panda_editor.assets" do |app|
22
26
  next unless app.config.respond_to?(:assets)
23
27
 
24
- app.config.assets.paths << root.join('app/javascript')
25
- app.config.assets.paths << root.join('public')
28
+ app.config.assets.paths << root.join("app/javascript")
29
+ app.config.assets.paths << root.join("public")
26
30
  app.config.assets.precompile += %w[panda/editor/*.js panda/editor/*.css]
27
31
  end
28
32
 
29
- initializer 'panda_editor.importmap', before: 'importmap' do |app|
30
- app.config.importmap.paths << root.join('config/importmap.rb') if app.config.respond_to?(:importmap)
33
+ # Create a separate importmap for panda-editor
34
+ # This keeps the engine's JavaScript separate from the app's importmap
35
+ # Admin uses panda_core_javascript helper which reads from ModuleRegistry
36
+ initializer "panda_editor.importmap", before: "importmap" do |app|
37
+ Panda::Editor.importmap = Importmap::Map.new.tap do |map|
38
+ map.draw(Panda::Editor::Engine.root.join("config/importmap.rb"))
39
+ end
31
40
  end
32
41
  end
33
42
  end
34
43
  end
44
+
45
+ # Register with ModuleRegistry so admin can access the importmap
46
+ Panda::Core::ModuleRegistry.register(
47
+ gem_name: "panda-editor",
48
+ engine: "Panda::Editor::Engine",
49
+ paths: {
50
+ views: "app/views/panda/editor/**/*.erb",
51
+ components: "app/components/panda/editor/**/*.{rb,erb,js}",
52
+ javascripts: "app/javascript/panda/editor/**/*.js"
53
+ }
54
+ )
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'redcarpet'
3
+ require "redcarpet"
4
4
 
5
5
  module Panda
6
6
  module Editor
@@ -19,7 +19,7 @@ module Panda
19
19
  return @footnote_ids[id] if @footnote_ids[id]
20
20
 
21
21
  # Add new footnote
22
- @footnotes << { id: id, content: content }
22
+ @footnotes << {id: id, content: content}
23
23
  number = @footnotes.length
24
24
 
25
25
  # Cache the number for this ID
@@ -29,7 +29,7 @@ module Panda
29
29
  end
30
30
 
31
31
  def render_sources_section
32
- return '' if @footnotes.empty?
32
+ return "" if @footnotes.empty?
33
33
 
34
34
  footnote_items = @footnotes.map.with_index do |footnote, index|
35
35
  number = index + 1
@@ -67,6 +67,11 @@ module Panda
67
67
  @footnotes.any?
68
68
  end
69
69
 
70
+ def get_content(id)
71
+ footnote = @footnotes.find { |fn| fn[:id] == id }
72
+ footnote ? process_content(footnote[:content]) : nil
73
+ end
74
+
70
75
  private
71
76
 
72
77
  def process_content(content)
@@ -88,7 +93,7 @@ module Panda
88
93
  no_images: true,
89
94
  no_styles: true,
90
95
  safe_links_only: true,
91
- link_attributes: { target: '_blank', rel: 'noopener noreferrer' }
96
+ link_attributes: {target: "_blank", rel: "noopener noreferrer"}
92
97
  )
93
98
 
94
99
  markdown = Redcarpet::Markdown.new(
@@ -127,7 +132,7 @@ module Panda
127
132
  url
128
133
  else
129
134
  # Add protocol if missing
130
- full_url = url.start_with?('www.') ? "https://#{url}" : url
135
+ full_url = url.start_with?("www.") ? "https://#{url}" : url
131
136
  %(<a href="#{full_url}" target="_blank" rel="noopener noreferrer">#{url}</a>)
132
137
  end
133
138
  end
@@ -95,7 +95,7 @@ module Panda
95
95
  end
96
96
 
97
97
  def list_block(node)
98
- style = node.name == "ol" ? "ordered" : "unordered"
98
+ style = (node.name == "ol") ? "ordered" : "unordered"
99
99
  items = node.css("li").map { |li| li.inner_html.strip }
100
100
 
101
101
  {