mirror of
https://github.com/m1ngsama/TUT.git
synced 2025-12-25 02:57:08 +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