Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ GEM
nokogiri (>= 1.5.9)
method_source (0.9.2)
minitest (5.14.4)
nokogiri (1.12.3-arm64-darwin)
racc (~> 1.4)
nokogiri (1.12.3-x86_64-darwin)
racc (~> 1.4)
parallel (1.20.1)
Expand Down Expand Up @@ -104,6 +106,7 @@ GEM
zeitwerk (2.4.2)

PLATFORMS
arm64-darwin-20
x86_64-darwin-20

DEPENDENCIES
Expand Down
115 changes: 94 additions & 21 deletions lib/editor_js/blocks/list_block.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ module EditorJs
module Blocks
# list block
class ListBlock < Base
LIST_STYLES = {
ol: %w[1 a i].freeze,
ul: %w[disc circle square].freeze
}.freeze

def schema
YAML.safe_load(<<~YAML)
type: object
Expand All @@ -13,21 +18,102 @@ def schema
type: string
pattern: ^(un)?ordered$
items:
oneOf:
- $ref: "#/definitions/list"
- $ref: "#/definitions/nestedList"
definitions:
list:
type: array
items:
type: string
nestedList:
type: array
items:
$ref: "#/definitions/nestedItems"
nestedItems:
type: object
properties:
content:
type: string
items:
type: array
items:
$ref: "#/definitions/nestedItems"
minItems: 0
YAML
end

def render(_options = {})
tag = data['style'] == 'unordered' ? :ul : :ol
content_tag(tag, class: css_name) do
children_tag_string = ''
data['items'].each do |v|
children_tag_string += content_tag(:li, v.html_safe)
content_tag(list_tag, class: css_name, type: list_style(0)) do
data['items'].map { |item| render_item(item) }.join.html_safe
end
end

def plain
data['items'].map { |item| plain_item(item) }.join(', ')
end

def sanitize!
data['items'] = data['items'].map { |item| sanitize_item!(item) }
end

private

def render_item(item, level = 1)
if nested?
if item['items'].blank?
return content_tag(:li, item['content'].html_safe)
end
children_tag_string.html_safe

list = content_tag(list_tag, class: css_name, type: list_style(level)) do
item['items'].map { |i| render_item(i, level + 1) }.join.html_safe
end
return content_tag(:li, (item['content'] + list).html_safe)
end

content_tag(:li, item.html_safe)
end

def plain_item(item)
if nested?
return [
decode_html(Sanitize.fragment(item['content'])).strip,
item['items'].map { |i| plain_item(i) }.join(', ')
].reject(&:empty?).join(', ')
end

decode_html(Sanitize.fragment(item)).strip
end

def sanitize_item!(item)
if nested?
# recursively sanitize nested item nodes
item['content'] = Sanitize.fragment(
item['content'],
elements: safe_tags.keys,
attributes: safe_tags.select { |_k, v| v },
remove_contents: true
)
item['items'] = item['items'].map { |nested| sanitize_item!(nested) }
return item
end

Sanitize.fragment(
item,
elements: safe_tags.keys,
attributes: safe_tags.select { |_k, v| v },
remove_contents: true
)
end

def list_tag
data['style'] == 'unordered' ? :ul : :ol
end

def nested?
return @nested if defined? @nested

@nested = data['items'].first.class != String
end

def safe_tags
Expand All @@ -42,21 +128,8 @@ def safe_tags
}
end

def sanitize!
data['items'] = data['items'].map do |text|
Sanitize.fragment(
text,
elements: safe_tags.keys,
attributes: safe_tags.select { |_k, v| v },
remove_contents: true
)
end
end

def plain
data['items'].map do |text|
decode_html(Sanitize.fragment(text)).strip
end.join(', ')
def list_style(level)
LIST_STYLES[list_tag][level % 3]
end
end
end
Expand Down
89 changes: 79 additions & 10 deletions spec/editor_js/blocks/list_block_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,89 @@
}
end

context 'with valid data' do
let(:list) { described_class.new(valid_data1) }
let(:valid_data3) do
{
type: 'list',
data: {
style: 'ordered',
items: [
{
content: "列表2 <b>hacker</b> &lt;1&gt;<strong>大字体</strong>《没》<i>斜体<i><a href='www.baidu.com' class='red' data-href='nodata'>go baidu<a>",
items: [
{
content: '列表2 &lt;2&gt;《<html>body<html>',
items: []
}
]
},
{
content: '列表2 3',
items: []
}
]
}
}
end

