Initial commit
This commit is contained in:
commit
18da6d23e1
4
.formatter.exs
Normal file
4
.formatter.exs
Normal file
@ -0,0 +1,4 @@
|
||||
# Used by "mix format"
|
||||
[
|
||||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||
]
|
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal file
@ -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
|
21
README.md
Normal file
21
README.md
Normal file
@ -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).
|
||||
|
101
lib/gemini.ex
Normal file
101
lib/gemini.ex
Normal file
@ -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
|
62
lib/gemini/response.ex
Normal file
62
lib/gemini/response.ex
Normal file
@ -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
|
27
mix.exs
Normal file
27
mix.exs
Normal file
@ -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
|
3
mix.lock
Normal file
3
mix.lock
Normal file
@ -0,0 +1,3 @@
|
||||
%{
|
||||
"socket": {:hex, :socket, "0.3.13", "98a2ab20ce17f95fb512c5cadddba32b57273e0d2dba2d2e5f976c5969d0c632", [:mix], [], "hexpm", "f82ea9833ef49dde272e6568ab8aac657a636acb4cf44a7de8a935acb8957c2e"},
|
||||
}
|
8
test/gemini_test.exs
Normal file
8
test/gemini_test.exs
Normal file
@ -0,0 +1,8 @@
|
||||
defmodule GeminiTest do
|
||||
use ExUnit.Case
|
||||
doctest Gemini
|
||||
|
||||
test "greets the world" do
|
||||
assert Gemini.hello() == :world
|
||||
end
|
||||
end
|
1
test/test_helper.exs
Normal file
1
test/test_helper.exs
Normal file
@ -0,0 +1 @@
|
||||
ExUnit.start()
|
Loading…
x
Reference in New Issue
Block a user