Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 134 additions & 21 deletions packages/heml-utils/src/createElement.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,146 @@
import { defaults, isFunction } from 'lodash'
// types.ts
export interface ElementConfig {
tagName: string
attrs: string[]
children: boolean | string[]
defaultAttrs: Record<string, any>
rules?: Record<string, (string | RegExp)[]>
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<ElementConfig> | ((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<ElementConfig, 'render' | 'tagName'> = {
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<string, ElementInput>
): Record<string, ElementConfig> {
const elements: Record<string, ElementConfig> = {}

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<ElementConfig> {
if (isFunction(elementInput)) {
return { render: elementInput }
}
return elementInput
}

/**
* Applies defaults and ensures required properties
*/
private static applyDefaults(
name: string,
element: Partial<ElementConfig>
): 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