it { expect(list).to be_valid }
it { expect(list.render).to eq(%|<ol class=\"editor_js--list\"><li>item <b>hacker</b> &lt;1&gt;《没》<i>斜体<i><a href=\"www.baidu.com\">go baidu</a><a></a></i></i></li><li>item &lt;2&gt;</li><li>item 3</li></ol>|) }
it { expect(list.plain).to eq('item hacker <1>《没》斜体go baidu, item <2>, item 3') }
let(:valid_data4) do
{
type: 'list',
data: {
style: 'unordered',
items: [
{
content: "列表2 <b>hacker</b> &lt;1&gt;<strong>大字体</strong>《没》<i>斜体<i><a href='www.baidu.com' class='red' data-href='nodata'>go baidu<a>",
items: [
{
content: '列表2 &lt;2&gt;《<html>body<html>',
items: []
}
]
},
{
content: '列表2 3',
items: []
}
]
}
}
end

context 'with valid data' do
let(:list) { described_class.new(valid_data2) }
context 'non-nested' do
context 'ordered list' do
let(:list) { described_class.new(valid_data1) }

it { expect(list).to be_valid }
it { expect(list.render).to eq(%|<ol class=\"editor_js--list\" type=\"1\"><li>item <b>hacker</b> &lt;1&gt;《没》<i>斜体<i><a href=\"www.baidu.com\">go baidu</a><a></a></i></i></li><li>item &lt;2&gt;</li><li>item 3</li></ol>|) }
it { expect(list.plain).to eq('item hacker <1>《没》斜体go baidu, item <2>, item 3') }
end

it { expect(list).to be_valid }
it { expect(list.render).to eq(%|<ul class=\"editor_js--list\"><li>列表2 <b>hacker</b> &lt;1&gt;《没》<i>斜体<i><a href=\"www.baidu.com\">go baidu</a><a></a></i></i></li><li>列表2 &lt;2&gt;《body</li><li>列表2 3</li></ul>|) }
it { expect(list.plain).to eq('列表2 hacker <1>《没》斜体go baidu, 列表2 <2>《body, 列表2 3') }
context 'unordered list' do
let(:list) { described_class.new(valid_data2) }

it { expect(list).to be_valid }
it { expect(list.render).to eq(%|<ul class=\"editor_js--list\" type=\"disc\"><li>列表2 <b>hacker</b> &lt;1&gt;《没》<i>斜体<i><a href=\"www.baidu.com\">go baidu</a><a></a></i></i></li><li>列表2 &lt;2&gt;《body</li><li>列表2 3</li></ul>|) }
it { expect(list.plain).to eq('列表2 hacker <1>《没》斜体go baidu, 列表2 <2>《body, 列表2 3') }
end
end

context 'nested' do
context 'ordered list' do
let(:list) { described_class.new(valid_data3) }

it { expect(list).to be_valid }
it do
expect(list.render).to eq("<ol class=\"editor_js--list\" type=\"1\"><li>列表2 <b>hacker</b> &lt;1&gt;《没》<i>斜体<i><a href=\"www.baidu.com\">go baidu</a><a></a></i></i><ol class=\"editor_js--list\" type=\"a\"><li>列表2 &lt;2&gt;《body</li></ol></li><li>列表2 3</li></ol>")
end
it { expect(list.plain).to eq("列表2 hacker <1>《没》斜体go baidu, 列表2 <2>《body, 列表2 3") }
end

context 'unordered list' do
let(:list) { described_class.new(valid_data3) }

it { expect(list).to be_valid }
it { expect(list.render).to eq("<ol class=\"editor_js--list\" type=\"1\"><li>列表2 <b>hacker</b> &lt;1&gt;《没》<i>斜体<i><a href=\"www.baidu.com\">go baidu</a><a></a></i></i><ol class=\"editor_js--list\" type=\"a\"><li>列表2 &lt;2&gt;《body</li></ol></li><li>列表2 3</li></ol>") }
it { expect(list.plain).to eq("列表2 hacker <1>《没》斜体go baidu, 列表2 <2>《body, 列表2 3") }
end
end
end