Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions packages/cheatsheet-local/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { Config } from "jest";
import { preactModuleNameMapper } from "@cursorless/common";

const config: Config = {
preset: "ts-jest",
testEnvironment: "jsdom",
moduleNameMapper: {
...preactModuleNameMapper,
"^@cursorless/cheatsheet$": "<rootDir>/../cheatsheet/src/index.ts",
"\\.(css|scss)$": "<rootDir>/src/test/styleMock.ts",
},
};

export default config;
10 changes: 3 additions & 7 deletions packages/cheatsheet-local/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
}
},
"scripts": {
"test": "jest",
Comment thread
AndreasArvidsson marked this conversation as resolved.
"compile": "tsc --build",
"watch": "tsc --build --watch",
"build": "pnpm build:prod",
Expand All @@ -29,19 +30,14 @@
"dependencies": {
"@cursorless/cheatsheet": "workspace:*",
"@cursorless/common": "workspace:*",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"preact": "^10.29.0",
"tslib": "^2.8.1"
},
"devDependencies": {
"@preact/preset-vite": "^2.10.3",
"@tailwindcss/postcss": "^4.2.1",
"@testing-library/dom": "^10.4.1",
"@testing-library/react": "^16.3.2",
"@types/jest": "^30.0.0",
"@types/node": "^24.12.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"jest": "^30.3.0",
"postcss": "^8.5.8",
"tailwindcss": "^4.2.1",
Expand Down
34 changes: 27 additions & 7 deletions packages/cheatsheet-local/src/app/app.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,36 @@
import { render } from "@testing-library/react";
import { fakeCheatsheetInfo } from "@cursorless/cheatsheet";
import { render } from "preact";
import { act } from "preact/test-utils";
import { App } from "./app";

describe("App", () => {
it("should render successfully", () => {
const { baseElement } = render(<App />);
beforeEach(() => {
document.cheatsheetInfo = fakeCheatsheetInfo;
});

afterEach(() => {
document.body.innerHTML = "";
});

expect(baseElement).toBeTruthy();
it("should render successfully", async () => {
const container = document.createElement("div");
document.body.append(container);

await act(() => {
render(<App />, container);
});

expect(container).toBeTruthy();
});

it("should have a greeting as the title", () => {
const { getByText } = render(<App />);
it("should have a greeting as the title", async () => {
const container = document.createElement("div");
document.body.append(container);

await act(() => {
render(<App />, container);
});

expect(getByText(/Welcome cheatsheet-local/gi)).toBeTruthy();
expect(container.textContent).toMatch(/Cursorless Cheatsheet/gi);
});
});
18 changes: 12 additions & 6 deletions packages/cheatsheet-local/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { StrictMode } from "react";
import * as ReactDOM from "react-dom/client";
import { render } from "preact";
import { StrictMode } from "preact/compat";
import { App } from "./app/app";

const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement,
);
root.render(
render(
<StrictMode>
<App />
</StrictMode>,
getRoot(),
);

function getRoot() {
const root = document.getElementById("root");
if (root == null) {
throw new Error("Missing root container");
}
return root;
}
1 change: 1 addition & 0 deletions packages/cheatsheet-local/src/test/styleMock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default {};
1 change: 1 addition & 0 deletions packages/cheatsheet-local/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"target": "es2022",
"lib": ["dom", "es2022"],
"jsx": "react-jsx",
"jsxImportSource": "preact",
"allowSyntheticDefaultImports": true,
"skipLibCheck": true,
"esModuleInterop": true,
Expand Down
4 changes: 2 additions & 2 deletions packages/cheatsheet-local/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { fakeCheatsheetInfo } from "@cursorless/cheatsheet";
import { viteHtmlParams } from "@cursorless/common";
import react from "@vitejs/plugin-react";
import preact from "@preact/preset-vite";
import { defineConfig } from "vite";
import { viteSingleFile } from "vite-plugin-singlefile";

Expand All @@ -11,7 +11,7 @@ export default defineConfig(() => {
},

plugins: [
react(),
preact(),
viteSingleFile(),
viteHtmlParams({
FAKE_CHEATSHEET_INFO: JSON.stringify(fakeCheatsheetInfo),
Expand Down
2 changes: 2 additions & 0 deletions packages/cheatsheet/jest.config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type { Config } from "jest";
import { preactModuleNameMapper } from "@cursorless/common";

const config: Config = {
preset: "ts-jest",
testEnvironment: "jsdom",
moduleNameMapper: preactModuleNameMapper,
};

export default config;
12 changes: 3 additions & 9 deletions packages/cheatsheet/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@cursorless/cheatsheet",
"version": "0.1.0",
"description": "Core cheatsheet react component",
"description": "Core cheatsheet Preact component",
"license": "MIT",
"type": "module",
"main": "./out/index.js",
Expand All @@ -26,21 +26,15 @@
"watch": "pnpm run --filter @cursorless/cheatsheet --parallel '/^watch:.*/'"
},
"dependencies": {
"@cursorless/common": "workspace:*",
"@fortawesome/fontawesome-svg-core": "^7.2.0",
"@fortawesome/free-solid-svg-icons": "^7.2.0",
"@fortawesome/react-fontawesome": "^3.2.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-string-replace": "^2.0.1",
"preact": "^10.29.0",
"react-use": "^17.6.0"
},
"devDependencies": {
"@testing-library/dom": "^10.4.1",
"@testing-library/react": "^16.3.2",
"@types/jest": "^30.0.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@types/react-helmet": "^6.1.11",
"jest": "^30.3.0",
"jest-environment-jsdom": "^30.3.0",
"ts-jest": "^29.4.6",
Expand Down
4 changes: 2 additions & 2 deletions packages/cheatsheet/src/lib/CheatsheetPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as React from "react";
import type { ComponentChildren } from "preact";
import CheatsheetListComponent from "./components/CheatsheetListComponent";
import CheatsheetLegendComponent from "./components/CheatsheetLegendComponent";
import cheatsheetLegend from "./cheatsheetLegend";
Expand Down Expand Up @@ -56,7 +56,7 @@ function Cheatsheet({ cheatsheetInfo }: Props) {
}

type CheatsheetSectionProps = {
children?: React.ReactNode;
children?: ComponentChildren;
};

function CheatsheetSection({ children }: CheatsheetSectionProps) {
Expand Down
21 changes: 15 additions & 6 deletions packages/cheatsheet/src/lib/cheatsheet.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import { render } from "@testing-library/react";
import { render } from "preact";
import { act } from "preact/test-utils";
import { CheatsheetPage } from "./CheatsheetPage";
import { fakeCheatsheetInfo } from "./fakeCheatsheetInfo";

describe("Cheatsheet", () => {
it("should render successfully", () => {
const { baseElement } = render(
<CheatsheetPage cheatsheetInfo={fakeCheatsheetInfo} />,
);
expect(baseElement).toBeTruthy();
afterEach(() => {
document.body.innerHTML = "";
});

it("should render successfully", async () => {
const container = document.createElement("div");
document.body.append(container);

await act(() => {
render(<CheatsheetPage cheatsheetInfo={fakeCheatsheetInfo} />, container);
});

expect(container).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as React from "react";
import type { JSX } from "preact";
import type { CheatsheetLegend } from "../cheatsheetLegend";
import useIsHighlighted from "../hooks/useIsHighlighted";
import { formatCaptures } from "./formatCaptures";
Expand All @@ -10,7 +10,7 @@ type Props = {

export default function CheatsheetLegendComponent({
data,
}: Props): React.JSX.Element {
}: Props): JSX.Element {
const isHighlighted = useIsHighlighted("legend");

const borderClassName = isHighlighted
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { JSX } from "react";
import type { JSX } from "preact";
import type { CheatsheetSection, Variation } from "../CheatsheetInfo";
import useIsHighlighted from "../hooks/useIsHighlighted";
import { formatCaptures } from "./formatCaptures";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from "react";
import type { JSX } from "preact";
import SmartLink from "./SmartLink";

export default function CheatsheetNotesComponent(): React.JSX.Element {
export default function CheatsheetNotesComponent(): JSX.Element {
return (
<div
id="notes"
Expand Down
12 changes: 5 additions & 7 deletions packages/cheatsheet/src/lib/components/SmartLink.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import * as React from "react";
import type { ComponentChildren } from "preact";

type SmartLinkProps = {
/**
* The target of the link
*/
to: string;

children?: React.ReactNode;
children?: ComponentChildren;
noFormatting?: boolean;
};

Expand All @@ -15,11 +15,11 @@ type SmartLinkProps = {
* internal
* @returns SmartLink component
*/
const SmartLink: React.FC<SmartLinkProps> = ({
export default function SmartLink({
to,
children,
noFormatting = false,
}) => {
}: SmartLinkProps) {
const className = noFormatting
? ""
: "text-blue-500 hover:text-violet-700 dark:text-cyan-400 dark:hover:text-violet-200";
Expand All @@ -35,6 +35,4 @@ const SmartLink: React.FC<SmartLinkProps> = ({
)}
</span>
);
};

export default SmartLink;
}
34 changes: 34 additions & 0 deletions packages/cheatsheet/src/lib/components/formatCaptures.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { render } from "preact";
import { act } from "preact/test-utils";
import { formatCaptures } from "./formatCaptures";

describe("formatCaptures", () => {
afterEach(() => {
document.body.innerHTML = "";
});

it("formats capture placeholders", async () => {
const container = document.createElement("div");
document.body.append(container);

await act(() => {
render(<div>{formatCaptures("hello <target> world")}</div>, container);
});

expect(container.textContent).toBe("hello [target] world");
expect(container.querySelector('a[href="#legend"]')).not.toBeNull();
});

it("leaves malformed captures as plain text", async () => {
const container = document.createElement("div");
document.body.append(container);
const input = "<<=<=<=";

await act(() => {
render(<div>{formatCaptures(input)}</div>, container);
});

expect(container.textContent).toBe(input);
expect(container.querySelector('a[href="#legend"]')).toBeNull();
});
});
35 changes: 27 additions & 8 deletions packages/cheatsheet/src/lib/components/formatCaptures.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,48 @@
import reactStringReplace from "react-string-replace";
import type { ComponentChildren } from "preact";
import SmartLink from "./SmartLink";

export function formatCaptures(input: string) {
return reactStringReplace(input, captureRegex, (match, i) => {
const parts: ComponentChildren[] = [];
let lastIndex = 0;

for (const match of input.matchAll(captureRegex)) {
Comment thread Dismissed
const [fullMatch, capture] = match;
const index = match.index ?? 0;

if (index > lastIndex) {
parts.push(input.slice(lastIndex, index));
}

const innerElement =
match === "ordinal" ? (
capture === "ordinal" ? (
<span>
n<sup>th</sup>
</span>
) : (
match
capture
);

return (
parts.push(
<span
key={i}
key={index}
className="inline-block rounded-md bg-[#8686864C] p-[2px] text-[#000000E3] dark:bg-[#FFFFFF33] dark:text-[#FFFFFFE3]"
>
<SmartLink to="#legend" noFormatting={true}>
{"["}
{innerElement}
{"]"}
</SmartLink>
</span>
</span>,
);
});

lastIndex = index + fullMatch.length;
}

if (lastIndex < input.length) {
parts.push(input.slice(lastIndex));
}

return parts;
}

const captureRegex = /<([^>]+)>/g;
Loading
Loading