parser.ts 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. import type {
  2. Token,
  3. HTMLNode,
  4. TagToken,
  5. NormalElement,
  6. TagEndToken,
  7. AttributeToken,
  8. TextToken,
  9. } from './types';
  10. import { closingTags, closingTagAncestorBreakers, voidTags } from './tags';
  11. interface StackItem {
  12. tagName: string | null;
  13. children: HTMLNode[];
  14. }
  15. interface State {
  16. stack: StackItem[];
  17. cursor: number;
  18. tokens: Token[];
  19. }
  20. export const parser = (tokens: Token[]) => {
  21. const root: StackItem = { tagName: null, children: [] };
  22. const state: State = { tokens, cursor: 0, stack: [root] };
  23. parse(state);
  24. return root.children;
  25. };
  26. export const hasTerminalParent = (tagName: string, stack: StackItem[]) => {
  27. const tagParents = closingTagAncestorBreakers[tagName];
  28. if (tagParents) {
  29. let currentIndex = stack.length - 1;
  30. while (currentIndex >= 0) {
  31. const parentTagName = stack[currentIndex].tagName;
  32. if (parentTagName === tagName) break;
  33. if (parentTagName && tagParents.includes(parentTagName)) return true;
  34. currentIndex--;
  35. }
  36. }
  37. return false;
  38. };
  39. export const rewindStack = (stack: StackItem[], newLength: number) => {
  40. stack.splice(newLength);
  41. };
  42. export const parse = (state: State) => {
  43. const { stack, tokens } = state;
  44. let { cursor } = state;
  45. let nodes = stack[stack.length - 1].children;
  46. const len = tokens.length;
  47. while (cursor < len) {
  48. const token = tokens[cursor];
  49. if (token.type !== 'tag-start') {
  50. nodes.push(token as TextToken);
  51. cursor++;
  52. continue;
  53. }
  54. const tagToken = tokens[++cursor] as TagToken;
  55. cursor++;
  56. const tagName = tagToken.content.toLowerCase();
  57. if (token.close) {
  58. let index = stack.length;
  59. let shouldRewind = false;
  60. while (--index > -1) {
  61. if (stack[index].tagName === tagName) {
  62. shouldRewind = true;
  63. break;
  64. }
  65. }
  66. while (cursor < len) {
  67. if (tokens[cursor].type !== 'tag-end') break;
  68. cursor++;
  69. }
  70. if (shouldRewind) {
  71. rewindStack(stack, index);
  72. break;
  73. } else continue;
  74. }
  75. const isClosingTag = closingTags.includes(tagName);
  76. let shouldRewindToAutoClose = isClosingTag;
  77. if (shouldRewindToAutoClose) {
  78. shouldRewindToAutoClose = !hasTerminalParent(tagName, stack);
  79. }
  80. if (shouldRewindToAutoClose) {
  81. let currentIndex = stack.length - 1;
  82. while (currentIndex > 0) {
  83. if (tagName === stack[currentIndex].tagName) {
  84. rewindStack(stack, currentIndex);
  85. const previousIndex = currentIndex - 1;
  86. nodes = stack[previousIndex].children;
  87. break;
  88. }
  89. currentIndex = currentIndex - 1;
  90. }
  91. }
  92. const attributes = [];
  93. let tagEndToken: TagEndToken | undefined;
  94. while (cursor < len) {
  95. const _token = tokens[cursor];
  96. if (_token.type === 'tag-end') {
  97. tagEndToken = _token;
  98. break;
  99. }
  100. attributes.push((_token as AttributeToken).content);
  101. cursor++;
  102. }
  103. if (!tagEndToken) break;
  104. cursor++;
  105. const children: HTMLNode[] = [];
  106. const elementNode: NormalElement = {
  107. type: 'element',
  108. tagName: tagToken.content,
  109. attributes,
  110. children,
  111. };
  112. nodes.push(elementNode);
  113. const hasChildren = !(tagEndToken.close || voidTags.includes(tagName));
  114. if (hasChildren) {
  115. stack.push({ tagName, children });
  116. const innerState = { tokens, cursor, stack };
  117. parse(innerState);
  118. cursor = innerState.cursor;
  119. }
  120. }
  121. state.cursor = cursor;
  122. };