panda-editor 0.2.0 → 0.3.0

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.
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Editor
5
+ class FootnoteRegistry
6
+ attr_reader :footnotes
7
+
8
+ def initialize(autolink_urls: false)
9
+ @footnotes = []
10
+ @footnote_ids = {}
11
+ @autolink_urls = autolink_urls
12
+ end
13
+
14
+ def add(id:, content:)
15
+ # Return existing number if this ID was already registered
16
+ return @footnote_ids[id] if @footnote_ids[id]
17
+
18
+ # Add new footnote
19
+ @footnotes << {id: id, content: content}
20
+ number = @footnotes.length
21
+
22
+ # Cache the number for this ID
23
+ @footnote_ids[id] = number
24
+
25
+ number
26
+ end
27
+
28
+ def render_sources_section
29
+ return "" if @footnotes.empty?
30
+
31
+ footnote_items = @footnotes.map.with_index do |footnote, index|
32
+ number = index + 1
33
+ content = @autolink_urls ? autolink_urls(footnote[:content]) : footnote[:content]
34
+ <<~HTML.strip
35
+ <li id="fn:#{number}">
36
+ <p>
37
+ #{content}
38
+ <a href="#fnref:#{number}" class="footnote-backref">↩</a>
39
+ </p>
40
+ </li>
41
+ HTML
42
+ end.join("\n")
43
+
44
+ <<~HTML
45
+ <div class="mx-6 lg:mx-8 mt-4 mb-8">
46
+ <div class="footnotes-section bg-gray-50 rounded-lg overflow-hidden">
47
+ <button class="footnotes-header w-full px-4 py-3 flex items-center justify-between cursor-pointer hover:bg-gray-100 transition-colors" data-footnotes-target="toggle" data-action="click->footnotes#toggle">
48
+ <h3 class="text-sm font-unbounded font-medium text-gray-900 m-0">Sources/References</h3>
49
+ <svg class="footnotes-chevron w-5 h-5 text-gray-600" data-footnotes-target="chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
50
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
51
+ </svg>
52
+ </button>
53
+ <div class="footnotes-content" data-footnotes-target="content">
54
+ <ol class="footnotes text-sm text-gray-700 space-y-2 px-4 pb-3">
55
+ #{footnote_items}
56
+ </ol>
57
+ </div>
58
+ </div>
59
+ </div>
60
+ HTML
61
+ end
62
+
63
+ def any?
64
+ @footnotes.any?
65
+ end
66
+
67
+ private
68
+
69
+ def autolink_urls(text)
70
+ # Regex to match URLs that aren't already in <a> tags
71
+ # Matches http://, https://, and other common protocols
72
+ url_pattern = %r{
73
+ (?<!["'>]) # Not preceded by quotes or >
74
+ \b # Word boundary
75
+ (https?://|ftp://|www\.) # Protocol or www
76
+ [^\s<>"']+ # URL characters (not whitespace, <, >, quotes)
77
+ [^\s<>"'.,;:!?)\]] # Ends with non-punctuation
78
+ }x
79
+
80
+ # Don't replace URLs that are already in <a> tags
81
+ text.gsub(url_pattern) do |url|
82
+ # Skip if this URL is already part of an href attribute
83
+ before_match = $`
84
+ if /<a[^>]*href\s*=\s*["']?\z/i.match?(before_match)
85
+ url
86
+ else
87
+ # Add protocol if missing
88
+ full_url = url.start_with?("www.") ? "https://#{url}" : url
89
+ %(<a href="#{full_url}" target="_blank" rel="noopener noreferrer">#{url}</a>)
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -5,7 +5,7 @@ require "sanitize"
5
5
  module Panda
6
6
  module Editor
7
7
  class Renderer
8
- attr_reader :content, :options, :custom_renderers, :cache_store
8
+ attr_reader :content, :options, :custom_renderers, :cache_store, :footnote_registry
9
9
 
10
10
  def initialize(content, options = {})
11
11
  @content = content
@@ -13,6 +13,9 @@ module Panda
13
13
  @custom_renderers = options.delete(:custom_renderers) || {}
14
14
  @cache_store = options.delete(:cache_store) || Rails.cache
15
15
  @validate_html = options.delete(:validate_html) || false
16
+ autolink_urls = options.delete(:autolink_urls) || false
17
+ @footnote_registry = FootnoteRegistry.new(autolink_urls: autolink_urls)
18
+ @options[:footnote_registry] = @footnote_registry
16
19
  end
17
20
 
18
21
  def render
@@ -24,6 +27,13 @@ module Panda
24
27
  end.join("\n")
25
28
 
26
29
  rendered = @validate_html ? validate_html(rendered) : rendered
30
+
31
+ # Append sources section if footnotes were collected
32
+ if @footnote_registry.any?
33
+ sources_section = @footnote_registry.render_sources_section
34
+ rendered = [rendered, sources_section].join("\n")
35
+ end
36
+
27
37
  rendered.presence || ""
28
38
  end
29
39
 
@@ -84,6 +94,12 @@ module Panda
84
94
  end
85
95
 
86
96
  def render_block_with_cache(block)
97
+ # Don't cache blocks with footnotes - they need to register with the footnote registry
98
+ if block["data"]["footnotes"].present?
99
+ renderer = renderer_for(block)
100
+ return renderer.render
101
+ end
102
+
87
103
  cache_key = "editor_js_block/#{block["type"]}/#{Digest::MD5.hexdigest(block["data"].to_json)}"
88
104
 
89
105
  cache_store.fetch(cache_key) do
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Panda
4
4
  module Editor
5
- VERSION = "0.2.0"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end
data/lib/panda/editor.rb CHANGED
@@ -1,15 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "dry-configurable"
3
4
  require_relative "editor/version"
4
5
  require_relative "editor/engine"
5
6
 
6
7
  module Panda
7
8
  module Editor
9
+ extend Dry::Configurable
10
+
11
+ # EditorJS configuration
12
+ setting :editor_js_tools, default: []
13
+ setting :editor_js_tool_config, default: {}
14
+
15
+ # Custom block renderers
16
+ setting :custom_renderers, default: {}
17
+
8
18
  class Error < StandardError; end
9
19
 
10
20
  # Autoload components
11
21
  autoload :Renderer, "panda/editor/renderer"
12
22
  autoload :Content, "panda/editor/content"
23
+ autoload :FootnoteRegistry, "panda/editor/footnote_registry"
13
24
 
14
25
  module Blocks
15
26
  autoload :Base, "panda/editor/blocks/base"
data/panda-editor.gemspec CHANGED
@@ -5,8 +5,8 @@ require_relative "lib/panda/editor/version"
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = "panda-editor"
7
7
  spec.version = Panda::Editor::VERSION
8
- spec.authors = ["Panda CMS Team"]
9
- spec.email = ["hello@pandacms.io"]
8
+ spec.authors = ["Otaina Limited", "James Inman"]
9
+ spec.email = ["james@otaina.co.uk"]
10
10
 
11
11
  spec.summary = "EditorJS integration for Rails applications"
12
12
  spec.description = "A modular, extensible rich text editor using EditorJS for Rails applications. Extracted from Panda CMS."
@@ -33,6 +33,7 @@ Gem::Specification.new do |spec|
33
33
  # Rails dependencies
34
34
  spec.add_dependency "rails", ">= 7.1"
35
35
  spec.add_dependency "sanitize", "~> 6.0"
36
+ spec.add_dependency "dry-configurable", "~> 1.0"
36
37
 
37
38
  # Development dependencies
38
39
  spec.add_development_dependency "rspec-rails", "~> 6.0"