Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85823331
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Authored By
Unknown
Size
14 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/fast_html.ex b/lib/fast_html.ex
index 6c66a6e..ac76c27 100644
--- a/lib/fast_html.ex
+++ b/lib/fast_html.ex
@@ -1,102 +1,102 @@
defmodule :fast_html do
@moduledoc """
A module to decode html into a tree structure.
Based on [Alexander Borisov's myhtml](https://github.com/lexborisov/myhtml),
this binding gains the properties of being html-spec compliant and very fast.
## Example
iex> :fast_html.decode("<h1>Hello world</h1>")
{"html", [], [{"head", [], []}, {"body", [], [{"h1", [], ["Hello world"]}]}]}
Benchmark results (removed Nif calling mode) on various file sizes on a 2,5Ghz Core i7:
Settings:
duration: 1.0 s
## FileSizesBench
[15:28:42] 1/3: github_trending_js.html 341k
[15:28:46] 2/3: w3c_html5.html 131k
[15:28:48] 3/3: wikipedia_hyperlink.html 97k
Finished in 7.52 seconds
## FileSizesBench
benchmark name iterations average time
wikipedia_hyperlink.html 97k 1000 1385.86 µs/op
w3c_html5.html 131k 1000 2179.30 µs/op
github_trending_js.html 341k 500 5686.21 µs/op
"""
@type tag() :: String.t() | atom()
@type attr() :: {String.t(), String.t()}
@type attr_list() :: [] | [attr()]
@type comment_node() :: {:comment, String.t()}
@type comment_node3() :: {:comment, [], String.t()}
@type tree() ::
{tag(), attr_list(), tree()}
| {tag(), attr_list(), nil}
| comment_node()
| comment_node3()
@type format_flag() :: :html_atoms | :nil_self_closing | :comment_tuple3
@doc """
Returns a tree representation from the given html string.
## Examples
iex> :fast_html.decode("<h1>Hello world</h1>")
{"html", [], [{"head", [], []}, {"body", [], [{"h1", [], ["Hello world"]}]}]}
iex> :fast_html.decode("<span class='hello'>Hi there</span>")
{"html", [],
[{"head", [], []},
{"body", [], [{"span", [{"class", "hello"}], ["Hi there"]}]}]}
iex> :fast_html.decode("<body><!-- a comment --!></body>")
{"html", [], [{"head", [], []}, {"body", [], [comment: " a comment "]}]}
iex> :fast_html.decode("<br>")
{"html", [], [{"head", [], []}, {"body", [], [{"br", [], []}]}]}
"""
@spec decode(String.t()) :: tree()
def decode(bin) do
decode(bin, format: [])
end
@doc """
Returns a tree representation from the given html string.
This variant allows you to pass in one or more of the following format flags:
* `:html_atoms` uses atoms for known html tags (faster), binaries for everything else.
* `:nil_self_closing` uses `nil` to designate self-closing tags and void elements.
For example `<br>` is then being represented like `{"br", [], nil}`.
See http://w3c.github.io/html-reference/syntax.html#void-elements for a full list of void elements.
* `:comment_tuple3` uses 3-tuple elements for comments, instead of the default 2-tuple element.
## Examples
iex> :fast_html.decode("<h1>Hello world</h1>", format: [:html_atoms])
{:html, [], [{:head, [], []}, {:body, [], [{:h1, [], ["Hello world"]}]}]}
iex> :fast_html.decode("<br>", format: [:nil_self_closing])
{"html", [], [{"head", [], []}, {"body", [], [{"br", [], nil}]}]}
iex> :fast_html.decode("<body><!-- a comment --!></body>", format: [:comment_tuple3])
{"html", [], [{"head", [], []}, {"body", [], [{:comment, [], " a comment "}]}]}
iex> html = "<body><!-- a comment --!><unknown /></body>"
iex> :fast_html.decode(html, format: [:html_atoms, :nil_self_closing, :comment_tuple3])
{:html, [],
[{:head, [], []},
{:body, [], [{:comment, [], " a comment "}, {"unknown", [], nil}]}]}
"""
@spec decode(String.t(), format: [format_flag()]) :: tree()
def decode(bin, format: flags) do
- {:ok, {:myhtml_worker, res}} = Nodex.Cnode.call(FastHtml.Cnode, {:decode, bin, flags})
+ {:ok, res} = FastHtml.Cnode.call({:decode, bin, flags})
res
end
end
diff --git a/lib/fast_html/application.ex b/lib/fast_html/application.ex
index 08243a0..48f6696 100644
--- a/lib/fast_html/application.ex
+++ b/lib/fast_html/application.ex
@@ -1,68 +1,53 @@
defmodule FastHtml.Application do
@moduledoc false
use Application
- application = Mix.Project.config()[:app]
-
- defp random_sname, do: :crypto.strong_rand_bytes(4) |> Base.encode16(case: :lower)
-
- defp sname, do: :"myhtml_#{random_sname()}"
+ def random_sname, do: :crypto.strong_rand_bytes(4) |> Base.encode16(case: :lower)
def start(_type, _args) do
- import Supervisor.Spec
-
case maybe_setup_node() do
{:error, message} -> raise message
_ -> :ok
end
- myhtml_worker = Path.join(:code.priv_dir(unquote(application)), "myhtml_worker")
-
- children = [
- worker(Nodex.Cnode, [
- %{exec_path: myhtml_worker, sname: sname()},
- [name: FastHtml.Cnode]
- ])
- ]
-
- Supervisor.start_link(children, strategy: :one_for_one, name: FastHtml.Supervisor)
+ Supervisor.start_link([FastHtml.Cnode], strategy: :one_for_one, name: FastHtml.Supervisor)
end
defp maybe_setup_node() do
with {_, false} <- {:alive, Node.alive?()},
{:ok, epmd_path} <- find_epmd(),
:ok <- start_epmd(epmd_path),
{:ok, _pid} = pid_tuple <- start_node() do
pid_tuple
else
{:alive, _} ->
:ok
{:error, _} = e ->
e
end
end
defp find_epmd() do
case System.find_executable("epmd") do
nil ->
{:error,
"Could not find epmd executable. Please ensure the location it's in is present in your PATH or start epmd manually beforehand"}
executable ->
{:ok, executable}
end
end
defp start_epmd(path) do
case System.cmd(path, ["-daemon"]) do
{_result, 0} -> :ok
{_result, exit_code} -> {:error, "Could not start epmd, exit code: #{exit_code}"}
end
end
defp start_node() do
Node.start(:"master_#{random_sname()}@127.0.0.1")
end
end
diff --git a/lib/fast_html/cnode.ex b/lib/fast_html/cnode.ex
new file mode 100644
index 0000000..c1faa6e
--- /dev/null
+++ b/lib/fast_html/cnode.ex
@@ -0,0 +1,137 @@
+defmodule FastHtml.Cnode do
+ @moduledoc false
+
+ @spawn_inactive_timeout 10000
+
+ application = Mix.Project.config()[:app]
+
+ use GenServer
+ require Logger
+
+ def start_link(args) do
+ GenServer.start_link(__MODULE__, args, name: __MODULE__)
+ end
+
+ def init(args) do
+ args =
+ if args == [] do
+ %{}
+ else
+ args
+ end
+
+ exec_path = Path.join(:code.priv_dir(unquote(application)), "myhtml_worker")
+
+ sname = Map.get_lazy(args, :sname, &default_sname/0)
+ hostname = Map.get_lazy(args, :hostname, &master_hostname/0)
+ addr = :"#{sname}@#{hostname}"
+
+ spawn_inactive_timeout = Map.get(args, :spawn_inactive_timeout, @spawn_inactive_timeout)
+
+ state = %{
+ exec_path: exec_path,
+ sname: sname,
+ addr: addr,
+ hostname: hostname,
+ spawn_inactive_timeout: spawn_inactive_timeout
+ }
+
+ connect_or_spawn_cnode(state)
+ end
+
+ defp default_sname, do: "myhtml_#{FastHtml.Application.random_sname()}"
+ defp master_sname, do: Node.self() |> to_string |> String.split("@") |> List.first()
+ defp master_hostname, do: Node.self() |> to_string |> String.split("@") |> List.last()
+
+ defp connect_or_spawn_cnode(state) do
+ case connect_cnode(state) do
+ {:stop, _} -> spawn_cnode(state)
+ {:ok, state} -> state
+ end
+ end
+
+ defp connect_cnode(%{addr: addr} = state) do
+ if Node.connect(addr) do
+ Logger.debug("connected to #{addr}")
+ {:ok, state}
+ else
+ Logger.debug("connecting to #{addr} failed")
+ {:stop, :cnode_connection_fail}
+ end
+ end
+
+ defp spawn_cnode(%{exec_path: exec_path, sname: sname, hostname: hostname} = state) do
+ Logger.debug("Spawning #{sname}@#{hostname}")
+
+ cookie = :erlang.get_cookie()
+
+ port =
+ Port.open({:spawn_executable, exec_path}, [
+ :binary,
+ :exit_status,
+ :stderr_to_stdout,
+ line: 4096,
+ args: [sname, hostname, cookie, master_sname()]
+ ])
+
+ pid = Keyword.get(Port.info(port), :os_pid)
+ state = Map.put(state, :pid, pid)
+ await_cnode_ready(port, state)
+ end
+
+ defp await_cnode_ready(
+ port,
+ %{spawn_inactive_timeout: timeout, addr: addr} = state
+ ) do
+ ready_line = to_string(addr) <> " ready"
+
+ receive do
+ {^port, {:data, {:eol, ^ready_line}}} ->
+ connect_cnode(state)
+
+ {^port, {:data, {:eol, line}}} ->
+ Logger.debug("c-node is saying: #{line}")
+ await_cnode_ready(port, state)
+
+ {^port, {:exit_status, exit_status}} ->
+ Logger.debug("unexpected c-node exit: #{exit_status}")
+ {:stop, :cnode_unexpected_exit}
+
+ message ->
+ Logger.warn("unhandled message while waiting for cnode to be ready:\n#{inspect(message)}")
+ await_cnode_ready(port, state)
+ after
+ timeout ->
+ {:stop, :spawn_inactive_timeout}
+ end
+ end
+
+ def handle_info({:nodedown, _cnode}, state) do
+ {:stop, :nodedown, state}
+ end
+
+ def handle_info(msg, state) do
+ Logger.warn("unhandled handle_info: #{inspect(msg)}")
+ {:noreply, state}
+ end
+
+ def handle_call(:addr, _from, %{addr: addr} = state) do
+ {:reply, addr, state}
+ end
+
+ def terminate(_reason, %{pid: pid}) when pid != nil do
+ System.cmd("kill", ["-9", to_string(pid)])
+ :normal
+ end
+
+ def call(msg, timeout \\ 10000) do
+ node = GenServer.call(__MODULE__, :addr)
+ send({nil, node}, msg)
+
+ receive do
+ {:myhtml_worker, res} -> {:ok, res}
+ after
+ timeout -> {:error, :timeout}
+ end
+ end
+end
diff --git a/mix.exs b/mix.exs
index ff8ad8d..27366d5 100644
--- a/mix.exs
+++ b/mix.exs
@@ -1,150 +1,146 @@
defmodule FastHTML.Mixfile do
use Mix.Project
def project do
[
app: :fast_html,
version: "0.2.1",
elixir: "~> 1.5",
deps: deps(),
package: package(),
compilers: [:fast_html_cnode_make] ++ Mix.compilers(),
build_embedded: Mix.env() == :prod,
start_permanent: Mix.env() == :prod,
name: "FastHTML",
description: """
A module to decode HTML into a tree,
porting all properties of the underlying
library myhtml, being fast and correct
in regards to the html spec.
Originally based on Myhtmlex.
""",
docs: docs()
]
end
def package do
[
maintainers: ["Ariadne Conill"],
licenses: ["GNU LGPL"],
links: %{
"GitLab" => "https://git.pleroma.social/pleroma/fast_html",
"Issues" => "https://git.pleroma.social/pleroma/fast_html/issues",
"MyHTML" => "https://github.com/lexborisov/myhtml"
},
files: [
"lib",
"c_src",
"priv/.gitignore",
"test",
"Makefile",
"mix.exs",
"README.md",
"LICENSE"
]
]
end
def application do
[
extra_applications: [:logger],
mod: {FastHtml.Application, []},
# used to detect conflicts with other applications named processes
registered: [FastHtml.Cnode, FastHtml.Supervisor]
]
end
defp deps do
[
# documentation helpers
{:ex_doc, ">= 0.0.0", only: :dev},
# benchmarking helpers
- {:benchee, "~> 1.0", only: :dev},
- # cnode helpers
- {:nodex,
- git: "https://git.pleroma.social/pleroma/nodex",
- ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"}
+ {:benchee, "~> 1.0", only: :dev}
]
end
defp docs do
[
main: "FastHTML"
]
end
end
defmodule Mix.Tasks.Compile.FastHtmlCnodeMake do
@artifacts [
"priv/myhtml_worker"
]
def find_make do
_make_cmd =
System.get_env("MAKE") ||
case :os.type() do
{:unix, :freebsd} -> "gmake"
{:unix, :openbsd} -> "gmake"
{:unix, :netbsd} -> "gmake"
{:unix, :dragonfly} -> "gmake"
_ -> "make"
end
end
defp otp_version do
:erlang.system_info(:otp_release)
|> to_string()
|> String.to_integer()
end
defp otp_22_or_newer? do
otp_version() >= 22
end
def run(_) do
make_cmd = find_make()
exit_code =
if match?({:win32, _}, :os.type()) do
IO.warn("Windows is not yet a target.")
1
else
{result, exit_code} =
System.cmd(
make_cmd,
@artifacts,
stderr_to_stdout: true,
env: [
{"MIX_ENV", to_string(Mix.env())},
{"OTP22_DEF", (otp_22_or_newer?() && "YES") || "NO"}
]
)
IO.binwrite(result)
exit_code
end
if exit_code == 0 do
:ok
else
{:error,
[
%Mix.Task.Compiler.Diagnostic{
compiler_name: "FastHTML Cnode",
message: "Make exited with #{exit_code}",
severity: :error,
file: nil,
position: nil
}
]}
end
end
def clean() do
make_cmd = find_make()
{result, _error_code} = System.cmd(make_cmd, ["clean"], stderr_to_stdout: true)
Mix.shell().info(result)
:ok
end
end
diff --git a/mix.lock b/mix.lock
index 1686bba..7152a93 100644
--- a/mix.lock
+++ b/mix.lock
@@ -1,9 +1,8 @@
%{
"benchee": {:hex, :benchee, "1.0.1", "66b211f9bfd84bd97e6d1beaddf8fc2312aaabe192f776e8931cb0c16f53a521", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}], "hexpm"},
- "cnodex": {:git, "https://github.com/Overbryd/cnodex.git", "c1c4cde21295db07f87bb74006ab5f7222720db9", []},
"deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm"},
"earmark": {:hex, :earmark, "1.2.3", "206eb2e2ac1a794aa5256f3982de7a76bf4579ff91cb28d0e17ea2c9491e46a4", [:mix], [], "hexpm"},
"ex_doc": {:hex, :ex_doc, "0.16.3", "cd2a4cfe5d26e37502d3ec776702c72efa1adfa24ed9ce723bb565f4c30bd31a", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"},
"myhtml": {:git, "https://github.com/lexborisov/myhtml.git", "fe2cf577570666d058a2b7167c26d3384a758e19", [branch: "master"]},
"nodex": {:git, "https://git.pleroma.social/pleroma/nodex", "cb6730f943cfc6aad674c92161be23a8411f15d1", [ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"]},
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Thu, Oct 29, 4:18 AM (7 h, 20 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1797527
Default Alt Text
(14 KB)
Attached To
Mode
R16 fast_html
Attached
Detach File
Event Timeline