diff --git a/.gitignore b/.gitignore index 77c8c94..cfd8da2 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ djot-*.tar # Temporary files, for example, from tests. /tmp/ + +# Rustler compiled artifacts +/priv/native/* diff --git a/README.md b/README.md index a29180e..523c762 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,32 @@ # Djot A fast [Djot](https://djot.net) parser and formatter for Elixir. + +Djot parsing and transformations powered by the [jotdown](https://crates.io/crates/jotdown) rust crate. + +## Goals +- [x] Fast Djot to HTML transforms +- [ ] Precompiled binary +- [ ] Optional sanitization +- [ ] Syntax highlighting via autumn or inkjet + +## Usage + +Simply call `to_html` on a string that contains Djot data, and you'll get back a html fragment. + +```elixir + +Djot.to_html("hello *world*!") +# => {:ok, "
hello world!
\n"} +``` + +## Installation + +Add `:djot` as a dependency: + +```elixir +def deps do +[ + {:djot, "~> 0.1.0"} +] +``` diff --git a/lib/djot.ex b/lib/djot.ex index 66e8546..125a1da 100644 --- a/lib/djot.ex +++ b/lib/djot.ex @@ -1,18 +1,22 @@ defmodule Djot do @moduledoc """ - Documentation for `Djot`. - """ + Transform a Djot string input to HTML output. - @doc """ - Hello world. + No sanitization is performed, the document comes directly from the Djot transformer. + """ - ## Examples + alias Djot.Native - iex> Djot.hello() - :world + @spec to_html(String.t()) :: {:ok, String.t()} | {:error, :djot_transform} + def to_html(dj) do + Native.to_html(dj) + end - """ - def hello do - :world + @spec to_html!(String.t()) :: String.t() + def to_html!(dj) do + case Native.to_html(dj) do + {:ok, html} -> html + {:error, :djot_transform} -> raise Djot.DjotError + end end end diff --git a/lib/djot/djot_error.ex b/lib/djot/djot_error.ex new file mode 100644 index 000000000..92fb1b3 --- /dev/null +++ b/lib/djot/djot_error.ex @@ -0,0 +1,7 @@ +defmodule Djot.DjotError do + @moduledoc """ + The djot parser/transformer does not expose many errors, it either parses and transforms a document, or fails to do so. + """ + + defexception message: "An error occured transforming the Djot document" +end diff --git a/lib/djot/native.ex b/lib/djot/native.ex new file mode 100644 index 000000000..e314e5c --- /dev/null +++ b/lib/djot/native.ex @@ -0,0 +1,5 @@ +defmodule Djot.Native do + use Rustler, otp_app: :djot, crate: :djot_nif + + def to_html(_dj), do: :erlang.nif_error(:nif_not_loaded) +end diff --git a/mix.exs b/mix.exs index f90fa60..3f7c41f 100644 --- a/mix.exs +++ b/mix.exs @@ -6,14 +6,15 @@ defmodule Djot.MixProject do def project do [ app: :djot, - version: "2023.11.0", + version: "0.1.0", elixir: "~> 1.15", start_permanent: Mix.env() == :prod, source_url: @source_url, homepage_url: @source_url, name: "Djot", - description: "A Djot parser and formatter", - deps: deps() + description: "A Djot markup language parser and formatter", + deps: deps(), + package: package() ] end @@ -35,7 +36,9 @@ defmodule Djot.MixProject do defp deps do [ - {:rustler, "~> 0.30.0"} + {:rustler, "~> 0.30.0"}, + + {:ex_doc, "~> 0.30.9", only: :dev, runtime: false} ] end end diff --git a/mix.lock b/mix.lock index 21fbfe3..d15861b 100644 --- a/mix.lock +++ b/mix.lock @@ -1,5 +1,11 @@ %{ + "earmark_parser": {:hex, :earmark_parser, "1.4.37", "2ad73550e27c8946648b06905a57e4d454e4d7229c2dafa72a0348c99d8be5f7", [:mix], [], "hexpm", "6b19783f2802f039806f375610faa22da130b8edc21209d0bff47918bb48360e"}, + "ex_doc": {:hex, :ex_doc, "0.30.9", "d691453495c47434c0f2052b08dd91cc32bc4e1a218f86884563448ee2502dd2", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "d7aaaf21e95dc5cddabf89063327e96867d00013963eadf2c6ad135506a8bc10"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, + "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [: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", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "rustler": {:hex, :rustler, "0.30.0", "cefc49922132b072853fa9b0ca4dc2ffcb452f68fb73b779042b02d545e097fb", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "9ef1abb6a7dda35c47cfc649e6a5a61663af6cf842a55814a554a84607dee389"}, "toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"}, } diff --git a/native/djot_nif/.cargo/config.toml b/native/djot_nif/.cargo/config.toml new file mode 100644 index 000000000..20f03f3 --- /dev/null +++ b/native/djot_nif/.cargo/config.toml @@ -0,0 +1,5 @@ +[target.'cfg(target_os = "macos")'] +rustflags = [ + "-C", "link-arg=-undefined", + "-C", "link-arg=dynamic_lookup", +] diff --git a/native/djot_nif/.gitignore b/native/djot_nif/.gitignore new file mode 100644 index 000000000..ea8c4bf --- /dev/null +++ b/native/djot_nif/.gitignore @@ -0,0 +1 @@ +/target diff --git a/native/djot_nif/Cargo.lock b/native/djot_nif/Cargo.lock new file mode 100644 index 000000000..f3ca784 --- /dev/null +++ b/native/djot_nif/Cargo.lock @@ -0,0 +1,156 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "djot_nif" +version = "0.1.0" +dependencies = [ + "jotdown", + "rustler", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "jotdown" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3c1b81e2665d2cf501a82c93164caf3f56919d73611e8d53340ba352912c996" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "proc-macro2" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "rustler" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4b4fea69e23de68c42c06769d6624d2d018da550c17244dd4b691f90ced4a7e" +dependencies = [ + "lazy_static", + "rustler_codegen", + "rustler_sys", +] + +[[package]] +name = "rustler_codegen" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "406061bd07aaf052c344257afed4988c5ec8efe4d2352b4c2cf27ea7c8575b12" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "rustler_sys" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a7c0740e5322b64e2b952d8f0edce5f90fcf6f6fe74cca3f6e78eb3de5ea858" +dependencies = [ + "regex", + "unreachable", +] + +[[package]] +name = "syn" +version = "2.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unreachable" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" +dependencies = [ + "void", +] + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" diff --git a/native/djot_nif/Cargo.toml b/native/djot_nif/Cargo.toml new file mode 100644 index 000000000..3456b70 --- /dev/null +++ b/native/djot_nif/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "djot_nif" +version = "0.1.0" +authors = ["Jeff Sandberg"] +edition = "2021" + +[lib] +name = "djot_nif" +path = "src/lib.rs" +crate-type = ["cdylib"] + +[dependencies] +jotdown = "0.3.2" +rustler = "0.30.0" diff --git a/native/djot_nif/README.md b/native/djot_nif/README.md new file mode 100644 index 000000000..b579bc1 --- /dev/null +++ b/native/djot_nif/README.md @@ -0,0 +1,20 @@ +# NIF for Elixir.Djot.Native + +## To build the NIF module: + +- Your NIF will now build along with your project. + +## To load the NIF: + +```elixir +defmodule Djot.Native do + use Rustler, otp_app: :djot, crate: "djot_nif" + + # When your NIF is loaded, it will override this function. + def add(_a, _b), do: :erlang.nif_error(:nif_not_loaded) +end +``` + +## Examples + +[This](https://github.com/rusterlium/NifIo) is a complete example of a NIF written in Rust. diff --git a/native/djot_nif/src/lib.rs b/native/djot_nif/src/lib.rs new file mode 100644 index 000000000..1075c61 --- /dev/null +++ b/native/djot_nif/src/lib.rs @@ -0,0 +1,25 @@ +extern crate rustler; + +use jotdown::Render; +use rustler::nif; +use rustler::Encoder; +use rustler::Env; +use rustler::Error as RustlerError; +use rustler::Term; + +rustler::atoms! { + ok, + djot_transform +} + +#[nif(schedule = "DirtyCpu")] +pub fn to_html<'a>(env: Env<'a>, dj: &str) -> Result