lupine.web
Version:
lupine.web is a extremely fast, small size and lightweight frontend framework, using React TSX syntax.
158 lines (138 loc) • 5.95 kB
text/typescript
import { processStyle } from './bind-styles';
import { VNodeType, VNodeProps } from '../jsx';
import { ComponentStateStore, evaluateComponentWithStore } from './use-state';
import { renderAttribute, genUniqueId } from './render-attribute';
const selfClosingTags = [
'area',
'base',
'br',
'col',
'embed',
'hr',
'img',
'input',
'link',
'meta',
'param',
'source',
'track',
'wbr',
];
// shared state, global throttle for requests but safe
let _yieldCounter = 0;
const YIELD_THRESHOLD = 50;
import { encodeHtml } from '../lib/encode-html';
async function renderChildrenAsync(html: string[], children: any, uniqueClassName?: string, referToCssId?: string) {
if (typeof children === 'string') {
html.push(encodeHtml(children));
} else if (children === false || children === null || typeof children === 'undefined') {
// add nothing
} else if (typeof children === 'number' || typeof children === 'boolean') {
// true will be added
html.push(children.toString());
} else if (Array.isArray(children)) {
for (const item of children) {
await renderChildrenAsync(html, item, uniqueClassName, referToCssId);
}
} else if (children.type && children.props) {
await renderComponentAsync(children.type, children.props, uniqueClassName, referToCssId);
html.push(...children.props._html);
children.props._html.length = 0;
} else {
console.warn('Unexpected', children);
}
}
export const renderComponentAsync = async (type: VNodeType<any>, props: VNodeProps<any>, uniqueClassName?: string, referToCssId?: string) => {
_yieldCounter++;
if (_yieldCounter >= YIELD_THRESHOLD) {
_yieldCounter = 0;
await new Promise((resolve) => {
setTimeout(resolve, 0);
});
}
if (Array.isArray(props)) {
const jsxNodes = { type: 'Fragment', props: { children: props } } as any;
await renderComponentAsync(jsxNodes.type, jsxNodes.props, uniqueClassName, referToCssId);
return;
}
props._html = [];
if (typeof type === 'function') {
const store = new ComponentStateStore(type, props);
props._result = await evaluateComponentWithStore(store);
if (props._result === null || props._result === undefined || props._result === false) {
props._result = { type: 'Fragment', props };
}
if (typeof props._result.type === 'function') {
await renderComponentAsync(props._result.type, props._result.props, uniqueClassName, referToCssId);
if (props._result.props._html) {
props._html.push(...props._result.props._html);
props._result.props._html.length = 0;
}
return;
}
}
const newType = (props._result && props._result.type) || type;
const newProps = (props._result && props._result.props) || props;
if (typeof newType === 'string') {
// if (newProps._id) {
// console.warn('This component reference is used more than once:', newProps);
// }
if (newProps['class'] && newProps['className']) {
console.warn(`One tag can't have both class and className, and className will be ignored.`, newProps);
delete newProps['className'];
} else if (newProps['className']) {
newProps['class'] = newProps['className'];
delete newProps['className'];
}
let newUniqueClassName = uniqueClassName;
// if having css, we need to replace & for mapping css and classnames
// if having ref, we still need to replace & for searching
if (newProps['css'] || newProps['ref']) {
newUniqueClassName = genUniqueId(newProps);
if (!newProps['class']) newProps['class'] = newUniqueClassName;
if (newProps['css'] || !referToCssId) referToCssId = newUniqueClassName;
}
let newReferToCssId = referToCssId;
if (newProps['ref']) {
if (newProps['ref'].referToCssId) {
newReferToCssId = newProps['ref'].referToCssId;
}
// save for outerComponent/innerComponent loading
newProps['ref']._refCssId = newReferToCssId;
newProps['ref']._cssName = newUniqueClassName;
}
const attrs = renderAttribute(newType, newProps, { type, props }, newUniqueClassName, newReferToCssId);
if (selfClosingTags.includes(newType.toLowerCase())) {
if (newType === 'Fragment' && newProps.ref) {
props._html.push(`<!--[ref-${newProps._id}]--><fragment ${newProps._id}></fragment>`);
} else if (newType !== 'Fragment') {
props._html.push(`<${newType}${attrs ? ' ' : ''}${attrs} />`);
}
} else {
if (newType === 'Fragment' && newProps.ref) {
props._html.push(`<!--[ref-${newProps._id}]--><fragment ${newProps._id}></fragment>`);
} else if (newType !== 'Fragment') {
props._html.push(`<${newType}${attrs ? ' ' : ''}${attrs}>`);
}
if (newProps['css'] && newUniqueClassName) {
// notice: if referToCssId is provided, it should be only used in classnames.
const cssText = processStyle(newUniqueClassName, newProps['css'], false).join('');
props._html.push(`<style id="sty-${newUniqueClassName}">${cssText}</style>`);
}
if (newProps.children || newProps.children === 0) {
await renderChildrenAsync(props._html, newProps.children, newUniqueClassName, newReferToCssId);
} else if (newProps['dangerouslySetInnerHTML']) {
props._html.push(newProps['dangerouslySetInnerHTML']);
}
if (newType === 'Fragment' && newProps.ref) {
props._html.push(`<!--/[ref-${newProps._id}]-->`);
} else if (newType !== 'Fragment') {
props._html.push(`</${newType}>`);
}
}
} else if (newType.name === 'Fragment') {
await renderChildrenAsync(props._html, newProps.children, uniqueClassName, referToCssId);
} else {
console.warn('Unknown type: ', type, props, newType, newProps);
}
};