diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/gemini.gleam | 49 | ||||
| -rw-r--r-- | src/gemtext/blockquote.gleam | 20 | ||||
| -rw-r--r-- | src/gemtext/gemtext.gleam | 14 | ||||
| -rw-r--r-- | src/gemtext/heading.gleam | 17 | ||||
| -rw-r--r-- | src/gemtext/list.gleam | 16 | ||||
| -rw-r--r-- | src/gemtext/parse.gleam | 49 | ||||
| -rw-r--r-- | src/gemtext/preformatted_block.gleam | 34 | ||||
| -rw-r--r-- | src/gopher.gleam | 62 | ||||
| -rw-r--r-- | src/momoka.gleam | 34 | ||||
| -rw-r--r-- | src/tcp.gleam | 16 |
10 files changed, 311 insertions, 0 deletions
diff --git a/src/gemini.gleam b/src/gemini.gleam new file mode 100644 index 0000000..9cd0884 --- /dev/null +++ b/src/gemini.gleam @@ -0,0 +1,49 @@ +import envoy +import gemtext/parse +import gleam/bit_array +import gleam/bytes_builder +import gleam/http/request +import gleam/httpc +import gleam/string +import gopher + +pub fn get_gemtext_from_capsule(message) { + let root_capsule = case envoy.get("ROOT") { + Ok(capsule) -> capsule + _ -> "fuwn.me" + } + // Gleam isn't mature enough to even have an SSL/TLS libraries that I could + // find, and Erlang FFI is unclear, so Momoka uses the + // [September](https://github.com/gemrest/september) proxy deployed at + // <https://fuwn.me> to fetch the raw Gemini content. + // + // I'm sure as the language grows, this will be replaced with a more direct + // approach. + let gemini_proxy = case envoy.get("GEMINI_PROXY") { + Ok(proxy) -> proxy + _ -> "https://fuwn.me/raw/" + } + let assert Ok(request) = case bit_array.to_string(message) { + Ok(path) -> { + case path { + "/\r\n" | "\r\n" -> request.to(gemini_proxy <> root_capsule) + "/proxy/" <> route -> + request.to(gemini_proxy <> string.replace(route, "\r\n", "")) + "/" <> path -> + request.to( + gemini_proxy + <> root_capsule + <> "/" + <> string.replace(path, "\r\n", ""), + ) + _ -> request.to(root_capsule <> string.replace(path, "\r\n", "")) + } + } + _ -> request.to(root_capsule) + } + let assert Ok(response) = httpc.send(request) + + bytes_builder.from_string( + gopher.gemtext_to_gopher(parse.parse_gemtext_raw(response.body)) <> "\r\n", + ) +} diff --git a/src/gemtext/blockquote.gleam b/src/gemtext/blockquote.gleam new file mode 100644 index 0000000..94aa937 --- /dev/null +++ b/src/gemtext/blockquote.gleam @@ -0,0 +1,20 @@ +import gemtext/gemtext.{type Gemtext} +import gleam/list +import gleam/string + +pub fn combine_adjacent_blockquote_lines(lines: List(Gemtext)) -> List(Gemtext) { + case lines { + [gemtext.BlockquoteLine(a), gemtext.BlockquoteLine(b), ..rest] -> + combine_adjacent_blockquote_lines([ + gemtext.Blockquote(string.join([a, b], "\n")), + ..rest + ]) + [gemtext.Blockquote(a), gemtext.BlockquoteLine(b), ..rest] -> + combine_adjacent_blockquote_lines([ + gemtext.Blockquote(string.join([a, b], "\n")), + ..rest + ]) + [g, ..rest] -> list.append([g], combine_adjacent_blockquote_lines(rest)) + [] -> [] + } +} diff --git a/src/gemtext/gemtext.gleam b/src/gemtext/gemtext.gleam new file mode 100644 index 0000000..a1dd3a5 --- /dev/null +++ b/src/gemtext/gemtext.gleam @@ -0,0 +1,14 @@ +import gleam/option.{type Option} + +pub type Gemtext { + Text(String) + Link(to: String, Option(String)) + Heading(String, depth: Int) + ListLine(String) + List(List(List(String))) + BlockquoteLine(String) + Blockquote(String) + Preformatted(description: Option(String), body: String) + PreformattedLine(String) + Whitespace +} diff --git a/src/gemtext/heading.gleam b/src/gemtext/heading.gleam new file mode 100644 index 0000000..153153e --- /dev/null +++ b/src/gemtext/heading.gleam @@ -0,0 +1,17 @@ +import gleam/string + +pub fn count_leading_hashes(line: String) -> Int { + do_count_leading_hashes(string.to_graphemes(line), 0) +} + +fn do_count_leading_hashes(characters: List(String), accumulator: Int) -> Int { + case characters { + [c, ..rest] -> { + case c { + "#" -> do_count_leading_hashes(rest, accumulator + 1) + _ -> accumulator + } + } + _ -> accumulator + } +} diff --git a/src/gemtext/list.gleam b/src/gemtext/list.gleam new file mode 100644 index 0000000..23a73c7 --- /dev/null +++ b/src/gemtext/list.gleam @@ -0,0 +1,16 @@ +import gemtext/gemtext.{type Gemtext} +import gleam/list + +pub fn group_adjacent_list_items(lines: List(Gemtext)) -> List(Gemtext) { + case lines { + [gemtext.ListLine(a), gemtext.ListLine(b), ..rest] -> + group_adjacent_list_items([gemtext.List([[a], [b]]), ..rest]) + [gemtext.List(lists), gemtext.ListLine(item), ..rest] -> + group_adjacent_list_items([ + gemtext.List(list.append(lists, [[item]])), + ..rest + ]) + [g, ..rest] -> list.append([g], group_adjacent_list_items(rest)) + [] -> [] + } +} diff --git a/src/gemtext/parse.gleam b/src/gemtext/parse.gleam new file mode 100644 index 0000000..f26cf3d --- /dev/null +++ b/src/gemtext/parse.gleam @@ -0,0 +1,49 @@ +import gemtext/blockquote +import gemtext/gemtext.{type Gemtext} +import gemtext/heading +import gemtext/list as gemtext_list +import gemtext/preformatted_block +import gleam/list +import gleam/option.{None, Some} +import gleam/string + +pub fn parse_gemtext_line(line) -> Gemtext { + let trimmed_line = string.trim(line) + + case string.split(trimmed_line, " ") { + ["=>", to] -> gemtext.Link(to, None) + ["=>", to, ..title] -> gemtext.Link(to, Some(string.join(title, " "))) + [">", ..rest] -> + gemtext.BlockquoteLine(string.trim_left(string.join(rest, " "))) + ["*", ..rest] -> gemtext.ListLine(string.trim_left(string.join(rest, " "))) + [""] -> gemtext.Whitespace + _ -> { + case string.to_graphemes(trimmed_line) { + ["#", ..rest] -> + gemtext.Heading( + string.trim_left(string.replace(string.join(rest, ""), "#", "")), + heading.count_leading_hashes(line), + ) + ["`", "`", "`", ..rest] -> + gemtext.PreformattedLine(string.trim_left( + "```\n" <> string.join(rest, ""), + )) + _ -> gemtext.Text(line) + } + } + } +} + +pub fn parse_gemtext(text: String) -> List(Gemtext) { + string.split(text, "\n") + |> preformatted_block.group_adjacent_preformatted_block_lines + |> list.map(parse_gemtext_line) + |> gemtext_list.group_adjacent_list_items + |> blockquote.combine_adjacent_blockquote_lines +} + +pub fn parse_gemtext_raw(text: String) -> List(Gemtext) { + string.split(text, "\n") + |> preformatted_block.group_adjacent_preformatted_block_lines + |> list.map(parse_gemtext_line) +} diff --git a/src/gemtext/preformatted_block.gleam b/src/gemtext/preformatted_block.gleam new file mode 100644 index 0000000..5e94fef --- /dev/null +++ b/src/gemtext/preformatted_block.gleam @@ -0,0 +1,34 @@ +import gleam/list +import gleam/string + +pub fn group_adjacent_preformatted_block_lines( + lines: List(String), +) -> List(String) { + case lines { + ["```", desc, ..rest] -> { + let #(body, rest) = do_group_adjacent_preformatted_block_lines(rest, []) + list.append( + ["```" <> desc <> "\n" <> body <> "\n```"], + group_adjacent_preformatted_block_lines(rest), + ) + } + [line, ..rest] -> + list.append([line], group_adjacent_preformatted_block_lines(rest)) + [] -> [] + } +} + +fn do_group_adjacent_preformatted_block_lines( + lines: List(String), + accumulator: List(String), +) { + case lines { + ["```", ..rest] -> #(string.join(accumulator, "\n"), rest) + [line, ..rest] -> + do_group_adjacent_preformatted_block_lines( + rest, + list.append(accumulator, [line]), + ) + [] -> #(string.join(accumulator, "\n"), []) + } +} diff --git a/src/gopher.gleam b/src/gopher.gleam new file mode 100644 index 0000000..044ee40 --- /dev/null +++ b/src/gopher.gleam @@ -0,0 +1,62 @@ +import gemtext/gemtext.{type Gemtext} +import gleam/list +import gleam/option.{None, Some} +import gleam/string + +pub fn gemtext_to_gopher(gemtext: List(Gemtext)) -> String { + gemtext + |> list.map(fn(line) { + case line { + gemtext.PreformattedLine(content) -> + content + |> string.split("\n") + |> list.map(fn(line) { + case line { + "```" -> "" + _ -> "i`` " <> line + } + }) + |> string.join("\n") + _ -> gemtext_line_to_gopher_line(line) + } + }) + |> string.join("\n") +} + +fn extract_domain_from_url(url: String) -> String { + case string.split(url, "://") { + [_, rest] -> { + case string.split(rest, "/") { + [domain, _] -> domain + _ -> rest + } + } + _ -> "null.host" + } +} + +fn gopher_link_line(to, description) { + "h" + <> description + <> "\tURL:" + <> to + <> "\t" + <> extract_domain_from_url(to) + <> "\t70" +} + +pub fn gemtext_line_to_gopher_line(line: Gemtext) -> String { + case line { + gemtext.Text(text) -> "i" <> text + gemtext.Link(to, description) -> + case description { + Some(description) -> gopher_link_line(to, description) + None -> gopher_link_line(to, to) + } + gemtext.Heading(text, depth) -> + "i" <> list.repeat("#", depth) |> string.join("") <> " " <> text + gemtext.ListLine(text) -> "i* " <> text + gemtext.BlockquoteLine(text) -> "i> " <> text + _ -> "" + } +} diff --git a/src/momoka.gleam b/src/momoka.gleam new file mode 100644 index 0000000..6b49c87 --- /dev/null +++ b/src/momoka.gleam @@ -0,0 +1,34 @@ +import envoy +import gemini +import gleam/erlang/process +import gleam/int +import gleam/option.{None} +import gleam/otp/actor +import glisten.{Packet} +import tcp + +pub fn main() { + let assert Ok(_) = + glisten.handler( + fn(_connection) { #(Nil, None) }, + fn(message, state, connection) { + let assert Packet(message) = message + let assert Ok(_) = + glisten.send(connection, gemini.get_gemtext_from_capsule(message)) + let assert Ok(_) = tcp.close_connection(connection) + + actor.continue(state) + }, + ) + |> glisten.serve(case envoy.get("PORT") { + Ok(port) -> { + case int.base_parse(port, 10) { + Ok(port) -> port + _ -> 70 + } + } + _ -> 70 + }) + + process.sleep_forever() +} diff --git a/src/tcp.gleam b/src/tcp.gleam new file mode 100644 index 0000000..30e443e --- /dev/null +++ b/src/tcp.gleam @@ -0,0 +1,16 @@ +import gleam/erlang/atom.{type Atom} +import glisten +import glisten/socket.{type Socket, type SocketReason} + +@external(erlang, "glisten_tcp_ffi", "shutdown") +fn do_shutdown(socket: Socket, write: Atom) -> Result(Nil, SocketReason) + +pub fn shutdown(socket: Socket) -> Result(Nil, SocketReason) { + let assert Ok(write) = atom.from_string("write") + + do_shutdown(socket, write) +} + +pub fn close_connection(connection: glisten.Connection(a)) { + shutdown(connection.socket) +} |