Packages

Convert jot documents to lustre elements. Can convert to custom renderers

Current section

Files

Jump to
jotkey src jotkey.gleam
Raw

src/jotkey.gleam

// Fork code derived from https://github.com/lustre-labs/ssg/blob/main/src/lustre/ssg/djot.gleam
// IMPORTS ---------------------------------------------------------------------
import gleam/dict.{type Dict}
import gleam/list
import gleam/option.{type Option}
import jot
import lustre/attribute.{attribute}
import lustre/element.{type Element}
import lustre/element/html
// TYPES -----------------------------------------------------------------------
/// A renderer for a djot document, knows how to turn each block or inline element
/// into some custom view. That view could be anything, but it's typically a
/// Lustre element.
///
/// Some ideas for other renderers include:
///
/// - A renderer that turns a djot document into a JSON object
/// - A renderer that generates a table of contents
/// - A renderer that generates Nakai elements instead of Lustre ones
///
///
/// This renderer is compatible with **v1.0.2** of the [jot](https://hexdocs.pm/jot/jot.html)
/// package.
///
/// For more advanced usage check examples implementing context aware renderers
///
pub type Renderer(view, context) {
Renderer(
codeblock: fn(Dict(String, String), Option(String), String, context) -> view,
emphasis: fn(List(view), context) -> view,
heading: fn(Dict(String, String), Int, List(view), context) -> view,
link: fn(jot.Destination, Dict(String, String), List(view), context) -> view,
paragraph: fn(Dict(String, String), List(view), context) -> view,
strong: fn(List(view), context) -> view,
text: fn(String, context) -> view,
code: fn(String, context) -> view,
image: fn(jot.Destination, String, context) -> view,
linebreak: fn(context) -> view,
)
}
// CONSTRUCTORS ----------------------------------------------------------------
/// The default renderer generates some sensible Lustre elements from a djot
/// document. You can use this if you need a quick drop-in renderer for some
/// markup in a Lustre project.
///
pub fn default_renderer() -> Renderer(Element(msg), context) {
let to_attributes = fn(attrs) {
use attrs, key, val <- dict.fold(attrs, [])
[attribute(key, val), ..attrs]
}
Renderer(
codeblock: fn(attrs, lang, code, _context) {
let lang = option.unwrap(lang, "text")
html.pre(to_attributes(attrs), [
html.code([attribute("data-lang", lang)], [element.text(code)]),
])
},
emphasis: fn(content, _context) { html.em([], content) },
heading: fn(attrs, level, content, _context) {
case level {
1 -> html.h1(to_attributes(attrs), content)
2 -> html.h2(to_attributes(attrs), content)
3 -> html.h3(to_attributes(attrs), content)
4 -> html.h4(to_attributes(attrs), content)
5 -> html.h5(to_attributes(attrs), content)
6 -> html.h6(to_attributes(attrs), content)
_ -> html.p(to_attributes(attrs), content)
}
},
link: fn(destination, references, content, _context) {
case destination {
jot.Reference(ref) ->
case dict.get(references, ref) {
Ok(url) -> html.a([attribute.href(url)], content)
Error(_) -> html.a([attribute.href(ref)], content)
}
jot.Url(url) -> html.a([attribute("href", url)], content)
}
},
paragraph: fn(attrs, content, _context) {
html.p(to_attributes(attrs), content)
},
strong: fn(content, _context) { html.strong([], content) },
text: fn(text, _context) { element.text(text) },
code: fn(content, _context) { html.code([], [element.text(content)]) },
image: fn(destination, alt, _context) {
case destination {
jot.Reference(ref) -> html.img([attribute.src(ref), attribute.alt(alt)])
jot.Url(url) -> html.img([attribute.src(url), attribute.alt(alt)])
}
},
linebreak: fn(_context) { html.br([]) },
)
}
// CONVERSIONS -----------------------------------------------------------------
/// Render a djot document using the given renderer.
///
pub fn render(
document: jot.Document,
renderer: Renderer(view, context),
context: context,
) -> List(view) {
document.content
|> list.map(render_block(_, document.references, renderer, context))
}
fn render_block(
block: jot.Container,
references: Dict(String, String),
renderer: Renderer(view, context),
context: context,
) -> view {
case block {
jot.Paragraph(attrs, inline) -> {
renderer.paragraph(
attrs,
list.map(inline, render_inline(_, references, renderer, context)),
context,
)
}
jot.Heading(attrs, level, inline) -> {
renderer.heading(
attrs,
level,
list.map(inline, render_inline(_, references, renderer, context)),
context,
)
}
jot.Codeblock(attrs, language, code) -> {
renderer.codeblock(attrs, language, code, context)
}
}
}
fn render_inline(
inline: jot.Inline,
references: Dict(String, String),
renderer: Renderer(view, context),
context: context,
) -> view {
case inline {
jot.Text(text) -> {
renderer.text(text, context)
}
jot.Link(content, destination) -> {
renderer.link(
destination,
references,
list.map(content, render_inline(_, references, renderer, context)),
context,
)
}
jot.Emphasis(content) -> {
renderer.emphasis(
list.map(content, render_inline(_, references, renderer, context)),
context,
)
}
jot.Strong(content) -> {
renderer.strong(
list.map(content, render_inline(_, references, renderer, context)),
context,
)
}
jot.Code(content) -> {
renderer.code(content, context)
}
jot.Image(alt, destination) -> {
renderer.image(destination, text_content(alt), context)
}
jot.Linebreak -> {
renderer.linebreak(context)
}
}
}
// UTILS -----------------------------------------------------------------------
fn text_content(segments: List(jot.Inline)) -> String {
use text, inline <- list.fold(segments, "")
case inline {
jot.Text(content) -> text <> content
jot.Link(content, _) -> text <> text_content(content)
jot.Emphasis(content) -> text <> text_content(content)
jot.Strong(content) -> text <> text_content(content)
jot.Code(content) -> text <> content
jot.Image(_, _) -> text
jot.Linebreak -> text
}
}