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("", 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;
}
|