panda-editor 0.6.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.
- 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 +7 -5
- data/app/javascript/panda/editor/editor_js_initializer.js +10 -3
- 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 +22 -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 +32 -3
|
@@ -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",
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
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
|
|
24
|
-
return content.to_s unless content.is_a?(Hash) && content[
|
|
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[
|
|
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
|
|
42
|
+
return "" if blocks.nil? || blocks.empty?
|
|
43
43
|
|
|
44
|
-
content = {
|
|
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
|
|
51
|
+
return "" if blocks.nil? || blocks.empty?
|
|
52
52
|
|
|
53
|
-
content = {
|
|
53
|
+
content = {"blocks" => blocks}
|
|
54
54
|
rendered = self.class.new(content, options).render
|
|
55
55
|
|
|
56
56
|
[
|
|
57
|
-
|
|
58
|
-
(title ? "<h1>#{title}</h1>" :
|
|
57
|
+
"<article>",
|
|
58
|
+
(title ? "<h1>#{title}</h1>" : ""),
|
|
59
59
|
rendered,
|
|
60
|
-
|
|
60
|
+
"</article>"
|
|
61
61
|
].join("\n")
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
private
|
|
65
65
|
|
|
66
66
|
def validate_html(html)
|
|
67
|
-
return
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
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[
|
|
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[
|
|
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[
|
|
114
|
-
custom_renderers[block[
|
|
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[
|
|
122
|
-
renderer_class.new(block[
|
|
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[
|
|
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[
|
|
129
|
+
block["type"] == "paragraph" && block["data"]["text"].blank?
|
|
130
130
|
end
|
|
131
131
|
end
|
|
132
132
|
|
|
133
133
|
def empty_paragraph?(block)
|
|
134
|
-
block[
|
|
134
|
+
block["type"] == "paragraph" && block["data"]["text"].blank?
|
|
135
135
|
end
|
|
136
136
|
|
|
137
137
|
def render_block(block)
|
data/lib/panda/editor/version.rb
CHANGED
data/lib/panda/editor.rb
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require_relative
|
|
5
|
-
require_relative
|
|
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: {}
|
|
@@ -18,21 +20,21 @@ module Panda
|
|
|
18
20
|
class Error < StandardError; end
|
|
19
21
|
|
|
20
22
|
# Require components
|
|
21
|
-
require_relative
|
|
22
|
-
require_relative
|
|
23
|
-
require_relative
|
|
24
|
-
require_relative
|
|
25
|
-
require_relative
|
|
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"
|
|
26
28
|
|
|
27
29
|
module Blocks
|
|
28
|
-
autoload :Base,
|
|
29
|
-
autoload :Alert,
|
|
30
|
-
autoload :Header,
|
|
31
|
-
autoload :Image,
|
|
32
|
-
autoload :List,
|
|
33
|
-
autoload :Paragraph,
|
|
34
|
-
autoload :Quote,
|
|
35
|
-
autoload :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"
|
|
36
38
|
end
|
|
37
39
|
end
|
|
38
40
|
end
|
data/lib/tasks/assets.rake
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
namespace :panda_editor do
|
|
4
4
|
namespace :assets do
|
|
5
|
-
desc
|
|
5
|
+
desc "Compile Panda Editor assets for production"
|
|
6
6
|
task compile: :environment do
|
|
7
|
-
require
|
|
7
|
+
require "fileutils"
|
|
8
8
|
|
|
9
|
-
puts
|
|
9
|
+
puts "Compiling Panda Editor assets..."
|
|
10
10
|
|
|
11
11
|
# Create temporary directory for assets
|
|
12
|
-
tmp_dir = Rails.root.join(
|
|
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(
|
|
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(
|
|
27
|
+
FileUtils.cp_r(Dir.glob(tmp_dir.join("*")), public_dir)
|
|
28
28
|
|
|
29
|
-
puts
|
|
29
|
+
puts "✅ Assets compiled successfully"
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
desc
|
|
32
|
+
desc "Download Panda Editor assets from GitHub"
|
|
33
33
|
task download: :environment do
|
|
34
|
-
require
|
|
34
|
+
require "panda/editor/asset_loader"
|
|
35
35
|
|
|
36
|
-
puts
|
|
36
|
+
puts "Downloading Panda Editor assets from GitHub..."
|
|
37
37
|
Panda::Editor::AssetLoader.send(:download_assets_from_github)
|
|
38
|
-
puts
|
|
38
|
+
puts "✅ Assets downloaded successfully"
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
desc
|
|
41
|
+
desc "Upload compiled assets to GitHub release"
|
|
42
42
|
task upload: :environment do
|
|
43
|
-
require
|
|
44
|
-
require
|
|
43
|
+
require "net/http"
|
|
44
|
+
require "json"
|
|
45
45
|
|
|
46
|
-
puts
|
|
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[
|
|
52
|
-
token = ENV[
|
|
51
|
+
version = ENV["GITHUB_REF_NAME"] || "v#{Panda::Editor::VERSION}"
|
|
52
|
+
token = ENV["GITHUB_TOKEN"]
|
|
53
53
|
|
|
54
54
|
unless token
|
|
55
|
-
puts
|
|
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(
|
|
61
|
-
js_file = Dir.glob(assets_dir.join(
|
|
62
|
-
css_file = Dir.glob(assets_dir.join(
|
|
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
|
|
67
|
+
puts "✅ Assets uploaded successfully"
|
|
68
68
|
else
|
|
69
|
-
puts
|
|
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
|
|
77
|
+
puts " Compiling JavaScript..."
|
|
78
78
|
|
|
79
|
-
js_files = Dir.glob(Panda::Editor::Engine.root.join(
|
|
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
|
|
91
|
+
puts " Compiling CSS..."
|
|
92
92
|
|
|
93
|
-
css_files = Dir.glob(Panda::Editor::Engine.root.join(
|
|
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