File size: 3,499 Bytes
c20f20c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import type {
  Token,
  HTMLNode,
  TagToken,
  NormalElement,
  TagEndToken,
  AttributeToken,
  TextToken,
} from './types';
import { closingTags, closingTagAncestorBreakers, voidTags } from './tags';

interface StackItem {
  tagName: string | null;
  children: HTMLNode[];
}

interface State {
  stack: StackItem[];
  cursor: number;
  tokens: Token[];
}

export const parser = (tokens: Token[]) => {
  const root: StackItem = { tagName: null, children: [] };
  const state: State = { tokens, cursor: 0, stack: [root] };
  parse(state);
  return root.children;
};

export const hasTerminalParent = (tagName: string, stack: StackItem[]) => {
  const tagParents = closingTagAncestorBreakers[tagName];
  if (tagParents) {
    let currentIndex = stack.length - 1;
    while (currentIndex >= 0) {
      const parentTagName = stack[currentIndex].tagName;
      if (parentTagName === tagName) break;
      if (parentTagName && tagParents.includes(parentTagName)) return true;
      currentIndex--;
    }
  }
  return false;
};

export const rewindStack = (stack: StackItem[], newLength: number) => {
  stack.splice(newLength);
};

export const parse = (state: State) => {
  const { stack, tokens } = state;
  let { cursor } = state;
  let nodes = stack[stack.length - 1].children;
  const len = tokens.length;

  while (cursor < len) {
    const token = tokens[cursor];
    if (token.type !== 'tag-start') {
      nodes.push(token as TextToken);
      cursor++;
      continue;
    }

    const tagToken = tokens[++cursor] as TagToken;
    cursor++;
    const tagName = tagToken.content.toLowerCase();
    if (token.close) {
      let index = stack.length;
      let shouldRewind = false;
      while (--index > -1) {
        if (stack[index].tagName === tagName) {
          shouldRewind = true;
          break;
        }
      }
      while (cursor < len) {
        if (tokens[cursor].type !== 'tag-end') break;
        cursor++;
      }
      if (shouldRewind) {
        rewindStack(stack, index);
        break;
      } else continue;
    }

    const isClosingTag = closingTags.includes(tagName);
    let shouldRewindToAutoClose = isClosingTag;
    if (shouldRewindToAutoClose) {
      shouldRewindToAutoClose = !hasTerminalParent(tagName, stack);
    }

    if (shouldRewindToAutoClose) {
      let currentIndex = stack.length - 1;
      while (currentIndex > 0) {
        if (tagName === stack[currentIndex].tagName) {
          rewindStack(stack, currentIndex);
          const previousIndex = currentIndex - 1;
          nodes = stack[previousIndex].children;
          break;
        }
        currentIndex = currentIndex - 1;
      }
    }

    const attributes = [];
    let tagEndToken: TagEndToken | undefined;
    while (cursor < len) {
      const _token = tokens[cursor];
      if (_token.type === 'tag-end') {
        tagEndToken = _token;
        break;
      }
      attributes.push((_token as AttributeToken).content);
      cursor++;
    }

    if (!tagEndToken) break;

    cursor++;
    const children: HTMLNode[] = [];
    const elementNode: NormalElement = {
      type: 'element',
      tagName: tagToken.content,
      attributes,
      children,
    };
    nodes.push(elementNode);

    const hasChildren = !(tagEndToken.close || voidTags.includes(tagName));
    if (hasChildren) {
      stack.push({ tagName, children });
      const innerState = { tokens, cursor, stack };
      parse(innerState);
      cursor = innerState.cursor;
    }
  }
  state.cursor = cursor;
};