import defu from "defu" import { row } from "./builtin/row" import { readModifier, type CordoModifier } from "./modifier" import type { AllowedComponents as ContainerAllowedComponents } from "./builtin/container" import type { AllowedComponents as ModalAllowedComponents } from "./builtin/modal" const CordoComponentSymbol = Symbol.for('CordoComponent') export const ComponentType = { ActionRow: 1, Button: 2, StringSelect: 3, TextInput: 4, UserSelect: 5, RoleSelect: 6, MentionableSelect: 7, ChannelSelect: 8, Section: 9, TextDisplay: 10, Thumbnail: 11, MediaGallery: 12, File: 13, Seperator: 14, Container: 17, Label: 18, FileUpload: 19, RadioGroup: 21, CheckboxGroup: 22, Checkbox: 23, Modal: -1, // This is a special type that is not used in the API, but for Cordo's internal use } as const export type StringComponentType = keyof typeof ComponentType export type ComponentIdFromName = typeof ComponentType[Name] export type CordoComponent = { [CordoComponentSymbol]: { nativeName: Type nativeType: typeof ComponentType[Type] visible: boolean attributes: Record render: (meta: { hirarchy: Array attributes: Record }) => Record | null } visible: (value: boolean) => CordoComponent attributes: (attrs: Record) => CordoComponent } & ( Type extends 'Container' ? { [Symbol.iterator]: () => Iterator } : Type extends 'Modal' ? { [Symbol.iterator]: () => Iterator } : { [Symbol.iterator]: never } ) export type CordoComponentPayload = CordoComponent[typeof CordoComponentSymbol] export function createComponent( type: Type, render: CordoComponentPayload['render'] ): CordoComponent { const comp = { nativeName: type, nativeType: ComponentType[type], visible: true, attributes: {}, render, } const out = { [CordoComponentSymbol]: comp, visible(value: boolean) { comp.visible = value return this }, attributes(attrs: Record) { comp.attributes = defu(attrs, comp.attributes) return this } } return out as any as CordoComponent } export function readComponent>(comp: T): T[typeof CordoComponentSymbol] { return comp[CordoComponentSymbol]! } export function isComponent(t: Record): t is CordoComponent { return !!t && CordoComponentSymbol in t } export function renderComponent( c: CordoComponent | CordoComponentPayload, parent: StringComponentType | null, hirarchy: Array = [], inheritAttributes: Record = {} ) { const extracted = CordoComponentSymbol in c ? readComponent(c) : c if (!extracted.visible) return null return extracted.render({ hirarchy: parent ? [ parent, ...hirarchy ] : hirarchy, attributes: defu(extracted.attributes, inheritAttributes) }) } export function renderComponentList( list: Array | CordoModifier>, parent: StringComponentType | null, hirarchy: Array = [], inheritAttributes: Record = {}, transform?: (component: CordoComponent, parsed: CordoComponentPayload) => CordoComponent | null ) { let pipeline: Array> = [] /* when we have buttons without a host row we use this to collect up to 5 and then create the row for it */ const rowBuilder: Array> = [] const modifiers: Array> = [] for (let item of list) { if (!item) continue if (!isComponent(item)) { const mod = readModifier(item) if (mod && !modifiers.some(m => m.name === mod.name)) modifiers.push(mod) continue } let parsed = readComponent(item) for (const mod of modifiers) { if (mod.hooks?.onRender) parsed = mod.hooks.onRender(parsed) } if (transform) { const transformed = transform(item, parsed) if (!transformed) continue item = transformed parsed = readComponent(transformed) } if (parent !== 'ActionRow') { if (parsed.nativeName === 'Button') { rowBuilder.push(item) if (rowBuilder.length === 5) { pipeline.push(readComponent(row(...rowBuilder as any))) rowBuilder.splice(0) } continue } if (parsed.nativeName === 'StringSelect' || parsed.nativeName === 'TextInput') { if (rowBuilder.length > 0) { pipeline.push(readComponent(row(...rowBuilder as any))) rowBuilder.splice(0) } pipeline.push(readComponent(row(item as any))) continue } } if (rowBuilder.length > 0) { pipeline.push(readComponent(row(...rowBuilder as any))) rowBuilder.splice(0) } pipeline.push(parsed) } if (rowBuilder.length > 0) { pipeline.push(readComponent(row(...rowBuilder as any))) rowBuilder.splice(0) } for (const mod of modifiers) { if (mod.hooks?.beforeRender) pipeline = mod.hooks.beforeRender(pipeline) } let output = pipeline .map(c => renderComponent(c, parent, hirarchy, inheritAttributes)) .filter(Boolean) as Record[] // join adjacent text components let previousTextComponent: Record | null = null output = output.filter((item) => { if (item.type === ComponentType.TextDisplay) { if (previousTextComponent) { (previousTextComponent as any).content += '\n' + item.content return false } else { previousTextComponent = item } } else { previousTextComponent = null } return true }) for (const mod of modifiers) { if (mod.hooks?.afterRender) output = mod.hooks.afterRender(output) } return output }