feat: v0.10.2 —— PDF 表格与代码高亮,Git 按钮重排
PDF 导出 - 表格用 QTextTable 渲染:表头灰底加粗、单元格边框、按 :---: 保留对齐 - 代码块接入 CodeHighlight:关键字/字符串/数字/注释/预处理指令分色 - 修掉 MarkdownRenderer 里表格分隔行正则丢失反斜杠的问题 侧栏 Git - 按钮重排:第一行 获取 / 拉取 / 添加,第二行 提交 / 推送 - 「暂存」改名为「添加」,和右键菜单用词一致 其它 - 默认欢迎文档里的示例表格确认可用(编辑模式渲染成表格控件) - README 同步,版本 0.10.2
This commit is contained in:
+8
-8
@@ -340,17 +340,17 @@ void MainWindow::createSidebar()
|
||||
return button;
|
||||
};
|
||||
|
||||
m_gitPullButton = makeGitButton(tr("拉取"), tr("git pull --ff-only"));
|
||||
m_gitFetchButton = makeGitButton(tr("获取"), tr("git fetch --all --prune"));
|
||||
m_gitPushButton = makeGitButton(tr("推送"), tr("git push"));
|
||||
m_gitStageButton = makeGitButton(tr("暂存"), tr("git add -A"));
|
||||
m_gitPullButton = makeGitButton(tr("拉取"), tr("git pull --ff-only"));
|
||||
m_gitStageButton = makeGitButton(tr("添加"), tr("git add -A"));
|
||||
m_gitCommitButton = makeGitButton(tr("提交"), tr("把暂存区的内容提交"));
|
||||
m_gitPushButton = makeGitButton(tr("推送"), tr("git push"));
|
||||
|
||||
gitLayout->addWidget(m_gitPullButton, 1, 0);
|
||||
gitLayout->addWidget(m_gitFetchButton, 1, 1);
|
||||
gitLayout->addWidget(m_gitPushButton, 1, 2);
|
||||
gitLayout->addWidget(m_gitStageButton, 2, 0);
|
||||
gitLayout->addWidget(m_gitCommitButton, 2, 1, 1, 2);
|
||||
gitLayout->addWidget(m_gitFetchButton, 1, 0);
|
||||
gitLayout->addWidget(m_gitPullButton, 1, 1);
|
||||
gitLayout->addWidget(m_gitStageButton, 1, 2);
|
||||
gitLayout->addWidget(m_gitCommitButton, 2, 0, 1, 2);
|
||||
gitLayout->addWidget(m_gitPushButton, 2, 2);
|
||||
|
||||
layout->addWidget(m_gitBar);
|
||||
m_gitBar->setVisible(false);
|
||||
|
||||
+166
-1
@@ -8,6 +8,7 @@
|
||||
#include <QTextBlockFormat>
|
||||
#include <QTextCharFormat>
|
||||
#include <QTextFrame>
|
||||
#include <QTextTable>
|
||||
#include <QTextCursor>
|
||||
#include <QTextDocument>
|
||||
|
||||
@@ -43,6 +44,12 @@ struct Style
|
||||
QColor codeText = hex("#24292f");
|
||||
QColor codeBg = hex("#f3f4f6");
|
||||
QColor border = hex("#d0d7de");
|
||||
QColor tableHeader = hex("#f6f8fa");
|
||||
QColor keyword = hex("#0550ae");
|
||||
QColor string = hex("#0a7d3a");
|
||||
QColor number = hex("#953800");
|
||||
QColor comment = hex("#6e7781");
|
||||
QColor preprocessor = hex("#8250df");
|
||||
QColor link = hex("#0969da");
|
||||
QColor marker = hex("#0969da");
|
||||
};
|
||||
@@ -50,6 +57,26 @@ struct Style
|
||||
constexpr qreal kBodySize = 11.5;
|
||||
constexpr qreal kCodeSize = 10.5;
|
||||
|
||||
bool isTableSeparator(const QString &text)
|
||||
{
|
||||
static const QRegularExpression pattern(
|
||||
QStringLiteral(R"(^\s*\|?\s*:?-{2,}:?\s*(?:\|\s*:?-{2,}:?\s*)+\|?\s*$)"));
|
||||
return pattern.match(text).hasMatch();
|
||||
}
|
||||
|
||||
QStringList splitTableRow(const QString &line)
|
||||
{
|
||||
QString text = line.trimmed();
|
||||
if (text.startsWith(QLatin1Char('|')))
|
||||
text.remove(0, 1);
|
||||
if (text.endsWith(QLatin1Char('|')))
|
||||
text.chop(1);
|
||||
QStringList cells = text.split(QLatin1Char('|'));
|
||||
for (QString &cell : cells)
|
||||
cell = cell.trimmed();
|
||||
return cells;
|
||||
}
|
||||
|
||||
class Renderer
|
||||
{
|
||||
public:
|
||||
@@ -67,6 +94,8 @@ private:
|
||||
void renderParagraph(const QString &text, int quoteDepth);
|
||||
void renderListItem(const QString &marker, const QString &text, int indentLevel);
|
||||
void renderCodeBlock(const QStringList &lines, const QString &info);
|
||||
void renderTable(const QStringList &rows, const QString &separator);
|
||||
QColor tokenColor(CodeHighlight::Token token) const;
|
||||
void renderRule();
|
||||
void renderInline(const QString &text, const QTextCharFormat &base);
|
||||
|
||||
@@ -217,9 +246,34 @@ void Renderer::renderCodeBlock(const QStringList &lines, const QString &info)
|
||||
if (content.isEmpty())
|
||||
content << QString();
|
||||
|
||||
const CodeHighlight::Language codeLanguage = CodeHighlight::fromInfoString(info);
|
||||
bool inComment = false;
|
||||
|
||||
for (const QString &line : content) {
|
||||
insideBlock(codeBlock, format);
|
||||
frameCursor.insertText(line.isEmpty() ? QStringLiteral(" ") : line, format);
|
||||
|
||||
bool endsInComment = inComment;
|
||||
int position = 0;
|
||||
|
||||
if (codeLanguage != CodeHighlight::Language::None) {
|
||||
const QVector<CodeHighlight::Span> spans =
|
||||
CodeHighlight::tokenize(line, codeLanguage, inComment, &endsInComment);
|
||||
for (const CodeHighlight::Span &span : spans) {
|
||||
if (span.start > position)
|
||||
frameCursor.insertText(line.mid(position, span.start - position), format);
|
||||
|
||||
QTextCharFormat tokenFormat = format;
|
||||
tokenFormat.setForeground(tokenColor(span.token));
|
||||
frameCursor.insertText(line.mid(span.start, span.length), tokenFormat);
|
||||
position = span.start + span.length;
|
||||
}
|
||||
}
|
||||
inComment = endsInComment;
|
||||
|
||||
if (position < line.size())
|
||||
frameCursor.insertText(line.mid(position), format);
|
||||
if (line.isEmpty())
|
||||
frameCursor.insertText(QStringLiteral(" "), format);
|
||||
}
|
||||
|
||||
// 光标挪到边框外面,后面的内容接着写
|
||||
@@ -227,6 +281,102 @@ void Renderer::renderCodeBlock(const QStringList &lines, const QString &info)
|
||||
m_cursor.setPosition(frame->lastPosition());
|
||||
}
|
||||
|
||||
QColor Renderer::tokenColor(CodeHighlight::Token token) const
|
||||
{
|
||||
switch (token) {
|
||||
case CodeHighlight::Token::Keyword:
|
||||
case CodeHighlight::Token::Type:
|
||||
return m_style.keyword;
|
||||
case CodeHighlight::Token::String:
|
||||
return m_style.string;
|
||||
case CodeHighlight::Token::Number:
|
||||
return m_style.number;
|
||||
case CodeHighlight::Token::Comment:
|
||||
return m_style.comment;
|
||||
case CodeHighlight::Token::Preprocessor:
|
||||
return m_style.preprocessor;
|
||||
}
|
||||
return m_style.codeText;
|
||||
}
|
||||
|
||||
void Renderer::renderTable(const QStringList &rows, const QString &separator)
|
||||
{
|
||||
if (rows.isEmpty())
|
||||
return;
|
||||
|
||||
if (m_firstBlock) {
|
||||
QTextBlockFormat emptyFormat;
|
||||
beginBlock(emptyFormat, bodyFormat());
|
||||
}
|
||||
|
||||
const QStringList headerCells = splitTableRow(rows.first());
|
||||
const int columns = qMax(1, headerCells.size());
|
||||
|
||||
QVector<Qt::Alignment> alignments;
|
||||
for (const QString &cell : splitTableRow(separator)) {
|
||||
const bool left = cell.startsWith(QLatin1Char(':'));
|
||||
const bool right = cell.endsWith(QLatin1Char(':'));
|
||||
if (left && right)
|
||||
alignments.append(Qt::AlignHCenter);
|
||||
else if (right)
|
||||
alignments.append(Qt::AlignRight);
|
||||
else
|
||||
alignments.append(Qt::AlignLeft);
|
||||
}
|
||||
|
||||
QTextTableFormat tableFormat;
|
||||
tableFormat.setBorder(1);
|
||||
tableFormat.setBorderBrush(m_style.border);
|
||||
tableFormat.setBorderStyle(QTextFrameFormat::BorderStyle_Solid);
|
||||
tableFormat.setCellSpacing(0);
|
||||
tableFormat.setCellPadding(6);
|
||||
tableFormat.setWidth(QTextLength(QTextLength::PercentageLength, 100));
|
||||
tableFormat.setTopMargin(6);
|
||||
tableFormat.setBottomMargin(12);
|
||||
|
||||
QTextTable *table = m_cursor.insertTable(rows.size(), columns, tableFormat);
|
||||
for (int row = 0; row < rows.size(); ++row) {
|
||||
const QStringList cells = (row == 0) ? headerCells : splitTableRow(rows.at(row));
|
||||
for (int column = 0; column < columns; ++column) {
|
||||
QTextTableCell cell = table->cellAt(row, column);
|
||||
|
||||
QTextTableCellFormat cellFormat = cell.format().toTableCellFormat();
|
||||
cellFormat.setBackground(row == 0 ? m_style.tableHeader : m_style.codeBg.lighter(104));
|
||||
cellFormat.setTopBorder(1);
|
||||
cellFormat.setBottomBorder(1);
|
||||
cellFormat.setLeftBorder(1);
|
||||
cellFormat.setRightBorder(1);
|
||||
cellFormat.setTopBorderBrush(m_style.border);
|
||||
cellFormat.setBottomBorderBrush(m_style.border);
|
||||
cellFormat.setLeftBorderBrush(m_style.border);
|
||||
cellFormat.setRightBorderBrush(m_style.border);
|
||||
cellFormat.setTopBorderStyle(QTextFrameFormat::BorderStyle_Solid);
|
||||
cellFormat.setBottomBorderStyle(QTextFrameFormat::BorderStyle_Solid);
|
||||
cellFormat.setLeftBorderStyle(QTextFrameFormat::BorderStyle_Solid);
|
||||
cellFormat.setRightBorderStyle(QTextFrameFormat::BorderStyle_Solid);
|
||||
cell.setFormat(cellFormat);
|
||||
|
||||
QTextCursor cellCursor = cell.firstCursorPosition();
|
||||
QTextBlockFormat blockFormat;
|
||||
blockFormat.setAlignment(alignments.value(column, Qt::AlignLeft));
|
||||
cellCursor.setBlockFormat(blockFormat);
|
||||
|
||||
QTextCharFormat charFormat;
|
||||
charFormat.setFontFamilies(proseFamilies());
|
||||
charFormat.setFontPointSize(kBodySize);
|
||||
charFormat.setForeground(m_style.text);
|
||||
if (row == 0) {
|
||||
charFormat.setFontWeight(QFont::Bold);
|
||||
charFormat.setForeground(m_style.heading);
|
||||
}
|
||||
cellCursor.insertText(cells.value(column), charFormat);
|
||||
}
|
||||
}
|
||||
|
||||
m_cursor = QTextCursor(m_document);
|
||||
m_cursor.setPosition(table->lastPosition());
|
||||
}
|
||||
|
||||
void Renderer::renderRule()
|
||||
{
|
||||
QTextBlockFormat blockFormat;
|
||||
@@ -365,6 +515,21 @@ void Renderer::run(const QString &markdown)
|
||||
while (index < lines.size()) {
|
||||
const QString line = lines.at(index);
|
||||
|
||||
// 表格:当前行有 |,下一行是 |---|
|
||||
if (line.contains(QLatin1Char('|')) && index + 1 < lines.size()
|
||||
&& isTableSeparator(lines.at(index + 1))) {
|
||||
QStringList rows{line};
|
||||
int next = index + 2;
|
||||
while (next < lines.size() && !lines.at(next).trimmed().isEmpty()
|
||||
&& lines.at(next).contains(QLatin1Char('|'))) {
|
||||
rows << lines.at(next);
|
||||
++next;
|
||||
}
|
||||
renderTable(rows, lines.at(index + 1));
|
||||
index = next;
|
||||
continue;
|
||||
}
|
||||
|
||||
const QRegularExpressionMatch fence = fencePattern.match(line);
|
||||
if (fence.hasMatch()) {
|
||||
QStringList code;
|
||||
|
||||
Reference in New Issue
Block a user