|
| 1 | +import React from 'react'; |
| 2 | + |
| 3 | +/* Convert a React component's children to a string. Each "tag" must be |
| 4 | + * converted to a numbered tag in the order they were encountered in and all |
| 5 | + * props must be stripped. The props must be preserved in the second return |
| 6 | + * value so that they can be reinserted again later. |
| 7 | + * |
| 8 | + * element = <><one two="three">four<five six="seven" /></one></>; |
| 9 | + * const [result, propsContainer] = toStr(element.props.children); |
| 10 | + * console.log(result.join('')); |
| 11 | + * // <<< '<1>four<2/></1>' |
| 12 | + * console.log(propsContainer); |
| 13 | + * // <<< [['one', {two: 'three'}], ['five', {six: 'seven'}]] |
| 14 | + * |
| 15 | + * The second argument and third return value are there because of how |
| 16 | + * recursion works. For high-level invocation you won't have to worry about |
| 17 | + * them. |
| 18 | + * */ |
| 19 | +export function toStr(children, counter = 0) { |
| 20 | + if (!children) { return [[], [], 0]; } |
| 21 | + let actualChildren = children; |
| 22 | + if (!Array.isArray(children)) { |
| 23 | + actualChildren = [children]; |
| 24 | + } |
| 25 | + |
| 26 | + // Return values |
| 27 | + let result = []; |
| 28 | + let propsContainer = []; |
| 29 | + |
| 30 | + let actualCounter = counter; |
| 31 | + for (let i = 0; i < actualChildren.length; i += 1) { |
| 32 | + const child = actualChildren[i]; |
| 33 | + if (React.isValidElement(child)) { |
| 34 | + actualCounter += 1; |
| 35 | + |
| 36 | + // Each entry in propsContainer matches one matched react element. So for |
| 37 | + // the element replaced with '<4>', the relevant props will be |
| 38 | + // `propsContainer[3]` (4 - 1) |
| 39 | + const props = [ |
| 40 | + child.type, |
| 41 | + { ...child.props }, // Do this so that delete can work later |
| 42 | + ]; |
| 43 | + delete props[1].children; |
| 44 | + propsContainer.push(props); |
| 45 | + |
| 46 | + if (child.props.children) { |
| 47 | + // child has children, recursively run 'toStr' on them |
| 48 | + const [newResult, newProps, newCounter] = toStr( |
| 49 | + child.props.children, |
| 50 | + actualCounter, |
| 51 | + ); |
| 52 | + result.push(`<${actualCounter}>`); // <4> |
| 53 | + result = result.concat(newResult); // <4>... |
| 54 | + result.push(`</${actualCounter}>`); // <4>...</4> |
| 55 | + // Extend propsContainer with what was found during the recursion |
| 56 | + propsContainer = propsContainer.concat(newProps); |
| 57 | + // Take numbered tags that were found during the recursion into account |
| 58 | + actualCounter = newCounter; |
| 59 | + } else { |
| 60 | + // child has no children of its own, replace with something like '<4/>' |
| 61 | + result.push(`<${actualCounter}/>`); |
| 62 | + } |
| 63 | + } else { |
| 64 | + // Child is not a React element, append as-is |
| 65 | + /* eslint-disable no-lonely-if */ |
| 66 | + if (typeof child === 'string' || child instanceof String) { |
| 67 | + const chunk = child.trim(); |
| 68 | + if (chunk) { result.push(chunk); } |
| 69 | + } else { |
| 70 | + result.push(child); |
| 71 | + } |
| 72 | + /* eslint-enable */ |
| 73 | + } |
| 74 | + } |
| 75 | + |
| 76 | + return [result, propsContainer, actualCounter]; |
| 77 | +} |
| 78 | + |
| 79 | +/* Convert a string that was generated from 'toStr', or its translation, back |
| 80 | + * to a React element, combining it with the props that were extracted during |
| 81 | + * 'toStr'. |
| 82 | + * |
| 83 | + * toElement( |
| 84 | + * 'one<1>five<2/></1>', |
| 85 | + * [['two', {three: 'four'}], ['six', {seven: 'eight'}]], |
| 86 | + * ); |
| 87 | + * // The browser will render the equivalent of |
| 88 | + * // one<two three="four">five<six seven="eight" /></two> |
| 89 | + * */ |
| 90 | +export function toElement(translation, propsContainer) { |
| 91 | + const regexp = /<(\d+)(\/?)>/; // Find opening or single tags |
| 92 | + const result = []; |
| 93 | + |
| 94 | + let lastEnd = 0; // Last position in 'translation' we have "consumed" so far |
| 95 | + let lastKey = 0; |
| 96 | + |
| 97 | + for (;;) { |
| 98 | + const match = regexp.exec(translation.substring(lastEnd)); |
| 99 | + if (match === null) { break; } // We've reached the end |
| 100 | + |
| 101 | + // Copy until match |
| 102 | + const matchIndex = lastEnd + match.index; |
| 103 | + const chunk = translation.substring(lastEnd, matchIndex); |
| 104 | + if (chunk) { result.push(chunk); } |
| 105 | + |
| 106 | + const [openingTag, numberString, rightSlash] = match; |
| 107 | + const number = parseInt(numberString, 10); |
| 108 | + const [type, props] = propsContainer[number - 1]; // Find relevant props |
| 109 | + if (rightSlash) { |
| 110 | + // Single tag, copy props and don't include children in the React element |
| 111 | + result.push(React.createElement(type, { ...props, key: lastKey })); |
| 112 | + lastEnd += openingTag.length; |
| 113 | + } else { |
| 114 | + // Opening tag, find the closing tag which is guaranteed to be there and |
| 115 | + // to be unique |
| 116 | + const endingTag = `</${number}>`; |
| 117 | + const endingTagPos = translation.indexOf(endingTag); |
| 118 | + // Recursively convert contents to React elements |
| 119 | + const newResult = toElement( |
| 120 | + translation.substring(matchIndex + openingTag.length, endingTagPos), |
| 121 | + propsContainer, |
| 122 | + ); |
| 123 | + // Copy props and include recursion result as children |
| 124 | + result.push(React.createElement( |
| 125 | + type, |
| 126 | + { ...props, key: lastKey }, |
| 127 | + ...newResult, |
| 128 | + )); |
| 129 | + lastEnd = endingTagPos + endingTag.length; |
| 130 | + } |
| 131 | + lastKey += 1; |
| 132 | + } |
| 133 | + |
| 134 | + // Copy rest of 'translation' |
| 135 | + const chunk = translation.substring(lastEnd, translation.length); |
| 136 | + if (chunk) { result.push(chunk); } |
| 137 | + |
| 138 | + return result; |
| 139 | +} |
0 commit comments