#include "html.h" #include "document.h" #include "stylesheet.h" #include "html_tag.h" #include "el_text.h" #include "el_para.h" #include "el_space.h" #include "el_body.h" #include "el_image.h" #include "el_table.h" #include "el_td.h" #include "el_link.h" #include "el_title.h" #include "el_style.h" #include "el_script.h" #include "el_comment.h" #include "el_cdata.h" #include "el_base.h" #include "el_anchor.h" #include "el_break.h" #include "el_div.h" #include "el_font.h" #include "el_tr.h" #include "gumbo.h" #include "render_item.h" #include "render_table.h" #include "render_block.h" namespace litehtml { document::document(document_container* container) { m_container = container; } document::~document() { m_over_element = nullptr; } document::ptr document::createFromString( const estring& str, document_container* container, const string& master_styles, const string& user_styles ) { // Create litehtml::document document::ptr doc = make_shared(container); // Parse document into GumboOutput GumboOutput* output = doc->parse_html(str); // mode must be set before doc->create_node because it is used in html_tag::set_attr switch (output->document->v.document.doc_type_quirks_mode) { case GUMBO_DOCTYPE_NO_QUIRKS: doc->m_mode = no_quirks_mode; break; case GUMBO_DOCTYPE_QUIRKS: doc->m_mode = quirks_mode; break; case GUMBO_DOCTYPE_LIMITED_QUIRKS: doc->m_mode = limited_quirks_mode; break; } // Create litehtml::elements. elements_list root_elements; doc->create_node(output->root, root_elements, true); if (!root_elements.empty()) { doc->m_root = root_elements.back(); } // Destroy GumboOutput gumbo_destroy_output(&kGumboDefaultOptions, output); if (master_styles != "") { doc->m_master_css.parse_css_stylesheet(master_styles, "", doc); doc->m_master_css.sort_selectors(); } if (user_styles != "") { doc->m_user_css.parse_css_stylesheet(user_styles, "", doc); doc->m_user_css.sort_selectors(); } // Let's process created elements tree if (doc->m_root) { doc->container()->get_media_features(doc->m_media); doc->m_root->set_pseudo_class(_root_, true); // apply master CSS doc->m_root->apply_stylesheet(doc->m_master_css); // parse elements attributes doc->m_root->parse_attributes(); // parse style sheets linked in document for (const auto& css : doc->m_css) { media_query_list_list::ptr media; if (css.media != "") { auto mq_list = parse_media_query_list(css.media, doc); media = make_shared(); media->add(mq_list); } doc->m_styles.parse_css_stylesheet(css.text, css.baseurl, doc, media); } // Sort css selectors using CSS rules. doc->m_styles.sort_selectors(); // Apply media features. doc->update_media_lists(doc->m_media); // Apply parsed styles. doc->m_root->apply_stylesheet(doc->m_styles); // Apply user styles if any doc->m_root->apply_stylesheet(doc->m_user_css); // Initialize element::m_css doc->m_root->compute_styles(); // Create rendering tree doc->m_root_render = doc->m_root->create_render_item(nullptr); // Now the m_tabular_elements is filled with tabular elements. // We have to check the tabular elements for missing table elements // and create the anonymous boxes in visual table layout doc->fix_tables_layout(); // Finally initialize elements // init() returns pointer to the render_init element because it can change its type if(doc->m_root_render) { doc->m_root_render = doc->m_root_render->init(); } } return doc; } // https://html.spec.whatwg.org/multipage/parsing.html#change-the-encoding encoding adjust_meta_encoding(encoding meta_encoding, encoding current_encoding) { // 1. if (current_encoding == encoding::utf_16le || current_encoding == encoding::utf_16be) return current_encoding; // 2. if (meta_encoding == encoding::utf_16le || meta_encoding == encoding::utf_16be) return encoding::utf_8; // 3. if (meta_encoding == encoding::x_user_defined) return encoding::windows_1252; // 4,5,6. return meta_encoding; } // https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inhead:change-the-encoding encoding get_meta_encoding(GumboNode* root) { // find GumboNode* head = nullptr; for (size_t i = 0; i < root->v.element.children.length; i++) { GumboNode* node = (GumboNode*)root->v.element.children.data[i]; if (node->type == GUMBO_NODE_ELEMENT && node->v.element.tag == GUMBO_TAG_HEAD) { head = node; break; } } if (!head) return encoding::null; // go through tags in for (size_t i = 0; i < head->v.element.children.length; i++) { GumboNode* node = (GumboNode*)head->v.element.children.data[i]; if (node->type != GUMBO_NODE_ELEMENT || node->v.element.tag != GUMBO_TAG_META) continue; auto charset = gumbo_get_attribute(&node->v.element.attributes, "charset"); auto http_equiv = gumbo_get_attribute(&node->v.element.attributes, "http-equiv"); auto content = gumbo_get_attribute(&node->v.element.attributes, "content"); // 1. If the element has a charset attribute... if (charset) { auto encoding = get_encoding(charset->value); if (encoding != encoding::null) return encoding; } // 2. Otherwise, if the element has an http-equiv attribute... else if (http_equiv && t_strcasecmp(http_equiv->value, "content-type") == 0 && content) { auto encoding = extract_encoding_from_meta_element(content->value); if (encoding != encoding::null) return encoding; } } return encoding::null; } // substitute for gumbo_parse that handles encodings GumboOutput* document::parse_html(estring str) { // https://html.spec.whatwg.org/multipage/parsing.html#the-input-byte-stream encoding_sniffing_algorithm(str); // cannot store output in local variable because gumbo keeps pointers into it, // which will be accessed later in gumbo_tag_from_original_text if (str.encoding == encoding::utf_8) m_text = str; else decode(str, str.encoding, m_text); // Gumbo does not support callbacks on node creation, so we cannot change encoding while parsing. // Instead, we parse entire file and then handle tags. // Using gumbo_parse_with_options to pass string length (m_text may contain NUL chars). GumboOutput* output = gumbo_parse_with_options(&kGumboDefaultOptions, m_text.data(), m_text.size()); if (str.confidence == confidence::certain) return output; // Otherwise: confidence is tentative. // If valid HTML encoding is specified in tag... encoding meta_encoding = get_meta_encoding(output->root); if (meta_encoding != encoding::null) { // ...and it is different from currently used encoding... encoding new_encoding = adjust_meta_encoding(meta_encoding, str.encoding); if (new_encoding != str.encoding) { // ...reparse with the new encoding. gumbo_destroy_output(&kGumboDefaultOptions, output); m_text.clear(); if (new_encoding == encoding::utf_8) m_text = str; else decode(str, new_encoding, m_text); output = gumbo_parse_with_options(&kGumboDefaultOptions, m_text.data(), m_text.size()); } } return output; } void document::create_node(void* gnode, elements_list& elements, bool parseTextNode) { auto* node = (GumboNode*)gnode; switch (node->type) { case GUMBO_NODE_ELEMENT: { string_map attrs; GumboAttribute* attr; for (unsigned int i = 0; i < node->v.element.attributes.length; i++) { attr = (GumboAttribute*)node->v.element.attributes.data[i]; attrs[attr->name] = attr->value; } element::ptr ret; const char* tag = gumbo_normalized_tagname(node->v.element.tag); if (tag[0]) { ret = create_element(tag, attrs); } else { if (node->v.element.original_tag.data && node->v.element.original_tag.length) { string str; gumbo_tag_from_original_text(&node->v.element.original_tag); str.append(node->v.element.original_tag.data, node->v.element.original_tag.length); ret = create_element(str.c_str(), attrs); } } if (!strcmp(tag, "script")) { parseTextNode = false; } if (ret) { elements_list child; for (unsigned int i = 0; i < node->v.element.children.length; i++) { child.clear(); create_node(static_cast (node->v.element.children.data[i]), child, parseTextNode); std::for_each(child.begin(), child.end(), [&ret](element::ptr& el) { ret->appendChild(el); } ); } elements.push_back(ret); } } break; case GUMBO_NODE_TEXT: { if (!parseTextNode) { elements.push_back(std::make_shared(node->v.text.text, shared_from_this())); } else { m_container->split_text(node->v.text.text, [this, &elements](const char* text) { elements.push_back(std::make_shared(text, shared_from_this())); }, [this, &elements](const char* text) { elements.push_back(std::make_shared(text, shared_from_this())); }); } } break; case GUMBO_NODE_CDATA: { element::ptr ret = std::make_shared(shared_from_this()); ret->set_data(node->v.text.text); elements.push_back(ret); } break; case GUMBO_NODE_COMMENT: { element::ptr ret = std::make_shared(shared_from_this()); ret->set_data(node->v.text.text); elements.push_back(ret); } break; case GUMBO_NODE_WHITESPACE: { string str = node->v.text.text; for (size_t i = 0; i < str.length(); i++) { elements.push_back(std::make_shared(str.substr(i, 1).c_str(), shared_from_this())); } } break; default: break; } } element::ptr document::create_element(const char* tag_name, const string_map& attributes) { element::ptr newTag; document::ptr this_doc = shared_from_this(); if (m_container) { newTag = m_container->create_element(tag_name, attributes, this_doc); } if (!newTag) { if (!strcmp(tag_name, "br")) { newTag = std::make_shared(this_doc); } else if (!strcmp(tag_name, "p")) { newTag = std::make_shared(this_doc); } else if (!strcmp(tag_name, "img")) { newTag = std::make_shared(this_doc); } else if (!strcmp(tag_name, "table")) { newTag = std::make_shared(this_doc); } else if (!strcmp(tag_name, "td") || !strcmp(tag_name, "th")) { newTag = std::make_shared(this_doc); } else if (!strcmp(tag_name, "link")) { newTag = std::make_shared(this_doc); } else if (!strcmp(tag_name, "title")) { newTag = std::make_shared(this_doc); } else if (!strcmp(tag_name, "a")) { newTag = std::make_shared(this_doc); } else if (!strcmp(tag_name, "tr")) { newTag = std::make_shared(this_doc); } else if (!strcmp(tag_name, "style")) { newTag = std::make_shared(this_doc); } else if (!strcmp(tag_name, "base")) { newTag = std::make_shared(this_doc); } else if (!strcmp(tag_name, "body")) { newTag = std::make_shared(this_doc); } else if (!strcmp(tag_name, "div")) { newTag = std::make_shared(this_doc); } else if (!strcmp(tag_name, "script")) { newTag = std::make_shared(this_doc); } else if (!strcmp(tag_name, "font")) { newTag = std::make_shared(this_doc); } else { newTag = std::make_shared(this_doc); } } if (newTag) { newTag->set_tagName(tag_name); for (const auto& attribute : attributes) { newTag->set_attr(attribute.first.c_str(), attribute.second.c_str()); } } return newTag; } uint_ptr document::add_font( const char* name, int size, const char* weight, const char* style, const char* decoration, font_metrics* fm ) { uint_ptr ret = 0; if(!name) { name = m_container->get_default_font_name(); } char strSize[20]; t_itoa(size, strSize, 20, 10); string key = name; key += ":"; key += strSize; key += ":"; key += weight; key += ":"; key += style; key += ":"; key += decoration; if(m_container->m_fonts.find(key) == m_container->m_fonts.end()) { font_style fs = (font_style) value_index(style, font_style_strings, font_style_normal); int fw = value_index(weight, font_weight_strings, -1); if(fw >= 0) { switch(fw) { case font_weight_bold: fw = 700; break; case font_weight_bolder: fw = 600; break; case font_weight_lighter: fw = 300; break; case font_weight_normal: fw = 400; break; } } else { fw = atoi(weight); if(fw < 100) { fw = 400; } } unsigned int decor = 0; if(decoration) { std::vector tokens; split_string(decoration, tokens, " "); for(auto & token : tokens) { if(!t_strcasecmp(token.c_str(), "underline")) { decor |= font_decoration_underline; } else if(!t_strcasecmp(token.c_str(), "line-through")) { decor |= font_decoration_linethrough; } else if(!t_strcasecmp(token.c_str(), "overline")) { decor |= font_decoration_overline; } } } font_item fi= {0, {}}; fi.font = m_container->create_font(name, size, fw, fs, decor, &fi.metrics); m_container->m_fonts[key] = fi; ret = fi.font; if(fm) { *fm = fi.metrics; } } return ret; } uint_ptr document::get_font( const char* name, int size, const char* weight, const char* style, const char* decoration, font_metrics* fm ) { if(!size) { return 0; } if(!name) { name = m_container->get_default_font_name(); } char strSize[20]; t_itoa(size, strSize, 20, 10); string key = name; key += ":"; key += strSize; key += ":"; key += weight; key += ":"; key += style; key += ":"; key += decoration; auto el = m_container->m_fonts.find(key); if(el != m_container->m_fonts.end()) { if(fm) { *fm = el->second.metrics; } return el->second.font; } return add_font(name, size, weight, style, decoration, fm); } int document::render( int max_width, render_type rt ) { int ret = 0; if(m_root && m_root_render) { position client_rc; m_container->get_client_rect(client_rc); containing_block_context cb_context; cb_context.width = max_width; cb_context.width.type = containing_block_context::cbc_value_type_absolute; cb_context.height = client_rc.height; cb_context.height.type = containing_block_context::cbc_value_type_absolute; if(rt == render_fixed_only) { m_fixed_boxes.clear(); m_root_render->render_positioned(rt); } else { ret = m_root_render->render(0, 0, cb_context, nullptr); if(m_root_render->fetch_positioned()) { m_fixed_boxes.clear(); m_root_render->render_positioned(rt); } m_size.width = 0; m_size.height = 0; m_content_size.width = 0; m_content_size.height = 0; m_root_render->calc_document_size(m_size, m_content_size); } } return ret; } void document::draw( uint_ptr hdc, int x, int y, const position* clip ) { if(m_root && m_root_render) { m_root->draw(hdc, x, y, clip, m_root_render); m_root_render->draw_stacking_context(hdc, x, y, clip, true); } } int document::to_pixels( const css_length& val, const font_metrics& metrics, int size ) const { if(val.is_predefined()) { return 0; } int ret; switch(val.units()) { case css_units_percentage: ret = val.calc_percent(size); break; case css_units_em: ret = round_f(val.val() * (float) metrics.font_size); break; // https://drafts.csswg.org/css-values-4/#absolute-lengths case css_units_pt: ret = m_container->pt_to_px(round_f(val.val())); break; case css_units_in: ret = m_container->pt_to_px(round_f(val.val() * 72)); // 1in = 72pt break; case css_units_pc: ret = m_container->pt_to_px(round_f(val.val() * 12)); // 1pc = (1/6)in = 12pt break; case css_units_cm: ret = m_container->pt_to_px(round_f(val.val() * 0.3937f * 72)); // 1cm = (1/2.54)in = (72/2.54)pt break; case css_units_mm: ret = m_container->pt_to_px(round_f(val.val() * 0.3937f * 72 / 10)); break; case css_units_vw: ret = (int)((double)m_media.width * (double)val.val() / 100.0); break; case css_units_vh: ret = (int)((double)m_media.height * (double)val.val() / 100.0); break; case css_units_vmin: ret = (int)((double)std::min(m_media.height, m_media.width) * (double)val.val() / 100.0); break; case css_units_vmax: ret = (int)((double)std::max(m_media.height, m_media.width) * (double)val.val() / 100.0); break; case css_units_rem: ret = (int) ((double) m_root->css().get_font_size() * (double) val.val()); break; case css_units_ex: ret = (int) ((double) metrics.x_height * val.val()); break; case css_units_ch: ret = (int) ((double) metrics.ch_width * val.val()); break; default: ret = (int) val.val(); break; } return ret; } void document::cvt_units( css_length& val, const font_metrics& metrics, int size ) const { if(val.is_predefined()) { return; } if(val.units() != css_units_percentage) { val.set_value((float)to_pixels(val, metrics, size), css_units_px); } } int document::width() const { return m_size.width; } int document::height() const { return m_size.height; } int document::content_width() const { return m_content_size.width; } int document::content_height() const { return m_content_size.height; } void document::add_stylesheet( const char* str, const char* baseurl, const char* media ) { if(str && str[0]) { m_css.emplace_back(str, baseurl, media); } } bool document::on_mouse_over( int x, int y, int client_x, int client_y, position::vector& redraw_boxes ) { if(!m_root || !m_root_render) { return false; } element::ptr over_el = m_root_render->get_element_by_point(x, y, client_x, client_y); bool state_was_changed = false; if(over_el != m_over_element) { if(m_over_element) { if(m_over_element->on_mouse_leave()) { m_container->on_mouse_event(m_over_element, mouse_event_leave); state_was_changed = true; } } m_over_element = over_el; } string cursor; if(m_over_element) { if(m_over_element->on_mouse_over()) { state_was_changed = true; } cursor = m_over_element->css().get_cursor(); } m_container->set_cursor(cursor.c_str()); if(state_was_changed) { m_container->on_mouse_event(m_over_element, mouse_event_enter); return m_root->find_styles_changes(redraw_boxes); } return false; } bool document::on_mouse_leave( position::vector& redraw_boxes ) { if(!m_root || !m_root_render) { return false; } if(m_over_element) { if(m_over_element->on_mouse_leave()) { m_container->on_mouse_event(m_over_element, mouse_event_leave); return m_root->find_styles_changes(redraw_boxes); } } return false; } bool document::on_lbutton_down( int x, int y, int client_x, int client_y, position::vector& redraw_boxes ) { if(!m_root || !m_root_render) { return false; } element::ptr over_el = m_root_render->get_element_by_point(x, y, client_x, client_y); bool state_was_changed = false; if(over_el != m_over_element) { if(m_over_element) { if(m_over_element->on_mouse_leave()) { m_container->on_mouse_event(m_over_element, mouse_event_leave); state_was_changed = true; } } m_over_element = over_el; if(m_over_element) { if(m_over_element->on_mouse_over()) { state_was_changed = true; } } } string cursor; if(m_over_element) { if(m_over_element->on_lbutton_down()) { state_was_changed = true; } cursor = m_over_element->css().get_cursor(); } m_container->set_cursor(cursor.c_str()); if(state_was_changed) { m_container->on_mouse_event(m_over_element, mouse_event_enter); return m_root->find_styles_changes(redraw_boxes); } return false; } bool document::on_lbutton_up( int /*x*/, int /*y*/, int /*client_x*/, int /*client_y*/, position::vector& redraw_boxes ) { if(!m_root || !m_root_render) { return false; } if(m_over_element) { if(m_over_element->on_lbutton_up()) { return m_root->find_styles_changes(redraw_boxes); } } return false; } void document::get_fixed_boxes( position::vector& fixed_boxes ) { fixed_boxes = m_fixed_boxes; } void document::add_fixed_box( const position& pos ) { m_fixed_boxes.push_back(pos); } bool document::media_changed() { container()->get_media_features(m_media); if (update_media_lists(m_media)) { m_root->refresh_styles(); m_root->compute_styles(); return true; } return false; } bool document::lang_changed() { if (!m_media_lists.empty()) { string culture; container()->get_language(m_lang, culture); if (!culture.empty()) { m_culture = m_lang + '-' + culture; } else { m_culture.clear(); } m_root->refresh_styles(); m_root->compute_styles(); return true; } return false; } // Apply media features (determine which selectors are active). bool document::update_media_lists(const media_features& features) { bool update_styles = false; for (auto& media_list : m_media_lists) { if (media_list->apply_media_features(features)) { update_styles = true; } } return update_styles; } void document::add_media_list(media_query_list_list::ptr list) { if (list && !contains(m_media_lists, list)) m_media_lists.push_back(list); } void document::fix_tables_layout() { for (const auto& el_ptr : m_tabular_elements) { switch (el_ptr->src_el()->css().get_display()) { case display_inline_table: case display_table: fix_table_children(el_ptr, display_table_row_group, "table-row-group"); break; case display_table_footer_group: case display_table_row_group: case display_table_header_group: { auto parent = el_ptr->parent(); if (parent) { if (parent->src_el()->css().get_display() != display_inline_table) fix_table_parent(el_ptr, display_table, "table"); } fix_table_children(el_ptr, display_table_row, "table-row"); } break; case display_table_row: fix_table_parent(el_ptr, display_table_row_group, "table-row-group"); fix_table_children(el_ptr, display_table_cell, "table-cell"); break; case display_table_cell: fix_table_parent(el_ptr, display_table_row, "table-row"); break; // TODO: make table layout fix for table-caption, table-column etc. elements case display_table_caption: case display_table_column: case display_table_column_group: default: break; } } } void document::fix_table_children(const std::shared_ptr& el_ptr, style_display disp, const char* disp_str) { std::list> tmp; auto first_iter = el_ptr->children().begin(); auto cur_iter = el_ptr->children().begin(); auto flush_elements = [&]() { element::ptr annon_tag = std::make_shared(el_ptr->src_el(), string("display:") + disp_str); std::shared_ptr annon_ri; if(annon_tag->css().get_display() == display_table_cell) { annon_tag->set_tagName("table_cell"); annon_ri = std::make_shared(annon_tag); } else if(annon_tag->css().get_display() == display_table_row) { annon_ri = std::make_shared(annon_tag); } else { annon_ri = std::make_shared(annon_tag); } for(const auto& el : tmp) { annon_ri->add_child(el); } // add annon item as tabular for future processing add_tabular(annon_ri); annon_ri->parent(el_ptr); first_iter = el_ptr->children().insert(first_iter, annon_ri); cur_iter = std::next(first_iter); while (cur_iter != el_ptr->children().end() && (*cur_iter)->parent() != el_ptr) { cur_iter = el_ptr->children().erase(cur_iter); } first_iter = cur_iter; tmp.clear(); }; while (cur_iter != el_ptr->children().end()) { if ((*cur_iter)->src_el()->css().get_display() != disp) { if (!(*cur_iter)->src_el()->is_table_skip() || ((*cur_iter)->src_el()->is_table_skip() && !tmp.empty())) { if (disp != display_table_row_group || (*cur_iter)->src_el()->css().get_display() != display_table_caption) { if (tmp.empty()) { first_iter = cur_iter; } tmp.push_back((*cur_iter)); } } cur_iter++; } else if (!tmp.empty()) { flush_elements(); } else { cur_iter++; } } if (!tmp.empty()) { flush_elements(); } } void document::fix_table_parent(const std::shared_ptr& el_ptr, style_display disp, const char* disp_str) { auto parent = el_ptr->parent(); if (parent->src_el()->css().get_display() != disp) { auto this_element = std::find_if(parent->children().begin(), parent->children().end(), [&](const std::shared_ptr& el) { if (el == el_ptr) { return true; } return false; } ); if (this_element != parent->children().end()) { style_display el_disp = el_ptr->src_el()->css().get_display(); auto first = this_element; auto last = this_element; auto cur = this_element; // find first element with same display while (true) { if (cur == parent->children().begin()) break; cur--; if ((*cur)->src_el()->is_table_skip() || (*cur)->src_el()->css().get_display() == el_disp) { first = cur; } else { break; } } // find last element with same display cur = this_element; while (true) { cur++; if (cur == parent->children().end()) break; if ((*cur)->src_el()->is_table_skip() || (*cur)->src_el()->css().get_display() == el_disp) { last = cur; } else { break; } } // extract elements with the same display and wrap them with anonymous object element::ptr annon_tag = std::make_shared(parent->src_el(), string("display:") + disp_str); std::shared_ptr annon_ri; if(annon_tag->css().get_display() == display_table || annon_tag->css().get_display() == display_inline_table) { annon_ri = std::make_shared(annon_tag); } else if(annon_tag->css().get_display() == display_table_row) { annon_ri = std::make_shared(annon_tag); } else { annon_ri = std::make_shared(annon_tag); } std::for_each(first, std::next(last, 1), [&annon_ri](std::shared_ptr& el) { annon_ri->add_child(el); } ); first = parent->children().erase(first, std::next(last)); parent->children().insert(first, annon_ri); add_tabular(annon_ri); annon_ri->parent(parent); } } } void document::append_children_from_string(element& parent, const char* str) { // parent must belong to this document if (parent.get_document().get() != this) { return; } // parse document into GumboOutput GumboOutput* output = gumbo_parse(str); // Create litehtml::elements. elements_list child_elements; create_node(output->root, child_elements, true); // Destroy GumboOutput gumbo_destroy_output(&kGumboDefaultOptions, output); // Let's process created elements tree for (const auto& child : child_elements) { // Add the child element to parent parent.appendChild(child); // apply master CSS child->apply_stylesheet(m_master_css); // parse elements attributes child->parse_attributes(); // Apply parsed styles. child->apply_stylesheet(m_styles); // Apply user styles if any child->apply_stylesheet(m_user_css); // Initialize m_css child->compute_styles(); // Now the m_tabular_elements is filled with tabular elements. // We have to check the tabular elements for missing table elements // and create the anonymous boxes in visual table layout fix_tables_layout(); // Finally initialize elements //child->init(); } } void document::dump(dumper& cout) { if(m_root_render) { m_root_render->dump(cout); } } } // namespace litehtml