The vanilla alternative for writing JSX-based React applications
git clone git://soophie.de/figure
blob: 41c4096798f540df98dff69cfad315978efc4e5b (5915 bytes)[doc][blame][raw]
1
import { CreateElement, Option, Options, Props, ReactElement, ReactFunction, Refs, Slices, Values } from './types.ts';3
const parser = new DOMParser();4
let count = 1000;5
let create: Option<CreateElement> = null;7
function figure({ createElement }: Options): void {8
create = createElement;9
}11
function html(slices: Slices, ...values: Values): ReactElement {12
const elements = parse(slices, ...values);13
if (elements.length === 0) {14
throw 'No DOM element was returned!';15
}16
if (elements.length > 1) {17
console.warn('Only one DOM element can be returned!');18
}19
return elements[0];20
}22
function parse(slices: Slices, ...values: Values): ReactElement[] {23
const [html, refs] = compose(slices, values);24
let dom;25
try {26
dom = parser.parseFromString(html, 'text/html');27
}28
catch (error) {29
console.error(error);30
throw 'Invalid DOM structure!';31
}32
const nodes = [...dom.head.childNodes, ...dom.body.childNodes];33
return nodes.map((node) => render(node, refs));34
}36
/**37
* Joins the template literal slices together and replaces the values with references.38
* The values are being mapped to there corresponding references and with the populated39
* HTML string returned.40
*41
* @param {string[]} slices - The template literal slices42
* @param {any[]} values - The template literal values43
* @return {any[]} The joined HTML string and the values mapped to there references.44
*/45
function compose(slices: Slices, values: Values): [string, Refs] {46
if (slices == null) {47
// handles dyn function without body48
return ['', {}];49
}50
const refs: Refs = {};51
let slice = '';52
for (let i = 0; i < slices.length; i++) {53
slice += slices[i];54
if (values[i] != null) {55
const uid = `$seg-${count++}`;56
refs[uid] = values[i];57
slice += uid ?? '';58
}59
}60
return [slice.trim(), refs];61
}63
/**64
* Injects the values into the corresponding reference locations of the string.65
*66
* @param {string} slice - The string containing references67
* @param {any[]} refs - The values mapped to there references68
* @return {any[]} The string populated with the passed values69
*/70
function feed(slice: string, refs: Refs): ReactElement[] {71
const expr = /\$seg-\d+/g;72
const elements: ReactElement[] = [];73
let match = null;74
let last = 0;75
while ((match = expr.exec(slice)) !== null) {76
const index = match.index;77
const uid = match[0];78
elements.push(slice.substring(last, index));79
let value = refs[uid];80
if (value instanceof Function) {81
value = value();82
}83
elements.push(value);84
last = index + uid.length;85
}86
elements.push(slice.substring(last));87
return elements;88
}90
function dyn(element: ReactFunction, props: Props): (slices: Slices, ...values: Values) => ReactElement {91
return (slices: Slices, ...values: Values) => {92
if (create == null) {93
throw 'Invalid state! Figure was not initialized!';94
}95
return create(element, props, ...parse(slices, ...values));96
};97
}99
function render(node: Node, refs: Refs): ReactElement[] {100
if (node.nodeType === Node.TEXT_NODE) {101
const text = node as Text;102
if (text.textContent == null) {103
return [];104
}105
return feed(text.textContent, refs);106
}107
if (node.nodeType === Node.COMMENT_NODE) {108
return [];109
}110
const element = node as HTMLElement;111
const tag = element.tagName;112
const attributes: Props = {};113
for (const attribute of element.attributes) {114
const key = attribute.name;115
const slice = attribute.textContent;116
if (slice == null) {117
continue;118
}119
let isDynamic = false;120
for (const ref in refs) {121
if (slice === ref) {122
let match = null;123
if ((match = /^@(\w+)$/.exec(key)) !== null) {124
const event = match[1];125
attributes[`on${event.substring(0, 1).toUpperCase()}${event.substring(1)}`] = refs[ref];126
}127
else if ((match = /^\[(\w+)\]$/.exec(key)) !== null) {128
const property = match[1];129
attributes[property] = refs[ref];130
}131
else {132
attributes[key] = refs[ref];133
}134
isDynamic = true;135
break;136
}137
}138
if (!isDynamic) {139
if (key === 'style') {140
const styles: Record<string, string> = {};141
for (const item of slice.split(';')) {142
if (item.trim().length === 0) {143
break;144
}145
let [key, value] = item.split(':');146
key = key.trim();147
let match = null;148
if ((match = /-(\w)/.exec(key)) !== null) {149
const char = match[1];150
key = key.replace(`-${char}`, char.toUpperCase());151
}152
value = value.trim();153
styles[key] = feed(value, refs).join('');154
}155
attributes[key] = styles;156
}157
else {158
attributes[key] = feed(slice, refs).join('');159
}160
}161
}162
const children: ReactElement[] = [];163
(node.childNodes ?? []).forEach((child) => children.push(...render(child, refs)));164
if (create == null) {165
throw 'Invalid state! Figure was not initialized!';166
}167
return [create(tag, attributes, ...children)];168
}170
function css(slices: Slices, ...values: Values): ReactElement {171
const [css, refs] = compose(slices, values);172
return html`173
<style type="text/css">174
${feed(css, refs).join('')}175
</style>176
`;177
}179
function style(slices: Slices, ...values: Values): Record<string, string> {180
const [css, refs] = compose(slices, values);181
const styles: Record<string, string> = {};182
for (const item of css.split(';')) {183
if (item.trim().length === 0) {184
break;185
}186
let [key, value] = item.split(':');187
key = key.trim();188
let match = null;189
if ((match = /-(\w)/.exec(key)) !== null) {190
const char = match[1];191
key = key.replace(`-${char}`, char.toUpperCase());192
}193
value = value.trim();194
styles[key] = feed(value, refs).join('');195
}196
return styles;197
}199
export { figure, html, dyn, css, style };