panda-editor 0.5.0 → 0.6.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 +19 -0
- data/README.md +68 -0
- data/lib/panda/editor/engine.rb +6 -0
- data/lib/panda/editor/html_to_editor_js_converter.rb +159 -0
- data/lib/panda/editor/markdown_to_editor_js_converter.rb +50 -0
- data/lib/panda/editor/version.rb +1 -1
- data/lib/panda/editor.rb +6 -4
- data/lib/panda-editor.rb +5 -0
- data/panda-editor.gemspec +1 -0
- metadata +18 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: adac7a7394c996ae17e8c3312f0b20bc8517724aa82397694ecce20e82821280
|
|
4
|
+
data.tar.gz: 595a17b99fde3d04816f1b957ce9c875de521475d69bc9358f3c370e9c380301
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9395e397b2c94eea2d8d8c0a0183a6262a1bfe79dbc9aae23f833a255b214be22ffc6ca4f92f2e2218adadf948410e388c853bea3b3eaf589ed555ba9c1c2df4
|
|
7
|
+
data.tar.gz: 6fc84783464e4761bcf1fec9f162ff7fec33b571bf816d026167514c1d63cc4f640ffc8c0863e765bf58730f0713a7a47ff5f62b99c810a5f1806804b62e7f8c
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.6.0] - 2025-11-09
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- HTML to EditorJS converter for importing existing HTML content
|
|
13
|
+
- Supports headers (h1-h6), paragraphs, lists (ordered/unordered), quotes, code blocks, tables, and delimiters
|
|
14
|
+
- Preserves inline formatting (bold, italic, links, etc.) as HTML
|
|
15
|
+
- Handles edge cases: empty content, malformed HTML, special characters, whitespace
|
|
16
|
+
- Comprehensive test coverage (22 examples)
|
|
17
|
+
- Available as `Panda::Editor::HtmlToEditorJsConverter.convert(html)`
|
|
18
|
+
- Markdown to EditorJS converter for importing Markdown content
|
|
19
|
+
- Uses Redcarpet to parse markdown to HTML, then converts to EditorJS
|
|
20
|
+
- Supports all standard markdown features: headers, paragraphs, lists, code blocks, tables, blockquotes
|
|
21
|
+
- Includes advanced features: superscript, footnotes, strikethrough, autolinks
|
|
22
|
+
- Security-hardened with noopener/noreferrer on links
|
|
23
|
+
- Comprehensive test coverage (26 examples)
|
|
24
|
+
- Available as `Panda::Editor::MarkdownToEditorJsConverter.convert(markdown)`
|
|
25
|
+
- Added nokogiri as explicit dependency (required for HTML parsing)
|
|
26
|
+
|
|
8
27
|
## [0.5.0] - 2025-11-04
|
|
9
28
|
|
|
10
29
|
### Fixed
|
data/README.md
CHANGED
|
@@ -55,6 +55,74 @@ renderer = Panda::Editor::Renderer.new(@post.content)
|
|
|
55
55
|
<%= raw renderer.render %>
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
+
### Converting Content to EditorJS
|
|
59
|
+
|
|
60
|
+
Panda Editor includes converters for importing existing HTML or Markdown content into EditorJS format:
|
|
61
|
+
|
|
62
|
+
#### From HTML
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
html = '<h1>Article Title</h1><p>Introduction with <strong>bold</strong> text.</p><ul><li>Point 1</li><li>Point 2</li></ul>'
|
|
66
|
+
editor_data = Panda::Editor::HtmlToEditorJsConverter.convert(html)
|
|
67
|
+
|
|
68
|
+
# Save to your model
|
|
69
|
+
@post.content = editor_data
|
|
70
|
+
@post.save
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Supported HTML elements:**
|
|
74
|
+
|
|
75
|
+
- Headers (h1-h6)
|
|
76
|
+
- Paragraphs with inline formatting (bold, italic, links, etc.)
|
|
77
|
+
- Ordered and unordered lists
|
|
78
|
+
- Blockquotes
|
|
79
|
+
- Code blocks (pre/code)
|
|
80
|
+
- Tables (with or without headers)
|
|
81
|
+
- Horizontal rules (converted to delimiters)
|
|
82
|
+
|
|
83
|
+
#### From Markdown
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
markdown = <<~MD
|
|
87
|
+
# Article Title
|
|
88
|
+
|
|
89
|
+
Introduction with **bold** and *italic* text.
|
|
90
|
+
|
|
91
|
+
- Point 1
|
|
92
|
+
- Point 2
|
|
93
|
+
|
|
94
|
+
> A famous quote
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
def hello
|
|
98
|
+
puts "world"
|
|
99
|
+
end
|
|
100
|
+
```
|
|
101
|
+
MD
|
|
102
|
+
|
|
103
|
+
editor_data = Panda::Editor::MarkdownToEditorJsConverter.convert(markdown)
|
|
104
|
+
|
|
105
|
+
# Save to your model
|
|
106
|
+
@post.content = editor_data
|
|
107
|
+
@post.save
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Supported Markdown features:**
|
|
111
|
+
|
|
112
|
+
- Headers (# through ######)
|
|
113
|
+
- Paragraphs with inline formatting (\*\*bold\*\*, \*italic\*, \`code\`, \~\~strikethrough\~\~)
|
|
114
|
+
- Links (with automatic noopener/noreferrer for security)
|
|
115
|
+
- Ordered and unordered lists
|
|
116
|
+
- Blockquotes
|
|
117
|
+
- Fenced and indented code blocks
|
|
118
|
+
- Tables (GitHub-flavored markdown)
|
|
119
|
+
- Horizontal rules
|
|
120
|
+
- Superscript (^2)
|
|
121
|
+
- Footnotes
|
|
122
|
+
- Automatic URL linking
|
|
123
|
+
|
|
124
|
+
Both converters return a hash in EditorJS format that can be saved directly to your content field.
|
|
125
|
+
|
|
58
126
|
### JavaScript Integration
|
|
59
127
|
|
|
60
128
|
In your application.js:
|
data/lib/panda/editor/engine.rb
CHANGED
|
@@ -12,6 +12,12 @@ module Panda
|
|
|
12
12
|
g.test_framework :rspec
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
+
# Eager load converter classes
|
|
16
|
+
config.to_prepare do
|
|
17
|
+
require 'panda/editor/markdown_to_editor_js_converter'
|
|
18
|
+
require 'panda/editor/html_to_editor_js_converter'
|
|
19
|
+
end
|
|
20
|
+
|
|
15
21
|
initializer 'panda_editor.assets' do |app|
|
|
16
22
|
next unless app.config.respond_to?(:assets)
|
|
17
23
|
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
|
|
5
|
+
module Panda
|
|
6
|
+
module Editor
|
|
7
|
+
# Converts HTML to EditorJS format
|
|
8
|
+
# Parses HTML and converts it to EditorJS blocks
|
|
9
|
+
class HtmlToEditorJsConverter
|
|
10
|
+
def self.convert(html)
|
|
11
|
+
new(html).convert
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(html)
|
|
15
|
+
@html = html
|
|
16
|
+
@blocks = []
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def convert
|
|
20
|
+
doc = Nokogiri::HTML.fragment(@html)
|
|
21
|
+
|
|
22
|
+
doc.children.each do |node|
|
|
23
|
+
block = node_to_block(node)
|
|
24
|
+
@blocks << block if block
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
{
|
|
28
|
+
time: Time.now.to_i * 1000,
|
|
29
|
+
blocks: @blocks,
|
|
30
|
+
version: "2.28.0"
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def node_to_block(node)
|
|
37
|
+
return nil if node.text? && node.text.strip.empty?
|
|
38
|
+
|
|
39
|
+
case node.name
|
|
40
|
+
when "h1", "h2", "h3", "h4", "h5", "h6"
|
|
41
|
+
header_block(node)
|
|
42
|
+
when "p"
|
|
43
|
+
paragraph_block(node)
|
|
44
|
+
when "ul", "ol"
|
|
45
|
+
list_block(node)
|
|
46
|
+
when "blockquote"
|
|
47
|
+
quote_block(node)
|
|
48
|
+
when "pre"
|
|
49
|
+
code_block(node)
|
|
50
|
+
when "table"
|
|
51
|
+
table_block(node)
|
|
52
|
+
when "hr"
|
|
53
|
+
delimiter_block
|
|
54
|
+
when "text"
|
|
55
|
+
# Handle text nodes that aren't wrapped in tags
|
|
56
|
+
text = node.text.strip
|
|
57
|
+
text.empty? ? nil : paragraph_block_from_text(text)
|
|
58
|
+
else
|
|
59
|
+
# For any other node, try to extract text content
|
|
60
|
+
text = node.text.strip
|
|
61
|
+
text.empty? ? nil : paragraph_block_from_text(text)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def header_block(node)
|
|
66
|
+
level = node.name[1].to_i
|
|
67
|
+
{
|
|
68
|
+
type: "header",
|
|
69
|
+
data: {
|
|
70
|
+
text: node.inner_html.strip,
|
|
71
|
+
level: level
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def paragraph_block(node)
|
|
77
|
+
text = node.inner_html.strip
|
|
78
|
+
return nil if text.empty?
|
|
79
|
+
|
|
80
|
+
{
|
|
81
|
+
type: "paragraph",
|
|
82
|
+
data: {
|
|
83
|
+
text: text
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def paragraph_block_from_text(text)
|
|
89
|
+
{
|
|
90
|
+
type: "paragraph",
|
|
91
|
+
data: {
|
|
92
|
+
text: text
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def list_block(node)
|
|
98
|
+
style = node.name == "ol" ? "ordered" : "unordered"
|
|
99
|
+
items = node.css("li").map { |li| li.inner_html.strip }
|
|
100
|
+
|
|
101
|
+
{
|
|
102
|
+
type: "list",
|
|
103
|
+
data: {
|
|
104
|
+
style: style,
|
|
105
|
+
items: items
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def quote_block(node)
|
|
111
|
+
{
|
|
112
|
+
type: "quote",
|
|
113
|
+
data: {
|
|
114
|
+
text: node.inner_html.strip,
|
|
115
|
+
caption: "",
|
|
116
|
+
alignment: "left"
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def code_block(node)
|
|
122
|
+
code = node.css("code").first
|
|
123
|
+
text = code ? code.text : node.text
|
|
124
|
+
|
|
125
|
+
{
|
|
126
|
+
type: "code",
|
|
127
|
+
data: {
|
|
128
|
+
code: text
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def table_block(node)
|
|
134
|
+
content = []
|
|
135
|
+
|
|
136
|
+
# Process table rows
|
|
137
|
+
node.css("tr").each do |row|
|
|
138
|
+
cells = row.css("th, td").map { |cell| cell.inner_html.strip }
|
|
139
|
+
content << cells
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
{
|
|
143
|
+
type: "table",
|
|
144
|
+
data: {
|
|
145
|
+
withHeadings: node.css("thead").any? || node.css("th").any?,
|
|
146
|
+
content: content
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def delimiter_block
|
|
152
|
+
{
|
|
153
|
+
type: "delimiter",
|
|
154
|
+
data: {}
|
|
155
|
+
}
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "redcarpet"
|
|
4
|
+
|
|
5
|
+
module Panda
|
|
6
|
+
module Editor
|
|
7
|
+
# Converts Markdown to EditorJS format
|
|
8
|
+
# Uses Redcarpet to parse markdown to HTML, then converts HTML to EditorJS blocks
|
|
9
|
+
class MarkdownToEditorJsConverter
|
|
10
|
+
def self.convert(markdown)
|
|
11
|
+
new(markdown).convert
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(markdown)
|
|
15
|
+
@markdown = markdown
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def convert
|
|
19
|
+
# Step 1: Convert Markdown to HTML using Redcarpet
|
|
20
|
+
html = markdown_to_html
|
|
21
|
+
|
|
22
|
+
# Step 2: Convert HTML to EditorJS using existing converter
|
|
23
|
+
Panda::Editor::HtmlToEditorJsConverter.convert(html)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def markdown_to_html
|
|
29
|
+
renderer = Redcarpet::Render::HTML.new(
|
|
30
|
+
hard_wrap: true,
|
|
31
|
+
link_attributes: {rel: "noopener noreferrer"}
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
markdown_processor = Redcarpet::Markdown.new(
|
|
35
|
+
renderer,
|
|
36
|
+
autolink: true,
|
|
37
|
+
tables: true,
|
|
38
|
+
fenced_code_blocks: true,
|
|
39
|
+
strikethrough: true,
|
|
40
|
+
superscript: true,
|
|
41
|
+
footnotes: true,
|
|
42
|
+
no_intra_emphasis: true,
|
|
43
|
+
space_after_headers: true
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
markdown_processor.render(@markdown)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/lib/panda/editor/version.rb
CHANGED
data/lib/panda/editor.rb
CHANGED
|
@@ -17,10 +17,12 @@ module Panda
|
|
|
17
17
|
|
|
18
18
|
class Error < StandardError; end
|
|
19
19
|
|
|
20
|
-
#
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
# Require components
|
|
21
|
+
require_relative 'editor/renderer'
|
|
22
|
+
require_relative 'editor/content'
|
|
23
|
+
require_relative 'editor/footnote_registry'
|
|
24
|
+
require_relative 'editor/markdown_to_editor_js_converter'
|
|
25
|
+
require_relative 'editor/html_to_editor_js_converter'
|
|
24
26
|
|
|
25
27
|
module Blocks
|
|
26
28
|
autoload :Base, 'panda/editor/blocks/base'
|
data/lib/panda-editor.rb
ADDED
data/panda-editor.gemspec
CHANGED
|
@@ -32,6 +32,7 @@ Gem::Specification.new do |spec|
|
|
|
32
32
|
|
|
33
33
|
# Rails dependencies
|
|
34
34
|
spec.add_dependency 'dry-configurable', '~> 1.0'
|
|
35
|
+
spec.add_dependency 'nokogiri', '~> 1.15'
|
|
35
36
|
spec.add_dependency 'rails', '>= 7.1'
|
|
36
37
|
spec.add_dependency 'redcarpet', '~> 3.6'
|
|
37
38
|
spec.add_dependency 'sanitize', '~> 6.0'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: panda-editor
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Otaina Limited
|
|
@@ -24,6 +24,20 @@ dependencies:
|
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
26
|
version: '1.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: nokogiri
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '1.15'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '1.15'
|
|
27
41
|
- !ruby/object:Gem::Dependency
|
|
28
42
|
name: rails
|
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -133,6 +147,7 @@ files:
|
|
|
133
147
|
- app/services/panda/editor/html_to_editor_js_converter.rb
|
|
134
148
|
- config/importmap.rb
|
|
135
149
|
- docs/FOOTNOTES.md
|
|
150
|
+
- lib/panda-editor.rb
|
|
136
151
|
- lib/panda/editor.rb
|
|
137
152
|
- lib/panda/editor/asset_loader.rb
|
|
138
153
|
- lib/panda/editor/blocks/alert.rb
|
|
@@ -146,6 +161,8 @@ files:
|
|
|
146
161
|
- lib/panda/editor/content.rb
|
|
147
162
|
- lib/panda/editor/engine.rb
|
|
148
163
|
- lib/panda/editor/footnote_registry.rb
|
|
164
|
+
- lib/panda/editor/html_to_editor_js_converter.rb
|
|
165
|
+
- lib/panda/editor/markdown_to_editor_js_converter.rb
|
|
149
166
|
- lib/panda/editor/renderer.rb
|
|
150
167
|
- lib/panda/editor/version.rb
|
|
151
168
|
- lib/tasks/assets.rake
|