| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274 |
- import type { Token } from './types';
- import { childlessTags } from './tags';
- interface State {
- str: string;
- position: number;
- tokens: Token[];
- }
- const jumpPosition = (state: State, end: number) => {
- const len = end - state.position;
- movePositopn(state, len);
- };
- const movePositopn = (state: State, len: number) => {
- state.position = state.position + len;
- };
- const findTextEnd = (str: string, index: number) => {
- const isEnd = false;
- while (!isEnd) {
- const textEnd = str.indexOf('<', index);
- if (textEnd === -1) {
- return textEnd;
- }
- const char = str.charAt(textEnd + 1);
- if (char === '/' || char === '!' || /[A-Za-z0-9]/.test(char)) {
- return textEnd;
- }
- index = textEnd + 1;
- }
- return -1;
- };
- const lexText = (state: State) => {
- const { str } = state;
- let textEnd = findTextEnd(str, state.position);
- if (textEnd === state.position) return;
- if (textEnd === -1) {
- textEnd = str.length;
- }
- const content = str.slice(state.position, textEnd);
- jumpPosition(state, textEnd);
- state.tokens.push({
- type: 'text',
- content,
- });
- };
- const lexComment = (state: State) => {
- const { str } = state;
- movePositopn(state, 4);
- let contentEnd = str.indexOf('-->', state.position);
- let commentEnd = contentEnd + 3;
- if (contentEnd === -1) {
- contentEnd = commentEnd = str.length;
- }
- const content = str.slice(state.position, contentEnd);
- jumpPosition(state, commentEnd);
- state.tokens.push({
- type: 'comment',
- content,
- });
- };
- const lexTagName = (state: State) => {
- const { str } = state;
- const len = str.length;
- let start = state.position;
- while (start < len) {
- const char = str.charAt(start);
- const isTagChar = !(/\s/.test(char) || char === '/' || char === '>');
- if (isTagChar) break;
- start++;
- }
- let end = start + 1;
- while (end < len) {
- const char = str.charAt(end);
- const isTagChar = !(/\s/.test(char) || char === '/' || char === '>');
- if (!isTagChar) break;
- end++;
- }
- jumpPosition(state, end);
- const tagName = str.slice(start, end);
- state.tokens.push({
- type: 'tag',
- content: tagName,
- });
- return tagName;
- };
- const lexTagAttributes = (state: State) => {
- const { str, tokens } = state;
- let cursor = state.position;
- let quote = null;
- let wordBegin = cursor;
- const words = [];
- const len = str.length;
- while (cursor < len) {
- const char = str.charAt(cursor);
- if (quote) {
- const isQuoteEnd = char === quote;
- if (isQuoteEnd) quote = null;
- cursor++;
- continue;
- }
- const isTagEnd = char === '/' || char === '>';
- if (isTagEnd) {
- if (cursor !== wordBegin) words.push(str.slice(wordBegin, cursor));
- break;
- }
- const isWordEnd = /\s/.test(char);
- if (isWordEnd) {
- if (cursor !== wordBegin) words.push(str.slice(wordBegin, cursor));
- wordBegin = cursor + 1;
- cursor++;
- continue;
- }
- const isQuoteStart = char === "'" || char === '"';
- if (isQuoteStart) {
- quote = char;
- cursor++;
- continue;
- }
- cursor++;
- }
- jumpPosition(state, cursor);
- const type = 'attribute';
- for (let i = 0; i < words.length; i++) {
- const word = words[i];
- const isNotPair = word.indexOf('=') === -1;
- if (isNotPair) {
- const secondWord = words[i + 1];
- if (secondWord && secondWord.startsWith('=')) {
- if (secondWord.length > 1) {
- const newWord = word + secondWord;
- tokens.push({ type, content: newWord });
- i += 1;
- continue;
- }
- const thirdWord = words[i + 2];
- i += 1;
- if (thirdWord) {
- const newWord = word + '=' + thirdWord;
- tokens.push({ type, content: newWord });
- i += 1;
- continue;
- }
- }
- }
- if (word.endsWith('=')) {
- const secondWord = words[i + 1];
- if (secondWord && secondWord.indexOf('=') === -1) {
- const newWord = word + secondWord;
- tokens.push({ type, content: newWord });
- i += 1;
- continue;
- }
- const newWord = word.slice(0, -1);
- tokens.push({ type, content: newWord });
- continue;
- }
- tokens.push({ type, content: word });
- }
- };
- const lexSkipTag = (tagName: string, state: State) => {
- const { str, tokens } = state;
- const safeTagName = tagName.toLowerCase();
- const len = str.length;
- let index = state.position;
- while (index < len) {
- const nextTag = str.indexOf('</', index);
- if (nextTag === -1) {
- lexText(state);
- break;
- }
- const tagState = {
- str,
- position: state.position,
- tokens: [],
- };
- jumpPosition(tagState, nextTag);
- const name = lexTag(tagState);
- if (safeTagName !== name.toLowerCase()) {
- index = tagState.position;
- continue;
- }
- if (nextTag !== state.position) {
- const textStart = state.position;
- jumpPosition(state, nextTag);
- tokens.push({
- type: 'text',
- content: str.slice(textStart, nextTag),
- });
- }
- tokens.push(...tagState.tokens);
- jumpPosition(state, tagState.position);
- break;
- }
- };
- const lexTag = (state: State) => {
- const { str } = state;
- const secondChar = str.charAt(state.position + 1);
- const tagStartClose = secondChar === '/';
- movePositopn(state, tagStartClose ? 2 : 1);
- state.tokens.push({
- type: 'tag-start',
- close: tagStartClose,
- });
- const tagName = lexTagName(state);
- lexTagAttributes(state);
- const firstChar = str.charAt(state.position);
- const tagEndClose = firstChar === '/';
- movePositopn(state, tagEndClose ? 2 : 1);
- state.tokens.push({
- type: 'tag-end',
- close: tagEndClose,
- });
- return tagName;
- };
- const lex = (state: State) => {
- const str = state.str;
- const len = str.length;
- while (state.position < len) {
- const start = state.position;
- lexText(state);
- if (state.position === start) {
- const isComment = str.startsWith('!--', start + 1);
- if (isComment) lexComment(state);
- else {
- const tagName = lexTag(state);
- const safeTag = tagName.toLowerCase();
- if (childlessTags.includes(safeTag)) lexSkipTag(tagName, state);
- }
- }
- }
- };
- export const lexer = (str: string): Token[] => {
- const state = {
- str,
- position: 0,
- tokens: [],
- };
- lex(state);
- return state.tokens;
- };
|