diff --git a/Gemfile.lock b/Gemfile.lock
index 0413021..b2e4b57 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -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)
@@ -104,6 +106,7 @@ GEM
zeitwerk (2.4.2)
PLATFORMS
+ arm64-darwin-20
x86_64-darwin-20
DEPENDENCIES
diff --git a/lib/editor_js/blocks/list_block.rb b/lib/editor_js/blocks/list_block.rb
index 99b46b0..565c89d 100644
--- a/lib/editor_js/blocks/list_block.rb
+++ b/lib/editor_js/blocks/list_block.rb
@@ -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
@@ -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
@@ -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
diff --git a/spec/editor_js/blocks/list_block_spec.rb b/spec/editor_js/blocks/list_block_spec.rb
index 6246fdf..30e4556 100644
--- a/spec/editor_js/blocks/list_block_spec.rb
+++ b/spec/editor_js/blocks/list_block_spec.rb
@@ -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 hacker <1>大字体《没》斜体go baidu",
+ items: [
+ {
+ content: '列表2 <2>《body',
+ items: []
+ }
+ ]
+ },
+ {
+ content: '列表2 3',
+ items: []
+ }
+ ]
+ }
+ }
+ end
- it { expect(list).to be_valid }
- it { expect(list.render).to eq(%|- item hacker <1>《没》斜体go baidu
- item <2>
- item 3
|) }
- 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 hacker <1>大字体《没》斜体go baidu",
+ items: [
+ {
+ content: '列表2 <2>《body',
+ 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(%|- item hacker <1>《没》斜体go baidu
- item <2>
- item 3
|) }
+ 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(%|- 列表2 hacker <1>《没》斜体go baidu
- 列表2 <2>《body
- 列表2 3
|) }
- 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(%|- 列表2 hacker <1>《没》斜体go baidu
- 列表2 <2>《body
- 列表2 3
|) }
+ 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("- 列表2 hacker <1>《没》斜体go baidu
- 列表2 <2>《body
- 列表2 3
")
+ 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("- 列表2 hacker <1>《没》斜体go baidu
- 列表2 <2>《body
- 列表2 3
") }
+ it { expect(list.plain).to eq("列表2 hacker <1>《没》斜体go baidu, 列表2 <2>《body, 列表2 3") }
+ end
+ end
end