From 8e291399aed0c032f08666387fa361304a4253a5 Mon Sep 17 00:00:00 2001 From: m1ngsama Date: Fri, 5 Dec 2025 14:59:17 +0800 Subject: [PATCH] feat: Add newspaper-style text rendering engine Implement text renderer with adaptive layout: - Adaptive width with maximum 80 characters - Center-aligned content for comfortable reading - Smart text wrapping and paragraph spacing - Color scheme optimized for terminal reading - Support for headings, paragraphs, lists, and links - Link indicators with numbering - Horizontal rules and visual separators The renderer creates a newspaper-like reading experience optimized for terminal displays. --- src/text_renderer.cpp | 300 ++++++++++++++++++++++++++++++++++++++++++ src/text_renderer.h | 60 +++++++++ 2 files changed, 360 insertions(+) create mode 100644 src/text_renderer.cpp create mode 100644 src/text_renderer.h diff --git a/src/text_renderer.cpp b/src/text_renderer.cpp new file mode 100644 index 0000000..3696e5d --- /dev/null +++ b/src/text_renderer.cpp @@ -0,0 +1,300 @@ +#include "text_renderer.h" +#include +#include +#include + +class TextRenderer::Impl { +public: + RenderConfig config; + + // 自动换行处理 + std::vector wrap_text(const std::string& text, int width) { + std::vector lines; + if (text.empty()) { + return lines; + } + + std::istringstream words_stream(text); + std::string word; + std::string current_line; + + while (words_stream >> word) { + // 处理单个词超长的情况 + if (word.length() > static_cast(width)) { + if (!current_line.empty()) { + lines.push_back(current_line); + current_line.clear(); + } + // 强制分割长词 + for (size_t i = 0; i < word.length(); i += width) { + lines.push_back(word.substr(i, width)); + } + continue; + } + + // 正常换行逻辑 + if (current_line.empty()) { + current_line = word; + } else if (current_line.length() + 1 + word.length() <= static_cast(width)) { + current_line += " " + word; + } else { + lines.push_back(current_line); + current_line = word; + } + } + + if (!current_line.empty()) { + lines.push_back(current_line); + } + + if (lines.empty()) { + lines.push_back(""); + } + + return lines; + } + + // 添加缩进 + std::string add_indent(const std::string& text, int indent) { + return std::string(indent, ' ') + text; + } +}; + +TextRenderer::TextRenderer() : pImpl(std::make_unique()) { + pImpl->config = RenderConfig(); +} + +TextRenderer::~TextRenderer() = default; + +std::vector TextRenderer::render(const ParsedDocument& doc, int screen_width) { + std::vector lines; + + // 计算实际内容宽度 + int content_width = std::min(pImpl->config.max_width, screen_width - 4); + if (content_width < 40) { + content_width = screen_width - 4; + } + + // 计算左边距(如果居中) + int margin = 0; + if (pImpl->config.center_content && content_width < screen_width) { + margin = (screen_width - content_width) / 2; + } + pImpl->config.margin_left = margin; + + // 渲染标题 + if (!doc.title.empty()) { + RenderedLine title_line; + title_line.text = std::string(margin, ' ') + doc.title; + title_line.color_pair = COLOR_HEADING1; + title_line.is_bold = true; + title_line.is_link = false; + title_line.link_index = -1; + lines.push_back(title_line); + + // 标题下划线 + RenderedLine underline; + underline.text = std::string(margin, ' ') + std::string(std::min((int)doc.title.length(), content_width), '='); + underline.color_pair = COLOR_HEADING1; + underline.is_bold = false; + underline.is_link = false; + underline.link_index = -1; + lines.push_back(underline); + + // 空行 + RenderedLine empty; + empty.text = ""; + empty.color_pair = COLOR_NORMAL; + empty.is_bold = false; + empty.is_link = false; + empty.link_index = -1; + lines.push_back(empty); + } + + // 渲染URL + if (!doc.url.empty()) { + RenderedLine url_line; + url_line.text = std::string(margin, ' ') + "URL: " + doc.url; + url_line.color_pair = COLOR_URL_BAR; + url_line.is_bold = false; + url_line.is_link = false; + url_line.link_index = -1; + lines.push_back(url_line); + + RenderedLine empty; + empty.text = ""; + empty.color_pair = COLOR_NORMAL; + empty.is_bold = false; + empty.is_link = false; + empty.link_index = -1; + lines.push_back(empty); + } + + // 渲染内容元素 + for (const auto& elem : doc.elements) { + int color = COLOR_NORMAL; + bool bold = false; + std::string prefix = ""; + + switch (elem.type) { + case ElementType::HEADING1: + color = COLOR_HEADING1; + bold = true; + prefix = "# "; + break; + case ElementType::HEADING2: + color = COLOR_HEADING2; + bold = true; + prefix = "## "; + break; + case ElementType::HEADING3: + color = COLOR_HEADING3; + bold = true; + prefix = "### "; + break; + case ElementType::PARAGRAPH: + color = COLOR_NORMAL; + bold = false; + break; + case ElementType::BLOCKQUOTE: + color = COLOR_DIM; + prefix = "> "; + break; + case ElementType::LIST_ITEM: + prefix = " • "; + break; + case ElementType::HORIZONTAL_RULE: + { + RenderedLine hr; + std::string hrline(content_width, '-'); + hr.text = std::string(margin, ' ') + hrline; + hr.color_pair = COLOR_DIM; + hr.is_bold = false; + hr.is_link = false; + hr.link_index = -1; + lines.push_back(hr); + continue; + } + default: + break; + } + + // 换行处理 + auto wrapped_lines = pImpl->wrap_text(elem.text, content_width - prefix.length()); + for (size_t i = 0; i < wrapped_lines.size(); ++i) { + RenderedLine line; + if (i == 0) { + line.text = std::string(margin, ' ') + prefix + wrapped_lines[i]; + } else { + line.text = std::string(margin + prefix.length(), ' ') + wrapped_lines[i]; + } + line.color_pair = color; + line.is_bold = bold; + line.is_link = false; + line.link_index = -1; + lines.push_back(line); + } + + // 段落间距 + if (elem.type == ElementType::PARAGRAPH || + elem.type == ElementType::HEADING1 || + elem.type == ElementType::HEADING2 || + elem.type == ElementType::HEADING3) { + for (int i = 0; i < pImpl->config.paragraph_spacing; ++i) { + RenderedLine empty; + empty.text = ""; + empty.color_pair = COLOR_NORMAL; + empty.is_bold = false; + empty.is_link = false; + empty.link_index = -1; + lines.push_back(empty); + } + } + } + + // 渲染链接列表 + if (!doc.links.empty() && pImpl->config.show_link_indicators) { + RenderedLine separator; + std::string sepline(content_width, '-'); + separator.text = std::string(margin, ' ') + sepline; + separator.color_pair = COLOR_DIM; + separator.is_bold = false; + separator.is_link = false; + separator.link_index = -1; + lines.push_back(separator); + + RenderedLine links_header; + links_header.text = std::string(margin, ' ') + "Links:"; + links_header.color_pair = COLOR_HEADING3; + links_header.is_bold = true; + links_header.is_link = false; + links_header.link_index = -1; + lines.push_back(links_header); + + RenderedLine empty; + empty.text = ""; + empty.color_pair = COLOR_NORMAL; + empty.is_bold = false; + empty.is_link = false; + empty.link_index = -1; + lines.push_back(empty); + + for (size_t i = 0; i < doc.links.size(); ++i) { + const auto& link = doc.links[i]; + std::string link_text = "[" + std::to_string(i) + "] " + link.text; + + auto wrapped = pImpl->wrap_text(link_text, content_width - 4); + for (size_t j = 0; j < wrapped.size(); ++j) { + RenderedLine link_line; + link_line.text = std::string(margin + 2, ' ') + wrapped[j]; + link_line.color_pair = COLOR_LINK; + link_line.is_bold = false; + link_line.is_link = true; + link_line.link_index = i; + lines.push_back(link_line); + } + + // URL on next line + auto url_wrapped = pImpl->wrap_text(link.url, content_width - 6); + for (const auto& url_line_text : url_wrapped) { + RenderedLine url_line; + url_line.text = std::string(margin + 4, ' ') + "→ " + url_line_text; + url_line.color_pair = COLOR_DIM; + url_line.is_bold = false; + url_line.is_link = false; + url_line.link_index = -1; + lines.push_back(url_line); + } + + lines.push_back(empty); + } + } + + return lines; +} + +void TextRenderer::set_config(const RenderConfig& config) { + pImpl->config = config; +} + +RenderConfig TextRenderer::get_config() const { + return pImpl->config; +} + +void init_color_scheme() { + if (has_colors()) { + start_color(); + use_default_colors(); + + init_pair(COLOR_NORMAL, COLOR_WHITE, -1); + init_pair(COLOR_HEADING1, COLOR_CYAN, -1); + init_pair(COLOR_HEADING2, COLOR_BLUE, -1); + init_pair(COLOR_HEADING3, COLOR_MAGENTA, -1); + init_pair(COLOR_LINK, COLOR_YELLOW, -1); + init_pair(COLOR_LINK_ACTIVE, COLOR_BLACK, COLOR_YELLOW); + init_pair(COLOR_STATUS_BAR, COLOR_BLACK, COLOR_WHITE); + init_pair(COLOR_URL_BAR, COLOR_GREEN, -1); + init_pair(COLOR_SEARCH_HIGHLIGHT, COLOR_BLACK, COLOR_YELLOW); + init_pair(COLOR_DIM, COLOR_BLACK, -1); + } +} diff --git a/src/text_renderer.h b/src/text_renderer.h new file mode 100644 index 0000000..3737a5c --- /dev/null +++ b/src/text_renderer.h @@ -0,0 +1,60 @@ +#pragma once + +#include "html_parser.h" +#include +#include +#include + +// 渲染后的行信息 +struct RenderedLine { + std::string text; + int color_pair; + bool is_bold; + bool is_link; + int link_index; // 如果是链接,对应的链接索引 +}; + +// 渲染配置 +struct RenderConfig { + int max_width = 80; // 内容最大宽度 + int margin_left = 0; // 左边距(居中时自动计算) + bool center_content = true; // 是否居中内容 + int paragraph_spacing = 1; // 段落间距 + bool show_link_indicators = true; // 是否显示链接指示器 +}; + +class TextRenderer { +public: + TextRenderer(); + ~TextRenderer(); + + // 渲染文档到行数组 + std::vector render(const ParsedDocument& doc, int screen_width); + + // 设置渲染配置 + void set_config(const RenderConfig& config); + + // 获取当前配置 + RenderConfig get_config() const; + +private: + class Impl; + std::unique_ptr pImpl; +}; + +// 颜色定义 +enum ColorScheme { + COLOR_NORMAL = 1, + COLOR_HEADING1, + COLOR_HEADING2, + COLOR_HEADING3, + COLOR_LINK, + COLOR_LINK_ACTIVE, + COLOR_STATUS_BAR, + COLOR_URL_BAR, + COLOR_SEARCH_HIGHLIGHT, + COLOR_DIM +}; + +// 初始化颜色方案 +void init_color_scheme();