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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +101 -0
- data/app/javascript/panda/editor/application.js +8 -0
- data/app/javascript/panda/editor/editor_js_config.js +28 -1
- data/app/javascript/panda/editor/editor_js_initializer.js +4 -1
- data/app/javascript/panda/editor/rich_text_editor.js +6 -1
- data/app/javascript/panda/editor/tools/footnote_tool.js +392 -0
- data/app/javascript/panda/editor/tools/paragraph_with_footnotes.js +280 -0
- data/docs/FOOTNOTES.md +591 -0
- data/lib/panda/editor/blocks/paragraph.rb +38 -0
- data/lib/panda/editor/content.rb +4 -2
- data/lib/panda/editor/engine.rb +2 -6
- data/lib/panda/editor/footnote_registry.rb +95 -0
- data/lib/panda/editor/renderer.rb +17 -1
- data/lib/panda/editor/version.rb +1 -1
- data/lib/panda/editor.rb +11 -0
- data/panda-editor.gemspec +3 -2
- data/test_footnotes_standalone.html +957 -0
- metadata +28 -5
|
@@ -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
|
data/lib/panda/editor/version.rb
CHANGED
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 = ["
|
|
9
|
-
spec.email = ["
|
|
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"
|