so://phie

figure

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

blob: c6c8850be58f5acc3278e509ae9d282ebdfe170a (6446 bytes)[doc][blame][raw]

1// @ts-check

2

3/**

4 * @typedef {Object} ReactElement

5 * @typedef {function(Props=): ReactElement} ReactFunction

6 * @typedef {function(ReactFunction | string, Props?=, ...Object): ReactElement} ReactCreateFunction

7 *

8 * @typedef {TemplateStringsArray} Slices

9 * @typedef {any} Value

10 * @typedef {{[key: string]: any}} Refs

11 * @typedef {any} Props

12 * @typedef {{[key: string]: ReactFunction | Dict}} Dict

13 * @typedef {{ dict: DictFunction, dyn: DynFunction }} Figure

14 *

15 * @typedef {function(Dict=): HTMLFunction} DictFunction

16 * @typedef {function(Slices, ...Value): ReactElement[]} HTMLFunction

17 * @typedef {ReactCreateFunction} DynFunction

18 */

19

20/**

21 * Initializes the figure framework.

22 *

23 * @param {ReactCreateFunction} create The React createElement function

24 * @returns {Figure} The util functions collected in an object

25 */

26export default function figure(create) {

27

28 // the parser for interpreting HTML

29 const parser = new DOMParser();

30 // the counter for creating unique references

31 let count = 0;

32

33 /**

34 * Returns the a function for rendering HTML.

35 *

36 * @param {Dict} [dict] The dictionary for resolving React components

37 * @returns {HTMLFunction} The function for rendering HTML

38 */

39 function dict(dict) {

40

41 /**

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

43 *

44 * @param {Slices} slices The template literal slices

45 * @param {...Value} values The template literal values

46 * @returns {ReactElement[]} The converted HTML as React elements

47 */

48 function html(slices, ...values) {

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

50 try {

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

52 // collect all nodes from head and body

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

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

55 }

56 catch (error) {

57 console.error(error);

58 throw 'Invalid DOM structure!';

59 }

60 }

61

62 return html;

63 }

64

65 /**

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

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

68 * HTML string returned.

69 *

70 * @param {Slices} slices The template literal slices

71 * @param {Value[]} values The template literal values

72 * @returns {[string, Refs]} The joined HTML string and the values mapped to there references

73 */

74 function compose(slices, values) {

75 if (slices == null) {

76 // handle dyn function without body

77 return ['', {}];

78 }

79 const refs = /** @type {Refs} */ ({});

80 let slice = '';

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

82 slice += slices[i];

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

84 // create unique reference

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

86 refs[uid] = values[i];

87 slice += uid ?? '';

88 }

89 }

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

91 }

92

93 /**

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

95 *

96 * @param {string} slice The string containing references

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

98 * @returns {ReactElement[]} The string populated with the passed values

99 */

100 function feed(slice, refs) {

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

102 const elements = /** @type {ReactElement[]} */ ([]);

103 let match = null;

104 let last = 0;

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

106 const index = match.index;

107 const uid = match[0];

108 const before = slice.substring(last, index);

109 // ignore empty strings

110 if (before.length > 0) {

111 elements.push(before);

112 }

113 const value = refs[uid];

114 // ignore empty values

115 if (value !== undefined && value !== null) {

116 elements.push(value);

117 }

118 last = index + uid.length;

119 }

120 const after = slice.substring(last);

121 // ignore empty strings

122 if (after.length > 0) {

123 elements.push(after);

124 }

125 return elements;

126 }

127

128 /**

129 * Converts a HTML node into a React element.

130 *

131 * @param {Node} node The HTML node

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

133 * @param {Dict} dict The dictionary for resolving React components

134 * @returns {ReactElement[]} The converted HTML node as React element

135 */

136 function render(node, refs, dict) {

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

138 const text = /** @type {Text} */ (node);

139 if (text.textContent == null) {

140 // ignore empty text nodes

141 return [];

142 }

143 return feed(text.textContent, refs);

144 }

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

146 // ignore comments

147 return [];

148 }

149 const element = /** @type {HTMLElement} */ (node);

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

151 const props = /** @type {Props} */ ({});

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

153 for (const attribute of element.attributes) {

154 const key = attribute.name;

155 const slice = attribute.textContent;

156 if (slice == null) {

157 // ignore empty attribute values

158 continue;

159 }

160 const values = feed(slice, refs);

161 const value = values.length == 1 ? values[0] : values;

162 let attr = key;

163 const match = /^(\w+):(\w+)$/.exec(attr);

164 if (match) {

165 // camel case attribute name

166 const [, pre, name] = match;

167 attr = `${pre}${name.substring(0, 1).toUpperCase()}${name.substring(1)}`;

168 }

169 props[attr] = value instanceof Array ? value.join('') : value;

170 }

171 const children = /** @type {ReactElement[]} */ ([]);

172 // recursively render all child nodes

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

174 const domains = tag.split(':');

175 let component = /** @type {ReactFunction | null} */ (null);

176 let layer = /** @type {Dict | ReactFunction | null} */ (dict);

177 // look up tag name in dictionary

178 for (const domain of domains) {

179 if (layer && typeof layer === 'object' && layer[domain]) {

180 // traverse sublayer

181 layer = /** @type {Dict} */ (layer[domain]);

182 continue;

183 }

184 // domain is not in layer

185 layer = null;

186 break;

187 }

188 if (layer) {

189 // found component for tag

190 component = /** @type {ReactFunction} */ (layer);

191 }

192 // use React component or tag name

193 return [create(component ?? tag, props, ...children)];

194 }

195

196 return { dict, dyn: create };

197}