mirror of
https://github.com/m1ngsama/TUT.git
synced 2025-12-24 10:51:46 +00:00
feat: Add inline link display and vim-style quick navigation
Major improvements to link handling and navigation: Features: - Display links inline with numbered indicators [0], [1], etc. - Quick navigation: type number + Enter to jump to link - Fast follow: press 'f' + number to open link directly - Visual improvements: links shown with underline and highlight - Remove separate link list at bottom for better readability Technical changes: - Add InlineLink structure to track link positions in text - Implement wrap_text_with_links() for intelligent text wrapping - Add GOTO_LINK and FOLLOW_LINK_NUM actions - Implement LINK input mode for 'f' command - Character-by-character rendering for proper link highlighting - Update help documentation with new navigation methods Usage examples: - 3<Enter> : Jump to link 3 - f5 or 5f : Open link 5 directly - Tab/Enter : Traditional navigation still works All comments converted to standard Unix style (English).
This commit is contained in:
parent
354133b500
commit
ea71b0ca02
9 changed files with 540 additions and 42 deletions
76
LINK_NAVIGATION.md
Normal file
76
LINK_NAVIGATION.md
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
# Quick Link Navigation Guide
|
||||
|
||||
The browser now supports vim-style quick navigation to links!
|
||||
|
||||
## Features
|
||||
|
||||
### 1. Visual Link Numbers
|
||||
All links are displayed inline in the text with numbers like `[0]`, `[1]`, `[2]`, etc.
|
||||
- Links are shown with yellow color and underline
|
||||
- Active link (selected with Tab) has yellow background
|
||||
|
||||
### 2. Quick Navigation Methods
|
||||
|
||||
#### Method 1: Number + Enter
|
||||
Type a number and press Enter to jump to that link:
|
||||
```
|
||||
3<Enter> - Jump to link [3]
|
||||
10<Enter> - Jump to link [10]
|
||||
```
|
||||
|
||||
#### Method 2: 'f' command (follow)
|
||||
Press `f` followed by a number to immediately open that link:
|
||||
```
|
||||
f3 - Open link [3] directly
|
||||
f10 - Open link [10] directly
|
||||
```
|
||||
|
||||
Or type the number first:
|
||||
```
|
||||
3f - Open link [3] directly
|
||||
10f - Open link [10] directly
|
||||
```
|
||||
|
||||
#### Method 3: Traditional Tab navigation (still works)
|
||||
```
|
||||
Tab - Next link
|
||||
Shift-Tab/T - Previous link
|
||||
Enter - Follow current highlighted link
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
Given a page with these links:
|
||||
- "Google[0]"
|
||||
- "GitHub[1]"
|
||||
- "Wikipedia[2]"
|
||||
|
||||
You can:
|
||||
- Press `1<Enter>` to select GitHub link
|
||||
- Press `f2` to immediately open Wikipedia
|
||||
- Press `Tab` twice then `Enter` to open Wikipedia
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Test with a real website
|
||||
./tut https://example.com
|
||||
|
||||
# View help
|
||||
./tut
|
||||
# Press ? for help
|
||||
```
|
||||
|
||||
## Key Bindings Summary
|
||||
|
||||
| Command | Action |
|
||||
|---------|--------|
|
||||
| `[N]<Enter>` | Jump to link N |
|
||||
| `f[N]` or `[N]f` | Open link N directly |
|
||||
| `Tab` | Next link |
|
||||
| `Shift-Tab` / `T` | Previous link |
|
||||
| `Enter` | Follow current link |
|
||||
| `h` | Go back |
|
||||
| `l` | Go forward |
|
||||
|
||||
All standard vim navigation keys (j/k, gg/G, /, n/N) still work as before!
|
||||
|
|
@ -138,7 +138,67 @@ public:
|
|||
int line_idx = scroll_pos + i;
|
||||
const auto& line = rendered_lines[line_idx];
|
||||
|
||||
if (line.is_link && line.link_index == current_link) {
|
||||
// Check if this line contains the active link
|
||||
bool has_active_link = (line.is_link && line.link_index == current_link);
|
||||
|
||||
// Check if this line is in search results
|
||||
bool in_search_results = !search_term.empty() &&
|
||||
std::find(search_results.begin(), search_results.end(), line_idx) != search_results.end();
|
||||
|
||||
// If line has link ranges, render character by character with proper highlighting
|
||||
if (!line.link_ranges.empty()) {
|
||||
int col = 0;
|
||||
for (size_t char_idx = 0; char_idx < line.text.length(); ++char_idx) {
|
||||
// Check if this character is within any link range
|
||||
bool is_in_link = false;
|
||||
|
||||
for (const auto& [start, end] : line.link_ranges) {
|
||||
if (char_idx >= start && char_idx < end) {
|
||||
is_in_link = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply appropriate color
|
||||
if (is_in_link && has_active_link) {
|
||||
attron(COLOR_PAIR(COLOR_LINK_ACTIVE));
|
||||
} else if (is_in_link) {
|
||||
attron(COLOR_PAIR(COLOR_LINK));
|
||||
attron(A_UNDERLINE);
|
||||
} else {
|
||||
attron(COLOR_PAIR(line.color_pair));
|
||||
if (line.is_bold) {
|
||||
attron(A_BOLD);
|
||||
}
|
||||
}
|
||||
|
||||
if (in_search_results) {
|
||||
attron(A_REVERSE);
|
||||
}
|
||||
|
||||
mvaddch(i, col, line.text[char_idx]);
|
||||
|
||||
if (in_search_results) {
|
||||
attroff(A_REVERSE);
|
||||
}
|
||||
|
||||
if (is_in_link && has_active_link) {
|
||||
attroff(COLOR_PAIR(COLOR_LINK_ACTIVE));
|
||||
} else if (is_in_link) {
|
||||
attroff(A_UNDERLINE);
|
||||
attroff(COLOR_PAIR(COLOR_LINK));
|
||||
} else {
|
||||
if (line.is_bold) {
|
||||
attroff(A_BOLD);
|
||||
}
|
||||
attroff(COLOR_PAIR(line.color_pair));
|
||||
}
|
||||
|
||||
col++;
|
||||
}
|
||||
} else {
|
||||
// No inline links, render normally
|
||||
if (has_active_link) {
|
||||
attron(COLOR_PAIR(COLOR_LINK_ACTIVE));
|
||||
} else {
|
||||
attron(COLOR_PAIR(line.color_pair));
|
||||
|
|
@ -147,19 +207,17 @@ public:
|
|||
}
|
||||
}
|
||||
|
||||
if (!search_term.empty() &&
|
||||
std::find(search_results.begin(), search_results.end(), line_idx) != search_results.end()) {
|
||||
if (in_search_results) {
|
||||
attron(A_REVERSE);
|
||||
}
|
||||
|
||||
mvprintw(i, 0, "%s", line.text.c_str());
|
||||
|
||||
if (!search_term.empty() &&
|
||||
std::find(search_results.begin(), search_results.end(), line_idx) != search_results.end()) {
|
||||
if (in_search_results) {
|
||||
attroff(A_REVERSE);
|
||||
}
|
||||
|
||||
if (line.is_link && line.link_index == current_link) {
|
||||
if (has_active_link) {
|
||||
attroff(COLOR_PAIR(COLOR_LINK_ACTIVE));
|
||||
} else {
|
||||
if (line.is_bold) {
|
||||
|
|
@ -168,6 +226,7 @@ public:
|
|||
attroff(COLOR_PAIR(line.color_pair));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
draw_status_bar();
|
||||
}
|
||||
|
|
@ -229,6 +288,26 @@ public:
|
|||
}
|
||||
break;
|
||||
|
||||
case Action::GOTO_LINK:
|
||||
// Jump to specific link by number
|
||||
if (result.number >= 0 && result.number < static_cast<int>(current_doc.links.size())) {
|
||||
current_link = result.number;
|
||||
scroll_to_link(current_link);
|
||||
status_message = "Link " + std::to_string(result.number);
|
||||
} else {
|
||||
status_message = "Invalid link number: " + std::to_string(result.number);
|
||||
}
|
||||
break;
|
||||
|
||||
case Action::FOLLOW_LINK_NUM:
|
||||
// Follow specific link by number directly
|
||||
if (result.number >= 0 && result.number < static_cast<int>(current_doc.links.size())) {
|
||||
load_page(current_doc.links[result.number].url);
|
||||
} else {
|
||||
status_message = "Invalid link number: " + std::to_string(result.number);
|
||||
}
|
||||
break;
|
||||
|
||||
case Action::GO_BACK:
|
||||
if (history_pos > 0) {
|
||||
history_pos--;
|
||||
|
|
@ -332,9 +411,12 @@ public:
|
|||
<< "<p>G: Go to bottom</p>"
|
||||
<< "<p>[number]G: Go to line number</p>"
|
||||
<< "<h2>Links</h2>"
|
||||
<< "<p>Links are displayed inline with numbers like [0], [1], etc.</p>"
|
||||
<< "<p>Tab: Next link</p>"
|
||||
<< "<p>Shift-Tab or T: Previous link</p>"
|
||||
<< "<p>Enter: Follow link</p>"
|
||||
<< "<p>Enter: Follow current link</p>"
|
||||
<< "<p>[number]Enter: Jump to link number N</p>"
|
||||
<< "<p>f[number]: Follow link number N directly</p>"
|
||||
<< "<p>h: Go back</p>"
|
||||
<< "<p>l: Go forward</p>"
|
||||
<< "<h2>Search</h2>"
|
||||
|
|
|
|||
|
|
@ -139,6 +139,70 @@ public:
|
|||
return links;
|
||||
}
|
||||
|
||||
// 从HTML中提取文本,同时保留内联链接位置信息
|
||||
std::string extract_text_with_links(const std::string& html,
|
||||
std::vector<Link>& all_links,
|
||||
std::vector<InlineLink>& inline_links) {
|
||||
std::string result;
|
||||
std::regex link_regex(R"(<a\s+[^>]*href\s*=\s*["']([^"']*)["'][^>]*>([\s\S]*?)</a>)",
|
||||
std::regex::icase);
|
||||
|
||||
size_t last_pos = 0;
|
||||
auto begin = std::sregex_iterator(html.begin(), html.end(), link_regex);
|
||||
auto end = std::sregex_iterator();
|
||||
|
||||
// 处理所有链接
|
||||
for (std::sregex_iterator i = begin; i != end; ++i) {
|
||||
std::smatch match = *i;
|
||||
|
||||
// 添加链接前的文本
|
||||
std::string before_link = html.substr(last_pos, match.position() - last_pos);
|
||||
std::string before_text = decode_html_entities(remove_tags(before_link));
|
||||
result += before_text;
|
||||
|
||||
// 提取链接信息
|
||||
std::string link_url = match[1].str();
|
||||
std::string link_text = decode_html_entities(remove_tags(match[2].str()));
|
||||
|
||||
// 跳过空链接或锚点链接
|
||||
if (link_url.empty() || link_url[0] == '#' || link_text.empty()) {
|
||||
result += link_text;
|
||||
last_pos = match.position() + match.length();
|
||||
continue;
|
||||
}
|
||||
|
||||
// 找到这个链接在全局链接列表中的索引
|
||||
int link_index = -1;
|
||||
for (size_t j = 0; j < all_links.size(); ++j) {
|
||||
if (all_links[j].url == link_url && all_links[j].text == link_text) {
|
||||
link_index = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (link_index != -1) {
|
||||
// 记录内联链接位置
|
||||
InlineLink inline_link;
|
||||
inline_link.text = link_text;
|
||||
inline_link.url = link_url;
|
||||
inline_link.start_pos = result.length();
|
||||
inline_link.end_pos = result.length() + link_text.length();
|
||||
inline_link.link_index = link_index;
|
||||
inline_links.push_back(inline_link);
|
||||
}
|
||||
|
||||
// 添加链接文本
|
||||
result += link_text;
|
||||
last_pos = match.position() + match.length();
|
||||
}
|
||||
|
||||
// 添加最后一段文本
|
||||
std::string remaining = html.substr(last_pos);
|
||||
result += decode_html_entities(remove_tags(remaining));
|
||||
|
||||
return trim(result);
|
||||
}
|
||||
|
||||
// 清理空白字符
|
||||
std::string trim(const std::string& str) {
|
||||
auto start = str.begin();
|
||||
|
|
@ -237,14 +301,13 @@ ParsedDocument HtmlParser::parse(const std::string& html, const std::string& bas
|
|||
}
|
||||
}
|
||||
|
||||
// 解析段落
|
||||
// 解析段落 (保留内联链接)
|
||||
auto paragraphs = pImpl->extract_all_tags(main_content, "p");
|
||||
for (const auto& para : paragraphs) {
|
||||
std::string text = pImpl->decode_html_entities(pImpl->trim(pImpl->remove_tags(para)));
|
||||
if (!text.empty() && text.length() > 1) {
|
||||
ContentElement elem;
|
||||
elem.type = ElementType::PARAGRAPH;
|
||||
elem.text = text;
|
||||
elem.text = pImpl->extract_text_with_links(para, doc.links, elem.inline_links);
|
||||
if (!elem.text.empty() && elem.text.length() > 1) {
|
||||
doc.elements.push_back(elem);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,11 +24,20 @@ struct Link {
|
|||
int position;
|
||||
};
|
||||
|
||||
struct InlineLink {
|
||||
std::string text;
|
||||
std::string url;
|
||||
size_t start_pos; // Position in the text where link starts
|
||||
size_t end_pos; // Position in the text where link ends
|
||||
int link_index; // Index in the document's links array
|
||||
};
|
||||
|
||||
struct ContentElement {
|
||||
ElementType type;
|
||||
std::string text;
|
||||
std::string url;
|
||||
int level;
|
||||
std::vector<InlineLink> inline_links; // Links within this element's text
|
||||
};
|
||||
|
||||
struct ParsedDocument {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ public:
|
|||
result.has_count = false;
|
||||
result.count = 1;
|
||||
|
||||
// Handle digit input for count or 'f' command
|
||||
if (std::isdigit(ch) && (ch != '0' || !count_buffer.empty())) {
|
||||
count_buffer += static_cast<char>(ch);
|
||||
return result;
|
||||
|
|
@ -31,33 +32,38 @@ public:
|
|||
if (!count_buffer.empty()) {
|
||||
result.has_count = true;
|
||||
result.count = std::stoi(count_buffer);
|
||||
count_buffer.clear();
|
||||
}
|
||||
|
||||
switch (ch) {
|
||||
case 'j':
|
||||
case KEY_DOWN:
|
||||
result.action = Action::SCROLL_DOWN;
|
||||
count_buffer.clear();
|
||||
break;
|
||||
case 'k':
|
||||
case KEY_UP:
|
||||
result.action = Action::SCROLL_UP;
|
||||
count_buffer.clear();
|
||||
break;
|
||||
case 'h':
|
||||
case KEY_LEFT:
|
||||
result.action = Action::GO_BACK;
|
||||
count_buffer.clear();
|
||||
break;
|
||||
case 'l':
|
||||
case KEY_RIGHT:
|
||||
result.action = Action::GO_FORWARD;
|
||||
count_buffer.clear();
|
||||
break;
|
||||
case 4:
|
||||
case ' ':
|
||||
result.action = Action::SCROLL_PAGE_DOWN;
|
||||
count_buffer.clear();
|
||||
break;
|
||||
case 21:
|
||||
case 'b':
|
||||
result.action = Action::SCROLL_PAGE_UP;
|
||||
count_buffer.clear();
|
||||
break;
|
||||
case 'g':
|
||||
buffer += 'g';
|
||||
|
|
@ -65,6 +71,7 @@ public:
|
|||
result.action = Action::GOTO_TOP;
|
||||
buffer.clear();
|
||||
}
|
||||
count_buffer.clear();
|
||||
break;
|
||||
case 'G':
|
||||
if (result.has_count) {
|
||||
|
|
@ -73,27 +80,52 @@ public:
|
|||
} else {
|
||||
result.action = Action::GOTO_BOTTOM;
|
||||
}
|
||||
count_buffer.clear();
|
||||
break;
|
||||
case '/':
|
||||
mode = InputMode::SEARCH;
|
||||
buffer = "/";
|
||||
count_buffer.clear();
|
||||
break;
|
||||
case 'n':
|
||||
result.action = Action::SEARCH_NEXT;
|
||||
count_buffer.clear();
|
||||
break;
|
||||
case 'N':
|
||||
result.action = Action::SEARCH_PREV;
|
||||
count_buffer.clear();
|
||||
break;
|
||||
case '\t':
|
||||
result.action = Action::NEXT_LINK;
|
||||
count_buffer.clear();
|
||||
break;
|
||||
case KEY_BTAB:
|
||||
case 'T':
|
||||
result.action = Action::PREV_LINK;
|
||||
count_buffer.clear();
|
||||
break;
|
||||
case '\n':
|
||||
case '\r':
|
||||
// If count buffer has a number, jump to that link
|
||||
if (result.has_count) {
|
||||
result.action = Action::GOTO_LINK;
|
||||
result.number = result.count;
|
||||
} else {
|
||||
result.action = Action::FOLLOW_LINK;
|
||||
}
|
||||
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";
|
||||
}
|
||||
break;
|
||||
case ':':
|
||||
mode = InputMode::COMMAND;
|
||||
|
|
@ -190,6 +222,41 @@ public:
|
|||
|
||||
return result;
|
||||
}
|
||||
|
||||
InputResult process_link_mode(int ch) {
|
||||
InputResult result;
|
||||
result.action = Action::NONE;
|
||||
|
||||
if (std::isdigit(ch)) {
|
||||
buffer += static_cast<char>(ch);
|
||||
} else if (ch == '\n' || ch == '\r') {
|
||||
// Follow the link number entered
|
||||
if (buffer.length() > 1) {
|
||||
try {
|
||||
int link_num = std::stoi(buffer.substr(1));
|
||||
result.action = Action::FOLLOW_LINK_NUM;
|
||||
result.number = link_num;
|
||||
} catch (...) {
|
||||
set_status("Invalid link number");
|
||||
}
|
||||
}
|
||||
mode = InputMode::NORMAL;
|
||||
buffer.clear();
|
||||
} else if (ch == 27) {
|
||||
// ESC cancels
|
||||
mode = InputMode::NORMAL;
|
||||
buffer.clear();
|
||||
} else if (ch == KEY_BACKSPACE || ch == 127 || ch == 8) {
|
||||
if (buffer.length() > 1) {
|
||||
buffer.pop_back();
|
||||
} else {
|
||||
mode = InputMode::NORMAL;
|
||||
buffer.clear();
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
InputHandler::InputHandler() : pImpl(std::make_unique<Impl>()) {}
|
||||
|
|
@ -204,6 +271,8 @@ InputResult InputHandler::handle_key(int ch) {
|
|||
return pImpl->process_command_mode(ch);
|
||||
case InputMode::SEARCH:
|
||||
return pImpl->process_search_mode(ch);
|
||||
case InputMode::LINK:
|
||||
return pImpl->process_link_mode(ch);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ enum class Action {
|
|||
NEXT_LINK,
|
||||
PREV_LINK,
|
||||
FOLLOW_LINK,
|
||||
GOTO_LINK, // Jump to specific link by number
|
||||
FOLLOW_LINK_NUM, // Follow specific link by number (f command)
|
||||
GO_BACK,
|
||||
GO_FORWARD,
|
||||
OPEN_URL,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,12 @@ class TextRenderer::Impl {
|
|||
public:
|
||||
RenderConfig config;
|
||||
|
||||
struct LinkPosition {
|
||||
int link_index;
|
||||
size_t start;
|
||||
size_t end;
|
||||
};
|
||||
|
||||
std::vector<std::string> wrap_text(const std::string& text, int width) {
|
||||
std::vector<std::string> lines;
|
||||
if (text.empty()) {
|
||||
|
|
@ -50,6 +56,151 @@ public:
|
|||
return lines;
|
||||
}
|
||||
|
||||
// Wrap text with links, tracking link positions and adding link numbers
|
||||
std::vector<std::pair<std::string, std::vector<LinkPosition>>>
|
||||
wrap_text_with_links(const std::string& original_text, int width,
|
||||
const std::vector<InlineLink>& inline_links) {
|
||||
std::vector<std::pair<std::string, std::vector<LinkPosition>>> result;
|
||||
|
||||
// If no links, use simple wrapping
|
||||
if (inline_links.empty()) {
|
||||
auto wrapped = wrap_text(original_text, width);
|
||||
for (const auto& line : wrapped) {
|
||||
result.push_back({line, {}});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Build modified text with link numbers inserted
|
||||
std::string text;
|
||||
std::vector<InlineLink> modified_links;
|
||||
size_t text_pos = 0;
|
||||
|
||||
for (const auto& link : inline_links) {
|
||||
// Add text before link
|
||||
if (link.start_pos > text_pos) {
|
||||
text += original_text.substr(text_pos, link.start_pos - text_pos);
|
||||
}
|
||||
|
||||
// Add link text with number indicator
|
||||
size_t link_start_in_modified = text.length();
|
||||
std::string link_text = original_text.substr(link.start_pos, link.end_pos - link.start_pos);
|
||||
std::string link_indicator = "[" + std::to_string(link.link_index) + "]";
|
||||
text += link_text + link_indicator;
|
||||
|
||||
// Store modified link position (including the indicator)
|
||||
InlineLink mod_link = link;
|
||||
mod_link.start_pos = link_start_in_modified;
|
||||
mod_link.end_pos = text.length();
|
||||
modified_links.push_back(mod_link);
|
||||
|
||||
text_pos = link.end_pos;
|
||||
}
|
||||
|
||||
// Add remaining text after last link
|
||||
if (text_pos < original_text.length()) {
|
||||
text += original_text.substr(text_pos);
|
||||
}
|
||||
|
||||
// Split text into words
|
||||
std::vector<std::string> words;
|
||||
std::vector<size_t> word_positions;
|
||||
|
||||
size_t pos = 0;
|
||||
while (pos < text.length()) {
|
||||
// Skip whitespace
|
||||
while (pos < text.length() && std::isspace(text[pos])) {
|
||||
pos++;
|
||||
}
|
||||
if (pos >= text.length()) break;
|
||||
|
||||
// Extract word
|
||||
size_t word_start = pos;
|
||||
while (pos < text.length() && !std::isspace(text[pos])) {
|
||||
pos++;
|
||||
}
|
||||
|
||||
words.push_back(text.substr(word_start, pos - word_start));
|
||||
word_positions.push_back(word_start);
|
||||
}
|
||||
|
||||
// Build lines
|
||||
std::string current_line;
|
||||
std::vector<LinkPosition> current_links;
|
||||
|
||||
for (size_t i = 0; i < words.size(); ++i) {
|
||||
const auto& word = words[i];
|
||||
size_t word_pos = word_positions[i];
|
||||
|
||||
bool can_fit = current_line.empty()
|
||||
? word.length() <= static_cast<size_t>(width)
|
||||
: current_line.length() + 1 + word.length() <= static_cast<size_t>(width);
|
||||
|
||||
if (!can_fit && !current_line.empty()) {
|
||||
// Save current line
|
||||
result.push_back({current_line, current_links});
|
||||
current_line.clear();
|
||||
current_links.clear();
|
||||
}
|
||||
|
||||
// Add word to current line
|
||||
if (!current_line.empty()) {
|
||||
current_line += " ";
|
||||
}
|
||||
size_t word_start_in_line = current_line.length();
|
||||
current_line += word;
|
||||
|
||||
// Check if this word overlaps with any links
|
||||
for (const auto& link : modified_links) {
|
||||
size_t word_end = word_pos + word.length();
|
||||
|
||||
// Check for overlap
|
||||
if (word_pos < link.end_pos && word_end > link.start_pos) {
|
||||
// Calculate link position in current line
|
||||
size_t link_start_in_line = word_start_in_line;
|
||||
if (link.start_pos > word_pos) {
|
||||
link_start_in_line += (link.start_pos - word_pos);
|
||||
}
|
||||
|
||||
size_t link_end_in_line = word_start_in_line + word.length();
|
||||
if (link.end_pos < word_end) {
|
||||
link_end_in_line -= (word_end - link.end_pos);
|
||||
}
|
||||
|
||||
// Check if link already added
|
||||
bool already_added = false;
|
||||
for (auto& existing : current_links) {
|
||||
if (existing.link_index == link.link_index) {
|
||||
// Extend existing link range
|
||||
existing.end = link_end_in_line;
|
||||
already_added = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!already_added) {
|
||||
LinkPosition lp;
|
||||
lp.link_index = link.link_index;
|
||||
lp.start = link_start_in_line;
|
||||
lp.end = link_end_in_line;
|
||||
current_links.push_back(lp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add last line
|
||||
if (!current_line.empty()) {
|
||||
result.push_back({current_line, current_links});
|
||||
}
|
||||
|
||||
if (result.empty()) {
|
||||
result.push_back({"", {}});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string add_indent(const std::string& text, int indent) {
|
||||
return std::string(indent, ' ') + text;
|
||||
}
|
||||
|
|
@ -167,18 +318,38 @@ std::vector<RenderedLine> TextRenderer::render(const ParsedDocument& doc, int sc
|
|||
break;
|
||||
}
|
||||
|
||||
auto wrapped_lines = pImpl->wrap_text(elem.text, content_width - prefix.length());
|
||||
for (size_t i = 0; i < wrapped_lines.size(); ++i) {
|
||||
auto wrapped_with_links = pImpl->wrap_text_with_links(elem.text,
|
||||
content_width - prefix.length(),
|
||||
elem.inline_links);
|
||||
|
||||
for (size_t i = 0; i < wrapped_with_links.size(); ++i) {
|
||||
const auto& [line_text, link_positions] = wrapped_with_links[i];
|
||||
RenderedLine line;
|
||||
|
||||
if (i == 0) {
|
||||
line.text = std::string(margin, ' ') + prefix + wrapped_lines[i];
|
||||
line.text = std::string(margin, ' ') + prefix + line_text;
|
||||
} else {
|
||||
line.text = std::string(margin + prefix.length(), ' ') + wrapped_lines[i];
|
||||
line.text = std::string(margin + prefix.length(), ' ') + line_text;
|
||||
}
|
||||
|
||||
line.color_pair = color;
|
||||
line.is_bold = bold;
|
||||
|
||||
// Store link information
|
||||
if (!link_positions.empty()) {
|
||||
line.is_link = true;
|
||||
line.link_index = link_positions[0].link_index; // Primary link for Tab navigation
|
||||
|
||||
// Adjust link positions for margin and prefix
|
||||
size_t offset = (i == 0) ? (margin + prefix.length()) : (margin + prefix.length());
|
||||
for (const auto& lp : link_positions) {
|
||||
line.link_ranges.push_back({lp.start + offset, lp.end + offset});
|
||||
}
|
||||
} else {
|
||||
line.is_link = false;
|
||||
line.link_index = -1;
|
||||
}
|
||||
|
||||
lines.push_back(line);
|
||||
}
|
||||
|
||||
|
|
@ -198,7 +369,8 @@ std::vector<RenderedLine> TextRenderer::render(const ParsedDocument& doc, int sc
|
|||
}
|
||||
}
|
||||
|
||||
if (!doc.links.empty() && pImpl->config.show_link_indicators) {
|
||||
// Don't show separate links section if inline links are displayed
|
||||
if (!doc.links.empty() && !pImpl->config.show_link_indicators) {
|
||||
RenderedLine separator;
|
||||
std::string sepline(content_width, '-');
|
||||
separator.text = std::string(margin, ' ') + sepline;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ struct RenderedLine {
|
|||
bool is_bold;
|
||||
bool is_link;
|
||||
int link_index;
|
||||
std::vector<std::pair<size_t, size_t>> link_ranges; // (start, end) positions of links in this line
|
||||
};
|
||||
|
||||
struct RenderConfig {
|
||||
|
|
@ -19,7 +20,7 @@ struct RenderConfig {
|
|||
int margin_left = 0;
|
||||
bool center_content = true;
|
||||
int paragraph_spacing = 1;
|
||||
bool show_link_indicators = true;
|
||||
bool show_link_indicators = false; // Set to false to show inline links by default
|
||||
};
|
||||
|
||||
class TextRenderer {
|
||||
|
|
|
|||
24
test_inline_links.html
Normal file
24
test_inline_links.html
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test Inline Links</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Test Page for Inline Links</h1>
|
||||
|
||||
<p>This is a paragraph with an <a href="https://example.com">inline link</a> in the middle of the text. You should be able to see the link highlighted directly in the text.</p>
|
||||
|
||||
<p>Here is another paragraph with multiple links: <a href="https://google.com">Google</a> and <a href="https://github.com">GitHub</a> are both popular websites.</p>
|
||||
|
||||
<p>This paragraph has a longer link text: <a href="https://en.wikipedia.org">Wikipedia is a free online encyclopedia</a> that anyone can edit.</p>
|
||||
|
||||
<h2>More Examples</h2>
|
||||
|
||||
<p>Press Tab to navigate between links, and Enter to follow them. The links should be <a href="https://example.com/test1">highlighted</a> directly in the text, not listed separately at the bottom.</p>
|
||||
|
||||
<ul>
|
||||
<li>List item with <a href="https://news.ycombinator.com">Hacker News</a></li>
|
||||
<li>Another item with <a href="https://reddit.com">Reddit</a></li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in a new issue