aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/core/browser.cc188
-rw-r--r--src/net/http_client.cc14
-rw-r--r--src/render/renderer.cc43
3 files changed, 208 insertions, 37 deletions
diff --git a/src/core/browser.cc b/src/core/browser.cc
index 2968ab1..4c42bd0 100644
--- a/src/core/browser.cc
+++ b/src/core/browser.cc
@@ -25,6 +25,7 @@
#include <cstddef>
#include <exception>
#include <iostream>
+#include <ranges>
namespace ricochet::core {
@@ -32,15 +33,57 @@ struct PageData
{
std::vector<std::string> lines;
std::vector<std::string> links;
+ std::string title;
};
namespace {
+
+std::string extract_title( const parser::DomNode& root_node )
+{
+ std::vector<const parser::DomNode*> stack;
+ stack.push_back( &root_node );
+
+ while ( !stack.empty() ) {
+ const parser::DomNode* current = stack.back();
+ stack.pop_back();
+
+ std::string base_tag = current->tag_name;
+ const std::size_t space_pos = base_tag.find( ' ' );
+ if ( space_pos != std::string::npos ) {
+ base_tag = base_tag.substr( 0, space_pos );
+ }
+
+ for ( char& c : base_tag ) {
+ if ( c >= 'A' && c <= 'Z' ) {
+ c = static_cast<char>( c + 32 );
+ }
+ }
+
+ if ( base_tag == "title" ) {
+ std::string full_title;
+ for ( const auto& child : current->children ) {
+ if ( child.tag_name.empty() ) {
+ full_title += child.text_content;
+ }
+ }
+ if ( !full_title.empty() ) {
+ return full_title;
+ }
+ }
+
+ for ( const auto& child : std::ranges::reverse_view( current->children ) ) {
+ stack.push_back( &child );
+ }
+ }
+ return "";
+}
+
PageData load_page( std::string_view url )
{
const net::HttpClient client;
auto response_result = client.fetch( url );
if ( !response_result.has_value() ) {
- return { .lines = { "[!] Failed to load: " + std::string( url ) }, .links = {} };
+ return { .lines = { "[!] Failed to load: " + std::string( url ) }, .links = {}, .title = "Error" };
}
const parser::HtmlLexer lexer;
@@ -61,17 +104,40 @@ PageData load_page( std::string_view url )
lines.push_back( result.text.substr( start, end - start ) );
start = end + 1;
}
- return { .lines = lines, .links = result.links };
+ const std::string title = extract_title( dom_root );
+ return { .lines = lines, .links = result.links, .title = title };
}
void draw_view( const tui::Terminal& terminal,
const std::vector<std::string>& lines,
std::size_t scroll_y,
- std::string_view url )
+ std::string_view url, // NOLINT(bugprone-easily-swappable-parameters)
+ std::string_view title ) // NOLINT(bugprone-easily-swappable-parameters)
{
terminal.clear_screen();
auto [width, height] = terminal.get_size();
- const std::size_t usable_height = ( height > 1 ) ? ( height - 1 ) : 1;
+ const std::size_t usable_height = ( height > 2 ) ? ( height - 2 ) : 1;
+
+ std::string clean_title( title );
+ if ( clean_title.empty() ) {
+ const std::size_t start = url.find( "://" );
+ const std::size_t host_start = ( start == std::string::npos ) ? 0 : start + 3;
+ const std::size_t host_end = url.find( '/', host_start );
+ clean_title = std::string( url.substr( host_start, host_end - host_start ) );
+ }
+ for ( char& c : clean_title ) {
+ if ( c == '\n' || c == '\r' ) {
+ c = ' ';
+ }
+ }
+
+ std::string header = " PAGE: " + clean_title;
+ if ( header.size() < width ) {
+ header.append( width - header.size(), ' ' );
+ } else if ( header.size() > width && width > 3 ) {
+ header = header.substr( 0, width - 3 ) + "...";
+ }
+ std::cout << "\033[7m" << header << "\033[0m\r\n";
for ( std::size_t i = 0; i < usable_height; ++i ) {
if ( scroll_y + i < lines.size() ) {
@@ -80,7 +146,14 @@ void draw_view( const tui::Terminal& terminal,
std::cout << "~\r\n";
}
}
- std::cout << "\033[7m URL: " << url << " | [j/k] Scroll [/] Jump [f] Follow [H/L] History [q] Quit \033[0m";
+
+ std::string footer = " URL: " + std::string( url ) + " | [j/k]Scroll [/]Jump [f]Follow [H/L]History [q]Quit ";
+ if ( footer.size() < width ) {
+ footer.append( width - footer.size(), ' ' );
+ } else if ( footer.size() > width && width > 3 ) {
+ footer = footer.substr( 0, width - 3 ) + "...";
+ }
+ std::cout << "\033[7m" << footer << "\033[0m";
std::cout.flush();
}
@@ -177,6 +250,52 @@ std::string get_link_input( const tui::Terminal& terminal )
return num_input;
}
+std::string normalize_url( const std::string& target, const std::string& current_url )
+{
+ if ( target.starts_with( "/" ) ) {
+ if ( target.starts_with( "//" ) ) {
+ return "https:" + target;
+ }
+ const std::size_t slash_pos = current_url.find( '/', 8 );
+ const std::string domain
+ = ( slash_pos == std::string::npos ) ? current_url : current_url.substr( 0, slash_pos );
+ return domain + target;
+ }
+ if ( !target.starts_with( "http" ) ) {
+ const std::size_t last_slash = current_url.rfind( '/' );
+ if ( last_slash > 7 ) {
+ return current_url.substr( 0, last_slash + 1 ) + target;
+ }
+ return current_url + "/" + target;
+ }
+ return target;
+}
+
+std::string strip_ddg_tracker( std::string target )
+{
+ const std::size_t uddg_pos = target.find( "uddg=" );
+ if ( uddg_pos != std::string::npos ) {
+ const std::size_t end_pos = target.find( '&', uddg_pos );
+ const std::string encoded = target.substr( uddg_pos + 5, end_pos - ( uddg_pos + 5 ) );
+ std::string decoded;
+ for ( std::size_t i = 0; i < encoded.length(); ++i ) {
+ if ( encoded[i] == '%' && i + 2 < encoded.length() ) {
+ const std::string hex = encoded.substr( i + 1, 2 );
+ try {
+ decoded += static_cast<char>( std::stoi( hex, nullptr, 16 ) );
+ } catch ( const std::exception& ) {
+ (void)0;
+ }
+ i += 2;
+ } else {
+ decoded += encoded[i];
+ }
+ }
+ return decoded;
+ }
+ return target;
+}
+
void handle_follow_link( const tui::Terminal& terminal,
const std::vector<std::string>& links,
std::string& current_url,
@@ -189,23 +308,8 @@ void handle_follow_link( const tui::Terminal& terminal,
if ( index > 0 && index <= links.size() ) {
std::string target = links[index - 1];
- if ( target.starts_with( "/" ) ) {
- if ( target.starts_with( "//" ) ) {
- target = "https:" + target; // "//example.com"
- } else {
- const std::size_t slash_pos = current_url.find( '/', 8 ); // 跳过 https://
- const std::string domain
- = ( slash_pos == std::string::npos ) ? current_url : current_url.substr( 0, slash_pos );
- target = domain + target; // "/about" -> "https://gnu.org/about"
- }
- } else if ( !target.starts_with( "http" ) ) {
- const std::size_t last_slash = current_url.rfind( '/' );
- if ( last_slash > 7 ) {
- target = current_url.substr( 0, last_slash + 1 ) + target;
- } else {
- target = current_url + "/" + target;
- }
- }
+ target = normalize_url( target, current_url );
+ target = strip_ddg_tracker( target );
current_url = target;
navigate = true;
@@ -276,6 +380,39 @@ InputAction process_key( char c,
return InputAction::None;
}
+std::vector<std::string> wrap_lines( const std::vector<std::string>& original_lines, std::size_t wrap_width )
+{
+ std::vector<std::string> wrapped_lines;
+ for ( const auto& line : original_lines ) {
+ std::string current_line;
+ std::size_t vis_count = 0;
+ bool in_ansi = false;
+ for ( const char c : line ) {
+ if ( c == '\x1b' ) {
+ in_ansi = true;
+ }
+ if ( in_ansi ) {
+ current_line += c;
+ if ( c == 'm' ) {
+ in_ansi = false;
+ }
+ } else {
+ if ( vis_count >= wrap_width ) {
+ wrapped_lines.push_back( current_line + "\033[0m" );
+ current_line.clear();
+ vis_count = 0;
+ }
+ current_line += c;
+ vis_count++;
+ }
+ }
+ if ( !current_line.empty() || line.empty() ) {
+ wrapped_lines.push_back( current_line + "\033[0m" );
+ }
+ }
+ return wrapped_lines;
+}
+
} // namespace
int Browser::run( std::string_view initial_url )
@@ -293,11 +430,16 @@ int Browser::run( std::string_view initial_url )
std::cout.flush();
auto page_data = load_page( current_url );
+
+ auto [term_width, term_height] = terminal.get_size();
+ const std::size_t wrap_width = ( term_width > 1 ) ? ( term_width - 1 ) : 80;
+ page_data.lines = wrap_lines( page_data.lines, wrap_width );
+
std::size_t scroll_y = 0;
bool navigate = false;
while ( !navigate ) {
- draw_view( terminal, page_data.lines, scroll_y, current_url );
+ draw_view( terminal, page_data.lines, scroll_y, current_url, page_data.title );
const char c = terminal.read_key();
const InputAction action = process_key( c, terminal, page_data, current_url, history, history_idx, scroll_y );
diff --git a/src/net/http_client.cc b/src/net/http_client.cc
index 5cf6a30..143e067 100644
--- a/src/net/http_client.cc
+++ b/src/net/http_client.cc
@@ -39,7 +39,6 @@ struct ParsedUrl
std::expected<ParsedUrl, std::string> parse_url( std::string_view url )
{
std::string_view prefix = "http://";
-
if ( url.starts_with( "https://" ) ) {
prefix = "https://";
} else if ( url.starts_with( "http://" ) ) {
@@ -49,17 +48,20 @@ std::expected<ParsedUrl, std::string> parse_url( std::string_view url )
}
url.remove_prefix( prefix.length() );
- auto slash_pos = url.find( '/' );
+
+ const auto sep_pos = url.find_first_of( "/?#" );
ParsedUrl result;
- if ( slash_pos == std::string_view::npos ) {
+ if ( sep_pos == std::string_view::npos ) {
result.host = std::string( url );
result.path = "/";
} else {
- result.host = std::string( url.substr( 0, slash_pos ) );
- result.path = std::string( url.substr( slash_pos ) );
+ result.host = std::string( url.substr( 0, sep_pos ) );
+ result.path = std::string( url.substr( sep_pos ) );
+ if ( result.path[0] != '/' ) {
+ result.path.insert( 0, "/" );
+ }
}
-
return result;
}
diff --git a/src/render/renderer.cc b/src/render/renderer.cc
index 2b690ca..ed622b4 100644
--- a/src/render/renderer.cc
+++ b/src/render/renderer.cc
@@ -18,19 +18,21 @@
#include "ricochet/render/renderer.hh"
#include <array>
+#include <cstdint>
+#include <stdexcept>
namespace ricochet::render {
namespace {
-std::string decode_entities( std::string text ) // NOLINT(readability-convert-member-functions-to-static)
+std::string decode_entities( std::string text )
{
- constexpr std::array<std::pair<std::string_view, std::string_view>, 24> entities
- = { { { "&nbsp;", " " }, { "&lt;", "<" }, { "&gt;", ">" }, { "&amp;", "&" }, { "&quot;", "\"" },
- { "&copy;", "©" }, { "&reg;", "®" }, { "&trade;", "™" }, { "&euro;", "€" }, { "&pound;", "£" },
- { "&yen;", "¥" }, { "&cent;", "¢" }, { "&sect;", "§" }, { "&para;", "¶" }, { "&hellip;", "…" },
- { "&dagger;", "†" }, { "&Dagger;", "‡" }, { "&bull;", "•" }, { "&prime;", "′" }, { "&Prime;", "″" },
- { "&lsaquo;", "‹" }, { "&rsaquo;", "›" }, { "&oline;", "‾" }, { "&frasl;", "⁄" } } };
+ constexpr std::array<std::pair<std::string_view, std::string_view>, 25> entities
+ = { { { "&nbsp;", " " }, { "&lt;", "<" }, { "&gt;", ">" }, { "&amp;", "&" }, { "&quot;", "\"" },
+ { "&apos;", "'" }, { "&copy;", "©" }, { "&reg;", "®" }, { "&trade;", "™" }, { "&euro;", "€" },
+ { "&pound;", "£" }, { "&yen;", "¥" }, { "&cent;", "¢" }, { "&sect;", "§" }, { "&para;", "¶" },
+ { "&hellip;", "…" }, { "&dagger;", "†" }, { "&Dagger;", "‡" }, { "&bull;", "•" }, { "&prime;", "′" },
+ { "&Prime;", "″" }, { "&lsaquo;", "‹" }, { "&rsaquo;", "›" }, { "&oline;", "‾" }, { "&frasl;", "⁄" } } };
for ( const auto& [entity, replacement] : entities ) {
std::size_t pos = 0;
@@ -39,6 +41,31 @@ std::string decode_entities( std::string text ) // NOLINT(readability-convert-me
pos += replacement.length();
}
}
+
+ std::size_t pos = 0;
+ while ( ( pos = text.find( "&#", pos ) ) != std::string::npos ) {
+ const std::size_t end = text.find( ';', pos );
+ if ( end != std::string::npos && end - pos < 10 ) {
+ const std::string code = text.substr( pos + 2, end - pos - 2 );
+ try {
+ std::uint32_t cp = 0;
+ if ( !code.empty() && ( code[0] == 'x' || code[0] == 'X' ) ) {
+ cp = std::stoul( code.substr( 1 ), nullptr, 16 );
+ } else {
+ cp = std::stoul( code );
+ }
+ if ( cp < 128 ) {
+ text.replace( pos, end - pos + 1, 1, static_cast<char>( cp ) );
+ continue;
+ }
+ } catch ( const std::invalid_argument& ) {
+ (void)0; // Not a valid number, leave as-is
+ } catch ( const std::out_of_range& ) {
+ (void)0; // Value too large, leave as-is
+ }
+ }
+ pos++;
+ }
return text;
}
@@ -107,7 +134,7 @@ void render_node( const parser::DomNode& node, // NOLINT(misc-no-recursion)
output += "\033[0m";
const std::string href = extract_href( node.tag_name );
if ( !href.empty() && !href.starts_with( "javascript:" ) && !href.starts_with( "#" ) ) {
- links.push_back( href );
+ links.push_back( decode_entities( href ) );
output += " \033[7m[" + std::to_string( links.size() ) + "]\033[0m"; // 打印数字标签!
}
}