From 1383899e5f11023ba78bf7527563e6f514e17b01 Mon Sep 17 00:00:00 2001 From: patrickhertling Date: Sat, 1 Feb 2025 01:24:57 +0200 Subject: [PATCH] json formatting in code-block plugin --- .../default/plate-ui/code-block-element.tsx | 4 +- .../plate-ui/code-block-format-button.tsx | 31 ++++++++++++++ .../code-block/src/lib/formatter/formatter.ts | 40 ++++++++++++++++++ .../code-block/src/lib/formatter/index.ts | 6 +++ .../src/lib/formatter/jsonFormatter.spec.tsx | 42 +++++++++++++++++++ .../src/lib/formatter/jsonFormatter.ts | 21 ++++++++++ packages/code-block/src/lib/index.ts | 1 + packages/code-block/src/react/hooks/index.ts | 1 + .../src/react/hooks/useCodeBlockFormat.ts | 32 ++++++++++++++ 9 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 apps/www/src/registry/default/plate-ui/code-block-format-button.tsx create mode 100644 packages/code-block/src/lib/formatter/formatter.ts create mode 100644 packages/code-block/src/lib/formatter/index.ts create mode 100644 packages/code-block/src/lib/formatter/jsonFormatter.spec.tsx create mode 100644 packages/code-block/src/lib/formatter/jsonFormatter.ts create mode 100644 packages/code-block/src/react/hooks/useCodeBlockFormat.ts diff --git a/apps/www/src/registry/default/plate-ui/code-block-element.tsx b/apps/www/src/registry/default/plate-ui/code-block-element.tsx index 5dee8c46b1..6b8a189c43 100644 --- a/apps/www/src/registry/default/plate-ui/code-block-element.tsx +++ b/apps/www/src/registry/default/plate-ui/code-block-element.tsx @@ -6,6 +6,7 @@ import { cn, withRef } from '@udecode/cn'; import { useCodeBlockElementState } from '@udecode/plate-code-block/react'; import { CodeBlockCombobox } from './code-block-combobox'; +import { CodeBlockFormatButton } from './code-block-format-button'; import { PlateElement } from './plate-element'; import './code-block-element.css'; @@ -28,9 +29,10 @@ export const CodeBlockElement = withRef( {state.syntax && (
+
)} diff --git a/apps/www/src/registry/default/plate-ui/code-block-format-button.tsx b/apps/www/src/registry/default/plate-ui/code-block-format-button.tsx new file mode 100644 index 0000000000..e147a5bbd1 --- /dev/null +++ b/apps/www/src/registry/default/plate-ui/code-block-format-button.tsx @@ -0,0 +1,31 @@ +'use client'; + +import type { TElement } from '@udecode/plate'; + +import { useCodeBlockFormat } from '@udecode/plate-code-block/react'; +import { BracesIcon } from 'lucide-react'; + +import { Button } from './button'; + +export function CodeBlockFormatButton({ element }: { element: TElement }) { + const { format, isSupported, validSyntax } = useCodeBlockFormat({ + element, + }); + + if (!isSupported) { + return null; + } + + return ( + + ); +} diff --git a/packages/code-block/src/lib/formatter/formatter.ts b/packages/code-block/src/lib/formatter/formatter.ts new file mode 100644 index 0000000000..98c632a254 --- /dev/null +++ b/packages/code-block/src/lib/formatter/formatter.ts @@ -0,0 +1,40 @@ +import { JsonFormatter } from './jsonFormatter'; + +export interface IFormatter { + format: (code: string) => string; + validSyntax: (code: string) => boolean; +} + +const supportedLanguages = new Set(['json']); + +export class Formatter { + format(code: string, lang?: string) { + if (!lang || !supportedLanguages.has(lang)) { + return ''; + } + + switch (lang) { + case 'json': { + return new JsonFormatter().format(code); + } + } + + return code; + } + + isLangSupported(lang?: string) { + return lang && supportedLanguages.has(lang); + } + + validSyntax(code: string, lang?: string) { + if (!lang || !supportedLanguages.has(lang)) { + return false; + } + + switch (lang) { + case 'json': { + return new JsonFormatter().validSyntax(code); + } + } + } +} diff --git a/packages/code-block/src/lib/formatter/index.ts b/packages/code-block/src/lib/formatter/index.ts new file mode 100644 index 0000000000..70d3362467 --- /dev/null +++ b/packages/code-block/src/lib/formatter/index.ts @@ -0,0 +1,6 @@ +/** + * @file Automatically generated by barrelsby. + */ + +export * from './formatter'; +export * from './jsonFormatter'; diff --git a/packages/code-block/src/lib/formatter/jsonFormatter.spec.tsx b/packages/code-block/src/lib/formatter/jsonFormatter.spec.tsx new file mode 100644 index 0000000000..7fbeb3d6cd --- /dev/null +++ b/packages/code-block/src/lib/formatter/jsonFormatter.spec.tsx @@ -0,0 +1,42 @@ +import { createEditor } from '@udecode/plate'; +import { createPlateEditor } from '@udecode/plate/react'; + +import { JsonFormatter } from './jsonFormatter'; + +describe('JsonFormatter', () => { + const formatter = new JsonFormatter(); + + it('should detect valid JSON', () => { + const json = '{ "name": "ChatGPT", "type": "AI" }'; + const isValid = formatter.validSyntax(json); + expect(isValid).toBe(true); + }); + + it('should detect invalid JSON', () => { + const editor = createEditor(); + const plateEditor = createPlateEditor({ editor }); + const json = '{ name: "ChatGPT", type: AI }'; + const isValid = formatter.validSyntax(json); + expect(isValid).toBe(false); + }); + + it('should format JSON', () => { + const editor = createEditor(); + const plateEditor = createPlateEditor({ editor }); + const json = '{"name":"ChatGPT","type":"AI"}'; + const formattedJson = formatter.format(json); + const expected = `{ + "name": "ChatGPT", + "type": "AI" +}`; + expect(formattedJson).toBe(expected); + }); + + it('should not format invalid JSON', () => { + const editor = createEditor(); + const plateEditor = createPlateEditor({ editor }); + const json = '{ name: "ChatGPT", type: AI }'; + const formattedJson = formatter.format(json); + expect(formattedJson).toBe(json); + }); +}); diff --git a/packages/code-block/src/lib/formatter/jsonFormatter.ts b/packages/code-block/src/lib/formatter/jsonFormatter.ts new file mode 100644 index 0000000000..bb5030f921 --- /dev/null +++ b/packages/code-block/src/lib/formatter/jsonFormatter.ts @@ -0,0 +1,21 @@ +import type { IFormatter } from './formatter'; + +export class JsonFormatter implements IFormatter { + format(code: string) { + try { + return JSON.stringify(JSON.parse(code), null, 2); + } catch (error) { + return code; + } + } + + validSyntax(code: string) { + try { + JSON.parse(code); + + return true; + } catch (error) { + return false; + } + } +} diff --git a/packages/code-block/src/lib/index.ts b/packages/code-block/src/lib/index.ts index ca856337a4..2c9aa755ee 100644 --- a/packages/code-block/src/lib/index.ts +++ b/packages/code-block/src/lib/index.ts @@ -10,5 +10,6 @@ export * from './withInsertDataCodeBlock'; export * from './withInsertFragmentCodeBlock'; export * from './withNormalizeCodeBlock'; export * from './deserializer/index'; +export * from './formatter/index'; export * from './queries/index'; export * from './transforms/index'; diff --git a/packages/code-block/src/react/hooks/index.ts b/packages/code-block/src/react/hooks/index.ts index c0ca34cad1..79f136e998 100644 --- a/packages/code-block/src/react/hooks/index.ts +++ b/packages/code-block/src/react/hooks/index.ts @@ -4,5 +4,6 @@ export * from './useCodeBlockCombobox'; export * from './useCodeBlockElement'; +export * from './useCodeBlockFormat'; export * from './useCodeSyntaxLeaf'; export * from './useToggleCodeBlockButton'; diff --git a/packages/code-block/src/react/hooks/useCodeBlockFormat.ts b/packages/code-block/src/react/hooks/useCodeBlockFormat.ts new file mode 100644 index 0000000000..aaa50e436a --- /dev/null +++ b/packages/code-block/src/react/hooks/useCodeBlockFormat.ts @@ -0,0 +1,32 @@ +import { useEditorRef } from '@udecode/plate/react'; + +import type { TCodeBlockElement } from '../../lib'; + +import { Formatter } from '../../lib/formatter/formatter'; + +export const useCodeBlockFormat = ({ + element, +}: { + element: TCodeBlockElement; +}) => { + const editor = useEditorRef(); + + const { lang: language } = element; + const code = editor.api.string(element); + + const formatter = new Formatter(); + const isSupported = formatter.isLangSupported(language); + + const format = () => { + const validSyntax = formatter.validSyntax(code, language); + + if (validSyntax) { + const formattedCode = formatter.format(code, language); + editor.tf.insertText(formattedCode, { at: element }); + } + }; + + const validSyntax = formatter.validSyntax(code, language); + + return { format, isSupported, validSyntax }; +};