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 -> {[:preformatting_end | lines], false} preformatting_toggle && !in_preformatting -> "```" <> alt = line alt = if alt == "", do: nil, else: alt {[{:preformatting_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