From 6cb316da30cb460d54c4eec6fe15e034c89a7b36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20K=C3=A4chele?= Date: Fri, 17 Oct 2025 17:00:49 +0200 Subject: [PATCH 01/44] Make token value hb_string --- src/include/token_struct.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/include/token_struct.h b/src/include/token_struct.h index 2727d2a4c..595bf27d1 100644 --- a/src/include/token_struct.h +++ b/src/include/token_struct.h @@ -3,6 +3,7 @@ #include "location.h" #include "range.h" +#include "util/hb_string.h" typedef enum { TOKEN_WHITESPACE, // ' ' @@ -49,7 +50,7 @@ typedef enum { } token_type_T; typedef struct TOKEN_STRUCT { - char* value; + hb_string_T value; range_T range; location_T location; token_type_T type; From 74ceb0e824b0c0cd5afa50bba1e7c9b4b46489b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20K=C3=A4chele?= Date: Fri, 17 Oct 2025 17:02:12 +0200 Subject: [PATCH 02/44] Make token_init take hb_string value --- src/include/token.h | 3 ++- src/token.c | 9 +++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/include/token.h b/src/include/token.h index 5628e2f0d..7c8dd8778 100644 --- a/src/include/token.h +++ b/src/include/token.h @@ -4,8 +4,9 @@ #include "lexer_struct.h" #include "position.h" #include "token_struct.h" +#include "util/hb_string.h" -token_T* token_init(const char* value, token_type_T type, lexer_T* lexer); +token_T* token_init(hb_string_T value, token_type_T type, lexer_T* lexer); char* token_to_string(const token_T* token); const char* token_type_to_string(token_type_T type); diff --git a/src/token.c b/src/token.c index b6ef42fe8..e0fd81ff8 100644 --- a/src/token.c +++ b/src/token.c @@ -4,6 +4,7 @@ #include "include/range.h" #include "include/token_struct.h" #include "include/util.h" +#include "include/util/hb_string.h" #include #include @@ -13,7 +14,7 @@ size_t token_sizeof(void) { return sizeof(struct TOKEN_STRUCT); } -token_T* token_init(const char* value, const token_type_T type, lexer_T* lexer) { +token_T* token_init(hb_string_T value, const token_type_T type, lexer_T* lexer) { token_T* token = calloc(1, token_sizeof()); if (type == TOKEN_NEWLINE) { @@ -21,11 +22,7 @@ token_T* token_init(const char* value, const token_type_T type, lexer_T* lexer) lexer->current_column = 0; } - if (value) { - token->value = herb_strdup(value); - } else { - token->value = NULL; - } + token->value = value; token->type = type; token->range = (range_T) { .from = lexer->previous_position, .to = lexer->current_position }; From 2de7ee36fe9d1f0f0fec8761d21f4f138edcfcf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20K=C3=A4chele?= Date: Fri, 17 Oct 2025 17:04:48 +0200 Subject: [PATCH 03/44] Allocate lexer errors using the arena --- src/lexer.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lexer.c b/src/lexer.c index 45995751c..7de89dc64 100644 --- a/src/lexer.c +++ b/src/lexer.c @@ -55,7 +55,7 @@ void lexer_init(lexer_T* lexer, const char* source) { } token_T* lexer_error(lexer_T* lexer, const char* message) { - char error_message[128]; + char *error_message = hb_arena_alloc(lexer->allocator, sizeof(char) * 128); snprintf( error_message, @@ -67,7 +67,7 @@ token_T* lexer_error(lexer_T* lexer, const char* message) { lexer->current_column ); - return token_init(error_message, TOKEN_ERROR, lexer); + return token_init(hb_string_from_c_string(error_message), TOKEN_ERROR, lexer); } static void lexer_advance(lexer_T* lexer) { From ef6120c29c266032321bd578ff17c359d4bc5f42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20K=C3=A4chele?= Date: Fri, 17 Oct 2025 17:14:21 +0200 Subject: [PATCH 04/44] Make lexer_match_and_advance take string --- src/lexer.c | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/lexer.c b/src/lexer.c index 7de89dc64..85d106ca3 100644 --- a/src/lexer.c +++ b/src/lexer.c @@ -55,7 +55,7 @@ void lexer_init(lexer_T* lexer, const char* source) { } token_T* lexer_error(lexer_T* lexer, const char* message) { - char *error_message = hb_arena_alloc(lexer->allocator, sizeof(char) * 128); + char* error_message = hb_arena_alloc(lexer->allocator, sizeof(char) * 128); snprintf( error_message, @@ -157,9 +157,11 @@ static token_T* lexer_advance_utf8_character(lexer_T* lexer, const token_type_T return token; } -static token_T* lexer_match_and_advance(lexer_T* lexer, const char* value, const token_type_T type) { - if (strncmp(lexer->source.data + lexer->current_position, value, strlen(value)) == 0) { - return lexer_advance_with(lexer, value, type); +static token_T* lexer_match_and_advance(lexer_T* lexer, hb_string_T value, const token_type_T type) { + hb_string_T remaining_source = hb_string_slice(lexer->source, lexer->current_position); + if (hb_string_starts_with(remaining_source, value)) { + // TODO(Tim): Fix string + return lexer_advance_with(lexer, value.data, type); } return NULL; @@ -206,11 +208,13 @@ static token_T* lexer_parse_identifier(lexer_T* lexer) { // ===== ERB Parsing static token_T* lexer_parse_erb_open(lexer_T* lexer) { - const char* erb_patterns[] = { "<%==", "<%%=", "<%=", "<%#", "<%-", "<%%", "<%" }; + hb_string_T erb_patterns[7] = { hb_string_from_c_string("<%=="), hb_string_from_c_string("<%%="), + hb_string_from_c_string("<%="), hb_string_from_c_string("<%#"), + hb_string_from_c_string("<%-"), hb_string_from_c_string("<%%"), + hb_string_from_c_string("<%") }; lexer->state = STATE_ERB_CONTENT; - - for (size_t i = 0; i < sizeof(erb_patterns) / sizeof(erb_patterns[0]); i++) { + for (size_t i = 0; i < 7; i++) { token_T* match = lexer_match_and_advance(lexer, erb_patterns[i], TOKEN_ERB_START); if (match) { return match; } } @@ -314,22 +318,22 @@ token_T* lexer_next_token(lexer_T* lexer) { } case '/': { - token_T* token = lexer_match_and_advance(lexer, "/>", TOKEN_HTML_TAG_SELF_CLOSE); + token_T* token = lexer_match_and_advance(lexer, hb_string_from_c_string("/>"), TOKEN_HTML_TAG_SELF_CLOSE); return token ? token : lexer_advance_current(lexer, TOKEN_SLASH); } case '?': { - token_T* token = lexer_match_and_advance(lexer, "?>", TOKEN_XML_DECLARATION_END); + token_T* token = lexer_match_and_advance(lexer, hb_string_from_c_string("?>"), TOKEN_XML_DECLARATION_END); return token ? token : lexer_advance_current(lexer, TOKEN_CHARACTER); } case '-': { - token_T* token = lexer_match_and_advance(lexer, "-->", TOKEN_HTML_COMMENT_END); + token_T* token = lexer_match_and_advance(lexer, hb_string_from_c_string("-->"), TOKEN_HTML_COMMENT_END); return token ? token : lexer_advance_current(lexer, TOKEN_DASH); } case ']': { - token_T* token = lexer_match_and_advance(lexer, "]]>", TOKEN_CDATA_END); + token_T* token = lexer_match_and_advance(lexer, hb_string_from_c_string("]]>"), TOKEN_CDATA_END); return token ? token : lexer_advance_current(lexer, TOKEN_CHARACTER); } From 86c995fd43131b21e5e70ee55b52c4937701c04b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20K=C3=A4chele?= Date: Fri, 17 Oct 2025 17:40:13 +0200 Subject: [PATCH 05/44] Make lexer_advance_with take string --- src/lexer.c | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/lexer.c b/src/lexer.c index 85d106ca3..3127966e7 100644 --- a/src/lexer.c +++ b/src/lexer.c @@ -102,8 +102,8 @@ static void lexer_advance_by(lexer_T* lexer, const size_t count) { } } -static token_T* lexer_advance_with(lexer_T* lexer, const char* value, const token_type_T type) { - lexer_advance_by(lexer, strlen(value)); +static token_T* lexer_advance_with(lexer_T* lexer, hb_string_T value, const token_type_T type) { + lexer_advance_by(lexer, value.length); return token_init(value, type, lexer); } @@ -125,7 +125,8 @@ static token_T* lexer_advance_with_next(lexer_T* lexer, size_t count, token_type } static token_T* lexer_advance_current(lexer_T* lexer, const token_type_T type) { - return lexer_advance_with(lexer, (char[]) { lexer->current_character, '\0' }, type); + hb_string_T value = { .data = lexer->source.data + lexer->current_position, .length = 1 }; + return lexer_advance_with(lexer, value, type); } static token_T* lexer_advance_utf8_character(lexer_T* lexer, const token_type_T type) { @@ -159,10 +160,7 @@ static token_T* lexer_advance_utf8_character(lexer_T* lexer, const token_type_T static token_T* lexer_match_and_advance(lexer_T* lexer, hb_string_T value, const token_type_T type) { hb_string_T remaining_source = hb_string_slice(lexer->source, lexer->current_position); - if (hb_string_starts_with(remaining_source, value)) { - // TODO(Tim): Fix string - return lexer_advance_with(lexer, value.data, type); - } + if (hb_string_starts_with(remaining_source, value)) { return lexer_advance_with(lexer, value, type); } return NULL; } @@ -260,11 +258,17 @@ static token_T* lexer_parse_erb_content(lexer_T* lexer) { static token_T* lexer_parse_erb_close(lexer_T* lexer) { lexer->state = STATE_DATA; - if (lexer_peek_erb_percent_close_tag(lexer, 0)) { return lexer_advance_with(lexer, "%%>", TOKEN_ERB_END); } - if (lexer_peek_erb_equals_close_tag(lexer, 0)) { return lexer_advance_with(lexer, "=%>", TOKEN_ERB_END); } - if (lexer_peek_erb_dash_close_tag(lexer, 0)) { return lexer_advance_with(lexer, "-%>", TOKEN_ERB_END); } + if (lexer_peek_erb_percent_close_tag(lexer, 0)) { + return lexer_advance_with(lexer, hb_string_from_c_string("%%>"), TOKEN_ERB_END); + } + if (lexer_peek_erb_equals_close_tag(lexer, 0)) { + return lexer_advance_with(lexer, hb_string_from_c_string("=%>"), TOKEN_ERB_END); + } + if (lexer_peek_erb_dash_close_tag(lexer, 0)) { + return lexer_advance_with(lexer, hb_string_from_c_string("-%>"), TOKEN_ERB_END); + } - return lexer_advance_with(lexer, "%>", TOKEN_ERB_END); + return lexer_advance_with(lexer, hb_string_from_c_string("%>"), TOKEN_ERB_END); } // ===== Tokenizing Function @@ -307,11 +311,11 @@ token_T* lexer_next_token(lexer_T* lexer) { if (isalnum(lexer_peek(lexer, 1))) { return lexer_advance_current(lexer, TOKEN_HTML_TAG_START); } if (lexer_peek_for_html_comment_start(lexer, 0)) { - return lexer_advance_with(lexer, ""), TOKEN_HTML_COMMENT_END); + token_T* token = lexer_match_and_advance(lexer, hb_string("-->"), TOKEN_HTML_COMMENT_END); return token ? token : lexer_advance_current(lexer, TOKEN_DASH); } case ']': { - token_T* token = lexer_match_and_advance(lexer, hb_string_from_c_string("]]>"), TOKEN_CDATA_END); + token_T* token = lexer_match_and_advance(lexer, hb_string("]]>"), TOKEN_CDATA_END); return token ? token : lexer_advance_current(lexer, TOKEN_CHARACTER); } From 70c32cd7d6fba2e1d6b7763fae6fe5f9698f6c9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20K=C3=A4chele?= Date: Mon, 27 Oct 2025 18:59:13 +0100 Subject: [PATCH 23/44] Fix hb_string_from_c_string usages in extract.c --- src/extract.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/extract.c b/src/extract.c index 7ff547da9..f0afde2f3 100644 --- a/src/extract.c +++ b/src/extract.c @@ -21,9 +21,9 @@ void herb_extract_ruby_to_buffer_with_semicolons(const char* source, hb_buffer_T } case TOKEN_ERB_START: { - if (hb_string_equals(token->value, hb_string_from_c_string("<%#")) - || hb_string_equals(token->value, hb_string_from_c_string("<%%")) - || hb_string_equals(token->value, hb_string_from_c_string("<%%="))) { + if (hb_string_equals(token->value, hb_string("<%#")) + || hb_string_equals(token->value, hb_string("<%%")) + || hb_string_equals(token->value, hb_string("<%%="))) { skip_erb_content = true; } @@ -73,9 +73,9 @@ void herb_extract_ruby_to_buffer(const char* source, hb_buffer_T* output) { } case TOKEN_ERB_START: { - if (hb_string_equals(token->value, hb_string_from_c_string("<%#")) - || hb_string_equals(token->value, hb_string_from_c_string("<%%")) - || hb_string_equals(token->value, hb_string_from_c_string("<%%="))) { + if (hb_string_equals(token->value, hb_string("<%#")) + || hb_string_equals(token->value, hb_string("<%%")) + || hb_string_equals(token->value, hb_string("<%%="))) { skip_erb_content = true; } From ca1fa0bb616989883b8c4f4fbcc234c8ed030d1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20K=C3=A4chele?= Date: Mon, 27 Oct 2025 18:59:54 +0100 Subject: [PATCH 24/44] Fix hb_string_from_c_string usages in parser.c --- src/parser.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/parser.c b/src/parser.c index de58cc58d..c76002c79 100644 --- a/src/parser.c +++ b/src/parser.c @@ -609,7 +609,7 @@ static AST_HTML_ATTRIBUTE_NODE_T* parser_parse_html_attribute(parser_T* parser) token_T* equals_with_whitespace = calloc(1, sizeof(token_T)); equals_with_whitespace->type = TOKEN_EQUALS; // TODO(Tim): This is a leak - equals_with_whitespace->value = hb_string_from_c_string(equals_buffer.value); + equals_with_whitespace->value = hb_string(equals_buffer.value); equals_with_whitespace->location = (location_T) { .start = equals_start, .end = equals_end }; equals_with_whitespace->range = (range_T) { .from = range_start, .to = range_end }; @@ -719,7 +719,7 @@ static bool parser_lookahead_erb_is_attribute(lexer_T* lexer) { static void parser_handle_erb_in_open_tag(parser_T* parser, hb_array_T* children) { bool is_output_tag = !hb_string_is_empty(parser->current_token->value) - && hb_string_starts_with(parser->current_token->value, hb_string_from_c_string("<%=")); + && hb_string_starts_with(parser->current_token->value, hb_string("<%=")); if (!is_output_tag) { hb_array_append(children, parser_parse_erb_tag(parser)); From 78d0f7899d71983ebd0174840670cf08f0065c26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20K=C3=A4chele?= Date: Tue, 28 Oct 2025 07:28:46 +0100 Subject: [PATCH 25/44] WIP: Use hb_string in token_type_to_string --- src/include/token.h | 4 +- src/token.c | 95 +++++++++++++++++++++------------------------ 2 files changed, 47 insertions(+), 52 deletions(-) diff --git a/src/include/token.h b/src/include/token.h index 7c8dd8778..2f4c0d29d 100644 --- a/src/include/token.h +++ b/src/include/token.h @@ -8,9 +8,9 @@ token_T* token_init(hb_string_T value, token_type_T type, lexer_T* lexer); char* token_to_string(const token_T* token); -const char* token_type_to_string(token_type_T type); +hb_string_T token_type_to_string(token_type_T type); -char* token_value(const token_T* token); +hb_string_T token_value(const token_T* token); int token_type(const token_T* token); size_t token_sizeof(void); diff --git a/src/token.c b/src/token.c index 1a3d5aa50..50ffeec91 100644 --- a/src/token.c +++ b/src/token.c @@ -43,53 +43,53 @@ token_T* token_init(hb_string_T value, const token_type_T type, lexer_T* lexer) return token; } -const char* token_type_to_string(const token_type_T type) { +hb_string_T token_type_to_string(const token_type_T type) { switch (type) { - case TOKEN_WHITESPACE: return "TOKEN_WHITESPACE"; - case TOKEN_NBSP: return "TOKEN_NBSP"; - case TOKEN_NEWLINE: return "TOKEN_NEWLINE"; - case TOKEN_IDENTIFIER: return "TOKEN_IDENTIFIER"; - case TOKEN_HTML_DOCTYPE: return "TOKEN_HTML_DOCTYPE"; - case TOKEN_XML_DECLARATION: return "TOKEN_XML_DECLARATION"; - case TOKEN_XML_DECLARATION_END: return "TOKEN_XML_DECLARATION_END"; - case TOKEN_CDATA_START: return "TOKEN_CDATA_START"; - case TOKEN_CDATA_END: return "TOKEN_CDATA_END"; - case TOKEN_HTML_TAG_START: return "TOKEN_HTML_TAG_START"; - case TOKEN_HTML_TAG_END: return "TOKEN_HTML_TAG_END"; - case TOKEN_HTML_TAG_START_CLOSE: return "TOKEN_HTML_TAG_START_CLOSE"; - case TOKEN_HTML_TAG_SELF_CLOSE: return "TOKEN_HTML_TAG_SELF_CLOSE"; - case TOKEN_HTML_COMMENT_START: return "TOKEN_HTML_COMMENT_START"; - case TOKEN_HTML_COMMENT_END: return "TOKEN_HTML_COMMENT_END"; - case TOKEN_EQUALS: return "TOKEN_EQUALS"; - case TOKEN_QUOTE: return "TOKEN_QUOTE"; - case TOKEN_BACKTICK: return "TOKEN_BACKTICK"; - case TOKEN_BACKSLASH: return "TOKEN_BACKSLASH"; - case TOKEN_DASH: return "TOKEN_DASH"; - case TOKEN_UNDERSCORE: return "TOKEN_UNDERSCORE"; - case TOKEN_EXCLAMATION: return "TOKEN_EXCLAMATION"; - case TOKEN_SLASH: return "TOKEN_SLASH"; - case TOKEN_SEMICOLON: return "TOKEN_SEMICOLON"; - case TOKEN_COLON: return "TOKEN_COLON"; - case TOKEN_AT: return "TOKEN_AT"; - case TOKEN_LT: return "TOKEN_LT"; - case TOKEN_PERCENT: return "TOKEN_PERCENT"; - case TOKEN_AMPERSAND: return "TOKEN_AMPERSAND"; - case TOKEN_ERB_START: return "TOKEN_ERB_START"; - case TOKEN_ERB_CONTENT: return "TOKEN_ERB_CONTENT"; - case TOKEN_ERB_END: return "TOKEN_ERB_END"; - case TOKEN_CHARACTER: return "TOKEN_CHARACTER"; - case TOKEN_ERROR: return "TOKEN_ERROR"; - case TOKEN_EOF: return "TOKEN_EOF"; + case TOKEN_WHITESPACE: return hb_string("TOKEN_WHITESPACE"); + case TOKEN_NBSP: return hb_string("TOKEN_NBSP"); + case TOKEN_NEWLINE: return hb_string("TOKEN_NEWLINE"); + case TOKEN_IDENTIFIER: return hb_string("TOKEN_IDENTIFIER"); + case TOKEN_HTML_DOCTYPE: return hb_string("TOKEN_HTML_DOCTYPE"); + case TOKEN_XML_DECLARATION: return hb_string("TOKEN_XML_DECLARATION"); + case TOKEN_XML_DECLARATION_END: return hb_string("TOKEN_XML_DECLARATION_END"); + case TOKEN_CDATA_START: return hb_string("TOKEN_CDATA_START"); + case TOKEN_CDATA_END: return hb_string("TOKEN_CDATA_END"); + case TOKEN_HTML_TAG_START: return hb_string("TOKEN_HTML_TAG_START"); + case TOKEN_HTML_TAG_END: return hb_string("TOKEN_HTML_TAG_END"); + case TOKEN_HTML_TAG_START_CLOSE: return hb_string("TOKEN_HTML_TAG_START_CLOSE"); + case TOKEN_HTML_TAG_SELF_CLOSE: return hb_string("TOKEN_HTML_TAG_SELF_CLOSE"); + case TOKEN_HTML_COMMENT_START: return hb_string("TOKEN_HTML_COMMENT_START"); + case TOKEN_HTML_COMMENT_END: return hb_string("TOKEN_HTML_COMMENT_END"); + case TOKEN_EQUALS: return hb_string("TOKEN_EQUALS"); + case TOKEN_QUOTE: return hb_string("TOKEN_QUOTE"); + case TOKEN_BACKTICK: return hb_string("TOKEN_BACKTICK"); + case TOKEN_BACKSLASH: return hb_string("TOKEN_BACKSLASH"); + case TOKEN_DASH: return hb_string("TOKEN_DASH"); + case TOKEN_UNDERSCORE: return hb_string("TOKEN_UNDERSCORE"); + case TOKEN_EXCLAMATION: return hb_string("TOKEN_EXCLAMATION"); + case TOKEN_SLASH: return hb_string("TOKEN_SLASH"); + case TOKEN_SEMICOLON: return hb_string("TOKEN_SEMICOLON"); + case TOKEN_COLON: return hb_string("TOKEN_COLON"); + case TOKEN_AT: return hb_string("TOKEN_AT"); + case TOKEN_LT: return hb_string("TOKEN_LT"); + case TOKEN_PERCENT: return hb_string("TOKEN_PERCENT"); + case TOKEN_AMPERSAND: return hb_string("TOKEN_AMPERSAND"); + case TOKEN_ERB_START: return hb_string("TOKEN_ERB_START"); + case TOKEN_ERB_CONTENT: return hb_string("TOKEN_ERB_CONTENT"); + case TOKEN_ERB_END: return hb_string("TOKEN_ERB_END"); + case TOKEN_CHARACTER: return hb_string("TOKEN_CHARACTER"); + case TOKEN_ERROR: return hb_string("TOKEN_ERROR"); + case TOKEN_EOF: return hb_string("TOKEN_EOF"); } - return "Unknown token_type_T"; + return hb_string("Unknown token_type_T"); } char* token_to_string(const token_T* token) { - const char* type_string = token_type_to_string(token->type); - const char* template = "#"; + hb_string_T type_string = token_type_to_string(token->type); + hb_string_T template = hb_string("#"); - char* string = calloc(strlen(type_string) + strlen(template) + token->value.length + 16, sizeof(char)); + char* string = calloc(template.length + type_string.length + token->value.length + 16, sizeof(char)); hb_string_T escaped; if (token->type == TOKEN_EOF) { @@ -100,8 +100,9 @@ char* token_to_string(const token_T* token) { sprintf( string, - template, - type_string, + template.data, + type_string.length, + type_string.data, escaped.length, escaped.data, token->range.from, @@ -117,12 +118,8 @@ char* token_to_string(const token_T* token) { return string; } -char* token_value(const token_T* token) { - hb_buffer_T buffer; - hb_buffer_init(&buffer, token->value.length); - hb_buffer_append_string(&buffer, token->value); - - return buffer.value; +hb_string_T token_value(const token_T* token) { + return token->value; } int token_type(const token_T* token) { @@ -149,7 +146,5 @@ token_T* token_copy(token_T* token) { void token_free(token_T* token) { if (!token) { return; } - // if (token->value != NULL) { free(token->value); } - free(token); } From 6c4814d0b257415ad27ba4d83f7500b1a7c415e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20K=C3=A4chele?= Date: Tue, 28 Oct 2025 07:39:18 +0100 Subject: [PATCH 26/44] Use hb_string_T in errors template --- templates/src/errors.c.erb | 5 +---- templates/template.rb | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/templates/src/errors.c.erb b/templates/src/errors.c.erb index b61edee00..6595914e4 100644 --- a/templates/src/errors.c.erb +++ b/templates/src/errors.c.erb @@ -42,10 +42,7 @@ void error_init(ERROR_T* error, const error_type_T type, position_T start, posit if (message) { <%- error.message_arguments.each_with_index do |argument, i| -%> <%- if error.message_template.scan(/%[sdulfz]/)[i] == "%s" -%> - char truncated_argument_<%= i %>[ERROR_MESSAGES_TRUNCATED_LENGTH + 1]; - strncpy(truncated_argument_<%= i %>, <%= argument %>, ERROR_MESSAGES_TRUNCATED_LENGTH); - truncated_argument_<%= i %>[ERROR_MESSAGES_TRUNCATED_LENGTH] = '\0'; - + hb_string_T truncated_argument_<%= i %> = hb_string_truncate(<%= argument %>, ERROR_MESSAGES_TRUNCATED_LENGTH); <%- end -%> <%- end -%> snprintf( diff --git a/templates/template.rb b/templates/template.rb index 749c970db..a651afc22 100755 --- a/templates/template.rb +++ b/templates/template.rb @@ -103,7 +103,7 @@ def ruby_type end def c_type - "const char*" + "hb_string_T" end end From a255eeba0877094d221b5a353694b0a24cc648dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20K=C3=A4chele?= Date: Tue, 28 Oct 2025 07:50:54 +0100 Subject: [PATCH 27/44] Adapt errors call sites to use hb_string_T --- src/include/parser_helpers.h | 4 ++-- src/parser.c | 38 ++++++++++++++++++------------------ src/parser_helpers.c | 9 ++++----- src/prism_helpers.c | 6 +++--- 4 files changed, 28 insertions(+), 29 deletions(-) diff --git a/src/include/parser_helpers.h b/src/include/parser_helpers.h index b3ab98300..ecdd6208a 100644 --- a/src/include/parser_helpers.h +++ b/src/include/parser_helpers.h @@ -15,8 +15,8 @@ token_T* parser_pop_open_tag(const parser_T* parser); void parser_append_unexpected_error( parser_T* parser, - const char* description, - const char* expected, + hb_string_T description, + hb_string_T expected, hb_array_T* errors ); void parser_append_unexpected_token_error(parser_T* parser, token_type_T expected_type, hb_array_T* errors); diff --git a/src/parser.c b/src/parser.c index c76002c79..94f9a48af 100644 --- a/src/parser.c +++ b/src/parser.c @@ -243,8 +243,8 @@ static AST_HTML_TEXT_NODE_T* parser_parse_text_content(parser_T* parser, hb_arra token_T* token = parser_consume_expected(parser, TOKEN_ERROR, document_errors); append_unexpected_error( - "Token Error", - "not TOKEN_ERROR", + hb_string("Token Error"), + hb_string("not TOKEN_ERROR"), token->value, token->location.start, token->location.end, @@ -267,9 +267,9 @@ static AST_HTML_TEXT_NODE_T* parser_parse_text_content(parser_T* parser, hb_arra if (hb_buffer_length(&content) > 0) { text_node = - ast_html_text_node_init(hb_buffer_value(&content), start, parser->current_token->location.start, errors); + ast_html_text_node_init(hb_string(content.value), start, parser->current_token->location.start, errors); } else { - text_node = ast_html_text_node_init("", start, parser->current_token->location.start, errors); + text_node = ast_html_text_node_init(hb_string(""), start, parser->current_token->location.start, errors); } free(content.value); @@ -394,8 +394,8 @@ static AST_HTML_ATTRIBUTE_VALUE_NODE_T* parser_parse_quoted_html_attribute_value if (token_is(parser, TOKEN_IDENTIFIER) || token_is(parser, TOKEN_CHARACTER)) { append_unexpected_error( - "Unescaped quote character in attribute value", - "escaped quote (\\') or different quote style (\")", + hb_string("Unescaped quote character in attribute value"), + hb_string("escaped quote (\\') or different quote style (\")"), opening_quote->value, potential_closing->location.start, potential_closing->location.end, @@ -518,9 +518,9 @@ static AST_HTML_ATTRIBUTE_VALUE_NODE_T* parser_parse_html_attribute_value(parser position_T end = token->location.end; append_unexpected_error( - "Invalid quote character for HTML attribute", - "single quote (') or double quote (\")", - "backtick (`)", + hb_string("Invalid quote character for HTML attribute"), + hb_string("single quote (') or double quote (\")"), + hb_string("backtick (`)"), start, end, errors @@ -535,8 +535,8 @@ static AST_HTML_ATTRIBUTE_VALUE_NODE_T* parser_parse_html_attribute_value(parser } append_unexpected_error( - "Unexpected Token", - "TOKEN_IDENTIFIER, TOKEN_QUOTE, TOKEN_ERB_START", + hb_string("Unexpected Token"), + hb_string("TOKEN_IDENTIFIER, TOKEN_QUOTE, TOKEN_ERB_START"), token_type_to_string(parser->current_token->type), parser->current_token->location.start, parser->current_token->location.end, @@ -799,8 +799,8 @@ static AST_HTML_OPEN_TAG_NODE_T* parser_parse_html_open_tag(parser_T* parser) { parser_append_unexpected_error( parser, - "Unexpected Token", - "TOKEN_IDENTIFIER, TOKEN_AT, TOKEN_ERB_START,TOKEN_WHITESPACE, or TOKEN_NEWLINE", + hb_string("Unexpected Token"), + hb_string("TOKEN_IDENTIFIER, TOKEN_AT, TOKEN_ERB_START,TOKEN_WHITESPACE, or TOKEN_NEWLINE"), errors ); } @@ -863,8 +863,8 @@ static AST_HTML_CLOSE_TAG_NODE_T* parser_parse_html_close_tag(parser_T* parser) append_void_element_closing_tag_error( tag_name, - expected.data, - got.data, + expected, + got, tag_opening->location.start, tag_closing->location.end, errors @@ -974,7 +974,7 @@ static AST_HTML_ELEMENT_NODE_T* parser_parse_html_element(parser_T* parser) { hb_array_T* errors = hb_array_init(8); - parser_append_unexpected_error(parser, "Unknown HTML open tag type", "HTMLOpenTag or HTMLSelfCloseTag", errors); + parser_append_unexpected_error(parser, hb_string("Unknown HTML open tag type"), hb_string("HTMLOpenTag or HTMLSelfCloseTag"), errors); return ast_html_element_node_init( open_tag, @@ -1134,9 +1134,9 @@ static void parser_parse_in_data_state(parser_T* parser, hb_array_T* children, h parser_append_unexpected_error( parser, - "Unexpected token", - "TOKEN_ERB_START, TOKEN_HTML_DOCTYPE, TOKEN_HTML_COMMENT_START, TOKEN_IDENTIFIER, TOKEN_WHITESPACE, " - "TOKEN_NBSP, TOKEN_AT, TOKEN_BACKSLASH, or TOKEN_NEWLINE", + hb_string("Unexpected token"), + hb_string("TOKEN_ERB_START, TOKEN_HTML_DOCTYPE, TOKEN_HTML_COMMENT_START, TOKEN_IDENTIFIER, TOKEN_WHITESPACE, " + "TOKEN_NBSP, TOKEN_AT, TOKEN_BACKSLASH, or TOKEN_NEWLINE"), errors ); } diff --git a/src/parser_helpers.c b/src/parser_helpers.c index a889a678e..3da3bc3b5 100644 --- a/src/parser_helpers.c +++ b/src/parser_helpers.c @@ -94,8 +94,8 @@ void parser_exit_foreign_content(parser_T* parser) { void parser_append_unexpected_error( parser_T* parser, - const char* description, - const char* expected, + hb_string_T description, + hb_string_T expected, hb_array_T* errors ) { token_T* token = parser_advance(parser); @@ -128,10 +128,9 @@ void parser_append_literal_node_from_buffer( hb_array_T* children, position_T start ) { - if (hb_buffer_length(buffer) == 0) { return; } - + if (buffer->length == 0) { return; } AST_LITERAL_NODE_T* literal = - ast_literal_node_init(hb_buffer_value(buffer), start, parser->current_token->location.start, NULL); + ast_literal_node_init(hb_string(buffer->value), start, parser->current_token->location.start, NULL); if (children != NULL) { hb_array_append(children, literal); } hb_buffer_clear(buffer); diff --git a/src/prism_helpers.c b/src/prism_helpers.c index 06ac155b5..22cef65c6 100644 --- a/src/prism_helpers.c +++ b/src/prism_helpers.c @@ -44,9 +44,9 @@ RUBY_PARSE_ERROR_T* ruby_parse_error_from_prism_error( position_T end = position_from_source_with_offset(source, end_offset); return ruby_parse_error_init( - error->message, - pm_diagnostic_id_human(error->diag_id), - pm_error_level_to_string(error->level), + hb_string(error->message), + hb_string(pm_diagnostic_id_human(error->diag_id)), + hb_string(pm_error_level_to_string(error->level)), start, end ); From 29d570409ff0f1f493c2ba73bc53260d5059d2ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20K=C3=A4chele?= Date: Tue, 28 Oct 2025 08:08:35 +0100 Subject: [PATCH 28/44] Stop freeing strings --- templates/src/errors.c.erb | 1 - 1 file changed, 1 deletion(-) diff --git a/templates/src/errors.c.erb b/templates/src/errors.c.erb index 6595914e4..107a9ac86 100644 --- a/templates/src/errors.c.erb +++ b/templates/src/errors.c.erb @@ -134,7 +134,6 @@ static void error_free_<%= error.human %>(<%= error.struct_type %>* <%= error.hu <%- when Herb::Template::SizeTField -%> // size_t is part of struct <%- when Herb::Template::StringField -%> - if (<%= error.human %>-><%= field.name %> != NULL) { free((char*) <%= error.human %>-><%= field.name %>); } <%- else -%> <%= field.inspect %> <%- end -%> From efe44253d821c9a5caae8f15de07e5cd6b7dbfbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20K=C3=A4chele?= Date: Thu, 30 Oct 2025 20:57:00 +0100 Subject: [PATCH 29/44] Revert to malloc in lexer error message --- src/lexer.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lexer.c b/src/lexer.c index 89c6b40f3..aa825257b 100644 --- a/src/lexer.c +++ b/src/lexer.c @@ -58,7 +58,7 @@ void lexer_init(lexer_T* lexer, const char* source) { token_T* lexer_error(lexer_T* lexer, const char* message) { size_t error_message_length = 128; - char* error_message = hb_arena_alloc(lexer->allocator, sizeof(char) * error_message_length); + char* error_message = malloc(sizeof(char) * error_message_length); // hb_arena_alloc(lexer->allocator, sizeof(char) * error_message_length); snprintf( error_message, From e62ab747353d5bee27fd59f5c65b08987cb2b474 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20K=C3=A4chele?= Date: Thu, 30 Oct 2025 20:57:08 +0100 Subject: [PATCH 30/44] Fix test_token.c --- test/c/test_token.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/c/test_token.c b/test/c/test_token.c index 69335ad9c..529ddfcb2 100644 --- a/test/c/test_token.c +++ b/test/c/test_token.c @@ -4,7 +4,7 @@ #include "../../src/include/token.h" TEST(test_token) - ck_assert_str_eq(token_type_to_string(TOKEN_IDENTIFIER), "TOKEN_IDENTIFIER"); + ck_assert(hb_string_equals(token_type_to_string(TOKEN_IDENTIFIER), hb_string("TOKEN_IDENTIFIER"))); END TEST(test_token_to_string) From a569e8838ad00fa62b5b37e648a7e30b43f8a7fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20K=C3=A4chele?= Date: Thu, 30 Oct 2025 20:57:19 +0100 Subject: [PATCH 31/44] Fix issues in generated code --- src/ast_node.c | 2 +- templates/src/ast_nodes.c.erb | 3 +-- templates/src/ast_pretty_print.c.erb | 2 +- templates/src/errors.c.erb | 6 +++--- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/ast_node.c b/src/ast_node.c index 976abfec8..4058c06e2 100644 --- a/src/ast_node.c +++ b/src/ast_node.c @@ -31,7 +31,7 @@ AST_LITERAL_NODE_T* ast_literal_node_init_from_token(const token_T* token) { ast_node_init(&literal->base, AST_LITERAL_NODE, token->location.start, token->location.end, NULL); - literal->content = NULL; // herb_strdup(token->value); + literal->content = token->value; return literal; } diff --git a/templates/src/ast_nodes.c.erb b/templates/src/ast_nodes.c.erb index 19cef5ef8..c00442570 100644 --- a/templates/src/ast_nodes.c.erb +++ b/templates/src/ast_nodes.c.erb @@ -35,7 +35,7 @@ <%- when Herb::Template::PrismNodeField -%> <%= node.human %>-><%= field.name %> = <%= field.name %>; <%- when Herb::Template::StringField -%> - <%= node.human %>-><%= field.name %> = herb_strdup(<%= field.name %>); + <%= node.human %>-><%= field.name %> = <%= field.name %>; <%- when Herb::Template::AnalyzedRubyField -%> <%= node.human %>-><%= field.name %> = <%= field.name %>; <%- when Herb::Template::VoidPointerField -%> @@ -107,7 +107,6 @@ static void ast_free_<%= node.human %>(<%= node.struct_type %>* <%= node.human % hb_array_free(&<%= node.human %>-><%= field.name %>); } <%- when Herb::Template::StringField -%> - if (<%= node.human %>-><%= field.name %> != NULL) { free((char*) <%= node.human %>-><%= field.name %>); } <%- when Herb::Template::PrismNodeField -%> if (<%= node.human %>-><%= field.name %> != NULL) { // The first argument to `pm_node_destroy` is a `pm_parser_t`, but it's currently unused: diff --git a/templates/src/ast_pretty_print.c.erb b/templates/src/ast_pretty_print.c.erb index f245e0224..b4be01ff5 100644 --- a/templates/src/ast_pretty_print.c.erb +++ b/templates/src/ast_pretty_print.c.erb @@ -41,7 +41,7 @@ void ast_pretty_print_node(AST_NODE_T* node, const size_t indent, const size_t r <%- when Herb::Template::ElementSourceField -%> pretty_print_string_property(element_source_to_string(<%= node.human %>-><%= field.name %>), hb_string("<%= field.name %>"), indent, relative_indent, <%= last %>, buffer); <%- when Herb::Template::StringField -%> - pretty_print_string_property(hb_string(<%= node.human %>-><%= field.name %>), hb_string("<%= field.name %>"), indent, relative_indent, <%= last %>, buffer); + pretty_print_string_property(<%= node.human %>-><%= field.name %>, hb_string("<%= field.name %>"), indent, relative_indent, <%= last %>, buffer); <%- when Herb::Template::PrismNodeField -%> pretty_print_string_property(hb_string("<%= field.name %>"), hb_string("<%= field.name %>"), indent, relative_indent, <%= last %>, buffer); <%- when Herb::Template::NodeField -%> diff --git a/templates/src/errors.c.erb b/templates/src/errors.c.erb index 107a9ac86..68ccdd840 100644 --- a/templates/src/errors.c.erb +++ b/templates/src/errors.c.erb @@ -78,7 +78,7 @@ void error_init(ERROR_T* error, const error_type_T type, position_T start, posit <%- when Herb::Template::SizeTField -%> <%= error.human %>-><%= field.name %> = <%= field.name %>; <%- when Herb::Template::StringField -%> - <%= error.human %>-><%= field.name %> = herb_strdup(<%= field.name %>); + <%= error.human %>-><%= field.name %> = <%= field.name %>; <%- else -%> <%= field.inspect %> <%- end -%> @@ -216,11 +216,11 @@ static void error_pretty_print_<%= error.human %>(<%= error.struct_type %>* erro <%- when Herb::Template::TokenField -%> pretty_print_token_property(error-><%= field.name %>, hb_string("<%= field.name %>"), indent, relative_indent, <%= error.fields.length - 1 == index %>, buffer); <%- when Herb::Template::TokenTypeField -%> - pretty_print_property(hb_string(token_type_to_string(error-><%= field.name %>)), hb_string("<%= field.name %>"), indent, relative_indent, <%= error.fields.length - 1 == index %>, buffer); + pretty_print_property(token_type_to_string(error-><%= field.name %>), hb_string("<%= field.name %>"), indent, relative_indent, <%= error.fields.length - 1 == index %>, buffer); <%- when Herb::Template::SizeTField -%> pretty_print_size_t_property(hb_string(error-><%= field.name %>), hb_string("<%= field.name %>"), indent, relative_indent, <%= error.fields.length - 1 == index %>, buffer); <%- when Herb::Template::StringField -%> - pretty_print_quoted_property(hb_string("<%= field.name %>"), hb_string(error-><%= field.name %>), indent, relative_indent, <%= error.fields.length - 1 == index %>, buffer); + pretty_print_quoted_property(hb_string("<%= field.name %>"), error-><%= field.name %>, indent, relative_indent, <%= error.fields.length - 1 == index %>, buffer); <%- else -%> <%= field.inspect %> <%- end -%> From 93d10abf9a48032dad32270950663c562b70cc7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20K=C3=A4chele?= Date: Thu, 30 Oct 2025 21:14:04 +0100 Subject: [PATCH 32/44] Return uint32_t in utf8_char_byte_length --- src/include/utf8.h | 2 +- src/utf8.c | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/include/utf8.h b/src/include/utf8.h index 4c2d6c5da..2ba0b44e0 100644 --- a/src/include/utf8.h +++ b/src/include/utf8.h @@ -5,7 +5,7 @@ #include #include -int utf8_char_byte_length(unsigned char first_byte); +uint32_t utf8_char_byte_length(unsigned char first_byte); uint32_t utf8_sequence_length(hb_string_T value); bool utf8_is_valid_continuation_byte(unsigned char byte); diff --git a/src/utf8.c b/src/utf8.c index f24e7015d..79a8aa5e0 100644 --- a/src/utf8.c +++ b/src/utf8.c @@ -1,12 +1,13 @@ #include "include/utf8.h" #include "include/util/hb_string.h" +#include // UTF-8 byte patterns: // 0xxxxxxx = 1 byte (ASCII) // 110xxxxx = 2 bytes // 1110xxxx = 3 bytes // 11110xxx = 4 bytes -int utf8_char_byte_length(unsigned char first_byte) { +uint32_t utf8_char_byte_length(unsigned char first_byte) { if ((first_byte & 0x80) == 0) { return 1; } else if ((first_byte & 0xE0) == 0xC0) { @@ -28,13 +29,13 @@ bool utf8_is_valid_continuation_byte(unsigned char byte) { uint32_t utf8_sequence_length(hb_string_T value) { if (hb_string_is_empty(value)) { return 0; } - int expected_length = utf8_char_byte_length(value.data[0]); + uint32_t expected_length = utf8_char_byte_length(value.data[0]); if (value.length - expected_length < expected_length) { return 1; // Not enough bytes, treat as single byte } if (expected_length > 1) { - for (int i = 1; i < expected_length; i++) { + for (uint32_t i = 1; i < expected_length; i++) { if (!utf8_is_valid_continuation_byte((unsigned char) value.data[i])) { return 1; // Invalid continuation byte, treat first byte as single byte } From 1039634b10070a0d6c9d9375a459d28bee4fd340 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20K=C3=A4chele?= Date: Thu, 30 Oct 2025 21:23:34 +0100 Subject: [PATCH 33/44] Use truncate method in lexer --- src/lexer.c | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/lexer.c b/src/lexer.c index aa825257b..21234dc8c 100644 --- a/src/lexer.c +++ b/src/lexer.c @@ -119,8 +119,11 @@ static token_T* lexer_advance_with_next(lexer_T* lexer, size_t count, token_type uint32_t end_position = lexer->current_position; + hb_string_T value = hb_string_slice(lexer->source, start_position); + value = hb_string_truncate(value, end_position - start_position); + token_T* token = token_init( - (hb_string_T) { .data = lexer->source.data + start_position, .length = end_position - start_position }, + value, type, lexer ); @@ -140,8 +143,8 @@ static token_T* lexer_advance_utf8_character(lexer_T* lexer, const token_type_T size_t start_position = lexer->current_position; lexer_advance_utf8_bytes(lexer, char_byte_length); - hb_string_T utf8_char = hb_string_slice(lexer->source, lexer->current_position); - utf8_char.length = MIN(char_byte_length, utf8_char.length); + hb_string_T utf8_char = hb_string_slice(lexer->source, start_position); + utf8_char = hb_string_truncate(utf8_char, char_byte_length); return token_init(utf8_char, type, lexer); } @@ -164,7 +167,7 @@ static token_T* lexer_parse_whitespace(lexer_T* lexer) { uint32_t end_position = lexer->current_position; hb_string_T value = hb_string_slice(lexer->source, start_position); - value.length = end_position - start_position; + value = hb_string_truncate(value, end_position - start_position); token_T* token = token_init(value, TOKEN_WHITESPACE, lexer); @@ -182,6 +185,7 @@ static token_T* lexer_parse_identifier(lexer_T* lexer) { uint32_t end_position = lexer->current_position; hb_string_T value = hb_string_slice(lexer->source, start_position); + value = hb_string_truncate(value, end_position - start_position); value.length = end_position - start_position; token_T* token = token_init(value, TOKEN_IDENTIFIER, lexer); @@ -233,7 +237,7 @@ static token_T* lexer_parse_erb_content(lexer_T* lexer) { uint32_t end_position = lexer->current_position; hb_string_T value = hb_string_slice(lexer->source, start_position); - value.length = end_position - start_position; + value = hb_string_truncate(value, end_position - start_position); return token_init(value, TOKEN_ERB_CONTENT, lexer); } From 6e58ee72dce356c46e4cffc02179cc45844fbb4f Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Mon, 2 Mar 2026 02:55:32 +0100 Subject: [PATCH 34/44] Format --- src/analyze/analyze.c | 4 +-- src/analyze/builders.c | 3 ++- src/analyze/conditional_elements.c | 38 +++++++++++++++++------------ src/analyze/conditional_open_tags.c | 11 ++++++--- src/extract.c | 3 +-- src/parser.c | 13 +++++----- src/parser_helpers.c | 7 +++--- src/token.c | 3 ++- 8 files changed, 48 insertions(+), 34 deletions(-) diff --git a/src/analyze/analyze.c b/src/analyze/analyze.c index ac91c38c9..cb023b914 100644 --- a/src/analyze/analyze.c +++ b/src/analyze/analyze.c @@ -64,8 +64,8 @@ static bool analyze_erb_content(const AST_NODE_T* node, void* data) { hb_string_T opening = erb_content_node->tag_opening->value; - if (!hb_string_equals(opening, hb_string("<%%")) && !hb_string_equals(opening, hb_string("<%%=")) && !hb_string_equals(opening, hb_string("<%#")) - && !hb_string_equals(opening, hb_string("<%graphql"))) { + if (!hb_string_equals(opening, hb_string("<%%")) && !hb_string_equals(opening, hb_string("<%%=")) + && !hb_string_equals(opening, hb_string("<%#")) && !hb_string_equals(opening, hb_string("<%graphql"))) { analyzed_ruby_T* analyzed = herb_analyze_ruby(erb_content_node->content->value); erb_content_node->parsed = true; diff --git a/src/analyze/builders.c b/src/analyze/builders.c index a78cc71fc..26f15c71a 100644 --- a/src/analyze/builders.c +++ b/src/analyze/builders.c @@ -29,7 +29,8 @@ location_T* compute_then_keyword(AST_ERB_CONTENT_NODE_T* erb_node, control_type_ } token_T* content = erb_node->content; - char* source = (content && !hb_string_is_empty(content->value)) ? hb_string_to_c_string_using_malloc(content->value) : NULL; + char* source = + (content && !hb_string_is_empty(content->value)) ? hb_string_to_c_string_using_malloc(content->value) : NULL; location_T* then_keyword = NULL; if (control_type == CONTROL_TYPE_WHEN || control_type == CONTROL_TYPE_IN) { diff --git a/src/analyze/conditional_elements.c b/src/analyze/conditional_elements.c index 37dd2cfb6..c75e14d7b 100644 --- a/src/analyze/conditional_elements.c +++ b/src/analyze/conditional_elements.c @@ -34,21 +34,35 @@ static hb_string_T extract_condition_from_erb_content(AST_NODE_T* erb_node, bool const char* data = content_token->value.data; size_t remaining = content_token->value.length; - while (remaining > 0 && is_whitespace(*data)) { data++; remaining--; } + while (remaining > 0 && is_whitespace(*data)) { + data++; + remaining--; + } if (*is_if) { - if (remaining >= 3 && strncmp(data, "if", 2) == 0 && is_whitespace(data[2])) { data += 3; remaining -= 3; } + if (remaining >= 3 && strncmp(data, "if", 2) == 0 && is_whitespace(data[2])) { + data += 3; + remaining -= 3; + } } else { - if (remaining >= 7 && strncmp(data, "unless", 6) == 0 && is_whitespace(data[6])) { data += 7; remaining -= 7; } + if (remaining >= 7 && strncmp(data, "unless", 6) == 0 && is_whitespace(data[6])) { + data += 7; + remaining -= 7; + } } - while (remaining > 0 && is_whitespace(*data)) { data++; remaining--; } + while (remaining > 0 && is_whitespace(*data)) { + data++; + remaining--; + } if (remaining == 0) { return hb_string(""); } - while (remaining > 0 && is_whitespace(data[remaining - 1])) { remaining--; } + while (remaining > 0 && is_whitespace(data[remaining - 1])) { + remaining--; + } if (remaining == 0) { return hb_string(""); } - return (hb_string_T){ .data = (char*) data, .length = (uint32_t) remaining }; + return (hb_string_T) { .data = (char*) data, .length = (uint32_t) remaining }; } static bool is_simple_erb_conditional(AST_NODE_T* node) { @@ -318,16 +332,12 @@ static void rewrite_conditional_elements(hb_array_T* nodes, hb_array_T* document conditional_open_tag_T* entry = (conditional_open_tag_T*) hb_array_get(open_stack, stack_index - 1); if (!entry) { continue; } - if (!hb_string_equals_case_insensitive(entry->tag_name, close_tag->tag_name->value)) { - continue; - } + if (!hb_string_equals_case_insensitive(entry->tag_name, close_tag->tag_name->value)) { continue; } bool close_is_if; hb_string_T close_condition = extract_condition_from_erb_content(node, &close_is_if); - if (entry->is_if != close_is_if) { - continue; - } + if (entry->is_if != close_is_if) { continue; } if (!conditions_are_equivalent(entry->condition, close_condition)) { if (!mismatched_open && entry->open_index < node_index) { @@ -425,9 +435,7 @@ static void rewrite_conditional_elements(hb_array_T* nodes, hb_array_T* document for (size_t stack_index = 0; stack_index < hb_array_size(open_stack); stack_index++) { conditional_open_tag_T* entry = (conditional_open_tag_T*) hb_array_get(open_stack, stack_index); - if (entry) { - free(entry); - } + if (entry) { free(entry); } } hb_array_free(&open_stack); diff --git a/src/analyze/conditional_open_tags.c b/src/analyze/conditional_open_tags.c index 87189800a..a0c4dbab5 100644 --- a/src/analyze/conditional_open_tags.c +++ b/src/analyze/conditional_open_tags.c @@ -97,7 +97,8 @@ static single_open_tag_result_T get_single_open_tag_from_statements(hb_array_T* if (result.tag) { hb_string_T tag_name = get_open_tag_name(result.tag); - if (!hb_string_is_empty(tag_name) && has_matching_close_tag_in_statements(statements, first_tag_index, tag_name)) { + if (!hb_string_is_empty(tag_name) + && has_matching_close_tag_in_statements(statements, first_tag_index, tag_name)) { result.tag = NULL; result.has_multiple_tags = false; result.second_tag = NULL; @@ -127,8 +128,8 @@ static single_open_tag_result_T get_single_open_tag_from_statements(hb_array_T* if (result.has_multiple_tags && result.second_tag) { hb_string_T first_tag_name = get_open_tag_name((AST_HTML_OPEN_TAG_NODE_T*) hb_array_get(statements, first_tag_index)); - bool first_has_close = - !hb_string_is_empty(first_tag_name) && has_matching_close_tag_in_statements(statements, first_tag_index, first_tag_name); + bool first_has_close = !hb_string_is_empty(first_tag_name) + && has_matching_close_tag_in_statements(statements, first_tag_index, first_tag_name); if (first_has_close) { result.has_multiple_tags = false; @@ -140,7 +141,9 @@ static single_open_tag_result_T get_single_open_tag_from_statements(hb_array_T* if (result.tag) { hb_string_T tag_name = get_open_tag_name(result.tag); - if (!hb_string_is_empty(tag_name) && has_matching_close_tag_in_statements(statements, first_tag_index, tag_name)) { result.tag = NULL; } + if (!hb_string_is_empty(tag_name) && has_matching_close_tag_in_statements(statements, first_tag_index, tag_name)) { + result.tag = NULL; + } } return result; diff --git a/src/extract.c b/src/extract.c index 4a1266fa4..c6ed5a529 100644 --- a/src/extract.c +++ b/src/extract.c @@ -57,8 +57,7 @@ void herb_extract_ruby_to_buffer_with_options( is_comment_tag = true; if (extract_options.preserve_positions) { hb_buffer_append_whitespace(output, range_length(token->range)); } } - } else if (hb_string_equals(token->value, hb_string("<%%")) - || hb_string_equals(token->value, hb_string("<%%=")) + } else if (hb_string_equals(token->value, hb_string("<%%")) || hb_string_equals(token->value, hb_string("<%%=")) || hb_string_equals(token->value, hb_string("<%graphql"))) { skip_erb_content = true; is_comment_tag = false; diff --git a/src/parser.c b/src/parser.c index d9db84c6c..533e0f508 100644 --- a/src/parser.c +++ b/src/parser.c @@ -300,8 +300,7 @@ static AST_HTML_TEXT_NODE_T* parser_parse_text_content(parser_T* parser, hb_arra AST_HTML_TEXT_NODE_T* text_node = NULL; if (hb_buffer_length(&content) > 0) { - text_node = - ast_html_text_node_init(hb_string(content.value), start, parser->current_token->location.start, errors); + text_node = ast_html_text_node_init(hb_string(content.value), start, parser->current_token->location.start, errors); } else { text_node = ast_html_text_node_init(hb_string(""), start, parser->current_token->location.start, errors); free(content.value); @@ -911,7 +910,10 @@ static bool parser_lookahead_erb_is_control_flow(parser_T* parser) { const char* data = content->value.data; size_t remaining = content->value.length; - while (remaining > 0 && is_whitespace(*data)) { data++; remaining--; } + while (remaining > 0 && is_whitespace(*data)) { + data++; + remaining--; + } bool is_control_flow = starts_with_keyword(data, remaining, "end") || starts_with_keyword(data, remaining, "else") || starts_with_keyword(data, remaining, "elsif") || starts_with_keyword(data, remaining, "in") @@ -925,7 +927,7 @@ static bool parser_lookahead_erb_is_control_flow(parser_T* parser) { static void parser_handle_erb_in_open_tag(parser_T* parser, hb_array_T* children) { bool is_output_tag = !hb_string_is_empty(parser->current_token->value) - && hb_string_starts_with(parser->current_token->value, hb_string("<%=")); + && hb_string_starts_with(parser->current_token->value, hb_string("<%=")); if (!is_output_tag) { hb_array_append(children, parser_parse_erb_tag(parser)); @@ -1360,8 +1362,7 @@ static void parser_parse_foreign_content(parser_T* parser, hb_array_T* children, bool is_potential_match = false; if (next_token && next_token->type == TOKEN_IDENTIFIER && !hb_string_is_empty(next_token->value)) { - is_potential_match = - parser_is_expected_closing_tag_name(next_token->value, parser->foreign_content_type); + is_potential_match = parser_is_expected_closing_tag_name(next_token->value, parser->foreign_content_type); } lexer_restore_state(parser->lexer, saved_state); diff --git a/src/parser_helpers.c b/src/parser_helpers.c index 8b547754f..2e70aade2 100644 --- a/src/parser_helpers.c +++ b/src/parser_helpers.c @@ -157,8 +157,7 @@ void parser_append_literal_node_from_buffer( hb_string_T content = { .data = buffer->value, .length = (uint32_t) buffer->length }; - AST_LITERAL_NODE_T* literal = - ast_literal_node_init(content, start, parser->current_token->location.start, NULL); + AST_LITERAL_NODE_T* literal = ast_literal_node_init(content, start, parser->current_token->location.start, NULL); if (children != NULL) { hb_array_append(children, literal); } @@ -269,7 +268,9 @@ bool parser_can_close_ancestor(const parser_T* parser, hb_string_T tag_name) { for (size_t i = stack_size; i > 0; i--) { token_T* open = hb_array_get(parser->open_tags_stack, i - 1); - if (open && !hb_string_is_empty(open->value) && hb_string_equals_case_insensitive(open->value, tag_name)) { return true; } + if (open && !hb_string_is_empty(open->value) && hb_string_equals_case_insensitive(open->value, tag_name)) { + return true; + } } return false; diff --git a/src/token.c b/src/token.c index b692d01ee..5421a3899 100644 --- a/src/token.c +++ b/src/token.c @@ -165,7 +165,8 @@ char* token_types_to_friendly_string_va(token_type_T first_token, ...) { hb_string_T token_to_string(const token_T* token) { hb_string_T type_string = token_type_to_string(token->type); - hb_string_T template = hb_string("#"); + hb_string_T template = + hb_string("#"); char* string = calloc(template.length + type_string.length + token->value.length + 16, sizeof(char)); From e0343b5a238adb95ce27c8139836c5f14814f8e4 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Mon, 2 Mar 2026 03:18:46 +0100 Subject: [PATCH 35/44] Handle `hb_string` in bindings --- java/extension_helpers.c | 17 +++++++++-- java/extension_helpers.h | 2 ++ .../node/extension/extension_helpers.cpp | 27 +++++------------ rust/src/convert.rs | 29 ++++++++++++------- templates/java/nodes.c.erb | 2 +- .../node/extension/error_helpers.cpp.erb | 4 +-- templates/rust/src/ast/nodes.rs.erb | 14 ++++----- templates/wasm/error_helpers.cpp.erb | 4 +-- wasm/extension_helpers.cpp | 9 ++---- 9 files changed, 55 insertions(+), 53 deletions(-) diff --git a/java/extension_helpers.c b/java/extension_helpers.c index 7ad963410..a06c19144 100644 --- a/java/extension_helpers.c +++ b/java/extension_helpers.c @@ -10,11 +10,24 @@ #include "../../src/include/range.h" #include "../../src/include/token.h" #include "../../src/include/util/hb_array.h" +#include "../../src/include/util/hb_string.h" #include #include #include +jstring CreateStringFromHbString(JNIEnv* env, hb_string_T string) { + if (hb_string_is_empty(string)) { return NULL; } + + char* cstr = hb_string_to_c_string_using_malloc(string); + if (!cstr) { return NULL; } + + jstring result = (*env)->NewStringUTF(env, cstr); + free(cstr); + + return result; +} + jobject CreatePosition(JNIEnv* env, position_T position) { jclass positionClass = (*env)->FindClass(env, "org/herb/Position"); jmethodID constructor = (*env)->GetMethodID(env, positionClass, "", "(II)V"); @@ -47,8 +60,8 @@ jobject CreateToken(JNIEnv* env, token_T* token) { jmethodID constructor = (*env)->GetMethodID( env, tokenClass, "", "(Ljava/lang/String;Ljava/lang/String;Lorg/herb/Location;Lorg/herb/Range;)V"); - jstring type = (*env)->NewStringUTF(env, token_type_to_string(token->type)); - jstring value = (*env)->NewStringUTF(env, token->value); + jstring type = CreateStringFromHbString(env, token_type_to_string(token->type)); + jstring value = CreateStringFromHbString(env, token->value); jobject location = CreateLocation(env, token->location); jobject range = CreateRange(env, token->range); diff --git a/java/extension_helpers.h b/java/extension_helpers.h index 536535040..4c7359cc2 100644 --- a/java/extension_helpers.h +++ b/java/extension_helpers.h @@ -9,11 +9,13 @@ #include "../../src/include/range.h" #include "../../src/include/token.h" #include "../../src/include/util/hb_array.h" +#include "../../src/include/util/hb_string.h" #ifdef __cplusplus extern "C" { #endif +jstring CreateStringFromHbString(JNIEnv* env, hb_string_T string); jobject CreatePosition(JNIEnv* env, position_T position); jobject CreateLocation(JNIEnv* env, location_T location); jobject CreateRange(JNIEnv* env, range_T range); diff --git a/javascript/packages/node/extension/extension_helpers.cpp b/javascript/packages/node/extension/extension_helpers.cpp index 56435e38c..fc720a4ef 100644 --- a/javascript/packages/node/extension/extension_helpers.cpp +++ b/javascript/packages/node/extension/extension_helpers.cpp @@ -104,26 +104,14 @@ napi_value CreateToken(napi_env env, token_T* token) { napi_value result; napi_create_object(env, &result); - // Value - napi_value value = token->value ? CreateString(env, token->value) : nullptr; - if (value) { - napi_set_named_property(env, result, "value", value); - } else { - napi_value null_value; - napi_get_null(env, &null_value); - napi_set_named_property(env, result, "value", null_value); - } - - // Range + napi_value value = CreateStringFromHbString(env, token->value); napi_value range = CreateRange(env, token->range); - napi_set_named_property(env, result, "range", range); - - // Location napi_value location = CreateLocation(env, token->location); - napi_set_named_property(env, result, "location", location); + napi_value type = CreateStringFromHbString(env, token_type_to_string(token->type)); - // Type - napi_value type = CreateString(env, token_type_to_string(token->type)); + napi_set_named_property(env, result, "value", value); + napi_set_named_property(env, result, "range", range); + napi_set_named_property(env, result, "location", location); napi_set_named_property(env, result, "type", type); return result; @@ -151,10 +139,10 @@ napi_value CreateLexResult(napi_env env, hb_array_T* tokens, napi_value source) napi_create_array(env, &errors_array); napi_create_array(env, &warnings_array); - // Add tokens to array if (tokens) { for (size_t i = 0; i < hb_array_size(tokens); i++) { token_T* token = (token_T*)hb_array_get(tokens, i); + if (token) { napi_value token_obj = CreateToken(env, token); napi_set_element(env, tokens_array, i, token_obj); @@ -177,8 +165,8 @@ napi_value CreateParseResult(napi_env env, AST_DOCUMENT_NODE_T* root, napi_value napi_create_array(env, &errors_array); napi_create_array(env, &warnings_array); - // Convert the AST to a JavaScript object napi_value ast_value; + if (root) { ast_value = NodeFromCStruct(env, (AST_NODE_T*)root); } else { @@ -201,7 +189,6 @@ napi_value CreateParseResult(napi_env env, AST_DOCUMENT_NODE_T* root, napi_value napi_set_named_property(env, options_object, "strict", strict_value); napi_set_named_property(env, options_object, "track_whitespace", track_whitespace_value); napi_set_named_property(env, options_object, "analyze", analyze_value); - napi_set_named_property(env, result, "options", options_object); return result; diff --git a/rust/src/convert.rs b/rust/src/convert.rs index f551313fc..6847f461f 100644 --- a/rust/src/convert.rs +++ b/rust/src/convert.rs @@ -1,6 +1,5 @@ -use crate::bindings::{location_T, position_T, range_T, token_T}; +use crate::bindings::{hb_string_T, location_T, position_T, range_T, token_T}; use crate::{Location, Position, Range, Token}; -use std::ffi::CStr; impl From for Position { fn from(c_position: position_T) -> Self { @@ -20,22 +19,32 @@ impl From for Location { } } +/// Converts an `hb_string_T` to a Rust `String`. +/// +/// # Safety +/// +/// The caller must ensure that `hb_string.data` points to valid memory of at least +/// `hb_string.length` bytes, or is null. +pub unsafe fn string_from_hb_string(hb_string: hb_string_T) -> String { + if hb_string.data.is_null() || hb_string.length == 0 { + String::new() + } else { + let slice = std::slice::from_raw_parts(hb_string.data as *const u8, hb_string.length as usize); + String::from_utf8_lossy(slice).into_owned() + } +} + /// Converts a C token pointer to a Rust Token. /// /// # Safety /// /// The caller must ensure that `token_ptr` is a valid, non-null pointer to a `token_T` -/// and that the token's string fields (`value`, `token_type`) point to valid C strings. +/// and that the token's string fields point to valid memory. pub unsafe fn token_from_c(token_ptr: *const token_T) -> Token { let token = &*token_ptr; - let value = if token.value.is_null() { - String::new() - } else { - CStr::from_ptr(token.value).to_string_lossy().into_owned() - }; - - let token_type = CStr::from_ptr(crate::ffi::token_type_to_string(token.type_)).to_string_lossy().into_owned(); + let value = string_from_hb_string(token.value); + let token_type = string_from_hb_string(crate::ffi::token_type_to_string(token.type_)); Token { token_type, diff --git a/templates/java/nodes.c.erb b/templates/java/nodes.c.erb index bb0ad80af..cc7f3da0a 100644 --- a/templates/java/nodes.c.erb +++ b/templates/java/nodes.c.erb @@ -25,7 +25,7 @@ jobject <%= node.name %>FromCStruct(JNIEnv* env, <%= node.struct_type %>* <%= no <%- node.fields.each do |field| -%> <%- if field.is_a?(Herb::Template::StringField) -%> - jstring <%= field.name %> = (*env)->NewStringUTF(env, <%= node.human %>-><%= field.name %>); + jstring <%= field.name %> = CreateStringFromHbString(env, <%= node.human %>-><%= field.name %>); <%- elsif field.is_a?(Herb::Template::TokenField) -%> jobject <%= field.name %> = <%= node.human %>-><%= field.name %> ? CreateToken(env, <%= node.human %>-><%= field.name %>) : NULL; <%- elsif field.is_a?(Herb::Template::BooleanField) -%> diff --git a/templates/javascript/packages/node/extension/error_helpers.cpp.erb b/templates/javascript/packages/node/extension/error_helpers.cpp.erb index ae257bfae..4997b24b1 100644 --- a/templates/javascript/packages/node/extension/error_helpers.cpp.erb +++ b/templates/javascript/packages/node/extension/error_helpers.cpp.erb @@ -38,7 +38,7 @@ napi_value <%= error.name %>FromCStruct(napi_env env, <%= error.struct_type %>* <%- error.fields.each do |field| -%> <%- case field -%> <%- when Herb::Template::StringField -%> - napi_value <%= field.name %> = CreateString(env, <%= error.human %>-><%= field.name %>); + napi_value <%= field.name %> = CreateStringFromHbString(env, <%= error.human %>-><%= field.name %>); napi_set_named_property(env, result, "<%= field.name %>", <%= field.name %>); <%- when Herb::Template::NodeField -%> @@ -50,7 +50,7 @@ napi_value <%= error.name %>FromCStruct(napi_env env, <%= error.struct_type %>* napi_set_named_property(env, result, "<%= field.name %>", <%= field.name %>); <%- when Herb::Template::TokenTypeField -%> - napi_value <%= field.name %> = CreateString(env, token_type_to_string(<%= error.human %>-><%= field.name %>)); + napi_value <%= field.name %> = CreateStringFromHbString(env, token_type_to_string(<%= error.human %>-><%= field.name %>)); napi_set_named_property(env, result, "<%= field.name %>", <%= field.name %>); <%- when Herb::Template::BooleanField -%> diff --git a/templates/rust/src/ast/nodes.rs.erb b/templates/rust/src/ast/nodes.rs.erb index 5709842cf..ad481c1a3 100644 --- a/templates/rust/src/ast/nodes.rs.erb +++ b/templates/rust/src/ast/nodes.rs.erb @@ -1,11 +1,11 @@ use crate::bindings::*; -use crate::convert::token_from_c; +use crate::convert::{token_from_c, string_from_hb_string}; use crate::errors::*; use crate::nodes::*; use crate::union_types::*; use crate::{Location, Position}; use std::ffi::CStr; -use std::os::raw::{c_char, c_void}; +use std::os::raw::c_void; unsafe fn convert_location(c_location: location_T) -> Location { Location::new( @@ -42,7 +42,7 @@ unsafe fn convert_<%= snake_name %>(error_ptr: *const <%= error.c_type %>) -> <% if error_ref.<%= field.name %> == u32::MAX { None } else { - Some(CStr::from_ptr(crate::ffi::token_type_to_string(error_ref.<%= field.name %>)).to_string_lossy().into_owned()) + Some(string_from_hb_string(crate::ffi::token_type_to_string(error_ref.<%= field.name %>))) }, <%- when Herb::Template::SizeTField -%> error_ref.<%= field.name %>, @@ -88,12 +88,8 @@ unsafe fn convert_errors(errors_array: *mut hb_array_T) -> Vec { errors } -unsafe fn get_string_field(string_ptr: *const c_char) -> String { - if string_ptr.is_null() { - String::new() - } else { - CStr::from_ptr(string_ptr).to_string_lossy().into_owned() - } +unsafe fn get_string_field(hb_string: hb_string_T) -> String { + string_from_hb_string(hb_string) } unsafe fn convert_element_source(source: u32) -> String { diff --git a/templates/wasm/error_helpers.cpp.erb b/templates/wasm/error_helpers.cpp.erb index fbd9f0e53..f9ca9c4e5 100644 --- a/templates/wasm/error_helpers.cpp.erb +++ b/templates/wasm/error_helpers.cpp.erb @@ -40,13 +40,13 @@ val <%= error.name %>FromCStruct(<%= error.struct_type %>* <%= error.human %>) { <%- error.fields.each do |field| -%> <%- case field -%> <%- when Herb::Template::StringField -%> - result.set("<%= field.name %>", CreateString(<%= error.human %>-><%= field.name %>)); + result.set("<%= field.name %>", CreateStringFromHbString(<%= error.human %>-><%= field.name %>)); <%- when Herb::Template::NodeField -%> result.set("<%= field.name %>", NodeFromCStruct((AST_NODE_T*)<%= error.human %>-><%= field.name %>)); <%- when Herb::Template::TokenField -%> result.set("<%= field.name %>", CreateToken(<%= error.human %>-><%= field.name %>)); <%- when Herb::Template::TokenTypeField -%> - result.set("<%= field.name %>", CreateString(token_type_to_string(<%= error.human %>-><%= field.name %>))); + result.set("<%= field.name %>", CreateStringFromHbString(token_type_to_string(<%= error.human %>-><%= field.name %>))); <%- when Herb::Template::BooleanField -%> result.set("<%= field.name %>", <%= error.human %>-><%= field.name %>); <%- when Herb::Template::PositionField -%> diff --git a/wasm/extension_helpers.cpp b/wasm/extension_helpers.cpp index 1d20ec656..5a2124325 100644 --- a/wasm/extension_helpers.cpp +++ b/wasm/extension_helpers.cpp @@ -76,13 +76,8 @@ val CreateToken(token_T* token) { val Object = val::global("Object"); val result = Object.new_(); - if (token->value) { - result.set("value", std::string(token->value)); - } else { - result.set("value", val::null()); - } - - result.set("type", std::string(token_type_to_string(token->type))); + result.set("value", CreateStringFromHbString(token->value)); + result.set("type", CreateStringFromHbString(token_type_to_string(token->type))); result.set("range", CreateRange(token->range)); result.set("location", CreateLocation(token->location)); From b3ec9b25e0317828e00f91e8a86ccfc043ce1c9c Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Mon, 2 Mar 2026 03:22:25 +0100 Subject: [PATCH 36/44] Also handle it in nodes.cpp --- templates/javascript/packages/node/extension/nodes.cpp.erb | 2 +- templates/wasm/nodes.cpp.erb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/javascript/packages/node/extension/nodes.cpp.erb b/templates/javascript/packages/node/extension/nodes.cpp.erb index d212e6a75..b82ef22bb 100644 --- a/templates/javascript/packages/node/extension/nodes.cpp.erb +++ b/templates/javascript/packages/node/extension/nodes.cpp.erb @@ -37,7 +37,7 @@ napi_value <%= node.human %>NodeFromCStruct(napi_env env, <%= node.struct_type % <%- node.fields.each do |field| -%> <%- case field -%> <%- when Herb::Template::StringField -%> - napi_value <%= field.name %> = CreateString(env, <%= node.human %>-><%= field.name %>); + napi_value <%= field.name %> = CreateStringFromHbString(env, <%= node.human %>-><%= field.name %>); napi_set_named_property(env, result, "<%= field.name %>", <%= field.name %>); <%- when Herb::Template::NodeField, Herb::Template::BorrowedNodeField -%> diff --git a/templates/wasm/nodes.cpp.erb b/templates/wasm/nodes.cpp.erb index 09cd57b09..c2e9809cb 100644 --- a/templates/wasm/nodes.cpp.erb +++ b/templates/wasm/nodes.cpp.erb @@ -28,7 +28,7 @@ val <%= node.name %>FromCStruct(<%= node.struct_type %>* <%= node.human %>) { <%- node.fields.each do |field| -%> <%- case field -%> <%- when Herb::Template::StringField -%> - result.set("<%= field.name %>", CreateString(<%= node.human %>-><%= field.name %>)); + result.set("<%= field.name %>", CreateStringFromHbString(<%= node.human %>-><%= field.name %>)); <%- when Herb::Template::NodeField, Herb::Template::BorrowedNodeField -%> result.set("<%= field.name %>", NodeFromCStruct((AST_NODE_T*) <%= node.human %>-><%= field.name %>)); <%- when Herb::Template::TokenField -%> From 3e47f214b950881eaa6eaaf3a51d56497700e645 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Mon, 2 Mar 2026 03:37:43 +0100 Subject: [PATCH 37/44] Fix `lex_file` --- ext/herb/extension.c | 3 ++- javascript/packages/node/extension/herb.cpp | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/ext/herb/extension.c b/ext/herb/extension.c index faf2ffcf9..d627f90ac 100644 --- a/ext/herb/extension.c +++ b/ext/herb/extension.c @@ -84,7 +84,8 @@ static VALUE Herb_lex_file(VALUE self, VALUE path) { char* file_path = (char*) check_string(path); VALUE source_value = read_file_to_ruby_string(file_path); - lex_args_T args = { .tokens = herb_lex_file(file_path), .source = source_value }; + char* source = (char*) check_string(source_value); + lex_args_T args = { .tokens = herb_lex(source), .source = source_value }; return rb_ensure(lex_convert_body, (VALUE) &args, lex_cleanup, (VALUE) &args); } diff --git a/javascript/packages/node/extension/herb.cpp b/javascript/packages/node/extension/herb.cpp index efebd37fd..2d8137567 100644 --- a/javascript/packages/node/extension/herb.cpp +++ b/javascript/packages/node/extension/herb.cpp @@ -53,12 +53,20 @@ napi_value Herb_lex_file(napi_env env, napi_callback_info info) { char* file_path = CheckString(env, args[0]); if (!file_path) { return nullptr; } - hb_array_T* tokens = herb_lex_file(file_path); napi_value source_value = ReadFileToString(env, file_path); + + char* source = CheckString(env, source_value); + if (!source) { + free(file_path); + return nullptr; + } + + hb_array_T* tokens = herb_lex(source); napi_value result = CreateLexResult(env, tokens, source_value); herb_free_tokens(&tokens); free(file_path); + free(source); return result; } From f410a5a670028c17c456298846934ae70c03f40b Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Mon, 2 Mar 2026 03:59:31 +0100 Subject: [PATCH 38/44] Fix memory leaks --- src/ast_node.c | 3 ++- src/include/util/hb_string.h | 5 +++-- src/parser.c | 3 ++- src/parser_helpers.c | 1 + src/util/hb_string.c | 10 ++++++++++ templates/src/ast_nodes.c.erb | 4 +++- 6 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/ast_node.c b/src/ast_node.c index de1d5f622..742a3c6c4 100644 --- a/src/ast_node.c +++ b/src/ast_node.c @@ -3,6 +3,7 @@ #include "include/errors.h" #include "include/position.h" #include "include/util.h" +#include "include/util/hb_string.h" #include "include/visitor.h" #include @@ -34,7 +35,7 @@ AST_LITERAL_NODE_T* ast_literal_node_init_from_token(const token_T* token) { ast_node_init(&literal->base, AST_LITERAL_NODE, token->location.start, token->location.end, NULL); - literal->content = token->value; + literal->content = hb_string_copy(token->value); return literal; } diff --git a/src/include/util/hb_string.h b/src/include/util/hb_string.h index 7b5d83c3c..708cb9624 100644 --- a/src/include/util/hb_string.h +++ b/src/include/util/hb_string.h @@ -14,16 +14,17 @@ typedef struct HB_STRING_STRUCT { hb_string_T hb_string(const char* null_terminated_c_string); hb_string_T hb_string_slice(hb_string_T string, uint32_t offset); + bool hb_string_equals(hb_string_T a, hb_string_T b); bool hb_string_equals_case_insensitive(hb_string_T a, hb_string_T b); bool hb_string_starts_with(hb_string_T string, hb_string_T expected_prefix); bool hb_string_is_empty(hb_string_T string); -hb_string_T hb_string_truncate(hb_string_T string, uint32_t max_length); +hb_string_T hb_string_truncate(hb_string_T string, uint32_t max_length); hb_string_T hb_string_range(hb_string_T string, uint32_t from, uint32_t to); +hb_string_T hb_string_copy(hb_string_T string); char* hb_string_to_c_string_using_malloc(hb_string_T string); - char* hb_string_to_c_string(hb_arena_T* allocator, hb_string_T string); #endif diff --git a/src/parser.c b/src/parser.c index 533e0f508..c0b0ddf20 100644 --- a/src/parser.c +++ b/src/parser.c @@ -303,9 +303,10 @@ static AST_HTML_TEXT_NODE_T* parser_parse_text_content(parser_T* parser, hb_arra text_node = ast_html_text_node_init(hb_string(content.value), start, parser->current_token->location.start, errors); } else { text_node = ast_html_text_node_init(hb_string(""), start, parser->current_token->location.start, errors); - free(content.value); } + free(content.value); + return text_node; } diff --git a/src/parser_helpers.c b/src/parser_helpers.c index 2e70aade2..1fcf65c6f 100644 --- a/src/parser_helpers.c +++ b/src/parser_helpers.c @@ -161,6 +161,7 @@ void parser_append_literal_node_from_buffer( if (children != NULL) { hb_array_append(children, literal); } + free(buffer->value); hb_buffer_init(buffer, 128); } diff --git a/src/util/hb_string.c b/src/util/hb_string.c index 3795eb67b..eb74bea98 100644 --- a/src/util/hb_string.c +++ b/src/util/hb_string.c @@ -62,6 +62,16 @@ hb_string_T hb_string_range(hb_string_T string, uint32_t from, uint32_t to) { return hb_string_truncate(hb_string_slice(string, from), to - from); } +hb_string_T hb_string_copy(hb_string_T string) { + if (hb_string_is_empty(string)) { + return (hb_string_T){ .data = NULL, .length = 0 }; + } + + char* copy = hb_string_to_c_string_using_malloc(string); + + return (hb_string_T){ .data = copy, .length = string.length }; +} + char* hb_string_to_c_string_using_malloc(hb_string_T string) { size_t string_length_in_bytes = sizeof(char) * (string.length); char* buffer = malloc(string_length_in_bytes + sizeof(char) * 1); diff --git a/templates/src/ast_nodes.c.erb b/templates/src/ast_nodes.c.erb index 8ad81d7f9..c0c6f3c62 100644 --- a/templates/src/ast_nodes.c.erb +++ b/templates/src/ast_nodes.c.erb @@ -10,6 +10,7 @@ #include "include/token.h" #include "include/util.h" #include "include/util/hb_array.h" +#include "include/util/hb_string.h" <%- nodes.each do |node| -%> <%- node_arguments = node.fields.any? ? node.fields.map { |field| [field.c_type, " ", field.name].join } : [] -%> @@ -37,7 +38,7 @@ <%- when Herb::Template::PrismNodeField -%> <%= node.human %>-><%= field.name %> = <%= field.name %>; <%- when Herb::Template::StringField -%> - <%= node.human %>-><%= field.name %> = <%= field.name %>; + <%= node.human %>-><%= field.name %> = hb_string_copy(<%= field.name %>); <%- when Herb::Template::AnalyzedRubyField -%> <%= node.human %>-><%= field.name %> = <%= field.name %>; <%- when Herb::Template::VoidPointerField -%> @@ -113,6 +114,7 @@ static void ast_free_<%= node.human %>(<%= node.struct_type %>* <%= node.human % hb_array_free(&<%= node.human %>-><%= field.name %>); } <%- when Herb::Template::StringField -%> + if (<%= node.human %>-><%= field.name %>.data != NULL) { free(<%= node.human %>-><%= field.name %>.data); } <%- when Herb::Template::PrismNodeField -%> if (<%= node.human %>-><%= field.name %> != NULL) { // The first argument to `pm_node_destroy` is a `pm_parser_t`, but it's currently unused: From 0e0b60ac41514278b40798302d9a7e318274d8d8 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Mon, 2 Mar 2026 04:01:16 +0100 Subject: [PATCH 39/44] Format --- src/util/hb_string.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/util/hb_string.c b/src/util/hb_string.c index eb74bea98..f01c411eb 100644 --- a/src/util/hb_string.c +++ b/src/util/hb_string.c @@ -63,13 +63,11 @@ hb_string_T hb_string_range(hb_string_T string, uint32_t from, uint32_t to) { } hb_string_T hb_string_copy(hb_string_T string) { - if (hb_string_is_empty(string)) { - return (hb_string_T){ .data = NULL, .length = 0 }; - } + if (hb_string_is_empty(string)) { return (hb_string_T) { .data = NULL, .length = 0 }; } char* copy = hb_string_to_c_string_using_malloc(string); - return (hb_string_T){ .data = copy, .length = string.length }; + return (hb_string_T) { .data = copy, .length = string.length }; } char* hb_string_to_c_string_using_malloc(hb_string_T string) { From 2864aa2a3863fba917d61243afcdbe16f1115124 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Mon, 2 Mar 2026 05:51:02 +0100 Subject: [PATCH 40/44] Fix UTF8 double conversation with emscripten 5 and val::u8string --- ext/herb/extension_helpers.c | 11 +- ext/herb/extension_helpers.h | 1 + java/extension_helpers.c | 2 +- javascript/packages/core/src/util.ts | 12 - .../__snapshots__/utf8-strings.test.ts.snap | 432 ++++++++++++++++++ .../node-wasm/test/utf8-strings.test.ts | 90 ++++ .../node/extension/extension_helpers.cpp | 7 + .../__snapshots__/utf8-strings.test.ts.snap | 432 ++++++++++++++++++ .../packages/node/test/utf8-strings.test.ts | 90 ++++ javascript/packages/printer/src/printer.ts | 6 +- src/include/util/hb_string.h | 1 + src/util/hb_string.c | 6 + templates/ext/herb/nodes.c.erb | 11 +- .../javascript/packages/core/src/nodes.ts.erb | 3 +- wasm/Makefile | 1 + wasm/extension_helpers.cpp | 12 +- 16 files changed, 1083 insertions(+), 34 deletions(-) create mode 100644 javascript/packages/node-wasm/test/__snapshots__/utf8-strings.test.ts.snap create mode 100644 javascript/packages/node-wasm/test/utf8-strings.test.ts create mode 100644 javascript/packages/node/test/__snapshots__/utf8-strings.test.ts.snap create mode 100644 javascript/packages/node/test/utf8-strings.test.ts diff --git a/ext/herb/extension_helpers.c b/ext/herb/extension_helpers.c index ea5db810d..0257b3810 100644 --- a/ext/herb/extension_helpers.c +++ b/ext/herb/extension_helpers.c @@ -45,15 +45,20 @@ VALUE rb_range_from_c_struct(range_T range) { return rb_class_new_instance(2, args, cRange); } +VALUE rb_string_from_hb_string(hb_string_T string) { + if (hb_string_is_null(string)) { return Qnil; } + + return rb_utf8_str_new(string.data, string.length); +} + VALUE rb_token_from_c_struct(token_T* token) { if (!token) { return Qnil; } - VALUE value = hb_string_is_empty(token->value) ? Qnil : rb_utf8_str_new(token->value.data, token->value.length); + VALUE value = rb_string_from_hb_string(token->value); VALUE range = rb_range_from_c_struct(token->range); VALUE location = rb_location_from_c_struct(token->location); - hb_string_T type_string = token_type_to_string(token->type); - VALUE type = rb_utf8_str_new(type_string.data, type_string.length); + VALUE type = rb_string_from_hb_string(token_type_to_string(token->type)); VALUE args[4] = { value, range, location, type }; diff --git a/ext/herb/extension_helpers.h b/ext/herb/extension_helpers.h index b0a74f212..d824812f7 100644 --- a/ext/herb/extension_helpers.h +++ b/ext/herb/extension_helpers.h @@ -11,6 +11,7 @@ const char* check_string(VALUE value); VALUE read_file_to_ruby_string(const char* file_path); +VALUE rb_string_from_hb_string(hb_string_T string); VALUE rb_position_from_c_struct(position_T position); VALUE rb_location_from_c_struct(location_T location); diff --git a/java/extension_helpers.c b/java/extension_helpers.c index a06c19144..07af8c7b4 100644 --- a/java/extension_helpers.c +++ b/java/extension_helpers.c @@ -17,7 +17,7 @@ #include jstring CreateStringFromHbString(JNIEnv* env, hb_string_T string) { - if (hb_string_is_empty(string)) { return NULL; } + if (hb_string_is_null(string)) { return NULL; } char* cstr = hb_string_to_c_string_using_malloc(string); if (!cstr) { return NULL; } diff --git a/javascript/packages/core/src/util.ts b/javascript/packages/core/src/util.ts index 890aa13f8..8e000b36e 100644 --- a/javascript/packages/core/src/util.ts +++ b/javascript/packages/core/src/util.ts @@ -5,15 +5,3 @@ export function ensureString(object: any): string { throw new TypeError("Argument must be a string") } - -export function convertToUTF8(string: string) { - const bytes = [] - - for (let i = 0; i < string.length; i++) { - bytes.push(string.charCodeAt(i)) - } - - const decoder = new TextDecoder("utf-8") - - return decoder.decode(new Uint8Array(bytes)) -} diff --git a/javascript/packages/node-wasm/test/__snapshots__/utf8-strings.test.ts.snap b/javascript/packages/node-wasm/test/__snapshots__/utf8-strings.test.ts.snap new file mode 100644 index 000000000..0c3133006 --- /dev/null +++ b/javascript/packages/node-wasm/test/__snapshots__/utf8-strings.test.ts.snap @@ -0,0 +1,432 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`UTF-8 string handling > extractHTML preserves UTF-8 > preserves UTF-8 in attributes 1`] = `"
πŸš€
"`; + +exports[`UTF-8 string handling > extractHTML preserves UTF-8 > preserves multi-byte characters 1`] = `"

