so://phie

figure

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

blob: d33c712231b4c1431a066a40b9d89df97b24bb70 (5611 bytes)[doc][blame][raw]

1import {

2 Dict,

3 Figure,

4 Props,

5 ReactElement,

6 HtmlFunction,

7 ReactFunction,

8 CreateFunction,

9 Refs,

10 Slices,

11 Values,

12} from './types.ts';

13

14/**

15 * Initializes the figure utility.

16 * @param {CreateFunction} create - The React createElement function.

17 * @return {Figure} The util functions collected in an object.

18 */

19export default function figure(create: CreateFunction): Figure {

20

21 // the parser for interpreting HTML

22 const parser = new DOMParser();

23 // the counter for creating unique references

24 let count = 0;

25

26 /**

27 * Returns the a function for rendering HTML.

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

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

30 */

31 function dict(dict?: Dict): HtmlFunction {

32

33 /**

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

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

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

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

38 */

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

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

41 try {

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

43 // collect all nodes from head and body

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

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

46 }

47 catch (error) {

48 console.error(error);

49 throw 'Invalid DOM structure!';

50 }

51 }

52

53 return html;

54 }

55

56 /**

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

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

59 * HTML string returned.

60 *

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

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

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

64 */

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

66 if (slices == null) {

67 // handle dyn function without body

68 return ['', {}];

69 }

70 const refs: Refs = {};

71 let slice = '';

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

73 slice += slices[i];

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

75 // create unique reference

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

77 refs[uid] = values[i];

78 slice += uid ?? '';

79 }

80 }

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

82 }

83

84 /**

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

86 *

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

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

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

90 */

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

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

93 const elements: ReactElement[] = [];

94 let match: RegExpExecArray | null = null;

95 let last = 0;

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

97 const index = match.index;

98 const uid = match[0];

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

100 // ignore empty strings

101 if (before.length > 0) {

102 elements.push(before);

103 }

104 const value = refs[uid];

105 // ignore empty values

106 if (value) {

107 elements.push(value);

108 }

109 last = index + uid.length;

110 }

111 const after = slice.substring(last);

112 // ignore empty strings

113 if (after.length > 0) {

114 elements.push(after);

115 }

116 return elements;

117 }

118

119 /**

120 * Converts a HTML node into a React element.

121 *

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

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

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

125 */

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

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

128 const text = node as Text;

129 if (text.textContent == null) {

130 // ignore empty text nodes

131 return [];

132 }

133 return feed(text.textContent, refs);

134 }

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

136 // ignore comments

137 return [];

138 }

139 const element = node as HTMLElement;

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

141 const props: Props = {};

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

143 for (const attribute of element.attributes) {

144 const key = attribute.name;

145 const slice = attribute.textContent;

146 if (slice == null) {

147 // ignore empty attribute values

148 continue;

149 }

150 const values = feed(slice, refs);

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

152 let attr = key;

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

154 if (match) {

155 // camel case attribute name

156 const [, pre, name] = match;

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

158 }

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

160 }

161 const children: ReactElement[] = [];

162 // recursively render all child nodes

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

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

165 // look up tag name in dictionary

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

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

168 return dict && dict[level] ? dict[level] : null;

169 }, dict);

170 // use React component or tag name

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

172 }

173

174 return { dict, dyn: create };

175}