diff options
| author | Richard Qin <richard.qin.x@gmail.com> | 2026-10-30 02:24:12 +0800 |
|---|---|---|
| committer | Richard Qin <richard.qin.x@gmail.com> | 2026-10-30 02:24:12 +0800 |
| commit | 464313d02b76385d3a2e8809a4595afabca88dae (patch) | |
| tree | 6cbf81416b856ff15fdc4239de5e3624ef10b2cc /ricochet/src/core | |
| parent | cf58b3b4dfa0dbbd0ca492484ca241b320c4ba9e (diff) | |
| parent | bdefdb301db843e87aee8c6c35808d295bb66ee5 (diff) | |
| download | rcc-project-464313d02b76385d3a2e8809a4595afabca88dae.tar.gz rcc-project-464313d02b76385d3a2e8809a4595afabca88dae.tar.xz | |
Diffstat (limited to 'ricochet/src/core')
| -rw-r--r-- | ricochet/src/core/browser.cc | 66 | ||||
| -rw-r--r-- | ricochet/src/core/form_handler.cc | 64 | ||||
| -rw-r--r-- | ricochet/src/core/license.cc | 69 | ||||
| -rw-r--r-- | ricochet/src/core/local_data.cc | 171 | ||||
| -rw-r--r-- | ricochet/src/core/page_loader.cc | 78 | ||||
| -rw-r--r-- | ricochet/src/core/page_processor.cc | 97 | ||||
| -rw-r--r-- | ricochet/src/core/utils.cc | 85 |
7 files changed, 630 insertions, 0 deletions
diff --git a/ricochet/src/core/browser.cc b/ricochet/src/core/browser.cc new file mode 100644 index 0000000..c413a0d --- /dev/null +++ b/ricochet/src/core/browser.cc @@ -0,0 +1,66 @@ +/* + * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation. + * Copyright (C) 2026 Richard Qin + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "ricochet/core/browser.hh" + +#include <fstream> + +#include "ricochet/core/local_data.hh" + +namespace ricochet::core { + +std::string Browser::format_url( std::string input ) +{ + if ( input.empty() ) { + return input; + } + + if ( input.starts_with( "http://" ) || input.starts_with( "https://" ) ) { + return input; + } + + const bool is_domain = input.contains( '.' ) && !input.contains( ' ' ); + if ( is_domain ) { + input.insert( 0, "https://" ); + return input; + } + + for ( char& ch : input ) { + if ( ch == ' ' ) { + ch = '+'; + } + } + return "https://lite.duckduckgo.com/lite/?q=" + input; +} + +std::string Browser::get_configured_homepage() +{ + std::ifstream file( get_config_path( "config.txt" ) ); + if ( file.is_open() ) { + std::string line; + while ( std::getline( file, line ) ) { + const std::size_t eq = line.find( '=' ); + if ( eq != std::string::npos && line.substr( 0, eq ) == "homepage" ) { + return line.substr( eq + 1 ); + } + } + } + return "https://wikipedia.org"; +} + +} // namespace ricochet::core diff --git a/ricochet/src/core/form_handler.cc b/ricochet/src/core/form_handler.cc new file mode 100644 index 0000000..6100a30 --- /dev/null +++ b/ricochet/src/core/form_handler.cc @@ -0,0 +1,64 @@ +/* + * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation. + * Copyright (C) 2026 Richard Qin + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "ricochet/core/form_handler.hh" +#include "ricochet/core/utils.hh" + +namespace ricochet::core { + +InputAction submit_form( const render::InputField& target_input, + const std::vector<render::InputField>& inputs, + const std::string& current_url, + std::vector<HttpRequest>& history, + std::size_t& history_idx ) +{ + std::string body; + for ( const auto& in : inputs ) { + if ( in.action == target_input.action && !in.name.empty() && in.type != "submit" && in.type != "button" ) { + if ( ( in.type == "checkbox" || in.type == "radio" ) && !in.checked ) { + continue; + } + std::string encoded = in.value; + for ( char& ch : encoded ) { + if ( ch == ' ' ) { + ch = '+'; + } + } + if ( !body.empty() ) { + body += "&"; + } + body += in.name + "=" + encoded; + } + } + const std::string action + = target_input.action.empty() ? current_url : normalize_url( target_input.action, current_url ); + HttpRequest next_req { .url = action, .method = "GET", .body = "" }; + if ( target_input.method == "post" || target_input.method == "POST" ) { + next_req.method = "POST"; + next_req.body = body; + } else { + const char sep = action.contains( '?' ) ? '&' : '?'; + next_req.url += sep + body; + } + history.resize( history_idx + 1 ); + history.push_back( next_req ); + history_idx++; + return InputAction::Navigate; +} + +} // namespace ricochet::core diff --git a/ricochet/src/core/license.cc b/ricochet/src/core/license.cc new file mode 100644 index 0000000..587a942 --- /dev/null +++ b/ricochet/src/core/license.cc @@ -0,0 +1,69 @@ +/* + * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation. + * Copyright (C) 2026 Richard Qin + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "ricochet/core/license.hh" +#include <iostream> + +namespace ricochet::core { + +void print_license_notice() +{ + std::cout << "Ricochet Copyright (C) 2026 Your Name\n" + << "Ricochet comes with ABSOLUTELY NO WARRANTY; for details type 'show w'.\n" + << "This is free software, and you are welcome to redistribute it\n" + << "under certain conditions; type 'show c' for details.\n\n"; +} + +void show_warranty() +{ + std::cout << R"( +[ Ricochet - WARRANTY INFORMATION ] + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER +PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER +EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + +THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. +SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY +SERVICING, REPAIR OR CORRECTION. +)" << '\n'; +} + +void show_copying() +{ + std::cout << R"( +[ Ricochet - COPYING CONDITIONS ] + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <https://www.gnu.org/licenses/>. +)" << '\n'; +} + +} // namespace ricochet::core
\ No newline at end of file diff --git a/ricochet/src/core/local_data.cc b/ricochet/src/core/local_data.cc new file mode 100644 index 0000000..9115844 --- /dev/null +++ b/ricochet/src/core/local_data.cc @@ -0,0 +1,171 @@ +/* + * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation. + * Copyright (C) 2026 Richard Qin + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "ricochet/core/local_data.hh" +#include <cstdlib> +#include <fstream> +#include <sys/stat.h> +#include <vector> + +namespace ricochet::core { + +std::string get_config_path( const std::string& filename ) +{ + const char* home = std::getenv( "HOME" ); // NOLINT(concurrency-mt-unsafe) + const std::string dir = ( home != nullptr ) ? std::string( home ) + "/.ricochet" : ".ricochet"; + static_cast<void>( mkdir( dir.c_str(), 0755 ) ); + return dir + "/" + filename; +} + +void save_bookmark( const std::string& title, // NOLINT(bugprone-easily-swappable-parameters) + const std::string& url ) +{ + if ( url == "ricochet://bookmarks" ) { + return; + } + std::ofstream file( get_config_path( "bookmarks.txt" ), std::ios::app ); + if ( !file.is_open() ) { + return; + } + + std::string safe_title = title; + if ( safe_title.empty() ) { + safe_title = url; + } + for ( char& c : safe_title ) { + if ( c == '\n' || c == '\r' || c == '\t' ) { + c = ' '; + } + } + file << url << '\t' << safe_title << '\n'; +} + +std::string generate_bookmarks_html() +{ + std::ifstream file( get_config_path( "bookmarks.txt" ) ); + std::string html = "<html><head><title>My Bookmarks</title></head><body><h1>Saved Bookmarks</h1><ul>"; + if ( !file.is_open() ) { + html += "<li>No bookmarks found. Press 'b' on any page to add one!</li>"; + } else { + std::string line; + while ( std::getline( file, line ) ) { + const std::size_t tab_pos = line.find( '\t' ); + if ( tab_pos != std::string::npos ) { + const std::string url = line.substr( 0, tab_pos ); + const std::string title = line.substr( tab_pos + 1 ); + html += "<li><a href=\""; + html += url; + html += "\">[Link] "; + html += title; + html += "</a></li>"; + } + } + } + html += "</ul></body></html>"; + return html; +} + +void append_to_history( const std::string& url, // NOLINT(bugprone-easily-swappable-parameters) + const std::string& title ) +{ + if ( url.starts_with( "ricochet://" ) ) { + return; + } + std::ofstream file( get_config_path( "history.txt" ), std::ios::app ); + if ( !file.is_open() ) { + return; + } + + std::string safe_title = title; + if ( safe_title.empty() ) { + safe_title = url; + } + for ( char& c : safe_title ) { + if ( c == '\n' || c == '\r' || c == '\t' ) { + c = ' '; + } + } + file << url << '\t' << safe_title << '\n'; +} + +std::string generate_history_html() +{ + std::ifstream file( get_config_path( "history.txt" ) ); + std::string html = "<html><head><title>History</title></head><body><h1>Browsing History</h1><ul>"; + if ( !file.is_open() ) { + html += "<li>No history found. Start browsing!</li>"; + } else { + std::vector<std::string> lines; + std::string line; + while ( std::getline( file, line ) ) { + lines.push_back( std::move( line ) ); + } + + std::size_t count = 0; + for ( auto it = lines.rbegin(); it != lines.rend() && count < 100; ++it, ++count ) { + const std::size_t tab_pos = it->find( '\t' ); + if ( tab_pos != std::string::npos ) { + const std::string url = it->substr( 0, tab_pos ); + const std::string title = it->substr( tab_pos + 1 ); + html += "<li><a href=\""; + html += url; + html += "\">[Link] "; + html += title; + html += "</a></li>"; + } + } + } + html += "</ul></body></html>"; + return html; +} + +std::string generate_help_html() +{ + return R"(<html> + <head><title>Ricochet Help Manual</title></head> + <body> + <h1>Ricochet User Manual</h1> + <h2>1. Navigation & Browsing</h2> + <ul> + <li><b>j / k</b> : Scroll down / up</li> + <li><b>H / L</b> : History backward / forward</li> + <li><b>/</b> : Open prompt to enter a new URL</li> + </ul> + <h2>2. Page Interaction</h2> + <ul> + <li><b>f</b> : Toggle Link Hints (type the two letters to follow a link instantly)</li> + <li><b>n</b> : Find in page (search for text)</li> + <li><b>N</b> : Jump to the next search match</li> + <li><b>i</b> : Enter form input mode (fill textboxes, click buttons, toggle checkboxes)</li> + </ul> + <h2>3. Bookmarks & History</h2> + <ul> + <li><b>b</b> : Bookmark the current page</li> + <li><b>B</b> : View all saved bookmarks</li> + <li><b>h</b> : View browsing history</li> + </ul> + <h2>4. System</h2> + <ul> + <li><b>?</b> : Show this help manual</li> + <li><b>q</b> : Quit Ricochet</li> + </ul> + </body> + </html>)"; +} + +} // namespace ricochet::core diff --git a/ricochet/src/core/page_loader.cc b/ricochet/src/core/page_loader.cc new file mode 100644 index 0000000..b9a5da5 --- /dev/null +++ b/ricochet/src/core/page_loader.cc @@ -0,0 +1,78 @@ +/* + * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation. + * Copyright (C) 2026 Richard Qin + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "ricochet/core/page_loader.hh" +#include "ricochet/core/local_data.hh" +#include "ricochet/core/page_processor.hh" +#include "ricochet/core/utils.hh" +#include "ricochet/parser/html_lexer.hh" +#include "ricochet/parser/tree_builder.hh" +#include "ricochet/render/renderer.hh" + +namespace ricochet::core { + +PageData load_page( const HttpRequest& req ) +{ + std::string html_body; + if ( req.url == "ricochet://bookmarks" ) { + html_body = generate_bookmarks_html(); + } else if ( req.url == "ricochet://history" ) { + html_body = generate_history_html(); + } else if ( req.url == "ricochet://help" ) { + html_body = generate_help_html(); + } else { + const net::HttpClient client; + auto response_result = client.fetch( req.url, req.method, req.body ); + if ( !response_result.has_value() ) { + return { .lines + = { "\033[1;31m[!] Failed to load:\033[0m " + req.url, "", "Reason: " + response_result.error() }, + .links = {}, + .inputs = {}, + .title = "Error" }; + } + + const auto& resp = response_result.value(); + std::string c_type = resp.content_type; + for ( char& c : c_type ) { + if ( c >= 'A' && c <= 'Z' ) { + c = static_cast<char>( c + 32 ); + } + } + + const bool is_text + = c_type.starts_with( "text/" ) || c_type.contains( "xml" ) || c_type.contains( "json" ) || c_type.empty(); + + if ( !is_text ) { + return process_binary_download( req, resp ); + } + html_body = response_result.value().body; + } + + const parser::HtmlLexer lexer; + const parser::TreeBuilder builder; + const render::Renderer renderer; + + const auto dom_root = builder.build( lexer.tokenize( html_body ) ); + const render::RenderResult result = renderer.render( dom_root ); + const std::string title = extract_title( dom_root ); + + return { + .lines = split_into_lines( result.text ), .links = result.links, .inputs = result.inputs, .title = title }; +} + +} // namespace ricochet::core diff --git a/ricochet/src/core/page_processor.cc b/ricochet/src/core/page_processor.cc new file mode 100644 index 0000000..00728a9 --- /dev/null +++ b/ricochet/src/core/page_processor.cc @@ -0,0 +1,97 @@ +/* + * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation. + * Copyright (C) 2026 Richard Qin + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "ricochet/core/page_processor.hh" +#include <fstream> +#include <ranges> +#include <vector> + +namespace ricochet::core { + +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 process_binary_download( const HttpRequest& req, const net::HttpResponse& resp ) +{ + std::string fname = "download.dat"; + const std::size_t slash = req.url.rfind( '/' ); + if ( slash != std::string::npos && slash + 1 < req.url.length() ) { + fname = req.url.substr( slash + 1 ); + const std::size_t q_pos = fname.find( '?' ); + if ( q_pos != std::string::npos ) { + fname = fname.substr( 0, q_pos ); + } + } + if ( fname.empty() ) { + fname = "download.dat"; + } + + std::ofstream out( fname, std::ios::binary ); + out.write( resp.body.data(), static_cast<std::streamsize>( resp.body.size() ) ); + out.close(); + + return { .lines = { "\033[1;32m[ Download Complete ]\033[0m", + "", + "Saved File : " + fname, + "File Type : " + resp.content_type, + "File Size : " + std::to_string( resp.body.size() ) + " bytes", + "", + "Press 'H' to return to the previous page." }, + .links = {}, + .inputs = {}, + .title = "Download Manager" }; +} + +} // namespace ricochet::core diff --git a/ricochet/src/core/utils.cc b/ricochet/src/core/utils.cc new file mode 100644 index 0000000..a410c8a --- /dev/null +++ b/ricochet/src/core/utils.cc @@ -0,0 +1,85 @@ +/* + * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation. + * Copyright (C) 2026 Richard Qin + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "ricochet/core/utils.hh" + +namespace ricochet::core { + +std::vector<std::string> split_into_lines( const std::string& text ) +{ + std::vector<std::string> lines; + std::size_t start = 0; + while ( start < text.length() ) { + const std::size_t end = text.find( '\n', start ); + if ( end == std::string::npos ) { + lines.push_back( text.substr( start ) ); + break; + } + lines.push_back( text.substr( start, end - start ) ); + start = end + 1; + } + return lines; +} + +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; +} + +} // namespace ricochet::core |
