// Imports ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
import gleam/bool
import gleam/int
import gleam/javascript/promise.{type Promise}
import lustre
import lustre/attribute.{type Attribute}
import lustre/component
import lustre/effect.{type Effect}
import lustre/element.{type Element}
import lustre/element/html
// Main ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
pub fn register() -> Result(Nil, lustre.Error) {
let component =
lustre.component(init, update, view, [
component.on_attribute_change("value", fn(value) {
Ok(ParentChangedValue(value))
}),
])
lustre.register(component, "fishgirl-diagram")
}
pub fn element(
attributes attributes: List(Attribute(message)),
) -> Element(message) {
element.element("fishgirl-diagram", attributes, [])
}
pub fn from(value: String) -> Attribute(message) {
attribute.value(value)
}
// Model ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
type Model {
Model(
mermaidcode: String,
output_svg: Result(String, String),
random_id: String,
)
}
fn init(_) -> #(Model, Effect(Message)) {
let Nil = ts_init_mermaid()
let random_id =
"fishgirl-mermaid-" <> int.random(10_000_000) |> int.to_string()
#(Model("", Ok(""), random_id), effect.none())
}
// Update ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
type Message {
ParentChangedValue(String)
MermaidRenderFinish(Result(String, String))
}
fn update(model: Model, message: Message) -> #(Model, Effect(Message)) {
case message {
ParentChangedValue(value) -> #(
Model(
mermaidcode: value,
output_svg: "" |> Ok,
random_id: model.random_id,
),
render_mermaid(value, model.random_id),
)
MermaidRenderFinish(new_svg) -> #(
Model(
mermaidcode: model.mermaidcode,
output_svg: new_svg,
random_id: model.random_id,
),
effect.none(),
)
}
}
// View ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
fn view(model: Model) -> Element(Message) {
use <- bool.guard(
model.mermaidcode == "",
html.div([attribute.id(model.random_id)], [
element.text("No Mermaid code to parse from."),
]),
)
use <- bool.guard(model.output_svg == Ok(""), element.text("Rendering..."))
case model.output_svg {
Ok(svg) -> {
element.unsafe_raw_html(
"",
"figure",
[attribute.id(model.random_id)],
svg,
)
}
Error(errmsg) ->
html.div([attribute.id(model.random_id)], [
html.p([], [
element.text("An error occured parsing this diagram:"),
]),
html.pre([], [
element.text(errmsg),
]),
])
}
}
// Effects ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@external(erlang, "stdlib", "Nil")
fn render_mermaid(value: String, id: String) -> Effect(Message) {
use send <- effect.from
let _ =
promise.await(ts_render_mermaid(value:, id:), fn(svg) {
send(MermaidRenderFinish(svg))
|> promise.resolve
})
Nil
}
// FFI
@external(javascript, "./fishgirl_ffi.mjs", "renderMermaid")
fn ts_render_mermaid(
value _: String,
id _: String,
) -> Promise(Result(String, String)) {
panic as "Rendering only works on the JS target."
}
@external(javascript, "./fishgirl_ffi.mjs", "init")
fn ts_init_mermaid() -> Nil {
Nil
}