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.
- checksums.yaml +4 -4
- data/.ruby-version +1 -0
- data/app/javascript/panda/editor/application.js +8 -6
- data/app/javascript/panda/editor/controllers/index.js +5 -0
- data/app/javascript/panda/editor/editor_js_config.js +17 -7
- data/app/javascript/panda/editor/editor_js_initializer.js +10 -3
- data/app/javascript/panda/editor/encoding.js +42 -0
- data/app/javascript/panda/editor/plugins/embed.min.js +2 -0
- data/app/javascript/panda/editor/plugins/header.min.js +9 -0
- data/app/javascript/panda/editor/plugins/nested-list.min.js +2 -0
- data/app/javascript/panda/editor/plugins/paragraph.min.js +9 -0
- data/app/javascript/panda/editor/plugins/quote.min.js +2 -0
- data/app/javascript/panda/editor/plugins/simple-image.min.js +2 -0
- data/app/javascript/panda/editor/plugins/table.min.js +2 -0
- data/app/javascript/panda/editor/rich_text_editor.js +2 -3
- data/app/services/panda/editor/html_to_editor_js_converter.rb +68 -68
- data/app/stylesheets/editor.css +120 -0
- data/config/importmap.rb +23 -11
- data/docs/FOOTNOTES.md +96 -3
- data/lefthook.yml +16 -0
- data/lib/panda/editor/asset_loader.rb +27 -27
- data/lib/panda/editor/blocks/alert.rb +10 -10
- data/lib/panda/editor/blocks/base.rb +1 -1
- data/lib/panda/editor/blocks/header.rb +2 -2
- data/lib/panda/editor/blocks/image.rb +11 -11
- data/lib/panda/editor/blocks/list.rb +25 -6
- data/lib/panda/editor/blocks/paragraph.rb +41 -10
- data/lib/panda/editor/blocks/quote.rb +6 -6
- data/lib/panda/editor/blocks/table.rb +6 -6
- data/lib/panda/editor/content.rb +11 -8
- data/lib/panda/editor/engine.rb +29 -9
- data/lib/panda/editor/footnote_registry.rb +10 -5
- data/lib/panda/editor/html_to_editor_js_converter.rb +1 -1
- data/lib/panda/editor/renderer.rb +31 -31
- data/lib/panda/editor/version.rb +1 -1
- data/lib/panda/editor.rb +18 -16
- data/lib/tasks/assets.rake +27 -27
- data/mise.toml +2 -0
- data/panda-editor.gemspec +25 -24
- metadata +33 -3
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
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 =
|
|
10
|
-
ASSET_CACHE_DIR = Rails.root.join(
|
|
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
|
-
|
|
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
|
-
|
|
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[
|
|
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:
|
|
56
|
-
stylesheet:
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
83
|
-
css_exists = Dir.glob(Rails.root.join(
|
|
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
|
|
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[
|
|
93
|
-
rescue
|
|
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 ==
|
|
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[
|
|
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[
|
|
117
|
+
uri = URI(asset["browser_download_url"])
|
|
118
118
|
response = Net::HTTP.get_response(uri)
|
|
119
119
|
|
|
120
|
-
return unless response.code ==
|
|
120
|
+
return unless response.code == "200"
|
|
121
121
|
|
|
122
|
-
save_asset(asset[
|
|
122
|
+
save_asset(asset["name"], response.body)
|
|
123
123
|
end
|
|
124
124
|
|
|
125
125
|
def save_asset(filename, content)
|
|
126
|
-
dir = Rails.root.join(
|
|
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
|
|
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(
|
|
141
|
-
dest_dir = Rails.root.join(
|
|
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(
|
|
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[
|
|
9
|
-
type = data[
|
|
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
|
-
|
|
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
|
|
23
|
-
when
|
|
24
|
-
when
|
|
25
|
-
when
|
|
26
|
-
when
|
|
27
|
-
when
|
|
28
|
-
else
|
|
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
|
|
@@ -5,19 +5,19 @@ module Panda
|
|
|
5
5
|
module Blocks
|
|
6
6
|
class Image < Base
|
|
7
7
|
def render
|
|
8
|
-
url = data[
|
|
9
|
-
caption = sanitize(data[
|
|
10
|
-
with_border = data[
|
|
11
|
-
with_background = data[
|
|
12
|
-
stretched = data[
|
|
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 = [
|
|
15
|
-
css_classes <<
|
|
16
|
-
css_classes <<
|
|
17
|
-
css_classes <<
|
|
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
|
|
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
|
-
|
|
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[
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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[
|
|
9
|
-
return
|
|
10
|
+
content = sanitize(data["text"])
|
|
11
|
+
return "" if content.blank?
|
|
10
12
|
|
|
11
|
-
content = inject_footnotes(content) if data[
|
|
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[
|
|
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[
|
|
24
|
+
footnotes = data["footnotes"].sort_by { |fn| -fn["position"].to_i }
|
|
23
25
|
|
|
24
26
|
footnotes.each do |footnote|
|
|
25
|
-
position = footnote[
|
|
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
|
-
#
|
|
34
|
-
|
|
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[
|
|
48
|
-
content: footnote[
|
|
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[
|
|
9
|
-
caption = data[
|
|
10
|
-
alignment = data[
|
|
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
|
-
|
|
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?(
|
|
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
|
|
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[
|
|
9
|
-
with_headings = data[
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
data/lib/panda/editor/content.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
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 = {
|
|
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[
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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[
|
|
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
|
data/lib/panda/editor/engine.rb
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
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
|
|
18
|
-
require
|
|
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
|
|
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(
|
|
25
|
-
app.config.assets.paths << root.join(
|
|
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
|
-
|
|
30
|
-
|
|
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
|
|
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 << {
|
|
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
|
|
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: {
|
|
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?(
|
|
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
|