diff --git a/src/html_parser.cpp b/src/html_parser.cpp index adedcbf..59d49bc 100644 --- a/src/html_parser.cpp +++ b/src/html_parser.cpp @@ -3,6 +3,7 @@ #include #include #include +#include class HtmlParser::Impl { public: @@ -234,6 +235,133 @@ public: return result; } + + // Extract images + std::vector extract_images(const std::string& html) { + std::vector images; + std::regex img_regex(R"(]*src\s*=\s*["']([^"']*)["'][^>]*>)", std::regex::icase); + + auto begin = std::sregex_iterator(html.begin(), html.end(), img_regex); + auto end = std::sregex_iterator(); + + for (std::sregex_iterator i = begin; i != end; ++i) { + std::smatch match = *i; + Image img; + img.src = match[1].str(); + img.width = -1; + img.height = -1; + + // Extract alt text + std::string img_tag = match[0].str(); + std::regex alt_regex(R"(alt\s*=\s*["']([^"']*)["'])", std::regex::icase); + std::smatch alt_match; + if (std::regex_search(img_tag, alt_match, alt_regex)) { + img.alt = decode_html_entities(alt_match[1].str()); + } + + // Extract width + std::regex width_regex(R"(width\s*=\s*["']?(\d+)["']?)", std::regex::icase); + std::smatch width_match; + if (std::regex_search(img_tag, width_match, width_regex)) { + try { + img.width = std::stoi(width_match[1].str()); + } catch (...) {} + } + + // Extract height + std::regex height_regex(R"(height\s*=\s*["']?(\d+)["']?)", std::regex::icase); + std::smatch height_match; + if (std::regex_search(img_tag, height_match, height_regex)) { + try { + img.height = std::stoi(height_match[1].str()); + } catch (...) {} + } + + images.push_back(img); + } + + return images; + } + + // Extract tables + std::vector extract_tables(const std::string& html, std::vector& all_links) { + std::vector
tables; + auto table_contents = extract_all_tags(html, "table"); + + for (const auto& table_html : table_contents) { + Table table; + table.has_header = false; + + // Extract rows + auto thead_html = extract_tag_content(table_html, "thead"); + auto tbody_html = extract_tag_content(table_html, "tbody"); + + // If no thead/tbody, just get all rows + std::vector row_htmls; + if (!thead_html.empty() || !tbody_html.empty()) { + if (!thead_html.empty()) { + auto header_rows = extract_all_tags(thead_html, "tr"); + row_htmls.insert(row_htmls.end(), header_rows.begin(), header_rows.end()); + table.has_header = !header_rows.empty(); + } + if (!tbody_html.empty()) { + auto body_rows = extract_all_tags(tbody_html, "tr"); + row_htmls.insert(row_htmls.end(), body_rows.begin(), body_rows.end()); + } + } else { + row_htmls = extract_all_tags(table_html, "tr"); + // Check if first row has
tags + if (!row_htmls.empty()) { + table.has_header = (row_htmls[0].find(" inline_links; + cell.text = extract_text_with_links(cell_html, all_links, inline_links); + cell.inline_links = inline_links; + cell.is_header = true; + cell.colspan = 1; + cell.rowspan = 1; + row.cells.push_back(cell); + } + + // Process td cells (data) + for (const auto& cell_html : td_cells) { + TableCell cell; + std::vector inline_links; + cell.text = extract_text_with_links(cell_html, all_links, inline_links); + cell.inline_links = inline_links; + cell.is_header = is_first_row && table.has_header && th_cells.empty(); + cell.colspan = 1; + cell.rowspan = 1; + row.cells.push_back(cell); + } + + if (!row.cells.empty()) { + table.rows.push_back(row); + } + + is_first_row = false; + } + + if (!table.rows.empty()) { + tables.push_back(table); + } + } + + return tables; + } }; HtmlParser::HtmlParser() : pImpl(std::make_unique()) {} @@ -271,33 +399,117 @@ ParsedDocument HtmlParser::parse(const std::string& html, const std::string& bas // 提取链接 doc.links = pImpl->extract_links(main_content, base_url); + // Extract and add images + auto images = pImpl->extract_images(main_content); + for (const auto& img : images) { + ContentElement elem; + elem.type = ElementType::IMAGE; + elem.image_data = img; + elem.level = 0; + elem.list_number = 0; + elem.nesting_level = 0; + doc.elements.push_back(elem); + } + + // Extract and add tables + auto tables = pImpl->extract_tables(main_content, doc.links); + for (const auto& tbl : tables) { + ContentElement elem; + elem.type = ElementType::TABLE; + elem.table_data = tbl; + elem.level = 0; + elem.list_number = 0; + elem.nesting_level = 0; + doc.elements.push_back(elem); + } + // 解析标题 for (int level = 1; level <= 6; ++level) { std::string tag = "h" + std::to_string(level); auto headings = pImpl->extract_all_tags(main_content, tag); for (const auto& heading : headings) { ContentElement elem; - elem.type = (level == 1) ? ElementType::HEADING1 : - (level == 2) ? ElementType::HEADING2 : ElementType::HEADING3; + ElementType type; + if (level == 1) type = ElementType::HEADING1; + else if (level == 2) type = ElementType::HEADING2; + else if (level == 3) type = ElementType::HEADING3; + else if (level == 4) type = ElementType::HEADING4; + else if (level == 5) type = ElementType::HEADING5; + else type = ElementType::HEADING6; + + elem.type = type; elem.text = pImpl->decode_html_entities(pImpl->trim(pImpl->remove_tags(heading))); elem.level = level; + elem.list_number = 0; + elem.nesting_level = 0; if (!elem.text.empty()) { doc.elements.push_back(elem); } } } - // 解析列表项 + // 解析列表项 - with nesting support if (pImpl->keep_lists) { - auto list_items = pImpl->extract_all_tags(main_content, "li"); - for (const auto& item : list_items) { - std::string text = pImpl->decode_html_entities(pImpl->trim(pImpl->remove_tags(item))); - if (!text.empty() && text.length() > 1) { - ContentElement elem; - elem.type = ElementType::LIST_ITEM; - elem.text = text; - doc.elements.push_back(elem); + // Extract both
    and
      lists + auto ul_lists = pImpl->extract_all_tags(main_content, "ul"); + auto ol_lists = pImpl->extract_all_tags(main_content, "ol"); + + // Helper to parse a list recursively + std::function parse_list; + parse_list = [&](const std::string& list_html, bool is_ordered, int nesting) { + auto list_items = pImpl->extract_all_tags(list_html, "li"); + int item_number = 1; + + for (const auto& item_html : list_items) { + // Check if this item contains nested lists + bool has_nested_ul = item_html.find("]*>[\\s\\S]*?
", std::regex::icase), ""); + item_text = std::regex_replace(item_text, + std::regex("]*>[\\s\\S]*?", std::regex::icase), ""); + } + + std::string text = pImpl->decode_html_entities(pImpl->trim(pImpl->remove_tags(item_text))); + if (!text.empty() && text.length() > 1) { + ContentElement elem; + elem.type = is_ordered ? ElementType::ORDERED_LIST_ITEM : ElementType::LIST_ITEM; + elem.text = text; + elem.level = 0; + elem.list_number = item_number++; + elem.nesting_level = nesting; + doc.elements.push_back(elem); + } + + // Parse nested lists + if (has_nested_ul) { + auto nested_uls = pImpl->extract_all_tags(item_html, "ul"); + for (const auto& nested_ul : nested_uls) { + parse_list(nested_ul, false, nesting + 1); + } + } + if (has_nested_ol) { + auto nested_ols = pImpl->extract_all_tags(item_html, "ol"); + for (const auto& nested_ol : nested_ols) { + parse_list(nested_ol, true, nesting + 1); + } + } } + }; + + // Parse unordered lists + for (const auto& ul : ul_lists) { + parse_list(ul, false, 0); + } + + // Parse ordered lists + for (const auto& ol : ol_lists) { + parse_list(ol, true, 0); } } @@ -307,6 +519,9 @@ ParsedDocument HtmlParser::parse(const std::string& html, const std::string& bas ContentElement elem; elem.type = ElementType::PARAGRAPH; elem.text = pImpl->extract_text_with_links(para, doc.links, elem.inline_links); + elem.level = 0; + elem.list_number = 0; + elem.nesting_level = 0; if (!elem.text.empty() && elem.text.length() > 1) { doc.elements.push_back(elem); } @@ -321,6 +536,9 @@ ParsedDocument HtmlParser::parse(const std::string& html, const std::string& bas ContentElement elem; elem.type = ElementType::PARAGRAPH; elem.text = text; + elem.level = 0; + elem.list_number = 0; + elem.nesting_level = 0; doc.elements.push_back(elem); } } @@ -339,6 +557,9 @@ ParsedDocument HtmlParser::parse(const std::string& html, const std::string& bas ContentElement elem; elem.type = ElementType::PARAGRAPH; elem.text = line; + elem.level = 0; + elem.list_number = 0; + elem.nesting_level = 0; doc.elements.push_back(elem); } } diff --git a/src/html_parser.h b/src/html_parser.h index 90c2872..ed6bac3 100644 --- a/src/html_parser.h +++ b/src/html_parser.h @@ -9,13 +9,28 @@ enum class ElementType { HEADING1, HEADING2, HEADING3, + HEADING4, + HEADING5, + HEADING6, PARAGRAPH, LINK, LIST_ITEM, + ORDERED_LIST_ITEM, BLOCKQUOTE, CODE_BLOCK, HORIZONTAL_RULE, - LINE_BREAK + LINE_BREAK, + TABLE, + IMAGE, + FORM, + SECTION_START, + SECTION_END, + NAV_START, + NAV_END, + HEADER_START, + HEADER_END, + ASIDE_START, + ASIDE_END }; struct Link { @@ -32,12 +47,57 @@ struct InlineLink { int link_index; // Index in the document's links array }; +struct TableCell { + std::string text; + std::vector inline_links; + bool is_header; + int colspan; + int rowspan; +}; + +struct TableRow { + std::vector cells; +}; + +struct Table { + std::vector rows; + bool has_header; +}; + +struct Image { + std::string src; + std::string alt; + int width; // -1 if not specified + int height; // -1 if not specified +}; + +struct FormField { + enum Type { TEXT, PASSWORD, CHECKBOX, RADIO, SUBMIT, BUTTON } type; + std::string name; + std::string value; + std::string placeholder; + bool checked; +}; + +struct Form { + std::string action; + std::string method; + std::vector fields; +}; + struct ContentElement { ElementType type; std::string text; std::string url; int level; + int list_number; // For ordered lists + int nesting_level; // For nested lists std::vector inline_links; // Links within this element's text + + // Extended content types + Table table_data; + Image image_data; + Form form_data; }; struct ParsedDocument { diff --git a/src/input_handler.cpp b/src/input_handler.cpp index acb1585..11ef411 100644 --- a/src/input_handler.cpp +++ b/src/input_handler.cpp @@ -23,7 +23,48 @@ public: result.has_count = false; result.count = 1; - // Handle digit input for count or 'f' command + // Handle multi-char commands like 'gg', 'gt', 'gT', 'm', ' + if (!buffer.empty()) { + if (buffer == "g") { + if (ch == 't') { + result.action = Action::NEXT_TAB; + buffer.clear(); + count_buffer.clear(); + return result; + } else if (ch == 'T') { + result.action = Action::PREV_TAB; + buffer.clear(); + count_buffer.clear(); + return result; + } + } else if (buffer == "m") { + // Set mark with letter + if (std::isalpha(ch)) { + result.action = Action::SET_MARK; + result.text = std::string(1, static_cast(ch)); + buffer.clear(); + count_buffer.clear(); + return result; + } + buffer.clear(); + count_buffer.clear(); + return result; + } else if (buffer == "'") { + // Jump to mark + if (std::isalpha(ch)) { + result.action = Action::GOTO_MARK; + result.text = std::string(1, static_cast(ch)); + buffer.clear(); + count_buffer.clear(); + return result; + } + buffer.clear(); + count_buffer.clear(); + return result; + } + } + + // Handle digit input for count if (std::isdigit(ch) && (ch != '0' || !count_buffer.empty())) { count_buffer += static_cast(ch); return result; @@ -116,16 +157,33 @@ public: count_buffer.clear(); break; case 'f': - // 'f' command: follow link by number - if (result.has_count) { - result.action = Action::FOLLOW_LINK_NUM; - result.number = result.count; - count_buffer.clear(); - } else { - // Enter link follow mode, wait for number - mode = InputMode::LINK; - buffer = "f"; - } + // 'f' command: vimium-style link hints + result.action = Action::SHOW_LINK_HINTS; + mode = InputMode::LINK_HINTS; + buffer.clear(); + count_buffer.clear(); + break; + case 'v': + // Enter visual mode + result.action = Action::ENTER_VISUAL_MODE; + mode = InputMode::VISUAL; + count_buffer.clear(); + break; + case 'V': + // Enter visual line mode + result.action = Action::ENTER_VISUAL_LINE_MODE; + mode = InputMode::VISUAL_LINE; + count_buffer.clear(); + break; + case 'm': + // Set mark (wait for next char) + buffer = "m"; + count_buffer.clear(); + break; + case '\'': + // Jump to mark (wait for next char) + buffer = "'"; + count_buffer.clear(); break; case ':': mode = InputMode::COMMAND; @@ -257,6 +315,75 @@ public: return result; } + + InputResult process_link_hints_mode(int ch) { + InputResult result; + result.action = Action::NONE; + + if (ch == 27) { + // ESC cancels link hints mode + mode = InputMode::NORMAL; + buffer.clear(); + return result; + } else if (ch == KEY_BACKSPACE || ch == 127 || ch == 8) { + // Backspace removes last character + if (!buffer.empty()) { + buffer.pop_back(); + } else { + mode = InputMode::NORMAL; + } + return result; + } else if (std::isalpha(ch)) { + // Add character to buffer + buffer += std::tolower(static_cast(ch)); + + // Try to match link hint + result.action = Action::FOLLOW_LINK_HINT; + result.text = buffer; + + // Mode will be reset by browser if link is followed + return result; + } + + return result; + } + + InputResult process_visual_mode(int ch) { + InputResult result; + result.action = Action::NONE; + + if (ch == 27 || ch == 'v') { + // ESC or 'v' exits visual mode + mode = InputMode::NORMAL; + return result; + } else if (ch == 'y') { + // Yank (copy) selected text + result.action = Action::YANK; + mode = InputMode::NORMAL; + return result; + } + + // Pass through navigation commands + switch (ch) { + case 'j': + case KEY_DOWN: + result.action = Action::SCROLL_DOWN; + break; + case 'k': + case KEY_UP: + result.action = Action::SCROLL_UP; + break; + case 'h': + case KEY_LEFT: + // In visual mode, h/l could extend selection + break; + case 'l': + case KEY_RIGHT: + break; + } + + return result; + } }; InputHandler::InputHandler() : pImpl(std::make_unique()) {} @@ -273,6 +400,11 @@ InputResult InputHandler::handle_key(int ch) { return pImpl->process_search_mode(ch); case InputMode::LINK: return pImpl->process_link_mode(ch); + case InputMode::LINK_HINTS: + return pImpl->process_link_hints_mode(ch); + case InputMode::VISUAL: + case InputMode::VISUAL_LINE: + return pImpl->process_visual_mode(ch); default: break; } diff --git a/src/input_handler.h b/src/input_handler.h index 0e842bc..bd1fec1 100644 --- a/src/input_handler.h +++ b/src/input_handler.h @@ -8,7 +8,10 @@ enum class InputMode { NORMAL, COMMAND, SEARCH, - LINK + LINK, + LINK_HINTS, // Vimium-style 'f' mode + VISUAL, // Visual mode + VISUAL_LINE // Visual line mode }; enum class Action { @@ -28,12 +31,24 @@ enum class Action { FOLLOW_LINK, GOTO_LINK, // Jump to specific link by number FOLLOW_LINK_NUM, // Follow specific link by number (f command) + SHOW_LINK_HINTS, // Activate link hints mode ('f') + FOLLOW_LINK_HINT, // Follow link by hint letters GO_BACK, GO_FORWARD, OPEN_URL, REFRESH, QUIT, - HELP + HELP, + ENTER_VISUAL_MODE, // Start visual mode + ENTER_VISUAL_LINE_MODE, // Start visual line mode + SET_MARK, // Set a mark (m + letter) + GOTO_MARK, // Jump to mark (' + letter) + YANK, // Copy selected text + NEXT_TAB, // gt - next tab + PREV_TAB, // gT - previous tab + NEW_TAB, // :tabnew + CLOSE_TAB, // :tabc + TOGGLE_MOUSE // Toggle mouse support }; struct InputResult { diff --git a/src/text_renderer.cpp b/src/text_renderer.cpp index e243272..217ad7c 100644 --- a/src/text_renderer.cpp +++ b/src/text_renderer.cpp @@ -2,6 +2,24 @@ #include #include #include +#include + +// Box-drawing characters for tables +namespace BoxChars { + constexpr const char* TOP_LEFT = "┌"; + constexpr const char* TOP_RIGHT = "┐"; + constexpr const char* BOTTOM_LEFT = "└"; + constexpr const char* BOTTOM_RIGHT = "┘"; + constexpr const char* HORIZONTAL = "─"; + constexpr const char* VERTICAL = "│"; + constexpr const char* T_DOWN = "┬"; + constexpr const char* T_UP = "┴"; + constexpr const char* T_RIGHT = "├"; + constexpr const char* T_LEFT = "┤"; + constexpr const char* CROSS = "┼"; + constexpr const char* HEAVY_HORIZONTAL = "━"; + constexpr const char* HEAVY_VERTICAL = "┃"; +} class TextRenderer::Impl { public: @@ -204,6 +222,212 @@ public: std::string add_indent(const std::string& text, int indent) { return std::string(indent, ' ') + text; } + + // Render a table with box-drawing characters + std::vector render_table(const Table& table, int content_width, int margin) { + std::vector lines; + if (table.rows.empty()) return lines; + + // Calculate column widths + size_t num_cols = 0; + for (const auto& row : table.rows) { + num_cols = std::max(num_cols, row.cells.size()); + } + + if (num_cols == 0) return lines; + + std::vector col_widths(num_cols, 0); + int available_width = content_width - (num_cols + 1) * 3; // Account for borders and padding + + // First pass: calculate minimum widths + for (const auto& row : table.rows) { + for (size_t i = 0; i < row.cells.size() && i < num_cols; ++i) { + int cell_len = static_cast(row.cells[i].text.length()); + int max_width = available_width / static_cast(num_cols); + int cell_width = std::min(cell_len, max_width); + col_widths[i] = std::max(col_widths[i], cell_width); + } + } + + // Normalize column widths + int total_width = std::accumulate(col_widths.begin(), col_widths.end(), 0); + if (total_width > available_width) { + // Scale down proportionally + for (auto& width : col_widths) { + width = (width * available_width) / total_width; + width = std::max(width, 5); // Minimum column width + } + } + + // Helper to create separator line + auto create_separator = [&](bool is_top, bool is_bottom, bool is_middle, bool is_header) { + RenderedLine line; + std::string sep = std::string(margin, ' '); + + if (is_top) { + sep += BoxChars::TOP_LEFT; + } else if (is_bottom) { + sep += BoxChars::BOTTOM_LEFT; + } else { + sep += BoxChars::T_RIGHT; + } + + for (size_t i = 0; i < num_cols; ++i) { + const char* horiz = is_header ? BoxChars::HEAVY_HORIZONTAL : BoxChars::HORIZONTAL; + sep += std::string(col_widths[i] + 2, horiz[0]); + + if (i < num_cols - 1) { + if (is_top) { + sep += BoxChars::T_DOWN; + } else if (is_bottom) { + sep += BoxChars::T_UP; + } else { + sep += BoxChars::CROSS; + } + } + } + + if (is_top) { + sep += BoxChars::TOP_RIGHT; + } else if (is_bottom) { + sep += BoxChars::BOTTOM_RIGHT; + } else { + sep += BoxChars::T_LEFT; + } + + line.text = sep; + line.color_pair = COLOR_DIM; + line.is_bold = false; + line.is_link = false; + line.link_index = -1; + return line; + }; + + // Top border + lines.push_back(create_separator(true, false, false, false)); + + // Render rows + bool first_row = true; + for (const auto& row : table.rows) { + bool is_header_row = first_row && table.has_header; + + // Wrap cell contents + std::vector> wrapped_cells(num_cols); + int max_cell_lines = 1; + + for (size_t i = 0; i < row.cells.size() && i < num_cols; ++i) { + const auto& cell = row.cells[i]; + auto cell_lines = wrap_text(cell.text, col_widths[i]); + wrapped_cells[i] = cell_lines; + max_cell_lines = std::max(max_cell_lines, static_cast(cell_lines.size())); + } + + // Render cell lines + for (int line_idx = 0; line_idx < max_cell_lines; ++line_idx) { + RenderedLine line; + std::string line_text = std::string(margin, ' ') + BoxChars::VERTICAL; + + for (size_t col_idx = 0; col_idx < num_cols; ++col_idx) { + std::string cell_text; + if (col_idx < wrapped_cells.size() && line_idx < static_cast(wrapped_cells[col_idx].size())) { + cell_text = wrapped_cells[col_idx][line_idx]; + } + + // Pad to column width + int padding = col_widths[col_idx] - cell_text.length(); + line_text += " " + cell_text + std::string(padding + 1, ' ') + BoxChars::VERTICAL; + } + + line.text = line_text; + line.color_pair = is_header_row ? COLOR_HEADING2 : COLOR_NORMAL; + line.is_bold = is_header_row; + line.is_link = false; + line.link_index = -1; + lines.push_back(line); + } + + // Separator after header or between rows + if (is_header_row) { + lines.push_back(create_separator(false, false, true, true)); + } + + first_row = false; + } + + // Bottom border + lines.push_back(create_separator(false, true, false, false)); + + return lines; + } + + // Render an image placeholder + std::vector render_image(const Image& img, int content_width, int margin) { + std::vector lines; + + // Create a box for the image + std::string img_text = "[IMG"; + if (!img.alt.empty()) { + img_text += ": " + img.alt; + } + img_text += "]"; + + // Truncate if too long + if (static_cast(img_text.length()) > content_width) { + img_text = img_text.substr(0, content_width - 3) + "...]"; + } + + // Top border + RenderedLine top; + top.text = std::string(margin, ' ') + BoxChars::TOP_LEFT + + std::string(img_text.length() + 2, BoxChars::HORIZONTAL[0]) + + BoxChars::TOP_RIGHT; + top.color_pair = COLOR_DIM; + top.is_bold = false; + top.is_link = false; + top.link_index = -1; + lines.push_back(top); + + // Content + RenderedLine content; + content.text = std::string(margin, ' ') + BoxChars::VERTICAL + " " + img_text + " " + BoxChars::VERTICAL; + content.color_pair = COLOR_LINK; + content.is_bold = true; + content.is_link = false; + content.link_index = -1; + lines.push_back(content); + + // Dimensions if available + if (img.width > 0 || img.height > 0) { + std::string dims = " "; + if (img.width > 0) dims += std::to_string(img.width) + "w"; + if (img.width > 0 && img.height > 0) dims += " × "; + if (img.height > 0) dims += std::to_string(img.height) + "h"; + dims += " "; + + RenderedLine dim_line; + int padding = img_text.length() + 2 - dims.length(); + dim_line.text = std::string(margin, ' ') + BoxChars::VERTICAL + dims + + std::string(padding, ' ') + BoxChars::VERTICAL; + dim_line.color_pair = COLOR_DIM; + dim_line.is_bold = false; + dim_line.is_link = false; + dim_line.link_index = -1; + lines.push_back(dim_line); + } + + // Bottom border + RenderedLine bottom; + bottom.text = std::string(margin, ' ') + BoxChars::BOTTOM_LEFT + + std::string(img_text.length() + 2, BoxChars::HORIZONTAL[0]) + + BoxChars::BOTTOM_RIGHT; + bottom.color_pair = COLOR_DIM; + bottom.is_bold = false; + bottom.is_link = false; + bottom.link_index = -1; + lines.push_back(bottom); + + return lines; + } }; TextRenderer::TextRenderer() : pImpl(std::make_unique()) { @@ -300,8 +524,52 @@ std::vector TextRenderer::render(const ParsedDocument& doc, int sc prefix = "> "; break; case ElementType::LIST_ITEM: - prefix = " • "; + { + // Different bullets for different nesting levels + const char* bullets[] = {"•", "◦", "▪", "▫"}; + int indent = elem.nesting_level * 2; + int bullet_idx = elem.nesting_level % 4; + prefix = std::string(indent, ' ') + " " + bullets[bullet_idx] + " "; + } break; + case ElementType::ORDERED_LIST_ITEM: + { + // Numbered lists with proper indentation + int indent = elem.nesting_level * 2; + prefix = std::string(indent, ' ') + " " + + std::to_string(elem.list_number) + ". "; + } + break; + case ElementType::TABLE: + { + auto table_lines = pImpl->render_table(elem.table_data, content_width, margin); + lines.insert(lines.end(), table_lines.begin(), table_lines.end()); + + // Add empty line after table + RenderedLine empty; + empty.text = ""; + empty.color_pair = COLOR_NORMAL; + empty.is_bold = false; + empty.is_link = false; + empty.link_index = -1; + lines.push_back(empty); + continue; + } + case ElementType::IMAGE: + { + auto img_lines = pImpl->render_image(elem.image_data, content_width, margin); + lines.insert(lines.end(), img_lines.begin(), img_lines.end()); + + // Add empty line after image + RenderedLine empty; + empty.text = ""; + empty.color_pair = COLOR_NORMAL; + empty.is_bold = false; + empty.is_link = false; + empty.link_index = -1; + lines.push_back(empty); + continue; + } case ElementType::HORIZONTAL_RULE: { RenderedLine hr; @@ -314,6 +582,13 @@ std::vector TextRenderer::render(const ParsedDocument& doc, int sc lines.push_back(hr); continue; } + case ElementType::HEADING4: + case ElementType::HEADING5: + case ElementType::HEADING6: + color = COLOR_HEADING3; // Use same color as H3 for H4-H6 + bold = true; + prefix = std::string(elem.level, '#') + " "; + break; default: break; }