mirror of
https://gitee.com/vnotex/vnote.git
synced 2025-07-05 05:49:53 +08:00
editor: auto format table
This commit is contained in:
parent
bcb6adef30
commit
70caa4d932
2
.gitignore
vendored
2
.gitignore
vendored
@ -1 +1 @@
|
|||||||
/VNote.pro.user
|
VNote.pro.user
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# VNote
|
# VNote
|
||||||
- [英文 English](./README.md)
|
[英文 English](./README.md)
|
||||||
|
|
||||||
**VNote是一个更懂程序员和Markdown的笔记!**
|
**VNote是一个更懂程序员和Markdown的笔记!**
|
||||||
|
|
||||||
|
@ -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.
|
// Highlight unit with global position and string style name.
|
||||||
struct HLUnitPos
|
struct HLUnitPos
|
||||||
{
|
{
|
||||||
@ -157,6 +191,11 @@ struct VElementRegion
|
|||||||
return m_startPos <= p_pos && m_endPos > p_pos;
|
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
|
bool intersect(int p_start, int p_end) const
|
||||||
{
|
{
|
||||||
return !(p_end <= m_startPos || p_start >= m_endPos);
|
return !(p_end <= m_startPos || p_start >= m_endPos);
|
||||||
|
@ -51,6 +51,8 @@ PegHighlighterResult::PegHighlighterResult(const PegMarkdownHighlighter *p_peg,
|
|||||||
parseMathjaxBlocks(p_peg, p_result);
|
parseMathjaxBlocks(p_peg, p_result);
|
||||||
|
|
||||||
parseHRuleBlocks(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)
|
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 ® = 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)
|
static inline bool isDisplayFormulaRawEnd(const QString &p_text)
|
||||||
{
|
{
|
||||||
QRegExp regex("\\\\end\\{[^{}\\s\\r\\n]+\\}$");
|
QRegExp regex("\\\\end\\{[^{}\\s\\r\\n]+\\}$");
|
||||||
@ -330,8 +392,8 @@ void PegHighlighterResult::parseMathjaxBlocks(const PegMarkdownHighlighter *p_pe
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
int pib = r.m_startPos - block.position();
|
int pib = qMax(r.m_startPos - block.position(), 0);
|
||||||
int length = r.m_endPos - r.m_startPos;
|
int length = qMin(r.m_endPos - block.position() - pib, block.length() - 1);
|
||||||
QString text = block.text().mid(pib, length);
|
QString text = block.text().mid(pib, length);
|
||||||
if (inBlock) {
|
if (inBlock) {
|
||||||
item.m_text = item.m_text + "\n" + text;
|
item.m_text = item.m_text + "\n" + text;
|
||||||
|
@ -88,6 +88,10 @@ public:
|
|||||||
|
|
||||||
QSet<int> m_hruleBlocks;
|
QSet<int> m_hruleBlocks;
|
||||||
|
|
||||||
|
// All table blocks.
|
||||||
|
// Sorted by start position ascendingly.
|
||||||
|
QVector<VTableBlock> m_tableBlocks;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Parse highlight elements for blocks from one parse result.
|
// Parse highlight elements for blocks from one parse result.
|
||||||
static void parseBlocksHighlightOne(QVector<QVector<HLUnit>> &p_blocksHighlights,
|
static void parseBlocksHighlightOne(QVector<QVector<HLUnit>> &p_blocksHighlights,
|
||||||
@ -108,6 +112,10 @@ private:
|
|||||||
void parseHRuleBlocks(const PegMarkdownHighlighter *p_peg,
|
void parseHRuleBlocks(const PegMarkdownHighlighter *p_peg,
|
||||||
const QSharedPointer<PegParseResult> &p_result);
|
const QSharedPointer<PegParseResult> &p_result);
|
||||||
|
|
||||||
|
// Parse table blocks from parse results.
|
||||||
|
void parseTableBlocks(const PegMarkdownHighlighter *p_peg,
|
||||||
|
const QSharedPointer<PegParseResult> &p_result);
|
||||||
|
|
||||||
#if 0
|
#if 0
|
||||||
void parseBlocksElementRegionOne(QHash<int, QVector<VElementRegion>> &p_regs,
|
void parseBlocksElementRegionOne(QHash<int, QVector<VElementRegion>> &p_regs,
|
||||||
const QTextDocument *p_doc,
|
const QTextDocument *p_doc,
|
||||||
|
@ -203,7 +203,7 @@ static bool containSpecialChar(const QString &p_str)
|
|||||||
QChar la = p_str[p_str.size() - 1];
|
QChar la = p_str[p_str.size() - 1];
|
||||||
|
|
||||||
return fi == '#'
|
return fi == '#'
|
||||||
|| la == '`' || la == '$' || la == '*' || la == '_';
|
|| la == '`' || la == '$' || la == '~' || la == '*' || la == '_';
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PegMarkdownHighlighter::preHighlightSingleFormatBlock(const QVector<QVector<HLUnit>> &p_highlights,
|
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 mathjaxBlocksUpdated(p_result->m_mathjaxBlocks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emit tableBlocksUpdated(p_result->m_tableBlocks);
|
||||||
|
|
||||||
emit imageLinksUpdated(p_result->m_imageRegions);
|
emit imageLinksUpdated(p_result->m_imageRegions);
|
||||||
emit headersUpdated(p_result->m_headerRegions);
|
emit headersUpdated(p_result->m_headerRegions);
|
||||||
}
|
}
|
||||||
|
@ -70,6 +70,9 @@ signals:
|
|||||||
// Emitted when Mathjax blocks updated.
|
// Emitted when Mathjax blocks updated.
|
||||||
void mathjaxBlocksUpdated(const QVector<VMathjaxBlock> &p_mathjaxBlocks);
|
void mathjaxBlocksUpdated(const QVector<VMathjaxBlock> &p_mathjaxBlocks);
|
||||||
|
|
||||||
|
// Emitted when table blocks updated.
|
||||||
|
void tableBlocksUpdated(const QVector<VTableBlock> &p_tableBlocks);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void highlightBlock(const QString &p_text) Q_DECL_OVERRIDE;
|
void highlightBlock(const QString &p_text) Q_DECL_OVERRIDE;
|
||||||
|
|
||||||
|
@ -25,30 +25,20 @@ void PegParseResult::parse(QAtomicInt &p_stop, bool p_fast)
|
|||||||
parseDisplayFormulaRegions(p_stop);
|
parseDisplayFormulaRegions(p_stop);
|
||||||
|
|
||||||
parseHRuleRegions(p_stop);
|
parseHRuleRegions(p_stop);
|
||||||
|
|
||||||
|
parseTableRegions(p_stop);
|
||||||
|
|
||||||
|
parseTableHeaderRegions(p_stop);
|
||||||
|
|
||||||
|
parseTableBorderRegions(p_stop);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PegParseResult::parseImageRegions(QAtomicInt &p_stop)
|
void PegParseResult::parseImageRegions(QAtomicInt &p_stop)
|
||||||
{
|
{
|
||||||
// From Qt5.7, the capacity is preserved.
|
parseRegions(p_stop,
|
||||||
m_imageRegions.clear();
|
pmh_IMAGE,
|
||||||
if (isEmpty()) {
|
m_imageRegions,
|
||||||
return;
|
false);
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PegParseResult::parseHeaderRegions(QAtomicInt &p_stop)
|
void PegParseResult::parseHeaderRegions(QAtomicInt &p_stop)
|
||||||
@ -113,64 +103,63 @@ void PegParseResult::parseFencedCodeBlockRegions(QAtomicInt &p_stop)
|
|||||||
|
|
||||||
void PegParseResult::parseInlineEquationRegions(QAtomicInt &p_stop)
|
void PegParseResult::parseInlineEquationRegions(QAtomicInt &p_stop)
|
||||||
{
|
{
|
||||||
m_inlineEquationRegions.clear();
|
parseRegions(p_stop,
|
||||||
if (isEmpty()) {
|
pmh_INLINEEQUATION,
|
||||||
return;
|
m_inlineEquationRegions,
|
||||||
}
|
false);
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PegParseResult::parseDisplayFormulaRegions(QAtomicInt &p_stop)
|
void PegParseResult::parseDisplayFormulaRegions(QAtomicInt &p_stop)
|
||||||
{
|
{
|
||||||
m_displayFormulaRegions.clear();
|
parseRegions(p_stop,
|
||||||
if (isEmpty()) {
|
pmh_DISPLAYFORMULA,
|
||||||
return;
|
m_displayFormulaRegions,
|
||||||
}
|
true);
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PegParseResult::parseHRuleRegions(QAtomicInt &p_stop)
|
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()) {
|
if (isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pmh_element *elem = m_pmhElements[pmh_HRULE];
|
pmh_element *elem = m_pmhElements[p_type];
|
||||||
while (elem != NULL) {
|
while (elem != NULL) {
|
||||||
if (elem->end <= elem->pos) {
|
if (elem->end <= elem->pos) {
|
||||||
elem = elem->next;
|
elem = elem->next;
|
||||||
@ -181,9 +170,13 @@ void PegParseResult::parseHRuleRegions(QAtomicInt &p_stop)
|
|||||||
return;
|
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;
|
elem = elem->next;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (p_sort && p_stop.load() != 1) {
|
||||||
|
std::sort(p_result.begin(), p_result.end());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -114,6 +114,16 @@ struct PegParseResult
|
|||||||
// HRule regions.
|
// HRule regions.
|
||||||
QVector<VElementRegion> m_hruleRegions;
|
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:
|
private:
|
||||||
void parseImageRegions(QAtomicInt &p_stop);
|
void parseImageRegions(QAtomicInt &p_stop);
|
||||||
|
|
||||||
@ -126,6 +136,17 @@ private:
|
|||||||
void parseDisplayFormulaRegions(QAtomicInt &p_stop);
|
void parseDisplayFormulaRegions(QAtomicInt &p_stop);
|
||||||
|
|
||||||
void parseHRuleRegions(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
|
class PegParserWorker : public QThread
|
||||||
|
@ -143,6 +143,7 @@ hr {
|
|||||||
|
|
||||||
table {
|
table {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
margin: 1rem 0.5rem;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,6 +141,7 @@ hr {
|
|||||||
|
|
||||||
table {
|
table {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
margin: 1rem 0.5rem;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,6 +136,7 @@ hr {
|
|||||||
|
|
||||||
table {
|
table {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
margin: 1rem 0.5rem;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,6 +138,7 @@ hr {
|
|||||||
|
|
||||||
table {
|
table {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
margin: 1rem 0.5rem;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
15
src/src.pro
15
src/src.pro
@ -19,6 +19,13 @@ ICON = resources/icons/vnote.icns
|
|||||||
|
|
||||||
TRANSLATIONS += translations/vnote_zh_CN.ts
|
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\
|
SOURCES += main.cpp\
|
||||||
vmainwindow.cpp \
|
vmainwindow.cpp \
|
||||||
vdirectorytree.cpp \
|
vdirectorytree.cpp \
|
||||||
@ -150,7 +157,9 @@ SOURCES += main.cpp\
|
|||||||
utils/vkeyboardlayoutmanager.cpp \
|
utils/vkeyboardlayoutmanager.cpp \
|
||||||
dialog/vkeyboardlayoutmappingdialog.cpp \
|
dialog/vkeyboardlayoutmappingdialog.cpp \
|
||||||
vfilelistwidget.cpp \
|
vfilelistwidget.cpp \
|
||||||
widgets/vcombobox.cpp
|
widgets/vcombobox.cpp \
|
||||||
|
vtablehelper.cpp \
|
||||||
|
vtable.cpp
|
||||||
|
|
||||||
HEADERS += vmainwindow.h \
|
HEADERS += vmainwindow.h \
|
||||||
vdirectorytree.h \
|
vdirectorytree.h \
|
||||||
@ -293,7 +302,9 @@ HEADERS += vmainwindow.h \
|
|||||||
utils/vkeyboardlayoutmanager.h \
|
utils/vkeyboardlayoutmanager.h \
|
||||||
dialog/vkeyboardlayoutmappingdialog.h \
|
dialog/vkeyboardlayoutmappingdialog.h \
|
||||||
vfilelistwidget.h \
|
vfilelistwidget.h \
|
||||||
widgets/vcombobox.h
|
widgets/vcombobox.h \
|
||||||
|
vtablehelper.h \
|
||||||
|
vtable.h
|
||||||
|
|
||||||
RESOURCES += \
|
RESOURCES += \
|
||||||
vnote.qrc \
|
vnote.qrc \
|
||||||
|
@ -213,6 +213,7 @@ public:
|
|||||||
virtual bool findW(const QRegExp &p_exp,
|
virtual bool findW(const QRegExp &p_exp,
|
||||||
QTextDocument::FindFlags p_options = QTextDocument::FindFlags()) = 0;
|
QTextDocument::FindFlags p_options = QTextDocument::FindFlags()) = 0;
|
||||||
|
|
||||||
|
virtual bool isReadOnlyW() const = 0;
|
||||||
virtual void setReadOnlyW(bool p_ro) = 0;
|
virtual void setReadOnlyW(bool p_ro) = 0;
|
||||||
|
|
||||||
virtual QWidget *viewportW() const = 0;
|
virtual QWidget *viewportW() const = 0;
|
||||||
|
@ -820,7 +820,7 @@ void VMainWindow::initHelpMenu()
|
|||||||
docAct->setToolTip(tr("View VNote's documentation"));
|
docAct->setToolTip(tr("View VNote's documentation"));
|
||||||
connect(docAct, &QAction::triggered,
|
connect(docAct, &QAction::triggered,
|
||||||
this, []() {
|
this, []() {
|
||||||
QString url("http://vnote.readthedocs.io");
|
QString url("https://tamlok.github.io/vnote");
|
||||||
QDesktopServices::openUrl(url);
|
QDesktopServices::openUrl(url);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -828,7 +828,7 @@ void VMainWindow::initHelpMenu()
|
|||||||
donateAct->setToolTip(tr("Donate to VNote or view the donate list"));
|
donateAct->setToolTip(tr("Donate to VNote or view the donate list"));
|
||||||
connect(donateAct, &QAction::triggered,
|
connect(donateAct, &QAction::triggered,
|
||||||
this, []() {
|
this, []() {
|
||||||
QString url("https://github.com/tamlok/vnote#donate");
|
QString url("https://tamlok.github.io/vnote/en_us/#!donate.md");
|
||||||
QDesktopServices::openUrl(url);
|
QDesktopServices::openUrl(url);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -32,6 +32,7 @@
|
|||||||
#include "vgraphvizhelper.h"
|
#include "vgraphvizhelper.h"
|
||||||
#include "vmdtab.h"
|
#include "vmdtab.h"
|
||||||
#include "vdownloader.h"
|
#include "vdownloader.h"
|
||||||
|
#include "vtablehelper.h"
|
||||||
|
|
||||||
extern VWebUtils *g_webUtils;
|
extern VWebUtils *g_webUtils;
|
||||||
|
|
||||||
@ -109,6 +110,10 @@ VMdEditor::VMdEditor(VFile *p_file,
|
|||||||
connect(m_previewMgr, &VPreviewManager::requestUpdateImageLinks,
|
connect(m_previewMgr, &VPreviewManager::requestUpdateImageLinks,
|
||||||
m_pegHighlighter, &PegMarkdownHighlighter::updateHighlight);
|
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);
|
m_editOps = new VMdEditOperations(this, m_file);
|
||||||
connect(m_editOps, &VEditOperations::statusMessage,
|
connect(m_editOps, &VEditOperations::statusMessage,
|
||||||
m_object, &VEditorObject::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) {
|
if (regExp.indexIn(text) > -1) {
|
||||||
const QVector<VElementRegion> &imgRegs = m_pegHighlighter->getImageRegions();
|
const QVector<VElementRegion> &imgRegs = m_pegHighlighter->getImageRegions();
|
||||||
for (auto const & reg : imgRegs) {
|
for (auto const & reg : imgRegs) {
|
||||||
if (!reg.contains(pos)) {
|
if (!reg.contains(pos)
|
||||||
|
&& (!reg.contains(pos - 1) || pos != (block.position() + text.size()))) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1608,7 +1614,8 @@ bool VMdEditor::initInPlacePreviewMenu(QAction *p_before,
|
|||||||
int pib = p_pos - p_block.position();
|
int pib = p_pos - p_block.position();
|
||||||
for (auto info : previews) {
|
for (auto info : previews) {
|
||||||
const VPreviewedImageInfo &pii = info->m_imageInfo;
|
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);
|
const QPixmap *img = findImage(pii.m_imageName);
|
||||||
if (img) {
|
if (img) {
|
||||||
image = *img;
|
image = *img;
|
||||||
|
@ -22,6 +22,7 @@ class VDocument;
|
|||||||
class VPreviewManager;
|
class VPreviewManager;
|
||||||
class VCopyTextAsHtmlDialog;
|
class VCopyTextAsHtmlDialog;
|
||||||
class VEditTab;
|
class VEditTab;
|
||||||
|
class VTableHelper;
|
||||||
|
|
||||||
class VMdEditor : public VTextEdit, public VEditor
|
class VMdEditor : public VTextEdit, public VEditor
|
||||||
{
|
{
|
||||||
@ -152,6 +153,11 @@ public:
|
|||||||
return find(p_exp, p_options);
|
return find(p_exp, p_options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isReadOnlyW() const Q_DECL_OVERRIDE
|
||||||
|
{
|
||||||
|
return isReadOnly();
|
||||||
|
}
|
||||||
|
|
||||||
void setReadOnlyW(bool p_ro) Q_DECL_OVERRIDE
|
void setReadOnlyW(bool p_ro) Q_DECL_OVERRIDE
|
||||||
{
|
{
|
||||||
setReadOnly(p_ro);
|
setReadOnly(p_ro);
|
||||||
@ -325,6 +331,8 @@ private:
|
|||||||
|
|
||||||
VPreviewManager *m_previewMgr;
|
VPreviewManager *m_previewMgr;
|
||||||
|
|
||||||
|
VTableHelper *m_tableHelper;
|
||||||
|
|
||||||
// Image links inserted while editing.
|
// Image links inserted while editing.
|
||||||
QVector<ImageLink> m_insertedImages;
|
QVector<ImageLink> m_insertedImages;
|
||||||
|
|
||||||
|
585
src/vtable.cpp
Normal file
585
src/vtable.cpp
Normal 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
181
src/vtable.h
Normal 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
54
src/vtablehelper.cpp
Normal 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
26
src/vtablehelper.h
Normal 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
|
@ -75,7 +75,7 @@ struct VPreviewedImageInfo
|
|||||||
|
|
||||||
QString toString() const
|
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_imageName)
|
||||||
.arg(m_startPos)
|
.arg(m_startPos)
|
||||||
.arg(m_endPos)
|
.arg(m_endPos)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user