From 048e258e3b30a26f5a478edb1f979528228e55a0 Mon Sep 17 00:00:00 2001 From: Joakim Nylen Date: Tue, 27 Jan 2026 12:24:11 +0100 Subject: [PATCH 1/3] feat: add json_library setting --- config/config.exs | 3 +- lib/exditorjs.ex | 25 ++- lib/exditorjs/json.ex | 33 +++ mix.exs | 17 +- mix.lock | 1 + test/config_test.exs | 33 +++ test/exditorjs/json_test.exs | 97 +++++++++ test/exditorjs_test.exs | 381 ++++++++++++++++++++++------------- 8 files changed, 435 insertions(+), 155 deletions(-) create mode 100644 lib/exditorjs/json.ex create mode 100644 test/config_test.exs create mode 100644 test/exditorjs/json_test.exs diff --git a/config/config.exs b/config/config.exs index 936515a..7a58742 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,3 +1,4 @@ import Config -config :rustler_precompiled, :force_build_all, exditorjs: true \ No newline at end of file +config :exditorjs, json_library: JSON +config :rustler_precompiled, :force_build_all, exditorjs: true diff --git a/lib/exditorjs.ex b/lib/exditorjs.ex index 2acf4ae..ccc5016 100644 --- a/lib/exditorjs.ex +++ b/lib/exditorjs.ex @@ -2,7 +2,7 @@ defmodule ExditorJS do @moduledoc """ Native Elixir module for converting Markdown and HTML to EditorJS format using Rustler for performance. - + This module provides functions to convert HTML and Markdown content into EditorJS block format, which can be used with the Editor.js library. """ @@ -18,15 +18,14 @@ defmodule ExditorJS do Enum.uniq(["aarch64-unknown-linux-musl" | RustlerPrecompiled.Config.default_targets()]), version: version - @doc """ Converts HTML to EditorJS blocks format. - + Takes a string containing HTML and returns a list of EditorJS blocks that can be used with Editor.js. - + ## Examples - + iex> ExditorJS.html_to_editorjs("

Hello

World

") {:ok, [%{"type" => "heading", ...}, %{"type" => "paragraph", ...}]} @@ -35,19 +34,19 @@ defmodule ExditorJS do """ def html_to_editorjs(html) do case html_to_editorjs_nif(html) do - {:ok, json} -> {:ok, Jason.decode!(json)} + {:ok, json} -> ExditorJS.JSON.decode(json, json_library()) {:error, reason} -> {:error, reason} end end @doc """ Converts Markdown to EditorJS blocks format. - + Takes a string containing Markdown and returns a list of EditorJS blocks that can be used with Editor.js. - + ## Examples - + iex> ExditorJS.markdown_to_editorjs("# Heading\\n\\nParagraph text") {:ok, [%{"type" => "heading", ...}, %{"type" => "paragraph", ...}]} @@ -56,11 +55,15 @@ defmodule ExditorJS do """ def markdown_to_editorjs(markdown) do case markdown_to_editorjs_nif(markdown) do - {:ok, json} -> {:ok, Jason.decode!(json)} + {:ok, json} -> ExditorJS.JSON.decode(json, json_library()) {:error, reason} -> {:error, reason} end end + defp json_library do + Application.get_env(:exditorjs, :json_library, JSON) + end + # Private NIF functions defp html_to_editorjs_nif(_html) do :erlang.nif_error(:not_loaded) @@ -69,4 +72,4 @@ defmodule ExditorJS do defp markdown_to_editorjs_nif(_markdown) do :erlang.nif_error(:not_loaded) end -end \ No newline at end of file +end diff --git a/lib/exditorjs/json.ex b/lib/exditorjs/json.ex new file mode 100644 index 0000000..ee273c7 --- /dev/null +++ b/lib/exditorjs/json.ex @@ -0,0 +1,33 @@ +defmodule ExditorJS.JSON do + @moduledoc false + + @spec encode(term(), module()) :: {:ok, String.t()} | {:error, term()} + def encode(data, json_library) + + if Code.ensure_loaded?(JSON) do + def encode(data, JSON) do + {:ok, JSON.encode!(data)} + rescue + error -> {:error, error} + end + end + + def encode(data, json_library) do + json_library.encode(data) + end + + @spec decode(binary(), module()) :: {:ok, term()} | {:error, term()} + def decode(binary, json_library) + + if Code.ensure_loaded?(JSON) do + def decode(binary, JSON) do + {:ok, JSON.decode!(binary)} + rescue + error -> {:error, error} + end + end + + def decode(binary, json_library) do + json_library.decode(binary) + end +end diff --git a/mix.exs b/mix.exs index ef63148..dcb60ae 100644 --- a/mix.exs +++ b/mix.exs @@ -26,7 +26,17 @@ defmodule ExditorJS.MixProject do links: %{ "GitHub" => "https://github.com/OutdoorMap/exditorjs" }, - files: ["lib", "mix.exs", "README*", "LICENSE", "native/exditorjs_native/src", "native/exditorjs_native/.cargo", "native/exditorjs_native/README*", "native/exditorjs_native/Cargo*", "checksum-*.exs"] + files: [ + "lib", + "mix.exs", + "README*", + "LICENSE", + "native/exditorjs_native/src", + "native/exditorjs_native/.cargo", + "native/exditorjs_native/README*", + "native/exditorjs_native/Cargo*", + "checksum-*.exs" + ] ] end @@ -35,7 +45,8 @@ defmodule ExditorJS.MixProject do {:rustler, "~> 0.37.1", optional: true, runtime: false}, {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, {:rustler_precompiled, "~> 0.8.3"}, - {:jason, "~> 1.4"} + {:json, "~> 1.4"}, + {:jason, "~> 1.4", optional: true} ] end -end \ No newline at end of file +end diff --git a/mix.lock b/mix.lock index 1a38450..90b73a6 100644 --- a/mix.lock +++ b/mix.lock @@ -5,6 +5,7 @@ "finch": {:hex, :finch, "0.20.0", "5330aefb6b010f424dcbbc4615d914e9e3deae40095e73ab0c1bb0968933cadf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2658131a74d051aabfcba936093c903b8e89da9a1b63e430bee62045fa9b2ee2"}, "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, + "json": {:hex, :json, "1.4.1", "8648f04a9439765ad449bc56a3ff7d8b11dd44ff08ffcdefc4329f7c93843dfa", [:mix], [], "hexpm", "9abf218dbe4ea4fcb875e087d5f904ef263d012ee5ed21d46e9dbca63f053d16"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.3", "4252d5d4098da7415c390e847c814bad3764c94a814a0b4245176215615e1035", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "953297c02582a33411ac6208f2c6e55f0e870df7f80da724ed613f10e6706afd"}, diff --git a/test/config_test.exs b/test/config_test.exs new file mode 100644 index 0000000..0e74039 --- /dev/null +++ b/test/config_test.exs @@ -0,0 +1,33 @@ +defmodule ExditorJS.ConfigTest do + use ExUnit.Case, async: true + + describe "json_library configuration" do + setup do + original = Application.get_env(:exditorjs, :json_library) + + on_exit(fn -> + if original do + Application.put_env(:exditorjs, :json_library, original) + else + Application.delete_env(:exditorjs, :json_library) + end + end) + end + + test "defaults to JSON when not configured" do + Application.delete_env(:exditorjs, :json_library) + assert Application.get_env(:exditorjs, :json_library, JSON) == JSON + end + + test "can be configured to Jason" do + Application.put_env(:exditorjs, :json_library, Jason) + assert Application.get_env(:exditorjs, :json_library) == Jason + end + + test "can be configured to custom library" do + custom_lib = Jason + Application.put_env(:exditorjs, :json_library, custom_lib) + assert Application.get_env(:exditorjs, :json_library) == custom_lib + end + end +end diff --git a/test/exditorjs/json_test.exs b/test/exditorjs/json_test.exs new file mode 100644 index 0000000..72374c6 --- /dev/null +++ b/test/exditorjs/json_test.exs @@ -0,0 +1,97 @@ +defmodule ExditorJS.JSONTest do + use ExUnit.Case, async: true + + describe "encode/2" do + test "encodes map to JSON string with Jason" do + data = %{"key" => "value", "nested" => %{"a" => 1}} + assert {:ok, json_string} = ExditorJS.JSON.encode(data, Jason) + assert String.contains?(json_string, ~s|"key":"value"|) + assert String.contains?(json_string, ~s|"nested":|) + end + + test "encodes list to JSON string with Jason" do + data = [1, 2, "three", %{"four" => 4}] + assert {:ok, json_string} = ExditorJS.JSON.encode(data, Jason) + assert String.contains?(json_string, "1") + assert String.contains?(json_string, "three") + end + + test "returns error for invalid data with Jason" do + data = make_ref() + result = ExditorJS.JSON.encode(data, Jason) + assert {:error, _} = result + end + + test "encodes with JSON module when available" do + if Code.ensure_loaded?(JSON) do + data = %{"test" => "value"} + assert {:ok, json_string} = ExditorJS.JSON.encode(data, JSON) + assert String.contains?(json_string, ~s|"test":"value"|) + end + end + + test "encodes with custom library" do + defmodule CustomJSON do + def encode(_), do: {:ok, "{\"custom\": true}"} + def decode(_, _), do: {:ok, %{"custom" => true}} + end + + assert {:ok, json_string} = ExditorJS.JSON.encode(%{}, CustomJSON) + assert json_string == "{\"custom\": true}" + end + end + + describe "decode/2" do + test "decodes JSON string to map with Jason" do + json_string = ~s|{"key":"value","nested":{"a":1}}| + assert {:ok, data} = ExditorJS.JSON.decode(json_string, Jason) + assert data["key"] == "value" + assert data["nested"]["a"] == 1 + end + + test "decodes array JSON string to list with Jason" do + json_string = ~s|[1,2,"three"]| + assert {:ok, data} = ExditorJS.JSON.decode(json_string, Jason) + assert data == [1, 2, "three"] + end + + test "returns error for invalid JSON with Jason" do + invalid_json = "{invalid json" + result = ExditorJS.JSON.decode(invalid_json, Jason) + assert {:error, _} = result + end + + test "decodes with JSON module when available" do + if Code.ensure_loaded?(JSON) do + json_string = ~s|{"test":"value"}| + assert {:ok, data} = ExditorJS.JSON.decode(json_string, JSON) + assert data["test"] == "value" + end + end + + test "decodes with custom library" do + defmodule CustomJSONDecoder do + def decode(_), do: {:ok, %{"custom" => true}} + end + + assert {:ok, data} = ExditorJS.JSON.decode("some json", CustomJSONDecoder) + assert data == %{"custom" => true} + end + end + + describe "UTF-8 support" do + test "encodes UTF-8 characters with Jason" do + data = %{"swedish" => "Upptäck Dalsland", "japanese" => "ダルスランド"} + assert {:ok, json_string} = ExditorJS.JSON.encode(data, Jason) + assert String.contains?(json_string, "Upptäck Dalsland") + assert String.contains?(json_string, "ダルスランド") + end + + test "decodes UTF-8 characters with Jason" do + json_string = ~s|{"swedish":"Upptäck Dalsland","japanese":"ダルスランド"}| + assert {:ok, data} = ExditorJS.JSON.decode(json_string, Jason) + assert data["swedish"] == "Upptäck Dalsland" + assert data["japanese"] == "ダルスランド" + end + end +end diff --git a/test/exditorjs_test.exs b/test/exditorjs_test.exs index 1828235..34edce9 100644 --- a/test/exditorjs_test.exs +++ b/test/exditorjs_test.exs @@ -5,24 +5,26 @@ defmodule ExditorJSTest do test "converts simple heading and paragraph" do html = "

