aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHayleigh Thompson <me@hayleigh.dev>2023-08-22 23:39:04 +0100
committerHayleigh Thompson <me@hayleigh.dev>2023-08-22 23:39:04 +0100
commite0e041cbf64a7fb75957606bdeb9c6376666d785 (patch)
treea482df4cec16c43157d537f17fea5561c7e13d73
parentae00383ec6cb1d78a48a3e59e51c5e1920c0e889 (diff)
downloadlustre-e0e041cbf64a7fb75957606bdeb9c6376666d785.tar.gz
lustre-e0e041cbf64a7fb75957606bdeb9c6376666d785.zip
:sparkles: [docs] Implement markdown parsing and rendering.
-rw-r--r--docs/src/app.ffi.mjs95
-rw-r--r--docs/src/app/ui/markdown.gleam116
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)
+}