mirror of
https://github.com/m1ngsama/TUT.git
synced 2026-02-08 00:54:05 +00:00
Major architectural refactoring from ncurses to FTXUI framework with professional engineering structure. Project Structure: - src/core/: Browser engine, URL parser, HTTP client - src/ui/: FTXUI components (main window, address bar, content view, panels) - src/renderer/: HTML renderer, text formatter, style parser - src/utils/: Logger, config manager, theme manager - tests/unit/: Unit tests for core components - tests/integration/: Integration tests - assets/: Default configs, themes, keybindings New Features: - btop-style four-panel layout with rounded borders - TOML-based configuration system - Multiple color themes (default, nord, gruvbox, solarized) - Comprehensive logging system - Modular architecture with clear separation of concerns Build System: - Updated CMakeLists.txt for modular build - Prefer system packages (Homebrew) over FetchContent - Google Test integration for testing - Version info generation via cmake/version.hpp.in Configuration: - Default config.toml with browser settings - Four built-in themes - Default keybindings configuration - Config stored in ~/.config/tut/ Removed: - Legacy v1 source files (ncurses-based) - Old render/ directory - Duplicate and obsolete test files - Old documentation files Binary: ~827KB (well under 5MB goal) Dependencies: FTXUI, cpp-httplib, toml11, gumbo-parser, OpenSSL
206 lines
6 KiB
C++
206 lines
6 KiB
C++
/**
|
|
* @file html_renderer.cpp
|
|
* @brief HTML 渲染器实现
|
|
*/
|
|
|
|
#include "renderer/html_renderer.hpp"
|
|
#include "core/browser_engine.hpp"
|
|
#include "core/url_parser.hpp"
|
|
|
|
#include <gumbo.h>
|
|
#include <sstream>
|
|
|
|
namespace tut {
|
|
|
|
class HtmlRenderer::Impl {
|
|
public:
|
|
RenderOptions options_;
|
|
|
|
void renderNode(GumboNode* node, std::ostringstream& output,
|
|
std::vector<LinkInfo>& links, int& link_count) {
|
|
if (node->type == GUMBO_NODE_TEXT) {
|
|
output << node->v.text.text;
|
|
return;
|
|
}
|
|
|
|
if (node->type != GUMBO_NODE_ELEMENT) {
|
|
return;
|
|
}
|
|
|
|
GumboElement& element = node->v.element;
|
|
GumboTag tag = element.tag;
|
|
|
|
// 跳过不可见元素
|
|
if (tag == GUMBO_TAG_SCRIPT || tag == GUMBO_TAG_STYLE ||
|
|
tag == GUMBO_TAG_HEAD || tag == GUMBO_TAG_NOSCRIPT) {
|
|
return;
|
|
}
|
|
|
|
// 处理块级元素
|
|
bool is_block = (tag == GUMBO_TAG_P || tag == GUMBO_TAG_DIV ||
|
|
tag == GUMBO_TAG_H1 || tag == GUMBO_TAG_H2 ||
|
|
tag == GUMBO_TAG_H3 || tag == GUMBO_TAG_H4 ||
|
|
tag == GUMBO_TAG_H5 || tag == GUMBO_TAG_H6 ||
|
|
tag == GUMBO_TAG_UL || tag == GUMBO_TAG_OL ||
|
|
tag == GUMBO_TAG_LI || tag == GUMBO_TAG_BR ||
|
|
tag == GUMBO_TAG_HR || tag == GUMBO_TAG_BLOCKQUOTE ||
|
|
tag == GUMBO_TAG_PRE || tag == GUMBO_TAG_TABLE ||
|
|
tag == GUMBO_TAG_TR);
|
|
|
|
// 标题格式
|
|
if (tag >= GUMBO_TAG_H1 && tag <= GUMBO_TAG_H6) {
|
|
output << "\n";
|
|
if (options_.use_colors) {
|
|
output << "\033[1m"; // Bold
|
|
}
|
|
}
|
|
|
|
// 列表项
|
|
if (tag == GUMBO_TAG_LI) {
|
|
output << "\n • ";
|
|
}
|
|
|
|
// 链接
|
|
if (tag == GUMBO_TAG_A && options_.show_links) {
|
|
GumboAttribute* href = gumbo_get_attribute(&element.attributes, "href");
|
|
if (href) {
|
|
link_count++;
|
|
LinkInfo link;
|
|
link.url = href->value;
|
|
|
|
// 提取链接文本
|
|
std::ostringstream link_text;
|
|
for (unsigned int i = 0; i < element.children.length; ++i) {
|
|
GumboNode* child = static_cast<GumboNode*>(element.children.data[i]);
|
|
if (child->type == GUMBO_NODE_TEXT) {
|
|
link_text << child->v.text.text;
|
|
}
|
|
}
|
|
link.text = link_text.str();
|
|
links.push_back(link);
|
|
|
|
if (options_.use_colors) {
|
|
output << "\033[4;34m"; // Underline blue
|
|
}
|
|
output << "[" << link_count << "]";
|
|
}
|
|
}
|
|
|
|
// 递归处理子节点
|
|
for (unsigned int i = 0; i < element.children.length; ++i) {
|
|
renderNode(static_cast<GumboNode*>(element.children.data[i]),
|
|
output, links, link_count);
|
|
}
|
|
|
|
// 关闭格式
|
|
if (tag >= GUMBO_TAG_H1 && tag <= GUMBO_TAG_H6) {
|
|
if (options_.use_colors) {
|
|
output << "\033[0m"; // Reset
|
|
}
|
|
output << "\n";
|
|
}
|
|
|
|
if (tag == GUMBO_TAG_A && options_.show_links && options_.use_colors) {
|
|
output << "\033[0m";
|
|
}
|
|
|
|
if (is_block) {
|
|
output << "\n";
|
|
}
|
|
}
|
|
|
|
std::string findTitle(GumboNode* node) {
|
|
if (node->type != GUMBO_NODE_ELEMENT) {
|
|
return "";
|
|
}
|
|
|
|
if (node->v.element.tag == GUMBO_TAG_TITLE) {
|
|
if (node->v.element.children.length > 0) {
|
|
GumboNode* child = static_cast<GumboNode*>(node->v.element.children.data[0]);
|
|
if (child->type == GUMBO_NODE_TEXT) {
|
|
return child->v.text.text;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (unsigned int i = 0; i < node->v.element.children.length; ++i) {
|
|
std::string title = findTitle(
|
|
static_cast<GumboNode*>(node->v.element.children.data[i]));
|
|
if (!title.empty()) {
|
|
return title;
|
|
}
|
|
}
|
|
|
|
return "";
|
|
}
|
|
};
|
|
|
|
HtmlRenderer::HtmlRenderer() : impl_(std::make_unique<Impl>()) {}
|
|
|
|
HtmlRenderer::~HtmlRenderer() = default;
|
|
|
|
RenderResult HtmlRenderer::render(const std::string& html,
|
|
const RenderOptions& options) {
|
|
impl_->options_ = options;
|
|
RenderResult result;
|
|
|
|
GumboOutput* output = gumbo_parse(html.c_str());
|
|
if (!output) {
|
|
return result;
|
|
}
|
|
|
|
// 提取标题
|
|
result.title = impl_->findTitle(output->root);
|
|
|
|
// 渲染内容
|
|
std::ostringstream text_output;
|
|
int link_count = 0;
|
|
impl_->renderNode(output->root, text_output, result.links, link_count);
|
|
|
|
result.text = text_output.str();
|
|
|
|
gumbo_destroy_output(&kGumboDefaultOptions, output);
|
|
|
|
return result;
|
|
}
|
|
|
|
std::string HtmlRenderer::extractTitle(const std::string& html) {
|
|
GumboOutput* output = gumbo_parse(html.c_str());
|
|
if (!output) {
|
|
return "";
|
|
}
|
|
|
|
std::string title = impl_->findTitle(output->root);
|
|
gumbo_destroy_output(&kGumboDefaultOptions, output);
|
|
|
|
return title;
|
|
}
|
|
|
|
std::vector<LinkInfo> HtmlRenderer::extractLinks(const std::string& html,
|
|
const std::string& base_url) {
|
|
RenderOptions options;
|
|
options.show_links = true;
|
|
auto result = render(html, options);
|
|
|
|
// 解析相对 URL
|
|
if (!base_url.empty()) {
|
|
UrlParser parser;
|
|
for (auto& link : result.links) {
|
|
if (link.url.find("://") == std::string::npos) {
|
|
link.url = parser.resolveRelative(base_url, link.url);
|
|
}
|
|
}
|
|
}
|
|
|
|
return result.links;
|
|
}
|
|
|
|
void HtmlRenderer::setOptions(const RenderOptions& options) {
|
|
impl_->options_ = options;
|
|
}
|
|
|
|
const RenderOptions& HtmlRenderer::getOptions() const {
|
|
return impl_->options_;
|
|
}
|
|
|
|
} // namespace tut
|