diff options
| -rw-r--r-- | include/ricochet/parser/html_lexer.hh | 61 | ||||
| -rw-r--r-- | src/core/browser.cc | 50 | ||||
| -rw-r--r-- | src/parser/html_lexer.cc | 121 |
3 files changed, 218 insertions, 14 deletions
diff --git a/include/ricochet/parser/html_lexer.hh b/include/ricochet/parser/html_lexer.hh new file mode 100644 index 0000000..f97bcfa --- /dev/null +++ b/include/ricochet/parser/html_lexer.hh @@ -0,0 +1,61 @@ +/* + * 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/>. + */ + +#pragma once + +#include <string> +#include <string_view> +#include <variant> +#include <vector> + +namespace ricochet::parser { + + struct TextToken { + std::string content{}; +}; + +struct TagOpenToken { + std::string name{}; +}; + +struct TagCloseToken { + std::string name{}; +}; + +using HtmlToken = std::variant<TextToken, TagOpenToken, TagCloseToken>; + +class HtmlLexer +{ +public: + HtmlLexer() = default; + ~HtmlLexer() = default; + + HtmlLexer( const HtmlLexer& ) = delete; + HtmlLexer& operator=( const HtmlLexer& ) = delete; + HtmlLexer( HtmlLexer&& ) = delete; + HtmlLexer& operator=( HtmlLexer&& ) = delete; + + /** + * @brief Perform lexical analysis to convert HTML source code into a structured sequence of tokens + */ + std::vector<HtmlToken> tokenize(std::string_view html) const; + +private: +}; + +} // namespace ricochet::parser
\ No newline at end of file diff --git a/src/core/browser.cc b/src/core/browser.cc index ccda9a1..1e9d044 100644 --- a/src/core/browser.cc +++ b/src/core/browser.cc @@ -18,26 +18,48 @@ #include "ricochet/core/browser.hh" #include "ricochet/net/http_client.hh" +#include "ricochet/parser/html_lexer.hh" #include <iostream> +namespace { +struct TokenPrinter { + void operator()(const ricochet::parser::TextToken& t) const { + std::cout << "[TEXT] " << t.content << "\n"; + } + void operator()(const ricochet::parser::TagOpenToken& t) const { + std::cout << "[OPEN] <" << t.name << ">\n"; + } + void operator()(const ricochet::parser::TagCloseToken& t) const { + std::cout << "[CLOSE] </" << t.name << ">\n"; + } +}; +} // namespace namespace ricochet::core { int Browser::run( std::string_view initial_url ) { - std::cout << "Browser: Navigating to " << initial_url << "...\n"; - - const net::HttpClient client; - auto response_result = client.fetch( initial_url ); - if ( !response_result.has_value() ) { - std::cerr << "[!] Ricochet failed to load page: " << response_result.error() << "\n"; - return 1; - } - - const auto& response = response_result.value(); - std::cout << "\n[=== HTTP Status: " << response.status_code << " ===]\n"; - std::cout << "[=== HTML Content Below ===]\n\n"; - std::cout << response.body << "\n"; - return 0; + std::cout << "-> Fetching: " << initial_url << "...\n"; + + const net::HttpClient client; + auto response_result = client.fetch(initial_url); + + if (!response_result.has_value()) { + std::cerr << "[!] Ricochet failed to load page: " << response_result.error() << "\n"; + return 1; + } + + const auto& response = response_result.value(); + std::cout << "[=== HTTP Status: " << response.status_code << " ===]\n"; + + std::cout << "\n[=== Tokenizing HTML ===]\n"; + const parser::HtmlLexer lexer; + auto tokens = lexer.tokenize(response.body); + + for (const auto& token : tokens) { + std::visit(TokenPrinter{}, token); + } + + return 0; } } // namespace ricochet::core diff --git a/src/parser/html_lexer.cc b/src/parser/html_lexer.cc new file mode 100644 index 0000000..bbf8c57 --- /dev/null +++ b/src/parser/html_lexer.cc @@ -0,0 +1,121 @@ +/* + * 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/parser/html_lexer.hh" + +#include <cstdint> +#include <string_view> + + +namespace ricochet::parser { + +namespace { +enum class LexerState : std::uint8_t { + Data, // Reading plain text + TagOpen, // Just encountered '<' + TagName, // Reading the name of an opening tag (e.g., "div") + ClosingTagName, // Reading the name of a closing tag (e.g., "/div") + SwallowAttributes // Ignore attributes mode (ignore until encountering '>') +}; + +void emit_text(std::string& buffer, std::vector<HtmlToken>& tokens) { + if (buffer.empty()) { + return; // 提前返回,减少 if 嵌套层级 + } + + bool all_whitespace = true; + for (const char c : buffer) { + if (!std::isspace(static_cast<unsigned char>(c))) { + all_whitespace = false; + break; + } + } + + if (!all_whitespace) { + tokens.emplace_back(TextToken{buffer}); + } + buffer.clear(); +} + +} // namespace + +std::vector<HtmlToken> HtmlLexer::tokenize( std::string_view html ) const // NOLINT(readability-convert-member-functions-to-static) +{ + std::vector<HtmlToken> tokens; + + LexerState state = LexerState::Data; + std::string buffer{}; + + for (const char c : html) { + switch (state) { + case LexerState::Data: + if (c == '<') { + emit_text(buffer, tokens); + state = LexerState::TagOpen; + } else { + buffer += c; + } + break; + case LexerState::TagOpen: + if (c == '/') { + state = LexerState::ClosingTagName; + } else if (std::isalpha(static_cast<unsigned char>(c))) { + buffer.push_back(c); + state = LexerState::TagName; + } else { + buffer.push_back('<'); + buffer.push_back(c); + state = LexerState::Data; + } + break; + case LexerState::TagName: + if (c == '>') { + tokens.emplace_back(TagOpenToken{buffer}); + buffer.clear(); + state = LexerState::Data; + } else if (std::isspace(static_cast<unsigned char>(c))) { + tokens.emplace_back(TagOpenToken{buffer}); + buffer.clear(); + state = LexerState::SwallowAttributes; + } else { + buffer += static_cast<char>(std::tolower(static_cast<unsigned char>(c))); + } + break; + case LexerState::ClosingTagName: + if (c == '>') { + tokens.emplace_back(TagCloseToken{buffer}); + buffer.clear(); + state = LexerState::Data; + } else { + buffer += static_cast<char>(std::tolower(static_cast<unsigned char>(c))); + } + break; + case LexerState::SwallowAttributes: + if (c == '>') { + state = LexerState::Data; + } + break; + } + } + + emit_text(buffer, tokens); + + return tokens; +} + +} // namespace ricochet::parser
\ No newline at end of file |
