Skip to main content

src/fishgirl.gleam

// 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
}