aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--include/ricochet/parser/html_lexer.hh61
-rw-r--r--src/core/browser.cc50
-rw-r--r--src/parser/html_lexer.cc121
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