Skip to content

Commit

Permalink
Merge pull request #8 from Mirascope/refine-first-version
Browse files Browse the repository at this point in the history
Refine first version
  • Loading branch information
Brendan Kao authored Oct 8, 2024
2 parents 3bc431e + a4c12a0 commit 9e30d49
Show file tree
Hide file tree
Showing 34 changed files with 741 additions and 197 deletions.
2 changes: 1 addition & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"build:notypescript": "vite build",
"build:notypescript": "vite build --emptyOutDir",
"lint": "eslint .",
"preview": "vite preview"
},
Expand Down
20 changes: 12 additions & 8 deletions client/src/components/InputsCards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,18 @@ export const InputsCards = ({ inputValues }: { inputValues: InputValues }) => {
<CardTitle>{"Inputs"}</CardTitle>
</CardHeader>
<CardContent className='flex gap-2'>
{Object.entries(inputValues).map(([key, value]) => (
<Card key={key}>
<CardHeader>
<CardTitle>{key}</CardTitle>
</CardHeader>
<CardContent>{`${value}`}</CardContent>
</Card>
))}
{Object.keys(inputValues).length > 0 ? (
Object.entries(inputValues).map(([key, value]) => (
<Card key={key}>
<CardHeader>
<CardTitle>{key}</CardTitle>
</CardHeader>
<CardContent>{`${value}`}</CardContent>
</Card>
))
) : (
<CardContent>{"No inputs"}</CardContent>
)}
</CardContent>
</Card>
);
Expand Down
8 changes: 4 additions & 4 deletions client/src/components/lexical/custom-data-plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
import { TextNode } from "lexical";
import { useCallback, useMemo, useState } from "react";
import { createPortal } from "react-dom";
import { $createCustomNode } from "./custom-node";
import { $createTemplateNode } from "./template-node";
import { TriggerFn } from "@lexical/react/LexicalTypeaheadMenuPlugin";

