lexer.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. import type { Token } from './types';
  2. import { childlessTags } from './tags';
  3. interface State {
  4. str: string;
  5. position: number;
  6. tokens: Token[];
  7. }
  8. const jumpPosition = (state: State, end: number) => {
  9. const len = end - state.position;
  10. movePositopn(state, len);
  11. };
  12. const movePositopn = (state: State, len: number) => {
  13. state.position = state.position + len;
  14. };
  15. const findTextEnd = (str: string, index: number) => {
  16. const isEnd = false;
  17. while (!isEnd) {
  18. const textEnd = str.indexOf('<', index);
  19. if (textEnd === -1) {
  20. return textEnd;
  21. }
  22. const char = str.charAt(textEnd + 1);
  23. if (char === '/' || char === '!' || /[A-Za-z0-9]/.test(char)) {
  24. return textEnd;
  25. }
  26. index = textEnd + 1;
  27. }
  28. return -1;
  29. };
  30. const lexText = (state: State) => {
  31. const { str } = state;
  32. let textEnd = findTextEnd(str, state.position);
  33. if (textEnd === state.position) return;
  34. if (textEnd === -1) {
  35. textEnd = str.length;
  36. }
  37. const content = str.slice(state.position, textEnd);
  38. jumpPosition(state, textEnd);
  39. state.tokens.push({
  40. type: 'text',
  41. content,
  42. });
  43. };
  44. const lexComment = (state: State) => {
  45. const { str } = state;
  46. movePositopn(state, 4);
  47. let contentEnd = str.indexOf('-->', state.position);
  48. let commentEnd = contentEnd + 3;
  49. if (contentEnd === -1) {
  50. contentEnd = commentEnd = str.length;
  51. }
  52. const content = str.slice(state.position, contentEnd);
  53. jumpPosition(state, commentEnd);
  54. state.tokens.push({
  55. type: 'comment',
  56. content,
  57. });
  58. };
  59. const lexTagName = (state: State) => {
  60. const { str } = state;
  61. const len = str.length;
  62. let start = state.position;
  63. while (start < len) {
  64. const char = str.charAt(start);
  65. const isTagChar = !(/\s/.test(char) || char === '/' || char === '>');
  66. if (isTagChar) break;
  67. start++;
  68. }
  69. let end = start + 1;
  70. while (end < len) {
  71. const char = str.charAt(end);
  72. const isTagChar = !(/\s/.test(char) || char === '/' || char === '>');
  73. if (!isTagChar) break;
  74. end++;
  75. }
  76. jumpPosition(state, end);
  77. const tagName = str.slice(start, end);
  78. state.tokens.push({
  79. type: 'tag',
  80. content: tagName,
  81. });
  82. return tagName;
  83. };
  84. const lexTagAttributes = (state: State) => {
  85. const { str, tokens } = state;
  86. let cursor = state.position;
  87. let quote = null;
  88. let wordBegin = cursor;
  89. const words = [];
  90. const len = str.length;
  91. while (cursor < len) {
  92. const char = str.charAt(cursor);
  93. if (quote) {
  94. const isQuoteEnd = char === quote;
  95. if (isQuoteEnd) quote = null;
  96. cursor++;
  97. continue;
  98. }
  99. const isTagEnd = char === '/' || char === '>';
  100. if (isTagEnd) {
  101. if (cursor !== wordBegin) words.push(str.slice(wordBegin, cursor));
  102. break;
  103. }
  104. const isWordEnd = /\s/.test(char);
  105. if (isWordEnd) {
  106. if (cursor !== wordBegin) words.push(str.slice(wordBegin, cursor));
  107. wordBegin = cursor + 1;
  108. cursor++;
  109. continue;
  110. }
  111. const isQuoteStart = char === "'" || char === '"';
  112. if (isQuoteStart) {
  113. quote = char;
  114. cursor++;
  115. continue;
  116. }
  117. cursor++;
  118. }
  119. jumpPosition(state, cursor);
  120. const type = 'attribute';
  121. for (let i = 0; i < words.length; i++) {
  122. const word = words[i];
  123. const isNotPair = word.indexOf('=') === -1;
  124. if (isNotPair) {
  125. const secondWord = words[i + 1];
  126. if (secondWord && secondWord.startsWith('=')) {
  127. if (secondWord.length > 1) {
  128. const newWord = word + secondWord;
  129. tokens.push({ type, content: newWord });
  130. i += 1;
  131. continue;
  132. }
  133. const thirdWord = words[i + 2];
  134. i += 1;
  135. if (thirdWord) {
  136. const newWord = word + '=' + thirdWord;
  137. tokens.push({ type, content: newWord });
  138. i += 1;
  139. continue;
  140. }
  141. }
  142. }
  143. if (word.endsWith('=')) {
  144. const secondWord = words[i + 1];
  145. if (secondWord && secondWord.indexOf('=') === -1) {
  146. const newWord = word + secondWord;
  147. tokens.push({ type, content: newWord });
  148. i += 1;
  149. continue;
  150. }
  151. const newWord = word.slice(0, -1);
  152. tokens.push({ type, content: newWord });
  153. continue;
  154. }
  155. tokens.push({ type, content: word });
  156. }
  157. };
  158. const lexSkipTag = (tagName: string, state: State) => {
  159. const { str, tokens } = state;
  160. const safeTagName = tagName.toLowerCase();
  161. const len = str.length;
  162. let index = state.position;
  163. while (index < len) {
  164. const nextTag = str.indexOf('</', index);
  165. if (nextTag === -1) {
  166. lexText(state);
  167. break;
  168. }
  169. const tagState = {
  170. str,
  171. position: state.position,
  172. tokens: [],
  173. };
  174. jumpPosition(tagState, nextTag);
  175. const name = lexTag(tagState);
  176. if (safeTagName !== name.toLowerCase()) {
  177. index = tagState.position;
  178. continue;
  179. }
  180. if (nextTag !== state.position) {
  181. const textStart = state.position;
  182. jumpPosition(state, nextTag);
  183. tokens.push({
  184. type: 'text',
  185. content: str.slice(textStart, nextTag),
  186. });
  187. }
  188. tokens.push(...tagState.tokens);
  189. jumpPosition(state, tagState.position);
  190. break;
  191. }
  192. };
  193. const lexTag = (state: State) => {
  194. const { str } = state;
  195. const secondChar = str.charAt(state.position + 1);
  196. const tagStartClose = secondChar === '/';
  197. movePositopn(state, tagStartClose ? 2 : 1);
  198. state.tokens.push({
  199. type: 'tag-start',
  200. close: tagStartClose,
  201. });
  202. const tagName = lexTagName(state);
  203. lexTagAttributes(state);
  204. const firstChar = str.charAt(state.position);
  205. const tagEndClose = firstChar === '/';
  206. movePositopn(state, tagEndClose ? 2 : 1);
  207. state.tokens.push({
  208. type: 'tag-end',
  209. close: tagEndClose,
  210. });
  211. return tagName;
  212. };
  213. const lex = (state: State) => {
  214. const str = state.str;
  215. const len = str.length;
  216. while (state.position < len) {
  217. const start = state.position;
  218. lexText(state);
  219. if (state.position === start) {
  220. const isComment = str.startsWith('!--', start + 1);
  221. if (isComment) lexComment(state);
  222. else {
  223. const tagName = lexTag(state);
  224. const safeTag = tagName.toLowerCase();
  225. if (childlessTags.includes(safeTag)) lexSkipTag(tagName, state);
  226. }
  227. }
  228. }
  229. };
  230. export const lexer = (str: string): Token[] => {
  231. const state = {
  232. str,
  233. position: 0,
  234. tokens: [],
  235. };
  236. lex(state);
  237. return state.tokens;
  238. };