Current section
Files
Jump to
Current section
Files
src/glemtext.gleam
import gleam/option.{type Option, None, Some}
import gleam/string
import gleam/list
/// A `Document` is a list of Gemtext elements. As Gemtext "elements"
/// are not nested, there is no need for a nested structure here, either.
///
/// As such, `Document` is a type alias to `List(GemElement)`
pub type Document =
List(GemElement)
/// `GemElement` is an element, or possible line type for Gemtext.
pub type GemElement {
/// `Text` is any text that doesn't have specific syntax for creating
/// an element. Note that empty lines are `Text("")`
Text(String)
/// A link to another document, both Gemini and non-Gemini,
/// and optionally, a label.
Link(to: String, label: Option(String))
Heading(level: Int, text: String)
ListItem(String)
Blockquote(String)
Preformatted(alt: Option(String), text: String)
}
/// `parse` parses a string into a Gemtext Document.
/// It will always return a valid document, and does not panic.
pub fn parse(text: String) -> Document {
text
|> string.replace("\r\n", "\n")
|> string.to_graphemes
|> parse_document([])
|> list.reverse
}
fn parse_document(in: Chars, ast: Document) -> Document {
case in {
["=", ">", ..rest] -> {
let #(link, in) = parse_link(rest)
parse_document(in, [link, ..ast])
}
["*", " ", ..rest] -> {
let #(item, in) = parse_list(rest)
parse_document(in, [item, ..ast])
}
[">", ..rest] -> {
let #(quote, in) = parse_blockquote(rest)
parse_document(in, [quote, ..ast])
}
["#", " ", ..rest] -> {
let #(heading, in) = parse_heading(1, rest)
parse_document(in, [heading, ..ast])
}
["#", "#", " ", ..rest] -> {
let #(heading, in) = parse_heading(2, rest)
parse_document(in, [heading, ..ast])
}
["#", "#", "#", " ", ..rest] -> {
let #(heading, in) = parse_heading(3, rest)
parse_document(in, [heading, ..ast])
}
["`", "`", "`", ..rest] -> {
let #(preformatted, in) = parse_preformatted(rest)
parse_document(in, [preformatted, ..ast])
}
[] -> ast
_ -> {
let #(text, in) = parse_text(in)
parse_document(in, [text, ..ast])
}
}
}
fn parse_blockquote(in: Chars) -> #(GemElement, Chars) {
let #(line, rest) = get_line(in)
let line =
line
|> drop_ws
|> string.join("")
#(Blockquote(line), rest)
}
fn parse_text(in: Chars) -> #(GemElement, Chars) {
let #(line, rest) = get_line(in)
let line = string.join(line, "")
#(Text(line), rest)
}
fn parse_preformatted(in: Chars) -> #(GemElement, Chars) {
let #(line, rest) = get_line(in)
let alt =
line
|> drop_ws
|> string.join("")
let alt = case alt {
"" -> None
_ -> Some(alt)
}
let #(pre_lines, in) = take_preformatted_lines(rest)
let pre_lines =
pre_lines
|> string.join("\n")
#(Preformatted(alt: alt, text: pre_lines), in)
}
fn take_preformatted_lines(in: Chars) -> #(List(String), Chars) {
let #(line, rest) = get_line(in)
case line {
["`", "`", "`", ..rest] -> {
let #(_, rest) = get_line(rest)
#([], rest)
}
[] -> #([], [])
_ -> {
let #(next, in) = take_preformatted_lines(rest)
let line =
line
|> string.join("")
#([line, ..next], in)
}
}
}
fn parse_heading(level: Int, in: Chars) -> #(GemElement, Chars) {
let #(line, rest) = get_line(in)
let line =
line
|> drop_ws
|> string.join("")
#(Heading(level, line), rest)
}
fn parse_link(in: Chars) -> #(GemElement, Chars) {
let #(line, rest) = get_line(in)
let #(url, linerest) =
line
|> drop_ws
|> list.split_while(fn(c) { !{ c == " " || c == "\t" } })
let url = string.join(url, "")
let label =
linerest
|> drop_ws
|> string.join("")
let label = case label {
"" -> None
_ -> Some(label)
}
#(Link(to: url, label: label), rest)
}
fn parse_list(in: Chars) -> #(GemElement, Chars) {
let #(line, rest) = get_line(in)
let line =
line
|> drop_ws
|> string.join("")
#(ListItem(line), rest)
}
fn get_line(in: Chars) -> #(Chars, Chars) {
let line =
in
|> list.take_while(until_nl)
let rest =
in
|> list.drop_while(until_nl)
|> list.drop(1)
#(line, rest)
}
fn until_nl(c) {
c != "\n"
}
fn drop_lines(in: Chars) {
case in {
["\n", ..rest] -> drop_lines(rest)
_ -> in
}
}
fn drop_ws(in: Chars) {
case in {
[" ", ..rest] -> drop_ws(rest)
["\t", ..rest] -> drop_ws(rest)
_ -> in
}
}
type Chars =
List(String)