From c40713c3a90ff5c9422e6570421095059da3c963 Mon Sep 17 00:00:00 2001 From: Joe Woodward Date: Tue, 9 Nov 2021 16:37:57 +0700 Subject: [PATCH] Add support for nested lists for @editorjs/nested-list --- Gemfile.lock | 3 + lib/editor_js/blocks/list_block.rb | 115 ++++++++++++++++++----- spec/editor_js/blocks/list_block_spec.rb | 89 ++++++++++++++++-- 3 files changed, 176 insertions(+), 31 deletions(-) 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(%|
  1. item hacker <1>《没》斜体go baidu
  2. item <2>
  3. 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(%|
  1. item hacker <1>《没》斜体go baidu
  2. item <2>
  3. 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("
  1. 列表2 hacker <1>《没》斜体go baidu
    1. 列表2 <2>《body
  2. 列表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("
  1. 列表2 hacker <1>《没》斜体go baidu
    1. 列表2 <2>《body
  2. 列表2 3
") } + it { expect(list.plain).to eq("列表2 hacker <1>《没》斜体go baidu, 列表2 <2>《body, 列表2 3") } + end + end end