Welcome to EditorJS

This is a simple paragraph.

" {:ok, document} = ExditorJS.html_to_editorjs(html) - + assert is_map(document) assert document["version"] == "2.25.0" assert is_integer(document["time"]) assert document["time"] > 0 assert is_list(document["blocks"]) assert length(document["blocks"]) >= 2 - + block_types = Enum.map(document["blocks"], & &1["type"]) assert "heading" in block_types assert "paragraph" in block_types - + heading_block = Enum.find(document["blocks"], fn block -> block["type"] == "heading" end) assert heading_block != nil assert heading_block["data"]["text"] == "Welcome to EditorJS" assert heading_block["data"]["level"] == 1 - - paragraph_block = Enum.find(document["blocks"], fn block -> block["type"] == "paragraph" end) + + paragraph_block = + Enum.find(document["blocks"], fn block -> block["type"] == "paragraph" end) + assert paragraph_block != nil assert paragraph_block["data"]["text"] == "This is a simple paragraph." end @@ -30,10 +32,10 @@ defmodule ExditorJSTest do test "converts unordered lists" do html = "" {:ok, document} = ExditorJS.html_to_editorjs(html) - + assert is_list(document["blocks"]) assert Enum.any?(document["blocks"], fn block -> block["type"] == "list" end) - + list_block = Enum.find(document["blocks"], fn block -> block["type"] == "list" end) assert list_block != nil assert is_map(list_block["data"]) @@ -47,13 +49,13 @@ defmodule ExditorJSTest do test "converts blockquotes" do html = "
This is a blockquote with some wisdom.
" {:ok, document} = ExditorJS.html_to_editorjs(html) - + assert is_list(document["blocks"]) assert document["version"] == "2.25.0" - + block_types = Enum.map(document["blocks"], & &1["type"]) assert "quote" in block_types - + quote_block = Enum.find(document["blocks"], fn block -> block["type"] == "quote" end) assert quote_block != nil assert quote_block["data"]["text"] == "This is a blockquote with some wisdom." @@ -61,7 +63,7 @@ defmodule ExditorJSTest do test "handles empty HTML" do {:ok, document} = ExditorJS.html_to_editorjs("") - + assert is_list(document["blocks"]) assert document["version"] == "2.25.0" end @@ -69,13 +71,13 @@ defmodule ExditorJSTest do test "converts images" do html = "\"Example" {:ok, document} = ExditorJS.html_to_editorjs(html) - + assert is_list(document["blocks"]) assert document["version"] == "2.25.0" - + block_types = Enum.map(document["blocks"], & &1["type"]) assert "image" in block_types - + image_block = Enum.find(document["blocks"], fn block -> block["type"] == "image" end) assert image_block != nil assert image_block["data"]["url"] == "https://example.com/image.jpg" @@ -85,13 +87,13 @@ defmodule ExditorJSTest do test "converts code blocks" do html = "let result = convert(input);" {:ok, document} = ExditorJS.html_to_editorjs(html) - + assert is_list(document["blocks"]) assert document["version"] == "2.25.0" - + block_types = Enum.map(document["blocks"], & &1["type"]) assert "code" in block_types - + code_block = Enum.find(document["blocks"], fn block -> block["type"] == "code" end) assert code_block != nil assert code_block["data"]["code"] == "let result = convert(input);" @@ -102,24 +104,26 @@ defmodule ExditorJSTest do test "converts headings and paragraphs" do markdown = "# Getting Started\n\nThis is a **markdown** document." {:ok, document} = ExditorJS.markdown_to_editorjs(markdown) - + assert is_map(document) assert document["version"] == "2.25.0" assert is_integer(document["time"]) assert document["time"] > 0 assert is_list(document["blocks"]) assert length(document["blocks"]) >= 2 - + block_types = Enum.map(document["blocks"], & &1["type"]) assert "heading" in block_types assert "paragraph" in block_types - + heading_block = Enum.find(document["blocks"], fn block -> block["type"] == "heading" end) assert heading_block != nil assert heading_block["data"]["text"] == "Getting Started" assert heading_block["data"]["level"] == 1 - - paragraph_block = Enum.find(document["blocks"], fn block -> block["type"] == "paragraph" end) + + paragraph_block = + Enum.find(document["blocks"], fn block -> block["type"] == "paragraph" end) + assert paragraph_block != nil assert String.contains?(paragraph_block["data"]["text"], "markdown") end @@ -127,10 +131,10 @@ defmodule ExditorJSTest do test "converts unordered lists" do markdown = "- Easy to use\n- Powerful conversion\n- Well documented" {:ok, document} = ExditorJS.markdown_to_editorjs(markdown) - + assert is_list(document["blocks"]) assert Enum.any?(document["blocks"], fn block -> block["type"] == "list" end) - + list_block = Enum.find(document["blocks"], fn block -> block["type"] == "list" end) assert list_block != nil assert is_map(list_block["data"]) @@ -144,13 +148,13 @@ defmodule ExditorJSTest do test "converts code blocks" do markdown = "```rust\nfn main() {\n println!(\"Hello, world!\");\n}\n```" {:ok, document} = ExditorJS.markdown_to_editorjs(markdown) - + assert is_list(document["blocks"]) assert document["version"] == "2.25.0" - + block_types = Enum.map(document["blocks"], & &1["type"]) assert "code" in block_types - + code_block = Enum.find(document["blocks"], fn block -> block["type"] == "code" end) assert code_block != nil assert code_block["data"]["language"] == "rust" @@ -161,13 +165,13 @@ defmodule ExditorJSTest do test "converts blockquotes" do markdown = "> This is a blockquote with some wisdom." {:ok, document} = ExditorJS.markdown_to_editorjs(markdown) - + assert is_list(document["blocks"]) assert document["version"] == "2.25.0" - + block_types = Enum.map(document["blocks"], & &1["type"]) assert "quote" in block_types - + quote_block = Enum.find(document["blocks"], fn block -> block["type"] == "quote" end) assert quote_block != nil assert quote_block["data"]["text"] == "This is a blockquote with some wisdom." @@ -176,15 +180,16 @@ defmodule ExditorJSTest do test "handles complex markdown document" do markdown = """ # Main Title - + Some introductory text. - + ## Section - + - Item 1 - Item 2 - Item 3 """ + {:ok, document} = ExditorJS.markdown_to_editorjs(markdown) assert is_map(document) @@ -192,28 +197,32 @@ defmodule ExditorJSTest do assert is_integer(document["time"]) assert is_list(document["blocks"]) assert length(document["blocks"]) > 0 - + block_types = Enum.map(document["blocks"], & &1["type"]) assert "heading" in block_types assert "paragraph" in block_types assert "list" in block_types - - h1_block = Enum.find(document["blocks"], fn block -> - block["type"] == "heading" && block["data"]["level"] == 1 - end) + + h1_block = + Enum.find(document["blocks"], fn block -> + block["type"] == "heading" && block["data"]["level"] == 1 + end) + assert h1_block != nil assert h1_block["data"]["text"] == "Main Title" - - h2_block = Enum.find(document["blocks"], fn block -> - block["type"] == "heading" && block["data"]["level"] == 2 - end) + + h2_block = + Enum.find(document["blocks"], fn block -> + block["type"] == "heading" && block["data"]["level"] == 2 + end) + assert h2_block != nil assert h2_block["data"]["text"] == "Section" end test "handles empty markdown" do {:ok, document} = ExditorJS.markdown_to_editorjs("") - + assert is_list(document["blocks"]) assert document["version"] == "2.25.0" end @@ -221,11 +230,11 @@ defmodule ExditorJSTest do test "markdown with UTF-8 in headings" do markdown = "# Upptäck Dalsland från cykelsadeln" {:ok, document} = ExditorJS.markdown_to_editorjs(markdown) - + assert is_list(document["blocks"]) heading_blocks = Enum.filter(document["blocks"], fn block -> block["type"] == "heading" end) assert length(heading_blocks) > 0 - + heading = Enum.at(heading_blocks, 0) assert heading["data"]["level"] == 1 assert heading["data"]["text"] == "Upptäck Dalsland från cykelsadeln" @@ -234,11 +243,14 @@ defmodule ExditorJSTest do test "markdown with UTF-8 in paragraphs" do markdown = "Cykeln är det perfekta redskapet för att upptäcka Dalsland." {:ok, document} = ExditorJS.markdown_to_editorjs(markdown) - + assert is_list(document["blocks"]) - paragraph_blocks = Enum.filter(document["blocks"], fn block -> block["type"] == "paragraph" end) + + paragraph_blocks = + Enum.filter(document["blocks"], fn block -> block["type"] == "paragraph" end) + assert length(paragraph_blocks) > 0 - + paragraph = Enum.at(paragraph_blocks, 0) assert String.contains?(paragraph["data"]["text"], "Dalsland") end @@ -249,11 +261,12 @@ defmodule ExditorJSTest do - Guidat cykeltur i Dalsland - Sevärdheter och aktiviteter """ + {:ok, document} = ExditorJS.markdown_to_editorjs(markdown) - + list_blocks = Enum.filter(document["blocks"], fn block -> block["type"] == "list" end) assert length(list_blocks) > 0 - + list = Enum.at(list_blocks, 0) assert list["data"]["style"] == "unordered" assert length(list["data"]["items"]) == 3 @@ -266,11 +279,12 @@ defmodule ExditorJSTest do 1. Cykelpaket för äventyrare 2. Guidat cykeltur i Dalsland """ + {:ok, document} = ExditorJS.markdown_to_editorjs(markdown) - + list_blocks = Enum.filter(document["blocks"], fn block -> block["type"] == "list" end) assert length(list_blocks) > 0 - + list = Enum.at(list_blocks, 0) assert list["data"]["style"] == "ordered" assert length(list["data"]["items"]) == 2 @@ -281,11 +295,12 @@ defmodule ExditorJSTest do > Cykeln är det perfekta redskapet för att upptäcka Dalsland > och älska naturen omkring dig. """ + {:ok, document} = ExditorJS.markdown_to_editorjs(markdown) - + quote_blocks = Enum.filter(document["blocks"], fn block -> block["type"] == "quote" end) assert length(quote_blocks) > 0 - + quote = Enum.at(quote_blocks, 0) assert String.contains?(quote["data"]["text"], "perfekta") assert String.contains?(quote["data"]["text"], "Dalsland") @@ -298,11 +313,12 @@ defmodule ExditorJSTest do fn test() {} ``` """ + {:ok, document} = ExditorJS.markdown_to_editorjs(markdown) - + code_blocks = Enum.filter(document["blocks"], fn block -> block["type"] == "code" end) assert length(code_blocks) > 0 - + code = Enum.at(code_blocks, 0) assert code["data"]["language"] == "rust" assert String.contains?(code["data"]["code"], "kommentar") @@ -311,11 +327,11 @@ defmodule ExditorJSTest do test "markdown with Japanese in headings" do markdown = "# ダルスランドを自転車で探検" {:ok, document} = ExditorJS.markdown_to_editorjs(markdown) - + assert is_list(document["blocks"]) heading_blocks = Enum.filter(document["blocks"], fn block -> block["type"] == "heading" end) assert length(heading_blocks) > 0 - + heading = Enum.at(heading_blocks, 0) assert heading["data"]["level"] == 1 assert heading["data"]["text"] == "ダルスランドを自転車で探検" @@ -324,11 +340,14 @@ defmodule ExditorJSTest do test "markdown with Japanese in paragraphs" do markdown = "自転車はダルスランドを探検するのに最適なツールです。" {:ok, document} = ExditorJS.markdown_to_editorjs(markdown) - + assert is_list(document["blocks"]) - paragraph_blocks = Enum.filter(document["blocks"], fn block -> block["type"] == "paragraph" end) + + paragraph_blocks = + Enum.filter(document["blocks"], fn block -> block["type"] == "paragraph" end) + assert length(paragraph_blocks) > 0 - + paragraph = Enum.at(paragraph_blocks, 0) assert String.contains?(paragraph["data"]["text"], "ダルスランド") end @@ -339,11 +358,12 @@ defmodule ExditorJSTest do - ガイド付きツアー - レンタル自転車 """ + {:ok, document} = ExditorJS.markdown_to_editorjs(markdown) - + list_blocks = Enum.filter(document["blocks"], fn block -> block["type"] == "list" end) assert length(list_blocks) > 0 - + list = Enum.at(list_blocks, 0) assert list["data"]["style"] == "unordered" assert length(list["data"]["items"]) == 3 @@ -358,11 +378,12 @@ defmodule ExditorJSTest do 2. 宿泊施設の手配 3. ガイドサービス """ + {:ok, document} = ExditorJS.markdown_to_editorjs(markdown) - + list_blocks = Enum.filter(document["blocks"], fn block -> block["type"] == "list" end) assert length(list_blocks) > 0 - + list = Enum.at(list_blocks, 0) assert list["data"]["style"] == "ordered" assert length(list["data"]["items"]) == 3 @@ -374,11 +395,12 @@ defmodule ExditorJSTest do > 自転車はダルスランドを探検するのに最適なツールです。 > より深く、より楽しい体験ができます。 """ + {:ok, document} = ExditorJS.markdown_to_editorjs(markdown) - + quote_blocks = Enum.filter(document["blocks"], fn block -> block["type"] == "quote" end) assert length(quote_blocks) > 0 - + quote = Enum.at(quote_blocks, 0) assert String.contains?(quote["data"]["text"], "最適な") assert String.contains?(quote["data"]["text"], "ツール") @@ -391,11 +413,12 @@ defmodule ExditorJSTest do function test() {} ``` """ + {:ok, document} = ExditorJS.markdown_to_editorjs(markdown) - + code_blocks = Enum.filter(document["blocks"], fn block -> block["type"] == "code" end) assert length(code_blocks) > 0 - + code = Enum.at(code_blocks, 0) assert code["data"]["language"] == "javascript" assert String.contains?(code["data"]["code"], "コメント") @@ -404,24 +427,27 @@ defmodule ExditorJSTest do test "markdown with mixed Japanese and ASCII" do markdown = """ # ダルスランド探検ガイド 2024 - + このガイドはDalsland Experience提供です。 - + - 初心者向けコース - 中級者向けコース (25km) - 上級者向けコース """ + {:ok, document} = ExditorJS.markdown_to_editorjs(markdown) - + block_types = Enum.map(document["blocks"], & &1["type"]) assert "heading" in block_types assert "paragraph" in block_types assert "list" in block_types - + # Verify heading - heading = Enum.find(document["blocks"], fn block -> - block["type"] == "heading" && String.contains?(block["data"]["text"], "ダルスランド") - end) + heading = + Enum.find(document["blocks"], fn block -> + block["type"] == "heading" && String.contains?(block["data"]["text"], "ダルスランド") + end) + assert heading != nil assert String.contains?(heading["data"]["text"], "2024") end @@ -429,12 +455,14 @@ defmodule ExditorJSTest do describe "embed support" do test "converts HTML iframe to embed block" do - html = ~s|| + html = + ~s|| + {:ok, document} = ExditorJS.html_to_editorjs(html) - + assert is_list(document["blocks"]) assert Enum.any?(document["blocks"], fn block -> block["type"] == "embed" end) - + embed_block = Enum.find(document["blocks"], fn block -> block["type"] == "embed" end) assert embed_block != nil assert embed_block["data"]["service"] == "youtube" @@ -446,7 +474,7 @@ defmodule ExditorJSTest do test "converts markdown URL to embed block for YouTube" do markdown = "https://www.youtube.com/watch?v=dQw4w9WgXcQ" {:ok, document} = ExditorJS.markdown_to_editorjs(markdown) - + assert is_list(document["blocks"]) embed_block = Enum.find(document["blocks"], fn block -> block["type"] == "embed" end) assert embed_block != nil @@ -456,7 +484,7 @@ defmodule ExditorJSTest do test "converts markdown short URL to embed block for YouTube" do markdown = "https://youtu.be/dQw4w9WgXcQ" {:ok, document} = ExditorJS.markdown_to_editorjs(markdown) - + assert is_list(document["blocks"]) embed_block = Enum.find(document["blocks"], fn block -> block["type"] == "embed" end) assert embed_block != nil @@ -466,7 +494,7 @@ defmodule ExditorJSTest do test "converts markdown link with caption to embed block for Vimeo" do markdown = "[Watch this](https://vimeo.com/123456789)" {:ok, document} = ExditorJS.markdown_to_editorjs(markdown) - + assert is_list(document["blocks"]) embed_block = Enum.find(document["blocks"], fn block -> block["type"] == "embed" end) assert embed_block != nil @@ -477,7 +505,7 @@ defmodule ExditorJSTest do test "converts Coub URL to embed block" do markdown = "https://coub.com/view/1czcdf" {:ok, document} = ExditorJS.markdown_to_editorjs(markdown) - + assert is_list(document["blocks"]) embed_block = Enum.find(document["blocks"], fn block -> block["type"] == "embed" end) assert embed_block != nil @@ -487,7 +515,7 @@ defmodule ExditorJSTest do test "converts Instagram URL to embed block" do markdown = "https://www.instagram.com/p/ABC123XYZ/" {:ok, document} = ExditorJS.markdown_to_editorjs(markdown) - + assert is_list(document["blocks"]) embed_block = Enum.find(document["blocks"], fn block -> block["type"] == "embed" end) assert embed_block != nil @@ -497,7 +525,7 @@ defmodule ExditorJSTest do test "converts Twitter URL to embed block" do markdown = "https://twitter.com/user/status/1234567890" {:ok, document} = ExditorJS.markdown_to_editorjs(markdown) - + assert is_list(document["blocks"]) embed_block = Enum.find(document["blocks"], fn block -> block["type"] == "embed" end) assert embed_block != nil @@ -507,7 +535,7 @@ defmodule ExditorJSTest do test "converts Twitch video URL to embed block" do markdown = "https://twitch.tv/videos/123456789" {:ok, document} = ExditorJS.markdown_to_editorjs(markdown) - + assert is_list(document["blocks"]) embed_block = Enum.find(document["blocks"], fn block -> block["type"] == "embed" end) assert embed_block != nil @@ -517,7 +545,7 @@ defmodule ExditorJSTest do test "converts Twitch channel URL to embed block" do markdown = "https://twitch.tv/channel_name" {:ok, document} = ExditorJS.markdown_to_editorjs(markdown) - + assert is_list(document["blocks"]) embed_block = Enum.find(document["blocks"], fn block -> block["type"] == "embed" end) assert embed_block != nil @@ -527,7 +555,7 @@ defmodule ExditorJSTest do test "ignores non-embed URLs" do markdown = "https://example.com/some/page" {:ok, document} = ExditorJS.markdown_to_editorjs(markdown) - + # Non-embed URLs should be treated as paragraphs assert is_list(document["blocks"]) assert !Enum.any?(document["blocks"], fn block -> block["type"] == "embed" end) @@ -548,44 +576,51 @@ defmodule ExditorJSTest do

