Skip to content
This repository was archived by the owner on Nov 8, 2022. It is now read-only.

Commit a8f52d5

Browse files
authored
Merge pull request #292 from coderplanets/editor-quote
feat(editor-export): quote block
2 parents a388aa3 + 2c281df commit a8f52d5

File tree

11 files changed

+317
-4
lines changed

11 files changed

+317
-4
lines changed

lib/helper/converter/editor_to_html/class.ex

+9
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@ defmodule Helper.Converter.EditorToHTML.Class do
2222
"eyebrow_title" => "eyebrow-title",
2323
"footer_title" => "footer-title"
2424
},
25+
# quote block
26+
"quote" => %{
27+
"short_wrapper" => "quote-short",
28+
"long_wrapper" => "quote-long",
29+
"text" => "quote__text",
30+
"caption" => "quote-caption",
31+
"caption_line" => "quote-caption__line",
32+
"caption_text" => "quote-caption__text"
33+
},
2534
# list
2635
"list" => %{
2736
"wrapper" => "list-wrapper",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
defmodule Helper.Converter.EditorToHTML.Frags.Quote do
2+
@moduledoc """
3+
parse editor.js's block fragments, use for test too
4+
5+
see https://editorjs.io/
6+
"""
7+
import Helper.Validator.Guards, only: [g_none_empty_str: 1]
8+
9+
alias Helper.Types, as: T
10+
alias Helper.Converter.EditorToHTML.Class
11+
12+
@class get_in(Class.article(), ["quote"])
13+
14+
@spec get(T.editor_quote()) :: T.html()
15+
def get(%{"mode" => "short", "text" => text}) do
16+
wrapper_class = @class["short_wrapper"]
17+
text_class = @class["text"]
18+
19+
~s(<blockquote class="#{wrapper_class}">
20+
<div class="#{text_class}">#{text}</div>
21+
</blockquote>)
22+
end
23+
24+
def get(%{"mode" => "long", "text" => text, "caption" => caption})
25+
when g_none_empty_str(caption) do
26+
wrapper_class = @class["long_wrapper"]
27+
text_class = @class["text"]
28+
29+
caption = frag(:caption, caption)
30+
31+
~s(<blockquote class="#{wrapper_class}">
32+
<div class="#{text_class}">#{text}</div>
33+
#{caption}
34+
</blockquote>)
35+
end
36+
37+
def get(%{"mode" => "long", "text" => text}) do
38+
wrapper_class = @class["long_wrapper"]
39+
text_class = @class["text"]
40+
41+
~s(<blockquote class="#{wrapper_class}">
42+
<div class="#{text_class}">#{text}</div>
43+
</blockquote>)
44+
end
45+
46+
@spec frag(:caption, String.t()) :: T.html()
47+
def frag(:caption, caption) do
48+
caption_class = @class["caption"]
49+
caption_line_class = @class["caption_line"]
50+
caption_text_class = @class["caption_text"]
51+
52+
~s(<div class="#{caption_class}">
53+
<div class="#{caption_line_class}"/>
54+
<div class="#{caption_text_class}">#{caption}</div>
55+
</div>)
56+
end
57+
end

lib/helper/converter/editor_to_html/frontend_test/script.js

+5-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/helper/converter/editor_to_html/frontend_test/styles.css

+89
Original file line numberDiff line numberDiff line change
@@ -277,3 +277,92 @@ td {
277277
}
278278

279279
/* table block end */
280+
281+
/* quote block */
282+
283+
.article-viewer-wrapper .quote-short {
284+
margin-left: 15px;
285+
min-height: 0;
286+
border: none;
287+
border-radius: 0;
288+
border-left: 4px solid;
289+
border-left-color: #388ae5;
290+
color: grey;
291+
box-shadow: none;
292+
}
293+
294+
.article-viewer-wrapper .quote__text {
295+
line-height: 1.8em;
296+
min-height: 30px;
297+
border: none;
298+
border-radius: 0;
299+
color: grey;
300+
box-shadow: none;
301+
padding: 10px 12px;
302+
outline: none;
303+
width: 100%;
304+
box-sizing: border-box;
305+
}
306+
307+
.article-viewer-wrapper .quote-long {
308+
position: relative;
309+
border: 2px solid;
310+
border-left: 1px solid;
311+
border-right: 1px solid;
312+
border-bottom: 1px solid;
313+
border-color: lightgrey;
314+
margin: 15px;
315+
margin-left: 22px;
316+
margin-right: 42px;
317+
min-height: 140px;
318+
padding: 6px 30px;
319+
padding-bottom: 60px;
320+
border-top-right-radius: 12px;
321+
border-bottom-left-radius: 12px;
322+
}
323+
324+
.article-viewer-wrapper .quote-long::before {
325+
position: absolute;
326+
content: "\201C";
327+
font-size: 50px;
328+
color: #e8e7e7;
329+
left: -16px;
330+
top: 9px;
331+
height: 35px;
332+
background: white;
333+
}
334+
335+
.article-viewer-wrapper .quote-long::after {
336+
position: absolute;
337+
content: "\201D";
338+
font-size: 42px;
339+
color: #e8e7e7;
340+
right: -13px;
341+
bottom: 9px;
342+
height: 28px;
343+
background: white;
344+
}
345+
346+
.article-viewer-wrapper .quote-caption {
347+
position: absolute;
348+
bottom: 10px;
349+
right: 42px;
350+
display: flex;
351+
align-items: center;
352+
}
353+
354+
.article-viewer-wrapper .quote-caption__line {
355+
width: 30px;
356+
height: 1px;
357+
margin-right: 10px;
358+
background: #bbb;
359+
}
360+
361+
.article-viewer-wrapper .quote-caption__text {
362+
cursor: text;
363+
outline: none;
364+
color: grey;
365+
font-size: 14px;
366+
}
367+
368+
/* quote block end */

lib/helper/converter/editor_to_html/index.ex

+2
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ defmodule Helper.Converter.EditorToHTML do
4545

4646
defp parse_block(%{"type" => "header", "data" => data}), do: Frags.Header.get(data)
4747

48+
defp parse_block(%{"type" => "quote", "data" => data}), do: Frags.Quote.get(data)
49+
4850
defp parse_block(%{"type" => "list", "data" => data}) do
4951
%{"items" => items, "mode" => mode} = data
5052

lib/helper/converter/editor_to_html/validator/editor_schema.ex

+13
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ defmodule Helper.Converter.EditorToHTML.Validator.EditorSchema do
44
# header
55
@valid_header_level [1, 2, 3]
66

7+
# quote
8+
@valid_quote_mode ["short", "long"]
9+
710
# list
811
@valid_list_mode ["checklist", "order_list", "unorder_list"]
912
@valid_list_label_type ["green", "red", "warn", "default"]
@@ -12,6 +15,7 @@ defmodule Helper.Converter.EditorToHTML.Validator.EditorSchema do
1215
# table
1316
@valid_table_align ["left", "center", "right"]
1417

18+
@spec get(String.t()) :: map
1519
def get("editor") do
1620
%{
1721
"time" => [:number],
@@ -31,6 +35,15 @@ defmodule Helper.Converter.EditorToHTML.Validator.EditorSchema do
3135

3236
def get("paragraph"), do: %{"text" => [:string]}
3337

38+
def get("quote") do
39+
%{
40+
"text" => [:string],
41+
"mode" => [enum: @valid_quote_mode],
42+
"caption" => [:string, required: false]
43+
}
44+
end
45+
46+
@spec get(String.t()) :: [parent: map, item: map]
3447
def get("list") do
3548
[
3649
parent: %{"mode" => [enum: @valid_list_mode], "items" => [:list]},

lib/helper/converter/editor_to_html/validator/index.ex

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ defmodule Helper.Converter.EditorToHTML.Validator do
77
alias Converter.EditorToHTML.Validator.EditorSchema
88

99
# blocks with no children items
10-
@simple_blocks ["header", "paragraph"]
10+
@normal_blocks ["header", "paragraph", "quote"]
1111
# blocks with "items" fields
1212
@complex_blocks ["list", "table"]
1313

@@ -54,7 +54,7 @@ defmodule Helper.Converter.EditorToHTML.Validator do
5454
end
5555

5656
# validate block which have no nested items
57-
defp validate_block(%{"type" => type, "data" => data}) when type in @simple_blocks do
57+
defp validate_block(%{"type" => type, "data" => data}) when type in @normal_blocks do
5858
validate_with(type, EditorSchema.get(type), data)
5959
end
6060

lib/helper/converter/html_sanitizer.ex

+3
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ defmodule Helper.Converter.HtmlSanitizer do
4545
Meta.allow_tag_with_these_attributes("th", ["class"])
4646
Meta.allow_tag_with_these_attributes("td", ["class", "style"])
4747

48+
# blockquote
49+
Meta.allow_tag_with_these_attributes("blockquote", ["class"])
50+
4851
Meta.allow_tag_with_these_attributes("svg", [
4952
"t",
5053
"p-id",

lib/helper/types.ex

+10
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,16 @@ defmodule Helper.Types do
3838
footerTitle: String.t()
3939
}
4040

41+
@typep editor_quote_mode :: :short | :long
42+
@typedoc """
43+
editor.js's quote tool data format
44+
"""
45+
@type editor_quote :: %{
46+
required(:text) => String.t(),
47+
required(:mode) => editor_quote_mode,
48+
caption: String.t()
49+
}
50+
4151
@typedoc """
4252
valid editor.js's list item indent
4353
"""

lib/helper/validator/guards.ex

+2
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,6 @@ defmodule Helper.Validator.Guards do
44
"""
55
defguard g_pos_int(value) when is_integer(value) and value >= 0
66
defguard g_not_nil(value) when not is_nil(value)
7+
8+
defguard g_none_empty_str(value) when is_binary(value) and byte_size(value) > 0
79
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
defmodule GroupherServer.Test.Helper.Converter.EditorToHTML.Quote do
2+
@moduledoc false
3+
4+
use GroupherServerWeb.ConnCase, async: true
5+
6+
alias Helper.Converter.EditorToHTML.Class
7+
alias Helper.Converter.EditorToHTML, as: Parser
8+
9+
alias Helper.Utils
10+
11+
@root_class Class.article()
12+
@class get_in(@root_class, ["quote"])
13+
14+
describe "[quote block]" do
15+
defp set_data(mode, text, caption \\ nil) do
16+
data =
17+
case caption do
18+
nil ->
19+
%{
20+
"mode" => mode,
21+
"text" => text
22+
}
23+
24+
_ ->
25+
%{
26+
"mode" => mode,
27+
"text" => text,
28+
"caption" => caption
29+
}
30+
end
31+
32+
%{
33+
"time" => 1_567_250_876_713,
34+
"blocks" => [
35+
%{
36+
"type" => "quote",
37+
"data" => data
38+
}
39+
],
40+
"version" => "2.15.0"
41+
}
42+
end
43+
44+
@tag :wip2
45+
test "short quote parse should work" do
46+
editor_json = set_data("short", "short quote")
47+
{:ok, editor_string} = Jason.encode(editor_json)
48+
{:ok, converted} = Parser.to_html(editor_string)
49+
50+
short_wrapper_class = @class["short_wrapper"]
51+
caption_class = @class["caption"]
52+
53+
assert Utils.str_occurence(converted, short_wrapper_class) == 1
54+
assert Utils.str_occurence(converted, "</blockquote>") == 1
55+
assert Utils.str_occurence(converted, caption_class) == 0
56+
end
57+
58+
@tag :wip2
59+
test "long quote parse should work" do
60+
editor_json = set_data("long", "long quote", "caption")
61+
{:ok, editor_string} = Jason.encode(editor_json)
62+
{:ok, converted} = Parser.to_html(editor_string)
63+
64+
long_wrapper_class = @class["long_wrapper"]
65+
caption_text_class = @class["caption_text"]
66+
67+
assert Utils.str_occurence(converted, long_wrapper_class) == 1
68+
assert Utils.str_occurence(converted, "</blockquote>") == 1
69+
assert Utils.str_occurence(converted, caption_text_class) == 1
70+
end
71+
72+
@tag :wip2
73+
test "long quote without caption parse should work" do
74+
editor_json = set_data("long", "long quote")
75+
{:ok, editor_string} = Jason.encode(editor_json)
76+
77+
{:ok, converted} = Parser.to_html(editor_string)
78+
79+
long_wrapper_class = @class["long_wrapper"]
80+
caption_text_class = @class["caption_text"]
81+
82+
assert Utils.str_occurence(converted, long_wrapper_class) == 1
83+
assert Utils.str_occurence(converted, "</blockquote>") == 1
84+
assert Utils.str_occurence(converted, caption_text_class) == 0
85+
end
86+
87+
@tag :wip2
88+
test "long quote without empty caption parse should work" do
89+
editor_json = set_data("long", "long quote", "")
90+
{:ok, editor_string} = Jason.encode(editor_json)
91+
92+
{:ok, converted} = Parser.to_html(editor_string)
93+
94+
long_wrapper_class = @class["long_wrapper"]
95+
caption_text_class = @class["caption_text"]
96+
97+
assert Utils.str_occurence(converted, long_wrapper_class) == 1
98+
assert Utils.str_occurence(converted, "</blockquote>") == 1
99+
assert Utils.str_occurence(converted, caption_text_class) == 0
100+
end
101+
102+
# @editor_json %{
103+
# "time" => 1_567_250_876_713,
104+
# "blocks" => [
105+
# %{
106+
# "type" => "paragraph",
107+
# "data" => %{
108+
# "text" => []
109+
# }
110+
# }
111+
# ],
112+
# "version" => "2.15.0"
113+
# }
114+
@tag :wip2
115+
test "invalid quote should have invalid hint" do
116+
editor_json = set_data("none_exsit", "long quote", "")
117+
{:ok, editor_string} = Jason.encode(editor_json)
118+
{:error, error} = Parser.to_html(editor_string)
119+
120+
assert error == [
121+
%{block: "quote", field: "mode", message: "should be: short | long"}
122+
]
123+
end
124+
end
125+
end

0 commit comments

Comments
 (0)