Adding hotfixes for packages
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
37
jsx-html/node/ComponentNode.ts
Normal file
37
jsx-html/node/ComponentNode.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import type {
|
||||
NodePropsType,
|
||||
ComponentFunctionType,
|
||||
NullableChildType,
|
||||
} from '../types.ts';
|
||||
import { NODE_TYPE } from '../constants.ts';
|
||||
import { FragmentNode } from './FragmentNode.ts';
|
||||
import { Node } from './Node.ts';
|
||||
import { normalizeChildren } from './utils/normalizeChildren.ts';
|
||||
|
||||
export class ComponentNode extends Node {
|
||||
type = NODE_TYPE.COMPONENT;
|
||||
|
||||
constructor(
|
||||
public component: ComponentFunctionType,
|
||||
public props: NodePropsType,
|
||||
children: NullableChildType[],
|
||||
) {
|
||||
super(children);
|
||||
}
|
||||
|
||||
async render(): Promise<string | any[]> {
|
||||
return [].concat((await this.renderComponent()) as any).join('');
|
||||
}
|
||||
|
||||
async renderComponent() {
|
||||
const child = await this.component(this.props, this.children);
|
||||
const children = normalizeChildren(
|
||||
Array.isArray(child) ? child : [child],
|
||||
);
|
||||
if (children.length === 1) {
|
||||
return children[0].render();
|
||||
} else if (children.length > 1) {
|
||||
return new FragmentNode(children).render();
|
||||
}
|
||||
}
|
||||
}
|
91
jsx-html/node/ElementNode.ts
Normal file
91
jsx-html/node/ElementNode.ts
Normal file
@ -0,0 +1,91 @@
|
||||
import { NODE_TYPE } from '../constants.ts';
|
||||
import type { NodePropsType, NullableChildType } from '../types.ts';
|
||||
import { Node } from './Node.ts';
|
||||
import { doubleQuoteEncode } from './utils/htmlEncode.ts';
|
||||
|
||||
const ELEMENT_PROP = {
|
||||
INNER_HTML: 'innerHTML',
|
||||
};
|
||||
|
||||
// List taken from http://w3c.github.io/html-reference/syntax.html
|
||||
const VOID_ELEMENTS = new Set<string>([
|
||||
'area',
|
||||
'base',
|
||||
'br',
|
||||
'col',
|
||||
'command',
|
||||
'embed',
|
||||
'hr',
|
||||
'img',
|
||||
'input',
|
||||
'keygen',
|
||||
'link',
|
||||
'meta',
|
||||
'param',
|
||||
'source',
|
||||
'track',
|
||||
'wbr',
|
||||
]);
|
||||
|
||||
export class ElementNode extends Node {
|
||||
type = NODE_TYPE.ELEMENT;
|
||||
|
||||
constructor(
|
||||
public name: string,
|
||||
public props: NodePropsType,
|
||||
children: NullableChildType[],
|
||||
) {
|
||||
super(children);
|
||||
}
|
||||
|
||||
async render(): Promise<string | any[]> {
|
||||
const renderedProps = this.propsToHTML();
|
||||
|
||||
const renderedChildren =
|
||||
typeof this.props[ELEMENT_PROP.INNER_HTML] === 'string'
|
||||
? this.props[ELEMENT_PROP.INNER_HTML]
|
||||
: (await this.renderChildren()).join('');
|
||||
|
||||
return renderedChildren || !VOID_ELEMENTS.has(this.name)
|
||||
? `<${this.name}${renderedProps}>${renderedChildren || ''}</${
|
||||
this.name
|
||||
}>`
|
||||
: `<${this.name}${renderedProps} />`;
|
||||
}
|
||||
|
||||
private getValidProps() {
|
||||
const props = this.props;
|
||||
return Object.keys(this.props).filter((key) => {
|
||||
if (key === ELEMENT_PROP.INNER_HTML) {
|
||||
return false;
|
||||
}
|
||||
const val = props[key];
|
||||
return (
|
||||
typeof val === 'string' ||
|
||||
typeof val === 'number' ||
|
||||
val === true
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private propsToHTML(): string {
|
||||
const keys = this.getValidProps();
|
||||
if (!keys.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const props = this.props;
|
||||
const pairs = keys.map((key) => {
|
||||
if (!/^[a-zA-Z0-9-:\._]+$/.test(key)) {
|
||||
throw new Error(`Invalid attribute name format ${key}`);
|
||||
}
|
||||
const val = props[key];
|
||||
// https://html.spec.whatwg.org/multipage/dom.html#attributes
|
||||
return val === true || val === ''
|
||||
? key
|
||||
: `${key}="${doubleQuoteEncode(val.toString())}"`;
|
||||
});
|
||||
|
||||
return ` ${pairs.join(' ')}`;
|
||||
}
|
||||
}
|
15
jsx-html/node/FragmentNode.ts
Normal file
15
jsx-html/node/FragmentNode.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { NODE_TYPE } from '../constants.ts';
|
||||
import type { ChildNodeType } from '../types.ts';
|
||||
import { Node } from './Node.ts';
|
||||
|
||||
export class FragmentNode extends Node {
|
||||
type = NODE_TYPE.FRAGMENT;
|
||||
|
||||
constructor(children: ChildNodeType[]) {
|
||||
super(children);
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.renderChildren();
|
||||
}
|
||||
}
|
29
jsx-html/node/Node.ts
Normal file
29
jsx-html/node/Node.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import type { NODE_TYPE } from '../constants.ts';
|
||||
import type { NullableChildType } from '../types.ts';
|
||||
import { normalizeChildren } from './utils/normalizeChildren.ts';
|
||||
|
||||
export abstract class Node {
|
||||
abstract type: NODE_TYPE;
|
||||
|
||||
constructor(public children: NullableChildType[]) {}
|
||||
|
||||
abstract async render(): Promise<string | any[]>;
|
||||
|
||||
async renderChildren() {
|
||||
const result: string[] = [];
|
||||
const children = normalizeChildren(this.children);
|
||||
for (const child of children) {
|
||||
const renderedChild = await child.render();
|
||||
if (renderedChild) {
|
||||
if (Array.isArray(renderedChild)) {
|
||||
renderedChild.forEach(
|
||||
(subchild) => subchild && result.push(subchild),
|
||||
);
|
||||
} else {
|
||||
result.push(renderedChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
12
jsx-html/node/TextNode.ts
Normal file
12
jsx-html/node/TextNode.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { NODE_TYPE } from '../constants.ts';
|
||||
import { htmlEncode } from './utils/htmlEncode.ts';
|
||||
|
||||
export class TextNode {
|
||||
type = NODE_TYPE.TEXT;
|
||||
|
||||
constructor(public text: string) {}
|
||||
|
||||
async render(): Promise<string | any[]> {
|
||||
return htmlEncode(this.text);
|
||||
}
|
||||
}
|
13
jsx-html/node/utils/htmlEncode.ts
Normal file
13
jsx-html/node/utils/htmlEncode.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export function doubleQuoteEncode(text: string): string {
|
||||
return text
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
|
||||
export function htmlEncode(text: string): string {
|
||||
return doubleQuoteEncode(text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/\//g, '/')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/'/g, '''));
|
||||
}
|
31
jsx-html/node/utils/normalizeChildren.ts
Normal file
31
jsx-html/node/utils/normalizeChildren.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import type { NullableChildType, ChildNodeType } from '../../types.ts';
|
||||
import { TextNode } from '../TextNode.ts';
|
||||
import { NODE_TYPE } from '../../constants.ts';
|
||||
|
||||
export function normalizeChildren(
|
||||
children: NullableChildType[],
|
||||
): ChildNodeType[] {
|
||||
const result: any[] = [];
|
||||
|
||||
for (const child of children) {
|
||||
if (child && typeof child !== 'boolean') {
|
||||
if (typeof child === 'string' || typeof child === 'number') {
|
||||
result.push(new TextNode(`${child}`));
|
||||
} else if (Array.isArray(child)) {
|
||||
normalizeChildren(child).forEach((normalized) =>
|
||||
result.push(normalized),
|
||||
);
|
||||
} else if (
|
||||
child.type === NODE_TYPE.ELEMENT ||
|
||||
child.type === NODE_TYPE.TEXT ||
|
||||
child.type === NODE_TYPE.COMPONENT
|
||||
) {
|
||||
result.push(child);
|
||||
} else {
|
||||
throw new TypeError(`Unrecognized node type: ${typeof child}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
Reference in New Issue
Block a user