Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Visualized the Grammar AST #216

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
67 changes: 58 additions & 9 deletions hugo/assets/scripts/minilogo/minilogo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ let shouldAnimate = true;
interface PreviewProps {
commands?: Command[];
diagnostics?: Diagnostic[];
hidden?: boolean;
}

interface DrawCanvasProps {
Expand Down Expand Up @@ -157,11 +158,13 @@ class DrawCanvas extends React.Component<DrawCanvasProps, DrawCanvasProps> {
}
class Preview extends React.Component<PreviewProps, PreviewProps> {
canvasRef: React.RefObject<HTMLCanvasElement>;
hidden: boolean;
constructor(props: PreviewProps) {
super(props);
this.state = {
commands: props.commands,
diagnostics: props.diagnostics,
hidden: false,
};
this.canvasRef = createRef();
this.startPreview = this.startPreview.bind(this);
Expand All @@ -171,7 +174,24 @@ class Preview extends React.Component<PreviewProps, PreviewProps> {
this.setState({ commands, diagnostics });
}

hide() {
this.setState({ hidden: true });
}

show() {
this.setState({ hidden: false });
}

isHidden(): boolean {
return this.hidden;
}

render() {
if (this.props.hidden) {
return;
}


// check if code contains an astNode
if (!this.state.commands) {
// Show the exception
Expand Down Expand Up @@ -210,12 +230,15 @@ class Preview extends React.Component<PreviewProps, PreviewProps> {

interface AppState {
currentExample: number;
currentWindow: 'preview' | 'editor';
}
class App extends React.Component<{}, AppState> {
monacoEditor: React.RefObject<MonacoEditorReactComp>;
preview: React.RefObject<Preview>;
copyHint: React.RefObject<HTMLDivElement>;
shareButton: React.RefObject<HTMLImageElement>;


constructor(props) {
super(props);

Expand All @@ -230,6 +253,7 @@ class App extends React.Component<{}, AppState> {

this.state = {
currentExample: 0,
currentWindow: 'editor'
};
}

Expand Down Expand Up @@ -306,32 +330,56 @@ class App extends React.Component<{}, AppState> {
<div className="justify-center self-center flex flex-col md:flex-row h-full w-full">
<div className="float-left w-full h-full flex flex-col">
<div className="border-solid border border-emeraldLangium bg-emeraldLangiumDarker flex items-center p-3 text-white font-mono ">
<span>Editor</span>
<span id="title">Editor</span>
<select className="ml-4 bg-emeraldLangiumDarker cursor-pointer border-0 border-b invalid:bg-emeraldLangiumABitDarker" onChange={(e) => this.setExample(parseInt(e.target.value))}>
{examples.map((example, index) => (
<option key={index} value={index}>{example.name}</option>
))}
</select>
<div className="flex flex-row justify-end w-full h-full gap-2">
<div>
{/* Button to switch between preview and editor */}
<button
className="bg-emeraldLangiumDarker hover:bg-emeraldLangiumABitDarker text-white border-emeraldLangium font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline sm:hidden"
type="button"
onClick={() => {
const preview = this.preview.current!;
if (preview.state.hidden) {
preview.show();
} else {
preview.hide();
}
}}
>
Switch
</button>
</div>
<div className="text-sm hidden" ref={this.copyHint}>Link was copied!</div>
<img src="/assets/share.svg" title="Copy URL to this grammar and content" className="inline w-4 h-4 cursor-pointer" ref={this.shareButton}></img>
</div>
</div>
<div className="wrapper relative bg-white dark:bg-gray-900 border border-emeraldLangium h-full w-full">
<MonacoEditorReactComp
ref={this.monacoEditor}
onLoad={this.onMonacoLoad}
userConfig={userConfig}
style={style}
/>
<div className="h-full w-full">
<MonacoEditorReactComp
ref={this.monacoEditor}
onLoad={this.onMonacoLoad}
userConfig={userConfig}
style={style}
className={this.preview.current && this.preview.current.state.hidden ? 'hidden' : ''}
/>
</div>
<div className="sm:hidden">
<Preview ref={this.preview} hidden={true} />
</div>

</div>
</div>
<div className="float-left w-full h-full flex flex-col" id="preview">
<div className="float-left w-full h-full flex flex-col hidden sm:block" id="preview">
<div className="border-solid border border-emeraldLangium bg-emeraldLangiumDarker flex items-center p-3 text-white font-mono ">
<span>Preview</span>
</div>
<div className="border border-emeraldLangium h-full w-full">
<Preview ref={this.preview} />
<Preview ref={this.preview} hidden={true} />
</div>
</div>
</div>
Expand All @@ -357,3 +405,4 @@ userConfig = createUserConfig({
});
const root = createRoot(document.getElementById("root") as HTMLElement);
root.render(<App />);

45 changes: 45 additions & 0 deletions hugo/content/playground/ForceGraph.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { AstNode } from "langium";
import { convertASTtoGraph } from "langium-ast-helper";
import React from "react";
import { ForceGraph3D } from "react-force-graph";
import { toHex } from "./preprocess";
import * as ReactDOM from "react-dom/client";

export let grammarRoot: ReactDOM.Root;

export function renderForceGraph(grammar?: AstNode) {
const location = document.getElementById("forcegraph-root")!;

if (!grammarRoot) {
// create a fresh root, if not already present
grammarRoot = ReactDOM.createRoot(location);
}

if (grammar) {
// the follow code is taken from https://github.com/TypeFox/language-engineering-visualization/blob/main/packages/visuals/src/index.ts
const graphData = convertASTtoGraph(grammar);
const gData = {
nodes: graphData.nodes.map(node => ({
id: (node as unknown as { $__dotID: string }).$__dotID,
nodeType: node.$type,
node
})),
links: graphData.edges.map(edge => ({
source: (edge.from as unknown as { $__dotID: string }).$__dotID,
target: (edge.to as unknown as { $__dotID: string }).$__dotID
}))
};

return grammarRoot.render(
<ForceGraph3D
showNavInfo={true}
width={window.innerWidth}
height={window.innerHeight}
graphData={gData}
nodeColor={node => toHex((node as any).nodeType)}
nodeLabel={node => (node as any).node.name ? `${(node as any).nodeType} - ${(node as any).node.name}` : (node as any).nodeType}
/>
);

}
}
23 changes: 14 additions & 9 deletions hugo/content/playground/Tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,19 @@ import { clsx } from "clsx";
import { AstNodeLocator } from "langium/lib/workspace/ast-node-locator";

export let treeRoot: ReactDOM.Root;
export type CurrentTreeWindow = "ast" | "grammar";

export function render(root: AstNode, locator: AstNodeLocator) {
const location = document.getElementById("ast-body")!;
const data = preprocessAstNodeObject(root, locator);

if (!treeRoot) {
// create a fresh root, if not already present
treeRoot = ReactDOM.createRoot(location);
}
treeRoot.render(

// generate the interactive tree
return treeRoot.render(
<ul>
<TreeNode root={data} hidden={false} />
</ul>
Expand All @@ -44,13 +48,13 @@ const TreeContent: FC<TreeProps> = ({ root, hidden }) => {
case "number":
case "string":
return (
<span className="literal">
{hidden
? "..."
: root.kind === "string"
<span className="literal">
{hidden
? "..."
: root.kind === "string"
? '"' + root.value + '"'
: root.value.toString()}
</span>
</span>
);
case "object":
return (
Expand All @@ -77,10 +81,10 @@ const TreeContent: FC<TreeProps> = ({ root, hidden }) => {
</>
);
case "array":
if(root.children.length === 0) {
if (root.children.length === 0) {
return <span className="opening-brace">{"[]"}</span>
}
if(hidden) {
if (hidden) {
return <span className="opening-brace">{"[...]"}</span>
}
return (
Expand Down Expand Up @@ -108,7 +112,7 @@ const TreeContent: FC<TreeProps> = ({ root, hidden }) => {
return (
<>
{hidden ? <span className="link">{"Reference(...)"}</span> :
<span className="link">Reference('{root.$text}')</span>}
<span className="link">Reference('{root.$text}')</span>}
</>
);
}
Expand All @@ -125,6 +129,7 @@ const TreeNode: FC<TreeProps> = ({ root, hidden }) => {

function Property({ p, comma }: { p: PropertyNode; comma: boolean }) {
const [open, setOpen] = useState(true);

return (
<li className={clsx("entry toggable", { closed: !open })}>
<span className={"value"}>
Expand Down
38 changes: 32 additions & 6 deletions hugo/content/playground/_index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
noMain: true
playground: true
---
<link rel="preload" as="script" href="./libs/worker/common.js"/>
<link rel="preload" as="script" href="./libs/worker/langiumServerWorker.js"/>
<link rel="preload" as="script" href="./libs/worker/userServerWorker.js"/>
<link rel="preload" as="script" href="./libs/worker/common.js" />
<link rel="preload" as="script" href="./libs/worker/langiumServerWorker.js" />
<link rel="preload" as="script" href="./libs/worker/userServerWorker.js" />
<script type="module">
import { addMonacoStyles, setupPlayground, share, overlay, getPlaygroundState, MonacoEditorLanguageClientWrapper } from "./libs/worker/common.js";
import { addMonacoStyles, setupPlayground, share, overlay, getPlaygroundState, MonacoEditorLanguageClientWrapper } from "./libs/worker/common.js";
import { buildWorkerDefinition } from "../libs/monaco-editor-workers/index.js";

addMonacoStyles('monaco-styles-helper');
Expand All @@ -26,13 +26,16 @@
);

// on doc load
addEventListener('load', function() {
addEventListener('load', function () {

// get a handle to our various interactive buttons
const copiedHint = document.getElementById('copiedHint');
const shareButton = document.getElementById('shareButton');
const grammarRoot = document.getElementById('grammar-root');
const contentRoot = document.getElementById('content-root');
let isTreeInteractive = false;



// register a listener for the share button
shareButton.onclick = () => {
Expand All @@ -52,18 +55,41 @@
};

const treeButton = document.getElementById('treeButton');
const forceGraphCloseButton = document.getElementById('forcegraph-close');
const forceGraphOpenButton = document.getElementById('forcegraph-open');

const forceGraphRoot = document.getElementById('forcegraph-root');
const forceGraphModal = document.getElementById('forcegraph-modal');

const grid = document.getElementById('grid');

const key = 'display-ast';
if(localStorage.getItem(key) === 'yes') {
if (localStorage.getItem(key) === 'yes') {
grid.classList.toggle('without-tree');
}

treeButton.onclick = () => {
const shown = !grid.classList.toggle('without-tree');
localStorage.setItem(key, shown ? 'yes' : 'no');
const resizeEvent = new Event('resize');
window.dispatchEvent(resizeEvent);
};

forceGraphCloseButton.onclick = () => {
forceGraphModal.classList.toggle('invisible');
};

forceGraphOpenButton.onclick = () => {
forceGraphModal.classList.toggle('invisible');
};

document.addEventListener('keydown', (event) => {
const graphShown = !forceGraphModal.classList.contains('invisible');
if (event.key === 'Escape' && graphShown) {
forceGraphModal.classList.toggle('invisible');
}
});

const url = new URL(window.location.toString());
const grammar = url.searchParams.get('grammar');
const content = url.searchParams.get('content');
Expand Down
Loading