Skip to content

Instantly share code, notes, and snippets.

@storyn26383
Last active April 15, 2022 09:47
Show Gist options
  • Save storyn26383/76d390eae7e8d6be3a5b2a19b2b5b981 to your computer and use it in GitHub Desktop.
Save storyn26383/76d390eae7e8d6be3a5b2a19b2b5b981 to your computer and use it in GitHub Desktop.
Metaprogramming
import { html as beautify } from 'js-beautify'
type childCallback = (builder: HTMLBuilder) => void
declare function manipulate(content: string): void
declare function manipulate(content: string, attributes: Record<string, string>): void
declare function manipulate(callback: childCallback): void
declare function manipulate(callback: childCallback, attributes: Record<string, string>): void
class Tag {
private name: string
private attributes: Record<string, string> = {}
private children: (Tag | string)[] = []
constructor(name: string = '') {
this.name = name
}
appendChild(child: Tag): void {
this.children.push(child)
}
get manipulate(): typeof manipulate {
const tag = this
return <typeof manipulate>function (
content: Tag | string | childCallback,
attributes?: Record<string, string>
) {
if (attributes) {
tag.attributes = attributes
}
if (content instanceof Function) {
return content(new HTMLBuilder(tag))
}
tag.children.push(content)
}
}
render(): string {
const attributes = this.renderAttributes()
const children = this.renderChildren()
if (!this.name || this.name === 'text') {
return children
}
if (!children) {
return `<${this.name}${attributes} />`
}
return `<${this.name}${attributes}>${children}</${this.name}>`
}
private renderAttributes(): string {
let attributes: string = ''
for (const name in this.attributes) {
attributes += ` ${name}="${this.attributes[name]}"`
}
return attributes
}
private renderChildren(): string {
return this.children.map((child) => {
if (typeof child === 'string') {
return child
}
return child.render()
}).join('')
}
}
function Builder(): new() => Pick<Tag, 'render'> & {
[key: string]: Tag['manipulate']
} {
return class {} as any
}
class HTMLBuilder extends Builder() {
constructor(root?: Tag) {
super()
if (root === undefined) {
root = new Tag()
}
return new Proxy(this, {
get (_, prop) {
if (prop === 'render') {
return function (): string {
return beautify(root!.render())
}
}
const tag = new Tag(prop.toString())
root!.appendChild(tag)
return tag.manipulate
}
})
}
}
const builder = new HTMLBuilder()
builder.html((html) => {
html.head((head) => {
head.title('Hello World')
})
html.body((body) => {
body.div((div) => {
div.h1('Hello World')
div.hr
div.p('Hello Kitty')
}, { id: 'app' })
})
})
console.log(builder.render())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment