From 76b1ce9486c76fe596e4cc7a098c0f342c3c1aaf Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sat, 27 Dec 2025 17:59:35 +0000 Subject: [PATCH] refactor: modularize lua block metadata --- frontends/nextjs/package-lock.json | 10 - .../components/editors/lua/blocks/basics.ts | 49 +++ .../src/components/editors/lua/blocks/data.ts | 36 +++ .../editors/lua/blocks/functions.ts | 26 ++ .../components/editors/lua/blocks/index.ts | 33 ++ .../components/editors/lua/blocks/logic.ts | 37 +++ .../components/editors/lua/blocks/loops.ts | 27 ++ .../lua/hooks/luaBlockSerialization.ts | 105 +++++++ .../lua/hooks/useBlockDefinitions.test.ts | 66 ++++ .../editors/lua/hooks/useBlockDefinitions.ts | 292 +----------------- 10 files changed, 392 insertions(+), 289 deletions(-) create mode 100644 frontends/nextjs/src/components/editors/lua/blocks/basics.ts create mode 100644 frontends/nextjs/src/components/editors/lua/blocks/data.ts create mode 100644 frontends/nextjs/src/components/editors/lua/blocks/functions.ts create mode 100644 frontends/nextjs/src/components/editors/lua/blocks/index.ts create mode 100644 frontends/nextjs/src/components/editors/lua/blocks/logic.ts create mode 100644 frontends/nextjs/src/components/editors/lua/blocks/loops.ts create mode 100644 frontends/nextjs/src/components/editors/lua/hooks/luaBlockSerialization.ts create mode 100644 frontends/nextjs/src/components/editors/lua/hooks/useBlockDefinitions.test.ts diff --git a/frontends/nextjs/package-lock.json b/frontends/nextjs/package-lock.json index 53b0ce34b..1fb379c9e 100644 --- a/frontends/nextjs/package-lock.json +++ b/frontends/nextjs/package-lock.json @@ -5743,16 +5743,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/jszip": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@types/jszip/-/jszip-3.4.1.tgz", - "integrity": "sha512-TezXjmf3lj+zQ651r6hPqvSScqBLvyPI9FxdXBqpEwBijNGQ2NXpaFW/7joGzveYkKQUil7iiDHLo6LV71Pc0A==", - "deprecated": "This is a stub types definition. jszip provides its own type definitions, so you do not need this installed.", - "license": "MIT", - "dependencies": { - "jszip": "*" - } - }, "node_modules/@types/node": { "version": "25.0.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", diff --git a/frontends/nextjs/src/components/editors/lua/blocks/basics.ts b/frontends/nextjs/src/components/editors/lua/blocks/basics.ts new file mode 100644 index 000000000..477af142d --- /dev/null +++ b/frontends/nextjs/src/components/editors/lua/blocks/basics.ts @@ -0,0 +1,49 @@ +import type { BlockDefinition } from '../types' + +export const basicBlocks: BlockDefinition[] = [ + { + type: 'log', + label: 'Log message', + description: 'Send a message to the Lua console', + category: 'Basics', + fields: [ + { + name: 'message', + label: 'Message', + placeholder: '"Hello from Lua"', + type: 'text', + defaultValue: '"Hello from Lua"', + }, + ], + }, + { + type: 'return', + label: 'Return', + description: 'Return a value from the script', + category: 'Basics', + fields: [ + { + name: 'value', + label: 'Value', + placeholder: 'true', + type: 'text', + defaultValue: 'true', + }, + ], + }, + { + type: 'comment', + label: 'Comment', + description: 'Add a comment to explain a step', + category: 'Basics', + fields: [ + { + name: 'text', + label: 'Comment', + placeholder: 'Explain what happens here', + type: 'text', + defaultValue: 'Explain what happens here', + }, + ], + }, +] diff --git a/frontends/nextjs/src/components/editors/lua/blocks/data.ts b/frontends/nextjs/src/components/editors/lua/blocks/data.ts new file mode 100644 index 000000000..7b18dcd67 --- /dev/null +++ b/frontends/nextjs/src/components/editors/lua/blocks/data.ts @@ -0,0 +1,36 @@ +import type { BlockDefinition } from '../types' + +export const dataBlocks: BlockDefinition[] = [ + { + type: 'set_variable', + label: 'Set variable', + description: 'Create or update a variable', + category: 'Data', + fields: [ + { + name: 'scope', + label: 'Scope', + type: 'select', + defaultValue: 'local', + options: [ + { label: 'local', value: 'local' }, + { label: 'global', value: 'global' }, + ], + }, + { + name: 'name', + label: 'Variable name', + placeholder: 'count', + type: 'text', + defaultValue: 'count', + }, + { + name: 'value', + label: 'Value', + placeholder: '0', + type: 'text', + defaultValue: '0', + }, + ], + }, +] diff --git a/frontends/nextjs/src/components/editors/lua/blocks/functions.ts b/frontends/nextjs/src/components/editors/lua/blocks/functions.ts new file mode 100644 index 000000000..2aaf364dd --- /dev/null +++ b/frontends/nextjs/src/components/editors/lua/blocks/functions.ts @@ -0,0 +1,26 @@ +import type { BlockDefinition } from '../types' + +export const functionBlocks: BlockDefinition[] = [ + { + type: 'call', + label: 'Call function', + description: 'Invoke a Lua function', + category: 'Functions', + fields: [ + { + name: 'function', + label: 'Function name', + placeholder: 'my_function', + type: 'text', + defaultValue: 'my_function', + }, + { + name: 'args', + label: 'Arguments', + placeholder: 'context.data', + type: 'text', + defaultValue: 'context.data', + }, + ], + }, +] diff --git a/frontends/nextjs/src/components/editors/lua/blocks/index.ts b/frontends/nextjs/src/components/editors/lua/blocks/index.ts new file mode 100644 index 000000000..33cf9167d --- /dev/null +++ b/frontends/nextjs/src/components/editors/lua/blocks/index.ts @@ -0,0 +1,33 @@ +import type { BlockCategory, BlockDefinition } from '../types' +import { basicBlocks } from './basics' +import { dataBlocks } from './data' +import { functionBlocks } from './functions' +import { logicBlocks } from './logic' +import { loopBlocks } from './loops' + +export const BLOCK_DEFINITIONS: BlockDefinition[] = [ + ...basicBlocks, + ...logicBlocks, + ...loopBlocks, + ...dataBlocks, + ...functionBlocks, +] + +const createCategoryIndex = (): Record => ({ + Basics: [], + Logic: [], + Loops: [], + Data: [], + Functions: [], +}) + +export const groupBlockDefinitionsByCategory = (definitions: BlockDefinition[]) => { + const categories = createCategoryIndex() + definitions.forEach((definition) => { + categories[definition.category].push(definition) + }) + return categories +} + +export const buildBlockDefinitionMap = (definitions: BlockDefinition[]) => + new Map(definitions.map((definition) => [definition.type, definition])) diff --git a/frontends/nextjs/src/components/editors/lua/blocks/logic.ts b/frontends/nextjs/src/components/editors/lua/blocks/logic.ts new file mode 100644 index 000000000..872b9249b --- /dev/null +++ b/frontends/nextjs/src/components/editors/lua/blocks/logic.ts @@ -0,0 +1,37 @@ +import type { BlockDefinition } from '../types' + +export const logicBlocks: BlockDefinition[] = [ + { + type: 'if', + label: 'If', + description: 'Run blocks when a condition is true', + category: 'Logic', + fields: [ + { + name: 'condition', + label: 'Condition', + placeholder: 'context.data.isActive', + type: 'text', + defaultValue: 'context.data.isActive', + }, + ], + hasChildren: true, + }, + { + type: 'if_else', + label: 'If / Else', + description: 'Branch execution with else fallback', + category: 'Logic', + fields: [ + { + name: 'condition', + label: 'Condition', + placeholder: 'context.data.count > 5', + type: 'text', + defaultValue: 'context.data.count > 5', + }, + ], + hasChildren: true, + hasElseChildren: true, + }, +] diff --git a/frontends/nextjs/src/components/editors/lua/blocks/loops.ts b/frontends/nextjs/src/components/editors/lua/blocks/loops.ts new file mode 100644 index 000000000..157c25f6b --- /dev/null +++ b/frontends/nextjs/src/components/editors/lua/blocks/loops.ts @@ -0,0 +1,27 @@ +import type { BlockDefinition } from '../types' + +export const loopBlocks: BlockDefinition[] = [ + { + type: 'repeat', + label: 'Repeat loop', + description: 'Run nested blocks multiple times', + category: 'Loops', + fields: [ + { + name: 'iterator', + label: 'Iterator', + placeholder: 'i', + type: 'text', + defaultValue: 'i', + }, + { + name: 'count', + label: 'Times', + placeholder: '3', + type: 'number', + defaultValue: '3', + }, + ], + hasChildren: true, + }, +] diff --git a/frontends/nextjs/src/components/editors/lua/hooks/luaBlockSerialization.ts b/frontends/nextjs/src/components/editors/lua/hooks/luaBlockSerialization.ts new file mode 100644 index 000000000..cb4aeff4a --- /dev/null +++ b/frontends/nextjs/src/components/editors/lua/hooks/luaBlockSerialization.ts @@ -0,0 +1,105 @@ +import type { LuaBlock } from '../types' + +export const BLOCKS_METADATA_PREFIX = '--@blocks ' + +const indent = (depth: number) => ' '.repeat(depth) + +const getFieldValue = (block: LuaBlock, fieldName: string, fallback: string) => { + const value = block.fields[fieldName] + if (value === undefined || value === null) return fallback + const normalized = String(value).trim() + return normalized.length > 0 ? normalized : fallback +} + +const renderBlocks = (blocks: LuaBlock[], depth: number, renderBlock: (block: LuaBlock, depth: number) => string) => + blocks + .map((block) => renderBlock(block, depth)) + .filter(Boolean) + .join('\n') + +const renderChildBlocks = ( + blocks: LuaBlock[] | undefined, + depth: number, + renderBlock: (block: LuaBlock, depth: number) => string +) => { + if (!blocks || blocks.length === 0) { + return `${indent(depth)}-- add blocks here` + } + return renderBlocks(blocks, depth, renderBlock) +} + +export const buildLuaFromBlocks = (blocks: LuaBlock[]) => { + const renderBlock = (block: LuaBlock, depth: number): string => { + switch (block.type) { + case 'log': { + const message = getFieldValue(block, 'message', '""') + return `${indent(depth)}log(${message})` + } + case 'set_variable': { + const scope = getFieldValue(block, 'scope', 'local') + const name = getFieldValue(block, 'name', 'value') + const value = getFieldValue(block, 'value', 'nil') + const keyword = scope === 'local' ? 'local ' : '' + return `${indent(depth)}${keyword}${name} = ${value}` + } + case 'if': { + const condition = getFieldValue(block, 'condition', 'true') + const body = renderChildBlocks(block.children, depth + 1, renderBlock) + return `${indent(depth)}if ${condition} then\n${body}\n${indent(depth)}end` + } + case 'if_else': { + const condition = getFieldValue(block, 'condition', 'true') + const thenBody = renderChildBlocks(block.children, depth + 1, renderBlock) + const elseBody = renderChildBlocks(block.elseChildren, depth + 1, renderBlock) + return `${indent(depth)}if ${condition} then\n${thenBody}\n${indent(depth)}else\n${elseBody}\n${indent(depth)}end` + } + case 'repeat': { + const iterator = getFieldValue(block, 'iterator', 'i') + const count = getFieldValue(block, 'count', '1') + const body = renderChildBlocks(block.children, depth + 1, renderBlock) + return `${indent(depth)}for ${iterator} = 1, ${count} do\n${body}\n${indent(depth)}end` + } + case 'return': { + const value = getFieldValue(block, 'value', 'nil') + return `${indent(depth)}return ${value}` + } + case 'call': { + const functionName = getFieldValue(block, 'function', 'my_function') + const args = getFieldValue(block, 'args', '') + const argsSection = args ? args : '' + return `${indent(depth)}${functionName}(${argsSection})` + } + case 'comment': { + const text = getFieldValue(block, 'text', '') + return `${indent(depth)}-- ${text}` + } + default: + return '' + } + } + + const metadata = `${BLOCKS_METADATA_PREFIX}${JSON.stringify({ version: 1, blocks })}` + const body = renderBlocks(blocks, 0, renderBlock) + if (!body.trim()) { + return `${metadata}\n-- empty block workspace\n` + } + return `${metadata}\n${body}\n` +} + +export const decodeBlocksMetadata = (code: string): LuaBlock[] | null => { + const metadataLine = code + .split('\n') + .map((line) => line.trim()) + .find((line) => line.startsWith(BLOCKS_METADATA_PREFIX)) + + if (!metadataLine) return null + + const json = metadataLine.slice(BLOCKS_METADATA_PREFIX.length) + try { + const parsed = JSON.parse(json) + if (!parsed || !Array.isArray(parsed.blocks)) return null + return parsed.blocks as LuaBlock[] + } catch { + return null + } +} diff --git a/frontends/nextjs/src/components/editors/lua/hooks/useBlockDefinitions.test.ts b/frontends/nextjs/src/components/editors/lua/hooks/useBlockDefinitions.test.ts new file mode 100644 index 000000000..1db4308c0 --- /dev/null +++ b/frontends/nextjs/src/components/editors/lua/hooks/useBlockDefinitions.test.ts @@ -0,0 +1,66 @@ +import { renderHook } from '@testing-library/react' +import { describe, expect, it } from 'vitest' +import { useBlockDefinitions } from './useBlockDefinitions' +import { BLOCKS_METADATA_PREFIX, buildLuaFromBlocks, decodeBlocksMetadata } from './luaBlockSerialization' +import type { LuaBlock } from '../types' + +describe('useBlockDefinitions', () => { + it('aggregates block metadata by category', () => { + const { result } = renderHook(() => useBlockDefinitions()) + + expect(result.current.blockDefinitions).toHaveLength(8) + expect(result.current.blocksByCategory.Basics.map((block) => block.type)).toEqual( + expect.arrayContaining(['log', 'return', 'comment']) + ) + expect(result.current.blocksByCategory.Data.map((block) => block.type)).toEqual(['set_variable']) + expect(result.current.blocksByCategory.Logic.map((block) => block.type)).toEqual( + expect.arrayContaining(['if', 'if_else']) + ) + expect(result.current.blocksByCategory.Loops.map((block) => block.type)).toEqual(['repeat']) + expect(result.current.blocksByCategory.Functions.map((block) => block.type)).toEqual(['call']) + }) +}) + +describe('lua block serialization', () => { + const sampleBlocks: LuaBlock[] = [ + { + id: 'if-block', + type: 'if_else', + fields: { condition: 'context.data.count > 5' }, + children: [ + { + id: 'log-then', + type: 'log', + fields: { message: '"High count"' }, + }, + ], + elseChildren: [ + { + id: 'reset-count', + type: 'set_variable', + fields: { scope: 'local', name: 'count', value: '0' }, + }, + ], + }, + ] + + it('serializes Lua with metadata header', () => { + const lua = buildLuaFromBlocks(sampleBlocks) + + expect(lua.startsWith(BLOCKS_METADATA_PREFIX)).toBe(true) + expect(lua).toContain('if context.data.count > 5 then') + expect(lua).toContain('log("High count")') + expect(lua).toContain('local count = 0') + }) + + it('round-trips block metadata through serialization', () => { + const lua = buildLuaFromBlocks(sampleBlocks) + const parsed = decodeBlocksMetadata(lua) + + expect(parsed).toEqual(sampleBlocks) + }) + + it('returns null when metadata is missing', () => { + expect(decodeBlocksMetadata('-- some lua code without metadata')).toBeNull() + }) +}) diff --git a/frontends/nextjs/src/components/editors/lua/hooks/useBlockDefinitions.ts b/frontends/nextjs/src/components/editors/lua/hooks/useBlockDefinitions.ts index e67ebe916..c729bc936 100644 --- a/frontends/nextjs/src/components/editors/lua/hooks/useBlockDefinitions.ts +++ b/frontends/nextjs/src/components/editors/lua/hooks/useBlockDefinitions.ts @@ -1,196 +1,22 @@ import { useCallback, useMemo } from 'react' +import { BLOCK_DEFINITIONS, buildBlockDefinitionMap, groupBlockDefinitionsByCategory } from '../blocks' import type { BlockCategory, BlockDefinition, LuaBlock, LuaBlockType } from '../types' - -const BLOCKS_METADATA_PREFIX = '--@blocks ' - -const BLOCK_DEFINITIONS: BlockDefinition[] = [ - { - type: 'log', - label: 'Log message', - description: 'Send a message to the Lua console', - category: 'Basics', - fields: [ - { - name: 'message', - label: 'Message', - placeholder: '"Hello from Lua"', - type: 'text', - defaultValue: '"Hello from Lua"', - }, - ], - }, - { - type: 'set_variable', - label: 'Set variable', - description: 'Create or update a variable', - category: 'Data', - fields: [ - { - name: 'scope', - label: 'Scope', - type: 'select', - defaultValue: 'local', - options: [ - { label: 'local', value: 'local' }, - { label: 'global', value: 'global' }, - ], - }, - { - name: 'name', - label: 'Variable name', - placeholder: 'count', - type: 'text', - defaultValue: 'count', - }, - { - name: 'value', - label: 'Value', - placeholder: '0', - type: 'text', - defaultValue: '0', - }, - ], - }, - { - type: 'if', - label: 'If', - description: 'Run blocks when a condition is true', - category: 'Logic', - fields: [ - { - name: 'condition', - label: 'Condition', - placeholder: 'context.data.isActive', - type: 'text', - defaultValue: 'context.data.isActive', - }, - ], - hasChildren: true, - }, - { - type: 'if_else', - label: 'If / Else', - description: 'Branch execution with else fallback', - category: 'Logic', - fields: [ - { - name: 'condition', - label: 'Condition', - placeholder: 'context.data.count > 5', - type: 'text', - defaultValue: 'context.data.count > 5', - }, - ], - hasChildren: true, - hasElseChildren: true, - }, - { - type: 'repeat', - label: 'Repeat loop', - description: 'Run nested blocks multiple times', - category: 'Loops', - fields: [ - { - name: 'iterator', - label: 'Iterator', - placeholder: 'i', - type: 'text', - defaultValue: 'i', - }, - { - name: 'count', - label: 'Times', - placeholder: '3', - type: 'number', - defaultValue: '3', - }, - ], - hasChildren: true, - }, - { - type: 'call', - label: 'Call function', - description: 'Invoke a Lua function', - category: 'Functions', - fields: [ - { - name: 'function', - label: 'Function name', - placeholder: 'my_function', - type: 'text', - defaultValue: 'my_function', - }, - { - name: 'args', - label: 'Arguments', - placeholder: 'context.data', - type: 'text', - defaultValue: 'context.data', - }, - ], - }, - { - type: 'return', - label: 'Return', - description: 'Return a value from the script', - category: 'Basics', - fields: [ - { - name: 'value', - label: 'Value', - placeholder: 'true', - type: 'text', - defaultValue: 'true', - }, - ], - }, - { - type: 'comment', - label: 'Comment', - description: 'Add a comment to explain a step', - category: 'Basics', - fields: [ - { - name: 'text', - label: 'Comment', - placeholder: 'Explain what happens here', - type: 'text', - defaultValue: 'Explain what happens here', - }, - ], - }, -] +import { buildLuaFromBlocks as serializeBlocks, decodeBlocksMetadata as parseBlocksMetadata } from './luaBlockSerialization' const createBlockId = () => `block_${Date.now()}_${Math.random().toString(16).slice(2)}` -const indent = (depth: number) => ' '.repeat(depth) - -const renderBlocks = (blocks: LuaBlock[], depth: number, renderBlock: (block: LuaBlock, depth: number) => string) => - blocks - .map((block) => renderBlock(block, depth)) - .filter(Boolean) - .join('\n') - export function useBlockDefinitions() { + const blockDefinitions = useMemo(() => BLOCK_DEFINITIONS, []) + const blockDefinitionMap = useMemo( - () => new Map(BLOCK_DEFINITIONS.map((definition) => [definition.type, definition])), - [] + () => buildBlockDefinitionMap(blockDefinitions), + [blockDefinitions] ) - const blocksByCategory = useMemo>(() => { - const initial: Record = { - Basics: [], - Logic: [], - Loops: [], - Data: [], - Functions: [], - } - - return BLOCK_DEFINITIONS.reduce((acc, definition) => { - acc[definition.category] = [...(acc[definition.category] || []), definition] - return acc - }, initial) - }, []) + const blocksByCategory = useMemo>( + () => groupBlockDefinitionsByCategory(blockDefinitions), + [blockDefinitions] + ) const createBlock = useCallback( (type: LuaBlockType): LuaBlock => { @@ -226,104 +52,12 @@ export function useBlockDefinitions() { [] ) - const getFieldValue = useCallback((block: LuaBlock, fieldName: string, fallback: string) => { - const value = block.fields[fieldName] - if (value === undefined || value === null) return fallback - const normalized = String(value).trim() - return normalized.length > 0 ? normalized : fallback - }, []) + const buildLuaFromBlocks = useCallback((blocks: LuaBlock[]) => serializeBlocks(blocks), []) - const renderChildBlocks = useCallback( - (blocks: LuaBlock[] | undefined, depth: number, renderBlock: (block: LuaBlock, depth: number) => string) => { - if (!blocks || blocks.length === 0) { - return `${indent(depth)}-- add blocks here` - } - return renderBlocks(blocks, depth, renderBlock) - }, - [] - ) - - const buildLuaFromBlocks = useCallback( - (blocks: LuaBlock[]) => { - const renderBlock = (block: LuaBlock, depth: number): string => { - switch (block.type) { - case 'log': { - const message = getFieldValue(block, 'message', '""') - return `${indent(depth)}log(${message})` - } - case 'set_variable': { - const scope = getFieldValue(block, 'scope', 'local') - const name = getFieldValue(block, 'name', 'value') - const value = getFieldValue(block, 'value', 'nil') - const keyword = scope === 'local' ? 'local ' : '' - return `${indent(depth)}${keyword}${name} = ${value}` - } - case 'if': { - const condition = getFieldValue(block, 'condition', 'true') - const body = renderChildBlocks(block.children, depth + 1, renderBlock) - return `${indent(depth)}if ${condition} then\n${body}\n${indent(depth)}end` - } - case 'if_else': { - const condition = getFieldValue(block, 'condition', 'true') - const thenBody = renderChildBlocks(block.children, depth + 1, renderBlock) - const elseBody = renderChildBlocks(block.elseChildren, depth + 1, renderBlock) - return `${indent(depth)}if ${condition} then\n${thenBody}\n${indent(depth)}else\n${elseBody}\n${indent(depth)}end` - } - case 'repeat': { - const iterator = getFieldValue(block, 'iterator', 'i') - const count = getFieldValue(block, 'count', '1') - const body = renderChildBlocks(block.children, depth + 1, renderBlock) - return `${indent(depth)}for ${iterator} = 1, ${count} do\n${body}\n${indent(depth)}end` - } - case 'return': { - const value = getFieldValue(block, 'value', 'nil') - return `${indent(depth)}return ${value}` - } - case 'call': { - const functionName = getFieldValue(block, 'function', 'my_function') - const args = getFieldValue(block, 'args', '') - const argsSection = args ? args : '' - return `${indent(depth)}${functionName}(${argsSection})` - } - case 'comment': { - const text = getFieldValue(block, 'text', '') - return `${indent(depth)}-- ${text}` - } - default: - return '' - } - } - - const metadata = `${BLOCKS_METADATA_PREFIX}${JSON.stringify({ version: 1, blocks })}` - const body = renderBlocks(blocks, 0, renderBlock) - if (!body.trim()) { - return `${metadata}\n-- empty block workspace\n` - } - return `${metadata}\n${body}\n` - }, - [getFieldValue, renderChildBlocks] - ) - - const decodeBlocksMetadata = useCallback((code: string): LuaBlock[] | null => { - const metadataLine = code - .split('\n') - .map((line) => line.trim()) - .find((line) => line.startsWith(BLOCKS_METADATA_PREFIX)) - - if (!metadataLine) return null - - const json = metadataLine.slice(BLOCKS_METADATA_PREFIX.length) - try { - const parsed = JSON.parse(json) - if (!parsed || !Array.isArray(parsed.blocks)) return null - return parsed.blocks as LuaBlock[] - } catch { - return null - } - }, []) + const decodeBlocksMetadata = useCallback((code: string) => parseBlocksMetadata(code), []) return { - blockDefinitions: BLOCK_DEFINITIONS, + blockDefinitions, blockDefinitionMap, blocksByCategory, createBlock,