CafΓ© β€” πŸš€ ВСст

"`; + +exports[`UTF-8 string handling > extractRuby preserves UTF-8 > preserves multi-byte characters 1`] = `" "CafΓ© πŸš€" ;"`; + +exports[`UTF-8 string handling > handles edge cases > incomplete comment has empty token value not null 1`] = ` +"@ DocumentNode (location: (1:0)-(1:4)) +β”œβ”€β”€ errors: [] +└── children: (1 item) + └── @ HTMLCommentNode (location: (1:0)-(1:4)) + β”œβ”€β”€ errors: (1 item) + β”‚ └── @ UnexpectedTokenError (location: (1:4)-(1:4)) + β”‚ β”œβ”€β”€ message: "Found end of file when expecting \`-->\` at (1:4)." + β”‚ β”œβ”€β”€ expected_type: "TOKEN_HTML_COMMENT_END" + β”‚ └── found: "" (location: (1:4)-(1:4)) + β”‚ + β”‚ + β”œβ”€β”€ comment_start: "\` at (1:13)." + β”‚ β”œβ”€β”€ expected_type: "TOKEN_HTML_COMMENT_END" + β”‚ └── found: "" (location: (1:13)-(1:13)) + β”‚ + β”‚ + β”œβ”€β”€ comment_start: "\` at (1:4)." + β”‚ β”œβ”€β”€ expected_type: "TOKEN_HTML_COMMENT_END" + β”‚ └── found: "" (location: (1:4)-(1:4)) + β”‚ + β”‚ + β”œβ”€β”€ comment_start: "\` at (1:13)." + β”‚ β”œβ”€β”€ expected_type: "TOKEN_HTML_COMMENT_END" + β”‚ └── found: "" (location: (1:13)-(1:13)) + β”‚ + β”‚ + β”œβ”€β”€ comment_start: "