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