mirror of
https://github.com/m1ngsama/TUT.git
synced 2025-12-24 10:51:46 +00:00
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.
This commit is contained in:
parent
2fedcebc25
commit
8e291399ae
2 changed files with 360 additions and 0 deletions
300
src/text_renderer.cpp
Normal file
300
src/text_renderer.cpp
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
#include "text_renderer.h"
|
||||
#include <sstream>
|
||||
#include <algorithm>
|
||||
#include <clocale>
|
||||
|
||||
class TextRenderer::Impl {
|
||||
public:
|
||||
RenderConfig config;
|
||||
|
||||
// 自动换行处理
|
||||
std::vector<std::string> wrap_text(const std::string& text, int width) {
|
||||
std::vector<std::string> 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<size_t>(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<size_t>(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<Impl>()) {
|
||||
pImpl->config = RenderConfig();
|
||||
}
|
||||
|
||||
TextRenderer::~TextRenderer() = default;
|
||||
|
||||
std::vector<RenderedLine> TextRenderer::render(const ParsedDocument& doc, int screen_width) {
|
||||
std::vector<RenderedLine> 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);
|
||||
}
|
||||
}
|
||||
60
src/text_renderer.h
Normal file
60
src/text_renderer.h
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
#pragma once
|
||||
|
||||
#include "html_parser.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <curses.h>
|
||||
|
||||
// 渲染后的行信息
|
||||
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<RenderedLine> render(const ParsedDocument& doc, int screen_width);
|
||||
|
||||
// 设置渲染配置
|
||||
void set_config(const RenderConfig& config);
|
||||
|
||||
// 获取当前配置
|
||||
RenderConfig get_config() const;
|
||||
|
||||
private:
|
||||
class Impl;
|
||||
std::unique_ptr<Impl> 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();
|
||||
Loading…
Reference in a new issue