Pārlūkot izejas kodu

Smart Table and Mark (#1649)

* support smart table and mark
Le Tan 5 gadi atpakaļ
vecāks
revīzija
75dc7c6f28

+ 1 - 1
libs/vtextedit

@@ -1 +1 @@
-Subproject commit 69bd57656ccac8cf75502506be0c87d80d86e577
+Subproject commit 3e45827ae9a662bdc61da1090f4e51fdff24af85

+ 1 - 0
src/core/editorconfig.h

@@ -43,6 +43,7 @@ namespace vnotex
             TypeLink,
             TypeImage,
             TypeTable,
+            TypeMark,
             Outline,
             RichPaste,
             FindAndReplace,

+ 8 - 0
src/core/global.h

@@ -79,6 +79,14 @@ namespace vnotex
         ForceEnable = 1,
         ForceDisable = 2
     };
+
+    enum class Alignment
+    {
+        None,
+        Left,
+        Center,
+        Right
+    };
 } // ns vnotex
 
 Q_DECLARE_OPERATORS_FOR_FLAGS(vnotex::FindOptions);

+ 22 - 0
src/core/markdowneditorconfig.cpp

@@ -43,11 +43,15 @@ void MarkdownEditorConfig::init(const QJsonObject &p_app, const QJsonObject &p_u
     m_constrainInPlacePreviewWidthEnabled = READBOOL(QStringLiteral("constrain_inplace_preview_width"));
     m_zoomFactorInReadMode = READREAL(QStringLiteral("zoom_factor_in_read_mode"));
     m_fetchImagesInParseAndPaste = READBOOL(QStringLiteral("fetch_images_in_parse_and_paste"));
+
     m_protectFromXss = READBOOL(QStringLiteral("protect_from_xss"));
     m_htmlTagEnabled = READBOOL(QStringLiteral("html_tag"));
     m_autoBreakEnabled = READBOOL(QStringLiteral("auto_break"));
     m_linkifyEnabled = READBOOL(QStringLiteral("linkify"));
     m_indentFirstLineEnabled = READBOOL(QStringLiteral("indent_first_line"));
+
+    m_smartTableEnabled = READBOOL(QStringLiteral("smart_table"));
+    m_smartTableInterval = READINT(QStringLiteral("smart_table_interval"));
 }
 
 QJsonObject MarkdownEditorConfig::toJson() const
@@ -73,6 +77,8 @@ QJsonObject MarkdownEditorConfig::toJson() const
     obj[QStringLiteral("auto_break")] = m_autoBreakEnabled;
     obj[QStringLiteral("linkify")] = m_linkifyEnabled;
     obj[QStringLiteral("indent_first_line")] = m_indentFirstLineEnabled;
+    obj[QStringLiteral("smart_table")] = m_smartTableEnabled;
+    obj[QStringLiteral("smart_table_interval")] = m_smartTableInterval;
     return obj;
 }
 
@@ -319,3 +325,19 @@ void MarkdownEditorConfig::setSectionNumberStyle(SectionNumberStyle p_style)
 {
     updateConfig(m_sectionNumberStyle, p_style, this);
 }
+
+bool MarkdownEditorConfig::getSmartTableEnabled() const
+{
+    return m_smartTableEnabled;
+}
+
+void MarkdownEditorConfig::setSmartTableEnabled(bool p_enabled)
+{
+    updateConfig(m_smartTableEnabled, p_enabled, this);
+}
+
+int MarkdownEditorConfig::getSmartTableInterval() const
+{
+    return m_smartTableInterval;
+}
+

+ 10 - 0
src/core/markdowneditorconfig.h

@@ -95,6 +95,11 @@ namespace vnotex
         bool getIndentFirstLineEnabled() const;
         void setIndentFirstLineEnabled(bool p_enabled);
 
+        bool getSmartTableEnabled() const;
+        void setSmartTableEnabled(bool p_enabled);
+
+        int getSmartTableInterval() const;
+
     private:
         QString sectionNumberModeToString(SectionNumberMode p_mode) const;
         SectionNumberMode stringToSectionNumberMode(const QString &p_str) const;
@@ -154,6 +159,11 @@ namespace vnotex
 
         // Whether indent the first line of a paragraph.
         bool m_indentFirstLineEnabled = false;
+
+        bool m_smartTableEnabled = true;
+
+        // Interval time to do smart table format.
+        int m_smartTableInterval = 2000;
     };
 }
 

+ 1 - 0
src/data/core/core.qrc

@@ -57,6 +57,7 @@
         <file>icons/type_quote_editor.svg</file>
         <file>icons/type_link_editor.svg</file>
         <file>icons/type_image_editor.svg</file>
+        <file>icons/type_mark_editor.svg</file>
         <file>icons/type_table_editor.svg</file>
         <file>icons/add.svg</file>
         <file>icons/clear.svg</file>

+ 1 - 0
src/data/core/icons/type_mark_editor.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1610071686158" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3107" width="512" height="512" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"></style></defs><path d="M650.368 192.64l-287.488 287.445333-30.165333 90.496-44.373334 44.416 120.661334 120.704 44.373333-44.416 90.538667-30.165333 287.445333-287.488-180.992-180.992z m271.530667 150.826667a42.666667 42.666667 0 0 1 0 60.330666l-331.904 331.904-90.453334 30.165334-60.373333 60.330666a42.666667 42.666667 0 0 1-60.330667 0l-181.034666-181.034666a42.666667 42.666667 0 0 1 0-60.330667l60.330666-60.330667 30.165334-90.496 331.904-331.904a42.666667 42.666667 0 0 1 60.330666 0l241.365334 241.365334z m-271.530667-30.165334l60.330667 60.330667-211.2 211.2-60.330667-60.330667 211.2-211.2zM182.741333 720.64l120.661334 120.704-60.330667 60.330667-181.034667-60.330667 120.661334-120.704z" p-id="3108" fill="#000000"></path></svg>

BIN
src/data/core/translations/qtbase_zh_CN.qm


+ 12 - 12
src/data/core/translations/qtbase_zh_CN.ts

@@ -1644,11 +1644,11 @@ Do you want to delete it anyway?</source>
     <name>QGnomeTheme</name>
     <message>
         <source>&amp;OK</source>
-        <translation>定(&amp;O)</translation>
+        <translation>定(&amp;O)</translation>
     </message>
     <message>
         <source>&amp;Save</source>
-        <translation>存(&amp;S)</translation>
+        <translation>存(&amp;S)</translation>
     </message>
     <message>
         <source>&amp;Cancel</source>
@@ -1656,11 +1656,11 @@ Do you want to delete it anyway?</source>
     </message>
     <message>
         <source>&amp;Close</source>
