so://phie

figure

The vanilla alternative for writing JSX-based React applications
git clone git://soophie.de/figure

blob: f5c3698be5743c3c6aeed14c0e741342f675712f (5909 bytes)[doc][blame][raw]

1import { Options, Props, ReactElement, ReactFunction, Refs, Slices, Values } from './types.ts';

2

3export default function figure({ createElement }: Options) {

4

5 // the parser for interpreting HTML

6 const parser = new DOMParser();

7 // the counter for creating unique references

8 let count = 0;

9

10 /**

11 * Converts the template literal HTML syntax into React elements.

12 * @param {Slices} slices - The template literal slices

13 * @param {Values} values - The template literal values

14 * @return {ReactElement[]} The converted HTML as React elements.

15 */

16 function html(slices: Slices, ...values: Values): ReactElement[] {

17 const [html, refs] = compose(slices, values);

18 let dom;

19 try {

20 dom = parser.parseFromString(html, 'text/html');

21 }

22 catch (error) {

23 console.error(error);

24 throw 'Invalid DOM structure!';

25 }

26 // collect all nodes from head and body

27 const nodes = [...dom.head.childNodes, ...dom.body.childNodes];

28 return nodes.map((node) => render(node, refs));

29 }

30

31 /**

32 * Joins the template literal slices together and replaces the values with references.

33 * The values are being mapped to there corresponding references and with the populated

34 * HTML string returned.

35 *

36 * @param {Slices} slices - The template literal slices

37 * @param {Values} values - The template literal values

38 * @return {[string, Refs]} The joined HTML string and the values mapped to there references.

39 */

40 function compose(slices: Slices, values: Values): [string, Refs] {

41 if (slices == null) {

42 // handle dyn function without body

43 return ['', {}];

44 }

45 const refs: Refs = {};

46 let slice = '';

47 for (let i = 0; i < slices.length; i++) {

48 slice += slices[i];

49 if (values[i] != null) {

50 const uid = `$fig-${count++}`;

51 refs[uid] = values[i];

52 slice += uid ?? '';

53 }

54 }

55 return [slice.trim(), refs];

56 }

57

58 /**

59 * Injects the values into the corresponding reference locations of the string.

60 *

61 * @param {string} slice - The string containing references

62 * @param {Refs} refs - The values mapped to there references

63 * @return {ReactElement[]} The string populated with the passed values

64 */

65 function feed(slice: string, refs: Refs): ReactElement[] {

66 const expr = /\$fig-\d+/g;

67 const elements: ReactElement[] = [];

68 let match = null;

69 let last = 0;

70 while ((match = expr.exec(slice)) !== null) {

71 const index = match.index;

72 const uid = match[0];

73 elements.push(slice.substring(last, index));

74 let value = refs[uid];

75 if (value instanceof Function) {

76 value = value();

77 }

78 elements.push(value);

79 last = index + uid.length;

80 }

81 elements.push(slice.substring(last));

82 return elements;

83 }

84

85 /**

86 * Converts a HTML node into a React element.

87 *

88 * @param {Node} node - The HTML node

89 * @param {Refs} refs - The values mapped to there references

90 * @return {ReactElement[]} The converted HTML node as React element

91 */

92 function render(node: Node, refs: Refs): ReactElement[] {

93 if (node.nodeType === Node.TEXT_NODE) {

94 const text = node as Text;

95 if (text.textContent == null) {

96 // ignore empty text nodes

97 return [];

98 }

99 return feed(text.textContent, refs);

100 }

101 if (node.nodeType === Node.COMMENT_NODE) {

102 // ignore comments

103 return [];

104 }

105 const element = node as HTMLElement;

106 const tag = element.tagName;

107 const props: Props = {};

108 // iterate over each attribute and add it to the props

109 for (const attribute of element.attributes) {

110 const key = attribute.name;

111 const slice = attribute.textContent;

112 if (slice == null) {

113 // ignore empty attribute values

114 continue;

115 }

116 let isDynamic = false;

117 for (const ref in refs) {

118 if (slice === ref) {

119 let match = null;

120 if ((match = /^on:(\w+)$/.exec(key)) !== null) {

121 // add event to props

122 const event = match[1];

123 props[`on${event.substring(0, 1).toUpperCase()}${event.substring(1)}`] = refs[ref];

124 }

125 else {

126 // add attribute to props

127 props[key] = refs[ref];

128 }

129 isDynamic = true;

130 break;

131 }

132 }

133 if (!isDynamic) {

134 if (key === 'style') {

135 // convert style attribute into object format

136 const styles: Record<string, string> = {};

137 for (const item of slice.split(';')) {

138 if (item.trim().length === 0) {

139 break;

140 }

141 let [key, value] = item.split(':');

142 key = key.trim();

143 let match = null;

144 if ((match = /-(\w)/.exec(key)) !== null) {

145 const char = match[1];

146 key = key.replace(`-${char}`, char.toUpperCase());

147 }

148 value = value.trim();

149 styles[key] = feed(value, refs).join('');

150 }

151 props[key] = styles;

152 }

153 else {

154 props[key] = feed(slice, refs).join('');

155 }

156 }

157 }

158 const children: ReactElement[] = [];

159 // recursively render all child nodes

160 (node.childNodes ?? []).forEach((child) => children.push(...render(child, refs)));

161 return [createElement(tag, props, ...children)];

162 }

163

164 /**

165 * Creates a dynamic component with its own state.

166 * Should be used if the component is statefull (contains hooks).

167 *

168 * @param {ReactFunction} element - The string containing references

169 * @param {Props} props - The component properties

170 * @param {ReactElement[]} children - The child elements

171 * @return {ReactElement} The created React component

172 */

173 function dyn(element: ReactFunction, props?: Props, children?: ReactElement[]): ReactElement {

174 return createElement(element, props, children);

175 }

176

177 return { html, dyn };

178}