2022-06-07 10:03:42 +00:00
|
|
|
import { escape } from '../../utils/html'
|
2022-06-12 01:24:50 +00:00
|
|
|
import type { HtmlEscapedString } from '../../utils/html'
|
2022-06-07 10:03:42 +00:00
|
|
|
|
|
|
|
declare global {
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
2022-06-13 00:25:27 +00:00
|
|
|
namespace jsx.JSX {
|
2022-06-07 10:03:42 +00:00
|
|
|
interface IntrinsicElements {
|
2022-06-10 01:19:04 +00:00
|
|
|
[tagName: string]: Record<string, any>
|
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-07-31 12:58:59 +00:00
|
|
|
const booleanAttributes = ['checked', 'selected', 'disabled', 'readonly', 'multiple']
|
|
|
|
|
|
|
|
const newHtmlEscapedString = (str: string): HtmlEscapedString => {
|
|
|
|
const escapedString = new String(str) as HtmlEscapedString
|
|
|
|
escapedString.isEscaped = true
|
|
|
|
return escapedString
|
|
|
|
}
|
2022-07-14 23:07:54 +00:00
|
|
|
|
2022-07-24 08:36:37 +00:00
|
|
|
export { jsxFn as jsx }
|
|
|
|
const jsxFn = (
|
2022-06-07 10:03:42 +00:00
|
|
|
tag: string | Function,
|
|
|
|
props: Record<string, any>,
|
2022-06-12 01:24:50 +00:00
|
|
|
...children: (string | HtmlEscapedString)[]
|
|
|
|
): HtmlEscapedString => {
|
2022-06-07 10:03:42 +00:00
|
|
|
if (typeof tag === 'function') {
|
2022-06-10 01:19:04 +00:00
|
|
|
return tag.call(null, { ...props, children: children.length <= 1 ? children[0] : children })
|
2022-06-07 10:03:42 +00:00
|
|
|
}
|
2022-06-10 01:19:04 +00:00
|
|
|
|
2022-06-11 23:20:31 +00:00
|
|
|
let result = tag !== '' ? `<${tag}` : ''
|
2022-06-10 01:19:04 +00:00
|
|
|
|
2022-06-07 10:03:42 +00:00
|
|
|
const propsKeys = Object.keys(props || {})
|
2022-07-30 04:49:00 +00:00
|
|
|
|
2022-06-07 10:03:42 +00:00
|
|
|
for (let i = 0, len = propsKeys.length; i < len; i++) {
|
2022-06-10 01:19:04 +00:00
|
|
|
const v = props[propsKeys[i]]
|
2022-07-31 12:58:59 +00:00
|
|
|
if (typeof v === 'string') {
|
|
|
|
result += ` ${propsKeys[i]}="${escape(v)}"`
|
|
|
|
} else if (typeof v === 'number') {
|
|
|
|
result += ` ${propsKeys[i]}="${v}"`
|
|
|
|
} else if (v === null || v === undefined) {
|
|
|
|
// Do nothing
|
|
|
|
} else if (typeof v === 'boolean' && booleanAttributes.includes(propsKeys[i])) {
|
|
|
|
if (v) {
|
|
|
|
result += ` ${propsKeys[i]}=""`
|
|
|
|
}
|
|
|
|
} else if (propsKeys[i] === 'dangerouslySetInnerHTML') {
|
2022-06-10 01:19:04 +00:00
|
|
|
if (children.length > 0) {
|
|
|
|
throw 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.'
|
|
|
|
}
|
|
|
|
|
2022-07-31 12:58:59 +00:00
|
|
|
children = [newHtmlEscapedString(v.__html)]
|
|
|
|
} else {
|
|
|
|
result += ` ${propsKeys[i]}="${escape(v.toString())}"`
|
2022-06-10 01:19:04 +00:00
|
|
|
}
|
2022-07-31 12:58:59 +00:00
|
|
|
}
|
2022-06-10 01:19:04 +00:00
|
|
|
|
2022-07-31 12:58:59 +00:00
|
|
|
if (emptyTags.includes(tag)) {
|
|
|
|
result += '/>'
|
|
|
|
return newHtmlEscapedString(result)
|
2022-06-07 10:03:42 +00:00
|
|
|
}
|
|
|
|
|
2022-06-11 23:20:31 +00:00
|
|
|
if (tag !== '') {
|
|
|
|
result += '>'
|
|
|
|
}
|
2022-06-10 01:19:04 +00:00
|
|
|
|
|
|
|
const flattenChildren = children.flat(Infinity)
|
|
|
|
for (let i = 0, len = flattenChildren.length; i < len; i++) {
|
|
|
|
const child = flattenChildren[i]
|
|
|
|
if (typeof child === 'boolean' || child === null || child === undefined) {
|
|
|
|
continue
|
|
|
|
} else if (typeof child === 'object' && (child as any).isEscaped) {
|
|
|
|
result += child
|
|
|
|
} else {
|
|
|
|
result += escape(child.toString())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-31 12:58:59 +00:00
|
|
|
if (tag !== '') {
|
2022-06-11 23:20:31 +00:00
|
|
|
result += `</${tag}>`
|
|
|
|
}
|
2022-06-10 01:19:04 +00:00
|
|
|
|
2022-07-31 12:58:59 +00:00
|
|
|
return newHtmlEscapedString(result)
|
2022-06-07 10:03:42 +00:00
|
|
|
}
|
2022-06-10 06:09:29 +00:00
|
|
|
|
2022-06-12 01:24:50 +00:00
|
|
|
type FC<T = Record<string, any>> = (props: T) => HtmlEscapedString
|
2022-06-10 06:09:29 +00:00
|
|
|
|
|
|
|
const shallowEqual = (a: Record<string, any>, b: Record<string, any>): boolean => {
|
|
|
|
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-06-12 01:24:50 +00:00
|
|
|
export const Fragment = (props: { key?: string; children?: any }): HtmlEscapedString => {
|
2022-07-24 08:36:37 +00:00
|
|
|
return jsxFn('', {}, ...(props.children || []))
|
2022-06-11 23:20:31 +00:00
|
|
|
}
|