-        <translation>關閉(&amp;C)</translation>
+        <translation>关闭(&amp;C)</translation>
     </message>
     <message>
         <source>Close without Saving</source>
-        <translation>關閉而不儲存</translation>
+        <translation>关闭而不保存</translation>
     </message>
 </context>
 <context>
@@ -2438,11 +2438,11 @@ Do you want to delete it anyway?</source>
     <name>QMessageBox</name>
     <message>
         <source>Show Details...</source>
-        <translation>顯示詳情...</translation>
+        <translation>显示详情...</translation>
     </message>
     <message>
         <source>Hide Details...</source>
-        <translation>隱藏詳情...</translation>
+        <translation>隐藏详情...</translation>
     </message>
     <message>
         <source>&lt;h3&gt;About Qt&lt;/h3&gt;&lt;p&gt;This program uses Qt version %1.&lt;/p&gt;</source>
@@ -2455,7 +2455,7 @@ Do you want to delete it anyway?</source>
     </message>
     <message>
         <source>About Qt</source>
-        <translation>關於 Qt</translation>
+        <translation>关于 Qt</translation>
     </message>
     <message>
         <source>&lt;p&gt;Qt is a C++ toolkit for cross-platform application development.&lt;/p&gt;&lt;p&gt;Qt provides single-source portability across all major desktop operating systems. It is also available for embedded Linux and other embedded and mobile operating systems.&lt;/p&gt;&lt;p&gt;Qt is available under three different licensing options designed to accommodate the needs of our various users.&lt;/p&gt;&lt;p&gt;Qt licensed under our commercial license agreement is appropriate for development of proprietary/commercial software where you do not want to share any source code with third parties or otherwise cannot comply with the terms of the GNU LGPL version 3.&lt;/p&gt;&lt;p&gt;Qt licensed under the GNU LGPL version 3 is appropriate for the development of Qt&amp;nbsp;applications provided you can comply with the terms and conditions of the GNU LGPL version 3.&lt;/p&gt;&lt;p&gt;Please see &lt;a href=&quot;http://%2/&quot;&gt;%2&lt;/a&gt; for an overview of Qt licensing.&lt;/p&gt;&lt;p&gt;Copyright (C) %1 The Qt Company Ltd and other contributors.&lt;/p&gt;&lt;p&gt;Qt and the Qt logo are trademarks of The Qt Company Ltd.&lt;/p&gt;&lt;p&gt;Qt is The Qt Company Ltd product developed as an open source project. See &lt;a href=&quot;http://%3/&quot;&gt;%3&lt;/a&gt; for more information.&lt;/p&gt;</source>
@@ -3540,15 +3540,15 @@ Do you want to delete it anyway?</source>
     <name>QPlatformTheme</name>
     <message>
         <source>OK</source>
-        <translation>定</translation>
+        <translation>定</translation>
     </message>
     <message>
         <source>Save</source>
-        <translation>存</translation>
+        <translation>存</translation>
     </message>
     <message>
         <source>Save All</source>
-        <translation>全部存</translation>
+        <translation>全部存</translation>
     </message>
     <message>
         <source>Open</source>
@@ -3584,7 +3584,7 @@ Do you want to delete it anyway?</source>
     </message>
     <message>
         <source>Close</source>
-        <translation>關閉</translation>
+        <translation>关闭</translation>
     </message>
     <message>
         <source>Cancel</source>
@@ -3592,7 +3592,7 @@ Do you want to delete it anyway?</source>
     </message>
     <message>
         <source>Discard</source>
-        <translation>丟棄</translation>
+        <translation>丢弃</translation>
     </message>
     <message>
         <source>Help</source>

+ 6 - 1
src/data/core/vnotex.json

@@ -56,6 +56,7 @@
                 "TypeMath" : "Ctrl+,",
                 "TypeMathBlock" : "Ctrl+.",
                 "TypeTable" : "Ctrl+/",
+                "TypeMark" : "Ctrl+G, M",
                 "Outline" : "Ctrl+G, O",
                 "RichPaste" : "Ctrl+Shift+V",
                 "FindAndReplace" : "Ctrl+F",
@@ -250,7 +251,11 @@
             "//comment" : "Whether convert URL-like text to links",
             "linkify" : true,
             "//comment" : "Whether add indentation to the first line of paragraph",
-            "indent_first_line" : false
+            "indent_first_line" : false,
+            "//comment" : "Whether enable smart table (formation)",
+            "smart_table" : true,
+            "//comment" : "Time interval (milliseconds) to do smart table formation",
+            "smart_table_interval" : 2000
         }
     },
     "widget" : {

+ 2 - 2
src/data/extra/themes/moonlight/text-editor.theme

@@ -185,8 +185,8 @@
             "font-family" : "YaHei Consolas Hybrid, Consolas, Monaco, Andale Mono, Monospace, Courier New"
         },
         "MARK" : {
-            "text-color" : "#ccd1d8",
-            "background-color" : "#551560"
+            "text-color" : "#d7dae0",
+            "background-color" : "#898900"
         },
         "TABLE" : {
             "font-family" : "YaHei Consolas Hybrid, Consolas, Monaco, Andale Mono, Monospace, Courier New"

+ 14 - 0
src/widgets/dialogs/settings/markdowneditorpage.cpp

@@ -72,6 +72,8 @@ void MarkdownEditorPage::loadInternal()
     m_linkifyCheckBox->setChecked(markdownConfig.getLinkifyEnabled());
 
     m_indentFirstLineCheckBox->setChecked(markdownConfig.getIndentFirstLineEnabled());
+
+    m_smartTableCheckBox->setChecked(markdownConfig.getSmartTableEnabled());
 }
 
 void MarkdownEditorPage::saveInternal()
@@ -110,6 +112,8 @@ void MarkdownEditorPage::saveInternal()
 
     markdownConfig.setIndentFirstLineEnabled(m_indentFirstLineCheckBox->isChecked());
 
+    markdownConfig.setSmartTableEnabled(m_smartTableCheckBox->isChecked());
+
     EditorPage::notifyEditorConfigChange();
 }
 
@@ -225,6 +229,16 @@ QGroupBox *MarkdownEditorPage::setupEditGroup()
                 this, &MarkdownEditorPage::pageIsChanged);
     }
 
+    {
+        const QString label(tr("Smart table"));
+        m_smartTableCheckBox = WidgetsFactory::createCheckBox(label, box);
+        m_smartTableCheckBox->setToolTip(tr("Smart table formation"));
+        layout->addRow(m_smartTableCheckBox);
+        addSearchItem(label, m_smartTableCheckBox->toolTip(), m_smartTableCheckBox);
+        connect(m_smartTableCheckBox, &QCheckBox::stateChanged,
+                this, &MarkdownEditorPage::pageIsChanged);
+    }
+
     return box;
 }
 

