#include "tag.h" #include "tree_sitter/parser.h" #include #include #include enum TokenType { AUTOMATIC_SEMICOLON, TERNARY_QMARK, ELVIS_OPERATOR, LOGICAL_OR, CF_START_TAG_NAME, CF_END_TAG_NAME, ERRONEOUS_CF_END_TAG_NAME, CF_SELF_CLOSING_TAG_DELIMITER, CF_SELF_CLOSING_VOID_TAG_DELIMITER, IMPLICIT_CF_END_TAG, RAW_TEXT, CFML_COMMENT, CLOSE_TAG_DELIM, CLOSE_CF_TAG_DELIM, HTML_TEXT, CF_VOID_START_TAG_NAME, CF_SET_START_TAG_NAME, CF_IF_START_TAG_NAME, CF_IF_END_TAG_NAME, CF_ELSEIF_TAG_NAME, CF_ELSE_TAG_NAME, CF_RETURN_START_TAG_NAME, CF_OUTPUT_START_TAG_NAME, // Rules below this point are not included by the cfquery dialect SCRIPT_START_TAG_NAME, STYLE_START_TAG_NAME, START_TAG_NAME, END_TAG_NAME, ERRONEOUS_END_TAG_NAME, SELF_CLOSING_TAG_DELIMITER, IMPLICIT_END_TAG, START_HASH_EXPRESSION, SINGLE_HASH, HASH_EMPTY, CF_XML_START_TAG_NAME, CF_XML_END_TAG_NAME, CF_XML_CONTENT, CF_QUERY_START_TAG_NAME, CF_QUERY_END_TAG_NAME, CF_QUERY_CONTENT, CF_SCRIPT_START_TAG_NAME, CF_SCRIPT_END_TAG_NAME, CF_SCRIPT_CONTENT, CF_SAVECONTENT_START_TAG_NAME, CF_SAVECONTENT_END_TAG_NAME, CF_SAVECONTENT_BODY_CFML, CF_SAVECONTENT_BODY_HTML, CF_SAVECONTENT_BODY_SCRIPT, CF_SAVECONTENT_BODY_CSS, CF_SAVECONTENT_BODY_XML, CF_SAVECONTENT_BODY_SQL, CF_SAVECONTENT_BODY_RAW, CF_SAVECONTENT_CONTENT, CF_FUNCTION_START_TAG_NAME, CF_FUNCTION_END_TAG_NAME, CF_COMPONENT_START_TAG_NAME, CF_COMPONENT_END_TAG_NAME, CF_COMPONENT_CONTENT, SCANNER_SYMBOL_COUNT }; typedef struct { Array(Tag) tags; Array(Tag) cf_tags; uint16_t cfoutput_depth; uint16_t cfcomponent_depth; uint16_t cffunction_depth; } Scanner; typedef enum { REJECT, // Semicolon is illegal, ie a syntax error occurred NO_NEWLINE, // Unclear if semicolon will be legal, continue ACCEPT, // Semicolon is legal, assuming a comment was encountered } WhitespaceResult; #define MAX(a, b) ((a) > (b) ? (a) : (b)) #define MAX_CF_END_DELIMITER_SIZE 256 #define VS(vs, sym, count) ((unsigned)(sym) < (count) && (vs)[(sym)]) static inline void advance(TSLexer *lexer) { lexer->advance(lexer, false); } static inline void skip(TSLexer *lexer) { lexer->advance(lexer, true); } static inline bool tag_has_name(TagType type, bool is_cfquery_context) { return type == CUSTOM || type == CFML || type == CF_VOID || type == CF_SET || type == CF_XML || type == CF_SCRIPT || type == CF_SAVECONTENT || type == CF_QUERY || type == CF_OUTPUT || type == CF_FUNCTION || type == CF_RETURN || type == CF_IF || type == CF_ELSEIF || type == CF_ELSE; } static inline bool valid_start_tag_name(const bool *vs, unsigned count) { return VS(vs, START_TAG_NAME, count) || VS(vs, SCRIPT_START_TAG_NAME, count) || VS(vs, CF_START_TAG_NAME, count) || VS(vs, CF_SET_START_TAG_NAME, count) || VS(vs, CF_VOID_START_TAG_NAME, count) || VS(vs, CF_RETURN_START_TAG_NAME, count) || VS(vs, CF_XML_START_TAG_NAME, count) || VS(vs, CF_QUERY_START_TAG_NAME, count) || VS(vs, CF_SCRIPT_START_TAG_NAME, count) || VS(vs, CF_SAVECONTENT_START_TAG_NAME, count) || VS(vs, CF_OUTPUT_START_TAG_NAME, count) || VS(vs, CF_FUNCTION_START_TAG_NAME, count) || VS(vs, CF_COMPONENT_START_TAG_NAME, count) || VS(vs, CF_IF_START_TAG_NAME, count) || VS(vs, CF_ELSEIF_TAG_NAME, count) || VS(vs, CF_ELSE_TAG_NAME, count); } static inline bool valid_end_tag_name(const bool *vs, unsigned count) { return VS(vs, END_TAG_NAME, count) || VS(vs, CF_END_TAG_NAME, count) || VS(vs, CF_XML_END_TAG_NAME, count) || VS(vs, CF_QUERY_END_TAG_NAME, count) || VS(vs, CF_SCRIPT_END_TAG_NAME, count) || VS(vs, CF_SAVECONTENT_END_TAG_NAME, count) || VS(vs, CF_FUNCTION_END_TAG_NAME, count) || VS(vs, CF_IF_END_TAG_NAME, count); } static inline bool valid_cf_start_tag_name(const bool *vs, unsigned count) { return VS(vs, CF_START_TAG_NAME, count) || VS(vs, CF_SET_START_TAG_NAME, count) || VS(vs, CF_VOID_START_TAG_NAME, count) || VS(vs, CF_RETURN_START_TAG_NAME, count) || VS(vs, CF_XML_START_TAG_NAME, count) || VS(vs, CF_QUERY_START_TAG_NAME, count) || VS(vs, CF_SCRIPT_START_TAG_NAME, count) || VS(vs, CF_SAVECONTENT_START_TAG_NAME, count) || VS(vs, CF_OUTPUT_START_TAG_NAME, count) || VS(vs, CF_FUNCTION_START_TAG_NAME, count) || VS(vs, CF_COMPONENT_START_TAG_NAME, count) || VS(vs, CF_IF_START_TAG_NAME, count) || VS(vs, CF_ELSEIF_TAG_NAME, count) || VS(vs, CF_ELSE_TAG_NAME, count); } static inline bool valid_cf_end_tag_name(const bool *vs, unsigned count) { return VS(vs, CF_END_TAG_NAME, count) || VS(vs, CF_XML_END_TAG_NAME, count) || VS(vs, CF_QUERY_END_TAG_NAME, count) || VS(vs, CF_SCRIPT_END_TAG_NAME, count) || VS(vs, CF_SAVECONTENT_END_TAG_NAME, count) || VS(vs, CF_FUNCTION_END_TAG_NAME, count) || VS(vs, CF_IF_END_TAG_NAME, count); } static inline bool no_content_symbols(const bool *vs, unsigned count) { return !VS(vs, RAW_TEXT, count) && !VS(vs, CF_XML_CONTENT, count) && !VS(vs, CF_QUERY_CONTENT, count) && !VS(vs, CF_SCRIPT_CONTENT, count); } static inline bool implicit_cf_end_tag_valid(const bool *vs, unsigned count) { return VS(vs, IMPLICIT_CF_END_TAG, count) && !VS(vs, CF_XML_END_TAG_NAME, count) && !VS(vs, CF_QUERY_END_TAG_NAME, count) && !VS(vs, CF_SCRIPT_END_TAG_NAME, count) && !VS(vs, CF_SAVECONTENT_END_TAG_NAME, count) && !VS(vs, CF_FUNCTION_END_TAG_NAME, count) && !VS(vs, CF_IF_END_TAG_NAME, count) && !VS(vs, CF_ELSEIF_TAG_NAME, count) && !VS(vs, CF_ELSE_TAG_NAME, count); } #define SERIALIZE_TAGS(tags_field, buffer, size, is_cfquery_context) do { \ uint16_t _count = (tags_field).size > UINT16_MAX ? UINT16_MAX : (tags_field).size; \ uint16_t _serialized = 0; \ unsigned _count_offset = (size); \ if ((size) + sizeof(_count) + sizeof(_count) > TREE_SITTER_SERIALIZATION_BUFFER_SIZE) break; \ (size) += sizeof(_count); \ (size) += sizeof(_count); \ for (; _serialized < _count; _serialized++) { \ Tag _tag = (tags_field).contents[_serialized]; \ if (tag_has_name(_tag.type, is_cfquery_context)) { \ unsigned _len = _tag.tag_name.size; \ if (_len > UINT8_MAX) _len = UINT8_MAX; \ if ((size) + 2 + _len + sizeof(_tag.html_depth) >= TREE_SITTER_SERIALIZATION_BUFFER_SIZE) break; \ (buffer)[(size)++] = (char)_tag.type; \ (buffer)[(size)++] = (char)_len; \ strncpy(&(buffer)[(size)], _tag.tag_name.contents, _len); \ (size) += _len; \ memcpy(&(buffer)[(size)], &_tag.html_depth, sizeof(_tag.html_depth)); \ (size) += sizeof(_tag.html_depth); \ } else { \ if ((size) + 1 >= TREE_SITTER_SERIALIZATION_BUFFER_SIZE) break; \ (buffer)[(size)++] = (char)_tag.type; \ } \ } \ memcpy(&(buffer)[_count_offset], &_serialized, sizeof(_serialized)); \ memcpy(&(buffer)[_count_offset + sizeof(_serialized)], &_count, sizeof(_count)); \ } while(0) static unsigned serialize(Scanner *scanner, char *buffer, bool is_cfquery_context) { unsigned size = 0; SERIALIZE_TAGS(scanner->tags, buffer, size, is_cfquery_context); SERIALIZE_TAGS(scanner->cf_tags, buffer, size, is_cfquery_context); if (size + sizeof(scanner->cfoutput_depth) + sizeof(scanner->cfcomponent_depth) + sizeof(scanner->cffunction_depth) < TREE_SITTER_SERIALIZATION_BUFFER_SIZE) { memcpy(&buffer[size], &scanner->cfoutput_depth, sizeof(scanner->cfoutput_depth)); size += sizeof(scanner->cfoutput_depth); memcpy(&buffer[size], &scanner->cfcomponent_depth, sizeof(scanner->cfcomponent_depth)); size += sizeof(scanner->cfcomponent_depth); memcpy(&buffer[size], &scanner->cffunction_depth, sizeof(scanner->cffunction_depth)); size += sizeof(scanner->cffunction_depth); } return size; } #define DESERIALIZE_TAGS(tags_field, buffer, size, is_cfquery_context) do { \ for (unsigned _i = 0; _i < (tags_field).size; _i++) tag_free(&(tags_field).contents[_i]); \ array_clear(&(tags_field)); \ uint16_t _serialized = 0, _count = 0; \ memcpy(&_serialized, &(buffer)[(size)], sizeof(_serialized)); (size) += sizeof(_serialized); \ memcpy(&_count, &(buffer)[(size)], sizeof(_count)); (size) += sizeof(_count); \ array_reserve(&(tags_field), _count); \ unsigned _iter = 0; \ for (_iter = 0; _iter < _serialized; _iter++) { \ Tag _tag = tag_new(); \ _tag.type = (TagType)(unsigned char)(buffer)[(size)++]; \ if (tag_has_name(_tag.type, is_cfquery_context)) { \ uint16_t _len = (uint8_t)(buffer)[(size)++]; \ array_reserve(&_tag.tag_name, _len); \ _tag.tag_name.size = _len; \ memcpy(_tag.tag_name.contents, &(buffer)[(size)], _len); \ (size) += _len; \ memcpy(&_tag.html_depth, &(buffer)[(size)], sizeof(_tag.html_depth)); \ (size) += sizeof(_tag.html_depth); \ } \ array_push(&(tags_field), _tag); \ } \ for (; _iter < _count; _iter++) array_push(&(tags_field), tag_new()); \ } while(0) static void deserialize(Scanner *scanner, const char *buffer, unsigned length, bool is_cfquery_context) { scanner->cfoutput_depth = 0; scanner->cfcomponent_depth = 0; scanner->cffunction_depth = 0; if (length > 0) { unsigned size = 0; DESERIALIZE_TAGS(scanner->tags, buffer, size, is_cfquery_context); DESERIALIZE_TAGS(scanner->cf_tags, buffer, size, is_cfquery_context); if (size + sizeof(scanner->cfoutput_depth) <= length) { memcpy(&scanner->cfoutput_depth, &buffer[size], sizeof(scanner->cfoutput_depth)); size += sizeof(scanner->cfoutput_depth); } if (size + sizeof(scanner->cfcomponent_depth) <= length) { memcpy(&scanner->cfcomponent_depth, &buffer[size], sizeof(scanner->cfcomponent_depth)); size += sizeof(scanner->cfcomponent_depth); } if (size + sizeof(scanner->cffunction_depth) <= length) { memcpy(&scanner->cffunction_depth, &buffer[size], sizeof(scanner->cffunction_depth)); size += sizeof(scanner->cffunction_depth); } } else { for (unsigned i = 0; i < scanner->tags.size; i++) tag_free(&scanner->tags.contents[i]); array_clear(&scanner->tags); for (unsigned i = 0; i < scanner->cf_tags.size; i++) tag_free(&scanner->cf_tags.contents[i]); array_clear(&scanner->cf_tags); } } typedef struct { String tag_name; bool is_cf_tag; } TagNameResult; static TagNameResult scan_tag_name(TSLexer *lexer, bool is_cfquery_context) { TagNameResult result; String tag_name = array_new(); bool is_cf_tag = false; // ColdFusion tags might start with 'C', ie. lookahead == 'c' || lexer->lookahead == 'C' ) { array_push(&tag_name, towupper(lexer->lookahead)); advance(lexer); if (lexer->lookahead == 'f' || lexer->lookahead == 'F') { is_cf_tag = true; array_delete(&tag_name); advance(lexer); } } while (( iswalnum(lexer->lookahead) || lexer->lookahead == '-' || lexer->lookahead == '_' || lexer->lookahead == ':' )) { array_push(&tag_name, towupper(lexer->lookahead)); advance(lexer); } result.tag_name = tag_name; result.is_cf_tag = is_cf_tag; return result; } static bool scan_comment(TSLexer *lexer, bool is_cfquery_context) { if (lexer->lookahead != '-') { return false; } advance(lexer); if (lexer->lookahead != '-') { return false; } advance(lexer); // IE conditional comments: or // These don't nest — just scan until --> if (lexer->lookahead == '[' || lexer->lookahead == '<') { unsigned close_dashes = 0; while (lexer->lookahead) { if (lexer->lookahead == '-') { close_dashes++; } else if (lexer->lookahead == '>' && close_dashes >= 2) { lexer->result_symbol = CFML_COMMENT; advance(lexer); lexer->mark_end(lexer); return true; } else { close_dashes = 0; } advance(lexer); } return false; } unsigned dashes = 0; unsigned direction = -1; unsigned nesting = 1; while (lexer->lookahead) { switch (lexer->lookahead) { case '-': ++dashes; if ( direction == 1 && dashes >= 2 ) { ++nesting; direction = -1; dashes = 0; } break; case '>': if (dashes >= 2) { --nesting; lexer->result_symbol = CFML_COMMENT; advance(lexer); lexer->mark_end(lexer); if ( nesting == 0 ) { return true; } dashes = 0; direction = -1; continue; } direction = -1; dashes = 0; break; case '<': direction = 0; dashes = 0; break; case '!': if ( direction == 0 ) { direction = 1; break; } direction = -1; dashes = 0; break; default: direction = -1; dashes = 0; break; } advance(lexer); } return false; } static WhitespaceResult scan_whitespace_and_comments(TSLexer *lexer, bool *scanned_comment, bool consume, bool is_cfquery_context) { bool saw_block_newline = false; for (;;) { while (iswspace(lexer->lookahead)) { skip(lexer); } if (lexer->lookahead == '/') { skip(lexer); if (lexer->lookahead == '/') { skip(lexer); while (lexer->lookahead != 0 && lexer->lookahead != '\n' && lexer->lookahead != 0x2028 && lexer->lookahead != 0x2029) { skip(lexer); } *scanned_comment = true; } else if (lexer->lookahead == '*') { skip(lexer); while (lexer->lookahead != 0) { if (lexer->lookahead == '*') { skip(lexer); if (lexer->lookahead == '/') { skip(lexer); *scanned_comment = true; if (lexer->lookahead != '/' && !consume) { return saw_block_newline ? ACCEPT : NO_NEWLINE; } break; } } else if (lexer->lookahead == '\n' || lexer->lookahead == 0x2028 || lexer->lookahead == 0x2029) { saw_block_newline = true; skip(lexer); } else { skip(lexer); } } } else { return REJECT; } } else { return ACCEPT; } } } static bool scan_html_text(Scanner *scanner, TSLexer *lexer, bool is_cfquery_context) { // Check if we're inside a script/style tag bool in_script_style = false; if (scanner->tags.size > 0) { TagType type = array_back(&scanner->tags)->type; if (type == SCRIPT || type == STYLE) { in_script_style = true; } } // saw_text will be true if we see any non-whitespace content, or any whitespace content that is not a newline and // does not immediately follow a newline. bool saw_text = false; // at_newline will be true if we are currently at a newline, or if we are at whitespace that is not a newline but // immediately follows a newline. bool at_newline = false; bool saw_any = false; if (in_script_style) { // Inside script/style: consume until #, mark_end(lexer); while (lexer->lookahead != 0 && lexer->lookahead != '#') { if (lexer->lookahead == '<') { // Peek for lookahead) == 'C') { advance(lexer); if (towupper(lexer->lookahead) == 'F') { break; } lexer->mark_end(lexer); saw_text = true; saw_any = true; continue; } else if (lexer->lookahead == '/') { advance(lexer); if (towupper(lexer->lookahead) == 'C') { advance(lexer); if (towupper(lexer->lookahead) == 'F') { break; } lexer->mark_end(lexer); saw_text = true; saw_any = true; continue; } else if (towupper(lexer->lookahead) == 'S') { // Potential mark_end(lexer); saw_text = true; saw_any = true; continue; } lexer->mark_end(lexer); saw_text = true; saw_any = true; continue; } saw_text = true; saw_any = true; advance(lexer); lexer->mark_end(lexer); } lexer->result_symbol = HTML_TEXT; return saw_text || (saw_any && lexer->lookahead == '#'); } while (lexer->lookahead != 0 && lexer->lookahead != '<' && lexer->lookahead != '>' && lexer->lookahead != '{' && lexer->lookahead != '}' && lexer->lookahead != '#') { if (lexer->lookahead == '&') { // Peek ahead to determine if this is an entity lexer->mark_end(lexer); advance(lexer); if (lexer->lookahead == '#') { // Could be numeric entity (&#digits; or &#xhex;) or &# followed by CFML hash // Mark end after & so it's consumed as text lexer->mark_end(lexer); saw_text = true; saw_any = true; advance(lexer); if (lexer->lookahead == 'x' || lexer->lookahead == 'X' || iswdigit(lexer->lookahead)) { // Numeric entity - consume fully as text if (lexer->lookahead == 'x' || lexer->lookahead == 'X') { advance(lexer); while (iswxdigit(lexer->lookahead)) advance(lexer); } else { while (iswdigit(lexer->lookahead)) advance(lexer); } if (lexer->lookahead == ';') advance(lexer); lexer->mark_end(lexer); continue; } // &# not followed by digit/x - & consumed as text, break at # break; } if (iswalpha(lexer->lookahead)) { // Could be &word; - scan ahead for ; unsigned count = 0; while (iswalpha(lexer->lookahead) && count < 31) { advance(lexer); count++; } if (lexer->lookahead == ';' && count > 0) { // Valid entity pattern - stop before & break; } // Not a valid entity - consume as text lexer->mark_end(lexer); saw_text = true; saw_any = true; continue; } // & followed by non-alpha, non-# - consume as text lexer->mark_end(lexer); saw_text = true; saw_any = true; continue; } bool is_wspace = iswspace(lexer->lookahead); if (lexer->lookahead == '\n') { at_newline = true; } else { at_newline &= is_wspace; if (!at_newline) { saw_text = true; } } saw_any = true; advance(lexer); } lexer->result_symbol = HTML_TEXT; // Emit whitespace-only content when the next char is '#' so the parser // advances to a state where hash tokens become valid. return saw_text || (saw_any && lexer->lookahead == '#'); } static bool scan_script_comment(TSLexer *lexer, bool is_cfquery_context) { for (;;) { if (lexer->lookahead == '/') { skip(lexer); while (lexer->lookahead != 0 && lexer->lookahead != '\n' && lexer->lookahead != 0x2028 && lexer->lookahead != 0x2029) { skip(lexer); } //*scanned_comment = true; } else if (lexer->lookahead == '*') { skip(lexer); while (lexer->lookahead != 0) { if (lexer->lookahead == '*') { skip(lexer); if (lexer->lookahead == '/') { skip(lexer); //*scanned_comment = true; break; } } else { skip(lexer); } } } else { return false; } } } static bool scan_cfquery_content(Scanner *scanner, TSLexer *lexer, bool is_cfquery_context) { if (scanner->cf_tags.size == 0) { return false; } Tag *cf_tag = array_back(&scanner->cf_tags); if (cf_tag->type != CF_QUERY) { return false; } lexer->mark_end(lexer); size_t tag_len = cf_tag->tag_name.size; if (tag_len > MAX_CF_END_DELIMITER_SIZE - 5) return false; char end_delimiter[MAX_CF_END_DELIMITER_SIZE]; memcpy(end_delimiter, "tag_name.contents, tag_len); end_delimiter[4 + tag_len] = '\0'; size_t delimiter_index = 0; size_t end_delim_len = 4 + tag_len; while (lexer->lookahead) { if (towupper(lexer->lookahead) == end_delimiter[delimiter_index]) { delimiter_index++; if (delimiter_index == end_delim_len) { break; } advance(lexer); } else { delimiter_index = 0; advance(lexer); lexer->mark_end(lexer); } } lexer->result_symbol = CF_QUERY_CONTENT; return true; } static bool scan_cfxml_content(Scanner *scanner, TSLexer *lexer, bool is_cfquery_context) { if (scanner->cf_tags.size == 0) { return false; } Tag *cf_tag = array_back(&scanner->cf_tags); if (cf_tag->type != CF_XML) { return false; } lexer->mark_end(lexer); size_t tag_len = cf_tag->tag_name.size; if (tag_len > MAX_CF_END_DELIMITER_SIZE - 5) return false; char end_delimiter[MAX_CF_END_DELIMITER_SIZE]; memcpy(end_delimiter, "tag_name.contents, tag_len); end_delimiter[4 + tag_len] = '\0'; size_t delimiter_index = 0; size_t end_delim_len = 4 + tag_len; while (lexer->lookahead) { if (towupper(lexer->lookahead) == end_delimiter[delimiter_index]) { delimiter_index++; if (delimiter_index == end_delim_len) { break; } advance(lexer); } else { delimiter_index = 0; advance(lexer); lexer->mark_end(lexer); } } lexer->result_symbol = CF_XML_CONTENT; return true; } static bool scan_cfscript_content(Scanner *scanner, TSLexer *lexer, bool is_cfquery_context) { if (scanner->cf_tags.size == 0) { return false; } Tag *cf_tag = array_back(&scanner->cf_tags); if (cf_tag->type != CF_SCRIPT) { return false; } lexer->mark_end(lexer); size_t tag_len = cf_tag->tag_name.size; if (tag_len > MAX_CF_END_DELIMITER_SIZE - 5) return false; char end_delimiter[MAX_CF_END_DELIMITER_SIZE]; memcpy(end_delimiter, "tag_name.contents, tag_len); end_delimiter[4 + tag_len] = '\0'; size_t delimiter_index = 0; size_t end_delim_len = 4 + tag_len; while (lexer->lookahead) { if (towupper(lexer->lookahead) == end_delimiter[delimiter_index]) { delimiter_index++; if (delimiter_index == end_delim_len) { break; } advance(lexer); } else { delimiter_index = 0; advance(lexer); lexer->mark_end(lexer); } } lexer->result_symbol = CF_SCRIPT_CONTENT; return true; } static bool scan_cfsavecontent_body_type(Scanner *scanner, TSLexer *lexer, const bool *valid_symbols, unsigned count, bool is_cfquery_context) { if (scanner->cf_tags.size == 0) return false; Tag *cf_tag = array_back(&scanner->cf_tags); if (cf_tag->type != CF_SAVECONTENT) return false; // Default to cfml unsigned result = CF_SAVECONTENT_BODY_CFML; // Mark end at current position - this is a zero-width token lexer->mark_end(lexer); // Peek ahead for // Skip whitespace first while (iswspace(lexer->lookahead)) advance(lexer); if (lexer->lookahead == '<') { advance(lexer); if (lexer->lookahead == '!') { advance(lexer); if (lexer->lookahead == '-') { advance(lexer); if (lexer->lookahead == '-') { advance(lexer); if (lexer->lookahead == '-') { advance(lexer); // Skip whitespace after