The Dalsland Experience kan hjälpa dig att skräddarsy upplevelser och aktiviteter runt om i Dalsland. Proffsiga samarbetspartners kan erbjuda spännande utflykter, sevärdheter och aktiviteter som sätter guldkant på er cykelresa.
Inget boende? Teamet hjälper dig även med boendealternativ anpassat efter dina förutsättningar och önskemål. Det ska vara lätt och roligt att uppleva The Dalsland Experience!

""" - + {:ok, document} = ExditorJS.html_to_editorjs(html) - + assert is_map(document) assert document["version"] == "2.25.0" assert is_integer(document["time"]) assert document["time"] > 0 assert is_list(document["blocks"]) assert length(document["blocks"]) > 0 - + # Verify we have heading blocks block_types = Enum.map(document["blocks"], & &1["type"]) assert "heading" in block_types assert "paragraph" in block_types - + # Check specific headings heading_blocks = Enum.filter(document["blocks"], fn block -> block["type"] == "heading" end) heading_texts = Enum.map(heading_blocks, & &1["data"]["text"]) - + assert Enum.any?(heading_texts, fn text -> String.contains?(text, "Färdiga") end) assert Enum.any?(heading_texts, fn text -> String.contains?(text, "Hyrcyklar") end) assert Enum.any?(heading_texts, fn text -> String.contains?(text, "cykel") end) - + # Check that link text is preserved (link should be stripped but text remains) - paragraph_blocks = Enum.filter(document["blocks"], fn block -> block["type"] == "paragraph" end) + paragraph_blocks = + Enum.filter(document["blocks"], fn block -> block["type"] == "paragraph" end) + paragraph_texts = Enum.map(paragraph_blocks, & &1["data"]["text"]) - - assert Enum.any?(paragraph_texts, fn text -> String.contains?(text, "The Dalsland Experience") end) + + assert Enum.any?(paragraph_texts, fn text -> + String.contains?(text, "The Dalsland Experience") + end) + assert Enum.any?(paragraph_texts, fn text -> String.contains?(text, "Dalsland") end) end test "handles complex attributes on links" do - html = ~s|

Visit our website for more info.

| + html = + ~s|

Visit our website for more info.

| + {:ok, document} = ExditorJS.html_to_editorjs(html) - + assert is_list(document["blocks"]) assert Enum.any?(document["blocks"], fn block -> block["type"] == "paragraph" end) - + paragraph = Enum.find(document["blocks"], fn block -> block["type"] == "paragraph" end) assert paragraph != nil # Link text should be extracted but link itself removed @@ -601,13 +636,13 @@ defmodule ExditorJSTest do

Third paragraph

""" - + {:ok, document} = ExditorJS.html_to_editorjs(html) - + assert is_map(document) assert is_list(document["blocks"]) assert length(document["blocks"]) > 0 - + # Should have paragraph blocks (empty paragraphs might be filtered or kept) block_types = Enum.map(document["blocks"], & &1["type"]) assert "paragraph" in block_types @@ -620,28 +655,32 @@ defmodule ExditorJSTest do

