Skip to content

Commit

Permalink
GRAPHS!!!
Browse files Browse the repository at this point in the history
  • Loading branch information
Zhincore committed Jan 10, 2025
1 parent ca1fbff commit 28c52d3
Show file tree
Hide file tree
Showing 12 changed files with 258 additions and 23 deletions.
3 changes: 3 additions & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@
"@codemirror/state": "^6.5.1",
"@codemirror/theme-one-dark": "github:codemirror/theme-one-dark",
"@codemirror/view": "^6.36.2",
"@mermaid-js/layout-elk": "^0.1.7",
"codemirror": "^6.0.1",
"mermaid": "^11.4.1",
"panzoom": "^9.4.3",
"solid-js": "^1.9.4",
"solid-markdown": "2.0.1",
"yaml": "^2.7.0"
Expand Down
20 changes: 13 additions & 7 deletions web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,24 @@ import { lazy, Match, Suspense, Switch } from "solid-js";
const Editor = lazy(() =>
import("./modes/editor/Editor").then((m) => ({ default: m.Editor }))
);
const Graph = lazy(() =>
import("./modes/graph/Graph").then((m) => ({ default: m.Graph }))
);

export function App() {
const editorMode = window.location.search.slice(1) == "editor";
const mode = window.location.search.slice(1);
const initialStep = window.location.hash.slice(1);

return (
<Switch fallback={<Guide content={CONTENT} initialStep={initialStep} />}>
<Match when={editorMode}>
<Suspense fallback={<p>Loading...</p>}>
<Suspense fallback={<p>Loading...</p>}>
<Switch fallback={<Guide content={CONTENT} initialStep={initialStep} />}>
<Match when={mode == "editor"}>
<Editor />
</Suspense>
</Match>
</Switch>
</Match>
<Match when={mode == "graph"}>
<Graph />
</Match>
</Switch>
</Suspense>
);
}
6 changes: 6 additions & 0 deletions web/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@
height: 100%;
}

.full-window {
overflow: hidden;
width: 100%;
height: 100%;
}

