diff --git a/.gitignore b/.gitignore index 901273aa..29c81455 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -/VNote.pro.user +VNote.pro.user diff --git a/README_zh.md b/README_zh.md index 2a143ba4..9514eb66 100644 --- a/README_zh.md +++ b/README_zh.md @@ -1,5 +1,5 @@ # VNote -- [英文 English](./README.md) +[英文 English](./README.md) **VNote是一个更懂程序员和Markdown的笔记!** diff --git a/src/markdownhighlighterdata.h b/src/markdownhighlighterdata.h index 33e21d55..dae95dbf 100644 --- a/src/markdownhighlighterdata.h +++ b/src/markdownhighlighterdata.h @@ -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 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); diff --git a/src/peghighlighterresult.cpp b/src/peghighlighterresult.cpp index c79468a7..b22ebda5 100644 --- a/src/peghighlighterresult.cpp +++ b/src/peghighlighterresult.cpp @@ -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 &p_result) +{ + const QVector &tableRegs = p_result->m_tableRegions; + const QVector &headerRegs = p_result->m_tableHeaderRegions; + const QVector &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) { 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; diff --git a/src/peghighlighterresult.h b/src/peghighlighterresult.h index 672b2229..939c947e 100644 --- a/src/peghighlighterresult.h +++ b/src/peghighlighterresult.h @@ -88,6 +88,10 @@ public: QSet m_hruleBlocks; + // All table blocks. + // Sorted by start position ascendingly. + QVector m_tableBlocks; + private: // Parse highlight elements for blocks from one parse result. static void parseBlocksHighlightOne(QVector> &p_blocksHighlights, @@ -108,6 +112,10 @@ private: void parseHRuleBlocks(const PegMarkdownHighlighter *p_peg, const QSharedPointer &p_result); + // Parse table blocks from parse results. + void parseTableBlocks(const PegMarkdownHighlighter *p_peg, + const QSharedPointer &p_result); + #if 0 void parseBlocksElementRegionOne(QHash> &p_regs, const QTextDocument *p_doc, diff --git a/src/pegmarkdownhighlighter.cpp b/src/pegmarkdownhighlighter.cpp index e90008b4..7bf1004d 100644 --- a/src/pegmarkdownhighlighter.cpp +++ b/src/pegmarkdownhighlighter.cpp @@ -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> &p_highlights, @@ -757,6 +757,8 @@ void PegMarkdownHighlighter::completeHighlight(QSharedPointerm_mathjaxBlocks); } + emit tableBlocksUpdated(p_result->m_tableBlocks); + emit imageLinksUpdated(p_result->m_imageRegions); emit headersUpdated(p_result->m_headerRegions); } diff --git a/src/pegmarkdownhighlighter.h b/src/pegmarkdownhighlighter.h index 65961425..6780937d 100644 --- a/src/pegmarkdownhighlighter.h +++ b/src/pegmarkdownhighlighter.h @@ -70,6 +70,9 @@ signals: // Emitted when Mathjax blocks updated. void mathjaxBlocksUpdated(const QVector &p_mathjaxBlocks); + // Emitted when table blocks updated. + void tableBlocksUpdated(const QVector &p_tableBlocks); + protected: void highlightBlock(const QString &p_text) Q_DECL_OVERRIDE; diff --git a/src/pegparser.cpp b/src/pegparser.cpp index b92858ac..0b93fa08 100644 --- a/src/pegparser.cpp +++ b/src/pegparser.cpp @@ -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 &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()); + } } diff --git a/src/pegparser.h b/src/pegparser.h index 18d9fe18..a5012d7b 100644 --- a/src/pegparser.h +++ b/src/pegparser.h @@ -114,6 +114,16 @@ struct PegParseResult // HRule regions. QVector m_hruleRegions; + // All table regions. + // Sorted by start position. + QVector m_tableRegions; + + // All table header regions. + QVector m_tableHeaderRegions; + + // All table border regions. + QVector 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 &p_result, + bool p_sort = false); }; class PegParserWorker : public QThread diff --git a/src/resources/themes/v_detorte/v_detorte.css b/src/resources/themes/v_detorte/v_detorte.css index 840b14e1..2ef0a337 100644 --- a/src/resources/themes/v_detorte/v_detorte.css +++ b/src/resources/themes/v_detorte/v_detorte.css @@ -143,6 +143,7 @@ hr { table { padding: 0; + margin: 1rem 0.5rem; border-collapse: collapse; } diff --git a/src/resources/themes/v_moonlight/v_moonlight.css b/src/resources/themes/v_moonlight/v_moonlight.css index 5e684c86..8fb4f0a7 100644 --- a/src/resources/themes/v_moonlight/v_moonlight.css +++ b/src/resources/themes/v_moonlight/v_moonlight.css @@ -141,6 +141,7 @@ hr { table { padding: 0; + margin: 1rem 0.5rem; border-collapse: collapse; } diff --git a/src/resources/themes/v_native/v_native.css b/src/resources/themes/v_native/v_native.css index 59fc814b..81e5be68 100644 --- a/src/resources/themes/v_native/v_native.css +++ b/src/resources/themes/v_native/v_native.css @@ -136,6 +136,7 @@ hr { table { padding: 0; + margin: 1rem 0.5rem; border-collapse: collapse; } diff --git a/src/resources/themes/v_pure/v_pure.css b/src/resources/themes/v_pure/v_pure.css index 947e8c36..fbcae58d 100644 --- a/src/resources/themes/v_pure/v_pure.css +++ b/src/resources/themes/v_pure/v_pure.css @@ -138,6 +138,7 @@ hr { table { padding: 0; + margin: 1rem 0.5rem; border-collapse: collapse; } diff --git a/src/src.pro b/src/src.pro index 2c4d8f2d..4a03034a 100644 --- a/src/src.pro +++ b/src/src.pro @@ -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 \ diff --git a/src/veditor.h b/src/veditor.h index 1e2901b5..0c2d5268 100644 --- a/src/veditor.h +++ b/src/veditor.h @@ -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; diff --git a/src/vmainwindow.cpp b/src/vmainwindow.cpp index b83a02e3..d6e2c18e 100644 --- a/src/vmainwindow.cpp +++ b/src/vmainwindow.cpp @@ -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); }); diff --git a/src/vmdeditor.cpp b/src/vmdeditor.cpp index 89814993..b670ad69 100644 --- a/src/vmdeditor.cpp +++ b/src/vmdeditor.cpp @@ -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 &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; diff --git a/src/vmdeditor.h b/src/vmdeditor.h index ceb451d2..9c3cb07d 100644 --- a/src/vmdeditor.h +++ b/src/vmdeditor.h @@ -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 m_insertedImages; diff --git a/src/vtable.cpp b/src/vtable.cpp new file mode 100644 index 00000000..28d09b62 --- /dev/null +++ b/src/vtable.cpp @@ -0,0 +1,585 @@ +#include "vtable.h" + +#include +#include + +#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 &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 &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(&m_rows[HeaderRowIndex]); +} + +VTable::Row *VTable::delimiter() const +{ + if (m_rows.size() <= DelimiterRowIndex) { + return NULL; + } + + return const_cast(&m_rows[DelimiterRowIndex]); +} + +void VTable::formatOneColumn(int p_idx, int p_cursorRowIdx, int p_cursorPib) +{ + QVector 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 &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 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); + } + } +} diff --git a/src/vtable.h b/src/vtable.h new file mode 100644 index 00000000..cd52ac31 --- /dev/null +++ b/src/vtable.h @@ -0,0 +1,181 @@ +#ifndef VTABLE_H +#define VTABLE_H + +#include + +#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 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 &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 &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 m_rows; + + int m_spaceWidth; + int m_minusWidth; + int m_colonWidth; + int m_defaultDelimiterWidth; + + static const QString c_defaultDelimiter; +}; + +#endif // VTABLE_H diff --git a/src/vtablehelper.cpp b/src/vtablehelper.cpp new file mode 100644 index 00000000..75e8ce74 --- /dev/null +++ b/src/vtablehelper.cpp @@ -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 &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 &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; +} diff --git a/src/vtablehelper.h b/src/vtablehelper.h new file mode 100644 index 00000000..b034fd30 --- /dev/null +++ b/src/vtablehelper.h @@ -0,0 +1,26 @@ +#ifndef VTABLEHELPER_H +#define VTABLEHELPER_H + +#include + +#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 &p_blocks); + +private: + // Return the block index which contains the cursor. + int currentCursorTableBlock(const QVector &p_blocks) const; + + VEditor *m_editor; +}; + +#endif // VTABLEHELPER_H diff --git a/src/vtextblockdata.h b/src/vtextblockdata.h index 8043cb4a..34bc86e1 100644 --- a/src/vtextblockdata.h +++ b/src/vtextblockdata.h @@ -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)