so://phie

figure

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

blob: e64ec40990a10d1786923a1a536f61aa6f4a9840 (6592 bytes)[doc][blame][raw]

1import { Options, Props, ReactElement, ReactFunction, Refs, Slices, Values, Dict } 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 * Returns the a function for rendering HTML.

12 * @param {Dict} dict - The dictionary for resolving React components.

13 * @return {Function} The function for rendering HTML.

14 */

15 function dict(dict?: Dict): (slices: Slices, ...values: Values) => ReactElement[] {

16

17 /**

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

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

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

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

22 */

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

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

25 let dom;

26 try {

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

28 }

29 catch (error) {

30 console.error(error);

31 throw 'Invalid DOM structure!';

32 }

33 // collect all nodes from head and body

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

35 return nodes.map((node) => render(node, refs, dict ?? {}));

36 }

37

38 return html;

39 }

40

41 /**

42 * Creates a dynamic component with its own state.

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

44 *

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

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

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

48 * @return {ReactElement} The created React component

49 */

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

51 return createElement(element, props, children);

52 }

53

54 /**

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

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

57 * HTML string returned.

58 *

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

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

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

62 */

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

64 if (slices == null) {

65 // handle dyn function without body

66 return ['', {}];

67 }

68 const refs: Refs = {};

69 let slice = '';

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

71 slice += slices[i];

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

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

74 refs[uid] = values[i];

75 slice += uid ?? '';

76 }

77 }

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

79 }

80

81 /**

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

83 *

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

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

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

87 */

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

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

90 const elements: ReactElement[] = [];

91 let match = null;

92 let last = 0;

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

94 const index = match.index;

95 const uid = match[0];

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

97 let value = refs[uid];

98 if (value instanceof Function) {

99 value = value();

100 }

101 elements.push(value);

102 last = index + uid.length;

103 }

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

105 return elements;

106 }

107

108 /**

109 * Converts a HTML node into a React element.

110 *

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

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

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

114 */

115 function render(node: Node, refs: Refs, dict: Dict): ReactElement[] {

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

117 const text = node as Text;

118 if (text.textContent == null) {

119 // ignore empty text nodes

120 return [];

121 }

122 return feed(text.textContent, refs);

123 }

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

125 // ignore comments

126 return [];

127 }

128 const element = node as HTMLElement;

129 const tag = element.tagName.toLowerCase();

130 const props: Props = {};

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

132 for (const attribute of element.attributes) {

133 const key = attribute.name;

134 const slice = attribute.textContent;

135 if (slice == null) {

136 // ignore empty attribute values

137 continue;

138 }

139 let isDynamic = false;

140 for (const ref in refs) {

141 if (slice === ref) {

142 let match = null;

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

144 // add event to props

145 const event = match[1];

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

147 }

148 else {

149 // add attribute to props

150 props[key] = refs[ref];

151 }

152 isDynamic = true;

153 break;

154 }

155 }

156 if (!isDynamic) {

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

158 // convert style attribute into object format

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

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

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

162 break;

163 }

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

165 key = key.trim();

166 let match = null;

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

168 const char = match[1];

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

170 }

171 value = value.trim();

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

173 }

174 props[key] = styles;

175 }

176 else {

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

178 }

179 }

180 }

181 const children: ReactElement[] = [];

182 // recursively render all child nodes

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

184 const domain = tag.split(':');

185 // look up tag name in dictionary

186 // deno-lint-ignore no-explicit-any

187 const component: ReactFunction | undefined = domain.reduce((dict: any, level) => {

188 return dict?.[level];

189 }, dict);

190 // use React component or tag name

191 return [createElement(component ?? tag, props, ...children)];

192 }

193

194 return { dict, dyn };

195}