链接

    1. type AST = {
    2. openingElement: {
    3. name: string;
    4. };
    5. closingElement: {
    6. name: string;
    7. };
    8. children: (string | AST)[];
    9. };
    10. type MyElement = {
    11. type: string;
    12. props: {
    13. [key: string]: string | MyNode | MyNode[];
    14. children: MyNode | MyNode[];
    15. };
    16. };
    17. type MyNode = MyElement | string;
    18. function parse(code: string): AST {
    19. // 1. extract open tag
    20. let openTag = "";
    21. let i = 0;
    22. for (; i < code.length; i++) {
    23. if (code[i] === " ") continue;
    24. openTag = openTag + code[i];
    25. if (code[i] === ">") break;
    26. }
    27. // 2. extract close tag
    28. let closeTag = "";
    29. let j = code.length - 1;
    30. for (; j >= 0; j--) {
    31. if (code[j] === " ") continue;
    32. closeTag = code[j] + closeTag;
    33. if (code[j] === "<") break;
    34. }
    35. // handle throw for <a>></a> or <a><</a> per requirement
    36. handleSpecificError(code, i, j);
    37. // 3. validate tags match
    38. if (!matches(openTag, closeTag)) {
    39. throw new Error(`don't match`);
    40. }
    41. // 4. extract children
    42. const childMarkup = code.slice(i + 1, j);
    43. const children = getChildrenFromHTML(childMarkup).map((child) => {
    44. // child is a html string, need to parse it recursively
    45. if (child.match(/^<.*>$/)) {
    46. return parse(child);
    47. }
    48. // child is a text string, return directly
    49. return child;
    50. });
    51. return {
    52. openingElement: {
    53. name: extractTagName(openTag),
    54. },
    55. closingElement: {
    56. name: extractTagName(closeTag),
    57. },
    58. children,
    59. };
    60. }
    61. function generate(ast: AST): MyElement {
    62. let type = ast.openingElement.name;
    63. // handle functional component
    64. if (ast.openingElement.name !== ast.openingElement.name.toLowerCase()) {
    65. type = eval(ast.openingElement.name); // gets the Functional Component definition from upper scope
    66. }
    67. return {
    68. type,
    69. props: {
    70. children: ast.children.map((child) => {
    71. if (typeof child === "string") {
    72. // handle string child
    73. return child;
    74. }
    75. // handle ast child recursively
    76. return generate(child);
    77. }),
    78. },
    79. };
    80. }
    81. /* --------------------------------------------------------------------------- */
    82. /* --------------------------------- Helpers --------------------------------- */
    83. /* --------------------------------------------------------------------------- */
    84. function extractTagName(tag: string): string {
    85. return tag.replace(/[^a-zA-Z0-9]/gi, "");
    86. }
    87. // returns whether open tag matches close tag
    88. function matches(openTag: string, closeTag: string): boolean {
    89. return (
    90. !openTag.includes("/") &&
    91. closeTag.length - openTag.length === 1 &&
    92. extractTagName(openTag) === extractTagName(closeTag)
    93. );
    94. }
    95. // handle throw for <a>></a> or <a><</a> per requirement
    96. function handleSpecificError(
    97. code: string,
    98. openTagEnd: number,
    99. closeTagStart: number
    100. ): void {
    101. if (code[openTagEnd + 1] !== ">" && code[closeTagStart - 1] !== "<") return;
    102. throw new Error();
    103. }
    104. // returns whether the cursor is pointing at the start of an open tag
    105. function isOpenTag(html: string, cursor: number): boolean {
    106. return Boolean(html[cursor] === "<" && html[cursor + 1].match(/[a-zA-Z0-9]/));
    107. }
    108. // returns whether the cursor is point at the start of an close tag
    109. function isCloseTag(html: string, cursor: number): boolean {
    110. return Boolean(
    111. html[cursor] === "<" &&
    112. html[cursor + 1] === "/" &&
    113. html[cursor + 2].match(/[a-zA-Z0-9]/)
    114. );
    115. }
    116. // returns the full tag(open or close) that starts at a given index, along with the ending index
    117. function getFullTag(
    118. html: string,
    119. statIndex: number
    120. ): {
    121. tag: string | null;
    122. endIndex: number;
    123. } {
    124. let tag = "";
    125. while (statIndex < html.length) {
    126. const char = html[statIndex];
    127. tag += char;
    128. if (char === ">") return { tag, endIndex: statIndex };
    129. statIndex++;
    130. }
    131. return {
    132. tag: null,
    133. endIndex: statIndex,
    134. };
    135. }
    136. // receives an html string, seperated it into an array of children text strings and html strings
    137. // this function only goes one level deep -
    138. // "ab<div>ab<p>c</p></div>cd<span>123</span>" -> [ "ab", "<div>ab<p>c</p></div>", "cd", "<span>123</span>" ]
    139. function getChildrenFromHTML(html: string): string[] {
    140. const children = []; // html string | text string
    141. let cursor = 0;
    142. let isTagStarted = false;
    143. let openTagName = "";
    144. let repeatedOpenTagCount = 0; // handle situations like <div>ab<div>c</div>de</div>, in this case repeatedOpenTagCount = 1
    145. let runningTag = "";
    146. let runningText = "";
    147. while (cursor < html.length) {
    148. // handle runningTag closes
    149. if (
    150. isTagStarted &&
    151. isCloseTag(html, cursor) &&
    152. extractTagName(getFullTag(html, cursor).tag) === openTagName
    153. ) {
    154. if (repeatedOpenTagCount === 0) {
    155. // handle last string child
    156. if (runningText.length) {
    157. children.push(runningText);
    158. runningText = "";
    159. }
    160. isTagStarted = false;
    161. openTagName = "";
    162. runningTag += getFullTag(html, cursor).tag;
    163. children.push(runningTag);
    164. cursor = getFullTag(html, cursor).endIndex + 1;
    165. runningTag = "";
    166. } else {
    167. repeatedOpenTagCount--;
    168. runningTag += html[cursor];
    169. cursor++;
    170. }
    171. continue;
    172. }
    173. // handle runningTag opens
    174. if (isOpenTag(html, cursor)) {
    175. if (!isTagStarted) {
    176. // handle last string child
    177. if (runningText.length) {
    178. children.push(runningText);
    179. runningText = "";
    180. }
    181. openTagName = extractTagName(getFullTag(html, cursor).tag); // set openTagName
    182. isTagStarted = true;
    183. runningTag += getFullTag(html, cursor).tag;
    184. cursor = getFullTag(html, cursor).endIndex + 1;
    185. } else {
    186. const tagName = extractTagName(getFullTag(html, cursor).tag);
    187. if (tagName === openTagName) repeatedOpenTagCount++;
    188. runningTag += html[cursor];
    189. cursor++;
    190. }
    191. continue;
    192. }
    193. // handle adding char to runningTag
    194. if (isTagStarted) {
    195. runningTag += html[cursor];
    196. cursor++;
    197. } else {
    198. // handle adding char to runningText
    199. runningText += html[cursor];
    200. cursor++;
    201. }
    202. }
    203. if (runningText.length) children.push(runningText); // handle trailing last string child
    204. return children;
    205. }
    206. /* --------------------------------------------------------------------------- */
    207. /* ------------------------------- End Helpers ------------------------------- */
    208. /* --------------------------------------------------------------------------- */
    209. export {};