Skip to content

Commit

Permalink
Report errors in usable format
Browse files Browse the repository at this point in the history
  • Loading branch information
rob-gordon committed May 29, 2023
1 parent dd265d6 commit 5ee2f60
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 28 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ d

This creates a graph with 4 nodes and 3 edges.

## Errors

In order to capture and display parsing errors in the editor, errors conform to the type `ParsingError` in `graph-selector/src/ParseError.ts`. Because in most application we imagine parsing will occur outside of the editor, displaying errors must also happen outside the error. A simple version of what that would look with monaco is below:

## Context

If you would like to find out more about the development and thought process behind this language, [A blog post](https://tone-row.com/blog/graph-syntax-css-selectors) has been published.
Expand Down
3 changes: 0 additions & 3 deletions examples/components/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@ export function Editor({
theme={highlight.defaultTheme}
beforeMount={highlight.registerHighlighter}
defaultLanguage={highlight.languageId}
onMount={(_editor, monaco) => {
monaco.editor.setTheme(highlight.defaultTheme);
}}
options={{
theme: highlight.defaultTheme,
fontSize: 16,
Expand Down
89 changes: 71 additions & 18 deletions examples/components/Sections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,71 @@ import {
YAxis,
} from "recharts";
import { CustomizedAxisTick, toPercent } from "./ReCharts";
import { Graph, parse, toCytoscapeElements } from "graph-selector";
import { Graph, ParseError, parse, toCytoscapeElements } from "graph-selector";
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";

import { CytoscapeGraph } from "./CytoscapeGraph";
import { D3Graph } from "./D3Graph";
import { Editor } from "./Editor";
import { FaGripLinesVertical } from "react-icons/fa";
import { SankeyChart } from "./SankeyChart";
import { useReducer } from "react";
import { useCallback, useReducer, useRef } from "react";
import { EditorProps } from "@monaco-editor/react";
import type { editor } from "monaco-editor";

type MonacoRefs = {
editor: editor.IStandaloneCodeEditor;
monaco: typeof import("monaco-editor");
};

type State = {
result: Graph;
error: string;
code: string;
};
const useCode = (initialCode: string) => {
return useReducer(
const refs = useRef<MonacoRefs | null>(null);
const onMount = useCallback<NonNullable<EditorProps["onMount"]>>(
(editor, monaco) => {
refs.current = { editor, monaco };
},
[]
);
const reducer = useReducer(
(s: State, n: string): State => {
try {
// remove modal markers
if (refs.current) {
const { editor, monaco } = refs.current;
const model = editor.getModel();
if (model) {
monaco.editor.setModelMarkers(model, "graph-selector", []);
}
}
const result = parse(n);
return { result, error: "", code: n };
} catch (e) {
console.log(e);
if (refs.current && isParseError(e)) {
const { editor, monaco } = refs.current;
const model = editor.getModel();
if (model) {
const { startLineNumber, endLineNumber, startColumn, endColumn } =
e;
const message = e.message;
const severity = monaco.MarkerSeverity.Error;
monaco.editor.setModelMarkers(model, "graph-selector", [
{
startLineNumber,
endLineNumber,
message,
severity,
startColumn,
endColumn,
},
]);
}
}
return {
result: { nodes: [], edges: [] },
error: (e as Error).message,
Expand All @@ -57,30 +100,40 @@ const useCode = (initialCode: string) => {
}
}
);

return [reducer[0], reducer[1], onMount] as const;
};

function isParseError(e: unknown): e is ParseError {
return e instanceof Error && e.name === "ParseError";
}

const idsClasses = `This is a label #a.large
(#c)
This is a longer label #b
(#c)
The longest label text of all #c`;
export function IdsClasses() {
const [state, dispatch] = useCode(idsClasses);
const [state, dispatch, onMount] = useCode(idsClasses);
return (
<PanelGroup direction="horizontal">
<Panel>
<Editor
value={state.code}
onChange={(value) => dispatch(value || "")}
/>
</Panel>
<PanelResizeHandle className="resize-handle grid place-content-center text-neutral-300 hover:text-neutral-600">
<FaGripLinesVertical />
</PanelResizeHandle>
<Panel>
<CytoscapeGraph elements={toCytoscapeElements(state.result)} />
</Panel>
</PanelGroup>
<>
<PanelGroup direction="horizontal">
<Panel>
<Editor
value={state.code}
onChange={(value) => dispatch(value || "")}
onMount={onMount}
/>
</Panel>
<PanelResizeHandle className="resize-handle grid place-content-center text-neutral-300 hover:text-neutral-600">
<FaGripLinesVertical />
</PanelResizeHandle>
<Panel>
<CytoscapeGraph elements={toCytoscapeElements(state.result)} />
</Panel>
</PanelGroup>
<div id="custom-container"></div>
</>
);
}

Expand Down
25 changes: 25 additions & 0 deletions graph-selector/src/ParseError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export class ParseError extends Error {
startLineNumber: number;
endLineNumber: number;
startColumn: number;
endColumn: number;
code: string;

constructor(
message: string,
startLineNumber: number,
endLineNumber: number,
startColumn: number,
endColumn: number,
/** A unique string referencing this error. Used for translations in consuming contexts. */
code: string,
) {
super(message);
this.name = "ParseError";
this.startLineNumber = startLineNumber;
this.endLineNumber = endLineNumber;
this.startColumn = startColumn;
this.endColumn = endColumn;
this.code = code;
}
}
1 change: 1 addition & 0 deletions graph-selector/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * as highlight from "./highlight";
export { toCytoscapeElements } from "./toCytoscapeElements";
export * from "./operate/operate";
export { stringify } from "./stringify";
export { ParseError } from "./ParseError";
67 changes: 60 additions & 7 deletions graph-selector/src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { getEdgeBreakIndex, getFeaturesIndex } from "./regexps";
import { getFeatureData } from "./getFeatureData";
import { matchAndRemovePointers } from "./matchAndRemovePointers";
import { strip } from "@tone-row/strip-comments";
import { ParseError } from "./ParseError";

// TODO: these types could probably be improved to match the target types (in ./types.ts) more closely

Expand Down Expand Up @@ -52,6 +53,8 @@ export function parse(text: string): Graph {
for (let line of lines) {
++lineNumber;

const originalLine = line;

// continue from empty line
if (!line.trim()) continue;

Expand Down Expand Up @@ -101,7 +104,14 @@ export function parse(text: string): Graph {

// throw if edge label and no indent
if (indentSize === 0 && edgeLabel) {
throw new Error(`Line ${lineNumber}: Edge label without parent`);
throw new ParseError(
`Line ${lineNumber}: Edge label without parent`,
lineNumber,
lineNumber,
0,
edgeLabel.length + 1,
"EDGE_LABEL_WITHOUT_PARENT",
);
}

// remove indent from line
Expand All @@ -119,7 +129,14 @@ export function parse(text: string): Graph {

// error if more than one pointer
if (pointers.length > 1) {
throw new Error(`Line ${lineNumber}: Can't create multiple pointers on same line`);
throw new ParseError(
`Line ${lineNumber}: Can't create multiple pointers on same line`,
lineNumber,
lineNumber,
0,
originalLine.length + 1,
"MULTIPLE_POINTERS_ON_SAME_LINE",
);
}

// the lable is what is left after everything is removed
Expand All @@ -138,12 +155,26 @@ export function parse(text: string): Graph {

// Throw if line has pointers and also opens container
if (pointers.length > 0 && isContainerStart) {
throw new Error(`Line ${lineNumber}: Can't create pointer and container on same line`);
throw new ParseError(
`Line ${lineNumber}: Can't create pointer and container on same line`,
lineNumber,
lineNumber,
originalLine.length,
originalLine.length + 1,
"POINTER_AND_CONTAINER_ON_SAME_LINE",
);
}

// error if line declares node and pointers
if (lineDeclaresNode && pointers.length > 0) {
throw new Error(`Line ${lineNumber}: Can't create node and pointer on same line`);
throw new ParseError(
`Line ${lineNumber}: Can't create node and pointer on same line`,
lineNumber,
lineNumber,
indentSize + 1,
originalLine.length + 1,
"NODE_AND_POINTER_ON_SAME_LINE",
);
}

// create a unique ID from line number
Expand All @@ -154,7 +185,14 @@ export function parse(text: string): Graph {

// Throw if id already exists
if (lineDeclaresNode && nodeIds.includes(id)) {
throw new Error(`Line ${lineNumber}: Duplicate node id "${id}"`);
throw new ParseError(
`Line ${lineNumber}: Duplicate node id "${id}"`,
lineNumber,
lineNumber,
indentSize + 1,
originalLine.length + 1,
"DUPLICATE_NODE_ID",
);
}

// Store id
Expand Down Expand Up @@ -217,7 +255,15 @@ export function parse(text: string): Graph {
edgeId = `${ancestor}-${id}-1`;
}
if (edgeIds.includes(edgeId)) {
throw new Error(`Line ${lineNumber}: Duplicate edge id "${edgeId}"`);
throw new ParseError(
`Line ${lineNumber}: Duplicate edge id "${edgeId}"`,
lineNumber,
lineNumber,
indentSize + 1,
// get length of edge id
indentSize + 1 + edgeId.length + 1,
"DUPLICATE_EDGE_ID",
);
}
edgeIds.push(edgeId);
edges.push({
Expand Down Expand Up @@ -320,7 +366,14 @@ export function parse(text: string): Graph {
}

if (edgeIds.includes(data.id)) {
throw new Error(`Line ${lineNumber}: Duplicate edge id "${data.id}"`);
throw new ParseError(
`Line ${lineNumber}: Duplicate edge id "${data.id}"`,
lineNumber,
lineNumber,
0,
lines[lineNumber - 1].length + 1,
"DUPLICATE_EDGE_ID",
);
}

edgeIds.push(data.id);
Expand Down

0 comments on commit 5ee2f60

Please sign in to comment.