From 23588fa519430cada5c749798dbc941bda78103f Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Wed, 10 Sep 2025 09:22:30 +0200 Subject: [PATCH] feat: idea for separate schema --- packages/playground/top-down.tsx | 521 +++++++++++++++++++++++++++++++ 1 file changed, 521 insertions(+) create mode 100644 packages/playground/top-down.tsx diff --git a/packages/playground/top-down.tsx b/packages/playground/top-down.tsx new file mode 100644 index 0000000000..0827d02cd4 --- /dev/null +++ b/packages/playground/top-down.tsx @@ -0,0 +1,521 @@ +import React from "react"; +import * as z from "zod"; +/** + * Should an ext be able to register another ext? + * + */ +type Extension = { + // constructor: (editor: BlockEditor[]>) => any; + // addMethods: (editor: BlockEditor[]>) => any; + /** + * TODO in some cases you want your init methods to be called in a specific order, there are two solutions to this: + * 1. A priority property + * 2. A dependsOn tree + */ + init?: () => void; + type: T; + destroy?: () => void; + // What other things should be top-level here? + // Does an ext need to add things to the schema? Or does a block add an ext? What is a bundle? Do we need it? +}; + +// function hasExtension[]>( +// editor: BlockEditor, +// type: string, +// ): editor is BlockEditor & { +// extensions: { [K in E[number]["type"]]: Extract> }; +// } { +// return type in editor.extensions; +// } + +type BlockConfig = { + type: T; + content: "inline" | "none"; + propSchema: P; + contentType: "block"; +}; + +type ConfigToProps< + T extends + | BlockConfig + | InlineConfig + | StyleConfig, +> = T["propSchema"] extends z.ZodObject ? z.infer : never; + +type InlineConfig = { + type: T; + content: "styled" | "none"; + propSchema: P; + contentType: "inline"; +}; + +type StyleConfig = { + type: T; + propSchema: P; + contentType: "style"; +}; + +type Schema = Record< + string, + | BlockConfig + | InlineConfig + | StyleConfig +>; + +type BlockEditor< + S extends Schema = Schema, + E extends Array<(editor: BlockEditor) => Extension> = [], +> = { + exampleBuiltInMethod: () => any; + + extensions: { + [K in ReturnType["type"]]: Extract< + ReturnType, + Extension + >; + }; + + registerRenderer: ( + type: T, + renderer: (component: { content: ConfigToProps }) => React.ReactNode, + ) => void; + + registerSerializer: ( + type: T, + serializer: (content: S[T]) => string, + ) => void; + + addExtension: (extension: any) => void; // need to create a derived extension with a different type + + // addExtension(name: string, extension: E); + + // addExtension(extension: E, name?: string); +}; + +type EditorWithinExtension< + S extends Schema = Schema, + E extends Array<(editor: BlockEditor) => Extension> = [], +> = Omit, "extensions"> & { + extensions: { + [K in ReturnType["type"]]?: Extract< + ReturnType, + Extension + >; + }; +}; + +function createExtension< + S extends Schema, + Ext extends Array<(editor: BlockEditor) => Extension> = [], + T extends ReturnType["type"] = string, + O extends Record = Record, +>( + otherStuff: (editor: EditorWithinExtension) => O, +): (editor: BlockEditor) => Extension & O { + return (editor) => + ({ + ...otherStuff(editor), + }) as any; +} + +function createBlockEditor< + E extends Array<(editor: BlockEditor) => Extension>, + S extends Schema, +>(extensions: E): BlockEditor { + const editor = {} as BlockEditor; + const extensionsMap = {} as Record>; + + extensions.forEach((ext) => { + const extension = ext(editor); + extensionsMap[extension.type] = extension; + }); + + // separately init after all extensions are added + Object.values(extensionsMap).forEach((extension) => { + if (extension.init) { + extension.init(); + } + }); + + return { + exampleBuiltInMethod: () => { + return "exampleBuiltInMethod"; + }, + extensions: extensionsMap as any, + addExtension: (extension: E) => { + // extensions[extension.type] = extension; + }, + } as const; +} + +const SlashMenuExtension = createExtension("slashMenu", () => ({ + registerItem: (item: { name: string; icon: string }) => { + return 3; + }, +})); +const XyzExtension = createExtension("xyz", () => ({})); + +// const editor = createBlockEditor([SlashMenuExtension, XyzExtension]); + +// editor.extensions.slashMenu.registerItem({ +// name: "xyz", +// icon: "xyz", +// }); + +const YoutubeExtension = createExtension( + // TODO just return the type in the second param?? + "youtube", + (editor: EditorWithinExtension) => { + // TODO do we like this onInit? + // onInit(() => { + // if (editor.extensions.slashMenu) { + // editor.extensions.slashMenu.registerItem({ + // name: "youtube", + // icon: "youtube", + // }); + // } + // }); + return { + init() { + if (editor.extensions.slashMenu) { + editor.extensions.slashMenu.registerItem({ + name: "youtube", + icon: "youtube", + }); + } + }, + insertYoutubeBlock: () => { + // do something + }, + }; + }, +); + +function createBlock< + T extends () => Omit, "contentType">, +>( + config: T, +): () => BlockConfig["type"], ReturnType["propSchema"]> { + return () => + ({ + ...config, + contentType: "block", + }) as any; +} + +// First principles: +// A schema is not dependent on anything else. + +const youtubeBlock = createBlock( + () => + ({ + type: "youtube", + propSchema: z.object({ + url: z.string(), + }), + content: "none", + }) as const, +); + +type YoutubeBlockSchema = ReturnType; + +const youtubeBlock2 = { + contentType: "block", + type: "youtube", + propSchema: z.object({ + url: z.string(), + }), + content: "none", +} as const; + +const youtubeBlock3 = { + contentType: "block", + type: "youtube", + propSchema: + 1 < 2 + ? z.object({ + url: z.string(), + }) + : z.object({ + xl: z.string(), + }), + content: "none", +} as const; + +function YoutubeBlock({ + content, + // mode, +}: { + content: { url?: string; xl?: string }; + // if you care, render the html mode + // mode: "html" | "view"; +}) { + return