@layer utils {
.button-list {
display: inline-grid;
Expand Down
39 changes: 35 additions & 4 deletions web/src/modes/editor/CodeMirror.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,60 @@
import { EditorView, basicSetup } from "codemirror";
import { onCleanup } from "solid-js";
import { createEffect, onCleanup } from "solid-js";
import { yaml } from "@codemirror/lang-yaml";
import { indentWithTab } from "@codemirror/commands";
import { oneDark } from "@codemirror/theme-one-dark";
import { keymap } from "@codemirror/view";
import { keymap, ViewPlugin } from "@codemirror/view";
import { customLinter } from "./linter";
import { lintGutter } from "@codemirror/lint";

export interface CodeMirrorProps {
content?: string;
initialContent?: string;
showStep?: string;
onChange?: (content: string) => void;
}

export function CodeMirror(props: CodeMirrorProps) {
let view = new EditorView({
doc: props.content,
doc: props.initialContent,
extensions: [
basicSetup,
oneDark,
yaml(),
customLinter,
lintGutter(),
keymap.of([indentWithTab]),
ViewPlugin.define((view) => ({
update(update) {
if (update.docChanged) {
props.onChange?.(view.state.doc.toString());
}
},
})),
],
});

function findStep(step: string) {
let i = 1;
for (const line of view.state.doc.iterLines()) {
if (line.startsWith(step)) return i;
i++;
}
}

async function scrollToStep(step: string) {
if (!step) return;
const lineNumber = await findStep(step);
if (lineNumber == undefined) return;
const line = view.state.doc.line(lineNumber);
if (!line) return;
const block = view.lineBlockAt(line.from);
view.scrollDOM.scrollTo({ ...block, behavior: "smooth" });
}

createEffect(async () => {
if (props.showStep) await scrollToStep(props.showStep);
});

onCleanup(() => view.destroy());

return <>{view.dom}</>;
Expand Down
59 changes: 56 additions & 3 deletions web/src/modes/editor/Editor.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,66 @@
import contentRaw from "$content/guide.yaml?raw";
import { CodeMirror } from "./CodeMirror";
import {
createSignal,
ErrorBoundary,
lazy,
Suspense,
createMemo,
} from "solid-js";
import YAML from "yaml";
import { contentToMermaid } from "../graph/mermaid";
import { Button } from "$components/Button";

import "./editor.css";
import { download } from "./util";

const Mermaid = lazy(() =>
import("../graph/Mermaid").then((m) => ({ default: m.Mermaid }))
);

export function Editor() {
const [showStep, setShowStep] = createSignal<string>();
const [content, setContent] = createSignal(contentRaw);
const mermaid = createMemo(() => contentToMermaid(YAML.parse(content())));

return (
<div id="editor">
<CodeMirror content={contentRaw} />
<div>E</div>
<div id="editor" class="full-window">
<div>
<CodeMirror
initialContent={content()}
onChange={setContent}
showStep={showStep()}
/>
</div>

<div class="full-window">
<div class="toolbar">
<Button onClick={() => download("guide.yaml", content())}>
Download YAML
</Button>
<Button
onClick={() =>
download("guide.md", "```mermaid\n" + mermaid() + "\n```")
}
>
Download Mermaid
</Button>
</div>

<Suspense fallback={<p>Loading...</p>}>
<ErrorBoundary
fallback={(err) => (
<div>
Error rendering graph
<br />
<pre>{err.toString()}</pre>
</div>
)}
>
<Mermaid content={mermaid()} onClick={setShowStep} />
</ErrorBoundary>
</Suspense>
</div>
</div>
);
}
16 changes: 11 additions & 5 deletions web/src/modes/editor/editor.css
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
body {
overflow: hidden;
}

#editor {
flex-grow: 1;

width: 100vw;
height: 100vh;

display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-columns: repeat(2, minmax(0, 1fr));
}

#editor .toolbar {
background: #222;
padding: 0.25rem;
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
position: relative;
z-index: 10;
}

.cm-editor {
Expand Down
15 changes: 11 additions & 4 deletions web/src/modes/editor/linter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Step } from "$lib/content";
import { ensureSyntaxTree, Language, syntaxTree } from "@codemirror/language";
import { ensureSyntaxTree, syntaxTree } from "@codemirror/language";
import { linter, Diagnostic } from "@codemirror/lint";

type SyntaxNodeRef = Parameters<
Expand Down Expand Up @@ -146,11 +146,18 @@ export const customLinter = linter(
tree.cursor().iterate(enter, leave);

// Analyze steps
const stepKeys = new Set([...steps.keys()]);
console.log(stepKeys);
for (const [key, step] of steps) {
if (!step.title) {
diagnostics.push({
from: step.keyNode.from,
to: step.keyNode.to,
severity: "error",
message: `Step '${key}' does not have a 'title'!`,
});
}

for (const [target, optionNodes] of step.optionTargets) {
if (!stepKeys.has(target)) {
if (!steps.has(target)) {
for (const optionNode of optionNodes) {
diagnostics.push({
from: optionNode.from,
Expand Down
14 changes: 14 additions & 0 deletions web/src/modes/editor/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export function download(
filename: string,
content: string,
type = "text/plain"
) {
const blob = new Blob([content], { type });
const href = URL.createObjectURL(blob);

const a = document.createElement("a");
a.setAttribute("download", filename);
a.setAttribute("href", href);
a.click();
URL.revokeObjectURL(href);
}
11 changes: 11 additions & 0 deletions web/src/modes/graph/Graph.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { CONTENT } from "$lib/content";
import { contentToMermaid } from "./mermaid";
import { Mermaid } from "./Mermaid";

export function Graph() {
return (
<div class="full-window">
<Mermaid content={contentToMermaid(CONTENT)} />
</div>
);
}
53 changes: 53 additions & 0 deletions web/src/modes/graph/Mermaid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { createResource, onMount, onCleanup } from "solid-js";
import mermaid from "mermaid";
import elkLayouts from "@mermaid-js/layout-elk";
import panzoom, { PanZoom } from "panzoom";

import "./mermaid.css";

mermaid.registerLayoutLoaders(elkLayouts);
mermaid.initialize({
startOnLoad: false,
theme: "dark",
layout: "elk",
elk: {
nodePlacementStrategy: "LINEAR_SEGMENTS",
cycleBreakingStrategy: "MODEL_ORDER",
},
});

export interface MermaidProps {
content: string;
onClick?: (step: string) => void;
}

export function Mermaid(props: MermaidProps) {
const [svg] = createResource(async () => {
const { svg } = await mermaid.render("mermaid", props.content);
return svg;
});

let element: HTMLDivElement | undefined = undefined;
let controls: PanZoom;

onMount(() => {
controls = panzoom(element!, {
bounds: true,
boundsPadding: 0.0,
});
});

onCleanup(() => {
controls.dispose();
});

function onClick(ev: MouseEvent) {
const element = ev.target as Element;
const parent = element.closest("g.node");
if (!parent) return;
const step = parent.getAttribute("id")?.split("-").slice(1, -1).join("-");
if (step) props.onClick?.(step);
}

return <div id="mermaid" ref={element} onClick={onClick} innerHTML={svg()} />;
}
11 changes: 11 additions & 0 deletions web/src/modes/graph/mermaid.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#mermaid .node.default {
cursor: pointer;
}

#mermaid .node.default .label-container {
transition: fill 0.1s;
}

#mermaid .node.default:hover .label-container {
fill: #333;
}
34 changes: 34 additions & 0 deletions web/src/modes/graph/mermaid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Content } from "$lib/content";

export function contentToMermaid(content: Content) {
let mermaid = `graph TD\n`;

for (const [stepName, step] of Object.entries(content)) {
let title = `"${step.title.replace('"', "#quot;")}"`;
if (stepName == "start" || !step.options?.length) {
title = "([" + title + "])";
} else if ((step.options?.length ?? 0) > 1) {
title = "{{" + title + "}}";
} else {
title = "[" + title + "]";
}

if (step.options?.length) {
let first = true;
for (const option of step.options) {
mermaid += `\t${stepName}`;

if (first) {
mermaid += `${title}`;
first = false;
}

mermaid += ` --"${option.label}"--> ${option.target}\n`;
}
} else {
mermaid += `\t${stepName}${title}\n`;
}
}

return mermaid;
}

0 comments on commit 28c52d3

Please sign in to comment.