Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,29 @@ Then, run the following command to fetch the dependency:
mix deps.get
```

## Configuration

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 (if available)
config :exditorjs, json_library: JSON

# Use Jason
config :exditorjs, json_library: Jason
```

Both libraries are optional dependencies. Add the ones you want to use:

```elixir
{:json, "~> 1.4", optional: true}
{:jason, "~> 1.4", optional: true}
```

## Usage

After installing the library, you can use it to convert Markdown or HTML to Editor.js JSON format.
Expand Down
2 changes: 1 addition & 1 deletion config/config.exs
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import Config

config :rustler_precompiled, :force_build_all, exditorjs: true
config :rustler_precompiled, :force_build_all, exditorjs: true
54 changes: 43 additions & 11 deletions lib/exditorjs.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
Expand All @@ -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("<h1>Hello</h1><p>World</p>")
{:ok, [%{"type" => "heading", ...}, %{"type" => "paragraph", ...}]}

Expand All @@ -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", ...}]}

Expand All @@ -56,11 +55,44 @@ 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
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
defp html_to_editorjs_nif(_html) do
:erlang.nif_error(:not_loaded)
Expand All @@ -69,4 +101,4 @@ defmodule ExditorJS do
defp markdown_to_editorjs_nif(_markdown) do
:erlang.nif_error(:not_loaded)
end
end
end
33 changes: 33 additions & 0 deletions lib/exditorjs/json.ex
Original file line number Diff line number Diff line change
@@ -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
17 changes: 14 additions & 3 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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", optional: true},
{:jason, "~> 1.4", optional: true}
]
end
end
end
1 change: 1 addition & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down
44 changes: 44 additions & 0 deletions test/config_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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 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
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

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 = "<h1>Test</h1>"
{:ok, document} = ExditorJS.html_to_editorjs(html)
assert is_map(document)
assert document["version"] == "2.25.0"
end
end
end
97 changes: 97 additions & 0 deletions test/exditorjs/json_test.exs
Original file line number Diff line number Diff line change
@@ -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
Loading