+ 2 - 0
src/widgets/dialogs/settings/markdowneditorpage.h

@@ -56,6 +56,8 @@ namespace vnotex
         QSpinBox *m_sectionNumberBaseLevelSpinBox = nullptr;
 
         QComboBox *m_sectionNumberStyleComboBox = nullptr;
+
+        QCheckBox *m_smartTableCheckBox = nullptr;
     };
 }
 

+ 91 - 0
src/widgets/dialogs/tableinsertdialog.cpp

@@ -0,0 +1,91 @@
+#include "tableinsertdialog.h"
+
+#include <QSpinBox>
+#include <QRadioButton>
+#include <QGridLayout>
+#include <QHBoxLayout>
+#include <QLabel>
+#include <QButtonGroup>
+
+#include <widgets/widgetsfactory.h>
+
+using namespace vnotex;
+
+TableInsertDialog::TableInsertDialog(const QString &p_title, QWidget *p_parent)
+    : ScrollDialog(p_parent)
+{
+    setupUI(p_title);
+}
+
+void TableInsertDialog::setupUI(const QString &p_title)
+{
+    auto mainWidget = new QWidget(this);
+    setCentralWidget(mainWidget);
+
+    auto mainLayout = new QGridLayout(mainWidget);
+
+    m_rowCountSpinBox = WidgetsFactory::createSpinBox(mainWidget);
+    m_rowCountSpinBox->setToolTip(tr("Row count of the table body"));
+    m_rowCountSpinBox->setMaximum(1000);
+    m_rowCountSpinBox->setMinimum(0);
+
+    mainLayout->addWidget(new QLabel(tr("Row:")), 0, 0, 1, 1);
+    mainLayout->addWidget(m_rowCountSpinBox, 0, 1, 1, 1);
+
+    m_colCountSpinBox = WidgetsFactory::createSpinBox(mainWidget);
+    m_colCountSpinBox->setToolTip(tr("Column count of the table"));
+    m_colCountSpinBox->setMaximum(1000);
+    m_colCountSpinBox->setMinimum(1);
+
+    mainLayout->addWidget(new QLabel(tr("Column:")), 0, 2, 1, 1);
+    mainLayout->addWidget(m_colCountSpinBox, 0, 3, 1, 1);
+
+    {
+        auto noneBtn = new QRadioButton(tr("None"), mainWidget);
+        auto leftBtn = new QRadioButton(tr("Left"), mainWidget);
+        auto centerBtn = new QRadioButton(tr("Center"), mainWidget);
+        auto rightBtn = new QRadioButton(tr("Right"), mainWidget);
+
+        auto alignLayout = new QHBoxLayout();
+        alignLayout->addWidget(noneBtn);
+        alignLayout->addWidget(leftBtn);
+        alignLayout->addWidget(centerBtn);
+        alignLayout->addWidget(rightBtn);
+        alignLayout->addStretch();
+
+        mainLayout->addWidget(new QLabel(tr("Alignment:")), 1, 0, 1, 1);
+        mainLayout->addLayout(alignLayout, 1, 1, 1, 3);
+
+        auto buttonGroup = new QButtonGroup(mainWidget);
+        buttonGroup->addButton(noneBtn, static_cast<int>(Alignment::None));
+        buttonGroup->addButton(leftBtn, static_cast<int>(Alignment::Left));
+        buttonGroup->addButton(centerBtn, static_cast<int>(Alignment::Center));
+        buttonGroup->addButton(rightBtn, static_cast<int>(Alignment::Right));
+
+        noneBtn->setChecked(true);
+        connect(buttonGroup, static_cast<void(QButtonGroup::*)(int, bool)>(&QButtonGroup::buttonToggled),
+                this, [this](int p_id, bool p_checked){
+                    if (p_checked) {
+                        m_alignment = static_cast<Alignment>(p_id);
+                    }
+                });
+    }
+
+    setDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+    setWindowTitle(p_title);
+}
+
+int TableInsertDialog::getRowCount() const
+{
+    return m_rowCountSpinBox->value();
+}
+
+int TableInsertDialog::getColumnCount() const
+{
+    return m_colCountSpinBox->value();
+}
+
+Alignment TableInsertDialog::getAlignment() const
+{
+    return m_alignment;
+}

+ 35 - 0
src/widgets/dialogs/tableinsertdialog.h

@@ -0,0 +1,35 @@
+#ifndef TABLEINSERTDIALOG_H
+#define TABLEINSERTDIALOG_H
+
+#include "scrolldialog.h"
+
+#include <core/global.h>
+
+class QSpinBox;
+
+namespace vnotex
+{
+    class TableInsertDialog : public ScrollDialog
+    {
+        Q_OBJECT
+    public:
+        TableInsertDialog(const QString &p_title, QWidget *p_parent = nullptr);
+
+        int getRowCount() const;
+
+        int getColumnCount() const;
+
+        Alignment getAlignment() const;
+
+    private:
+        void setupUI(const QString &p_title);
+
+        QSpinBox *m_rowCountSpinBox = nullptr;
+
+        QSpinBox *m_colCountSpinBox = nullptr;
+
+        Alignment m_alignment = Alignment::None;
+    };
+}
+
+#endif // TABLEINSERTDIALOG_H

+ 52 - 0
src/widgets/editors/markdowneditor.cpp

@@ -24,6 +24,7 @@
 
 #include <widgets/dialogs/linkinsertdialog.h>
 #include <widgets/dialogs/imageinsertdialog.h>
+#include <widgets/dialogs/tableinsertdialog.h>
 #include <widgets/messageboxhelper.h>
 
 #include <widgets/dialogs/selectdialog.h>
@@ -43,6 +44,7 @@
 
 #include "previewhelper.h"
 #include "../outlineprovider.h"
+#include "markdowntablehelper.h"
 
 using namespace vnotex;
 
@@ -78,6 +80,8 @@ MarkdownEditor::MarkdownEditor(const MarkdownEditorConfig &p_config,
     connect(getHighlighter(), &vte::PegMarkdownHighlighter::headersUpdated,
             this, &MarkdownEditor::updateHeadings);
 
+    setupTableHelper();
+
     m_headingTimer = new QTimer(this);
     m_headingTimer->setInterval(500);
     m_headingTimer->setSingleShot(true);
@@ -143,6 +147,12 @@ void MarkdownEditor::typeStrikethrough()
     vte::MarkdownUtils::typeStrikethrough(m_textEdit);
 }
 
+void MarkdownEditor::typeMark()
+{
+    enterInsertModeIfApplicable();
+    vte::MarkdownUtils::typeMark(m_textEdit);
+}
+
 void MarkdownEditor::typeUnorderedList()
 {
     enterInsertModeIfApplicable();
@@ -293,6 +303,41 @@ void MarkdownEditor::typeImage()
     }
 }
 
