The vanilla alternative for writing JSX-based React applications
git clone git://soophie.de/figure
1
{2
"compilerOptions": {3
"lib": [4
"dom",5
"dom.iterable",6
"dom.asynciterable",7
"deno.ns",8
"deno.unstable"9
]10
}11
}
43
}45
const app = document.querySelector('#app');46
figure({createElement});46
figure({ createElement });47
render(createElement(Counter), app);49
</script>
85
}87
const app = document.querySelector('#app');88
figure({createElement});88
figure({ createElement });89
render(createElement(Picker), app);91
</script>
1
const parser = new DOMParser();2
let count = 1000;3
let create = null;5
const figure = ({createElement}) => {6
create = createElement;7
}9
const html = (strings, ...props) => {10
const elements = parse(strings, ...props);11
if(elements.length === 0) {12
throw 'No DOM element was returned!';13
}14
if(elements.length > 1) {15
console.warn('Only one DOM element can be returned!');16
}17
return elements[0];18
}20
const parse = (strings, ...props) => {21
const [html, refs] = compose(strings, props);22
let dom;23
try {24
dom = parser.parseFromString(html, 'text/html');25
}26
catch(error) {27
console.error(error);28
throw 'Invalid DOM structure!';29
}30
const nodes = [...dom.head.childNodes, ...dom.body.childNodes];31
return nodes.map((node) => render(node, refs));32
}34
/**35
* Joins the template literal strings together and replaces the properties with references.36
* The properties are being mapped to there corresponding references and with the populated37
* HTML string returned.38
*39
* @param {string[]} strings - The template literal strings40
* @param {any[]} props - The template literal properties41
* @return {any[]} The joined HTML string and the properties mapped to there references.42
*/43
const compose = (strings, props) => {44
if(strings === undefined) {45
// handles dyn function without body46
return ['', {}];47
}48
const refs = {};49
let string = '';50
for(let i = 0; i < strings.length; i++) {51
string += strings[i];52
if(props[i] !== undefined) {53
const uid = `$seg-${count++}`;54
refs[uid] = props[i];55
string += uid ?? '';56
}57
}58
return [string.trim(), refs];59
}61
/**62
* Injects the properties into the corresponding reference locations of the string.63
*64
* @param {string} string - The string containing references65
* @param {any[]} refs - The properties mapped to there references66
* @return {any[]} The string populate with the passed properties67
*/68
const feed = (string, refs) => {69
const expr = /\$seg-\d+/g;70
let values = [];71
let match = null;72
let last = 0;73
while((match = expr.exec(string)) !== null) {74
const index = match.index;75
const uid = match[0];76
values.push(string.substring(last, index));77
let value = refs[uid];78
if(value instanceof Function) {79
value = value();80
}81
values.push(value);82
last = index + uid.length;83
}84
values.push(string.substring(last));85
return values;86
}88
const dyn = (element, props) => {89
return (strings, ...childProps) => {90
return create(element, props, ...parse(strings, ...childProps));91
};92
}94
const render = (node, refs) => {95
if(node.nodeType === Node.TEXT_NODE) {96
return feed(node.textContent, refs);97
}98
if(node.nodeType === Node.COMMENT_NODE) {99
return [];100
}101
const tag = node.tagName;102
const attributes = {};103
for(const attribute of node.attributes) {104
const key = attribute.name;105
const value = attribute.textContent;106
let isDynamic = false;107
for(const ref in refs) {108
if(value === ref) {109
let match = null;110
if((match = /^@(\w+)$/.exec(key)) !== null) {111
const event = match[1];112
attributes[`on${event.substring(0, 1).toUpperCase()}${event.substring(1)}`] = refs[ref];113
}114
else if((match = /^\[(\w+)\]$/.exec(key)) !== null) {115
const property = match[1];116
attributes[property] = refs[ref];117
}118
else {119
attributes[key] = refs[ref];120
}121
isDynamic = true;122
break;123
}124
}125
if(!isDynamic) {126
if(key === 'style') {127
const styles = {};128
for(const item of value.split(';')) {129
if(item.trim().length === 0) {130
break;131
}132
let [key, value] = item.split(':');133
key = key.trim();134
let match = null;135
if((match = /-(\w)/.exec(key)) !== null) {136
const char = match[1];137
key = key.replace(`-${char}`, char.toUpperCase());138
}139
value = value.trim();140
styles[key] = feed(value, refs).join('');141
}142
attributes[key] = styles;143
}144
else {145
attributes[key] = feed(value, refs).join('');146
}147
}148
}149
const children = [];150
(node.childNodes ?? []).forEach((child) => children.push(...render(child, refs)));151
return [create(tag, attributes, ...children)];152
}154
import {defCss, defStyle} from './mods.js';156
const css = defCss({html, compose, feed});157
const style = defStyle({html, compose, feed});159
export { figure, html, dyn, css, style };
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 };
1
export const defCss = ({html, compose, feed}) => {2
return (strings, ...props) => {3
const [css, refs] = compose(strings, props);4
return html`5
<style type="text/css">6
${feed(css, refs).join('')}7
</style>8
`;9
};10
}12
export const defStyle = ({compose, feed}) => {13
return (strings, ...props) => {14
const [css, refs] = compose(strings, props);15
const styles = {};16
for(const item of css.split(';')) {17
if(item.trim().length === 0) {18
break;19
}20
let [key, value] = item.split(':');21
key = key.trim();22
let match = null;23
if((match = /-(\w)/.exec(key)) !== null) {24
const char = match[1];25
key = key.replace(`-${char}`, char.toUpperCase());26
}27
value = value.trim();28
styles[key] = feed(value, refs).join('');29
}30
return styles;31
};32
}
1
export type Option<T> = T | null | undefined;3
export type ReactElement = unknown;4
export type ReactFunction = unknown;5
export type CreateElement = (element: ReactFunction, props?: Props, ...children: ReactElement[]) => ReactElement;7
export type Options = { createElement: CreateElement };8
export type Refs = Record<string, unknown>;9
export type Props = Record<string, unknown>;10
export type Slices = TemplateStringsArray;11
export type Values = unknown[];