const PUNCTUATION =
Expand Down Expand Up @@ -144,14 +144,14 @@ export const CustomDataSuggestionPlugin = ({
closeMenu: () => void
) => {
editor.update(() => {
const customNode = $createCustomNode(
const templateNode = $createTemplateNode(
selectedOption.key,
selectedOption.metadata
);
if (nodeToReplace) {
nodeToReplace.replace(customNode);
nodeToReplace.replace(templateNode);
}
customNode.select();
templateNode.select();
closeMenu();
});
},
Expand Down
82 changes: 44 additions & 38 deletions client/src/components/lexical/template-auto-replace-plugin.tsx
Original file line number Diff line number Diff line change
@@ -1,57 +1,63 @@
import { TextNode } from "lexical";
import { $createCustomNode, CustomNode } from "./custom-node";
import { $createTemplateNode, TemplateNode } from "./template-node";
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
import { useEffect, useCallback, useMemo } from "react";
import { useEffect, useCallback } from "react";
import { useLexicalTextEntity } from "@lexical/react/useLexicalTextEntity";

const escapeRegExp = (string: string) => {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
};

export const TemplateAutoReplacePlugin = ({
inputs,
}: {
inputs: string[];
}): JSX.Element | null => {
export const TemplateAutoReplacePlugin = (
{
// inputs,
}: {
// inputs: string[];
}
): JSX.Element | null => {
const [editor] = useLexicalComposerContext();
const inputs: string[] = ["genre"];
useEffect(() => {
if (!editor.hasNodes([CustomNode])) {
if (!editor.hasNodes([TemplateNode])) {
throw new Error(
"TemplateAutoReplacePlugin: CustomNode not registered on editor"
"TemplateAutoReplacePlugin: TemplateNode not registered on editor"
);
}
}, [editor]);

const $createCustomNode_ = useCallback((textNode: TextNode): CustomNode => {
return $createCustomNode(textNode.getTextContent());
const createTemplateNode = useCallback((textNode: TextNode): TemplateNode => {
return $createTemplateNode(textNode.getTextContent());
}, []);

const contentPattern = inputs.map(escapeRegExp).join("|");

const getTemplateMatch = useCallback((text: string) => {
const matchArr = new RegExp(
`(?<!\\{)\\{(${contentPattern})\\}(?!\\})`,
"g"
).exec(text);

if (matchArr === null) {
return null;
}

const matchedContent = matchArr[1];
const startOffset = matchArr.index;
const endOffset = startOffset + matchedContent.length + 2; // +2 for the braces
return {
end: endOffset,
start: startOffset,
};
}, []);

// useLexicalTextEntity<CustomNode>(
// getTemplateMatch,
// CustomNode,
// $createCustomNode_
// );
console.log(inputs);
const getTemplateMatch = useCallback(
(text: string) => {
console.log(inputs);
const contentPattern = inputs.map(escapeRegExp).join("|");
const matchArr = new RegExp(
`(?<!\\{)\\{(${contentPattern})\\}(?!\\})`,
"g"
).exec(text);

if (matchArr === null) {
return null;
}

const matchedContent = matchArr[1];
const startOffset = matchArr.index;
const endOffset = startOffset + matchedContent.length + 2; // +2 for the braces
return {
end: endOffset,
start: startOffset,
};
},
[JSON.stringify(inputs)]
);

useLexicalTextEntity<TemplateNode>(
getTemplateMatch,
TemplateNode,
createTemplateNode
);

return null;
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,23 @@ import {
TextNode,
} from "lexical";

// Define the type for the serialized custom node
export type SerializedCustomNode = Spread<
// Define the type for the serialized template node
export type SerializedTemplateNode = Spread<
{
value: string;
moreDetails?: object;
},
SerializedTextNode
>;

// Function to convert a DOM element to a custom node
function $convertCustomElement(
// Function to convert a DOM element to a template node
function $convertTemplateElement(
domNode: HTMLElement
): DOMConversionOutput | null {
const textContent = domNode.textContent;

if (textContent !== null) {
const node = $createCustomNode(textContent);
const node = $createTemplateNode(textContent);
return {
node,
};
Expand All @@ -36,18 +36,18 @@ function $convertCustomElement(
return null;
}

export class CustomNode extends TextNode {
export class TemplateNode extends TextNode {
__value: string;
__moreDetails?: object;

// Return the type of the node
static getType(): string {
return "custom-node";
return "template-node";
}

// Clone the node
static clone(node: CustomNode): CustomNode {
return new CustomNode(
static clone(node: TemplateNode): TemplateNode {
return new TemplateNode(
node.__value,
node.__moreDetails,
node.__text,
Expand All @@ -56,8 +56,8 @@ export class CustomNode extends TextNode {
}

// Import the serialized node
static importJSON(serializedNode: SerializedCustomNode): CustomNode {
const node = $createCustomNode(
static importJSON(serializedNode: SerializedTemplateNode): TemplateNode {
const node = $createTemplateNode(
serializedNode.value,
serializedNode.moreDetails
);
Expand All @@ -81,12 +81,12 @@ export class CustomNode extends TextNode {
}

// Export the node to a serialized format
exportJSON(): SerializedCustomNode {
exportJSON(): SerializedTemplateNode {
return {
...super.exportJSON(),
value: this.__value,
moreDetails: this.__moreDetails,
type: "custom-node",
type: "template-node",
version: 1,
};
}
Expand All @@ -95,14 +95,15 @@ export class CustomNode extends TextNode {
createDOM(config: EditorConfig): HTMLElement {
const dom = super.createDOM(config);
dom.textContent = this.__value;
dom.setAttribute("data-lexical-template", "true");
dom.className = "text-purple-500 font-normal ";
return dom;
}

// Export the DOM representation of the node
exportDOM(): DOMExportOutput {
const element = document.createElement("span");
element.setAttribute("data-lexical-custom", "true");
element.setAttribute("data-lexical-template", "true");
element.textContent = this.__text;
return { element };
}
Expand All @@ -111,11 +112,11 @@ export class CustomNode extends TextNode {
static importDOM(): DOMConversionMap | null {
return {
span: (domNode: HTMLElement) => {
if (!domNode.hasAttribute("data-lexical-custom")) {
if (!domNode.hasAttribute("data-lexical-template")) {
return null;
}
return {
conversion: $convertCustomElement,
conversion: $convertTemplateElement,
priority: 1,
};
},
Expand All @@ -135,19 +136,19 @@ export class CustomNode extends TextNode {
}
}

// Helper function to create a new custom node
export function $createCustomNode(
// Helper function to create a new template node
export function $createTemplateNode(
value: string,
moreDetails?: object
): CustomNode {
const customNode = new CustomNode(value, moreDetails);
customNode.setMode("segmented").toggleDirectionless();
return $applyNodeReplacement(customNode);
): TemplateNode {
const templateNode = new TemplateNode(value, moreDetails);
templateNode.setMode("segmented").toggleDirectionless();
return $applyNodeReplacement(templateNode);
}

// Helper function to check if a node is a custom node
export function $isCustomNode(
// Helper function to check if a node is a template node
export function $isTemplateNode(
node: LexicalNode | null | undefined
): node is CustomNode {
return node instanceof CustomNode;
): node is TemplateNode {
return node instanceof TemplateNode;
}
67 changes: 67 additions & 0 deletions client/src/components/lexical/template-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
import { TextNode } from "lexical";
import { useEffect } from "react";
import { $createTemplateNode, TemplateNode } from "./template-node";

function $findAndTransformTemplate(
node: TextNode,
inputs: string[]
): null | TextNode {
const text = node.getTextContent();
const regex = /\{(.*?)\}/g;
let match;

while ((match = regex.exec(text)) !== null) {
const matchedContent = match[1]; // Content inside the braces
const startOffset = match.index;
const endOffset = startOffset + match[0].length; // Include braces in length
if (inputs.includes(matchedContent)) {
let targetNode;
if (startOffset === 0) {
[targetNode] = node.splitText(endOffset);
} else {
[, targetNode] = node.splitText(startOffset, endOffset);
}

const templateNode = $createTemplateNode(match[0]);
targetNode.replace(templateNode);
return templateNode;
}
}
return null;
}

function $textNodeTransform(node: TextNode, inputs: string[]): void {
let targetNode: TextNode | null = node;

while (targetNode !== null) {
if (!targetNode.isSimpleText()) {
return;
}
targetNode = $findAndTransformTemplate(targetNode, inputs);
}
}

function useTemplates(editor, inputs: string[]): void {
useEffect(() => {
if (!editor.hasNodes([TemplateNode])) {
throw new Error(
"TemplateAutoReplacePlugin: TemplateNode not registered on editor"
);
}

return editor.registerNodeTransform(TextNode, (node: TextNode) => {
$textNodeTransform(node, inputs);
});
}, [editor, inputs]);
}

export const TemplatePlugin = ({
inputs,
}: {
inputs: string[];
}): JSX.Element | null => {
const [editor] = useLexicalComposerContext();
useTemplates(editor, inputs);
return null;
};
6 changes: 2 additions & 4 deletions client/src/components/ui/combobox.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
"use client";

import * as React from "react";
import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";

import { cn } from "@/lib/utils";
Expand All @@ -18,14 +15,15 @@ import {
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { useState } from "react";

type ComboboxProps = {
items: { value: string; label: string }[];
value: string;
setValue: (value: string) => void;
};
export function Combobox({ items, value, setValue }: ComboboxProps) {
const [open, setOpen] = React.useState(false);
const [open, setOpen] = useState(false);

return (
<Popover open={open} onOpenChange={setOpen}>
Expand Down
Loading

0 comments on commit 9e30d49

Please sign in to comment.