#include "parsers.h" #include #include "os.h" #include "darr.h" // == INI ============================================ void ini__parse(arena_t *arena, ini_t *ini, const iniopt_t *options); ini_t ini_parse(arena_t *arena, strview_t filename, iniopt_t *opt) { oshandle_t fp = os_file_open(filename, FILEMODE_READ); ini_t out = ini_parse_fp(arena, fp, opt); os_file_close(fp); return out; } ini_t ini_parse_fp(arena_t *arena, oshandle_t file, iniopt_t *opt) { str_t data = os_file_read_all_str_fp(arena, file); return ini_parse_str(arena, strv(data), opt); } ini_t ini_parse_str(arena_t *arena, strview_t str, iniopt_t *opt) { ini_t out = { .text = str, .tables = NULL, }; ini__parse(arena, &out, opt); return out; } bool ini_is_valid(ini_t *ini) { return ini && !strv_is_empty(ini->text); } initable_t *ini_get_table(ini_t *ini, strview_t name) { initable_t *t = ini ? ini->tables : NULL; while (t) { if (strv_equals(t->name, name)) { return t; } t = t->next; } return NULL; } inivalue_t *ini_get(initable_t *table, strview_t key) { inivalue_t *v = table ? table->values : NULL; while (v) { if (strv_equals(v->key, key)) { return v; } v = v->next; } return NULL; } iniarray_t ini_as_arr(arena_t *arena, inivalue_t *value, char delim) { strview_t v = value ? value->value : STRV_EMPTY; if (!delim) delim = ' '; strview_t *beg = (strview_t *)arena->cur; usize count = 0; usize start = 0; for (usize i = 0; i < v.len; ++i) { if (v.buf[i] == delim) { strview_t arrval = strv_trim(strv_sub(v, start, i)); if (!strv_is_empty(arrval)) { strview_t *newval = alloc(arena, strview_t); *newval = arrval; ++count; } start = i + 1; } } strview_t last = strv_trim(strv_sub(v, start, SIZE_MAX)); if (!strv_is_empty(last)) { strview_t *newval = alloc(arena, strview_t); *newval = last; ++count; } return (iniarray_t){ .values = beg, .count = count, }; } u64 ini_as_uint(inivalue_t *value) { strview_t v = value ? value->value : STRV_EMPTY; instream_t in = istr_init(v); u64 out = 0; if (!istr_get_u64(&in, &out)) { out = 0; } return out; } i64 ini_as_int(inivalue_t *value) { strview_t v = value ? value->value : STRV_EMPTY; instream_t in = istr_init(v); i64 out = 0; if (!istr_get_i64(&in, &out)) { out = 0; } return out; } double ini_as_num(inivalue_t *value) { strview_t v = value ? value->value : STRV_EMPTY; instream_t in = istr_init(v); double out = 0; if (!istr_get_num(&in, &out)) { out = 0; } return out; } bool ini_as_bool(inivalue_t *value) { strview_t v = value ? value->value : STRV_EMPTY; instream_t in = istr_init(v); bool out = 0; if (!istr_get_bool(&in, &out)) { out = 0; } return out; } void ini_pretty_print(ini_t *ini, const ini_pretty_opts_t *options) { ini_pretty_opts_t opt = {0}; if (options) { memmove(&opt, options, sizeof(ini_pretty_opts_t)); } if (!os_handle_valid(opt.custom_target)) { opt.custom_target = os_stdout(); } if (!opt.use_custom_colours) { os_log_colour_e default_col[INI_PRETTY_COLOUR__COUNT] = { LOG_COL_YELLOW, // INI_PRETTY_COLOUR_KEY, LOG_COL_GREEN, // INI_PRETTY_COLOUR_VALUE, LOG_COL_WHITE, // INI_PRETTY_COLOUR_DIVIDER, LOG_COL_RED, // INI_PRETTY_COLOUR_TABLE, }; memmove(opt.colours, default_col, sizeof(default_col)); } for_each (t, ini->tables) { if (!strv_equals(t->name, INI_ROOT)) { os_log_set_colour(opt.colours[INI_PRETTY_COLOUR_TABLE]); os_file_puts(opt.custom_target, strv("[")); os_file_puts(opt.custom_target, t->name); os_file_puts(opt.custom_target, strv("]\n")); } for_each (pair, t->values) { if (strv_is_empty(pair->key) || strv_is_empty(pair->value)) continue; os_log_set_colour(opt.colours[INI_PRETTY_COLOUR_KEY]); os_file_puts(opt.custom_target, pair->key); os_log_set_colour(opt.colours[INI_PRETTY_COLOUR_DIVIDER]); os_file_puts(opt.custom_target, strv(" = ")); os_log_set_colour(opt.colours[INI_PRETTY_COLOUR_VALUE]); os_file_puts(opt.custom_target, pair->value); os_file_puts(opt.custom_target, strv("\n")); } } os_log_set_colour(LOG_COL_RESET); } ///// ini-private //////////////////////////////////// iniopt_t ini__get_options(const iniopt_t *options) { iniopt_t out = { .key_value_divider = '=', .comment_vals = strv(";#"), }; #define SETOPT(v) out.v = options->v ? options->v : out.v if (options) { SETOPT(key_value_divider); SETOPT(merge_duplicate_keys); SETOPT(merge_duplicate_tables); out.comment_vals = strv_is_empty(options->comment_vals) ? out.comment_vals : options->comment_vals; } #undef SETOPT return out; } void ini__add_value(arena_t *arena, initable_t *table, instream_t *in, iniopt_t *opts) { assert(table); strview_t key = strv_trim(istr_get_view(in, opts->key_value_divider)); istr_skip(in, 1); strview_t value = strv_trim(istr_get_view(in, '\n')); usize comment_pos = strv_find_either(value, opts->comment_vals, 0); if (comment_pos != STR_NONE) { value = strv_sub(value, 0, comment_pos); } istr_skip(in, 1); inivalue_t *newval = NULL; if (opts->merge_duplicate_keys) { newval = table->values; while (newval) { if (strv_equals(newval->key, key)) { break; } newval = newval->next; } } if (newval) { newval->value = value; } else { newval = alloc(arena, inivalue_t); newval->key = key; newval->value = value; if (!table->values) { table->values = newval; } else { table->tail->next = newval; } table->tail = newval; } } void ini__add_table(arena_t *arena, ini_t *ctx, instream_t *in, iniopt_t *options) { istr_skip(in, 1); // skip [ strview_t name = istr_get_view(in, ']'); istr_skip(in, 1); // skip ] initable_t *table = NULL; if (options->merge_duplicate_tables) { table = ctx->tables; while (table) { if (strv_equals(table->name, name)) { break; } table = table->next; } } if (!table) { table = alloc(arena, initable_t); table->name = name; if (!ctx->tables) { ctx->tables = table; } else { ctx->tail->next = table; } ctx->tail = table; } istr_ignore_and_skip(in, '\n'); while (!istr_is_finished(in)) { switch (istr_peek(in)) { case '\n': // fallthrough case '\r': return; case '#': // fallthrough case ';': istr_ignore_and_skip(in, '\n'); break; default: ini__add_value(arena, table, in, options); break; } } } void ini__parse(arena_t *arena, ini_t *ini, const iniopt_t *options) { iniopt_t opts = ini__get_options(options); initable_t *root = alloc(arena, initable_t); root->name = INI_ROOT; ini->tables = root; ini->tail = root; instream_t in = istr_init(ini->text); while (!istr_is_finished(&in)) { istr_skip_whitespace(&in); switch (istr_peek(&in)) { case '[': ini__add_table(arena, ini, &in, &opts); break; case '#': // fallthrough case ';': istr_ignore_and_skip(&in, '\n'); break; default: ini__add_value(arena, ini->tables, &in, &opts); break; } } } // == JSON =========================================== bool json__parse_obj(arena_t *arena, instream_t *in, jsonflags_e flags, json_t **out); json_t *json_parse(arena_t *arena, strview_t filename, jsonflags_e flags) { str_t data = os_file_read_all_str(arena, filename); return json_parse_str(arena, strv(data), flags); } json_t *json_parse_str(arena_t *arena, strview_t str, jsonflags_e flags) { arena_t before = *arena; json_t *root = alloc(arena, json_t); root->type = JSON_OBJECT; instream_t in = istr_init(str); if (!json__parse_obj(arena, &in, flags, &root->object)) { // reset arena *arena = before; return NULL; } return root; } json_t *json_get(json_t *node, strview_t key) { if (!node) return NULL; if (node->type != JSON_OBJECT) { return NULL; } node = node->object; while (node) { if (strv_equals(node->key, key)) { return node; } node = node->next; } return NULL; } void json__pretty_print_value(json_t *value, int indent, const json_pretty_opts_t *options); void json_pretty_print(json_t *root, const json_pretty_opts_t *options) { json_pretty_opts_t default_options = { 0 }; if (options) { memmove(&default_options, options, sizeof(json_pretty_opts_t)); } if (!os_handle_valid(default_options.custom_target)) { default_options.custom_target = os_stdout(); } if (!default_options.use_custom_colours) { os_log_colour_e default_col[JSON_PRETTY_COLOUR__COUNT] = { LOG_COL_YELLOW, // JSON_PRETTY_COLOUR_KEY, LOG_COL_CYAN, // JSON_PRETTY_COLOUR_STRING, LOG_COL_BLUE, // JSON_PRETTY_COLOUR_NUM, LOG_COL_DARK_GREY, // JSON_PRETTY_COLOUR_NULL, LOG_COL_GREEN, // JSON_PRETTY_COLOUR_TRUE, LOG_COL_RED, // JSON_PRETTY_COLOUR_FALSE, }; memmove(default_options.colours, default_col, sizeof(default_col)); } json__pretty_print_value(root, 0, &default_options); os_file_putc(default_options.custom_target, '\n'); } ///// json-private /////////////////////////////////// #define json__ensure(c) json__check_char(in, c) bool json__parse_value(arena_t *arena, instream_t *in, jsonflags_e flags, json_t **out); bool json__check_char(instream_t *in, char c) { if (istr_get(in) == c) { return true; } istr_rewind_n(in, 1); return false; } bool json__is_value_finished(instream_t *in) { usize old_pos = istr_tell(in); istr_skip_whitespace(in); switch(istr_peek(in)) { case '}': // fallthrough case ']': // fallthrough case ',': return true; } in->cur = in->beg + old_pos; return false; } bool json__parse_null(instream_t *in) { strview_t null_view = istr_get_view_len(in, 4); bool is_valid = true; if (!strv_equals(null_view, strv("null"))) { is_valid = false; } if (!json__is_value_finished(in)) { is_valid = false; } return is_valid; } bool json__parse_array(arena_t *arena, instream_t *in, jsonflags_e flags, json_t **out) { json_t *head = NULL; if (!json__ensure('[')) { goto fail; } istr_skip_whitespace(in); // if it is an empty array if (istr_peek(in) == ']') { istr_skip(in, 1); goto success; } if (!json__parse_value(arena, in, flags, &head)) { goto fail; } json_t *cur = head; while (true) { istr_skip_whitespace(in); switch (istr_get(in)) { case ']': goto success; case ',': { istr_skip_whitespace(in); // trailing comma if (istr_peek(in) == ']') { if (flags & JSON_NO_TRAILING_COMMAS) { goto fail; } else { continue; } } json_t *next = NULL; if (!json__parse_value(arena, in, flags, &next)) { goto fail; } cur->next = next; next->prev = cur; cur = next; break; } default: istr_rewind_n(in, 1); goto fail; } } success: *out = head; return true; fail: *out = NULL; return false; } bool json__parse_string(arena_t *arena, instream_t *in, strview_t *out) { COLLA_UNUSED(arena); *out = STRV_EMPTY; istr_skip_whitespace(in); if (!json__ensure('"')) { goto fail; } const char *from = in->cur; for (; !istr_is_finished(in) && *in->cur != '"'; ++in->cur) { if (istr_peek(in) == '\\') { ++in->cur; } } usize len = in->cur - from; *out = strv(from, len); if (!json__ensure('"')) { goto fail; } return true; fail: return false; } bool json__parse_pair(arena_t *arena, instream_t *in, jsonflags_e flags, json_t **out) { strview_t key = {0}; if (!json__parse_string(arena, in, &key)) { goto fail; } // skip preamble istr_skip_whitespace(in); if (!json__ensure(':')) { goto fail; } if (!json__parse_value(arena, in, flags, out)) { goto fail; } (*out)->key = key; return true; fail: *out = NULL; return false; } bool json__parse_obj(arena_t *arena, instream_t *in, jsonflags_e flags, json_t **out) { if (!json__ensure('{')) { goto fail; } istr_skip_whitespace(in); // if it is an empty object if (istr_peek(in) == '}') { istr_skip(in, 1); *out = NULL; return true; } json_t *head = NULL; if (!json__parse_pair(arena, in, flags, &head)) { goto fail; } json_t *cur = head; while (true) { istr_skip_whitespace(in); switch (istr_get(in)) { case '}': goto success; case ',': { istr_skip_whitespace(in); // trailing commas if (!(flags & JSON_NO_TRAILING_COMMAS) && istr_peek(in) == '}') { goto success; } json_t *next = NULL; if (!json__parse_pair(arena, in, flags, &next)) { goto fail; } cur->next = next; next->prev = cur; cur = next; break; } default: istr_rewind_n(in, 1); goto fail; } } success: *out = head; return true; fail: *out = NULL; return false; } bool json__parse_value(arena_t *arena, instream_t *in, jsonflags_e flags, json_t **out) { json_t *val = alloc(arena, json_t); istr_skip_whitespace(in); switch (istr_peek(in)) { // object case '{': if (!json__parse_obj(arena, in, flags, &val->object)) { goto fail; } val->type = JSON_OBJECT; break; // array case '[': if (!json__parse_array(arena, in, flags, &val->array)) { goto fail; } val->type = JSON_ARRAY; break; // string case '"': if (!json__parse_string(arena, in, &val->string)) { goto fail; } val->type = JSON_STRING; break; // boolean case 't': // fallthrough case 'f': if (!istr_get_bool(in, &val->boolean)) { goto fail; } val->type = JSON_BOOL; break; // null case 'n': if (!json__parse_null(in)) { goto fail; } val->type = JSON_NULL; break; // comment case '/': err("TODO comments"); break; // number default: if (!istr_get_num(in, &val->number)) { goto fail; } val->type = JSON_NUMBER; break; } *out = val; return true; fail: *out = NULL; return false; } #undef json__ensure #define JSON_PRETTY_INDENT(ind) for (int i = 0; i < ind; ++i) os_file_puts(options->custom_target, strv(" ")) void json__pretty_print_value(json_t *value, int indent, const json_pretty_opts_t *options) { switch (value->type) { case JSON_NULL: os_log_set_colour(options->colours[JSON_PRETTY_COLOUR_NULL]); os_file_puts(options->custom_target, strv("null")); os_log_set_colour(LOG_COL_RESET); break; case JSON_ARRAY: os_file_puts(options->custom_target, strv("[\n")); for_each (node, value->array) { JSON_PRETTY_INDENT(indent + 1); json__pretty_print_value(node, indent + 1, options); if (node->next) { os_file_putc(options->custom_target, ','); } os_file_putc(options->custom_target, '\n'); } JSON_PRETTY_INDENT(indent); os_file_putc(options->custom_target, ']'); break; case JSON_STRING: os_log_set_colour(options->colours[JSON_PRETTY_COLOUR_STRING]); os_file_putc(options->custom_target, '\"'); os_file_puts(options->custom_target, value->string); os_file_putc(options->custom_target, '\"'); os_log_set_colour(LOG_COL_RESET); break; case JSON_NUMBER: { os_log_set_colour(options->colours[JSON_PRETTY_COLOUR_NUM]); u8 scratchbuf[256]; arena_t scratch = arena_make(ARENA_STATIC, sizeof(scratchbuf), scratchbuf); os_file_print( scratch, options->custom_target, "%g", value->number ); os_log_set_colour(LOG_COL_RESET); break; } case JSON_BOOL: os_log_set_colour(options->colours[value->boolean ? JSON_PRETTY_COLOUR_TRUE : JSON_PRETTY_COLOUR_FALSE]); os_file_puts(options->custom_target, value->boolean ? strv("true") : strv("false")); os_log_set_colour(LOG_COL_RESET); break; case JSON_OBJECT: os_file_puts(options->custom_target, strv("{\n")); for_each(node, value->object) { JSON_PRETTY_INDENT(indent + 1); os_log_set_colour(options->colours[JSON_PRETTY_COLOUR_KEY]); os_file_putc(options->custom_target, '\"'); os_file_puts(options->custom_target, node->key); os_file_putc(options->custom_target, '\"'); os_log_set_colour(LOG_COL_RESET); os_file_puts(options->custom_target, strv(": ")); json__pretty_print_value(node, indent + 1, options); if (node->next) { os_file_putc(options->custom_target, ','); } os_file_putc(options->custom_target, '\n'); } JSON_PRETTY_INDENT(indent); os_file_putc(options->custom_target, '}'); break; } } #undef JSON_PRETTY_INDENT // == XML ============================================ xmltag_t *xml__parse_tag(arena_t *arena, instream_t *in); xml_t xml_parse(arena_t *arena, strview_t filename) { str_t str = os_file_read_all_str(arena, filename); return xml_parse_str(arena, strv(str)); } xml_t xml_parse_str(arena_t *arena, strview_t xmlstr) { xml_t out = { .text = xmlstr, .root = alloc(arena, xmltag_t), }; instream_t in = istr_init(xmlstr); while (!istr_is_finished(&in)) { xmltag_t *tag = xml__parse_tag(arena, &in); if (out.tail) out.tail->next = tag; else out.root->child = tag; out.tail = tag; } return out; } xmltag_t *xml_get_tag(xmltag_t *parent, strview_t key, bool recursive) { xmltag_t *t = parent ? parent->child : NULL; while (t) { if (strv_equals(key, t->key)) { return t; } if (recursive && t->child) { xmltag_t *out = xml_get_tag(t, key, recursive); if (out) { return out; } } t = t->next; } return NULL; } strview_t xml_get_attribute(xmltag_t *tag, strview_t key) { xmlattr_t *a = tag ? tag->attributes : NULL; while (a) { if (strv_equals(key, a->key)) { return a->value; } a = a->next; } return STRV_EMPTY; } ///// xml-private //////////////////////////////////// xmlattr_t *xml__parse_attr(arena_t *arena, instream_t *in) { if (istr_peek(in) != ' ') { return NULL; } strview_t key = strv_trim(istr_get_view(in, '=')); istr_skip(in, 1); // skip = strview_t val = strv_trim(istr_get_view_either(in, strv("\">"))); if (istr_peek(in) != '>') { istr_skip(in, 1); // skip " } if (strv_is_empty(key) || strv_is_empty(val)) { warn("key or value empty"); return NULL; } xmlattr_t *attr = alloc(arena, xmlattr_t); attr->key = key; attr->value = val; return attr; } xmltag_t *xml__parse_tag(arena_t *arena, instream_t *in) { istr_skip_whitespace(in); // we're either parsing the body, or we have finished the object if (istr_peek(in) != '<' || istr_peek_next(in) == '/') { return NULL; } istr_skip(in, 1); // skip < // meta tag, we don't care about these if (istr_peek(in) == '?') { istr_ignore_and_skip(in, '\n'); return NULL; } xmltag_t *tag = alloc(arena, xmltag_t); tag->key = strv_trim(istr_get_view_either(in, strv(" >"))); xmlattr_t *attr = xml__parse_attr(arena, in); while (attr) { attr->next = tag->attributes; tag->attributes = attr; attr = xml__parse_attr(arena, in); } // this tag does not have children, return if (istr_peek(in) == '/') { istr_skip(in, 2); // skip / and > return tag; } istr_skip(in, 1); // skip > xmltag_t *child = xml__parse_tag(arena, in); while (child) { if (tag->tail) { tag->tail->next = child; tag->tail = child; } else { tag->child = tag->tail = child; } child = xml__parse_tag(arena, in); } // parse content istr_skip_whitespace(in); tag->content = istr_get_view(in, '<'); // closing tag istr_skip(in, 2); // skip < and / strview_t closing = strv_trim(istr_get_view(in, '>')); if (!strv_equals(tag->key, closing)) { warn("opening and closing tags are different!: (%v) != (%v)", tag->key, closing); } istr_skip(in, 1); // skip > return tag; } // == HTML =========================================== htmltag_t *html__parse_tag(arena_t *arena, instream_t *in); html_t html_parse(arena_t *arena, strview_t filename) { str_t str = os_file_read_all_str(arena, filename); return html_parse_str(arena, strv(str)); } html_t html_parse_str(arena_t *arena, strview_t str) { html_t out = { .text = str, .root = alloc(arena, xmltag_t), }; instream_t in = istr_init(str); while (!istr_is_finished(&in)) { htmltag_t *tag = html__parse_tag(arena, &in); if (out.tail) out.tail->next = tag; else out.root->children = tag; out.tail = tag; } return out; } htmltag_t *html__get_tag_internal(htmltag_t *parent, str_t key, bool recursive) { htmltag_t *t = parent ? parent->children : NULL; while (t) { if (str_equals(key, t->key)) { return t; } if (recursive && t->children) { htmltag_t *out = html__get_tag_internal(t, key, recursive); if (out) { return out; } } t = t->next; } return NULL; } htmltag_t *html_get_tag(htmltag_t *parent, strview_t key, bool recursive) { u8 tmpbuf[KB(1)]; arena_t scratch = arena_make(ARENA_STATIC, sizeof(tmpbuf), tmpbuf); str_t upper = strv_to_upper(&scratch, key); return html__get_tag_internal(parent, upper, recursive); } strview_t html_get_attribute(htmltag_t *tag, strview_t key) { xmlattr_t *a = tag ? tag->attributes : NULL; while (a) { if (strv_equals(key, a->key)) { return a->value; } a = a->next; } return STRV_EMPTY; } ///// html-private /////////////////////////////////// /* special rules:

