diff --git a/.iex.exs b/.iex.exs new file mode 100644 index 000000000..88e4c63a5 --- /dev/null +++ b/.iex.exs @@ -0,0 +1,2 @@ +# see: https://github.com/blackode/elixir-tips#loading-project-module-aliases-iexexs +alias Helper.Converter.EditorToHTML diff --git a/Makefile b/Makefile index 2d629524c..425a392f9 100644 --- a/Makefile +++ b/Makefile @@ -171,9 +171,8 @@ test.watch.wip: # test.watch not work now, see: https://github.com/lpil/mix-test.watch/issues/116 # mix test.watch --only wip --stale test.watch.wip2: - mix test --listen-on-stdin --stale --trace --only wip2 + mix test --listen-on-stdin --stale --only wip2 # mix test.watch --only wip2 - mix test.watch --only wip2 test.watch.bug: mix test.watch --only bug test.report: diff --git a/lib/helper/converter/assets/delimiter_icons.ex b/lib/helper/converter/assets/delimiter_icons.ex index 8b6b44ad3..485934230 100644 --- a/lib/helper/converter/assets/delimiter_icons.ex +++ b/lib/helper/converter/assets/delimiter_icons.ex @@ -1,4 +1,4 @@ -defmodule Helper.Converter.EditorToHtml.Assets.DelimiterIcons do +defmodule Helper.Converter.EditorToHTML.Assets.DelimiterIcons do @moduledoc """ svg icons for delimiter block NOTE: those svg should be sync with frontend svg diff --git a/lib/helper/converter/editor_to_html/header.ex b/lib/helper/converter/editor_to_html/header.ex new file mode 100644 index 000000000..b021f1f7e --- /dev/null +++ b/lib/helper/converter/editor_to_html/header.ex @@ -0,0 +1,80 @@ +defmodule Helper.Converter.EditorToHTML.Header do + @moduledoc """ + parse editor.js's header block + + see https://editorjs.io/ + """ + + # @behaviour Helper.Converter.EditorToHTML.Parser + + defmacro __using__(_opts) do + quote do + alias Helper.Metric + + @clazz Metric.Article.class_names(:html) + + defp parse_block(%{ + "type" => "header", + "data" => + %{ + "text" => text, + "level" => level, + "eyebrowTitle" => eyebrow_title, + "footerTitle" => footer_title + } = data + }) do + """ +
#{text}
" - end - - # IO.inspect(data, label: "parse image") defp parse_block(%{"type" => "image", "data" => data}) do url = get_in(data, ["file", "url"]) - "#{text}
" + end + end + end +end diff --git a/lib/helper/converter/editor_to_html/validator/editor_schema.ex b/lib/helper/converter/editor_to_html/validator/editor_schema.ex new file mode 100644 index 000000000..6c72745df --- /dev/null +++ b/lib/helper/converter/editor_to_html/validator/editor_schema.ex @@ -0,0 +1,48 @@ +defmodule Helper.Converter.EditorToHTML.Validator.EditorSchema do + @moduledoc false + + # header + @valid_header_level [1, 2, 3] + + # list + @valid_list_mode ["checklist", "order_list", "unorder_list"] + @valid_list_label_type ["success", "done", "todo"] + @valid_list_indent [0, 1, 2, 3, 4] + + def get("editor") do + %{ + "time" => [:number], + "version" => [:string], + "blocks" => [:list] + } + end + + def get("header") do + %{ + "text" => [:string], + "level" => [enum: @valid_header_level], + "eyebrowTitle" => [:string, required: false], + "footerTitle" => [:string, required: false] + } + end + + def get("paragraph"), do: %{"text" => [:string]} + + def get("list") do + [ + parent: %{"mode" => [enum: @valid_list_mode], "items" => [:list]}, + item: %{ + "checked" => [:boolean], + "hideLabel" => [:boolean], + "label" => [:string], + "labelType" => [enum: @valid_list_label_type], + "indent" => [enum: @valid_list_indent], + "text" => [:string] + } + ] + end + + def get(_) do + %{} + end +end diff --git a/lib/helper/converter/editor_to_html/validator/index.ex b/lib/helper/converter/editor_to_html/validator/index.ex new file mode 100644 index 000000000..6775fb937 --- /dev/null +++ b/lib/helper/converter/editor_to_html/validator/index.ex @@ -0,0 +1,110 @@ +defmodule Helper.Converter.EditorToHTML.Validator do + @moduledoc false + + alias Helper.{Converter, Validator} + + alias Validator.Schema + alias Converter.EditorToHTML.Validator.EditorSchema + + # blocks with no children items + @simple_blocks ["header", "paragraph"] + # blocks with "mode" and "items" fields + @complex_blocks ["list"] + + def is_valid(data) when is_map(data) do + with {:ok, _} <- validate_editor_fmt(data), + blocks <- Map.get(data, "blocks") do + try do + validate_blocks(blocks) + rescue + e in MatchError -> + format_parse_error(e) + + e in RuntimeError -> + format_parse_error(e) + + _ -> + format_parse_error() + end + end + end + + defp validate_editor_fmt(data) do + try do + validate_with("editor", EditorSchema.get("editor"), data) + rescue + e in MatchError -> + format_parse_error(e) + + _ -> + format_parse_error() + end + end + + defp validate_blocks([]), do: {:ok, :pass} + + defp validate_blocks(blocks) do + Enum.each(blocks, fn block -> + # if error happened, will be rescued + {:ok, _} = validate_block(block) + end) + + {:ok, :pass} + end + + # validate block which have no nested items + defp validate_block(%{"type" => type, "data" => data}) when type in @simple_blocks do + validate_with(type, EditorSchema.get(type), data) + end + + # validate block which has mode and items + defp validate_block(%{"type" => type, "data" => data}) + when type in @complex_blocks do + [parent: parent_schema, item: item_schema] = EditorSchema.get(type) + validate_with(type, parent_schema, item_schema, data) + end + + defp validate_block(%{"type" => type}), do: raise("undown #{type} block") + + defp validate_block(e), do: raise("undown block: #{e}") + + # validate with given schema + defp validate_with(block, schema, data) do + case Schema.cast(schema, data) do + {:error, errors} -> + {:error, message} = format_parse_error(block, errors) + raise %MatchError{term: {:error, message}} + + _ -> + {:ok, :pass} + end + end + + defp validate_with(block, parent_schema, item_schema, data) do + with {:ok, _} <- validate_with(block, parent_schema, data), + %{"mode" => mode, "items" => items} <- data do + Enum.each(items, fn item -> + validate_with("#{block}(#{mode})", item_schema, item) + end) + + {:ok, :pass} + end + end + + defp format_parse_error(type, error_list) when is_list(error_list) do + {:error, + Enum.map(error_list, fn error -> + Map.merge(error, %{block: type}) + end)} + end + + defp format_parse_error(%MatchError{term: {:error, error}}) do + {:error, error} + end + + defp format_parse_error(%{message: message}) do + {:error, message} + end + + defp format_parse_error(), do: {:error, "undown validate error"} +end diff --git a/lib/helper/metric/article.ex b/lib/helper/metric/article.ex new file mode 100644 index 000000000..c1f265d5b --- /dev/null +++ b/lib/helper/metric/article.ex @@ -0,0 +1,30 @@ +defmodule Helper.Metric.Article do + @moduledoc """ + html article class names parsed from editor.js's json data + + currently use https://editorjs.io/ as rich-text editor + # NOTE: DONOT CHANGE ONCE SET, OTHERWISE IT WILL CAUSE INCOMPATIBILITY ISSUE + """ + + @doc """ + get all the class names of the parsed editor.js's html parts + """ + def class_names(:html) do + %{ + # root wrapper + viewer: "article-viewer-wrapper", + unknow_block: "unknow-block", + invalid_block: "invalid-block", + # header + header: %{ + wrapper: "header-wrapper", + eyebrow_title: "eyebrow-title", + footer_title: "footer-title" + }, + # list + list: %{ + wrapper: "list-wrapper" + } + } + end +end diff --git a/lib/helper/PublicIpPlug.ex b/lib/helper/public_ip_plug.ex similarity index 100% rename from lib/helper/PublicIpPlug.ex rename to lib/helper/public_ip_plug.ex diff --git a/lib/helper/utils.ex b/lib/helper/utils.ex index f52e5c339..13b64b4ff 100644 --- a/lib/helper/utils.ex +++ b/lib/helper/utils.ex @@ -94,6 +94,24 @@ defmodule Helper.Utils do map |> Enum.reduce(%{}, fn {key, val}, acc -> Map.put(acc, to_string(key), val) end) end + @doc """ + see https://stackoverflow.com/a/61559842/4050784 + adjust it for map keys from atom to string + """ + def keys_to_atoms(json) when is_map(json) do + Map.new(json, &reduce_keys_to_atoms/1) + end + + def keys_to_atoms(string) when is_binary(string), do: string + + def reduce_keys_to_atoms({key, val}) when is_map(val), + do: {String.to_existing_atom(key), keys_to_atoms(val)} + + def reduce_keys_to_atoms({key, val}) when is_list(val), + do: {String.to_existing_atom(key), Enum.map(val, &keys_to_atoms(&1))} + + def reduce_keys_to_atoms({key, val}), do: {String.to_existing_atom(key), val} + @doc """ see https://stackoverflow.com/a/61559842/4050784 adjust it for map keys from atom to string diff --git a/lib/helper/validator/schema.ex b/lib/helper/validator/schema.ex new file mode 100644 index 000000000..bef9f2969 --- /dev/null +++ b/lib/helper/validator/schema.ex @@ -0,0 +1,78 @@ +defmodule Helper.Validator.Schema do + @moduledoc """ + validate json data by given schema, mostly used in editorjs validator + + currently support boolean / string / number / enum + """ + + use Helper.Validator.Schema.Matchers, [:string, :number, :list, :boolean] + + @doc """ + cast data by given schema + + ## example + schema = %{ + checked: [:boolean], + hideLabel: [:boolean], + label: [:string], + labelType: [:string], + indent: [enum: [0, 1, 2, 3, 4]], + text: [:string] + } + + data = %{checked: true, label: "done"} + Schema.cast(schema, data) + """ + def cast(schema, data) do + errors_info = cast_errors(schema, data) + + case errors_info do + [] -> {:ok, :pass} + _ -> {:error, errors_info} + end + end + + defp cast_errors(schema, data) do + schema_fields = Map.keys(schema) + + Enum.reduce(schema_fields, [], fn field, acc -> + value = get_in(data, [field]) + field_schema = get_in(schema, [field]) + + case match(field, value, field_schema) do + {:error, error} -> + acc ++ [error] + + _ -> + acc + end + end) + end + + # enum + defp match(field, nil, enum: _, required: false), do: done(field, nil) + + defp match(field, value, enum: enum, required: false) do + match(field, value, enum: enum) + end + + defp match(field, value, enum: enum) do + case value in enum do + true -> + {:ok, value} + + false -> + {:error, + %{ + field: field, + message: "should be: #{enum |> Enum.join(" | ")}" + }} + end + end + + defp done(field, value), do: {:ok, %{field: field, value: value}} + + defp error(field, value, schema) do + {:error, %{field: field |> to_string, value: value, message: "should be: #{schema}"}} + end +end diff --git a/lib/helper/validator/schema_matchers.ex b/lib/helper/validator/schema_matchers.ex new file mode 100644 index 000000000..bcc62c774 --- /dev/null +++ b/lib/helper/validator/schema_matchers.ex @@ -0,0 +1,29 @@ +defmodule Helper.Validator.Schema.Matchers do + @moduledoc """ + matchers for basic type, support required option + """ + + defmacro __using__(types) do + # can not use Enum.each here, see https://elixirforum.com/t/define-multiple-modules-in-macro-only-last-one-gets-created/1654/4 + for type <- types do + guard_name = if type == :string, do: "is_binary", else: "is_#{to_string(type)}" + + quote do + defp match(field, nil, [unquote(type), required: false]), do: done(field, nil) + + defp match(field, value, [unquote(type), required: false]) + when unquote(:"#{guard_name}")(value) do + done(field, value) + end + + defp match(field, value, [unquote(type)]) when unquote(:"#{guard_name}")(value), + do: done(field, value) + + defp match(field, value, [unquote(type), required: false]), + do: error(field, value, unquote(type)) + + defp match(field, value, [unquote(type)]), do: error(field, value, unquote(type)) + end + end + end +end diff --git a/mix.lock b/mix.lock index 6bd0343af..91c426ea0 100644 --- a/mix.lock +++ b/mix.lock @@ -28,6 +28,7 @@ "ecto": {:hex, :ecto, "3.5.6", "29c77e999e471921c7ce7347732bab7bfa3e24c587640a36f17e0744d1474b8e", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3ae1f3eaecc3e72eeb65ed43239b292bb1eaf335c7e6cea3a7fc27aadb6e93e7"}, "ecto_sql": {:hex, :ecto_sql, "3.5.4", "a9e292c40bd79fff88885f95f1ecd7b2516e09aa99c7dd0201aa84c54d2358e4", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.5.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1fff1a28a898d7bbef263f1f3ea425b04ba9f33816d843238c84eff883347343"}, "elixir_make": {:hex, :elixir_make, "0.4.0", "992f38fabe705bb45821a728f20914c554b276838433349d4f2341f7a687cddf", [], [], "hexpm"}, + "elixir_uuid": {:hex, :elixir_uuid, "1.2.1", "dce506597acb7e6b0daeaff52ff6a9043f5919a4c3315abb4143f0b00378c097", [], [], "hexpm", "f7eba2ea6c3555cea09706492716b0d87397b88946e6380898c2889d68585752"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "eternal": {:hex, :eternal, "1.2.1", "d5b6b2499ba876c57be2581b5b999ee9bdf861c647401066d3eeed111d096bc4", [:mix], [], "hexpm", "b14f1dc204321429479c569cfbe8fb287541184ed040956c8862cb7a677b8406"}, "ex_unit_notifier": {:hex, :ex_unit_notifier, "0.1.4", "36a2dcab829f506e01bf17816590680dd1474407926d43e64c1263e627c364b8", [:mix], [], "hexpm", "fddf5054dd5fd2f809e837b749570baa5c9798e11d0163921baec49b7d5762f2"}, @@ -45,6 +46,7 @@ "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "4bdd305eb64e18b0273864920695cb18d7a2021f31a11b9c5fbcd9a253f936e2"}, "inch_ex": {:hex, :inch_ex, "2.0.0", "24268a9284a1751f2ceda569cd978e1fa394c977c45c331bb52a405de544f4de", [:mix], [{:bunt, "~> 0.2", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "96d0ec5ecac8cf63142d02f16b7ab7152cf0f0f1a185a80161b758383c9399a8"}, "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fdf843bca858203ae1de16da2ee206f53416bbda5dc8c9e78f43243de4bc3afe"}, + "joi": {:hex, :joi, "0.1.4", "cdef436c62ddcaa596f7612d1f903c7be32050248dfc9692b6e7255a5dddd926", [:mix], [{:elixir_uuid, "~> 1.2", [hex: :elixir_uuid, repo: "hexpm", optional: false]}], "hexpm", "5ef5f35e3b716a60dfbf312ccef27b4a4543b01d9c4a59ddadca6fd688c09db6"}, "jose": {:hex, :jose, "1.11.1", "59da64010c69aad6cde2f5b9248b896b84472e99bd18f246085b7b9fe435dcdb", [:mix, :rebar3], [], "hexpm", "078f6c9fb3cd2f4cfafc972c814261a7d1e8d2b3685c0a76eb87e158efff1ac5"}, "jumper": {:hex, :jumper, "1.0.1", "3c00542ef1a83532b72269fab9f0f0c82bf23a35e27d278bfd9ed0865cecabff", [:mix], [], "hexpm", "318c59078ac220e966d27af3646026db9b5a5e6703cb2aa3e26bcfaba65b7433"}, "libring": {:hex, :libring, "1.4.0", "41246ba2f3fbc76b3971f6bce83119dfec1eee17e977a48d8a9cfaaf58c2a8d6", [:mix], [], "hexpm", "1feaf05ee886815ad047cad7ede17d6910710986148ae09cf73eee2989717b81"}, diff --git a/test/helper/converter/editor_to_html_test.exs b/test/helper/converter/editor_to_html_test.exs deleted file mode 100644 index 730357bdc..000000000 --- a/test/helper/converter/editor_to_html_test.exs +++ /dev/null @@ -1,219 +0,0 @@ -defmodule GroupherServer.Test.Helper.Converter.EditorToHtml do - @moduledoc false - - use GroupherServerWeb.ConnCase, async: true - - alias Helper.Converter.EditorToHtml, as: Parser - - @real_editor_data ~S({ - "time" : 1567250876713, - "blocks" : [ - { - "type" : "header", - "data" : { - "text" : "Editor.js", - "level" : 2 - } - }, - { - "type" : "paragraph", - "data" : { - "text" : "Hey. Meet the new Editor. On this page you can see it in action — try to edit this text." - } - }, - { - "type" : "header", - "data" : { - "text" : "Key features", - "level" : 3 - } - }, - { - "type" : "list", - "data" : { - "style" : "unordered", - "items" : [ - "It is a block-styled editor", - "It returns clean data output in JSON", - "Designed to be extendable and pluggable with a simple API" - ] - } - }, - { - "type" : "header", - "data" : { - "text" : "Key features", - "level" : 3 - } - }, - { - "type" : "list", - "data" : { - "style" : "ordered", - "items" : [ - "It is a block-styled editor", - "It returns clean data output in JSON", - "Designed to be extendable and pluggable with a simple API" - ] - } - }, - { - "type" : "header", - "data" : { - "text" : "What does it mean «block-styled editor»", - "level" : 3 - } - }, - { - "type" : "checklist", - "data" : { - "items" : [ - { - "text" : "This is a block-styled editor", - "checked" : true - }, - { - "text" : "Clean output data", - "checked" : false - }, - { - "text" : "Simple and powerful API", - "checked" : true - } - ] - } - }, - { - "type" : "paragraph", - "data" : { - "text" : "Workspace in classic editors is made of a single contenteditable element, used to create different HTML markups. Editor.js workspace consists of separate Blocks: paragraphs, headings, images, lists, quotes, etc. Each of them is an independent contenteditable element (or more complex structure\) provided by Plugin and united by Editor's Core." - } - }, - { - "type" : "paragraph", - "data" : { - "text" : "There are dozens of ready-to-use Blocks and the simple API for creation any Block you need. For example, you can implement Blocks for Tweets, Instagram posts, surveys and polls, CTA-buttons and even games." - } - }, - { - "type" : "header", - "data" : { - "text" : "What does it mean clean data output", - "level" : 3 - } - }, - { - "type" : "paragraph", - "data" : { - "text" : "Classic WYSIWYG-editors produce raw HTML-markup with both content data and content appearance. On the contrary, Editor.js outputs JSON object with data of each Block. You can see an example below" - } - }, - { - "type" : "paragraph", - "data" : { - "text" : "Given data can be used as you want: render with HTML forWeb clients, render natively for mobile apps, create markup for Facebook Instant Articles or Google AMP, generate an audio version and so on."
- }
- },
- {
- "type" : "paragraph",
- "data" : {
- "text" : "Clean data is useful to sanitize, validate and process on the backend."
- }
- },
- {
- "type" : "delimiter",
- "data" : {}
- },
- {
- "type" : "paragraph",
- "data" : {
- "text" : "We have been working on this project more than three years. Several large media projects help us to test and debug the Editor, to make it's core more stable. At the same time we significantly improved the API. Now, it can be used to create any plugin for any task. Hope you enjoy. 😏"
- }
- },
- {
- "type" : "image",
- "data" : {
- "file" : {
- "url" : "https://codex.so/upload/redactor_images/o_e48549d1855c7fc1807308dd14990126.jpg"
- },
- "caption" : "",
- "withBorder" : true,
- "stretched" : false,
- "withBackground" : false
- }
- },
- {
- "type" : "linkTool",
- "data" : {
- "link" : "https://www.github.com",
- "meta" : {
- "url" : "https://www.github.com",
- "domain" : "www.github.com",
- "title" : "Build software better, together",
- "description" : "GitHub is where people build software. More than 40 million people use GitHub to discover, fork, and contribute to over 100 million projects.",
- "image" : {
- "url" : "https://github.githubassets.com/images/modules/open_graph/github-logo.png"
- }
- }
- }
- },
- {
- "type" : "quote",
- "data" : {
- "text" : "quote demo text",
- "caption" : "desc?",
- "alignment" : "left"
- }
- },
- {
- "type" : "delimiter",
- "data" : {
- "type" : "pen"
- }
- },
- {
- "type" : "code",
- "data" : {
- "lang" : "js",
- "text" : ""
- }
- }
- ],
- "version" : "2.15.0"
- })
-
- describe "[basic convert]" do
- test "basic string_json should work" do
- string = ~S({"time":1566184478687,"blocks":[{}],"version":"2.15.0"})
- {:ok, converted} = Parser.string_to_json(string)
-
- assert converted["time"] == 1_566_184_478_687
- assert converted["version"] == "2.15.0"
- end
-
- test "invalid string data should get error" do
- string = ~S({"time":1566184478687,"blocks":[{}],"version":})
- assert {:error, converted} = Parser.string_to_json(string)
- end
-
- test "real-world editor.js data should work" do
- {:ok, converted} = Parser.string_to_json(@real_editor_data)
-
- assert not Enum.empty?(converted["blocks"])
- assert converted["blocks"] |> is_list
- assert converted["version"] |> is_binary
- assert converted["time"] |> is_integer
- end
- end
-
- describe "[block convert]" do
- test "code block should avoid potential xss script attack" do
- {:ok, converted} = Parser.to_html(@real_editor_data)
-
- safe_script =
- "<script>evil scripts</script>"
-
- assert converted |> String.contains?(safe_script)
- end
- end
-end
diff --git a/test/helper/converter/editor_to_html_test/header_test.exs b/test/helper/converter/editor_to_html_test/header_test.exs
new file mode 100644
index 000000000..c77e15cad
--- /dev/null
+++ b/test/helper/converter/editor_to_html_test/header_test.exs
@@ -0,0 +1,181 @@
+defmodule GroupherServer.Test.Helper.Converter.EditorToHTML.Header do
+ @moduledoc false
+
+ use GroupherServerWeb.ConnCase, async: true
+
+ alias Helper.Metric
+ alias Helper.Converter.EditorToHTML, as: Parser
+
+ @clazz Metric.Article.class_names(:html)
+
+ describe "[header block unit]" do
+ @editor_json %{
+ "time" => 1_567_250_876_713,
+ "blocks" => [
+ %{
+ "type" => "header",
+ "data" => %{
+ "text" => "header content",
+ "level" => 1
+ }
+ },
+ %{
+ "type" => "header",
+ "data" => %{
+ "text" => "header content",
+ "level" => 2
+ }
+ },
+ %{
+ "type" => "header",
+ "data" => %{
+ "text" => "header content",
+ "level" => 3
+ }
+ }
+ ],
+ "version" => "2.15.0"
+ }
+ @tag :wip
+ test "header parse should work" do
+ {:ok, editor_string} = Jason.encode(@editor_json)
+ {:ok, converted} = Parser.to_html(editor_string)
+
+ assert converted ==
+ "evel script
Editor.js is an element <script>evel script</script>
paragraph content
this is a basic markdown text
My in-line-code-content is best
this is a basic markdown text
My in-line-code-content is best