Section

Another paragraph with emphasis and another link.

""" - + {:ok, document} = ExditorJS.html_to_editorjs(html) - + assert is_map(document) assert document["version"] == "2.25.0" assert is_list(document["blocks"]) - + block_types = Enum.map(document["blocks"], & &1["type"]) assert "heading" in block_types assert "paragraph" in block_types - + # Verify h2 exists - h2_blocks = Enum.filter(document["blocks"], fn block -> - block["type"] == "heading" && block["data"]["level"] == 2 - end) + h2_blocks = + Enum.filter(document["blocks"], fn block -> + block["type"] == "heading" && block["data"]["level"] == 2 + end) + assert length(h2_blocks) > 0 assert Enum.at(h2_blocks, 0)["data"]["text"] == "Welcome" - + # Verify h3 exists - h3_blocks = Enum.filter(document["blocks"], fn block -> - block["type"] == "heading" && block["data"]["level"] == 3 - end) + h3_blocks = + Enum.filter(document["blocks"], fn block -> + block["type"] == "heading" && block["data"]["level"] == 3 + end) + assert length(h3_blocks) > 0 assert Enum.at(h3_blocks, 0)["data"]["text"] == "Section" end @@ -658,44 +697,48 @@ defmodule ExditorJSTest do

自転車以上の価値

Dalsland Experience では、Dalsland 周辺での体験やアクティビティをお客様に合わせてカスタマイズできます。
ご宿泊先がお決まりですか?お客様のご都合やご希望に合わせた宿泊施設のご案内もいたします。

""" - + {:ok, document} = ExditorJS.html_to_editorjs(html) - + assert is_map(document) assert document["version"] == "2.25.0" assert is_integer(document["time"]) assert document["time"] > 0 assert is_list(document["blocks"]) assert length(document["blocks"]) > 0 - + # Verify we have heading blocks block_types = Enum.map(document["blocks"], & &1["type"]) assert "heading" in block_types assert "paragraph" in block_types - + # Check specific headings with Japanese text heading_blocks = Enum.filter(document["blocks"], fn block -> block["type"] == "heading" end) heading_texts = Enum.map(heading_blocks, & &1["data"]["text"]) - + assert Enum.any?(heading_texts, fn text -> String.contains?(text, "パッケージ") end) assert Enum.any?(heading_texts, fn text -> String.contains?(text, "高品質") end) assert Enum.any?(heading_texts, fn text -> String.contains?(text, "自転車") end) - + # Check that Japanese text is preserved - paragraph_blocks = Enum.filter(document["blocks"], fn block -> block["type"] == "paragraph" end) + paragraph_blocks = + Enum.filter(document["blocks"], fn block -> block["type"] == "paragraph" end) + paragraph_texts = Enum.map(paragraph_blocks, & &1["data"]["text"]) - + assert Enum.any?(paragraph_texts, fn text -> String.contains?(text, "ダルスランド") end) assert Enum.any?(paragraph_texts, fn text -> String.contains?(text, "エクスペリエンス") end) end test "handles Japanese with complex attributes on links" do - html = ~s|