+void MarkdownEditor::typeTable()
+{
+    TableInsertDialog dialog(tr("Insert Table"), this);
+    if (dialog.exec() != QDialog::Accepted) {
+        return;
+    }
+
+    auto cursor = m_textEdit->textCursor();
+    cursor.beginEditBlock();
+    if (cursor.hasSelection()) {
+        cursor.setPosition(qMax(cursor.selectionStart(), cursor.selectionEnd()));
+    }
+
+    bool newBlock = !cursor.atBlockEnd();
+    if (!newBlock && !cursor.atBlockStart()) {
+        QString text = cursor.block().text().trimmed();
+        if (!text.isEmpty() && text != QStringLiteral(">")) {
+            // Insert a new block before inserting table.
+            newBlock = true;
+        }
+    }
+
+    if (newBlock) {
+        auto indentationStr = vte::TextEditUtils::fetchIndentationSpaces(cursor.block());
+        vte::TextEditUtils::insertBlock(cursor, false);
+        cursor.insertText(indentationStr);
+    }
+
+    cursor.endEditBlock();
+    m_textEdit->setTextCursor(cursor);
+
+    // Insert table.
+    m_tableHelper->insertTable(dialog.getRowCount(), dialog.getColumnCount(), dialog.getAlignment());
+}
+
 void MarkdownEditor::setBuffer(Buffer *p_buffer)
 {
     m_buffer = p_buffer;
@@ -1214,3 +1259,10 @@ void MarkdownEditor::updateFromConfig(bool p_initialized)
         getHighlighter()->updateHighlight();
     }
 }
+
+void MarkdownEditor::setupTableHelper()
+{
+    m_tableHelper = new MarkdownTableHelper(this, this);
+    connect(getHighlighter(), &vte::PegMarkdownHighlighter::tableBlocksUpdated,
+            m_tableHelper, &MarkdownTableHelper::updateTableBlocks);
+}

+ 10 - 0
src/widgets/editors/markdowneditor.h

@@ -22,6 +22,7 @@ namespace vnotex
     class PreviewHelper;
     class Buffer;
     class MarkdownEditorConfig;
+    class MarkdownTableHelper;
 
     class MarkdownEditor : public vte::VMarkdownEditor
     {
@@ -65,6 +66,8 @@ namespace vnotex
 
         void typeStrikethrough();
 
+        void typeMark();
+
         void typeUnorderedList();
 
         void typeOrderedList();
@@ -85,6 +88,8 @@ namespace vnotex
 
         void typeImage();
 
+        void typeTable();
+
         const QVector<MarkdownEditor::Heading> &getHeadings() const;
         int getCurrentHeadingIndex() const;
 
@@ -171,6 +176,8 @@ namespace vnotex
         // Return true if there is change.
         bool updateSectionNumber(const QVector<Heading> &p_headings);
 
+        void setupTableHelper();
+
         static QString generateImageFileNameToInsertAs(const QString &p_title, const QString &p_suffix);
 
         const MarkdownEditorConfig &m_config;
@@ -190,6 +197,9 @@ namespace vnotex
         bool m_sectionNumberEnabled = false;
 
         OverrideState m_overriddenSectionNumber = OverrideState::NoOverride;
+
+        // Managed by QObject.
+        MarkdownTableHelper *m_tableHelper = nullptr;
     };
 }
 

+ 781 - 0
src/widgets/editors/markdowntable.cpp

