-
Notifications
You must be signed in to change notification settings - Fork 50
/
elements.tsx
128 lines (114 loc) · 3.9 KB
/
elements.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
/// <reference path="./jsx/element-types.d.ts" />
/// <reference path="./jsx/events.d.ts" />
/// <reference path="./jsx/intrinsic-elements.d.ts" />
type AttributeValue = number | string | Date | boolean | string[];
export interface Children {
children?: AttributeValue;
}
export interface CustomElementHandler {
(attributes: Attributes & Children, contents: string[]): string;
}
export interface Attributes {
[key: string]: AttributeValue;
}
const capitalACharCode = 'A'.charCodeAt(0);
const capitalZCharCode = 'Z'.charCodeAt(0);
const isUpper = (input: string, index: number) => {
const charCode = input.charCodeAt(index);
return capitalACharCode <= charCode && capitalZCharCode >= charCode;
};
const toKebabCase = (camelCased: string) => {
let kebabCased = '';
for (let i = 0; i < camelCased.length; i++) {
const prevUpperCased = i > 0 ? isUpper(camelCased, i - 1) : true;
const currentUpperCased = isUpper(camelCased, i);
const nextUpperCased = i < camelCased.length - 1 ? isUpper(camelCased, i + 1) : true;
if (!prevUpperCased && currentUpperCased || currentUpperCased && !nextUpperCased) {
kebabCased += '-';
kebabCased += camelCased[i].toLowerCase();
} else {
kebabCased += camelCased[i];
}
}
return kebabCased;
};
const escapeAttrNodeValue = (value: string) => {
return value.replace(/(&)|(")|(\u00A0)/g, function (_, amp, quote) {
if (amp) return '&';
if (quote) return '"';
return ' ';
});
};
const attributeToString = (attributes: Attributes) => (name: string): string => {
const value = attributes[name];
const formattedName = toKebabCase(name);
const makeAttribute = (value: string) => `${formattedName}="${value}"`;
if (value instanceof Date) {
return makeAttribute(value.toISOString());
} else switch (typeof value) {
case 'boolean':
// https://www.w3.org/TR/2008/WD-html5-20080610/semantics.html#boolean
if (value) {
return formattedName;
} else {
return '';
}
default:
return makeAttribute(escapeAttrNodeValue(value.toString()));
}
};
const attributesToString = (attributes: Attributes | undefined): string => {
if (attributes) {
return ' ' + Object.keys(attributes)
.filter(attribute => attribute !== 'children') // filter out children attributes
.map(attributeToString(attributes))
.filter(attribute => attribute.length) // filter out negative boolean attributes
.join(' ');
} else {
return '';
}
};
const contentsToString = (contents: any[] | undefined) => {
if (contents) {
return contents
.map(elements => Array.isArray(elements) ? elements.join('\n') : elements)
.join('\n');
} else {
return '';
}
};
const isVoidElement = (tagName: string) => {
return [
'area',
'base',
'br',
'col',
'command',
'embed',
'hr',
'img',
'input',
'keygen',
'link',
'meta',
'param',
'source',
'track',
'wbr'
].indexOf(tagName) > -1;
};
export function createElement(name: string | CustomElementHandler,
attributes: Attributes & Children | undefined = {},
...contents: string[]) {
const children = attributes && attributes.children || contents;
if (typeof name === 'function') {
return name(children ? { children, ...attributes } : attributes, contents);
} else {
const tagName = toKebabCase(name);
if (isVoidElement(tagName) && !contents.length) {
return `<${tagName}${attributesToString(attributes)}>`;
} else {
return `<${tagName}${attributesToString(attributes)}>${contentsToString(contents)}</${tagName}>`;
}
}
}