gemini-ex/lib/gemini.ex

120 lines
3.1 KiB
Elixir

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")
sock
|> receive_loop()
|> Gemini.Response.parse()
end
end
defp receive_loop(sock, acc \\ []) do
res = Socket.Stream.recv(sock)
case res do
{:ok, nil} ->
Socket.Stream.close!(sock)
acc
|> Enum.reverse()
|> IO.iodata_to_binary()
{:ok, data} ->
receive_loop(sock, [data | acc])
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_start, String.t() | nil}
| :preformatted_end
| {:preformatted, String.t()}
| {:heading, String.t(), 1 | 2 | 3}
| {:list_item, String.t()}
| {:quoted, String.t()}
@link_line_regex ~r/\s*([^\s]+)(?:\s+(.+))?/
@spec parse(String.t()) :: [line()]
@doc """
Parses a `text/gemini` document into its lines.
"""
def parse(doc) do
{lines, _} =
doc
|> String.split("\n")
|> Enum.reduce({[], false}, fn line, {lines, in_preformatting} ->
preformatting_toggle = match?("```" <> _, line)
cond do
preformatting_toggle && in_preformatting ->
{[:preformatted_end | lines], false}
preformatting_toggle && !in_preformatting ->
"```" <> alt = line
alt = if alt == "", do: nil, else: alt
{[{:preformatted_start, alt} | lines], true}
in_preformatting ->
{[{:preformatted, line} | lines], true}
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}
"###" <> rest ->
{[{:heading, String.trim(rest), 3} | lines], false}
"##" <> rest ->
{[{:heading, String.trim(rest), 2} | lines], false}
"#" <> rest ->
{[{:heading, String.trim(rest), 1} | lines], false}
"* " <> rest ->
{[{:list_item, String.trim(rest)} | lines], false}
">" <> rest ->
{[{:quoted, String.trim(rest)} | lines], false}
line ->
{[{:text, line} | lines], false}
end
end
end)
Enum.reverse(lines)
end
end