so://phie

figure

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

blob: 5f63656007a855cb2320fa8c37909c2d952a3820 (6056 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 * Joins the template literal slices together and replaces the values with references.

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

44 * HTML string returned.

45 *

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

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

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

49 */

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

51 if (slices == null) {

52 // handle dyn function without body

53 return ['', {}];

54 }

55 const refs: Refs = {};

56 let slice = '';

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

58 slice += slices[i];

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

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

61 refs[uid] = values[i];

62 slice += uid ?? '';

63 }

64 }

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

66 }

67

68 /**

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

70 *

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

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

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

74 */

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

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

77 const elements: ReactElement[] = [];

78 let match = null;

79 let last = 0;

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

81 const index = match.index;

82 const uid = match[0];

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

84 let value = refs[uid];

85 if (value instanceof Function) {

86 value = value();

87 }

88 elements.push(value);

89 last = index + uid.length;

90 }

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

92 return elements;

93 }

94

95 /**

96 * Converts a HTML node into a React element.

97 *

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

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

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

101 */

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

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

104 const text = node as Text;

105 if (text.textContent == null) {

106 // ignore empty text nodes

107 return [];

108 }

109 return feed(text.textContent, refs);

110 }

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

112 // ignore comments

113 return [];

114 }

115 const element = node as HTMLElement;

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

117 const props: Props = {};

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

119 for (const attribute of element.attributes) {

120 const key = attribute.name;

121 const slice = attribute.textContent;

122 if (slice == null) {

123 // ignore empty attribute values

124 continue;

125 }

126 let isDynamic = false;

127 for (const ref in refs) {

128 if (slice === ref) {

129 let match = null;

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

131 // add event to props

132 const event = match[1];

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

134 }

135 else {

136 // add attribute to props

137 props[key] = refs[ref];

138 }

139 isDynamic = true;

140 break;

141 }

142 }

143 if (!isDynamic) {

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

145 // convert style attribute into object format

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

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

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

149 break;

150 }

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

152 key = key.trim();

153 let match = null;

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

155 const char = match[1];

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

157 }

158 value = value.trim();

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

160 }

161 props[key] = styles;

162 }

163 else {

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

165 }

166 }

167 }

168 const children: ReactElement[] = [];

169 // recursively render all child nodes

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

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

172 // look up tag name in dictionary

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

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

175 return dict?.[level];

176 }, dict);

177 // use React component or tag name

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

179 }

180

181 return { dict };

182}