tag does not need to be closed when followed by address, article, aside, blockquote, details, dialog, div, dl, fieldset, figcaption, figure, footer, form, h1, h2, h3, h4, h5, h6, header, hgroup, hr, main, menu, nav, ol, p, pre, search, section, table, or ul */ strview_t html_closing_p_tags[] = { cstrv("ADDRESS"), cstrv("ARTICLE"), cstrv("ASIDE"), cstrv("BLOCKQUOTE"), cstrv("DETAILS"), cstrv("DIALOG"), cstrv("DIV"), cstrv("DL"), cstrv("FIELDSET"), cstrv("FIGCAPTION"), cstrv("FIGURE"), cstrv("FOOTER"), cstrv("FORM"), cstrv("H1"), cstrv("H2"), cstrv("H3"), cstrv("H4"), cstrv("H5"), cstrv("H6"), cstrv("HEADER"), cstrv("HGROUP"), cstrv("HR"), cstrv("MAIN"), cstrv("MENU"), cstrv("NAV"), cstrv("OL"), cstrv("P"), cstrv("PRE"), cstrv("SEARCH"), cstrv("SECTION"), cstrv("TABLE"), cstrv("UL"), }; bool html__closes_p_tag(strview_t tag) { for (int i = 0; i < arrlen(html_closing_p_tags); ++i) { if (strv_equals(html_closing_p_tags[i], tag)) { return true; } } return false; } htmltag_t *html__parse_tag(arena_t *arena, instream_t *in) { istr_skip_whitespace(in); // we're either parsing the body, or we have finished the object if (istr_peek(in) != '<' || istr_peek_next(in) == '/') { return NULL; } istr_skip(in, 1); // skip < // meta tag, we don't care about these if (istr_peek(in) == '?') { istr_ignore_and_skip(in, '\n'); return NULL; } htmltag_t *tag = alloc(arena, htmltag_t); tag->key = strv_to_upper( arena, strv_trim(istr_get_view_either(in, strv(" >"))) ); xmlattr_t *attr = xml__parse_attr(arena, in); while (attr) { attr->next = tag->attributes; tag->attributes = attr; attr = xml__parse_attr(arena, in); } // this tag does not have children, return if (istr_peek(in) == '/') { istr_skip(in, 2); // skip / and > return tag; } istr_skip(in, 1); // skip > bool is_p_tag = strv_equals(strv(tag->key), strv("P")); while (!istr_is_finished(in)) { istr_skip_whitespace(in); strview_t content = strv_trim(istr_get_view(in, '<')); // skip < istr_skip(in, 1); bool is_closing = istr_peek(in) == '/'; if (is_closing) { istr_skip(in, 1); } arena_t scratch = *arena; instream_t scratch_in = *in; str_t next_tag = strv_to_upper(&scratch, strv_trim(istr_get_view_either(&scratch_in, strv(" >")))); // rewind < istr_rewind_n(in, 1); // if we don't have children, it means this is the only content // otherwise, it means this is content in-between other tags, // if so: create an empty tag with the content and add it as a child if (!strv_is_empty(content)) { if (tag->children == NULL) { tag->content = content; } else { htmltag_t *empty = alloc(arena, htmltag_t); empty->content = content; olist_push(tag->children, tag->tail, empty); } } bool close_tag = (is_closing && str_equals(tag->key, next_tag)) || (is_p_tag && html__closes_p_tag(strv(next_tag))); if (close_tag) { if (is_closing) { istr_skip(in, 2 + next_tag.len); } break; } htmltag_t *child = html__parse_tag(arena, in); if (tag->tail) { (tag->tail)->next = (child); (tag->tail) = (child); } else { (tag->children) = (tag->tail) = (child); } } return tag; }