From 2efed7bfb113d5057a0bae44a19d3ad6721f0149 Mon Sep 17 00:00:00 2001
From: Bastien Sun <43788700+bastiensun@users.noreply.github.com>
Date: Wed, 1 Nov 2023 21:30:08 +0100
Subject: [PATCH] feat: add json support
---
index.html | 2 +-
package-lock.json | 254 +++++++++++++++++++++++++++-
package.json | 1 +
src/app.test.tsx | 120 ++++++++++++-
src/app.tsx | 2 +
src/components/formatted-code.tsx | 14 +-
src/components/language-select.tsx | 29 ++++
src/components/title.tsx | 19 ++-
src/components/ui/select.tsx | 130 ++++++++++++++
src/components/unformatted-code.tsx | 11 +-
src/lib/format-java.ts | 11 --
src/lib/format.ts | 37 ++++
src/lib/use-code.ts | 14 +-
src/lib/use-language.ts | 65 +++++++
src/routes.tsx | 8 +-
15 files changed, 684 insertions(+), 33 deletions(-)
create mode 100644 src/components/language-select.tsx
create mode 100644 src/components/ui/select.tsx
delete mode 100644 src/lib/format-java.ts
create mode 100644 src/lib/format.ts
create mode 100644 src/lib/use-language.ts
diff --git a/index.html b/index.html
index b7d06b2..ac5317a 100644
--- a/index.html
+++ b/index.html
@@ -4,7 +4,7 @@
-
Prettier Java Playground
+ Prettier Playground
diff --git a/package-lock.json b/package-lock.json
index 921dbc8..ac78870 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
"version": "0.0.0",
"dependencies": {
"@radix-ui/react-label": "^2.0.2",
+ "@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.0.7",
@@ -2151,6 +2152,14 @@
"url": "https://opencollective.com/unts"
}
},
+ "node_modules/@radix-ui/number": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.1.tgz",
+ "integrity": "sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10"
+ }
+ },
"node_modules/@radix-ui/primitive": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz",
@@ -2242,6 +2251,23 @@
}
}
},
+ "node_modules/@radix-ui/react-direction": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.1.tgz",
+ "integrity": "sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz",
@@ -2269,6 +2295,48 @@
}
}
},
+ "node_modules/@radix-ui/react-focus-guards": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz",
+ "integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-scope": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz",
+ "integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/react-compose-refs": "1.0.1",
+ "@radix-ui/react-primitive": "1.0.3",
+ "@radix-ui/react-use-callback-ref": "1.0.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0",
+ "react-dom": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-id": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz",
@@ -2412,6 +2480,49 @@
}
}
},
+ "node_modules/@radix-ui/react-select": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.0.0.tgz",
+ "integrity": "sha512-RH5b7af4oHtkcHS7pG6Sgv5rk5Wxa7XI8W5gvB1N/yiuDGZxko1ynvOiVhFM7Cis2A8zxF9bTOUVbRDzPepe6w==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/number": "1.0.1",
+ "@radix-ui/primitive": "1.0.1",
+ "@radix-ui/react-collection": "1.0.3",
+ "@radix-ui/react-compose-refs": "1.0.1",
+ "@radix-ui/react-context": "1.0.1",
+ "@radix-ui/react-direction": "1.0.1",
+ "@radix-ui/react-dismissable-layer": "1.0.5",
+ "@radix-ui/react-focus-guards": "1.0.1",
+ "@radix-ui/react-focus-scope": "1.0.4",
+ "@radix-ui/react-id": "1.0.1",
+ "@radix-ui/react-popper": "1.1.3",
+ "@radix-ui/react-portal": "1.0.4",
+ "@radix-ui/react-primitive": "1.0.3",
+ "@radix-ui/react-slot": "1.0.2",
+ "@radix-ui/react-use-callback-ref": "1.0.1",
+ "@radix-ui/react-use-controllable-state": "1.0.1",
+ "@radix-ui/react-use-layout-effect": "1.0.1",
+ "@radix-ui/react-use-previous": "1.0.1",
+ "@radix-ui/react-visually-hidden": "1.0.3",
+ "aria-hidden": "^1.1.1",
+ "react-remove-scroll": "2.5.5"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0",
+ "react-dom": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-slot": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz",
@@ -4099,6 +4210,17 @@
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
},
+ "node_modules/aria-hidden": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.3.tgz",
+ "integrity": "sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/aria-query": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
@@ -5251,6 +5373,11 @@
"node": ">=6"
}
},
+ "node_modules/detect-node-es": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
+ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="
+ },
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@@ -7411,6 +7538,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/get-nonce": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
+ "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/get-port": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/get-port/-/get-port-7.0.0.tgz",
@@ -7951,6 +8086,14 @@
"node": ">= 0.4"
}
},
+ "node_modules/invariant": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
+ "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
+ "dependencies": {
+ "loose-envify": "^1.0.0"
+ }
+ },
"node_modules/is-arguments": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
@@ -10381,6 +10524,51 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-remove-scroll": {
+ "version": "2.5.5",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz",
+ "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.3",
+ "react-style-singleton": "^2.2.1",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.0",
+ "use-sidecar": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-remove-scroll-bar": {
+ "version": "2.3.4",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz",
+ "integrity": "sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==",
+ "dependencies": {
+ "react-style-singleton": "^2.2.1",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-router": {
"version": "6.17.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.17.0.tgz",
@@ -10411,6 +10599,28 @@
"react-dom": ">=16.8"
}
},
+ "node_modules/react-style-singleton": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
+ "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==",
+ "dependencies": {
+ "get-nonce": "^1.0.0",
+ "invariant": "^2.2.4",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -11968,8 +12178,7 @@
"node_modules/tslib": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
- "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
- "dev": true
+ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
},
"node_modules/tsutils": {
"version": "3.21.0",
@@ -12250,6 +12459,47 @@
"integrity": "sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==",
"dev": true
},
+ "node_modules/use-callback-ref": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.0.tgz",
+ "integrity": "sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w==",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sidecar": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz",
+ "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==",
+ "dependencies": {
+ "detect-node-es": "^1.1.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/util": {
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
diff --git a/package.json b/package.json
index 9b3adb7..bbcb97b 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
},
"dependencies": {
"@radix-ui/react-label": "^2.0.2",
+ "@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.0.7",
diff --git a/src/app.test.tsx b/src/app.test.tsx
index e0a3fbb..f8eed5e 100644
--- a/src/app.test.tsx
+++ b/src/app.test.tsx
@@ -2,7 +2,7 @@ import { render, renderWithRouter, screen } from "@/lib/test-utils";
import { routes } from "@/routes";
import { userEvent } from "@testing-library/user-event";
import { createMemoryRouter, RouterProvider } from "react-router-dom";
-import { describe, expect, it, test } from "vitest";
+import { describe, expect, it, test, vi } from "vitest";
test("happy path", async () => {
// Arrange
@@ -208,7 +208,7 @@ describe("data query string", () => {
data: "undefined",
});
const router = createMemoryRouter(routes, {
- initialEntries: [`/?${urlSearchParameters.toString()}`],
+ initialEntries: [`/java?${urlSearchParameters.toString()}`],
});
// Act
@@ -275,3 +275,119 @@ describe("data query string", () => {
);
});
});
+
+test("language pathname", async () => {
+ // Arrange
+ const user = userEvent.setup();
+ const router = createMemoryRouter(routes, { initialEntries: ["/"] });
+
+ // Act
+ render();
+
+ // Assert
+ expect(router.state.location.pathname).toEqual("/java");
+
+ const title = screen.getByRole("heading", {
+ name: /prettier java playground/iu,
+ });
+ expect(title).toHaveTextContent(/prettier java playground/iu);
+
+ const unformattedCodeTextarea = screen.getByRole("textbox");
+ expect(unformattedCodeTextarea).toHaveTextContent(
+ 'class HelloWorld { public static void main( String[ ] args ) { System.out.println( "Hello, World!" ) ; } }',
+ );
+
+ // Arrange
+ window.HTMLElement.prototype.hasPointerCapture = vi.fn();
+ window.HTMLElement.prototype.scrollIntoView = vi.fn();
+
+ // Act
+ const languageSelect = screen.getByRole("combobox");
+ await user.click(languageSelect);
+
+ // Assert
+ expect(
+ screen.getByRole("option", { name: /java/iu, selected: true }),
+ ).toBeVisible();
+
+ // Act
+ await user.click(
+ screen.getByRole("option", { name: /json/iu, selected: false }),
+ );
+ await user.click(languageSelect);
+
+ // Assert
+ expect(
+ screen.getByRole("option", { name: /json/iu, selected: true }),
+ ).toBeVisible();
+ expect(router.state.location.pathname).toEqual("/json");
+ expect(title).toHaveTextContent(/prettier json playground/iu);
+ expect(unformattedCodeTextarea).toHaveTextContent('{ "hello" : "world"}');
+
+ // Act
+ await user.click(
+ screen.getByRole("option", { name: /java/iu, selected: false }),
+ );
+
+ // Assert
+ expect(unformattedCodeTextarea).toHaveTextContent(
+ 'class HelloWorld { public static void main( String[ ] args ) { System.out.println( "Hello, World!" ) ; } }',
+ );
+});
+
+test.each(["/invalid-language", "/invalid-language/other-pathname"])(
+ "invalid pathname ('%s')",
+ async (invalidPathname) => {
+ // Arrange
+ const user = userEvent.setup();
+ const router = createMemoryRouter(routes, {
+ initialEntries: [invalidPathname],
+ });
+
+ // Act
+ render();
+
+ // Assert
+ expect(router.state.location.pathname).toEqual("/java");
+
+ // Arrange
+ window.HTMLElement.prototype.hasPointerCapture = vi.fn();
+ window.HTMLElement.prototype.scrollIntoView = vi.fn();
+
+ // Act
+ await user.click(screen.getByRole("combobox"));
+
+ // Assert
+ expect(
+ screen.getByRole("option", { name: /java/iu, selected: true }),
+ ).toBeVisible();
+ },
+);
+
+test("json", async () => {
+ // Arrange
+ const user = userEvent.setup();
+ const router = createMemoryRouter(routes, {
+ initialEntries: ["/json"],
+ });
+
+ window.HTMLElement.prototype.hasPointerCapture = vi.fn();
+ window.HTMLElement.prototype.scrollIntoView = vi.fn();
+
+ // Act
+ render();
+
+ const unformattedCodeTextarea = screen.getByRole("textbox");
+ await user.clear(unformattedCodeTextarea);
+ await user.type(
+ unformattedCodeTextarea,
+ '{\\[}"Immortality","Heat Immunity","Inferno","Teleportation","Interdimensional travel"{\\]}',
+ );
+
+ // Assert
+ expect(
+ screen.getByRole("button", {
+ name: '[ "Immortality" , "Heat Immunity" , "Inferno" , "Teleportation" , "Interdimensional travel" ]',
+ }),
+ ).toBeVisible();
+});
diff --git a/src/app.tsx b/src/app.tsx
index 6def541..ab238e0 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -1,4 +1,5 @@
import { CodeDisplay } from "@/components/code-display";
+import { LanguageSelect } from "@/components/language-select";
import { Title } from "@/components/title";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
@@ -14,6 +15,7 @@ export const App = (): JSX.Element => {
+
{
+ switch (language) {
+ case "java":
+ return "kotlin";
+ case "json":
+ return "javascript";
+ }
+};
+
type FormattedCodeProps = {
readonly formattedCode: string;
readonly setTooltipToCopied: () => void;
@@ -17,6 +28,7 @@ export const FormattedCode = ({
setTooltipToCopied,
tooltipMessage,
}: FormattedCodeProps): JSX.Element => {
+ const [language] = useLanguage();
const [isTooltipOpen, setIsTooltipOpen] = useState(false);
const handleClick = async (): Promise => {
@@ -40,7 +52,7 @@ export const FormattedCode = ({
>
{({ getLineProps, getTokenProps, style, tokens }): JSX.Element => (
diff --git a/src/components/language-select.tsx b/src/components/language-select.tsx
new file mode 100644
index 0000000..4c6f572
--- /dev/null
+++ b/src/components/language-select.tsx
@@ -0,0 +1,29 @@
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { SUPPORTED_LANGUAGES, useLanguage } from "@/lib/use-language";
+import { type JSX } from "react";
+
+export const LanguageSelect = (): JSX.Element => {
+ const [language, setLanguage] = useLanguage();
+
+ return (
+
+ );
+};
diff --git a/src/components/title.tsx b/src/components/title.tsx
index 4bb0508..178ae41 100644
--- a/src/components/title.tsx
+++ b/src/components/title.tsx
@@ -1,8 +1,15 @@
+import { SUPPORTED_LANGUAGES, useLanguage } from "@/lib/use-language";
import { type JSX } from "react";
-export const Title = (): JSX.Element => (
-
- ☕ Prettier Java{" "}
- Playground
-
-);
+export const Title = (): JSX.Element => {
+ const [language] = useLanguage();
+
+ return (
+
+ ☕ Prettier {SUPPORTED_LANGUAGES[language]}{" "}
+
+ Playground
+
+
+ );
+};
diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx
new file mode 100644
index 0000000..b08e713
--- /dev/null
+++ b/src/components/ui/select.tsx
@@ -0,0 +1,130 @@
+import { cn } from "@/lib/utils";
+import * as SelectPrimitive from "@radix-ui/react-select";
+import { Check, ChevronDown } from "lucide-react";
+import {
+ type ComponentPropsWithoutRef,
+ type ElementRef,
+ forwardRef,
+} from "react";
+
+const Select = SelectPrimitive.Root;
+
+const SelectGroup = SelectPrimitive.Group;
+
+const SelectValue = SelectPrimitive.Value;
+
+const SelectTrigger = forwardRef<
+ ElementRef,
+ ComponentPropsWithoutRef
+>(({ children, className, ...props }, ref) => (
+
+ {children}
+
+ {/* eslint-disable-next-line react/forbid-component-props */}
+
+
+
+));
+SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
+
+const SelectContent = forwardRef<
+ ElementRef,
+ ComponentPropsWithoutRef
+>(({ children, className, position = "popper", ...props }, ref) => (
+
+
+
+ {children}
+
+
+
+));
+SelectContent.displayName = SelectPrimitive.Content.displayName;
+
+const SelectLabel = forwardRef<
+ ElementRef,
+ ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+SelectLabel.displayName = SelectPrimitive.Label.displayName;
+
+const SelectItem = forwardRef<
+ ElementRef,
+ ComponentPropsWithoutRef
+>(({ children, className, ...props }, ref) => (
+
+
+
+ {/* eslint-disable-next-line react/forbid-component-props */}
+
+
+
+
+ {children}
+
+));
+SelectItem.displayName = SelectPrimitive.Item.displayName;
+
+const SelectSeparator = forwardRef<
+ ElementRef,
+ ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
+
+export {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectLabel,
+ SelectSeparator,
+ SelectTrigger,
+ SelectValue,
+};
diff --git a/src/components/unformatted-code.tsx b/src/components/unformatted-code.tsx
index fa415a9..a7fb888 100644
--- a/src/components/unformatted-code.tsx
+++ b/src/components/unformatted-code.tsx
@@ -1,6 +1,7 @@
import { Textarea } from "@/components/ui/textarea";
import { useToast } from "@/components/ui/use-toast";
-import { formatJava } from "@/lib/format-java";
+import { format } from "@/lib/format";
+import { useLanguage } from "@/lib/use-language";
import { type ChangeEvent, type ClipboardEvent, type JSX } from "react";
type UnformattedCodeProps = {
@@ -18,6 +19,7 @@ export const UnformattedCode = ({
setUnformattedCode,
unformattedCode,
}: UnformattedCodeProps): JSX.Element => {
+ const [language] = useLanguage();
const { toast } = useToast();
const handleChange = async (
@@ -32,9 +34,10 @@ export const UnformattedCode = ({
): Promise => {
let formattedCode: string;
try {
- formattedCode = await formatJava(
- event.clipboardData.getData("text/plain"),
- );
+ formattedCode = await format({
+ code: event.clipboardData.getData("text/plain"),
+ language,
+ });
} catch {
return;
}
diff --git a/src/lib/format-java.ts b/src/lib/format-java.ts
deleted file mode 100644
index 9715631..0000000
--- a/src/lib/format-java.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { format } from "prettier";
-// @ts-expect-error Could not find a declaration file for module `prettier-plugin-java`.
-import parserJava from "prettier-plugin-java";
-
-export const formatJava = (code: string): Promise =>
- format(code, {
- parser: "java",
- plugins: [parserJava],
- printWidth: 120,
- tabWidth: 4,
- });
diff --git a/src/lib/format.ts b/src/lib/format.ts
new file mode 100644
index 0000000..a36faf5
--- /dev/null
+++ b/src/lib/format.ts
@@ -0,0 +1,37 @@
+import { type Language } from "@/lib/use-language";
+import { format as prettierFormat } from "prettier";
+import * as prettierPluginBabel from "prettier/plugins/babel";
+import * as prettierPluginEstree from "prettier/plugins/estree";
+// @ts-expect-error Could not find a declaration file for module `prettier-plugin-java`.
+import parserJava from "prettier-plugin-java";
+
+const formatJava = (code: string): Promise =>
+ prettierFormat(code, {
+ parser: "java",
+ plugins: [parserJava],
+ printWidth: 120,
+ tabWidth: 4,
+ });
+
+const formatJson = (code: string): Promise =>
+ prettierFormat(code, {
+ parser: "json",
+ plugins: [prettierPluginBabel, prettierPluginEstree],
+ });
+
+type FormatParameters = {
+ code: string;
+ language: Language;
+};
+
+export const format = ({
+ code,
+ language, // eslint-disable-next-line consistent-return
+}: FormatParameters): Promise => {
+ switch (language) {
+ case "java":
+ return formatJava(code);
+ case "json":
+ return formatJson(code);
+ }
+};
diff --git a/src/lib/use-code.ts b/src/lib/use-code.ts
index 0ba9a6c..a1e9432 100644
--- a/src/lib/use-code.ts
+++ b/src/lib/use-code.ts
@@ -1,4 +1,5 @@
-import { formatJava } from "@/lib/format-java";
+import { format } from "@/lib/format";
+import { useLanguage } from "@/lib/use-language";
import { useEffect, useState } from "react";
import { useSearchParams } from "react-router-dom";
@@ -27,8 +28,7 @@ const useUnformattedCode = (
}
setUnformattedCode(decodedData);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
+ }, [encodedData]);
const synchronizeUnformattedCodeAndDataQueryString = (
updatedUnformattedCode: string,
@@ -69,6 +69,7 @@ export const useCode = (
setUnformattedCode: (code: string) => void;
unformattedCode: string;
} => {
+ const [language] = useLanguage();
const [unformattedCode, setUnformattedCode] =
useUnformattedCode(initialState);
const [formattedCode, setFormattedCode] = useState("");
@@ -77,7 +78,10 @@ export const useCode = (
const runEffect = async (): Promise => {
let content;
try {
- content = await formatJava(unformattedCode);
+ content = await format({
+ code: unformattedCode,
+ language,
+ });
} catch (error) {
if (error instanceof Error) {
setFormattedCode(error.message);
@@ -90,7 +94,7 @@ export const useCode = (
};
runEffect();
- }, [unformattedCode]);
+ }, [language, unformattedCode]);
return {
formattedCode,
diff --git a/src/lib/use-language.ts b/src/lib/use-language.ts
new file mode 100644
index 0000000..5ca5128
--- /dev/null
+++ b/src/lib/use-language.ts
@@ -0,0 +1,65 @@
+import { useEffect } from "react";
+import { useNavigate, useParams } from "react-router-dom";
+
+export const SUPPORTED_LANGUAGES = { java: "Java", json: "JSON" };
+export type Language = keyof typeof SUPPORTED_LANGUAGES;
+
+export const DEFAULT_LANGUAGE = "java";
+
+const isLanguage = (language: string): language is Language =>
+ language in SUPPORTED_LANGUAGES;
+
+// eslint-disable-next-line consistent-return
+const getExampleFormattedCode = (language: Language): string => {
+ switch (language) {
+ case "java":
+ return `class HelloWorld
+{
+ public static void main( String[ ] args )
+ {
+ System.out.println( "Hello, World!" ) ;
+ }
+}`;
+ case "json":
+ return ` {
+ "hello" : "world"}`;
+ }
+};
+
+export const useLanguage = (): [Language, (language: Language) => void] => {
+ const navigate = useNavigate();
+ const { language } = useParams();
+
+ useEffect(() => {
+ if (!language || !isLanguage(language)) {
+ navigate(
+ {
+ pathname: `/${DEFAULT_LANGUAGE}`,
+ search: `?${new URLSearchParams({
+ data: btoa(getExampleFormattedCode(DEFAULT_LANGUAGE)),
+ })}`,
+ },
+ { replace: true },
+ );
+ }
+ }, [language, navigate]);
+
+ if (!language) {
+ throw new Error("`language` is always defined (cf. `routes`)");
+ }
+
+ const setLanguage = (selectedLanguage: Language): void => {
+ navigate({
+ pathname: `/${selectedLanguage}`,
+ search: `?${new URLSearchParams({
+ data: btoa(getExampleFormattedCode(selectedLanguage)),
+ })}`,
+ });
+ };
+
+ if (!isLanguage(language)) {
+ return [DEFAULT_LANGUAGE, setLanguage];
+ }
+
+ return [language, setLanguage];
+};
diff --git a/src/routes.tsx b/src/routes.tsx
index 53b35ce..a418f48 100644
--- a/src/routes.tsx
+++ b/src/routes.tsx
@@ -1,8 +1,14 @@
import { App } from "@/app";
+import { DEFAULT_LANGUAGE } from "@/lib/use-language";
+import { Navigate } from "react-router-dom";
export const routes = [
{
element: ,
- path: "/",
+ path: "/:language",
+ },
+ {
+ element: ,
+ path: "*",
},
];