@@ -0,0 +1,781 @@
+#include "markdowntable.h"
+
+#include <QTextEdit>
+#include <QDebug>
+
+using namespace vnotex;
+
+void MarkdownTable::Cell::clear()
+{
+    m_offset = -1;
+    m_length = 0;
+    m_text.clear();
+    m_formattedText.clear();
+    m_cursorCoreOffset = -1;
+    m_deleted = false;
+}
+
+bool MarkdownTable::Row::isValid() const
+{
+    return !m_cells.isEmpty();
+}
+
+void MarkdownTable::Row::clear()
+{
+    m_block = QTextBlock();
+    m_preText.clear();
+    m_cells.clear();
+}
+
+QString MarkdownTable::Row::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);
+}
+
+qreal MarkdownTable::s_spaceWidth = -1;
+
+qreal MarkdownTable::s_minusWidth = -1;
+
+qreal MarkdownTable::s_colonWidth = -1;
+
+qreal MarkdownTable::s_defaultDelimiterWidth = -1;
+
+const QString MarkdownTable::c_defaultDelimiter = "---";
+
+const QChar MarkdownTable::c_borderChar = '|';
+
+enum
+{
+    HeaderRowIndex = 0,
+    DelimiterRowIndex = 1
+};
+
+MarkdownTable::MarkdownTable(QTextEdit *p_textEdit, const vte::peg::TableBlock &p_block)
+    : m_textEdit(p_textEdit)
+{
+    parseTableBlock(p_block);
+}
+
+MarkdownTable::MarkdownTable(QTextEdit *p_textEdit, int p_bodyRow, int p_col, Alignment p_alignment)
+    : m_textEdit(p_textEdit),
+      m_isNew(true)
+{
+    Q_ASSERT(p_bodyRow >= 0 && p_col > 0);
+    m_rows.resize(p_bodyRow + 2);
+
+    // PreText for each row.
+    QString preText;
+    const QTextCursor cursor = m_textEdit->textCursor();
+    Q_ASSERT(cursor.atBlockEnd());
+    if (!cursor.atBlockStart()) {
+        preText = cursor.block().text();
+    }
+
+    QString delimiterCore(c_defaultDelimiter);
+    switch (p_alignment) {
+    case Alignment::Left:
+        delimiterCore[0] = ':';
+        break;
+
+    case Alignment::Center:
+        delimiterCore[0] = ':';
+        delimiterCore[delimiterCore.size() - 1] = ':';
+        break;
+
+    case Alignment::Right:
+        delimiterCore[delimiterCore.size() - 1] = ':';
+        break;
+
+    default:
+        break;
+    }
+    const QString delimiterCell = generateFormattedText(delimiterCore, 0);
+    const QString contentCell = generateFormattedText(QString(c_defaultDelimiter.size(), QLatin1Char(' ')), 0);
+
+    for (int rowIdx = 0; rowIdx < m_rows.size(); ++rowIdx) {
+        auto &row = m_rows[rowIdx];
+        row.m_preText = preText;
+        row.m_cells.resize(p_col);
+
+        const QString &content = isDelimiterRow(rowIdx) ? delimiterCell : contentCell;
+        for (auto &cell : row.m_cells) {
+            cell.m_text = content;
+        }
+    }
+}
+
+bool MarkdownTable::isValid() const
+{
+    return header() && header()->isValid() && delimiter() && delimiter()->isValid();
+}
+
+void MarkdownTable::format()
+{
+    if (!isValid()) {
+        return;
+    }
+
+    const QTextCursor cursor = m_textEdit->textCursor();
+    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();
+    pruneColumns(nrCols);
+
+    for (int i = 0; i < nrCols; ++i) {
+        formatColumn(i, curRowIdx, curPib);
+    }
+}
+
+void MarkdownTable::write()
+{
+    if (m_isNew) {
+        writeNewTable();
+    } else {
+        writeTable();
+    }
+}
+
+void MarkdownTable::parseTableBlock(const vte::peg::TableBlock &p_block)
+{
+    auto doc = m_textEdit->document();
+
+    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;
+    }
+
+    initWidths(block, borders[0]);
+
+    int borderIdx = 0;
+    m_rows.reserve(numRows);
+    for (int i = 0; i < numRows; ++i) {
+        m_rows.append(Row());
+        if (!parseRow(block, borders, borderIdx, m_rows.last())) {
+            clear();
+            return;
+        }
+
+        qDebug() << "row" << i << m_rows.last().toString();
+
+        block = block.next();
+    }
+}
+
+void MarkdownTable::clear()
+{
+    m_rows.clear();
+}
+
+void MarkdownTable::initWidths(const QTextBlock &p_block, int p_borderPos)
+{
+    if (s_spaceWidth != -1) {
+        return;
+    }
+
+    QFont font = m_textEdit->font();
+    int pib = p_borderPos - p_block.position();
+    auto 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;
+            }
+        }
+    }
+
+    QFontMetricsF fmf(font);
+    s_spaceWidth = fmf.width(' ');
+    s_minusWidth = fmf.width('-');
+    s_colonWidth = fmf.width(':');
+    s_defaultDelimiterWidth = fmf.width(c_defaultDelimiter);
+
+    qDebug() << "smart table widths" << font.family() << s_spaceWidth << s_minusWidth << s_colonWidth << s_defaultDelimiterWidth;
+}
+
+bool MarkdownTable::parseRow(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;
+    }
+
+    // Get pre text.
+    int firstCellOffset = p_borders[p_borderIdx] - startPos;
+    if (text[firstCellOffset] != c_borderChar) {
+        return false;
+    }
+    p_row.m_preText = text.left(firstCellOffset);
+
+    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] != c_borderChar) {
+            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] != c_borderChar) {
+            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;
+}
+
+const MarkdownTable::Row *MarkdownTable::header() const
+{
+    if (m_rows.size() <= HeaderRowIndex) {
+        return nullptr;
+    }
+
+    return &m_rows[HeaderRowIndex];
+}
+
+const MarkdownTable::Row *MarkdownTable::delimiter() const
+{
+    if (m_rows.size() <= DelimiterRowIndex) {
+        return nullptr;
+    }
+
+    return &m_rows[DelimiterRowIndex];
+}
+
+int MarkdownTable::calculateColumnCount() const
+{
+    // We use the width of the header as the width of the table.
+    // With this, we could add or remove one column by just changing the header row.
+    return header()->m_cells.size();
+}
+
+void MarkdownTable::pruneColumns(int p_nrCols)
+{
+    for (auto &row : m_rows) {
+        for (int i = p_nrCols; i < row.m_cells.size(); ++i) {
+            row.m_cells[i].m_deleted = true;
+        }
+    }
+}
+
+void MarkdownTable::formatColumn(int p_idx, int p_cursorRowIdx, int p_cursorPib)
+{
+    QVector<CellInfo> cells;
+    // Target width of this column.
+    qreal targetWidth = 0;
+    fetchCellInfoOfColumn(p_idx, p_cursorRowIdx, p_cursorPib, cells, targetWidth);
+
+    // Get the alignment of this column.
+    const auto align = getColumnAlignment(p_idx);
+
+    // Calculate the formatted text of each cell.
+    for (int rowIdx = 0; rowIdx < cells.size(); ++rowIdx) {
+        const 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;
+                int delta = s_minusWidth - 1;
+                switch (align) {
+                case Alignment::None:
+                {
+                    int coreLength = static_cast<int>((targetWidth + delta) / s_minusWidth);
+                    core = QString(coreLength, '-');
+                    break;
+                }
+
+                case Alignment::Left:
+                {
+                    int coreLength = static_cast<int>((targetWidth - s_colonWidth + delta) / s_minusWidth);
+                    core = QStringLiteral(":");
+                    core += QString(coreLength, '-');
+                    break;
+                }
+
+                case Alignment::Center:
+                {
+                    int coreLength = static_cast<int>((targetWidth - 2 * s_colonWidth + delta) / s_minusWidth);
+                    core = QStringLiteral(":");
+                    core += QString(coreLength, '-');
+                    core += QStringLiteral(":");
+                    break;
+                }
+
+                case Alignment::Right:
+                {
+                    int coreLength = static_cast<int>((targetWidth - s_colonWidth + delta) / s_minusWidth);
+                    core = QString(coreLength, '-');
+                    core += QStringLiteral(":");
+                    break;
+                }
+
+                default:
+                    Q_ASSERT(false);
+                    break;
+                }
+
+                cell.m_formattedText = generateFormattedText(core, 0);
+                if (cell.m_text == cell.m_formattedText) {
+                    // Avoid infinite change.
+                    cell.m_formattedText.clear();
+                }
+            }
+        } 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 = static_cast<int>((targetWidth - info.m_coreWidth + s_spaceWidth - 1) / s_spaceWidth);
+                cell.m_formattedText = generateFormattedText(core, nr, fakeAlign);
+
+                // For cells crossing lines and having spaces at the end of one line,
+                // Qt will collapse those spaces, which make it not well formatted.
+                if (cell.m_text == cell.m_formattedText) {
+                    cell.m_formattedText.clear();
+                }
+            }
+        }
+    }
+}
+
+void MarkdownTable::fetchCellInfoOfColumn(int p_idx,
+                                          int p_cursorRowIdx,
+                                          int p_cursorPib,
+                                          QVector<CellInfo> &p_cellsInfo,
+                                          qreal &p_targetWidth) const
+{
+    p_targetWidth = s_defaultDelimiterWidth;
+    p_cellsInfo.resize(m_rows.size());
+
+    // Fetch the trimmed core content and its width.
+    for (int i = 0; i < m_rows.size(); ++i) {
+        const 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 = fetchCoreOffset(cell.m_text);
+        if (first == -1) {
+            // Empty cell.
+            continue;
+        }
+        info.m_coreOffset = first;
+
+        // If the cursor is in this cell, then we should treat the core length at least not
+        // less than the cursor position even if there is trailing spaces before the cursor.
+        int last = cell.m_length - 1;
+        for (; last >= first; --last) {
+            if ((p_cursorRowIdx == i && p_cursorPib - cell.m_offset - 1 == last) || 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;
+        }
+    }
+}
+
+bool MarkdownTable::isDelimiterRow(int p_idx) const
+{
+    return p_idx == DelimiterRowIndex;
+}
+
+qreal MarkdownTable::calculateTextWidth(const QTextBlock &p_block, int p_pib, int p_length) const
+{
+    // The block may cross multiple lines.
+    qreal textWidth = 0;
+    QTextLayout *layout = p_block.layout();
+    QTextLine line = layout->lineForTextPosition(p_pib);
+    while (line.isValid()) {
+        int lineEnd = line.textStart() + line.textLength();
+        if (lineEnd >= p_pib + p_length) {
+            // The last line.
+            textWidth += line.cursorToX(p_pib + p_length) - line.cursorToX(p_pib);
+            break;
+        } else {
+            // Cross lines.
+            textWidth += line.cursorToX(lineEnd) - line.cursorToX(p_pib);
+
+            // Move to next line.
+            p_length = p_length - (lineEnd - p_pib);
+            p_pib = lineEnd;
+            line = layout->lineForTextPosition(p_pib + 1);
+        }
+    }
+
+    return textWidth > 0 ? textWidth : -1;
+}
+
+Alignment MarkdownTable::getColumnAlignment(int p_idx) const
+{
+    auto 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 bool equalWidth(int p_a, int p_b, int p_margin = 5)
+{
+    return qAbs(p_a - p_b) <= p_margin;
+}
+
+bool MarkdownTable::isDelimiterCellWellFormatted(const Cell &p_cell,
+                                                 const CellInfo &p_info,
+                                                 qreal p_targetWidth) const
+{
+    // We could use core width here for delimiter cell.
+    if (!equalWidth(p_info.m_coreWidth, p_targetWidth, s_minusWidth / 2)) {
+        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;
+}
+
+QString MarkdownTable::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%4 ").arg(c_borderChar,
+                                     QString(leftSpaces, ' '),
+                                     p_core,
+                                     QString(rightSpaces, ' '));
+}
+
+bool MarkdownTable::isHeaderRow(int p_idx) const
+{
+    return p_idx == HeaderRowIndex;
+}
+
+bool MarkdownTable::isCellWellFormatted(const Row &p_row,
+                                        const Cell &p_cell,
+                                        const CellInfo &p_info,
+                                        int p_targetWidth,
+                                        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, s_spaceWidth / 2)) {
+        return false;
+    }
+
+    return true;
+}
+
+void MarkdownTable::writeTable()
+{
+    bool changed = false;
+    // Use cursor(QTextDocument) to handle the corner case when cursor locates at the end of one row.
+    QTextCursor cursor(m_textEdit->document());
+    int cursorBlock = -1, cursorPib = -1;
+    bool cursorHit = false;
+
+    // Write the table row by row.
+    for (const auto &row : m_rows) {
+        bool needChange = false;
+        for (const auto &cell : row.m_cells) {
+            if (!cell.m_formattedText.isEmpty() || cell.m_deleted) {
+                needChange = true;
+                break;
+            }
+        }
+
+        if (!needChange) {
+            continue;
+        }
+
+        if (!changed) {
+            changed = true;
+            const QTextCursor curCursor = m_textEdit->textCursor();
+            cursorBlock = curCursor.blockNumber();
+            cursorPib = curCursor.positionInBlock();
+            cursor.beginEditBlock();
+        }
+
+        // Construct the block text.
+        QString newBlockText(row.m_preText);
+        for (const auto &cell : row.m_cells) {
+            if (cell.m_deleted) {
+                continue;
+            }
+
+            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.
+                cursorHit = true;
+                // We need to calculate the new core offset of this cell.
+                // For delimiter row, this way won't work, but that is fine.
+                int coreOffset = fetchCoreOffset(cell.m_formattedText.isEmpty() ? cell.m_text : cell.m_formattedText);
+                cursorPib = pos + cell.m_cursorCoreOffset + coreOffset;
+                if (cursorPib >= newBlockText.size()) {
+                    cursorPib = newBlockText.size() - 1;
+                }
+            }
+        }
+
+        newBlockText += c_borderChar;
+
+        // 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();
+
+        // Restore the cursor.
+        if (cursorHit) {
+            QTextBlock block = m_textEdit->document()->findBlockByNumber(cursorBlock);
+            if (block.isValid()) {
+                int pos = block.position() + cursorPib;
+                auto curCursor = m_textEdit->textCursor();
+                curCursor.setPosition(pos);
+                m_textEdit->setTextCursor(curCursor);
+            }
+        }
+    }
+}
+
+void MarkdownTable::writeNewTable()
+{
+    // Generate the text of the whole table.
+    QString tableText;
+    for (int rowIdx = 0; rowIdx < m_rows.size(); ++rowIdx) {
+        const auto &row = m_rows[rowIdx];
+        tableText += row.m_preText;
+        for (const auto &cell : row.m_cells) {
+            if (cell.m_deleted) {
+                continue;
+            }
+
+            tableText += cell.m_text;
+        }
+
+        tableText += c_borderChar;
+
+        if (rowIdx < m_rows.size() - 1) {
+            tableText += '\n';
+        }
+    }
+
+    QTextCursor cursor = m_textEdit->textCursor();
+    int pos = cursor.position() + 2;
+    cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor);
+    cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
+    cursor.insertText(tableText);
+    cursor.setPosition(pos);
+    m_textEdit->setTextCursor(cursor);
+}
+
+int MarkdownTable::fetchCoreOffset(const QString &p_cellText)
+{
+    // [0] is the border char. To find the offset of the core content.
+    for (int i = 1; i < p_cellText.size(); ++i) {
+        if (p_cellText[i] != ' ') {
+            return i;
+        }
+    }
+
+    return -1;
+}

