120 lines
3.1 KiB
Elixir
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
|