Skip to content

Commit

Permalink
Sanitize user text on paste
Browse files Browse the repository at this point in the history
  • Loading branch information
rob-gordon committed Jul 17, 2024
1 parent d2a1438 commit 32d33ee
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 7 deletions.
2 changes: 1 addition & 1 deletion app/src/components/AiToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function AiToolbar() {
useEffect(() => {
if (userPasted) {
const timeout = setTimeout(() => {
useEditorStore.setState({ userPasted: false });
useEditorStore.setState({ userPasted: "" });
}, 15000);
return () => clearTimeout(timeout);
}
Expand Down
2 changes: 1 addition & 1 deletion app/src/components/ConvertToFlowchart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export function ConvertToFlowchart() {
})
.finally(() => {
stopConvert();
useEditorStore.setState({ userPasted: false });
useEditorStore.setState({ userPasted: "" });
});
}}
disabled={convertIsRunning}
Expand Down
38 changes: 36 additions & 2 deletions app/src/components/TextEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { updateModelMarkers, useEditorStore } from "../lib/useEditorStore";
import Loading from "./Loading";
import { usePromptStore } from "../lib/usePromptStore";
import classNames from "classnames";
import { sanitizeOnPaste } from "../lib/sanitizeOnPaste";

type TextEditorProps = EditorProps & {
extendOptions?: editor.IEditorOptions;
Expand Down Expand Up @@ -67,8 +68,19 @@ export function TextEditor({ extendOptions = {}, ...props }: TextEditorProps) {
});

// Listen to when the user pastes into the document
editor.onDidPaste(() => {
useEditorStore.setState({ userPasted: true });
editor.onDidPaste((e) => {
// get the text in the range
const text = editor.getModel()?.getValueInRange(e.range);
if (text) {
// store it in the editor
useEditorStore.setState({ userPasted: text });

// sanitize it if necessary
const sanitized = sanitizeOnPaste(text);
if (sanitized) {
replaceRange(editor, e.range, sanitized);
}
}
});
}}
wrapperProps={{
Expand Down Expand Up @@ -113,3 +125,25 @@ function useEditorHover(hoverLineNumber?: number) {
};
}, [hoverLineNumber]);
}

/**
* Given the instance of the editor, the range to replace, and the new text
* replace that range with the new text
*/
/**
* Given the instance of the editor, the range to replace, and the new text
* replace that range with the new text
*/
function replaceRange(
editor: editor.IStandaloneCodeEditor,
range: editor.ISingleEditOperation["range"],
text: string
) {
editor.executeEdits("", [
{
range: range,
text: text,
forceMoveMarkers: true,
},
]);
}
36 changes: 36 additions & 0 deletions app/src/lib/sanitizeOnPaste.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { sanitizeOnPaste } from "./sanitizeOnPaste";

describe("sanitizeOnPaste", () => {
test("returns null for valid text", () => {
expect(sanitizeOnPaste("hello world")).toBe(null);
});
test("returns text with escaped parentheses for invalid text", () => {
expect(sanitizeOnPaste("hello (world)")).toBe("hello \\(world\\)");
});

test("returns escaped for multiple pointers", () => {
expect(sanitizeOnPaste("hello (world) (again)")).toBe(
"hello \\(world\\) \\(again\\)"
);
});

test("handles colon", () => {
expect(sanitizeOnPaste("hello: world")).toBe("hello\\: world");
});

test("Edge missing indentation", () => {
expect(
sanitizeOnPaste(
"export function TextEditor({ extendOptions = {}, ...props }: TextEditorProps) {"
)
).toBe(
"export function TextEditor({ extendOptions = {}, ...props }\\: TextEditorProps) {"
);
});

test("Pointer bug", () => {
expect(
sanitizeOnPaste(`export function sanitizeOnPaste(text: string) {`)
).toBe(`export function sanitizeOnPaste\\(text\\: string\\) {`);
});
});
40 changes: 40 additions & 0 deletions app/src/lib/sanitizeOnPaste.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { parse } from "graph-selector";
import { isError } from "./helpers";

/**
* Given the pasted text, this function checks if it is valid
* Flowchart Fun Syntax.
* - If it returns any errors which indicate
* parentheses issues, it will escape parentheses in the text
* and return the sanitized text.
*/
export function sanitizeOnPaste(text: string) {
let newText: string | null = null,
hasError = true,
count = 0;

while (hasError && count < 10) {
count++;
try {
parse(getText());
hasError = false;
} catch (error) {
if (isError(error)) {
if (error.message.includes("pointer")) {
newText = getText().replace(/[()]/g, "\\$&");
} else if (error.message.includes("label without parent")) {
newText = getText().replace(/:/g, "\\:");
} else if (error.message.includes("missing indentation")) {
newText = getText().replace(/:/g, "\\:");
} else {
console.log(error.message);
}
}
}
}
return newText;

function getText() {
return newText ? newText : text;
}
}
6 changes: 3 additions & 3 deletions app/src/lib/useEditorStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ export const useEditorStore = create<{
markers: editor.IMarkerData[];
/** The current text selection */
selection: string;
/** Becomes true after the user pastes into the document */
userPasted: boolean;
/** Stores the text the user recently pasted into the editor */
userPasted: string;
}>((_set) => ({
editor: null,
monaco: null,
isDragging: false,
markers: [],
selection: "",
userPasted: false,
userPasted: "",
}));

export function updateModelMarkers() {
Expand Down

0 comments on commit 32d33ee

Please sign in to comment.