+ 165 - 0
src/widgets/editors/markdowntable.h

@@ -0,0 +1,165 @@
+#ifndef MARKDOWNTABLE_H
+#define MARKDOWNTABLE_H
+
+#include <QTextBlock>
+#include <QVector>
+
+#include <vtextedit/pegmarkdownhighlighterdata.h>
+
+#include <core/global.h>
+
+class QTextEdit;
+
+namespace vnotex
+{
+    class MarkdownTable
+    {
+    public:
+        MarkdownTable(QTextEdit *p_textEdit, const vte::peg::TableBlock &p_block);
+
+        MarkdownTable(QTextEdit *p_textEdit, int p_bodyRow, int p_col, Alignment p_alignment);
+
+        bool isValid() const;
+
+        void format();
+
+        void write();
+
+    private:
+        struct Cell
+        {
+            void clear();
+
+            // Start offset within block, including the starting border |.
+            int m_offset = -1;
+
+            // Length of this cell, till next border |.
+            int m_length = 0;
+
+            // 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 = -1;
+
+            // Whether this cell need to be deleted.
+            bool m_deleted = false;
+        };
+
+        struct Row
+        {
+            bool isValid() const;
+
+            void clear();
+
+            QString toString() const;
+
+            QTextBlock m_block;
+
+            // Text before (the first cell of) table row.
+            QString m_preText;
+
+            QVector<Cell> m_cells;
+        };
+
+        // Used to hold info about a cell when formatting a column.
+        struct CellInfo
+        {
+            // The offset of the core content within the cell.
+            // Will be 0 if it is an empty cell.
+            int m_coreOffset = 0;
+
+            // The length of the core content.
+            // Will be 0 if it is an empty cell.
+            int m_coreLength = 0;
+
+            // Pixel width of the core content.
+            qreal m_coreWidth = 0;
+        };
+
+        void parseTableBlock(const vte::peg::TableBlock &p_block);
+
+        void clear();
+
+        void initWidths(const QTextBlock &p_block, int p_borderPos);
+
+        // Parse one row into @p_row and move @p_borderIdx forward.
+        bool parseRow(const QTextBlock &p_block,
+                      const QVector<int> &p_borders,
+                      int &p_borderIdx,
+                      Row &p_row) const;
+
+        const Row *header() const;
+
+        const Row *delimiter() const;
+
+        int calculateColumnCount() const;
+
+        // Prune columns beyond the header row that should be deleted.
+        void pruneColumns(int p_nrCols);
+
+        void formatColumn(int p_idx, int p_cursorRowIdx, int p_cursorPib);
+
+        void fetchCellInfoOfColumn(int p_idx,
+                                   int p_cursorRowIdx,
+                                   int p_cursorPib,
+                                   QVector<CellInfo> &p_cellsInfo,
+                                   qreal &p_targetWidth) const;
+
+        bool isHeaderRow(int p_idx) const;
+
+        bool isDelimiterRow(int p_idx) const;
+
+        qreal calculateTextWidth(const QTextBlock &p_block, int p_pib, int p_length) const;
+
+        Alignment getColumnAlignment(int p_idx) const;
+
+        bool isDelimiterCellWellFormatted(const Cell &p_cell,
+                                          const CellInfo &p_info,
+                                          qreal p_targetWidth) const;
+
+        // @p_nrSpaces: number of spaces to fill core content.
+        QString generateFormattedText(const QString &p_core,
+                                      int p_nrSpaces,
+                                      Alignment p_align = Alignment::Left) const;
+
+        bool isCellWellFormatted(const Row &p_row,
+                                 const Cell &p_cell,
+                                 const CellInfo &p_info,
+                                 int p_targetWidth,
+                                 Alignment p_align) const;
+
+        void writeTable();
+
+        void writeNewTable();
+
+        // Return -1 if it is an empty cell.
+        static int fetchCoreOffset(const QString &p_cellText);
+
+        QTextEdit *m_textEdit = nullptr;
+
+        // Whether this table is a new table or not.
+        bool m_isNew = false;
+
+        // Header, delimiter, and body.
+        QVector<Row> m_rows;
+
+        static qreal s_spaceWidth;
+
+        static qreal s_minusWidth;
+
+        static qreal s_colonWidth;
+
+        static qreal s_defaultDelimiterWidth;
+
+        static const QString c_defaultDelimiter;
+
+        static const QChar c_borderChar;
+    };
+}
+
+#endif // MARKDOWNTABLE_H

