diff --git a/packages/heml-utils/src/createElement.js b/packages/heml-utils/src/createElement.js index e39b887..d53c8b5 100644 --- a/packages/heml-utils/src/createElement.js +++ b/packages/heml-utils/src/createElement.js @@ -1,33 +1,146 @@ -import { defaults, isFunction } from 'lodash' +// types.ts +export interface ElementConfig { + tagName: string + attrs: string[] + children: boolean | string[] + defaultAttrs: Record + rules?: Record + containsText?: boolean + preRender?: (attrs: any, children: any) => void + render: (attrs: any, children: any) => string | false + postRender?: (output: string) => string +} -const textRegex = /^(text(-([^-\s]+))?(-([^-\s]+))?|word-(break|spacing|wrap)|line-break|hanging-punctuation|hyphens|letter-spacing|overflow-wrap|tab-size|white-space|font-family|font-weight|font-style|font-variant|color)$/i +export type ElementInput = Partial | ((attrs: any, children: any) => string) -export default function (name, element) { - if (!name || name.trim().length === 0) { - throw new Error(`When creating an element, you must set the name. ${name.trim().length === 0 ? 'An empty string' : `"${name}"`} was given.`) - } +// constants.ts +export const TEXT_STYLE_RULES = { + header: [ + /^(text(-([^-\s]+))?(-([^-\s]+))?|word-(break|spacing|wrap)|line-break|hanging-punctuation|hyphens|letter-spacing|overflow-wrap|tab-size|white-space|font-family|font-weight|font-style|font-variant|color)$/i + ], + text: [ + /^(text(-([^-\s]+))?(-([^-\s]+))?|word-(break|spacing|wrap)|line-break|hanging-punctuation|hyphens|letter-spacing|overflow-wrap|tab-size|white-space|font-family|font-weight|font-style|font-variant|color)$/i, + 'font-size', + 'line-height' + ] +} as const - if (isFunction(element)) { - element = { render: element } +// validators.ts +export class ValidationError extends Error { + constructor(message: string) { + super(message) + this.name = 'ValidationError' } +} - if (element.containsText) { - element.rules = element.rules || {} - element.rules['.header'] = [ textRegex ] - element.rules['.text'] = [ textRegex, 'font-size', 'line-height' ] +export function validateName(name: string): void { + if (!name?.trim()) { + throw new ValidationError( + `Element name is required. Received: "${name}"` + ) + } + + if (!/^[a-zA-Z][a-zA-Z0-9-]*$/.test(name)) { + throw new ValidationError( + `Element name must start with a letter and contain only alphanumeric characters and hyphens. Received: "${name}"` + ) } +} + +// element-factory.ts +import { defaults, isFunction } from 'lodash' +import { ElementConfig, ElementInput } from './types' +import { TEXT_STYLE_RULES } from './constants' +import { validateName, ValidationError } from './validators' - element = defaults({}, element || {}, { - tagName: name.trim().toLowerCase(), +export class ElementFactory { + private static readonly DEFAULT_CONFIG: Omit = { attrs: [], children: true, - defaultAttrs: {}, - preRender () {}, - render () { return false }, - postRender () {} - }) + defaultAttrs: { class: '' }, + preRender: () => {}, + postRender: (output: string) => output, + rules: {} + } - element.defaultAttrs.class = element.defaultAttrs.class || '' + /** + * Creates a new element with validation and smart defaults + */ + static create(name: string, elementInput: ElementInput): ElementConfig { + try { + validateName(name) + + const element = this.normalizeInput(elementInput) + const config = this.applyDefaults(name, element) + + return this.enhanceWithTextRules(config) + } catch (error) { + if (error instanceof ValidationError) { + throw error + } + throw new Error(`Failed to create element "${name}": ${error.message}`) + } + } - return element + /** + * Creates multiple elements at once + */ + static createMultiple( + definitions: Record + ): Record { + const elements: Record = {} + + for (const [name, elementInput] of Object.entries(definitions)) { + elements[name] = this.create(name, elementInput) + } + + return elements + } + + /** + * Normalizes function shorthand to object format + */ + private static normalizeInput(elementInput: ElementInput): Partial { + if (isFunction(elementInput)) { + return { render: elementInput } + } + return elementInput + } + + /** + * Applies defaults and ensures required properties + */ + private static applyDefaults( + name: string, + element: Partial + ): ElementConfig { + const tagName = name.trim().toLowerCase() + + return defaults({}, element, { + ...this.DEFAULT_CONFIG, + tagName, + render: () => false + }) as ElementConfig + } + + /** + * Enhances element with text styling rules if needed + */ + private static enhanceWithTextRules(config: ElementConfig): ElementConfig { + if (!config.containsText) { + return config + } + + return { + ...config, + rules: { + ...config.rules, + '.header': TEXT_STYLE_RULES.header, + '.text': TEXT_STYLE_RULES.text + } + } + } } + +// Convenience export +export default ElementFactory.create