diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7eb45359b..c1e541d92 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: with: configFile: .commitlintrc.js - name: Set up Elixir and OTP - uses: actions/setup-elixir@v1 + uses: erlef/setup-elixir@v1 with: elixir-version: "1.10.3" # Define the elixir version [required] otp-version: "22.3" # Define the OTP version [required] diff --git a/config/config.exs b/config/config.exs index 6dc3ff2cb..754ddedbb 100644 --- a/config/config.exs +++ b/config/config.exs @@ -62,6 +62,9 @@ config :groupher_server, :customization, config :groupher_server, GroupherServerWeb.Gettext, default_locale: "zh_CN", locales: ~w(en zh_CN) +config :groupher_server, :cloud_assets, + static_icon: "https://cps-oss.oss-cn-shanghai.aliyuncs.com/icons/static" + # config email services config :groupher_server, :system_emails, support_email: "coderplanets ", diff --git a/lib/helper/converter/editor_to_html/class.ex b/lib/helper/converter/editor_to_html/class.ex index 42a115529..217aa98c9 100644 --- a/lib/helper/converter/editor_to_html/class.ex +++ b/lib/helper/converter/editor_to_html/class.ex @@ -15,6 +15,7 @@ defmodule Helper.Converter.EditorToHTML.Class do "viewer" => "article-viewer-wrapper", "unknow_block" => "unknow-block", "invalid_block" => "invalid-block", + "hide" => "hide", # header "header" => %{ "wrapper" => "header-wrapper", @@ -80,6 +81,23 @@ defmodule Helper.Converter.EditorToHTML.Class do # minimap "gallery_minimap" => "gallery-minimap", "gallery_minimap_image" => "gallery-minimap-block-image" + }, + "people" => %{ + "wrapper" => "people-wrapper", + # gallery + "gallery_wrapper" => "gallery-wrapper", + "gallery_previewer_wrapper" => "gallery-previewer-wrapper", + "gallery_previewer_item" => "gallery-previewer-item", + "gallery_previewer_active_item" => "gallery-previewer-item-active", + "gallery_card_wrapper" => "gallery-card-wrapper", + "gallery_avatar" => "gallery-avatar", + "gallery_intro" => "gallery-intro", + "gallery_intro_title" => "gallery-intro-title", + "gallery_intro_bio" => "gallery-intro-bio", + "gallery_intro_desc" => "gallery-intro-desc", + "gallery_social_wrapper" => "gallery-social-wrapper", + "gallery_social_icon" => "gallery-social-icon" + ## social } } end diff --git a/lib/helper/converter/editor_to_html/frags/header.ex b/lib/helper/converter/editor_to_html/frags/header.ex index d2b9038b6..1acf4968d 100644 --- a/lib/helper/converter/editor_to_html/frags/header.ex +++ b/lib/helper/converter/editor_to_html/frags/header.ex @@ -14,47 +14,32 @@ defmodule Helper.Converter.EditorToHTML.Frags.Header do @spec get(T.editor_header()) :: T.html() def get(%{"eyebrowTitle" => eyebrow_title, "footerTitle" => footer_title} = data) do %{"text" => text, "level" => level} = data - - wrapper_class = @class["wrapper"] - header_class = @class["header"] - eyebrow_class = @class["eyebrow_title"] - footer_class = @class["footer_title"] - anchor_id = Utils.uid(:html, data) - ~s(
-
#{eyebrow_title}
- #{text} -
#{footer_title}
+ ~s(
+
#{eyebrow_title}
+ #{text} +
#{footer_title}
) end def get(%{"eyebrowTitle" => eyebrow_title} = data) do %{"text" => text, "level" => level} = data - - wrapper_class = @class["wrapper"] - header_class = @class["header"] - eyebrow_class = @class["eyebrow_title"] - anchor_id = Utils.uid(:html, data) - ~s(
-
#{eyebrow_title}
- #{text} + ~s(
+
#{eyebrow_title}
+ #{text}
) end def get(%{"footerTitle" => footer_title} = data) do %{"text" => text, "level" => level} = data - - wrapper_class = @class["wrapper"] - header_class = @class["header"] - footer_class = @class["footer_title"] - anchor_id = Utils.uid(:html, data) - ~s(
- #{text} -
#{footer_title}
+ + ~s(
+ #{text} +
#{footer_title}
) end diff --git a/lib/helper/converter/editor_to_html/frags/image.ex b/lib/helper/converter/editor_to_html/frags/image.ex index 75e913e8c..c82c0cfec 100644 --- a/lib/helper/converter/editor_to_html/frags/image.ex +++ b/lib/helper/converter/editor_to_html/frags/image.ex @@ -23,12 +23,11 @@ defmodule Helper.Converter.EditorToHTML.Frags.Image do when g_none_empty_str(width) and g_none_empty_str(height) do caption = get_caption(data) - image_wrapper_class = @class["single_image_wrapper"] - image_class = @class["single_image"] - - ~s(
+ ~s() end @@ -36,12 +35,9 @@ defmodule Helper.Converter.EditorToHTML.Frags.Image do def get_item(:single, %{"src" => src} = data) do caption = get_caption(data) - image_wrapper_class = @class["single_image_wrapper"] - image_class = @class["single_image"] - - ~s(
+ ~s() end @@ -50,12 +46,9 @@ defmodule Helper.Converter.EditorToHTML.Frags.Image do caption = get_caption(data) # image_wrapper_class = @class["jiugongge-image"] - jiugongge_image_block_class = @class["jiugongge_image_block"] - image_class = @class["jiugongge_image"] - - ~s(
+ ~s() end @@ -63,62 +56,36 @@ defmodule Helper.Converter.EditorToHTML.Frags.Image do def get_item(:gallery, %{"src" => src, "index" => index} = data) do caption = get_caption(data) - gallery_image_block_class = @class["gallery_image_block"] - image_class = @class["gallery_image"] - # IO.inspect(index, label: "index -> ") - ~s(
+ ~s() end @spec get_minimap([T.editor_image_item()]) :: T.html() def get_minimap(items) do - wrapper_class = @class["gallery_minimap"] - items_content = Enum.reduce(items, "", fn item, acc -> acc <> frag(:minimap_image, item) end) - ~s(
+ ~s(
#{items_content}
) end defp frag(:minimap_image, %{"src" => src, "index" => index}) do - image_class = @class["gallery_minimap_image"] - - ~s() + ~s() end def get_caption(%{"caption" => caption}) when g_none_empty_str(caption), do: caption def get_caption(_), do: "" def get_caption(:html, %{"caption" => caption}) when g_none_empty_str(caption) do - image_caption = @class["image_caption"] - ~s(
#{caption}
) + ~s(
#{caption}
) end def get_caption(:html, _), do: "" - - # @spec frag(:checkbox, :text, String.t()) :: T.html() - # def frag(:checkbox, :text, text) do - # text_class = @class["checklist_text"] - - # ~s(
- # #{text} - #
) - # end - - # defp svg(type) do - # # workarround for https://github.com/rrrene/html_sanitize_ex/issues/48 - # svg_frag(type) |> String.replace(" viewBox=\"", " viewbox=\"") - # end - - # defp svg_frag(:checked) do - # ~s() - # end end diff --git a/lib/helper/converter/editor_to_html/frags/list.ex b/lib/helper/converter/editor_to_html/frags/list.ex index 645c1e597..64c1e2f48 100644 --- a/lib/helper/converter/editor_to_html/frags/list.ex +++ b/lib/helper/converter/editor_to_html/frags/list.ex @@ -24,10 +24,9 @@ defmodule Helper.Converter.EditorToHTML.Frags.List do label_frag = if hide_label, do: "", else: frag(:label, label_type, indent, label) text_frag = frag(:text, text) - item_class = @class["list_item"] indent_class = @class["indent_#{indent}"] - ~s(
+ ~s(
#{prefix_frag} #{label_frag} #{text_frag} @@ -49,10 +48,9 @@ defmodule Helper.Converter.EditorToHTML.Frags.List do label_frag = if hide_label, do: "", else: frag(:label, label_type, indent, label) text_frag = frag(:text, text) - item_class = @class["list_item"] indent_class = @class["indent_#{indent}"] - ~s(
+ ~s(
#{prefix_frag} #{label_frag} #{text_frag} @@ -75,10 +73,9 @@ defmodule Helper.Converter.EditorToHTML.Frags.List do label_frag = if hide_label, do: "", else: frag(:label, label_type, indent, label) text_frag = frag(:checkbox, :text, text) - item_class = @class["checklist_item"] indent_class = @class["indent_#{indent}"] - ~s(
+ ~s(
#{checkbox_frag} #{label_frag} #{text_frag} @@ -87,38 +84,30 @@ defmodule Helper.Converter.EditorToHTML.Frags.List do @spec frag(:label, T.editor_list_label_type(), T.editor_list_indent(), String.t()) :: T.html() def frag(:label, label_type, indent, label) do - label_class = @class["label"] label_type_class = @class["label__#{label_type}"] - ~s(
+ ~s(
#{label}
) end @spec frag(:unorder_list_prefix) :: T.html() def frag(:unorder_list_prefix) do - unorder_list_prefix_class = @class["unorder_list_prefix"] - - ~s(
) + ~s(
) end @spec frag(:order_list_prefix, String.t()) :: T.html() def frag(:order_list_prefix, prefix_index) when is_binary(prefix_index) do - order_list_prefix_class = @class["order_list_prefix"] - - ~s(
#{prefix_index}
) + ~s(
#{prefix_index}
) end @spec frag(:checkbox, Boolean.t()) :: T.html() def frag(:checkbox, checked) when is_boolean(checked) do checked_svg = svg(:checked) - - checkbox_class = @class["checklist_checkbox"] checkbox_checked_class = if checked, do: @class["checklist_checkbox_checked"], else: "" - checkbox_checksign_class = @class["checklist_checksign"] - ~s(
-
+ ~s(
+
#{checked_svg}
) @@ -126,18 +115,14 @@ defmodule Helper.Converter.EditorToHTML.Frags.List do @spec frag(:text, String.t()) :: T.html() def frag(:text, text) when is_binary(text) do - text_class = @class["text"] - - ~s(
+ ~s(
#{text}
) end @spec frag(:checkbox, :text, String.t()) :: T.html() def frag(:checkbox, :text, text) do - text_class = @class["checklist_text"] - - ~s(
+ ~s(
#{text}
) end diff --git a/lib/helper/converter/editor_to_html/frags/people.ex b/lib/helper/converter/editor_to_html/frags/people.ex new file mode 100644 index 000000000..bbd7c1ba0 --- /dev/null +++ b/lib/helper/converter/editor_to_html/frags/people.ex @@ -0,0 +1,95 @@ +defmodule Helper.Converter.EditorToHTML.Frags.People do + @moduledoc """ + parse editor.js's block fragments, use for test too + + see https://editorjs.io/ + """ + import Helper.Utils, only: [get_config: 2] + + alias Helper.Converter.EditorToHTML.Class + alias Helper.Types, as: T + + @static_icon get_config(:cloud_assets, :static_icon) + + @class get_in(Class.article(), ["people"]) + @class_hide get_in(Class.article(), ["hide"]) + + def get_previewer(:gallery, items) when length(items) > 1 do + previewer_content = + Enum.reduce(items, "", fn item, acc -> + acc <> frag(:previewer_item, item, acc) + end) + + ~s(
+ #{previewer_content} +
+ ) + end + + # if list item < 2 then return empty string, means no previewer + def get_previewer(:gallery, _items), do: "" + + @doc """ + render every card, use display block/none to switch between them + """ + @spec get_card(:gallery, [T.editor_people_item()]) :: T.html() + def get_card(:gallery, items) do + Enum.reduce(items, "", fn item, acc -> acc <> frag(:card, item, acc) end) + end + + @spec frag(:card, T.editor_people_item(), String.t()) :: T.html() + defp frag(:card, item, acc) do + # hide all by default except first + gallery_card_wrapper_class = + if byte_size(acc) == 0, + do: @class["gallery_card_wrapper"], + else: @class["gallery_card_wrapper"] <> " " <> @class_hide + + social_content = + Enum.reduce(item["socials"], "", fn item, acc -> acc <> frag(:social, item) end) + + ~s(
+
+ +
+
+
+ #{item["title"]} +
+
+ #{item["bio"]} +
+
+ #{item["desc"]} +
+
+ #{social_content} +
+
+
) + end + + @spec frag(:previewer_item, T.editor_people_item(), T.string()) :: T.html() + defp frag(:previewer_item, item, acc) do + avatar = item["avatar"] + active_class = if byte_size(acc) == 0, do: @class["gallery_previewer_active_item"], else: "" + id = item["id"] + + ~s(
+ +
) + end + + @spec frag(:social, T.editor_social_item()) :: T.html() + defp frag(:social, %{"name" => name, "link" => link}) do + icon_cdn = "#{@static_icon}/social/" + + ~s() + end +end diff --git a/lib/helper/converter/editor_to_html/frags/quote.ex b/lib/helper/converter/editor_to_html/frags/quote.ex index f49c62a2e..0309350ab 100644 --- a/lib/helper/converter/editor_to_html/frags/quote.ex +++ b/lib/helper/converter/editor_to_html/frags/quote.ex @@ -14,49 +14,35 @@ defmodule Helper.Converter.EditorToHTML.Frags.Quote do @spec get(T.editor_quote()) :: T.html() def get(%{"mode" => "short", "text" => text} = data) do - wrapper_class = @class["short_wrapper"] - text_class = @class["text"] - anchor_id = Utils.uid(:html, data) - ~s(
-
#{text}
+ ~s(
+
#{text}
) end def get(%{"mode" => "long", "text" => text, "caption" => caption} = data) when g_none_empty_str(caption) do - wrapper_class = @class["long_wrapper"] - text_class = @class["text"] - - caption = frag(:caption, caption) - + caption_content = frag(:caption, caption) anchor_id = Utils.uid(:html, data) - ~s(
-
#{text}
- #{caption} + ~s(
+
#{text}
+ #{caption_content}
) end def get(%{"mode" => "long", "text" => text}) do - wrapper_class = @class["long_wrapper"] - text_class = @class["text"] - - ~s(
-
#{text}
+ ~s(
+
#{text}
) end @spec frag(:caption, String.t()) :: T.html() def frag(:caption, caption) do - caption_class = @class["caption"] - caption_line_class = @class["caption_line"] - caption_text_class = @class["caption_text"] - - ~s(
-
-
#{caption}
+ ~s(
+
+
#{caption}
) end end diff --git a/lib/helper/converter/editor_to_html/frags/table.ex b/lib/helper/converter/editor_to_html/frags/table.ex index 5ad5627c1..14e049826 100644 --- a/lib/helper/converter/editor_to_html/frags/table.ex +++ b/lib/helper/converter/editor_to_html/frags/table.ex @@ -28,7 +28,6 @@ defmodule Helper.Converter.EditorToHTML.Frags.Table do "text" => text } = item - cell_class = @class["cell"] align_class = get_align_class(align) scripe_class = if is_stripe, do: @class["td_stripe"], else: "" @@ -36,24 +35,21 @@ defmodule Helper.Converter.EditorToHTML.Frags.Table do true -> style = ~s(width: #{Map.get(item, "width")}) - ~s(
#{ - text - }
) + ~s(
#{text}
) false -> - ~s(
#{text}
) + ~s(
#{text}
) end end @spec frag(:th, T.editor_table_cell()) :: T.html() def frag(:th, item) do %{"align" => align, "text" => text} = item - - cell_class = @class["cell"] align_class = get_align_class(align) - header_class = @class["th_header"] - ~s(
#{text}
) + ~s(
#{text}
) end defp get_align_class("center"), do: @class["align_center"] diff --git a/lib/helper/converter/editor_to_html/frontend_test/script.js b/lib/helper/converter/editor_to_html/frontend_test/script.js index 092e34dc6..d8853d911 100644 --- a/lib/helper/converter/editor_to_html/frontend_test/script.js +++ b/lib/helper/converter/editor_to_html/frontend_test/script.js @@ -1,7 +1,10 @@ content = - // image - // gallery - '
\n \n \n
'; + // people + "
\n
\n
\n
\n \n
\n \n
\n
\n \n
\n
\n \n
\n
\n
\n title\n
\n
\n this is a X man\n
\n
\n hello world i am x man\n
\n
\n \n
\n
\n
\n
\n \n
\n
\n
\n title2\n
\n
\n this is a X man2\n
\n
\n hello world i am x man2\n
\n
\n \n
\n
\n
\n
\n
" + +// image +// gallery +// '
\n \n \n
'; // jiugongge // '
\n
\n
\n \n image\n \n
\n \n image\n \n
\n \n image\n \n
\n \n image\n \n
\n \n image\n \n
\n
\n
'; @@ -24,8 +27,60 @@ content = const articleEl = document.getElementById("article"); articleEl.innerHTML = content; +// utils +const hideClass = "hide"; + +/** + * toggleClass + * + * @memberof utils + * @param {HTMLElement} element + * @param {HTMLElement[]} elementList + * @param {string} className + */ +const toggleClass = (element, elementList, className) => { + for (let i = 0; i < elementList.length; i++) { + const el = elementList[i]; + el.classList.remove(className); + } -// for images + element.classList.add(className); +}; + +/** + * toggleElement display + * + * @memberof utils + * @param {HTMLElement} element + * @param {HTMLElement[]} elementList + * @param {attr} "block" | "flex" + */ +const toggleElement = (element, elementList, attr = "block") => { + _hideAll(elementList); + _showEl(element, attr); +}; + +/** + * hide all elements + * @param {HTMLElement[]} elementList + */ +const _hideAll = (elementList) => { + for (let i = 0; i < elementList.length; i++) { + const el = elementList[i]; + if (!el.classList.contains(hideClass)) { + el.classList.add(hideClass); + } + } +}; + +const _showEl = (el, attr = "block") => { + el.classList.remove(hideClass); + el.style.display = attr; +}; + +// utils end + +// image block const lightbox = GLightbox({ loop: true }); const galleryMiniImageClass = ".gallery-minimap-block-image"; @@ -52,3 +107,38 @@ if (minimapImageEls.length > 0) { }); } } + +// people block +const peoplePreviewerClass = "gallery-previewer-item"; +const peoplePreviewerItemActiveClass = "gallery-previewer-item-active"; +const peopleCardClass = "gallery-card-wrapper"; + +const peoplePreviewerEls = document.querySelectorAll( + "." + peoplePreviewerClass +); + +if (peoplePreviewerEls.length > 0) { + for (let i = 0; i < peoplePreviewerEls.length; i++) { + const previewerEl = peoplePreviewerEls[i]; + previewerEl.addEventListener("click", function () { + toggleClass( + previewerEl, + peoplePreviewerEls, + peoplePreviewerItemActiveClass + ); + + const activeIndex = previewerEl.dataset.index; + const WrapperEl = previewerEl.parentElement.parentElement; + + const ActiveCardEl = WrapperEl.querySelector( + `.${peopleCardClass}[data-index="${activeIndex}"]` + ); + + const CardsElementsList = WrapperEl.querySelectorAll( + "." + peopleCardClass + ); + + toggleElement(ActiveCardEl, CardsElementsList, "flex"); + }); + } +} diff --git a/lib/helper/converter/editor_to_html/frontend_test/styles.css b/lib/helper/converter/editor_to_html/frontend_test/styles.css index 81d92e3fc..ab33f6328 100644 --- a/lib/helper/converter/editor_to_html/frontend_test/styles.css +++ b/lib/helper/converter/editor_to_html/frontend_test/styles.css @@ -7,6 +7,11 @@ body { #article { width: 650px; border: 1px solid #ececec; + padding-top: 100px; +} + +.hide { + display: none !important; } .glightbox-clean .gslide-description { @@ -500,3 +505,195 @@ td { } /* image block end */ + +/* people block */ +.article-viewer-wrapper .people-wrapper { + padding: 10px 0; +} +.article-viewer-wrapper .people-wrapper .gallery-wrapper { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding-top: 20px; + padding-bottom: 25px; + margin: 0 60px; +} + +.article-viewer-wrapper .people-wrapper .gallery-previewer-wrapper { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: center; + width: 100%; + margin-bottom: 32px; +} + +.article-viewer-wrapper + .people-wrapper + .gallery-previewer-wrapper + .gallery-previewer-item { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + opacity: 0.6; + cursor: pointer; + margin-left: 16px; + transition: opacity 0.25s; +} + +.article-viewer-wrapper + .people-wrapper + .gallery-previewer-wrapper + .gallery-previewer-item-active { + opacity: 1; + cursor: default; + position: relative; +} + +.article-viewer-wrapper + .people-wrapper + .gallery-previewer-wrapper + .gallery-previewer-item-active::before { + content: ""; + width: 20px; + height: 3px; + background: #76c5e4; + position: absolute; + top: -12px; + left: 7px; + border-radius: 3px; +} + +.article-viewer-wrapper + .people-wrapper + .gallery-previewer-wrapper + .gallery-previewer-item + img { + height: 35px; + width: 35px; + border-radius: 50%; + display: block; +} + +.article-viewer-wrapper .people-wrapper .gallery-card-wrapper { + display: flex; + justify-content: center; + align-items: flex-start; + width: 100%; +} + +.article-viewer-wrapper .people-wrapper .gallery-card-wrapper .gallery-avatar { + width: 200px; + border-radius: 100%; + z-index: 1; +} + +.article-viewer-wrapper + .people-wrapper + .gallery-card-wrapper + .gallery-avatar + img { + width: 120px; + height: 120px; + margin-top: 4px; + border-radius: 100%; +} + +.article-viewer-wrapper .people-wrapper .gallery-card-wrapper .gallery-intro { + display: flex; + flex-direction: column; + width: 100%; + padding-right: 15px; + border-radius: 20px; +} + +.article-viewer-wrapper + .people-wrapper + .gallery-card-wrapper + .gallery-intro + .gallery-intro-title { + font-size: 18px; + background-color: transparent; + font-weight: bold; + color: #5f5f5f; + border: none; +} + +.article-viewer-wrapper + .people-wrapper + .gallery-card-wrapper + .gallery-intro + .gallery-intro-bio { + font-size: 15px; + background-color: transparent; + color: #a2a2a2; + margin-top: 4px; + margin-bottom: 10px; +} + +.article-viewer-wrapper + .people-wrapper + .gallery-card-wrapper + .gallery-intro + .gallery-intro-desc { + color: #5f5f5f; + font-size: 14px; + padding-left: 2px; +} + +.article-viewer-wrapper + .people-wrapper + .gallery-card-wrapper + .gallery-social-wrapper { + display: flex; + align-items: center; + margin-top: 14px; +} + +.article-viewer-wrapper + .people-wrapper + .gallery-card-wrapper + .gallery-social-wrapper + .gallery-social-icon { + display: flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + margin-right: 10px; +} +.article-viewer-wrapper + .people-wrapper + .gallery-card-wrapper + .gallery-social-wrapper + .gallery-social-icon + a { + opacity: 0.8; + transition: all 0.25s; +} + +.article-viewer-wrapper + .people-wrapper + .gallery-card-wrapper + .gallery-social-wrapper + .gallery-social-icon + a:hover { + opacity: 1; + cursor: pointer; +} + +.article-viewer-wrapper + .people-wrapper + .gallery-card-wrapper + .gallery-social-wrapper + .gallery-social-icon + svg, +image { + fill: #4f575d; + width: 15px; + height: 15px; + transition: all 0.25s; +} +/* people block end */ diff --git a/lib/helper/converter/editor_to_html/index.ex b/lib/helper/converter/editor_to_html/index.ex index 4a370e28f..d2aa75fa5 100644 --- a/lib/helper/converter/editor_to_html/index.ex +++ b/lib/helper/converter/editor_to_html/index.ex @@ -145,6 +145,28 @@ defmodule Helper.Converter.EditorToHTML do
) end + defp parse_block(%{"type" => "people", "data" => %{"mode" => "gallery"} = data}) do + %{"items" => items} = data + + # set id to each people for switch them + items = Enum.map(items, fn item -> Map.merge(item, %{"id" => Utils.uid(:html, item)}) end) + + wrapper_class = get_in(@root_class, ["people", "wrapper"]) + gallery_wrapper_class = get_in(@root_class, ["people", "gallery_wrapper"]) + + previewer_content = Frags.People.get_previewer(:gallery, items) + card_content = Frags.People.get_card(:gallery, items) + + anchor_id = Utils.uid(:html, data) + + ~s(
+
+ #{previewer_content} + #{card_content} +
+
) + end + defp parse_block(%{"type" => "code", "data" => data}) do text = get_in(data, ["text"]) code = text |> Phoenix.HTML.html_escape() |> Phoenix.HTML.safe_to_string() diff --git a/lib/helper/converter/editor_to_html/validator/editor_schema.ex b/lib/helper/converter/editor_to_html/validator/editor_schema.ex index 81f8a5c31..152d29e94 100644 --- a/lib/helper/converter/editor_to_html/validator/editor_schema.ex +++ b/lib/helper/converter/editor_to_html/validator/editor_schema.ex @@ -18,6 +18,9 @@ defmodule Helper.Converter.EditorToHTML.Validator.EditorSchema do # image @valid_image_mode ["single", "jiugongge", "gallery"] + # people + @valid_people_mode ["gallery"] + @spec get(String.t()) :: map | [parent: map, item: map] def get("editor") do %{ @@ -106,6 +109,23 @@ defmodule Helper.Converter.EditorToHTML.Validator.EditorSchema do ] end + def get("people") do + [ + parent: %{ + "id" => [:string, required: false], + "mode" => [enum: @valid_people_mode], + "items" => [:list, type: :map, allow_empty: false] + }, + item: %{ + "avatar" => [:string, starts_with: "https://"], + "title" => [:string, required: false], + "bio" => [:string, required: false], + "desc" => [:string, required: false], + "socials" => [:list, type: :map] + } + ] + end + def get(_) do %{} end diff --git a/lib/helper/converter/editor_to_html/validator/index.ex b/lib/helper/converter/editor_to_html/validator/index.ex index 55667f21f..617286705 100644 --- a/lib/helper/converter/editor_to_html/validator/index.ex +++ b/lib/helper/converter/editor_to_html/validator/index.ex @@ -9,7 +9,7 @@ defmodule Helper.Converter.EditorToHTML.Validator do @normal_blocks ["header", "paragraph", "quote"] # blocks with "items" fields (has many children item) - @children_blocks ["list", "table", "image"] + @children_blocks ["list", "table", "image", "people"] # all the supported blocks @supported_blocks @normal_blocks ++ @children_blocks diff --git a/lib/helper/converter/html_sanitizer.ex b/lib/helper/converter/html_sanitizer.ex index 45f9a6110..476c014e3 100644 --- a/lib/helper/converter/html_sanitizer.ex +++ b/lib/helper/converter/html_sanitizer.ex @@ -48,6 +48,8 @@ defmodule Helper.Converter.HtmlSanitizer do # blockquote Meta.allow_tag_with_these_attributes("blockquote", ["id", "class"]) + Meta.allow_tag_with_these_attributes("image", ["xlink:href"]) + Meta.allow_tag_with_these_attributes("svg", [ "t", "p-id", diff --git a/lib/helper/types.ex b/lib/helper/types.ex index d5c5c8a62..d2fc29770 100644 --- a/lib/helper/types.ex +++ b/lib/helper/types.ex @@ -85,10 +85,6 @@ defmodule Helper.Types do isHeader: Boolean.t() } - @typedoc """ - valid editor.js's image mode - """ - # @typep editor_image_mode :: :single | :jiugongge | :gallery @typedoc """ @@ -103,6 +99,24 @@ defmodule Helper.Types do } @typedoc """ + editor.js's people item + """ + @type editor_people_item :: %{ + required(:id) => String.t(), + required(:avatar) => String.t(), + required(:title) => String.t(), + required(:bio) => String.t(), + required(:desc) => String.t() + } + + @typedoc """ + editor.js's social item for any block + """ + @type editor_social_item :: %{ + required(:name) => String.t(), + required(:link) => String.t() + } + @typedoc """ html fragment """ @type html :: String.t() diff --git a/test/helper/converter/editor_to_html_test/header_test.exs b/test/helper/converter/editor_to_html_test/header_test.exs index c6f5f7716..da2398433 100644 --- a/test/helper/converter/editor_to_html_test/header_test.exs +++ b/test/helper/converter/editor_to_html_test/header_test.exs @@ -10,9 +10,6 @@ defmodule GroupherServer.Test.Helper.Converter.EditorToHTML.Header do @root_class Class.article() @class get_in(@root_class, ["header"]) - @eyebrow_class @class["eyebrow_title"] - @footer_class @class["footer_title"] - describe "[header block unit]" do defp set_data(data) do %{ @@ -80,8 +77,8 @@ defmodule GroupherServer.Test.Helper.Converter.EditorToHTML.Header do # header_class = @class["header"] assert Utils.str_occurence(converted, "id=") == 1 # assert Utils.str_occurence(converted, header_class) == 1 - assert Utils.str_occurence(converted, @eyebrow_class) == 1 - assert Utils.str_occurence(converted, @footer_class) == 1 + assert Utils.str_occurence(converted, @class["eyebrow_title"]) == 1 + assert Utils.str_occurence(converted, @class["footer_title"]) == 1 end @tag :wip @@ -113,8 +110,8 @@ defmodule GroupherServer.Test.Helper.Converter.EditorToHTML.Header do {:ok, editor_string} = Jason.encode(editor_json) {:ok, converted} = Parser.to_html(editor_string) - assert Utils.str_occurence(converted, @eyebrow_class) == 1 - assert Utils.str_occurence(converted, @footer_class) == 0 + assert Utils.str_occurence(converted, @class["eyebrow_title"]) == 1 + assert Utils.str_occurence(converted, @class["footer_title"]) == 0 editor_json = set_data(%{ @@ -126,8 +123,8 @@ defmodule GroupherServer.Test.Helper.Converter.EditorToHTML.Header do {:ok, editor_string} = Jason.encode(editor_json) {:ok, converted} = Parser.to_html(editor_string) - assert Utils.str_occurence(converted, @eyebrow_class) == 0 - assert Utils.str_occurence(converted, @footer_class) == 1 + assert Utils.str_occurence(converted, @class["eyebrow_title"]) == 0 + assert Utils.str_occurence(converted, @class["footer_title"]) == 1 end @tag :wip diff --git a/test/helper/converter/editor_to_html_test/image_test.exs b/test/helper/converter/editor_to_html_test/image_test.exs index fb46f74a6..88d0e1a50 100644 --- a/test/helper/converter/editor_to_html_test/image_test.exs +++ b/test/helper/converter/editor_to_html_test/image_test.exs @@ -2,6 +2,7 @@ defmodule GroupherServer.Test.Helper.Converter.EditorToHTML.Image do @moduledoc false use GroupherServerWeb.ConnCase, async: true + import GroupherServer.Support.Factory alias Helper.Converter.EditorToHTML, as: Parser alias Helper.Converter.EditorToHTML.Class @@ -10,18 +11,6 @@ defmodule GroupherServer.Test.Helper.Converter.EditorToHTML.Image do @root_class Class.article() @class get_in(@root_class, ["image"]) - @images [ - "https://images.unsplash.com/photo-1506034861661-ad49bbcf7198?ixlib=rb-1.2.1&ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&auto=format&fit=crop&w=1350&q=80", - "https://images.unsplash.com/photo-1614607206234-f7b56bdff6e7?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=634&q=80", - "https://images.unsplash.com/photo-1614526261139-1e5ebbd5086c?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=1350&q=80" - # "https://images.unsplash.com/photo-1614366559478-edf9d1cc4719?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=634&q=80", - # "https://images.unsplash.com/photo-1614588108027-22a021c8d8e1?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=1349&q=80" - # "https://images.unsplash.com/photo-1614522407266-ad3c5fa6bc24?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=1352&q=80", - # "https://images.unsplash.com/photo-1601933470096-0e34634ffcde?ixid=MXwxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=1350&q=80", - # "https://images.unsplash.com/photo-1614598943918-3d0f1e65c22c?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=1350&q=80", - # "https://images.unsplash.com/photo-1614542530265-7a46ededfd64?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=634&q=80" - ] - describe "[image block unit]" do defp set_items(mode, items, id \\ "") do %{ @@ -40,13 +29,13 @@ defmodule GroupherServer.Test.Helper.Converter.EditorToHTML.Image do } end - @tag :wip + @tag :wip2 test "single image parse should work" do editor_json = set_items("single", [ %{ "index" => 0, - "src" => @images |> List.first(), + "src" => mock_image(), "caption" => "this is a caption", "width" => "368px", "height" => "552px" @@ -69,13 +58,13 @@ defmodule GroupherServer.Test.Helper.Converter.EditorToHTML.Image do assert Utils.str_occurence(converted, "this is a caption") == 2 end - @tag :wip + @tag :wip2 test "single image parse should work without wight && height" do editor_json = set_items("single", [ %{ "index" => 0, - "src" => @images |> List.first(), + "src" => mock_image(), "caption" => "this is a caption" } ]) @@ -92,13 +81,13 @@ defmodule GroupherServer.Test.Helper.Converter.EditorToHTML.Image do assert Utils.str_occurence(converted, single_image_wrapper_class) == 1 end - @tag :wip + @tag :wip2 test "single image parse should work without caption" do editor_json = set_items("single", [ %{ "index" => 0, - "src" => @images |> List.first() + "src" => mock_image() } ]) @@ -117,12 +106,12 @@ defmodule GroupherServer.Test.Helper.Converter.EditorToHTML.Image do assert Utils.str_occurence(converted, image_caption_class) == 0 end - @tag :wip + @tag :wip2 test "jiugongge image parse should work" do editor_json = set_items( "jiugongge", - @images + mock_images(9) |> Enum.with_index() |> Enum.map(fn {src, index} -> %{ @@ -140,15 +129,15 @@ defmodule GroupherServer.Test.Helper.Converter.EditorToHTML.Image do jiugongge_image_class = @class["jiugongge_image"] assert Utils.str_occurence(converted, jiugongge_image_wrapper_class) == 1 - assert Utils.str_occurence(converted, jiugongge_image_class) == length(@images) + assert Utils.str_occurence(converted, jiugongge_image_class) == length(mock_images(9)) end - @tag :wip + @tag :wip2 test "gallery image parse should work" do editor_json = set_items( "gallery", - @images + mock_images(9) |> Enum.with_index() |> Enum.map(fn {src, index} -> %{ @@ -165,16 +154,16 @@ defmodule GroupherServer.Test.Helper.Converter.EditorToHTML.Image do gallery_image_class = @class["gallery_image"] gallery_mini_image_class = @class["gallery_image"] - assert Utils.str_occurence(converted, gallery_image_class) == length(@images) - assert Utils.str_occurence(converted, gallery_mini_image_class) == length(@images) + assert Utils.str_occurence(converted, gallery_image_class) == length(mock_images(9)) + assert Utils.str_occurence(converted, gallery_mini_image_class) == length(mock_images(9)) end - @tag :wip + @tag :wip2 test "edit exsit block will not change id value" do editor_json = set_items( "gallery", - @images + mock_images(9) |> Enum.with_index() |> Enum.map(fn {src, index} -> %{ diff --git a/test/helper/converter/editor_to_html_test/index_test.exs b/test/helper/converter/editor_to_html_test/index_test.exs index c0ededa4b..417579257 100644 --- a/test/helper/converter/editor_to_html_test/index_test.exs +++ b/test/helper/converter/editor_to_html_test/index_test.exs @@ -132,10 +132,8 @@ defmodule GroupherServer.Test.Helper.Converter.EditorToHTML do {:ok, editor_string} = Jason.encode(editor_json) {:ok, converted} = Parser.to_html(editor_string) - viewer_class = @root_class["viewer"] - assert converted == - ~s(

evel script

) + ~s(

evel script

) editor_json = %{ "time" => 1_567_250_876_713, @@ -153,10 +151,8 @@ defmodule GroupherServer.Test.Helper.Converter.EditorToHTML do {:ok, editor_string} = Jason.encode(editor_json) {:ok, converted} = Parser.to_html(editor_string) - viewer_class = @root_class["viewer"] - assert converted == - ~s(

Editor.js is an element <script>evel script</script>

) + ~s(

Editor.js is an element <script>evel script</script>

) end end end diff --git a/test/helper/converter/editor_to_html_test/list_test.exs b/test/helper/converter/editor_to_html_test/list_test.exs index 476e2e64a..88938e54c 100644 --- a/test/helper/converter/editor_to_html_test/list_test.exs +++ b/test/helper/converter/editor_to_html_test/list_test.exs @@ -61,8 +61,7 @@ defmodule GroupherServer.Test.Helper.Converter.EditorToHTML.List do {:ok, editor_string} = Jason.encode(editor_json) {:ok, converted} = Parser.to_html(editor_string) - unorder_list_prefix_class = @class["unorder_list_prefix"] - assert Utils.str_occurence(converted, unorder_list_prefix_class) == 3 + assert Utils.str_occurence(converted, @class["unorder_list_prefix"]) == 3 end @tag :wip @@ -104,8 +103,7 @@ defmodule GroupherServer.Test.Helper.Converter.EditorToHTML.List do assert Utils.str_occurence(converted, "id=") == 1 - order_list_prefix_class = @class["order_list_prefix"] - assert Utils.str_occurence(converted, order_list_prefix_class) == 3 + assert Utils.str_occurence(converted, @class["order_list_prefix"]) == 3 end @tag :wip @@ -176,8 +174,7 @@ defmodule GroupherServer.Test.Helper.Converter.EditorToHTML.List do {:ok, editor_string} = Jason.encode(editor_json) {:ok, converted} = Parser.to_html(editor_string) - checked_class = @class["checklist_checkbox_checked"] - assert Utils.str_occurence(converted, checked_class) == 1 + assert Utils.str_occurence(converted, @class["checklist_checkbox_checked"]) == 1 end @tag :wip @@ -205,8 +202,7 @@ defmodule GroupherServer.Test.Helper.Converter.EditorToHTML.List do {:ok, editor_string} = Jason.encode(editor_json) {:ok, converted} = Parser.to_html(editor_string) - label_class = @class["label"] - assert Utils.str_occurence(converted, label_class) == 0 + assert Utils.str_occurence(converted, @class["label"]) == 0 end @tag :wip diff --git a/test/helper/converter/editor_to_html_test/paragraph_test.exs b/test/helper/converter/editor_to_html_test/paragraph_test.exs index 618306cd6..d5f2a4364 100644 --- a/test/helper/converter/editor_to_html_test/paragraph_test.exs +++ b/test/helper/converter/editor_to_html_test/paragraph_test.exs @@ -26,8 +26,7 @@ defmodule GroupherServer.Test.Helper.Converter.EditorToHTML.Paragraph do {:ok, editor_string} = Jason.encode(@editor_json) {:ok, converted} = Parser.to_html(editor_string) - viewer_class = @root_class["viewer"] - assert converted == ~s(

paragraph content

) + assert converted == ~s(

paragraph content

) end @editor_json %{ diff --git a/test/helper/converter/editor_to_html_test/people_test.exs b/test/helper/converter/editor_to_html_test/people_test.exs new file mode 100644 index 000000000..e9c90cc6f --- /dev/null +++ b/test/helper/converter/editor_to_html_test/people_test.exs @@ -0,0 +1,119 @@ +defmodule GroupherServer.Test.Helper.Converter.EditorToHTML.People do + @moduledoc false + + use GroupherServerWeb.ConnCase, async: true + import GroupherServer.Support.Factory + + alias Helper.Converter.EditorToHTML, as: Parser + alias Helper.Converter.EditorToHTML.Class + alias Helper.Utils + + @root_class Class.article() + @class get_in(@root_class, ["people"]) + + describe "[people block unit]" do + defp set_items(mode, items, id \\ "") do + %{ + "time" => 1_567_250_876_713, + "blocks" => [ + %{ + "type" => "people", + "data" => %{ + "id" => id, + "mode" => mode, + "items" => items + } + } + ], + "version" => "2.15.0" + } + end + + @tag :wip2 + test "multi people should have previewer" do + editor_json = + set_items("gallery", [ + %{ + "avatar" => mock_image(), + "title" => "title", + "bio" => "this is a X man", + "desc" => "hello world i am x man", + "socials" => [ + %{ + "name" => "zhihu", + "link" => "https://link" + }, + %{ + "name" => "twitter", + "link" => "https://link" + } + ] + }, + %{ + "avatar" => mock_image(), + "title" => "title2", + "bio" => "this is a X man2", + "desc" => "hello world i am x man2", + "socials" => [ + %{ + "name" => "github", + "link" => "https://link" + }, + %{ + "name" => "twitter", + "link" => "https://link" + } + ] + } + ]) + + {:ok, editor_string} = Jason.encode(editor_json) + {:ok, converted} = Parser.to_html(editor_string) + + assert Utils.str_occurence(converted, @class["gallery_previewer_wrapper"]) == 1 + assert Utils.str_occurence(converted, @class["gallery_previewer_item"]) == 3 + assert Utils.str_occurence(converted, @class["gallery_previewer_active_item"]) == 1 + assert Utils.str_occurence(converted, @class["gallery_card_wrapper"]) == 2 + + assert Utils.str_occurence(converted, mock_image()) == 4 + end + + @tag :wip2 + test "one people should not have previewer bar" do + editor_json = + set_items("gallery", [ + %{ + "avatar" => mock_image(), + "title" => "title", + "bio" => "this is a X man", + "desc" => "hello world i am x man", + "socials" => [ + %{ + "name" => "zhihu", + "link" => "https://link" + }, + %{ + "name" => "twitter", + "link" => "https://link" + } + ] + } + ]) + + {:ok, editor_string} = Jason.encode(editor_json) + {:ok, converted} = Parser.to_html(editor_string) + + assert Utils.str_occurence(converted, @class["gallery_previewer_wrapper"]) == 0 + assert Utils.str_occurence(converted, @class["gallery_card_wrapper"]) == 1 + assert Utils.str_occurence(converted, @class["gallery_avatar"]) == 1 + + assert Utils.str_occurence(converted, @class["gallery_intro_title"]) == 1 + assert Utils.str_occurence(converted, @class["gallery_intro_bio"]) == 1 + assert Utils.str_occurence(converted, @class["gallery_intro_desc"]) == 1 + + assert Utils.str_occurence(converted, @class["gallery_social_icon"]) == 2 + + assert Utils.str_occurence(converted, mock_image()) == 1 + end + end +end diff --git a/test/helper/converter/editor_to_html_test/quote_test.exs b/test/helper/converter/editor_to_html_test/quote_test.exs index 03e87203c..467871f5a 100644 --- a/test/helper/converter/editor_to_html_test/quote_test.exs +++ b/test/helper/converter/editor_to_html_test/quote_test.exs @@ -49,12 +49,9 @@ defmodule GroupherServer.Test.Helper.Converter.EditorToHTML.Quote do assert Utils.str_occurence(converted, "id=") == 1 - short_wrapper_class = @class["short_wrapper"] - caption_class = @class["caption"] - - assert Utils.str_occurence(converted, short_wrapper_class) == 1 + assert Utils.str_occurence(converted, @class["short_wrapper"]) == 1 assert Utils.str_occurence(converted, "
") == 1 - assert Utils.str_occurence(converted, caption_class) == 0 + assert Utils.str_occurence(converted, @class["caption"]) == 0 end @tag :wip @@ -63,42 +60,31 @@ defmodule GroupherServer.Test.Helper.Converter.EditorToHTML.Quote do {:ok, editor_string} = Jason.encode(editor_json) {:ok, converted} = Parser.to_html(editor_string) - long_wrapper_class = @class["long_wrapper"] - caption_text_class = @class["caption_text"] - - assert Utils.str_occurence(converted, long_wrapper_class) == 1 + assert Utils.str_occurence(converted, @class["long_wrapper"]) == 1 assert Utils.str_occurence(converted, "
") == 1 - assert Utils.str_occurence(converted, caption_text_class) == 1 + assert Utils.str_occurence(converted, @class["caption_text"]) == 1 end @tag :wip test "long quote without caption parse should work" do editor_json = set_data("long", "long quote") {:ok, editor_string} = Jason.encode(editor_json) - {:ok, converted} = Parser.to_html(editor_string) - long_wrapper_class = @class["long_wrapper"] - caption_text_class = @class["caption_text"] - - assert Utils.str_occurence(converted, long_wrapper_class) == 1 + assert Utils.str_occurence(converted, @class["long_wrapper"]) == 1 assert Utils.str_occurence(converted, "
") == 1 - assert Utils.str_occurence(converted, caption_text_class) == 0 + assert Utils.str_occurence(converted, @class["caption_text"]) == 0 end @tag :wip test "long quote without empty caption parse should work" do editor_json = set_data("long", "long quote", "") {:ok, editor_string} = Jason.encode(editor_json) - {:ok, converted} = Parser.to_html(editor_string) - long_wrapper_class = @class["long_wrapper"] - caption_text_class = @class["caption_text"] - - assert Utils.str_occurence(converted, long_wrapper_class) == 1 + assert Utils.str_occurence(converted, @class["long_wrapper"]) == 1 assert Utils.str_occurence(converted, "") == 1 - assert Utils.str_occurence(converted, caption_text_class) == 0 + assert Utils.str_occurence(converted, @class["caption_text"]) == 0 end @tag :wip diff --git a/test/helper/converter/editor_to_html_test/table_test.exs b/test/helper/converter/editor_to_html_test/table_test.exs index c6139a352..8596190f6 100644 --- a/test/helper/converter/editor_to_html_test/table_test.exs +++ b/test/helper/converter/editor_to_html_test/table_test.exs @@ -100,11 +100,8 @@ defmodule GroupherServer.Test.Helper.Converter.EditorToHTML.Table do assert Utils.str_occurence(converted, "id=") == 1 - th_header_class = @class["th_header"] - td_stripe_class = @class["td_stripe"] - - assert Utils.str_occurence(converted, th_header_class) == 4 - assert Utils.str_occurence(converted, td_stripe_class) == 3 + assert Utils.str_occurence(converted, @class["th_header"]) == 4 + assert Utils.str_occurence(converted, @class["td_stripe"]) == 3 end @tag :wip diff --git a/test/helper/validator/schema_test.exs b/test/helper/validator/schema_test.exs index 39ce8a5e6..ee63fc3f7 100644 --- a/test/helper/validator/schema_test.exs +++ b/test/helper/validator/schema_test.exs @@ -6,7 +6,7 @@ defmodule GroupherServer.Test.Helper.Validator.Schema do alias Helper.Validator.Schema describe "[basic schema]" do - @tag :wip2 + @tag :wip test "string with options" do schema = %{"text" => [:string, required: false]} data = %{"no_exsit" => "text"} @@ -122,7 +122,7 @@ defmodule GroupherServer.Test.Helper.Validator.Schema do assert error == [%{field: "text", message: "should be: number", value: "aa"}] end - @tag :wip2 + @tag :wip test "list with options" do schema = %{"text" => [:list, required: false]} data = %{"no_exsit" => []} diff --git a/test/support/factory.ex b/test/support/factory.ex index d738cec88..a2435c1e0 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -403,4 +403,30 @@ defmodule GroupherServer.Support.Factory do {:ok, _} = Delivery.notify_someone(u, user, info) end) end + + @images [ + "https://rmt.dogedoge.com/fetch/~/source/unsplash/photo-1557555187-23d685287bc3?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=1000&q=80", + "https://rmt.dogedoge.com/fetch/~/source/unsplash/photo-1484399172022-72a90b12e3c1?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=1000&q=80", + "https://images.unsplash.com/photo-1506034861661-ad49bbcf7198?ixlib=rb-1.2.1&ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&auto=format&fit=crop&w=1350&q=80", + "https://images.unsplash.com/photo-1614607206234-f7b56bdff6e7?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=634&q=80", + "https://images.unsplash.com/photo-1614526261139-1e5ebbd5086c?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=1350&q=80", + "https://images.unsplash.com/photo-1614366559478-edf9d1cc4719?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=634&q=80", + "https://images.unsplash.com/photo-1614588108027-22a021c8d8e1?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=1349&q=80", + "https://images.unsplash.com/photo-1614522407266-ad3c5fa6bc24?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=1352&q=80", + "https://images.unsplash.com/photo-1601933470096-0e34634ffcde?ixid=MXwxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=1350&q=80", + "https://images.unsplash.com/photo-1614598943918-3d0f1e65c22c?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=1350&q=80", + "https://images.unsplash.com/photo-1614542530265-7a46ededfd64?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=634&q=80" + ] + + @doc "mock image" + @spec mock_image(Number.t()) :: String.t() + def mock_image(index \\ 0) do + Enum.at(@images, index) + end + + @doc "mock images" + @spec mock_images(Number.t()) :: [String.t()] + def mock_images(count \\ 1) do + @images |> Enum.slice(0, count) + end end