詳細はこちらをご覧ください。

| + html = + ~s|

詳細はこちらをご覧ください。

| + {:ok, document} = ExditorJS.html_to_editorjs(html) - + assert is_list(document["blocks"]) assert Enum.any?(document["blocks"], fn block -> block["type"] == "paragraph" end) - + paragraph = Enum.find(document["blocks"], fn block -> block["type"] == "paragraph" end) assert paragraph != nil # Link text should be extracted but link itself removed @@ -710,30 +753,88 @@ defmodule ExditorJSTest do

第三見出し

第四見出し

""" - + {:ok, document} = ExditorJS.html_to_editorjs(html) - + assert is_map(document) assert is_list(document["blocks"]) - + # Verify all heading levels - h1_blocks = Enum.filter(document["blocks"], fn block -> - block["type"] == "heading" && block["data"]["level"] == 1 - end) + h1_blocks = + Enum.filter(document["blocks"], fn block -> + block["type"] == "heading" && block["data"]["level"] == 1 + end) + assert length(h1_blocks) > 0 assert h1_blocks |> Enum.at(0) |> Map.get("data") |> Map.get("text") == "第一見出し" - - h2_blocks = Enum.filter(document["blocks"], fn block -> - block["type"] == "heading" && block["data"]["level"] == 2 - end) + + h2_blocks = + Enum.filter(document["blocks"], fn block -> + block["type"] == "heading" && block["data"]["level"] == 2 + end) + assert length(h2_blocks) > 0 assert h2_blocks |> Enum.at(0) |> Map.get("data") |> Map.get("text") == "第二見出し" - - h3_blocks = Enum.filter(document["blocks"], fn block -> - block["type"] == "heading" && block["data"]["level"] == 3 - end) + + h3_blocks = + Enum.filter(document["blocks"], fn block -> + block["type"] == "heading" && block["data"]["level"] == 3 + end) + assert length(h3_blocks) > 0 assert h3_blocks |> Enum.at(0) |> Map.get("data") |> Map.get("text") == "第三見出し" end end -end \ No newline at end of file + + describe "json_library configuration" do + setup do + original = Application.get_env(:exditorjs, :json_library) + + on_exit(fn -> + if original do + Application.put_env(:exditorjs, :json_library, original, persistent: true) + else + Application.delete_env(:exditorjs, :json_library) + end + end) + end + + test "html_to_editorjs uses configured json_library" do + Application.put_env(:exditorjs, :json_library, JSON, persistent: true) + html = "

Test

" + {:ok, document} = ExditorJS.html_to_editorjs(html) + + assert is_map(document) + assert document["version"] == "2.25.0" + assert is_list(document["blocks"]) + end + + test "markdown_to_editorjs uses configured json_library" do + Application.put_env(:exditorjs, :json_library, JSON, persistent: true) + markdown = "# Test Heading" + {:ok, document} = ExditorJS.markdown_to_editorjs(markdown) + + assert is_map(document) + assert document["version"] == "2.25.0" + assert is_list(document["blocks"]) + end + + test "functions work with Jason when configured" do + Application.put_env(:exditorjs, :json_library, Jason, persistent: true) + html = "

Test paragraph

" + {:ok, document} = ExditorJS.html_to_editorjs(html) + + assert document["version"] == "2.25.0" + assert Enum.any?(document["blocks"], fn b -> b["type"] == "paragraph" end) + end + + test "functions work with JSON when configured" do + Application.put_env(:exditorjs, :json_library, JSON, persistent: true) + markdown = "- Item 1\n- Item 2" + {:ok, document} = ExditorJS.markdown_to_editorjs(markdown) + + assert document["version"] == "2.25.0" + assert Enum.any?(document["blocks"], fn b -> b["type"] == "list" end) + end + end +end From ae28afdf0abc8db752d769e1f82b5c354db62761 Mon Sep 17 00:00:00 2001 From: Joakim Nylen Date: Tue, 27 Jan 2026 12:27:24 +0100 Subject: [PATCH 2/3] update readme --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index ebc7e60..29782d1 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,24 @@ Then, run the following command to fetch the dependency: mix deps.get ``` +## Configuration + +By default, the library uses Erlang's built-in `JSON` module for encoding/decoding. You can configure it to use a different JSON library in your `config.exs`: + +```elixir +# Use Erlang's JSON (default) +config :exditorjs, json_library: JSON + +# Use Jason instead +config :exditorjs, json_library: Jason +``` + +If you want to use Jason, make sure to add it to your dependencies: + +```elixir +{:jason, "~> 1.4"} +``` + ## Usage After installing the library, you can use it to convert Markdown or HTML to Editor.js JSON format. From da338807e5b910d65c0daa28367b2893dae1928e Mon Sep 17 00:00:00 2001 From: Joakim Nylen Date: Tue, 27 Jan 2026 12:31:34 +0100 Subject: [PATCH 3/3] feat: dont force json on users --- README.md | 15 ++++++++++----- config/config.exs | 1 - lib/exditorjs.ex | 31 ++++++++++++++++++++++++++++++- mix.exs | 2 +- test/config_test.exs | 17 ++++++++++++++--- 5 files changed, 55 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 29782d1..df827c2 100644 --- a/README.md +++ b/README.md @@ -22,20 +22,25 @@ mix deps.get ## Configuration -By default, the library uses Erlang's built-in `JSON` module for encoding/decoding. You can configure it to use a different JSON library in your `config.exs`: +By default, the library automatically selects the best available JSON library: +1. If Erlang's `JSON` module is available, it uses that +2. Otherwise, it falls back to `Jason` + +You can explicitly configure which JSON library to use in your `config.exs`: ```elixir -# Use Erlang's JSON (default) +# Use Erlang's JSON (if available) config :exditorjs, json_library: JSON -# Use Jason instead +# Use Jason config :exditorjs, json_library: Jason ``` -If you want to use Jason, make sure to add it to your dependencies: +Both libraries are optional dependencies. Add the ones you want to use: ```elixir -{:jason, "~> 1.4"} +{:json, "~> 1.4", optional: true} +{:jason, "~> 1.4", optional: true} ``` ## Usage diff --git a/config/config.exs b/config/config.exs index 7a58742..c0f99fd 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,4 +1,3 @@ import Config -config :exditorjs, json_library: JSON config :rustler_precompiled, :force_build_all, exditorjs: true diff --git a/lib/exditorjs.ex b/lib/exditorjs.ex index ccc5016..86b2850 100644 --- a/lib/exditorjs.ex +++ b/lib/exditorjs.ex @@ -61,7 +61,36 @@ defmodule ExditorJS do end defp json_library do - Application.get_env(:exditorjs, :json_library, JSON) + case Application.get_env(:exditorjs, :json_library) do + nil -> + if Code.ensure_loaded?(JSON) do + JSON + else + if Code.ensure_loaded?(Jason) do + Jason + else + raise """ + No JSON library configured and none available. + + Please add a JSON library to your deps and config: + + # Use Jason (recommended) + {:jason, "~> 1.4"} + + # Or use Erlang's JSON + {:json, "~> 1.4"} + + And configure it in config.exs: + config :exditorjs, json_library: Jason + # or + config :exditorjs, json_library: JSON + """ + end + end + + lib -> + lib + end end # Private NIF functions diff --git a/mix.exs b/mix.exs index dcb60ae..74addb0 100644 --- a/mix.exs +++ b/mix.exs @@ -45,7 +45,7 @@ defmodule ExditorJS.MixProject do {:rustler, "~> 0.37.1", optional: true, runtime: false}, {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, {:rustler_precompiled, "~> 0.8.3"}, - {:json, "~> 1.4"}, + {:json, "~> 1.4", optional: true}, {:jason, "~> 1.4", optional: true} ] end diff --git a/test/config_test.exs b/test/config_test.exs index 0e74039..0545d17 100644 --- a/test/config_test.exs +++ b/test/config_test.exs @@ -14,9 +14,9 @@ defmodule ExditorJS.ConfigTest do end) end - test "defaults to JSON when not configured" do - Application.delete_env(:exditorjs, :json_library) - assert Application.get_env(:exditorjs, :json_library, JSON) == JSON + test "defaults to JSON when configured and available" do + Application.put_env(:exditorjs, :json_library, JSON) + assert Application.get_env(:exditorjs, :json_library) == JSON end test "can be configured to Jason" do @@ -30,4 +30,15 @@ defmodule ExditorJS.ConfigTest do assert Application.get_env(:exditorjs, :json_library) == custom_lib end end + + describe "json_library/0 auto-detection" do + test "uses JSON when available and not explicitly configured" do + Application.delete_env(:exditorjs, :json_library) + # JSON is available in this environment, so functions should work + html = "

Test

" + {:ok, document} = ExditorJS.html_to_editorjs(html) + assert is_map(document) + assert document["version"] == "2.25.0" + end + end end