2022-08-01 23:17:24 +00:00
|
|
|
import { escapeToBuffer } from '../../utils/html'
|
2022-08-03 02:24:51 +00:00
|
|
|
import type { StringBuffer, HtmlEscaped, HtmlEscapedString } from '../../utils/html'
|
2022-06-07 10:03:42 +00:00
|
|
|
|
2022-11-30 22:49:28 +00:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
2023-02-17 21:50:52 +00:00
|
|
|
type Props = Record<string, any>
|
2022-11-30 22:49:28 +00:00
|
|
|
|
2022-06-07 10:03:42 +00:00
|
|
|
declare global {
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
2023-02-17 21:50:52 +00:00
|
|
|
namespace JSX {
|
|
|
|
type Element = HtmlEscapedString
|
2023-05-29 13:00:27 +00:00
|
|
|
interface ElementChildrenAttribute {
|
|
|
|
children: Child
|
|
|
|
}
|
2022-06-07 10:03:42 +00:00
|
|
|
interface IntrinsicElements {
|
2023-02-17 21:50:52 +00:00
|
|
|
[tagName: string]: Props
|
2022-06-07 10:03:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-14 23:07:54 +00:00
|
|
|
const emptyTags = [
|
|
|
|
'area',
|
|
|
|
'base',
|
|
|
|
'br',
|
|
|
|
'col',
|
|
|
|
'embed',
|
|
|
|
'hr',
|
|
|
|
'img',
|
|
|
|
'input',
|
|
|
|
'keygen',
|
|
|
|
'link',
|
|
|
|
'meta',
|
|
|
|
'param',
|
|
|
|
'source',
|
|
|
|
'track',
|
|
|
|
'wbr',
|
|
|
|
]
|
2022-08-02 14:04:13 +00:00
|
|
|
const booleanAttributes = [
|
|
|
|
'allowfullscreen',
|
|
|
|
'async',
|
|
|
|
'autofocus',
|
|
|
|
'autoplay',
|
|
|
|
'checked',
|
|
|
|
'controls',
|
|
|
|
'default',
|
|
|
|
'defer',
|
|
|
|
'disabled',
|
|
|
|
'formnovalidate',
|
|
|
|
'hidden',
|
|
|
|
'inert',
|
|
|
|
'ismap',
|
|
|
|
'itemscope',
|
|
|
|
'loop',
|
|
|
|
'multiple',
|
|
|
|
'muted',
|
|
|
|
'nomodule',
|
|
|
|
'novalidate',
|
|
|
|
'open',
|
|
|
|
'playsinline',
|
|
|
|
'readonly',
|
|
|
|
'required',
|
|
|
|
'reversed',
|
|
|
|
'selected',
|
|
|
|
]
|
2022-07-31 12:58:59 +00:00
|
|
|
|
2022-08-03 02:24:51 +00:00
|
|
|
const childrenToStringToBuffer = (children: Child[], buffer: StringBuffer): void => {
|
2022-08-01 23:17:24 +00:00
|
|
|
for (let i = 0, len = children.length; i < len; i++) {
|
|
|
|
const child = children[i]
|
|
|
|
if (typeof child === 'string') {
|
|
|
|
escapeToBuffer(child, buffer)
|
|
|
|
} else if (typeof child === 'boolean' || child === null || child === undefined) {
|
|
|
|
continue
|
|
|
|
} else if (child instanceof JSXNode) {
|
|
|
|
child.toStringToBuffer(buffer)
|
2023-02-17 21:50:52 +00:00
|
|
|
} else if (
|
|
|
|
typeof child === 'number' ||
|
|
|
|
(child as unknown as { isEscaped: boolean }).isEscaped
|
|
|
|
) {
|
2022-08-01 23:17:24 +00:00
|
|
|
buffer[0] += child
|
|
|
|
} else {
|
|
|
|
// `child` type is `Child[]`, so stringify recursively
|
|
|
|
childrenToStringToBuffer(child, buffer)
|
|
|
|
}
|
|
|
|
}
|
2022-07-31 12:58:59 +00:00
|
|
|
}
|
2022-07-14 23:07:54 +00:00
|
|
|
|
2022-08-01 23:17:24 +00:00
|
|
|
type Child = string | number | JSXNode | Child[]
|
|
|
|
export class JSXNode implements HtmlEscaped {
|
|
|
|
tag: string | Function
|
2022-11-30 22:49:28 +00:00
|
|
|
props: Props
|
2022-08-01 23:17:24 +00:00
|
|
|
children: Child[]
|
2023-05-02 09:45:21 +00:00
|
|
|
isEscaped: true = true as const
|
2022-11-30 22:49:28 +00:00
|
|
|
constructor(tag: string | Function, props: Props, children: Child[]) {
|
2022-08-01 23:17:24 +00:00
|
|
|
this.tag = tag
|
|
|
|
this.props = props
|
|
|
|
this.children = children
|
2022-06-07 10:03:42 +00:00
|
|
|
}
|
2022-06-10 01:19:04 +00:00
|
|
|
|
2022-08-01 23:17:24 +00:00
|
|
|
toString(): string {
|
2022-08-03 02:24:51 +00:00
|
|
|
const buffer: StringBuffer = ['']
|
2022-08-01 23:17:24 +00:00
|
|
|
this.toStringToBuffer(buffer)
|
|
|
|
return buffer[0]
|
|
|
|
}
|
|
|
|
|
2022-08-03 02:24:51 +00:00
|
|
|
toStringToBuffer(buffer: StringBuffer): void {
|
2022-08-01 23:17:24 +00:00
|
|
|
const tag = this.tag as string
|
|
|
|
const props = this.props
|
|
|
|
let { children } = this
|
|
|
|
|
|
|
|
buffer[0] += `<${tag}`
|
|
|
|
|
|
|
|
const propsKeys = Object.keys(props || {})
|
|
|
|
|
|
|
|
for (let i = 0, len = propsKeys.length; i < len; i++) {
|
|
|
|
const v = props[propsKeys[i]]
|
|
|
|
if (typeof v === 'string') {
|
|
|
|
buffer[0] += ` ${propsKeys[i]}="`
|
|
|
|
escapeToBuffer(v, buffer)
|
|
|
|
buffer[0] += '"'
|
|
|
|
} else if (typeof v === 'number') {
|
|
|
|
buffer[0] += ` ${propsKeys[i]}="${v}"`
|
|
|
|
} else if (v === null || v === undefined) {
|
|
|
|
// Do nothing
|
|
|
|
} else if (typeof v === 'boolean' && booleanAttributes.includes(propsKeys[i])) {
|
|
|
|
if (v) {
|
|
|
|
buffer[0] += ` ${propsKeys[i]}=""`
|
|
|
|
}
|
|
|
|
} else if (propsKeys[i] === 'dangerouslySetInnerHTML') {
|
|
|
|
if (children.length > 0) {
|
|
|
|
throw 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.'
|
|
|
|
}
|
|
|
|
|
|
|
|
const escapedString = new String(v.__html) as HtmlEscapedString
|
|
|
|
escapedString.isEscaped = true
|
|
|
|
children = [escapedString]
|
|
|
|
} else {
|
|
|
|
buffer[0] += ` ${propsKeys[i]}="`
|
|
|
|
escapeToBuffer(v.toString(), buffer)
|
|
|
|
buffer[0] += '"'
|
2022-06-10 01:19:04 +00:00
|
|
|
}
|
2022-08-01 23:17:24 +00:00
|
|
|
}
|
2022-06-10 01:19:04 +00:00
|
|
|
|
2022-08-01 23:17:24 +00:00
|
|
|
if (emptyTags.includes(tag as string)) {
|
|
|
|
buffer[0] += '/>'
|
|
|
|
return
|
2022-06-10 01:19:04 +00:00
|
|
|
}
|
|
|
|
|
2022-08-01 23:17:24 +00:00
|
|
|
buffer[0] += '>'
|
|
|
|
|
|
|
|
childrenToStringToBuffer(children, buffer)
|
2022-06-07 10:03:42 +00:00
|
|
|
|
2022-08-01 23:17:24 +00:00
|
|
|
buffer[0] += `</${tag}>`
|
2022-06-11 23:20:31 +00:00
|
|
|
}
|
2022-08-01 23:17:24 +00:00
|
|
|
}
|
2022-06-10 01:19:04 +00:00
|
|
|
|
2022-08-01 23:17:24 +00:00
|
|
|
class JSXFunctionNode extends JSXNode {
|
2022-08-03 02:24:51 +00:00
|
|
|
toStringToBuffer(buffer: StringBuffer): void {
|
2022-08-01 23:17:24 +00:00
|
|
|
const { children } = this
|
|
|
|
|
|
|
|
const res = (this.tag as Function).call(null, {
|
|
|
|
...this.props,
|
|
|
|
children: children.length <= 1 ? children[0] : children,
|
|
|
|
})
|
|
|
|
|
|
|
|
if (res instanceof JSXNode) {
|
|
|
|
res.toStringToBuffer(buffer)
|
|
|
|
} else if (typeof res === 'number' || (res as HtmlEscaped).isEscaped) {
|
|
|
|
buffer[0] += res
|
2022-06-10 01:19:04 +00:00
|
|
|
} else {
|
2022-08-01 23:17:24 +00:00
|
|
|
escapeToBuffer(res, buffer)
|
2022-06-10 01:19:04 +00:00
|
|
|
}
|
|
|
|
}
|
2022-08-01 23:17:24 +00:00
|
|
|
}
|
2022-06-10 01:19:04 +00:00
|
|
|
|
2022-08-01 23:17:24 +00:00
|
|
|
class JSXFragmentNode extends JSXNode {
|
2022-08-03 02:24:51 +00:00
|
|
|
toStringToBuffer(buffer: StringBuffer): void {
|
2022-08-01 23:17:24 +00:00
|
|
|
childrenToStringToBuffer(this.children, buffer)
|
2022-06-11 23:20:31 +00:00
|
|
|
}
|
2022-08-01 23:17:24 +00:00
|
|
|
}
|
2022-06-10 01:19:04 +00:00
|
|
|
|
2022-08-01 23:17:24 +00:00
|
|
|
export { jsxFn as jsx }
|
|
|
|
const jsxFn = (
|
|
|
|
tag: string | Function,
|
2022-11-30 22:49:28 +00:00
|
|
|
props: Props,
|
2022-08-01 23:17:24 +00:00
|
|
|
...children: (string | HtmlEscapedString)[]
|
|
|
|
): JSXNode => {
|
|
|
|
if (typeof tag === 'function') {
|
|
|
|
return new JSXFunctionNode(tag, props, children)
|
|
|
|
} else {
|
|
|
|
return new JSXNode(tag, props, children)
|
|
|
|
}
|
2022-06-07 10:03:42 +00:00
|
|
|
}
|
2022-06-10 06:09:29 +00:00
|
|
|
|
2022-11-30 22:49:28 +00:00
|
|
|
type FC<T = Props> = (props: T) => HtmlEscapedString
|
2022-06-10 06:09:29 +00:00
|
|
|
|
2022-11-30 22:49:28 +00:00
|
|
|
const shallowEqual = (a: Props, b: Props): boolean => {
|
2022-06-10 06:09:29 +00:00
|
|
|
if (a === b) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
const aKeys = Object.keys(a)
|
|
|
|
const bKeys = Object.keys(b)
|
|
|
|
if (aKeys.length !== bKeys.length) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let i = 0, len = aKeys.length; i < len; i++) {
|
|
|
|
if (a[aKeys[i]] !== b[aKeys[i]]) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
export const memo = <T>(
|
|
|
|
component: FC<T>,
|
|
|
|
propsAreEqual: (prevProps: Readonly<T>, nextProps: Readonly<T>) => boolean = shallowEqual
|
|
|
|
): FC<T> => {
|
|
|
|
let computed = undefined
|
|
|
|
let prevProps: T | undefined = undefined
|
2022-06-12 01:24:50 +00:00
|
|
|
return ((props: T): HtmlEscapedString => {
|
2022-06-10 06:09:29 +00:00
|
|
|
if (prevProps && !propsAreEqual(prevProps, props)) {
|
|
|
|
computed = undefined
|
|
|
|
}
|
|
|
|
prevProps = props
|
|
|
|
return (computed ||= component(props))
|
|
|
|
}) as FC<T>
|
|
|
|
}
|
2022-06-11 23:20:31 +00:00
|
|
|
|
2022-11-30 22:49:28 +00:00
|
|
|
export const Fragment = (props: { key?: string; children?: Child[] }): JSXNode => {
|
2022-08-01 23:17:24 +00:00
|
|
|
return new JSXFragmentNode('', {}, props.children || [])
|
2022-06-11 23:20:31 +00:00
|
|
|
}
|