#include "Dom/Dom.h" #include #include #include #include #include #define HTML_NODE_METATABLE "yapssg.html_node" static int absolute_index(lua_State *lua, int index) { return index < 0 ? lua_gettop(lua) + index + 1 : index; } static void set_html_node_metatable(lua_State *lua, int table_index) { table_index = absolute_index(lua, table_index); luaL_getmetatable(lua, HTML_NODE_METATABLE); lua_setmetatable(lua, table_index); } static int table_is_node(lua_State *lua, int table_index) { int is_node; table_index = absolute_index(lua, table_index); lua_getfield(lua, table_index, "tag"); is_node = !lua_isnil(lua, -1); lua_pop(lua, 1); if (!is_node) { lua_getfield(lua, table_index, "kind"); is_node = !lua_isnil(lua, -1); lua_pop(lua, 1); } return is_node; } static void append_normalized_child(lua_State *lua, int children_index, int child_index, size_t *child_count, unsigned int depth, const char *function_name) { size_t array_length; size_t index; int child_type; if (!lua_checkstack(lua, 2)) { luaL_error(lua, "%s children are nested too deeply", function_name); } children_index = absolute_index(lua, children_index); child_index = absolute_index(lua, child_index); child_type = lua_type(lua, child_index); if (child_type == LUA_TNIL) { return; } if (child_type == LUA_TSTRING || (child_type == LUA_TTABLE && table_is_node(lua, child_index))) { if (*child_count == INT_MAX) { luaL_error(lua, "%s has too many children", function_name); } ++*child_count; lua_pushvalue(lua, child_index); lua_rawseti(lua, children_index, (int)*child_count); return; } if (child_type != LUA_TTABLE) { luaL_error(lua, "%s child must be a string, node, child array, or nil (got " "%s)", function_name, lua_typename(lua, child_type)); } if (depth == 100) { luaL_error(lua, "%s children are nested too deeply", function_name); } array_length = lua_objlen(lua, child_index); if (array_length > INT_MAX) { luaL_error(lua, "%s child array is too large", function_name); } for (index = 1; index <= array_length; ++index) { lua_rawgeti(lua, child_index, (int)index); append_normalized_child(lua, children_index, -1, child_count, depth + 1, function_name); lua_pop(lua, 1); } } static int html_element(lua_State *lua) { const char *tag; size_t child_count = 0; int argument_count = lua_gettop(lua); int children_index; int index; luaL_checktype(lua, 1, LUA_TSTRING); tag = lua_tostring(lua, 1); if (tag[0] == '\0') { return luaL_argerror(lua, 1, "tag cannot be empty"); } if (!lua_isnoneornil(lua, 2)) { luaL_checktype(lua, 2, LUA_TTABLE); } lua_newtable(lua); lua_pushvalue(lua, 1); lua_setfield(lua, -2, "tag"); if (lua_istable(lua, 2)) { lua_pushvalue(lua, 2); } else { lua_newtable(lua); } lua_setfield(lua, -2, "attributes"); lua_newtable(lua); children_index = lua_gettop(lua); for (index = 3; index <= argument_count; ++index) { append_normalized_child(lua, children_index, index, &child_count, 0, "html.element"); } lua_setfield(lua, -2, "children"); set_html_node_metatable(lua, -1); return 1; } static int html_append(lua_State *lua) { size_t new_child_count = 0; size_t existing_child_count; size_t index; int argument_count = lua_gettop(lua); int new_children_index; int children_index; const char *tag; luaL_checktype(lua, 1, LUA_TTABLE); lua_getfield(lua, 1, "tag"); tag = lua_tostring(lua, -1); if (tag == NULL || tag[0] == '\0') { return luaL_argerror(lua, 1, "element expected"); } lua_pop(lua, 1); lua_newtable(lua); new_children_index = lua_gettop(lua); for (index = 2; index <= (size_t)argument_count; ++index) { append_normalized_child(lua, new_children_index, (int)index, &new_child_count, 0, "html.append"); } if (new_child_count == 0) { lua_pushvalue(lua, 1); return 1; } lua_getfield(lua, 1, "children"); if (lua_isnil(lua, -1)) { lua_pop(lua, 1); lua_newtable(lua); lua_pushvalue(lua, -1); lua_setfield(lua, 1, "children"); } else if (!lua_istable(lua, -1)) { return luaL_error(lua, "html.append element children must be a table"); } children_index = lua_gettop(lua); existing_child_count = lua_objlen(lua, children_index); if (existing_child_count > INT_MAX) { return luaL_error(lua, "html.append element has too many children"); } if (existing_child_count == 0) { size_t text_length; lua_getfield(lua, 1, "text"); if (lua_type(lua, -1) == LUA_TSTRING) { lua_tolstring(lua, -1, &text_length); if (text_length > 0) { lua_pushvalue(lua, -1); lua_rawseti(lua, children_index, 1); existing_child_count = 1; } } lua_pop(lua, 1); } if (new_child_count > INT_MAX - existing_child_count) { return luaL_error(lua, "html.append element has too many children"); } for (index = 1; index <= new_child_count; ++index) { lua_rawgeti(lua, new_children_index, (int)index); lua_rawseti(lua, children_index, (int)(existing_child_count + index)); } lua_pushnil(lua); lua_setfield(lua, 1, "text"); lua_pushvalue(lua, 1); return 1; } static int is_html_whitespace(char character) { return character == ' ' || character == '\t' || character == '\n' || character == '\f' || character == '\r'; } static int class_list_contains(const char *classes, size_t classes_length, const char *wanted, size_t wanted_length) { size_t index = 0; while (index < classes_length) { size_t start; while (index < classes_length && is_html_whitespace(classes[index])) { ++index; } start = index; while (index < classes_length && !is_html_whitespace(classes[index])) { ++index; } if (index - start == wanted_length && memcmp(classes + start, wanted, wanted_length) == 0) { return 1; } } return 0; } static int node_has_class(lua_State *lua, int node_index, const char *wanted, size_t wanted_length) { const char *classes; size_t classes_length; int matches = 0; node_index = absolute_index(lua, node_index); lua_getfield(lua, node_index, "attributes"); if (lua_istable(lua, -1)) { lua_getfield(lua, -1, "class"); if (lua_type(lua, -1) == LUA_TSTRING) { classes = lua_tolstring(lua, -1, &classes_length); matches = class_list_contains(classes, classes_length, wanted, wanted_length); } lua_pop(lua, 1); } lua_pop(lua, 1); return matches; } typedef struct { const char *tag; size_t tag_length; const char *id; size_t id_length; const char *suffixes; size_t suffixes_length; } CssSelector; static int is_selector_delimiter(char character) { return character == '.' || character == '#'; } static int is_unsupported_selector_character(char character) { return is_html_whitespace(character) || character == '>' || character == '+' || character == '~' || character == ',' || character == '[' || character == ']'; } static int parse_css_selector(lua_State *lua, const char *source, size_t length, CssSelector *selector) { size_t index = 0; memset(selector, 0, sizeof(*selector)); if (length == 0) { return luaL_argerror(lua, 2, "selector cannot be empty"); } if (source[0] == '*') { index = 1; } else if (!is_selector_delimiter(source[0])) { selector->tag = source; while (index < length && !is_selector_delimiter(source[index])) { if (is_unsupported_selector_character(source[index])) { return luaL_argerror(lua, 2, "unsupported CSS selector"); } ++index; } selector->tag_length = index; } selector->suffixes = source + index; selector->suffixes_length = length - index; while (index < length) { char delimiter = source[index++]; size_t start = index; if (!is_selector_delimiter(delimiter)) { return luaL_argerror(lua, 2, "unsupported CSS selector"); } while (index < length && !is_selector_delimiter(source[index])) { if (is_unsupported_selector_character(source[index])) { return luaL_argerror(lua, 2, "unsupported CSS selector"); } ++index; } if (index == start) { return luaL_argerror(lua, 2, "invalid CSS selector"); } if (delimiter == '#') { if (selector->id != NULL) { return luaL_argerror(lua, 2, "selector can contain only one id"); } selector->id = source + start; selector->id_length = index - start; } } return 0; } static int string_field_equals(lua_State *lua, int table_index, const char *field, const char *wanted, size_t wanted_length, int ignore_case) { const char *value; size_t value_length; int matches = 0; table_index = absolute_index(lua, table_index); lua_getfield(lua, table_index, field); if (lua_type(lua, -1) == LUA_TSTRING) { value = lua_tolstring(lua, -1, &value_length); if (value_length == wanted_length) { matches = ignore_case ? strncasecmp(value, wanted, wanted_length) == 0 : memcmp(value, wanted, wanted_length) == 0; } } lua_pop(lua, 1); return matches; } static int node_matches_selector(lua_State *lua, int node_index, const CssSelector *selector) { size_t index = 0; int attributes_index; node_index = absolute_index(lua, node_index); lua_getfield(lua, node_index, "tag"); if (lua_type(lua, -1) != LUA_TSTRING) { lua_pop(lua, 1); return 0; } lua_pop(lua, 1); if (selector->tag != NULL && !string_field_equals(lua, node_index, "tag", selector->tag, selector->tag_length, 1)) { return 0; } lua_getfield(lua, node_index, "attributes"); if (!lua_istable(lua, -1)) { lua_pop(lua, 1); return selector->suffixes_length == 0; } attributes_index = lua_gettop(lua); while (index < selector->suffixes_length) { char delimiter = selector->suffixes[index++]; size_t start = index; int matches; while (index < selector->suffixes_length && !is_selector_delimiter(selector->suffixes[index])) { ++index; } if (delimiter == '.') { matches = node_has_class(lua, node_index, selector->suffixes + start, index - start); } else { matches = string_field_equals(lua, attributes_index, "id", selector->suffixes + start, index - start, 0); } if (!matches) { lua_pop(lua, 1); return 0; } } lua_pop(lua, 1); return 1; } static void append_matches_by_selector(lua_State *lua, int node_index, const CssSelector *selector, int matches_index, size_t *match_count) { size_t child_count; size_t index; if (!lua_checkstack(lua, 3)) { luaL_error(lua, "HTML tree is too deep to search"); } node_index = absolute_index(lua, node_index); matches_index = absolute_index(lua, matches_index); if (node_matches_selector(lua, node_index, selector)) { if (*match_count == INT_MAX) { luaL_error(lua, "HTML query has too many matches"); } ++*match_count; lua_pushvalue(lua, node_index); lua_rawseti(lua, matches_index, (int)*match_count); } lua_getfield(lua, node_index, "children"); if (!lua_istable(lua, -1)) { lua_pop(lua, 1); return; } child_count = lua_objlen(lua, -1); if (child_count > INT_MAX) { luaL_error(lua, "HTML node has too many children to search"); } for (index = 1; index <= child_count; ++index) { lua_rawgeti(lua, -1, (int)index); if (lua_istable(lua, -1)) { append_matches_by_selector(lua, -1, selector, matches_index, match_count); } lua_pop(lua, 1); } lua_pop(lua, 1); } static int query_all(lua_State *lua, const char *source, size_t length) { CssSelector selector; size_t match_count = 0; int matches_index; parse_css_selector(lua, source, length, &selector); lua_newtable(lua); matches_index = lua_gettop(lua); append_matches_by_selector(lua, 1, &selector, matches_index, &match_count); return 1; } static int query(lua_State *lua, const char *source, size_t length) { query_all(lua, source, length); lua_rawgeti(lua, -1, 1); lua_remove(lua, -2); return 1; } static const char *check_query_arguments(lua_State *lua, size_t *length) { luaL_checktype(lua, 1, LUA_TTABLE); luaL_checktype(lua, 2, LUA_TSTRING); return lua_tolstring(lua, 2, length); } static int html_query(lua_State *lua) { const char *source; size_t length; source = check_query_arguments(lua, &length); return query(lua, source, length); } static int html_query_all(lua_State *lua) { const char *source; size_t length; source = check_query_arguments(lua, &length); return query_all(lua, source, length); } static int find_by_attribute_token(lua_State *lua, char prefix, const char *empty_error, const char *token_error) { const char *wanted; size_t wanted_length; size_t index; luaL_Buffer selector; luaL_checktype(lua, 1, LUA_TTABLE); luaL_checktype(lua, 2, LUA_TSTRING); wanted = lua_tolstring(lua, 2, &wanted_length); if (wanted_length == 0) { return luaL_argerror(lua, 2, empty_error); } for (index = 0; index < wanted_length; ++index) { if (is_html_whitespace(wanted[index]) || is_selector_delimiter(wanted[index]) || is_unsupported_selector_character(wanted[index])) { return luaL_argerror(lua, 2, token_error); } } luaL_buffinit(lua, &selector); luaL_addchar(&selector, prefix); luaL_addlstring(&selector, wanted, wanted_length); luaL_pushresult(&selector); wanted = lua_tolstring(lua, -1, &wanted_length); return query(lua, wanted, wanted_length); } static int html_find_by_class(lua_State *lua) { return find_by_attribute_token(lua, '.', "class cannot be empty", "class must be a single token"); } static int html_find_by_id(lua_State *lua) { return find_by_attribute_token(lua, '#', "id cannot be empty", "id must be a single token"); } void dom_register_html(lua_State *lua) { if (luaL_newmetatable(lua, HTML_NODE_METATABLE)) { lua_newtable(lua); lua_pushcfunction(lua, html_append); lua_setfield(lua, -2, "append"); lua_pushcfunction(lua, html_query); lua_setfield(lua, -2, "query"); lua_pushcfunction(lua, html_query_all); lua_setfield(lua, -2, "query_all"); lua_setfield(lua, -2, "__index"); lua_pushboolean(lua, 0); lua_setfield(lua, -2, "__metatable"); } lua_pop(lua, 1); lua_newtable(lua); lua_pushcfunction(lua, html_element); lua_setfield(lua, -2, "element"); lua_pushcfunction(lua, html_append); lua_setfield(lua, -2, "append"); lua_pushcfunction(lua, html_find_by_class); lua_setfield(lua, -2, "find_by_class"); lua_pushcfunction(lua, html_find_by_id); lua_setfield(lua, -2, "find_by_id"); lua_pushcfunction(lua, html_query); lua_setfield(lua, -2, "query"); lua_pushcfunction(lua, html_query_all); lua_setfield(lua, -2, "query_all"); lua_setglobal(lua, "html"); } static int is_lua_element(const xmlNode *node) { return node->type == XML_ELEMENT_NODE && strcasecmp((const char *)node->name, "lua") == 0; } static void set_string_field(lua_State *lua, int table_index, const char *name, const char *value) { lua_pushstring(lua, value); lua_setfield(lua, table_index, name); } static int has_structured_children(const xmlNode *node) { const xmlNode *child; for (child = node->children; child != NULL; child = child->next) { if (is_lua_element(child)) { continue; } if (child->type != XML_TEXT_NODE && child->type != XML_CDATA_SECTION_NODE) { return 1; } } return 0; } static void push_element(lua_State *lua, const xmlNode *node); static void push_special_node(lua_State *lua, const xmlNode *node) { lua_newtable(lua); if (node->type == XML_COMMENT_NODE) { set_string_field(lua, -2, "kind", "comment"); } else { set_string_field(lua, -2, "kind", "processing_instruction"); set_string_field(lua, -2, "target", (const char *)node->name); } set_string_field(lua, -2, "text", node->content == NULL ? "" : (const char *)node->content); } static void add_child_alias(lua_State *lua, int element_index, const xmlNode *child, int child_index) { const char *name = (const char *)child->name; lua_getfield(lua, element_index, name); if (lua_isnil(lua, -1)) { lua_pop(lua, 1); lua_pushvalue(lua, child_index); lua_setfield(lua, element_index, name); return; } lua_pop(lua, 1); } static void push_children(lua_State *lua, const xmlNode *node, int element_index, int include_text) { const xmlNode *child; int children_index; int array_index = 1; lua_newtable(lua); children_index = lua_gettop(lua); for (child = node->children; child != NULL; child = child->next) { if (is_lua_element(child)) { continue; } if (child->type == XML_ELEMENT_NODE) { int child_index; push_element(lua, child); child_index = lua_gettop(lua); lua_pushvalue(lua, child_index); lua_rawseti(lua, children_index, array_index++); add_child_alias(lua, element_index, child, child_index); lua_pop(lua, 1); } else if (include_text && (child->type == XML_TEXT_NODE || child->type == XML_CDATA_SECTION_NODE)) { lua_pushstring( lua, child->content == NULL ? "" : (const char *)child->content); lua_rawseti(lua, children_index, array_index++); } else if (child->type == XML_COMMENT_NODE || child->type == XML_PI_NODE) { push_special_node(lua, child); lua_rawseti(lua, children_index, array_index++); } } lua_setfield(lua, element_index, "children"); } static void push_attributes(lua_State *lua, const xmlNode *node, int element_index) { const xmlAttr *attribute; lua_newtable(lua); for (attribute = node->properties; attribute != NULL; attribute = attribute->next) { xmlChar *value = xmlNodeListGetString(node->doc, attribute->children, 1); lua_pushstring(lua, value == NULL ? "" : (const char *)value); lua_setfield(lua, -2, (const char *)attribute->name); xmlFree(value); } lua_setfield(lua, element_index, "attributes"); } static void push_text(lua_State *lua, const xmlNode *node, int element_index) { const xmlNode *child; luaL_Buffer buffer; luaL_buffinit(lua, &buffer); for (child = node->children; child != NULL; child = child->next) { if (child->type == XML_TEXT_NODE || child->type == XML_CDATA_SECTION_NODE) { luaL_addstring( &buffer, child->content == NULL ? "" : (const char *)child->content); } } luaL_pushresult(&buffer); lua_setfield(lua, element_index, "text"); } static void push_element(lua_State *lua, const xmlNode *node) { int element_index; int structured_children = has_structured_children(node); lua_newtable(lua); element_index = lua_gettop(lua); set_html_node_metatable(lua, element_index); set_string_field(lua, element_index, "tag", (const char *)node->name); push_attributes(lua, node, element_index); push_children(lua, node, element_index, structured_children); if (!structured_children) { push_text(lua, node, element_index); } } void dom_push_page(lua_State *lua, const xmlNode *root) { push_element(lua, root); } static xmlNodePtr node_from_table(lua_State *lua, int table_index, int *valid); static int add_attributes(lua_State *lua, int table_index, xmlNodePtr node) { int attributes_index; int valid = 1; lua_getfield(lua, table_index, "attributes"); attributes_index = lua_gettop(lua); if (lua_istable(lua, -1)) { lua_pushnil(lua); while (valid && lua_next(lua, -2) != 0) { if (lua_type(lua, -2) != LUA_TSTRING || lua_type(lua, -1) != LUA_TSTRING || xmlNewProp(node, BAD_CAST lua_tostring(lua, -2), BAD_CAST lua_tostring(lua, -1)) == NULL) { valid = 0; } lua_pop(lua, 1); } } lua_settop(lua, attributes_index - 1); return valid ? 0 : -1; } static int append_lua_child(lua_State *lua, xmlNodePtr parent, int child_index) { xmlNodePtr child = NULL; int valid = 1; if (lua_type(lua, child_index) == LUA_TSTRING) { child = xmlNewText(BAD_CAST lua_tostring(lua, child_index)); } else if (lua_istable(lua, child_index)) { child = node_from_table(lua, child_index, &valid); } else { valid = 0; } if (!valid || child == NULL) { xmlFreeNode(child); return -1; } if (xmlAddChild(parent, child) == NULL) { xmlFreeNode(child); return -1; } return 0; } static int add_children(lua_State *lua, int table_index, xmlNodePtr node) { size_t index; size_t length; int result = 0; lua_getfield(lua, table_index, "children"); length = lua_istable(lua, -1) ? lua_objlen(lua, -1) : 0; if (length > INT_MAX) { lua_pop(lua, 1); return -1; } for (index = 1; result == 0 && index <= length; ++index) { lua_rawgeti(lua, -1, (int)index); result = append_lua_child(lua, node, lua_gettop(lua)); lua_pop(lua, 1); } lua_pop(lua, 1); if (result == 0 && length == 0) { lua_getfield(lua, table_index, "text"); if (lua_type(lua, -1) == LUA_TSTRING) { xmlNodePtr text = xmlNewText(BAD_CAST lua_tostring(lua, -1)); if (text == NULL || xmlAddChild(node, text) == NULL) { xmlFreeNode(text); result = -1; } } lua_pop(lua, 1); } return result; } static xmlNodePtr special_node_from_table(lua_State *lua, int table_index, const char *kind, int *valid) { xmlNodePtr node; const char *text; lua_getfield(lua, table_index, "text"); text = lua_tostring(lua, -1); if (strcmp(kind, "comment") == 0) { node = xmlNewComment(BAD_CAST(text == NULL ? "" : text)); } else if (strcmp(kind, "processing_instruction") == 0) { const char *target; lua_getfield(lua, table_index, "target"); target = lua_tostring(lua, -1); node = target == NULL || target[0] == '\0' ? NULL : xmlNewPI(BAD_CAST target, BAD_CAST(text == NULL ? "" : text)); lua_pop(lua, 1); } else { node = NULL; } lua_pop(lua, 1); if (node == NULL) { *valid = 0; } return node; } static xmlNodePtr node_from_table(lua_State *lua, int table_index, int *valid) { const char *kind; const char *tag; xmlNodePtr node; table_index = absolute_index(lua, table_index); lua_getfield(lua, table_index, "kind"); kind = lua_tostring(lua, -1); if (kind != NULL) { node = special_node_from_table(lua, table_index, kind, valid); lua_pop(lua, 1); return node; } lua_pop(lua, 1); lua_getfield(lua, table_index, "tag"); tag = lua_tostring(lua, -1); node = tag == NULL || tag[0] == '\0' ? NULL : xmlNewNode(NULL, BAD_CAST tag); lua_pop(lua, 1); if (node == NULL || add_attributes(lua, table_index, node) != 0 || add_children(lua, table_index, node) != 0) { xmlFreeNode(node); *valid = 0; return NULL; } return node; } xmlNodePtr dom_page_from_lua(lua_State *lua, int table_index) { int valid = 1; xmlNodePtr root = node_from_table(lua, table_index, &valid); if (!valid) { xmlFreeNode(root); return NULL; } return root; }