+ 113 - 0
src/widgets/editors/markdowntablehelper.cpp

@@ -0,0 +1,113 @@
+#include "markdowntablehelper.h"
+
+#include <QTimer>
+#include <QTextEdit>
+#include <QTextCursor>
+
+#include <vtextedit/vtexteditor.h>
+#include <vtextedit/vtextedit.h>
+
+#include <core/configmgr.h>
+#include <core/markdowneditorconfig.h>
+#include <core/editorconfig.h>
+
+using namespace vnotex;
+
+MarkdownTableHelper::MarkdownTableHelper(vte::VTextEditor *p_editor, QObject *p_parent)
+    : QObject(p_parent),
+      m_editor(p_editor)
+{
+}
+
+bool MarkdownTableHelper::isSmartTableEnabled() const
+{
+    return ConfigMgr::getInst().getEditorConfig().getMarkdownEditorConfig().getSmartTableEnabled();
+}
+
+QTimer *MarkdownTableHelper::getTimer()
+{
+    if (!m_timer) {
+        m_timer = new QTimer(this);
+        m_timer->setSingleShot(true);
+        m_timer->setInterval(ConfigMgr::getInst().getEditorConfig().getMarkdownEditorConfig().getSmartTableInterval());
+        connect(m_timer, &QTimer::timeout,
+                this, &MarkdownTableHelper::formatTable);
+    }
+
+    return m_timer;
+}
+
+void MarkdownTableHelper::formatTable()
+{
+    if (!isSmartTableEnabled()) {
+        return;
+    }
+
+    if (!m_block.isValid()) {
+        return;
+    }
+
+
+    MarkdownTable table(m_editor->getTextEdit(), m_block);
+    if (!table.isValid()) {
+        return;
+    }
+
+    table.format();
+
+    table.write();
+}
+
+void MarkdownTableHelper::updateTableBlocks(const QVector<vte::peg::TableBlock> &p_blocks)
+{
+    if (!isSmartTableEnabled()) {
+        return;
+    }
+
+    getTimer()->stop();
+
+    if (m_editor->isReadOnly() || !m_editor->isModified()) {
+        return;
+    }
+
+    int idx = currentCursorTableBlock(p_blocks);
+    if (idx == -1) {
+        return;
+    }
+
+    m_block = p_blocks[idx];
+    getTimer()->start();
+}
+
+int MarkdownTableHelper::currentCursorTableBlock(const QVector<vte::peg::TableBlock> &p_blocks) const
+{
+    // Binary search.
+    int curPos = m_editor->getTextEdit()->textCursor().position();
+
+    int first = 0, last = p_blocks.size() - 1;
+    while (first <= last) {
+        int mid = (first + last) / 2;
+        const auto &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;
+}
+
+void MarkdownTableHelper::insertTable(int p_bodyRow, int p_col, Alignment p_alignment)
+{
+    MarkdownTable table(m_editor->getTextEdit(), p_bodyRow, p_col, p_alignment);
+    if (!table.isValid()) {
+        return;
+    }
+
+    table.write();
+}

+ 47 - 0
src/widgets/editors/markdowntablehelper.h

@@ -0,0 +1,47 @@
+#ifndef MARKDOWNTABLEHELPER_H
+#define MARKDOWNTABLEHELPER_H
+
+#include <QObject>
+
+#include "markdowntable.h"
+
+class QTimer;
+
+namespace vte
+{
+    class VTextEditor;
+}
+
+namespace vnotex
+{
+    class MarkdownTableHelper : public QObject
+    {
+        Q_OBJECT
+    public:
+        MarkdownTableHelper(vte::VTextEditor *p_editor, QObject *p_parent = nullptr);
+
+        void insertTable(int p_bodyRow, int p_col, Alignment p_alignment);
+
+    public slots:
+        void updateTableBlocks(const QVector<vte::peg::TableBlock> &p_blocks);
+
+    private:
+        // Return the block index which contains the cursor.
+        int currentCursorTableBlock(const QVector<vte::peg::TableBlock> &p_blocks) const;
+
+        void formatTable();
+
+        bool isSmartTableEnabled() const;
+
+        QTimer *getTimer();
+
+        vte::VTextEditor *m_editor = nullptr;
+
+        // Use getTimer() to access.
+        QTimer *m_timer = nullptr;
+
+        vte::peg::TableBlock m_block;
+    };
+}
+
+#endif // MARKDOWNTABLEHELPER_H

+ 9 - 0
src/widgets/markdownviewwindow.cpp

@@ -274,6 +274,7 @@ void MarkdownViewWindow::setupToolBar()
     addAction(toolBar, ViewWindowToolBarHelper::TypeBold);
     addAction(toolBar, ViewWindowToolBarHelper::TypeItalic);
     addAction(toolBar, ViewWindowToolBarHelper::TypeStrikethrough);
+    addAction(toolBar, ViewWindowToolBarHelper::TypeMark);
     addAction(toolBar, ViewWindowToolBarHelper::TypeUnorderedList);
     addAction(toolBar, ViewWindowToolBarHelper::TypeOrderedList);
     addAction(toolBar, ViewWindowToolBarHelper::TypeTodoList);
@@ -630,6 +631,10 @@ void MarkdownViewWindow::handleTypeAction(TypeAction p_action)
         m_editor->typeStrikethrough();
         break;
 
+    case TypeAction::Mark:
+        m_editor->typeMark();
+        break;
+
     case TypeAction::UnorderedList:
         m_editor->typeUnorderedList();
         break;
@@ -674,6 +679,10 @@ void MarkdownViewWindow::handleTypeAction(TypeAction p_action)
         m_editor->typeImage();
         break;
 
+    case TypeAction::Table:
+        m_editor->typeTable();
+        break;
+
     default:
         qWarning() << "TypeAction not handled" << p_action;
         break;

+ 3 - 1
src/widgets/viewwindow.cpp

@@ -360,6 +360,8 @@ QAction *ViewWindow::addAction(QToolBar *p_toolBar, ViewWindowToolBarHelper::Act
     case ViewWindowToolBarHelper::TypeImage:
         Q_FALLTHROUGH();
     case ViewWindowToolBarHelper::TypeTable:
+        Q_FALLTHROUGH();
+    case ViewWindowToolBarHelper::TypeMark:
     {
         act = ViewWindowToolBarHelper::addAction(p_toolBar, p_action);
         connect(this, &ViewWindow::modeChanged,
@@ -616,7 +618,7 @@ void ViewWindow::handleSectionNumberOverride(OverrideState p_state)
 ViewWindow::TypeAction ViewWindow::toolBarActionToTypeAction(ViewWindowToolBarHelper::Action p_action)
 {
     Q_ASSERT(p_action >= ViewWindowToolBarHelper::Action::TypeBold
-             && p_action <= ViewWindowToolBarHelper::Action::TypeTable);
+             && p_action <= ViewWindowToolBarHelper::Action::TypeMax);
     return static_cast<TypeAction>(TypeAction::Bold + (p_action - ViewWindowToolBarHelper::Action::TypeBold));
 }
 

+ 2 - 1
src/widgets/viewwindow.h

@@ -128,7 +128,8 @@ namespace vnotex
             Quote,
             Link,
             Image,
-            TypeTable
+            Table,
+            Mark
         };
 
     protected slots:

+ 6 - 0
src/widgets/viewwindowtoolbarhelper.cpp

@@ -268,6 +268,12 @@ QAction *ViewWindowToolBarHelper::addAction(QToolBar *p_tb, Action p_action)
         addActionShortcut(act, editorConfig.getShortcut(Shortcut::TypeTable), viewWindow);
         break;
 
+    case Action::TypeMark:
+        act = p_tb->addAction(ToolBarHelper::generateIcon("type_mark_editor.svg"),
+                              ViewWindow::tr("Mark"));
+        addActionShortcut(act, editorConfig.getShortcut(Shortcut::TypeMark), viewWindow);
+        break;
+
     case Action::Attachment:
     {
         act = p_tb->addAction(ToolBarHelper::generateIcon("attachment_editor.svg"),

+ 3 - 0
src/widgets/viewwindowtoolbarhelper.h

@@ -36,6 +36,9 @@ namespace vnotex
             TypeLink,
             TypeImage,
             TypeTable,
+            TypeMark,
+            // Ending TypeXXX.
+            TypeMax,
 
             Attachment,
             Outline,

+ 6 - 0
src/widgets/widgets.pri

@@ -22,9 +22,12 @@ SOURCES += \
     $$PWD/dialogs/settings/settingsdialog.cpp \
     $$PWD/dialogs/settings/texteditorpage.cpp \
     $$PWD/dialogs/settings/themepage.cpp \
+    $$PWD/dialogs/tableinsertdialog.cpp \
     $$PWD/dragdropareaindicator.cpp \
     $$PWD/editors/editormarkdownvieweradapter.cpp \
     $$PWD/editors/markdowneditor.cpp \
+    $$PWD/editors/markdowntable.cpp \
+    $$PWD/editors/markdowntablehelper.cpp \
     $$PWD/editors/markdownviewer.cpp \
     $$PWD/editors/markdownvieweradapter.cpp \
     $$PWD/editors/previewhelper.cpp \
@@ -104,9 +107,12 @@ HEADERS += \
     $$PWD/dialogs/settings/settingsdialog.h \
     $$PWD/dialogs/settings/texteditorpage.h \
     $$PWD/dialogs/settings/themepage.h \
+    $$PWD/dialogs/tableinsertdialog.h \
     $$PWD/dragdropareaindicator.h \
     $$PWD/editors/editormarkdownvieweradapter.h \
     $$PWD/editors/markdowneditor.h \
+    $$PWD/editors/markdowntable.h \
+    $$PWD/editors/markdowntablehelper.h \
     $$PWD/editors/markdownviewer.h \
     $$PWD/editors/markdownvieweradapter.h \
     $$PWD/editors/previewhelper.h \