diff options
author | Hayleigh Thompson <me@hayleigh.dev> | 2023-08-22 23:39:04 +0100 |
---|---|---|
committer | Hayleigh Thompson <me@hayleigh.dev> | 2023-08-22 23:39:04 +0100 |
commit | e0e041cbf64a7fb75957606bdeb9c6376666d785 (patch) | |
tree | a482df4cec16c43157d537f17fea5561c7e13d73 | |
parent | ae00383ec6cb1d78a48a3e59e51c5e1920c0e889 (diff) | |
download | lustre-e0e041cbf64a7fb75957606bdeb9c6376666d785.tar.gz lustre-e0e041cbf64a7fb75957606bdeb9c6376666d785.zip |
:sparkles: [docs] Implement markdown parsing and rendering.
-rw-r--r-- | docs/src/app.ffi.mjs | 95 | ||||
-rw-r--r-- | docs/src/app/ui/markdown.gleam | 116 |
2 files changed, 211 insertions, 0 deletions
diff --git a/docs/src/app.ffi.mjs b/docs/src/app.ffi.mjs new file mode 100644 index 0000000..f5e7f29 --- /dev/null +++ b/docs/src/app.ffi.mjs @@ -0,0 +1,95 @@ +import { fromMarkdown } from "mdast-util-from-markdown"; +import { attribute } from "../lustre/lustre/attribute.mjs"; +import { element, text } from "../lustre/lustre/element.mjs"; +import { List } from "./gleam.mjs"; +import * as Markdown from "./app/ui/markdown.mjs"; + +// Parsing markdown and then walking the AST is expensive so we're going to trade +// some memory for speed and cache the results. +const cache = new Map(); + +// fn parse_markdown(md: String) -> #(List(Element(msg)), List(Element(msg))) +export const parse_markdown = (md) => { + if (cache.has(md)) return cache.get(md); + + const ast = fromMarkdown(md); + const summary = []; + const content = ast.children.map(function to_lustre_element(node) { + switch (node.type) { + case "code": + return Markdown.code(node.value); + + case "emphasis": + return Markdown.emphasis( + List.fromArray(node.children.map(to_lustre_element)) + ); + + case "heading": { + const [title, rest] = node.children[0].value.split("|"); + const tags = List.fromArray(rest ? rest.trim().split(" ") : []); + const id = + /^[A-Z]/.test(title.trim()) && node.depth === 3 + ? `${title.toLowerCase().trim().replace(/\s/g, "-")}-type` + : `${title.toLowerCase().trim().replace(/\s/g, "-")}`; + + if (node.depth > 1) { + summary.push( + element( + "a", + List.fromArray([ + attribute("href", `#${id}`), + attribute("class", "text-sm text-gray-400 no-underline"), + attribute("class", "hover:text-gray-700 hover:underline"), + attribute( + "class", + node.depth === 2 ? `mt-4 first:mt-0` : `ml-2` + ), + ]), + List.fromArray([text(title.trim())]) + ) + ); + } + + return Markdown.heading(node.depth, title.trim(), tags, id); + } + + case "inlineCode": + return Markdown.inline_code(node.value); + + case "link": + return Markdown.link(node.url, List.fromArray(node.children)); + + case "list": + return Markdown.list( + !!node.ordered, + List.fromArray(node.children.map(to_lustre_element)) + ); + + case "listItem": + return Markdown.list_item( + List.fromArray(node.children.map(to_lustre_element)) + ); + + case "paragraph": + return Markdown.paragraph( + List.fromArray(node.children.map(to_lustre_element)) + ); + + case "strong": + return Markdown.strong( + List.fromArray(node.children.map(to_lustre_element)) + ); + + case "text": + return Markdown.text(node.value); + + default: + return Markdown.text(""); + } + }); + + const result = [List.fromArray(content), List.fromArray(summary)]; + cache.set(md, result); + + return result; +}; diff --git a/docs/src/app/ui/markdown.gleam b/docs/src/app/ui/markdown.gleam new file mode 100644 index 0000000..8bc00da --- /dev/null +++ b/docs/src/app/ui/markdown.gleam @@ -0,0 +1,116 @@ +// IMPORTS --------------------------------------------------------------------- + +import gleam/int +import gleam/list +import lustre/attribute +import lustre/element.{Element} +import lustre/element/html + +// MARKDOWN ELEMENTS ----------------------------------------------------------- + +pub fn code(src: String) -> Element(msg) { + html.pre([], [html.code([], [element.text(src)])]) +} + +pub fn emphasis(content: List(Element(msg))) { + html.em([], content) +} + +pub fn heading( + depth: Int, + title: String, + tags: List(String), + id: String, +) -> Element(msg) { + let depth = int.min(depth, 3) + let tags = list.map(tags, heading_tag) + + case depth { + 1 -> + html.h1( + [attribute.class("flex items-center justify-between"), attribute.id(id)], + [ + heading_title(title, id), + html.p([attribute.class("flex gap-4")], tags), + ], + ) + 2 -> + html.h2( + [ + attribute.class("flex items-center justify-between border-t"), + attribute.id(id), + ], + [ + heading_title(title, id), + html.p([attribute.class("flex gap-4")], tags), + ], + ) + 3 -> + html.h3( + [attribute.class("flex items-center justify-between"), attribute.id(id)], + [ + heading_title(title, id), + html.p([attribute.class("flex gap-2")], tags), + ], + ) + } +} + +fn heading_title(title: String, href: String) -> Element(msg) { + html.span( + [attribute.class("group")], + [ + element.text(title), + html.a( + [ + attribute.href("#" <> href), + attribute.class("pl-2 text-gray-200 opacity-0 transition-opacity"), + attribute.class("group-hover:underline group-hover:opacity-100"), + ], + [element.text("#")], + ), + ], + ) +} + +fn heading_tag(tag: String) -> Element(msg) { + html.span( + [ + attribute.class( + "px-2 py-1 text-xs text-gray-700 border border-gray-200 rounded", + ), + ], + [element.text(tag)], + ) +} + +pub fn inline_code(src: String) -> Element(msg) { + html.code([], [element.text(src)]) +} + +pub fn link(href: String, content: List(Element(msg))) -> Element(msg) { + html.a([attribute.href(href)], content) +} + +pub fn list(ordered: Bool, items: List(Element(msg))) -> Element(msg) { + case ordered { + True -> html.ol([], items) + False -> html.ul([], items) + } +} + +pub fn list_item(content: List(Element(msg))) -> Element(msg) { + html.li([], content) +} + +pub fn paragraph(content: List(Element(msg))) -> Element(msg) { + html.p([], content) +} + +pub fn strong(content: List(Element(msg))) -> Element(msg) { + html.strong([], content) +} + +pub fn text(content: String) -> Element(msg) { + element.text(content) +} |