Initial commit
This commit is contained in:
commit
18da6d23e1
|
@ -0,0 +1,4 @@
|
||||||
|
# Used by "mix format"
|
||||||
|
[
|
||||||
|
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||||
|
]
|
|
@ -0,0 +1,31 @@
|
||||||
|
# The directory Mix will write compiled artifacts to.
|
||||||
|
/_build/
|
||||||
|
|
||||||
|
# If you run "mix test --cover", coverage assets end up here.
|
||||||
|
/cover/
|
||||||
|
|
||||||
|
# The directory Mix downloads your dependencies sources to.
|
||||||
|
/deps/
|
||||||
|
|
||||||
|
# Where 3rd-party dependencies like ExDoc output generated docs.
|
||||||
|
/doc/
|
||||||
|
|
||||||
|
# Ignore .fetch files in case you like to edit your project deps locally.
|
||||||
|
/.fetch
|
||||||
|
|
||||||
|
# If the VM crashes, it generates a dump, let's ignore it too.
|
||||||
|
erl_crash.dump
|
||||||
|
|
||||||
|
# Also ignore archive artifacts (built via "mix archive.build").
|
||||||
|
*.ez
|
||||||
|
|
||||||
|
# Ignore package tarball (built via "mix hex.build").
|
||||||
|
frenzy-*.tar
|
||||||
|
|
||||||
|
# Files matching config/*.secret.exs pattern contain sensitive
|
||||||
|
# data and you should not commit them into version control.
|
||||||
|
#
|
||||||
|
# Alternatively, you may comment the line below and commit the
|
||||||
|
# secrets files as long as you replace their contents by environment
|
||||||
|
# variables.
|
||||||
|
/config/*.secret.exs
|
|
@ -0,0 +1,21 @@
|
||||||
|
# Gemini
|
||||||
|
|
||||||
|
**TODO: Add description**
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
|
||||||
|
by adding `gemini` to your list of dependencies in `mix.exs`:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
def deps do
|
||||||
|
[
|
||||||
|
{:gemini, "~> 0.1.0"}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
|
||||||
|
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
|
||||||
|
be found at [https://hexdocs.pm/gemini](https://hexdocs.pm/gemini).
|
||||||
|
|
|
@ -0,0 +1,101 @@
|
||||||
|
defmodule Gemini do
|
||||||
|
@moduledoc """
|
||||||
|
Documentation for `Gemini`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@spec request(URI.t() | String.t()) :: {:ok, Gemini.Response.t()} | {:error, term()}
|
||||||
|
@doc """
|
||||||
|
Sends a Gemini request to the given URI.
|
||||||
|
"""
|
||||||
|
def request(uri) when is_binary(uri) do
|
||||||
|
request(URI.parse(uri))
|
||||||
|
end
|
||||||
|
|
||||||
|
def request(%URI{host: host} = uri) do
|
||||||
|
port = gemini_port(uri)
|
||||||
|
|
||||||
|
case Socket.SSL.connect(host, port) do
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
|
||||||
|
{:ok, sock} ->
|
||||||
|
Socket.Stream.send(sock, "#{URI.to_string(uri)}\r\n")
|
||||||
|
|
||||||
|
case Socket.Stream.recv(sock) do
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
|
||||||
|
{:ok, data} ->
|
||||||
|
Gemini.Response.parse(data)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp gemini_port(%URI{scheme: "gemini", port: nil}), do: 1965
|
||||||
|
defp gemini_port(%URI{scheme: "gemini", port: port}), do: port
|
||||||
|
|
||||||
|
@type line ::
|
||||||
|
{:text, String.t()}
|
||||||
|
| {:link, URI.t(), String.t() | nil}
|
||||||
|
| {:preformatted, String.t(), String.t() | nil}
|
||||||
|
| {:heading, String.t(), 1 | 2 | 3}
|
||||||
|
| {:list_item, String.t()}
|
||||||
|
| {:quoted, String.t()}
|
||||||
|
|
||||||
|
@link_line_regex ~r/\s*([^\s]+)(?:\s+([^\s]+))?/
|
||||||
|
|
||||||
|
@spec parse(String.t()) :: [line()]
|
||||||
|
|
||||||
|
def parse(doc) do
|
||||||
|
{lines, _, _} =
|
||||||
|
doc
|
||||||
|
|> String.split("\n")
|
||||||
|
|> Enum.reduce({[], false, nil}, fn line, {lines, in_preformatting, preformatting_alt} ->
|
||||||
|
preformatting_toggle = match?("```" <> _, line)
|
||||||
|
|
||||||
|
cond do
|
||||||
|
preformatting_toggle && in_preformatting ->
|
||||||
|
{lines, false, nil}
|
||||||
|
|
||||||
|
preformatting_toggle && !in_preformatting ->
|
||||||
|
"```" <> alt = line
|
||||||
|
{lines, true, alt}
|
||||||
|
|
||||||
|
in_preformatting ->
|
||||||
|
{[{:preformatted, line, preformatting_alt} | lines], true, preformatting_alt}
|
||||||
|
|
||||||
|
true ->
|
||||||
|
case line do
|
||||||
|
"=>" <> rest ->
|
||||||
|
{link, text} =
|
||||||
|
case Regex.run(@link_line_regex, rest) do
|
||||||
|
[_, link] -> {link, nil}
|
||||||
|
[_, link, text] -> {link, text}
|
||||||
|
end
|
||||||
|
|
||||||
|
{[{:link, URI.parse(link), text} | lines], false, nil}
|
||||||
|
|
||||||
|
"###" <> rest ->
|
||||||
|
{[{:heading, String.trim(rest), 3} | lines], false, nil}
|
||||||
|
|
||||||
|
"##" <> rest ->
|
||||||
|
{[{:heading, String.trim(rest), 2} | lines], false, nil}
|
||||||
|
|
||||||
|
"#" <> rest ->
|
||||||
|
{[{:heading, String.trim(rest), 1} | lines], false, nil}
|
||||||
|
|
||||||
|
"* " <> rest ->
|
||||||
|
{[{:list_item, String.trim(rest)} | lines], false, nil}
|
||||||
|
|
||||||
|
">" <> rest ->
|
||||||
|
{[{:quoted, String.trim(rest)} | rest], false, nil}
|
||||||
|
|
||||||
|
line ->
|
||||||
|
{[{:text, line} | lines], false, nil}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
lines
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,62 @@
|
||||||
|
defmodule Gemini.Response do
|
||||||
|
@moduledoc """
|
||||||
|
A response to a Gemini protocol request.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@enforce_keys [:status, :meta]
|
||||||
|
defstruct [:status, :meta, :body]
|
||||||
|
|
||||||
|
@type t :: %__MODULE__{
|
||||||
|
status: integer(),
|
||||||
|
meta: String.t(),
|
||||||
|
body: nil | binary()
|
||||||
|
}
|
||||||
|
|
||||||
|
@spec parse(data :: binary()) :: {:ok, t()} | {:error, term()}
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Parse a Gemini response from the given data.
|
||||||
|
"""
|
||||||
|
def parse(<<status::binary-size(2), " ", rest::binary>>) do
|
||||||
|
status = String.to_integer(status)
|
||||||
|
|
||||||
|
case parse_meta(rest) do
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
|
||||||
|
{:ok, meta, body} ->
|
||||||
|
{
|
||||||
|
:ok,
|
||||||
|
%__MODULE__{
|
||||||
|
status: status,
|
||||||
|
meta: meta,
|
||||||
|
body: body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse(_) do
|
||||||
|
{:error, "Expected Gemini response to begin with two digit status code and space"}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_meta(data, acc \\ [], length \\ 0)
|
||||||
|
|
||||||
|
defp parse_meta(<<"\r\n", rest::binary>>, acc, _length) do
|
||||||
|
{
|
||||||
|
:ok,
|
||||||
|
acc
|
||||||
|
|> Enum.reverse()
|
||||||
|
|> :erlang.list_to_binary(),
|
||||||
|
rest
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_meta(_data, _acc, 1024) do
|
||||||
|
{:error, "Expected meta string in Gemini response to be no longer than 1024 bytes"}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_meta(<<c::binary-size(1), rest::binary>>, acc, length) do
|
||||||
|
parse_meta(rest, [c | acc], length + 1)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,27 @@
|
||||||
|
defmodule Gemini.MixProject do
|
||||||
|
use Mix.Project
|
||||||
|
|
||||||
|
def project do
|
||||||
|
[
|
||||||
|
app: :gemini,
|
||||||
|
version: "0.1.0",
|
||||||
|
elixir: "~> 1.10",
|
||||||
|
start_permanent: Mix.env() == :prod,
|
||||||
|
deps: deps()
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Run "mix help compile.app" to learn about applications.
|
||||||
|
def application do
|
||||||
|
[
|
||||||
|
extra_applications: [:logger]
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Run "mix help deps" to learn about dependencies.
|
||||||
|
defp deps do
|
||||||
|
[
|
||||||
|
{:socket, "~> 0.3.13"}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,3 @@
|
||||||
|
%{
|
||||||
|
"socket": {:hex, :socket, "0.3.13", "98a2ab20ce17f95fb512c5cadddba32b57273e0d2dba2d2e5f976c5969d0c632", [:mix], [], "hexpm", "f82ea9833ef49dde272e6568ab8aac657a636acb4cf44a7de8a935acb8957c2e"},
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
defmodule GeminiTest do
|
||||||
|
use ExUnit.Case
|
||||||
|
doctest Gemini
|
||||||
|
|
||||||
|
test "greets the world" do
|
||||||
|
assert Gemini.hello() == :world
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1 @@
|
||||||
|
ExUnit.start()
|
Loading…
Reference in New Issue