editor: auto format table

This commit is contained in:
Le Tan 2018-11-28 19:31:26 +08:00
parent bcb6adef30
commit 70caa4d932
23 changed files with 1084 additions and 79 deletions

2
.gitignore vendored
View File

@ -1 +1 @@
/VNote.pro.user
VNote.pro.user

View File

@ -1,5 +1,5 @@
# VNote
- [英文 English](./README.md)
[英文 English](./README.md)
**VNote是一个更懂程序员和Markdown的笔记**

View File

@ -121,6 +121,40 @@ struct VMathjaxBlock
};
struct VTableBlock
{
VTableBlock()
: m_startPos(-1),
m_endPos(-1)
{
}
bool isValid() const
{
return m_startPos > -1 && m_endPos >= m_startPos;
}
void clear()
{
m_startPos = m_endPos = -1;
m_borders.clear();
}
QString toString() const
{
return QString("table [%1,%2) borders %3").arg(m_startPos)
.arg(m_endPos)
.arg(m_borders.size());
}
int m_startPos;
int m_endPos;
// Global position of the table borders in ascending order.
QVector<int> m_borders;
};
// Highlight unit with global position and string style name.
struct HLUnitPos
{
@ -157,6 +191,11 @@ struct VElementRegion
return m_startPos <= p_pos && m_endPos > p_pos;
}
bool contains(const VElementRegion &p_reg) const
{
return m_startPos <= p_reg.m_startPos && m_endPos >= p_reg.m_endPos;
}
bool intersect(int p_start, int p_end) const
{
return !(p_end <= m_startPos || p_start >= m_endPos);

View File

@ -51,6 +51,8 @@ PegHighlighterResult::PegHighlighterResult(const PegMarkdownHighlighter *p_peg,
parseMathjaxBlocks(p_peg, p_result);
parseHRuleBlocks(p_peg, p_result);
parseTableBlocks(p_peg, p_result);
}
static bool compHLUnit(const HLUnit &p_a, const HLUnit &p_b)
@ -270,6 +272,66 @@ void PegHighlighterResult::parseFencedCodeBlocks(const PegMarkdownHighlighter *p
}
}
void PegHighlighterResult::parseTableBlocks(const PegMarkdownHighlighter *p_peg,
const QSharedPointer<PegParseResult> &p_result)
{
const QVector<VElementRegion> &tableRegs = p_result->m_tableRegions;
const QVector<VElementRegion> &headerRegs = p_result->m_tableHeaderRegions;
const QVector<VElementRegion> &borderRegs = p_result->m_tableBorderRegions;
VTableBlock item;
int headerIdx = 0, borderIdx = 0;
for (int tableIdx = 0; tableIdx < tableRegs.size(); ++tableIdx) {
const auto &reg = tableRegs[tableIdx];
if (headerIdx < headerRegs.size()) {
if (reg.contains(headerRegs[headerIdx])) {
// A new table.
if (item.isValid()) {
// Save previous table.
m_tableBlocks.append(item);
auto &table = m_tableBlocks.back();
// Fill borders.
for (; borderIdx < borderRegs.size(); ++borderIdx) {
if (borderRegs[borderIdx].m_startPos >= table.m_startPos
&& borderRegs[borderIdx].m_endPos <= table.m_endPos) {
table.m_borders.append(borderRegs[borderIdx].m_startPos);
} else {
break;
}
}
}
item.clear();
item.m_startPos = reg.m_startPos;
item.m_endPos = reg.m_endPos;
++headerIdx;
continue;
}
}
// Continue previous table.
item.m_endPos = reg.m_endPos;
}
if (item.isValid()) {
// Another table.
m_tableBlocks.append(item);
// Fill borders.
auto &table = m_tableBlocks.back();
for (; borderIdx < borderRegs.size(); ++borderIdx) {
if (borderRegs[borderIdx].m_startPos >= table.m_startPos
&& borderRegs[borderIdx].m_endPos <= table.m_endPos) {
table.m_borders.append(borderRegs[borderIdx].m_startPos);
} else {
break;
}
}
}
}
static inline bool isDisplayFormulaRawEnd(const QString &p_text)
{
QRegExp regex("\\\\end\\{[^{}\\s\\r\\n]+\\}$");
@ -330,8 +392,8 @@ void PegHighlighterResult::parseMathjaxBlocks(const PegMarkdownHighlighter *p_pe
break;
}
int pib = r.m_startPos - block.position();
int length = r.m_endPos - r.m_startPos;
int pib = qMax(r.m_startPos - block.position(), 0);
int length = qMin(r.m_endPos - block.position() - pib, block.length() - 1);
QString text = block.text().mid(pib, length);
if (inBlock) {
item.m_text = item.m_text + "\n" + text;

View File

@ -88,6 +88,10 @@ public:
QSet<int> m_hruleBlocks;
// All table blocks.
// Sorted by start position ascendingly.
QVector<VTableBlock> m_tableBlocks;
private:
// Parse highlight elements for blocks from one parse result.
static void parseBlocksHighlightOne(QVector<QVector<HLUnit>> &p_blocksHighlights,
@ -108,6 +112,10 @@ private:
void parseHRuleBlocks(const PegMarkdownHighlighter *p_peg,
const QSharedPointer<PegParseResult> &p_result);
// Parse table blocks from parse results.
void parseTableBlocks(const PegMarkdownHighlighter *p_peg,
const QSharedPointer<PegParseResult> &p_result);
#if 0
void parseBlocksElementRegionOne(QHash<int, QVector<VElementRegion>> &p_regs,
const QTextDocument *p_doc,

View File

@ -203,7 +203,7 @@ static bool containSpecialChar(const QString &p_str)
QChar la = p_str[p_str.size() - 1];
return fi == '#'
|| la == '`' || la == '$' || la == '*' || la == '_';
|| la == '`' || la == '$' || la == '~' || la == '*' || la == '_';
}
bool PegMarkdownHighlighter::preHighlightSingleFormatBlock(const QVector<QVector<HLUnit>> &p_highlights,
@ -757,6 +757,8 @@ void PegMarkdownHighlighter::completeHighlight(QSharedPointer<PegHighlighterResu
emit mathjaxBlocksUpdated(p_result->m_mathjaxBlocks);
}
emit tableBlocksUpdated(p_result->m_tableBlocks);
emit imageLinksUpdated(p_result->m_imageRegions);
emit headersUpdated(p_result->m_headerRegions);
}

View File

@ -70,6 +70,9 @@ signals:
// Emitted when Mathjax blocks updated.
void mathjaxBlocksUpdated(const QVector<VMathjaxBlock> &p_mathjaxBlocks);
// Emitted when table blocks updated.
void tableBlocksUpdated(const QVector<VTableBlock> &p_tableBlocks);
protected:
void highlightBlock(const QString &p_text) Q_DECL_OVERRIDE;

View File

@ -25,30 +25,20 @@ void PegParseResult::parse(QAtomicInt &p_stop, bool p_fast)
parseDisplayFormulaRegions(p_stop);
parseHRuleRegions(p_stop);
parseTableRegions(p_stop);
parseTableHeaderRegions(p_stop);
parseTableBorderRegions(p_stop);
}
void PegParseResult::parseImageRegions(QAtomicInt &p_stop)
{
// From Qt5.7, the capacity is preserved.
m_imageRegions.clear();
if (isEmpty()) {
return;
}
pmh_element *elem = m_pmhElements[pmh_IMAGE];
while (elem != NULL) {
if (elem->end <= elem->pos) {
elem = elem->next;
continue;
}
if (p_stop.load() == 1) {
return;
}
m_imageRegions.push_back(VElementRegion(m_offset + elem->pos, m_offset + elem->end));
elem = elem->next;
}
parseRegions(p_stop,
pmh_IMAGE,
m_imageRegions,
false);
}
void PegParseResult::parseHeaderRegions(QAtomicInt &p_stop)
@ -113,64 +103,63 @@ void PegParseResult::parseFencedCodeBlockRegions(QAtomicInt &p_stop)
void PegParseResult::parseInlineEquationRegions(QAtomicInt &p_stop)
{
m_inlineEquationRegions.clear();
if (isEmpty()) {
return;
}
pmh_element *elem = m_pmhElements[pmh_INLINEEQUATION];
while (elem != NULL) {
if (elem->end <= elem->pos) {
elem = elem->next;
continue;
}
if (p_stop.load() == 1) {
return;
}
m_inlineEquationRegions.push_back(VElementRegion(m_offset + elem->pos, m_offset + elem->end));
elem = elem->next;
}
parseRegions(p_stop,
pmh_INLINEEQUATION,
m_inlineEquationRegions,
false);
}
void PegParseResult::parseDisplayFormulaRegions(QAtomicInt &p_stop)
{
m_displayFormulaRegions.clear();
if (isEmpty()) {
return;
}
pmh_element *elem = m_pmhElements[pmh_DISPLAYFORMULA];
while (elem != NULL) {
if (elem->end <= elem->pos) {
elem = elem->next;
continue;
}
if (p_stop.load() == 1) {
return;
}
m_displayFormulaRegions.push_back(VElementRegion(m_offset + elem->pos, m_offset + elem->end));
elem = elem->next;
}
if (p_stop.load() == 1) {
return;
}
std::sort(m_displayFormulaRegions.begin(), m_displayFormulaRegions.end());
parseRegions(p_stop,
pmh_DISPLAYFORMULA,
m_displayFormulaRegions,
true);
}
void PegParseResult::parseHRuleRegions(QAtomicInt &p_stop)
{
m_hruleRegions.clear();
parseRegions(p_stop,
pmh_HRULE,
m_hruleRegions,
false);
}
void PegParseResult::parseTableRegions(QAtomicInt &p_stop)
{
parseRegions(p_stop,
pmh_TABLE,
m_tableRegions,
true);
}
void PegParseResult::parseTableHeaderRegions(QAtomicInt &p_stop)
{
parseRegions(p_stop,
pmh_TABLEHEADER,
m_tableHeaderRegions,
true);
}
void PegParseResult::parseTableBorderRegions(QAtomicInt &p_stop)
{
parseRegions(p_stop,
pmh_TABLEBORDER,
m_tableBorderRegions,
true);
}
void PegParseResult::parseRegions(QAtomicInt &p_stop,
pmh_element_type p_type,
QVector<VElementRegion> &p_result,
bool p_sort)
{
p_result.clear();
if (isEmpty()) {
return;
}
pmh_element *elem = m_pmhElements[pmh_HRULE];
pmh_element *elem = m_pmhElements[p_type];
while (elem != NULL) {
if (elem->end <= elem->pos) {
elem = elem->next;
@ -181,9 +170,13 @@ void PegParseResult::parseHRuleRegions(QAtomicInt &p_stop)
return;
}
m_hruleRegions.push_back(VElementRegion(m_offset + elem->pos, m_offset + elem->end));
p_result.push_back(VElementRegion(m_offset + elem->pos, m_offset + elem->end));
elem = elem->next;
}
if (p_sort && p_stop.load() != 1) {
std::sort(p_result.begin(), p_result.end());
}
}

View File

@ -114,6 +114,16 @@ struct PegParseResult
// HRule regions.
QVector<VElementRegion> m_hruleRegions;
// All table regions.
// Sorted by start position.
QVector<VElementRegion> m_tableRegions;
// All table header regions.
QVector<VElementRegion> m_tableHeaderRegions;
// All table border regions.
QVector<VElementRegion> m_tableBorderRegions;
private:
void parseImageRegions(QAtomicInt &p_stop);
@ -126,6 +136,17 @@ private:
void parseDisplayFormulaRegions(QAtomicInt &p_stop);
void parseHRuleRegions(QAtomicInt &p_stop);
void parseTableRegions(QAtomicInt &p_stop);
void parseTableHeaderRegions(QAtomicInt &p_stop);
void parseTableBorderRegions(QAtomicInt &p_stop);
void parseRegions(QAtomicInt &p_stop,
pmh_element_type p_type,
QVector<VElementRegion> &p_result,
bool p_sort = false);
};
class PegParserWorker : public QThread

View File

@ -143,6 +143,7 @@ hr {
table {
padding: 0;
margin: 1rem 0.5rem;
border-collapse: collapse;
}

View File

@ -141,6 +141,7 @@ hr {
table {
padding: 0;
margin: 1rem 0.5rem;
border-collapse: collapse;
}

View File

@ -136,6 +136,7 @@ hr {
table {
padding: 0;
margin: 1rem 0.5rem;
border-collapse: collapse;
}

View File

@ -138,6 +138,7 @@ hr {
table {
padding: 0;
margin: 1rem 0.5rem;
border-collapse: collapse;
}

View File

@ -19,6 +19,13 @@ ICON = resources/icons/vnote.icns
TRANSLATIONS += translations/vnote_zh_CN.ts
*-g++ {
QMAKE_CFLAGS_WARN_ON += -Wno-class-memaccess
QMAKE_CXXFLAGS_WARN_ON += -Wno-class-memaccess
QMAKE_CFLAGS += -Wno-class-memaccess
QMAKE_CXXFLAGS += -Wno-class-memaccess
}
SOURCES += main.cpp\
vmainwindow.cpp \
vdirectorytree.cpp \
@ -150,7 +157,9 @@ SOURCES += main.cpp\
utils/vkeyboardlayoutmanager.cpp \
dialog/vkeyboardlayoutmappingdialog.cpp \
vfilelistwidget.cpp \
widgets/vcombobox.cpp
widgets/vcombobox.cpp \
vtablehelper.cpp \
vtable.cpp
HEADERS += vmainwindow.h \
vdirectorytree.h \
@ -293,7 +302,9 @@ HEADERS += vmainwindow.h \
utils/vkeyboardlayoutmanager.h \
dialog/vkeyboardlayoutmappingdialog.h \
vfilelistwidget.h \
widgets/vcombobox.h
widgets/vcombobox.h \
vtablehelper.h \
vtable.h
RESOURCES += \
vnote.qrc \

View File

@ -213,6 +213,7 @@ public:
virtual bool findW(const QRegExp &p_exp,
QTextDocument::FindFlags p_options = QTextDocument::FindFlags()) = 0;
virtual bool isReadOnlyW() const = 0;
virtual void setReadOnlyW(bool p_ro) = 0;
virtual QWidget *viewportW() const = 0;

View File

@ -820,7 +820,7 @@ void VMainWindow::initHelpMenu()
docAct->setToolTip(tr("View VNote's documentation"));
connect(docAct, &QAction::triggered,
this, []() {
QString url("http://vnote.readthedocs.io");
QString url("https://tamlok.github.io/vnote");
QDesktopServices::openUrl(url);
});
@ -828,7 +828,7 @@ void VMainWindow::initHelpMenu()
donateAct->setToolTip(tr("Donate to VNote or view the donate list"));
connect(donateAct, &QAction::triggered,
this, []() {
QString url("https://github.com/tamlok/vnote#donate");
QString url("https://tamlok.github.io/vnote/en_us/#!donate.md");
QDesktopServices::openUrl(url);
});

View File

@ -32,6 +32,7 @@
#include "vgraphvizhelper.h"
#include "vmdtab.h"
#include "vdownloader.h"
#include "vtablehelper.h"
extern VWebUtils *g_webUtils;
@ -109,6 +110,10 @@ VMdEditor::VMdEditor(VFile *p_file,
connect(m_previewMgr, &VPreviewManager::requestUpdateImageLinks,
m_pegHighlighter, &PegMarkdownHighlighter::updateHighlight);
m_tableHelper = new VTableHelper(this);
connect(m_pegHighlighter, &PegMarkdownHighlighter::tableBlocksUpdated,
m_tableHelper, &VTableHelper::updateTableBlocks);
m_editOps = new VMdEditOperations(this, m_file);
connect(m_editOps, &VEditOperations::statusMessage,
m_object, &VEditorObject::statusMessage);
@ -1446,7 +1451,8 @@ void VMdEditor::initLinkAndPreviewMenu(QAction *p_before, QMenu *p_menu, const Q
if (regExp.indexIn(text) > -1) {
const QVector<VElementRegion> &imgRegs = m_pegHighlighter->getImageRegions();
for (auto const & reg : imgRegs) {
if (!reg.contains(pos)) {
if (!reg.contains(pos)
&& (!reg.contains(pos - 1) || pos != (block.position() + text.size()))) {
continue;
}
@ -1608,7 +1614,8 @@ bool VMdEditor::initInPlacePreviewMenu(QAction *p_before,
int pib = p_pos - p_block.position();
for (auto info : previews) {
const VPreviewedImageInfo &pii = info->m_imageInfo;
if (pii.contains(pib)) {
if (pii.contains(pib)
|| (pii.contains(pib - 1) && pib == p_block.length() - 1)) {
const QPixmap *img = findImage(pii.m_imageName);
if (img) {
image = *img;

View File

@ -22,6 +22,7 @@ class VDocument;
class VPreviewManager;
class VCopyTextAsHtmlDialog;
class VEditTab;
class VTableHelper;
class VMdEditor : public VTextEdit, public VEditor
{
@ -152,6 +153,11 @@ public:
return find(p_exp, p_options);
}
bool isReadOnlyW() const Q_DECL_OVERRIDE
{
return isReadOnly();
}
void setReadOnlyW(bool p_ro) Q_DECL_OVERRIDE
{
setReadOnly(p_ro);
@ -325,6 +331,8 @@ private:
VPreviewManager *m_previewMgr;
VTableHelper *m_tableHelper;
// Image links inserted while editing.
QVector<ImageLink> m_insertedImages;

585
src/vtable.cpp Normal file
View File

@ -0,0 +1,585 @@
#include "vtable.h"
#include <QTextDocument>
#include <QTextLayout>
#include "veditor.h"
const QString VTable::c_defaultDelimiter = "---";
enum { HeaderRowIndex = 0, DelimiterRowIndex = 1 };
VTable::VTable(VEditor *p_editor, const VTableBlock &p_block)
: m_editor(p_editor)
{
parseFromTableBlock(p_block);
}
bool VTable::isValid() const
{
return header() && header()->isValid()
&& delimiter() && delimiter()->isValid();
}
void VTable::parseFromTableBlock(const VTableBlock &p_block)
{
clear();
QTextDocument *doc = m_editor->documentW();
QTextBlock block = doc->findBlock(p_block.m_startPos);
if (!block.isValid()) {
return;
}
int lastBlockNumber = doc->findBlock(p_block.m_endPos - 1).blockNumber();
if (lastBlockNumber == -1) {
return;
}
const QVector<int> &borders = p_block.m_borders;
if (borders.isEmpty()) {
return;
}
int numRows = lastBlockNumber - block.blockNumber() + 1;
if (numRows <= DelimiterRowIndex) {
return;
}
calculateBasicWidths(block, borders[0]);
int borderIdx = 0;
m_rows.reserve(numRows);
for (int i = 0; i < numRows; ++i) {
m_rows.append(Row());
if (!parseOneRow(block, borders, borderIdx, m_rows.last())) {
clear();
return;
}
block = block.next();
}
}
bool VTable::parseOneRow(const QTextBlock &p_block,
const QVector<int> &p_borders,
int &p_borderIdx,
Row &p_row) const
{
if (!p_block.isValid() || p_borderIdx >= p_borders.size()) {
return false;
}
p_row.m_block = p_block;
QString text = p_block.text();
int startPos = p_block.position();
int endPos = startPos + text.length();
if (p_borders[p_borderIdx] < startPos
|| p_borders[p_borderIdx] >= endPos) {
return false;
}
for (; p_borderIdx < p_borders.size(); ++p_borderIdx) {
int border = p_borders[p_borderIdx];
if (border >= endPos) {
break;
}
int offset = border - startPos;
if (text[offset] != '|') {
return false;
}
int nextIdx = p_borderIdx + 1;
if (nextIdx >= p_borders.size() || p_borders[nextIdx] >= endPos) {
// The last border of this row.
++p_borderIdx;
break;
}
int nextOffset = p_borders[nextIdx] - startPos;
if (text[nextOffset] != '|') {
return false;
}
// Got one cell.
Cell cell;
cell.m_offset = offset;
cell.m_length = nextOffset - offset;
cell.m_text = text.mid(cell.m_offset, cell.m_length);
p_row.m_cells.append(cell);
}
return true;
}
void VTable::clear()
{
m_rows.clear();
m_spaceWidth = 0;
m_minusWidth = 0;
m_colonWidth = 0;
m_defaultDelimiterWidth = 0;
}
void VTable::format()
{
if (!isValid()) {
return;
}
QTextCursor cursor = m_editor->textCursorW();
int curRowIdx = cursor.blockNumber() - m_rows[0].m_block.blockNumber();
int curPib = -1;
if (curRowIdx < 0 || curRowIdx >= m_rows.size()) {
curRowIdx = -1;
} else {
curPib = cursor.positionInBlock();
}
int nrCols = calculateColumnCount();
for (int i = 0; i < nrCols; ++i) {
formatOneColumn(i, curRowIdx, curPib);
}
}
int VTable::calculateColumnCount() const
{
int nr = 0;
// Find the longest row.
for (const auto & row : m_rows) {
if (row.m_cells.size() > nr) {
nr = row.m_cells.size();
}
}
return nr;
}
VTable::Row *VTable::header() const
{
if (m_rows.size() <= HeaderRowIndex) {
return NULL;
}
return const_cast<VTable::Row *>(&m_rows[HeaderRowIndex]);
}
VTable::Row *VTable::delimiter() const
{
if (m_rows.size() <= DelimiterRowIndex) {
return NULL;
}
return const_cast<VTable::Row *>(&m_rows[DelimiterRowIndex]);
}
void VTable::formatOneColumn(int p_idx, int p_cursorRowIdx, int p_cursorPib)
{
QVector<CellInfo> cells;
int targetWidth = 0;
fetchCellInfoOfColumn(p_idx, cells, targetWidth);
// Get the alignment of this column.
const VTable::Alignment align = getColumnAlignment(p_idx);
// Calculate the formatted text of each cell.
for (int rowIdx = 0; rowIdx < cells.size(); ++rowIdx) {
auto & info = cells[rowIdx];
auto & row = m_rows[rowIdx];
if (row.m_cells.size() <= p_idx) {
row.m_cells.resize(p_idx + 1);
}
auto & cell = row.m_cells[p_idx];
Q_ASSERT(cell.m_formattedText.isEmpty());
Q_ASSERT(cell.m_cursorCoreOffset == -1);
// Record the cursor position.
if (rowIdx == p_cursorRowIdx) {
if (cell.m_offset <= p_cursorPib && cell.m_offset + cell.m_length > p_cursorPib) {
// Cursor in this cell.
int offset = p_cursorPib - cell.m_offset;
offset = offset - info.m_coreOffset;
if (offset > info.m_coreLength) {
offset = info.m_coreLength;
} else if (offset < 0) {
offset = 0;
}
cell.m_cursorCoreOffset = offset;
}
}
if (isDelimiterRow(rowIdx)) {
if (!isDelimiterCellWellFormatted(cell, info, targetWidth)) {
QString core;
// Round to 1 when above 0.5 approximately.
int delta = m_minusWidth / 2;
switch (align) {
case Alignment::None:
core = QString((targetWidth + delta) / m_minusWidth, '-');
break;
case Alignment::Left:
core = ":";
core += QString((targetWidth - m_colonWidth + delta) / m_minusWidth, '-');
break;
case Alignment::Center:
core = ":";
core += QString((targetWidth - 2 * m_colonWidth + delta) / m_minusWidth, '-');
core += ":";
break;
case Alignment::Right:
core = QString((targetWidth - m_colonWidth + delta) / m_minusWidth, '-');
core += ":";
break;
default:
Q_ASSERT(false);
break;
}
Alignment fakeAlign = align == Alignment::None ? Alignment::Left : align;
cell.m_formattedText = generateFormattedText(core,
0,
fakeAlign);
}
} else {
Alignment fakeAlign = align;
if (fakeAlign == Alignment::None) {
// For Alignment::None, we make the header align center while
// content cells align left.
if (isHeaderRow(rowIdx)) {
fakeAlign = Alignment::Center;
} else {
fakeAlign = Alignment::Left;
}
}
if (!isCellWellFormatted(row, cell, info, targetWidth, fakeAlign)) {
QString core = cell.m_text.mid(info.m_coreOffset, info.m_coreLength);
int nr = (targetWidth - info.m_coreWidth + m_spaceWidth / 2) / m_spaceWidth;
cell.m_formattedText = generateFormattedText(core, nr, fakeAlign);
}
}
}
}
void VTable::fetchCellInfoOfColumn(int p_idx,
QVector<CellInfo> &p_cellsInfo,
int &p_targetWidth) const
{
p_targetWidth = m_defaultDelimiterWidth;
p_cellsInfo.resize(m_rows.size());
// Fetch the trimmed core content and its width.
for (int i = 0; i < m_rows.size(); ++i) {
auto & row = m_rows[i];
auto & info = p_cellsInfo[i];
if (row.m_cells.size() <= p_idx) {
// Need to add a new cell later.
continue;
}
// Get the info of this cell.
const auto & cell = row.m_cells[p_idx];
int first = 1, last = cell.m_length - 2;
for (; first <= last; ++first) {
if (cell.m_text[first] != ' ') {
// Found the core content.
info.m_coreOffset = first;
break;
}
}
if (first > last) {
// Empty cell.
continue;
}
for (; last >= first; --last) {
if (cell.m_text[last] != ' ') {
// Found the last of core content.
info.m_coreLength = last - first + 1;
break;
}
}
// Calculate the core width.
info.m_coreWidth = calculateTextWidth(row.m_block,
cell.m_offset + info.m_coreOffset,
info.m_coreLength);
// Delimiter row's width should not be considered.
if (info.m_coreWidth > p_targetWidth && !isDelimiterRow(i)) {
p_targetWidth = info.m_coreWidth;
}
}
}
void VTable::calculateBasicWidths(const QTextBlock &p_block, int p_borderPos)
{
QFont font;
int pib = p_borderPos - p_block.position();
QVector<QTextLayout::FormatRange> fmts = p_block.layout()->formats();
for (const auto & fmt : fmts) {
if (fmt.start <= pib && fmt.start + fmt.length > pib) {
// Hit.
if (!fmt.format.fontFamily().isEmpty()) {
font = fmt.format.font();
break;
}
}
}
QFontMetrics fm(font);
m_spaceWidth = fm.width(' ');
m_minusWidth = fm.width('-');
m_colonWidth = fm.width(':');
m_defaultDelimiterWidth = fm.width(c_defaultDelimiter);
}
int VTable::calculateTextWidth(const QTextBlock &p_block, int p_pib, int p_length) const
{
QTextLine line = p_block.layout()->lineForTextPosition(p_pib);
if (line.isValid()) {
return line.cursorToX(p_pib + p_length) - line.cursorToX(p_pib);
}
return -1;
}
bool VTable::isHeaderRow(int p_idx) const
{
return p_idx == HeaderRowIndex;
}
bool VTable::isDelimiterRow(int p_idx) const
{
return p_idx == DelimiterRowIndex;
}
QString VTable::generateFormattedText(const QString &p_core,
int p_nrSpaces,
Alignment p_align) const
{
Q_ASSERT(p_align != Alignment::None);
// Align left.
int leftSpaces = 0;
int rightSpaces = p_nrSpaces;
if (p_align == Alignment::Center) {
leftSpaces = p_nrSpaces / 2;
rightSpaces = p_nrSpaces - leftSpaces;
} else if (p_align == Alignment::Right) {
leftSpaces = p_nrSpaces;
rightSpaces = 0;
}
return QString("| %1%2%3 ").arg(QString(leftSpaces, ' '))
.arg(p_core)
.arg(QString(rightSpaces, ' '));
}
VTable::Alignment VTable::getColumnAlignment(int p_idx) const
{
Row *row = delimiter();
if (row->m_cells.size() <= p_idx) {
return Alignment::None;
}
QString core = row->m_cells[p_idx].m_text.mid(1).trimmed();
Q_ASSERT(!core.isEmpty());
bool leftColon = core[0] == ':';
bool rightColon = core[core.size() - 1] == ':';
if (leftColon) {
if (rightColon) {
return Alignment::Center;
} else {
return Alignment::Left;
}
} else {
if (rightColon) {
return Alignment::Right;
} else {
return Alignment::None;
}
}
}
static inline bool equalWidth(int p_a, int p_b, int p_margin = 5)
{
return qAbs(p_a - p_b) < p_margin;
}
bool VTable::isDelimiterCellWellFormatted(const Cell &p_cell,
const CellInfo &p_info,
int p_targetWidth) const
{
// We could use core width here for delimiter cell.
if (!equalWidth(p_info.m_coreWidth, p_targetWidth, m_minusWidth)) {
return false;
}
const QString &text = p_cell.m_text;
if (text.size() < 4) {
return false;
}
if (text[1] != ' ' || text[text.size() - 1] != ' ') {
return false;
}
if (text[2] == ' ' || text[text.size() - 2] == ' ') {
return false;
}
return true;
}
bool VTable::isCellWellFormatted(const Row &p_row,
const Cell &p_cell,
const CellInfo &p_info,
int p_targetWidth,
VTable::Alignment p_align) const
{
Q_ASSERT(p_align != Alignment::None);
const QString &text = p_cell.m_text;
if (text.size() < 4) {
return false;
}
if (text[1] != ' ' || text[text.size() - 1] != ' ') {
return false;
}
// Skip alignment check of empty cell.
if (p_info.m_coreOffset > 0) {
int leftSpaces = p_info.m_coreOffset - 2;
int rightSpaces = text.size() - p_info.m_coreOffset - p_info.m_coreLength - 1;
switch (p_align) {
case Alignment::Left:
if (leftSpaces > 0) {
return false;
}
break;
case Alignment::Center:
if (qAbs(leftSpaces - rightSpaces) > 1) {
return false;
}
break;
case Alignment::Right:
if (rightSpaces > 0) {
return false;
}
break;
default:
Q_ASSERT(false);
break;
}
}
// Calculate the width of the text without two spaces around.
int cellWidth = calculateTextWidth(p_row.m_block,
p_cell.m_offset + 2,
p_cell.m_length - 3);
if (!equalWidth(cellWidth, p_targetWidth, m_spaceWidth)) {
return false;
}
return true;
}
void VTable::write()
{
bool changed = false;
QTextCursor cursor = m_editor->textCursorW();
int cursorBlock = -1, cursorPib = -1;
// Write the table row by row.
for (auto & row : m_rows) {
bool needChange = false;
for (const auto & cell : row.m_cells) {
if (!cell.m_formattedText.isEmpty()) {
needChange = true;
break;
}
}
if (!needChange) {
continue;
}
if (!changed) {
changed = true;
cursorBlock = cursor.blockNumber();
cursorPib = cursor.positionInBlock();
cursor.beginEditBlock();
}
// Construct the block text.
QString newBlockText;
int firstOffset = row.m_cells.first().m_offset;
if (firstOffset > 0) {
// Get the prefix text.
QString text = row.m_block.text();
newBlockText = text.left(firstOffset);
}
for (auto & cell : row.m_cells) {
int pos = newBlockText.size();
if (cell.m_formattedText.isEmpty()) {
newBlockText += cell.m_text;
} else {
newBlockText += cell.m_formattedText;
}
if (cell.m_cursorCoreOffset > -1) {
// Cursor in this cell.
cursorPib = pos + cell.m_cursorCoreOffset + 2;
if (cursorPib >= newBlockText.size()) {
cursorPib = newBlockText.size() - 1;
}
}
}
newBlockText += "|";
// Replace the whole block.
cursor.setPosition(row.m_block.position());
cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
cursor.insertText(newBlockText);
}
if (changed) {
qDebug() << "write formatted table with cursor block" << cursorBlock;
cursor.endEditBlock();
m_editor->setTextCursorW(cursor);
// Restore the cursor.
QTextBlock block = m_editor->documentW()->findBlockByNumber(cursorBlock);
if (block.isValid()) {
int pos = block.position() + cursorPib;
QTextCursor cur = m_editor->textCursorW();
cur.setPosition(pos);
m_editor->setTextCursorW(cur);
}
}
}

181
src/vtable.h Normal file
View File

@ -0,0 +1,181 @@
#ifndef VTABLE_H
#define VTABLE_H
#include <QTextBlock>
#include "markdownhighlighterdata.h"
class VEditor;
class VTable
{
public:
struct Cell
{
Cell()
: m_offset(-1),
m_length(0),
m_cursorCoreOffset(-1)
{
}
void clear()
{
m_offset = -1;
m_length = 0;
m_text.clear();
m_formattedText.clear();
m_cursorCoreOffset = -1;
}
// Start offset within block, including the starting border |.
int m_offset;
// Length of this cell, till next border |.
int m_length;
// Text like "| vnote ".
QString m_text;
// Formatted text, such as "| vnote ".
// It is empty if it does not need formatted.
QString m_formattedText;
// If cursor is within this cell, this will not be -1.
int m_cursorCoreOffset;
};
struct Row
{
Row()
{
}
bool isValid() const
{
return m_block.isValid();
}
void clear()
{
m_block = QTextBlock();
m_cells.clear();
}
QString toString() const
{
QString cells;
for (auto & cell : m_cells) {
cells += QString(" (%1, %2 [%3])").arg(cell.m_offset)
.arg(cell.m_length)
.arg(cell.m_text);
}
return QString("row %1 %2").arg(m_block.blockNumber()).arg(cells);
}
QTextBlock m_block;
QVector<Cell> m_cells;
};
enum Alignment
{
None,
Left,
Center,
Right
};
VTable(VEditor *p_editor, const VTableBlock &p_block);
bool isValid() const;
void format();
// Write a formatted table.
void write();
VTable::Row *header() const;
VTable::Row *delimiter() const;
private:
// Used to hold info about a cell when formatting a column.
struct CellInfo
{
CellInfo()
: m_coreOffset(0),
m_coreLength(0),
m_coreWidth(0)
{
}
// The offset of the core content within the cell.
// Will be 0 if it is an empty cell.
int m_coreOffset;
// The length of the core content.
// Will be 0 if it is an empty cell.
int m_coreLength;
// Pixel width of the core content.
int m_coreWidth;
};
void parseFromTableBlock(const VTableBlock &p_block);
void clear();
bool parseOneRow(const QTextBlock &p_block,
const QVector<int> &p_borders,
int &p_borderIdx,
Row &p_row) const;
int calculateColumnCount() const;
// When called with i, the (i - 1) column must have been formatted.
void formatOneColumn(int p_idx, int p_cursorRowIdx, int p_cursorPib);
void fetchCellInfoOfColumn(int p_idx,
QVector<CellInfo> &p_cellsInfo,
int &p_targetWidth) const;
void calculateBasicWidths(const QTextBlock &p_block, int p_borderPos);
int calculateTextWidth(const QTextBlock &p_block, int p_pib, int p_length) const;
bool isHeaderRow(int p_idx) const;
bool isDelimiterRow(int p_idx) const;
// @p_nrSpaces: number of spaces to fill core content.
QString generateFormattedText(const QString &p_core,
int p_nrSpaces = 0,
Alignment p_align = Alignment::Left) const;
VTable::Alignment getColumnAlignment(int p_idx) const;
bool isDelimiterCellWellFormatted(const Cell &p_cell,
const CellInfo &p_info,
int p_targetWidth) const;
bool isCellWellFormatted(const Row &p_row,
const Cell &p_cell,
const CellInfo &p_info,
int p_targetWidth,
VTable::Alignment p_align) const;
VEditor *m_editor;
// Header, delimiter, and body.
QVector<Row> m_rows;
int m_spaceWidth;
int m_minusWidth;
int m_colonWidth;
int m_defaultDelimiterWidth;
static const QString c_defaultDelimiter;
};
#endif // VTABLE_H

54
src/vtablehelper.cpp Normal file
View File

@ -0,0 +1,54 @@
#include "vtablehelper.h"
#include "veditor.h"
#include "vtable.h"
VTableHelper::VTableHelper(VEditor *p_editor, QObject *p_parent)
: QObject(p_parent),
m_editor(p_editor)
{
}
void VTableHelper::updateTableBlocks(const QVector<VTableBlock> &p_blocks)
{
if (m_editor->isReadOnlyW() || !m_editor->isModified()) {
return;
}
int idx = currentCursorTableBlock(p_blocks);
if (idx == -1) {
return;
}
VTable table(m_editor, p_blocks[idx]);
if (!table.isValid()) {
return;
}
table.format();
table.write();
}
int VTableHelper::currentCursorTableBlock(const QVector<VTableBlock> &p_blocks) const
{
// Binary search.
int curPos = m_editor->textCursorW().position();
int first = 0, last = p_blocks.size() - 1;
while (first <= last) {
int mid = (first + last) / 2;
const VTableBlock &block = p_blocks[mid];
if (block.m_startPos <= curPos && block.m_endPos >= curPos) {
return mid;
}
if (block.m_startPos > curPos) {
last = mid - 1;
} else {
first = mid + 1;
}
}
return -1;
}

26
src/vtablehelper.h Normal file
View File

@ -0,0 +1,26 @@
#ifndef VTABLEHELPER_H
#define VTABLEHELPER_H
#include <QObject>
#include "markdownhighlighterdata.h"
class VEditor;
class VTableHelper : public QObject
{
Q_OBJECT
public:
explicit VTableHelper(VEditor *p_editor, QObject *p_parent = nullptr);
public slots:
void updateTableBlocks(const QVector<VTableBlock> &p_blocks);
private:
// Return the block index which contains the cursor.
int currentCursorTableBlock(const QVector<VTableBlock> &p_blocks) const;
VEditor *m_editor;
};
#endif // VTABLEHELPER_H

View File

@ -75,7 +75,7 @@ struct VPreviewedImageInfo
QString toString() const
{
return QString("previewed image (%1): [%2, %3] padding %4 inline %5 (%6,%7) bg(%8)")
return QString("previewed image (%1): [%2, %3) padding %4 inline %5 (%6,%7) bg(%8)")
.arg(m_imageName)
.arg(m_startPos)
.arg(m_endPos)