Browse Source

refactor TOC logics

Le Tan 8 years ago
parent
commit
97051badf0

+ 0 - 79
src/resources/markdown-it.js

@@ -83,85 +83,6 @@ var markdownToHtml = function(markdown, needToc) {
     }
 };
 
-// Handle wrong title levels, such as '#' followed by '###'
-var toPerfectToc = function(toc) {
-    var i;
-    var curLevel = 1;
-    var perfToc = [];
-    for (i in toc) {
-        var item = toc[i];
-        while (item.level > curLevel + 1) {
-            curLevel += 1;
-            var tmp = { level: curLevel,
-                        anchor: item.anchor,
-                        title: '[EMPTY]'
-                      };
-            perfToc.push(tmp);
-        }
-        perfToc.push(item);
-        curLevel = item.level;
-    }
-    return perfToc;
-};
-
-var itemToHtml = function(item) {
-    return '<a href="#' + item.anchor + '">' + item.title + '</a>';
-};
-
-// Turn a perfect toc to a tree using <ul>
-var tocToTree = function(toc) {
-    var i;
-    var front = '<li>';
-    var ending = ['</li>'];
-    var curLevel = 1;
-    for (i in toc) {
-        var item = toc[i];
-        if (item.level == curLevel) {
-            front += '</li>';
-            front += '<li>';
-            front += itemToHtml(item);
-        } else if (item.level > curLevel) {
-            // assert(item.level - curLevel == 1)
-            front += '<ul>';
-            ending.push('</ul>');
-            front += '<li>';
-            front += itemToHtml(item);
-            ending.push('</li>');
-            curLevel = item.level;
-        } else {
-            while (item.level < curLevel) {
-                var ele = ending.pop();
-                front += ele;
-                if (ele == '</ul>') {
-                    curLevel--;
-                }
-            }
-            front += '</li>';
-            front += '<li>';
-            front += itemToHtml(item);
-        }
-    }
-    while (ending.length > 0) {
-        front += ending.pop();
-    }
-    front = front.replace("<li></li>", "");
-    front = '<ul>' + front + '</ul>';
-    return front;
-};
-
-var handleToc = function(needToc) {
-    var tocTree = tocToTree(toPerfectToc(toc));
-    content.setToc(tocTree);
-
-    // Add it to html
-    if (needToc) {
-        var eles = document.getElementsByClassName('vnote-toc');
-        for (var i = 0; i < eles.length; ++i) {
-            eles[i].innerHTML = tocTree;
-        }
-    }
-};
-
 var updateText = function(text) {
     var needToc = mdHasTocSection(text);
     var html = markdownToHtml(text, needToc);

+ 127 - 4
src/resources/markdown_template.js

@@ -41,11 +41,23 @@ new QWebChannel(qt.webChannelTransport,
         }
     });
 
+var g_muteScroll = false;
+
 var scrollToAnchor = function(anchor) {
+    g_muteScroll = true;
+    if (!anchor) {
+        window.scrollTo(0, 0);
+        g_muteScroll = false;
+        return;
+    }
+
     var anc = document.getElementById(anchor);
     if (anc != null) {
         anc.scrollIntoView();
     }
+
+    // Disable scroll temporarily.
+    setTimeout("g_muteScroll = false", 100);
 };
 
 window.onwheel = function(e) {
@@ -57,13 +69,19 @@ window.onwheel = function(e) {
 }
 
 window.onscroll = function() {
+    if (g_muteScroll) {
+        return;
+    }
+
     var scrollTop = document.documentElement.scrollTop || document.body.scrollTop || window.pageYOffset;
     var eles = document.querySelectorAll("h1, h2, h3, h4, h5, h6");
 
     if (eles.length == 0) {
+        content.setHeader("");
         return;
     }
-    var curIdx = 0;
+
+    var curIdx = -1;
     var biaScrollTop = scrollTop + 50;
     for (var i = 0; i < eles.length; ++i) {
         if (biaScrollTop >= eles[i].offsetTop) {
@@ -73,10 +91,12 @@ window.onscroll = function() {
         }
     }
 
-    var curHeader = eles[curIdx].getAttribute("id");
-    if (curHeader != null) {
-        content.setHeader(curHeader);
+    var curHeader = null;
+    if (curIdx != -1) {
+        curHeader = eles[curIdx].getAttribute("id");
     }
+
+    content.setHeader(curHeader ? curHeader : "");
 };
 
 document.onkeydown = function(e) {
@@ -326,3 +346,106 @@ var escapeHtml = function(text) {
 
   return text.replace(/[&<>"']/g, function(m) { return map[m]; });
 };
+
+// Return the topest level of @toc, starting from 1.
+var baseLevelOfToc = function(p_toc) {
+    var level = -1;
+    for (i in p_toc) {
+        if (level == -1) {
+            level = p_toc[i].level;
+        } else if (level > p_toc[i].level) {
+            level = p_toc[i].level;
+        }
+    }
+
+    if (level == -1) {
+        level = 1;
+    }
+
+    return level;
+};
+
+// Handle wrong title levels, such as '#' followed by '###'
+var toPerfectToc = function(p_toc, p_baseLevel) {
+    var i;
+    var curLevel = p_baseLevel - 1;
+    var perfToc = [];
+    for (i in p_toc) {
+        var item = p_toc[i];
+
+        // Insert empty header.
+        while (item.level > curLevel + 1) {
+            curLevel += 1;
+            var tmp = { level: curLevel,
+                        anchor: '',
+                        title: '[EMPTY]'
+                      };
+            perfToc.push(tmp);
+        }
+
+        perfToc.push(item);
+        curLevel = item.level;
+    }
+
+    return perfToc;
+};
+
+var itemToHtml = function(item) {
+    return '<a href="#' + item.anchor + '">' + item.title + '</a>';
+};
+
+// Turn a perfect toc to a tree using <ul>
+var tocToTree = function(p_toc, p_baseLevel) {
+    var i;
+    var front = '<li>';
+    var ending = ['</li>'];
+    var curLevel = p_baseLevel;
+    for (i in p_toc) {
+        var item = p_toc[i];
+        if (item.level == curLevel) {
+            front += '</li>';
+            front += '<li>';
+            front += itemToHtml(item);
+        } else if (item.level > curLevel) {
+            // assert(item.level - curLevel == 1)
+            front += '<ul>';
+            ending.push('</ul>');
+            front += '<li>';
+            front += itemToHtml(item);
+            ending.push('</li>');
+            curLevel = item.level;
+        } else {
+            while (item.level < curLevel) {
+                var ele = ending.pop();
+                front += ele;
+                if (ele == '</ul>') {
+                    curLevel--;
+                }
+            }
+            front += '</li>';
+            front += '<li>';
+            front += itemToHtml(item);
+        }
+    }
+    while (ending.length > 0) {
+        front += ending.pop();
+    }
+    front = front.replace("<li></li>", "");
+    front = '<ul>' + front + '</ul>';
+    return front;
+};
+
+var handleToc = function(needToc) {
+    var baseLevel = baseLevelOfToc(toc);
+    var tocTree = tocToTree(toPerfectToc(toc, baseLevel), baseLevel);
+    content.setToc(tocTree, baseLevel);
+
+    // Add it to html
+    if (needToc) {
+        var eles = document.getElementsByClassName('vnote-toc');
+        for (var i = 0; i < eles.length; ++i) {
+            eles[i].innerHTML = tocTree;
+        }
+    }
+};
+

+ 0 - 79
src/resources/marked.js

@@ -36,85 +36,6 @@ var markdownToHtml = function(markdown, needToc) {
     }
 };
 
-// Handle wrong title levels, such as '#' followed by '###'
-var toPerfectToc = function(toc) {
-    var i;
-    var curLevel = 1;
-    var perfToc = [];
-    for (i in toc) {
-        var item = toc[i];
-        while (item.level > curLevel + 1) {
-            curLevel += 1;
-            var tmp = { level: curLevel,
-                        anchor: item.anchor,
-                        title: '[EMPTY]'
-                      };
-            perfToc.push(tmp);
-        }
-        perfToc.push(item);
-        curLevel = item.level;
-    }
-    return perfToc;
-};
-
-var itemToHtml = function(item) {
-    return '<a href="#' + item.anchor + '">' + item.title + '</a>';
-};
-
-// Turn a perfect toc to a tree using <ul>
-var tocToTree = function(toc) {
-    var i;
-    var front = '<li>';
-    var ending = ['</li>'];
-    var curLevel = 1;
-    for (i in toc) {
-        var item = toc[i];
-        if (item.level == curLevel) {
-            front += '</li>';
-            front += '<li>';
-            front += itemToHtml(item);
-        } else if (item.level > curLevel) {
-            // assert(item.level - curLevel == 1)
-            front += '<ul>';
-            ending.push('</ul>');
-            front += '<li>';
-            front += itemToHtml(item);
-            ending.push('</li>');
-            curLevel = item.level;
-        } else {
-            while (item.level < curLevel) {
-                var ele = ending.pop();
-                front += ele;
-                if (ele == '</ul>') {
-                    curLevel--;
-                }
-            }
-            front += '</li>';
-            front += '<li>';
-            front += itemToHtml(item);
-        }
-    }
-    while (ending.length > 0) {
-        front += ending.pop();
-    }
-    front = front.replace("<li></li>", "");
-    front = '<ul>' + front + '</ul>';
-    return front;
-};
-
-var handleToc = function(needToc) {
-    var tocTree = tocToTree(toPerfectToc(toc));
-    content.setToc(tocTree);
-
-    // Add it to html
-    if (needToc) {
-        var eles = document.getElementsByClassName('vnote-toc');
-        for (var i = 0; i < eles.length; ++i) {
-            eles[i].innerHTML = tocTree;
-        }
-    }
-};
-
 var mdHasTocSection = function(markdown) {
     var n = markdown.search(/(\n|^)\[toc\]/i);
     return n != -1;

+ 0 - 79
src/resources/showdown.js

@@ -44,85 +44,6 @@ var markdownToHtml = function(markdown, needToc) {
     }
 };
 
-// Handle wrong title levels, such as '#' followed by '###'
-var toPerfectToc = function(toc) {
-    var i;
-    var curLevel = 1;
-    var perfToc = [];
-    for (i in toc) {
-        var item = toc[i];
-        while (item.level > curLevel + 1) {
-            curLevel += 1;
-            var tmp = { level: curLevel,
-                        anchor: item.anchor,
-                        title: '[EMPTY]'
-                      };
-            perfToc.push(tmp);
-        }
-        perfToc.push(item);
-        curLevel = item.level;
-    }
-    return perfToc;
-};
-
-var itemToHtml = function(item) {
-    return '<a href="#' + item.anchor + '">' + item.title + '</a>';
-};
-
-// Turn a perfect toc to a tree using <ul>
-var tocToTree = function(toc) {
-    var i;
-    var front = '<li>';
-    var ending = ['</li>'];
-    var curLevel = 1;
-    for (i in toc) {
-        var item = toc[i];
-        if (item.level == curLevel) {
-            front += '</li>';
-            front += '<li>';
-            front += itemToHtml(item);
-        } else if (item.level > curLevel) {
-            // assert(item.level - curLevel == 1)
-            front += '<ul>';
-            ending.push('</ul>');
-            front += '<li>';
-            front += itemToHtml(item);
-            ending.push('</li>');
-            curLevel = item.level;
-        } else {
-            while (item.level < curLevel) {
-                var ele = ending.pop();
-                front += ele;
-                if (ele == '</ul>') {
-                    curLevel--;
-                }
-            }
-            front += '</li>';
-            front += '<li>';
-            front += itemToHtml(item);
-        }
-    }
-    while (ending.length > 0) {
-        front += ending.pop();
-    }
-    front = front.replace("<li></li>", "");
-    front = '<ul>' + front + '</ul>';
-    return front;
-};
-
-var handleToc = function(needToc) {
-    var tocTree = tocToTree(toPerfectToc(toc));
-    content.setToc(tocTree);
-
-    // Add it to html
-    if (needToc) {
-        var eles = document.getElementsByClassName('vnote-toc');
-        for (var i = 0; i < eles.length; ++i) {
-            eles[i].innerHTML = tocTree;
-        }
-    }
-};
-
 var mdHasTocSection = function(markdown) {
     var n = markdown.search(/(\n|^)\[toc\]/i);
     return n != -1;

+ 2 - 0
src/vconstants.h

@@ -32,4 +32,6 @@ namespace DirConfig
     static const QString c_imageFolder = "image_folder";
     static const QString c_name = "name";
 }
+
+static const QString c_emptyHeaderName = "[EMPTY]";
 #endif

+ 4 - 1
src/vdocument.cpp

@@ -14,7 +14,7 @@ void VDocument::updateText()
     }
 }
 
-void VDocument::setToc(const QString &toc)
+void VDocument::setToc(const QString &toc, int /* baseLevel */)
 {
     if (toc == m_toc) {
         return;
@@ -30,6 +30,8 @@ QString VDocument::getToc()
 
 void VDocument::scrollToAnchor(const QString &anchor)
 {
+    m_header = anchor;
+
     emit requestScrollToAnchor(anchor);
 }
 
@@ -38,6 +40,7 @@ void VDocument::setHeader(const QString &anchor)
     if (anchor == m_header) {
         return;
     }
+
     m_header = anchor;
     emit headerChanged(m_header);
 }

+ 9 - 1
src/vdocument.h

@@ -27,8 +27,16 @@ public:
 
 public slots:
     // Will be called in the HTML side
-    void setToc(const QString &toc);
+
+    // @toc: the HTML of the TOC.
+    // @baseLevel: the base level of @toc, starting from 1. It is the top level
+    // in the @toc.
+    void setToc(const QString &toc, int baseLevel);
+
+    // When the Web view has been scrolled, it will signal current header anchor.
+    // Empty @anchor to indicate an invalid header.
     void setHeader(const QString &anchor);
+
     void setLog(const QString &p_log);
     void keyPressEvent(int p_key, bool p_ctrl, bool p_shift);
     void updateText();

+ 2 - 2
src/vhtmltab.cpp

@@ -195,7 +195,7 @@ void VHtmlTab::discardAndRead()
     readFile();
 }
 
-void VHtmlTab::scrollToAnchor(const VAnchor &p_anchor)
+void VHtmlTab::scrollToAnchor(const VAnchor & /* p_anchor */)
 {
 }
 
@@ -240,7 +240,7 @@ void VHtmlTab::clearSearchedWordHighlight()
     m_editor->clearSearchedWordHighlight();
 }
 
-void VHtmlTab::zoom(bool p_zoomIn, qreal p_step)
+void VHtmlTab::zoom(bool /* p_zoomIn */, qreal /* p_step */)
 {
 }
 

+ 58 - 22
src/vmdedit.cpp

@@ -244,56 +244,87 @@ void VMdEdit::clearUnusedImages()
 
 void VMdEdit::updateCurHeader()
 {
-    int curHeader = 0;
-    QTextCursor cursor(this->textCursor());
-    int curLine = cursor.block().firstLineNumber();
+    if (m_headers.isEmpty()) {
+        return;
+    }
+
+    int curLine = textCursor().block().firstLineNumber();
     int i = 0;
     for (i = m_headers.size() - 1; i >= 0; --i) {
-        if (m_headers[i].lineNumber <= curLine) {
-            curHeader = m_headers[i].lineNumber;
-            break;
+        if (!m_headers[i].isEmpty()) {
+            if (m_headers[i].lineNumber <= curLine) {
+                break;
+            }
         }
     }
-    emit curHeaderChanged(curHeader, i == -1 ? 0 : i);
+
+    if (i == -1) {
+        emit curHeaderChanged(VAnchor(m_file, "", -1, -1));
+        return;
+    }
+
+    V_ASSERT(m_headers[i].index == i);
+
+    emit curHeaderChanged(VAnchor(m_file, "", m_headers[i].lineNumber, m_headers[i].index));
 }
 
 void VMdEdit::generateEditOutline()
 {
     QTextDocument *doc = document();
+
     m_headers.clear();
+
+    QVector<VHeader> headers;
+
     // Assume that each block contains only one line
     // Only support # syntax for now
     QRegExp headerReg("(#{1,6})\\s*(\\S.*)");  // Need to trim the spaces
-    int lastLevel = 0;
+    int baseLevel = -1;
     for (QTextBlock block = doc->begin(); block != doc->end(); block = block.next()) {
-        Q_ASSERT(block.lineCount() == 1);
+        V_ASSERT(block.lineCount() == 1);
         if ((block.userState() == HighlightBlockState::Normal) &&
             headerReg.exactMatch(block.text())) {
             int level = headerReg.cap(1).length();
             VHeader header(level, headerReg.cap(2).trimmed(),
-                           "", block.firstLineNumber());
-            while (level > lastLevel + 1) {
-                // Insert empty level.
-                m_headers.append(VHeader(++lastLevel, "[EMPTY]",
-                                         "", block.firstLineNumber()));
+                           "", block.firstLineNumber(), headers.size());
+            headers.append(header);
+
+            if (baseLevel == -1) {
+                baseLevel = level;
+            } else if (baseLevel > level) {
+                baseLevel = level;
             }
-            m_headers.append(header);
-            lastLevel = level;
         }
     }
 
+    int curLevel = baseLevel - 1;
+    for (auto & item : headers) {
+        while (item.level > curLevel + 1) {
+            curLevel += 1;
+
+            // Insert empty level which is an invalid header.
+            m_headers.append(VHeader(curLevel, c_emptyHeaderName, "", -1, m_headers.size()));
+        }
+
+        item.index = m_headers.size();
+        m_headers.append(item);
+        curLevel = item.level;
+    }
+
     emit headersChanged(m_headers);
+
     updateCurHeader();
 }
 
-void VMdEdit::scrollToHeader(int p_headerIndex)
+void VMdEdit::scrollToHeader(const VAnchor &p_anchor)
 {
-    Q_ASSERT(p_headerIndex >= 0);
-    if (p_headerIndex < m_headers.size()) {
-        int line = m_headers[p_headerIndex].lineNumber;
-        qDebug() << "scroll editor to" << p_headerIndex << "line" << line;
-        scrollToLine(line);
+    if (p_anchor.lineNumber == -1
+        || p_anchor.m_outlineIndex < 0
+        || p_anchor.m_outlineIndex >= m_headers.size()) {
+        return;
     }
+
+    scrollToLine(p_anchor.lineNumber);
 }
 
 QString VMdEdit::toPlainTextWithoutImg() const
@@ -407,3 +438,8 @@ void VMdEdit::resizeEvent(QResizeEvent *p_event)
 
     VEdit::resizeEvent(p_event);
 }
+
+const QVector<VHeader> &VMdEdit::getHeaders() const
+{
+    return m_headers;
+}

+ 11 - 3
src/vmdedit.h

@@ -32,19 +32,27 @@ public:
     // @p_path is the absolute path of the inserted image.
     void imageInserted(const QString &p_path);
 
-    // Scroll to m_headers[p_headerIndex].
-    void scrollToHeader(int p_headerIndex);
+    void scrollToHeader(const VAnchor &p_anchor);
+
     // Like toPlainText(), but remove special blocks containing images.
     QString toPlainTextWithoutImg() const;
 
+    const QVector<VHeader> &getHeaders() const;
+
 signals:
     void headersChanged(const QVector<VHeader> &headers);
-    void curHeaderChanged(int p_lineNumber, int p_outlineIndex);
+
+    // Signal when current header change.
+    void curHeaderChanged(VAnchor p_anchor);
+
     void statusChanged();
 
 private slots:
     void generateEditOutline();
+
+    // When there is no header in current cursor, will signal an invalid header.
     void updateCurHeader();
+
     void handleEditStateChanged(KeyState p_state);
     void handleSelectionChanged();
     void handleClipboardChanged(QClipboard::Mode p_mode);

+ 54 - 28
src/vmdtab.cpp

@@ -50,8 +50,8 @@ void VMdTab::setupUI()
                 this, &VMdTab::updateTocFromHeaders);
         connect(dynamic_cast<VMdEdit *>(m_editor), &VMdEdit::statusChanged,
                 this, &VMdTab::noticeStatusChanged);
-        connect(m_editor, SIGNAL(curHeaderChanged(int, int)),
-                this, SLOT(updateCurHeader(int, int)));
+        connect(m_editor, SIGNAL(curHeaderChanged(VAnchor)),
+                this, SLOT(updateCurHeader(VAnchor)));
         connect(m_editor, &VEdit::textChanged,
                 this, &VMdTab::handleTextChanged);
         connect(m_editor, &VEdit::saveAndRead,
@@ -91,6 +91,7 @@ void VMdTab::showFileReadMode()
     m_isEditMode = false;
 
     int outlineIndex = m_curHeader.m_outlineIndex;
+
     if (m_mdConType == MarkdownConverterType::Hoedown) {
         viewWebByConverter();
     } else {
@@ -100,6 +101,7 @@ void VMdTab::showFileReadMode()
 
     m_stacks->setCurrentWidget(m_webViewer);
     clearSearchedWordHighlight();
+
     scrollWebViewToHeader(outlineIndex);
 
     noticeStatusChanged();
@@ -107,14 +109,20 @@ void VMdTab::showFileReadMode()
 
 void VMdTab::scrollWebViewToHeader(int p_outlineIndex)
 {
-    V_ASSERT(p_outlineIndex >= 0);
+    QString anchor;
 
-    if (p_outlineIndex < m_toc.headers.size()) {
-        QString anchor = m_toc.headers[p_outlineIndex].anchor;
-        if (!anchor.isEmpty()) {
-            m_document->scrollToAnchor(anchor.mid(1));
-        }
+    m_curHeader = VAnchor(m_file, anchor, -1, p_outlineIndex);
+
+    if (p_outlineIndex < m_toc.headers.size() && p_outlineIndex >= 0) {
+        QString tmp = m_toc.headers[p_outlineIndex].anchor;
+        V_ASSERT(!tmp.isEmpty());
+        m_curHeader.anchor = tmp;
+        anchor = tmp.mid(1);
     }
+
+    m_document->scrollToAnchor(anchor);
+
+    emit curHeaderChanged(m_curHeader);
 }
 
 void VMdTab::viewWebByConverter()
@@ -136,14 +144,28 @@ void VMdTab::showFileEditMode()
 
     m_isEditMode = true;
 
+    VMdEdit *mdEdit = dynamic_cast<VMdEdit *>(m_editor);
+    V_ASSERT(mdEdit);
+
     // beginEdit() may change m_curHeader.
     int outlineIndex = m_curHeader.m_outlineIndex;
+    int lineNumber = -1;
+    auto headers = mdEdit->getHeaders();
+    if (outlineIndex < 0 || outlineIndex >= headers.size()) {
+        lineNumber = -1;
+        outlineIndex = -1;
+    } else {
+        lineNumber = headers[outlineIndex].lineNumber;
+    }
+
+    VAnchor anchor(m_file, "", lineNumber, outlineIndex);
 
-    m_editor->beginEdit();
-    m_stacks->setCurrentWidget(m_editor);
+    mdEdit->beginEdit();
+    m_stacks->setCurrentWidget(mdEdit);
 
-    dynamic_cast<VMdEdit *>(m_editor)->scrollToHeader(outlineIndex);
-    m_editor->setFocus();
+    mdEdit->scrollToHeader(anchor);
+
+    mdEdit->setFocus();
 
     noticeStatusChanged();
 }
@@ -368,7 +390,7 @@ static void parseTocLi(QXmlStreamReader &p_xml, QVector<VHeader> &p_headers, int
                     return;
                 }
 
-                VHeader header(p_level, name, anchor, -1);
+                VHeader header(p_level, name, anchor, -1, p_headers.size());
                 p_headers.append(header);
             } else {
                 // Error
@@ -376,7 +398,7 @@ static void parseTocLi(QXmlStreamReader &p_xml, QVector<VHeader> &p_headers, int
             }
         } else if (p_xml.name() == "ul") {
             // Such as header 3 under header 1 directly
-            VHeader header(p_level, "[EMPTY]", "#", -1);
+            VHeader header(p_level, c_emptyHeaderName, "#", -1, p_headers.size());
             p_headers.append(header);
             parseTocUl(p_xml, p_headers, p_level + 1);
         } else {
@@ -478,10 +500,9 @@ void VMdTab::scrollToAnchor(const VAnchor &p_anchor)
     }
 
     m_curHeader = p_anchor;
+
     if (m_isEditMode) {
-        if (p_anchor.lineNumber > -1) {
-            m_editor->scrollToLine(p_anchor.lineNumber);
-        }
+        dynamic_cast<VMdEdit *>(m_editor)->scrollToHeader(p_anchor);
     } else {
         if (!p_anchor.anchor.isEmpty()) {
             m_document->scrollToAnchor(p_anchor.anchor.mid(1));
@@ -500,26 +521,31 @@ void VMdTab::updateCurHeader(const QString &p_anchor)
         const QVector<VHeader> &headers = m_toc.headers;
         for (int i = 0; i < headers.size(); ++i) {
             if (headers[i].anchor == m_curHeader.anchor) {
-                m_curHeader.m_outlineIndex = i;
+                V_ASSERT(headers[i].index == i);
+                m_curHeader.m_outlineIndex = headers[i].index;
                 break;
             }
         }
-
-        emit curHeaderChanged(m_curHeader);
     }
+
+    emit curHeaderChanged(m_curHeader);
 }
 
-void VMdTab::updateCurHeader(int p_lineNumber, int p_outlineIndex)
+void VMdTab::updateCurHeader(VAnchor p_anchor)
 {
-    if (!m_isEditMode || m_curHeader.lineNumber == p_lineNumber) {
-        return;
+    if (m_isEditMode) {
+        if (!p_anchor.anchor.isEmpty() || p_anchor.lineNumber == m_curHeader.lineNumber) {
+            return;
+        }
+    } else {
+        if (p_anchor.lineNumber != -1 || p_anchor.anchor == m_curHeader.anchor) {
+            return;
+        }
     }
 
-    m_curHeader = VAnchor(m_file, "", p_lineNumber);
-    m_curHeader.m_outlineIndex = p_outlineIndex;
-    if (p_lineNumber > -1) {
-        emit curHeaderChanged(m_curHeader);
-    }
+    m_curHeader = p_anchor;
+
+    emit curHeaderChanged(m_curHeader);
 }
 
 void VMdTab::insertImage()

+ 1 - 1
src/vmdtab.h

@@ -76,7 +76,7 @@ private slots:
     void updateCurHeader(const QString &p_anchor);
 
     // Editor requests to update current header.
-    void updateCurHeader(int p_lineNumber, int p_outlineIndex);
+    void updateCurHeader(VAnchor p_anchor);
 
     // Handle key press event in Web view.
     void handleWebKeyPressed(int p_key, bool p_ctrl, bool p_shift);

+ 89 - 29
src/voutline.cpp

@@ -1,7 +1,6 @@
 #include <QDebug>
 #include <QVector>
 #include <QString>
-#include <QJsonObject>
 #include <QKeyEvent>
 #include <QLabel>
 #include <QCoreApplication>
@@ -9,8 +8,10 @@
 #include "vtoc.h"
 #include "utils/vutils.h"
 #include "vnote.h"
+#include "vconfigmanager.h"
 
 extern VNote *g_vnote;
+extern VConfigManager vconfig;
 
 VOutline::VOutline(QWidget *parent)
     : QTreeWidget(parent), VNavigationMode()
@@ -23,23 +24,38 @@ VOutline::VOutline(QWidget *parent)
             this, &VOutline::handleCurItemChanged);
 }
 
+void VOutline::checkOutline(const VToc &p_toc) const
+{
+    const QVector<VHeader> &headers = p_toc.headers;
+
+    for (int i = 0; i < headers.size(); ++i) {
+        V_ASSERT(headers[i].index == i);
+    }
+}
+
 void VOutline::updateOutline(const VToc &toc)
 {
     // Clear current header
     curHeader = VAnchor();
+
+    checkOutline(toc);
+
     outline = toc;
-    updateTreeFromOutline(outline);
+
+    updateTreeFromOutline();
+
     expandTree();
 }
 
-void VOutline::updateTreeFromOutline(const VToc &toc)
+void VOutline::updateTreeFromOutline()
 {
     clear();
 
-    if (!toc.valid) {
+    if (!outline.valid) {
         return;
     }
-    const QVector<VHeader> &headers = toc.headers;
+
+    const QVector<VHeader> &headers = outline.headers;
     int idx = 0;
     updateTreeByLevel(headers, idx, NULL, NULL, 1);
 }
@@ -56,13 +72,8 @@ void VOutline::updateTreeByLevel(const QVector<VHeader> &headers, int &index,
             } else {
                 item = new QTreeWidgetItem(this);
             }
-            QJsonObject itemJson;
-            itemJson["anchor"] = header.anchor;
-            itemJson["line_number"] = header.lineNumber;
-            itemJson["outline_index"] = index;
-            item->setData(0, Qt::UserRole, itemJson);
-            item->setText(0, header.name);
-            item->setToolTip(0, header.name);
+
+            fillItem(item, header);
 
             last = item;
             ++index;
@@ -74,6 +85,17 @@ void VOutline::updateTreeByLevel(const QVector<VHeader> &headers, int &index,
     }
 }
 
+void VOutline::fillItem(QTreeWidgetItem *p_item, const VHeader &p_header)
+{
+    p_item->setData(0, Qt::UserRole, p_header.index);
+    p_item->setText(0, p_header.name);
+    p_item->setToolTip(0, p_header.name);
+
+    if (p_header.isEmpty()) {
+        p_item->setForeground(0, QColor("grey"));
+    }
+}
+
 void VOutline::expandTree()
 {
     if (topLevelItemCount() == 0) {
@@ -87,20 +109,22 @@ void VOutline::handleCurItemChanged(QTreeWidgetItem *p_curItem, QTreeWidgetItem
     if (!p_curItem) {
         return;
     }
-    QJsonObject itemJson = p_curItem->data(0, Qt::UserRole).toJsonObject();
-    QString anchor = itemJson["anchor"].toString();
-    int lineNumber = itemJson["line_number"].toInt();
-    int outlineIndex = itemJson["outline_index"].toInt();
-    VAnchor tmp;
-    tmp.m_file = outline.m_file;
-    tmp.anchor = anchor;
-    tmp.lineNumber = lineNumber;
-    tmp.m_outlineIndex = outlineIndex;
+
+    const VHeader *header = getHeaderFromItem(p_curItem);
+    if (!header) {
+        return;
+    }
+
+    VAnchor tmp(outline.m_file, header->anchor, header->lineNumber, header->index);
     if (tmp == curHeader) {
         return;
     }
+
     curHeader = tmp;
-    emit outlineItemActivated(curHeader);
+
+    if (!header->isEmpty()) {
+        emit outlineItemActivated(curHeader);
+    }
 }
 
 void VOutline::updateCurHeader(const VAnchor &anchor)
@@ -108,17 +132,24 @@ void VOutline::updateCurHeader(const VAnchor &anchor)
     if (anchor == curHeader) {
         return;
     }
+
     curHeader = anchor;
     if (outline.type == VHeaderType::Anchor) {
         selectAnchor(anchor.anchor);
     } else {
-        // Select by lineNumber
+        // Select by lineNumber.
         selectLineNumber(anchor.lineNumber);
     }
 }
 
 void VOutline::selectAnchor(const QString &anchor)
 {
+    setCurrentItem(NULL);
+
+    if (anchor.isEmpty()) {
+        return;
+    }
+
     int nrTop = topLevelItemCount();
     for (int i = 0; i < nrTop; ++i) {
         if (selectAnchorOne(topLevelItem(i), anchor)) {
@@ -132,9 +163,13 @@ bool VOutline::selectAnchorOne(QTreeWidgetItem *item, const QString &anchor)
     if (!item) {
         return false;
     }
-    QJsonObject itemJson = item->data(0, Qt::UserRole).toJsonObject();
-    QString itemAnchor = itemJson["anchor"].toString();
-    if (itemAnchor == anchor) {
+
+    const VHeader *header = getHeaderFromItem(item);
+    if (!header) {
+        return false;
+    }
+
+    if (header->anchor == anchor) {
         setCurrentItem(item);
         return true;
     }
@@ -150,6 +185,12 @@ bool VOutline::selectAnchorOne(QTreeWidgetItem *item, const QString &anchor)
 
 void VOutline::selectLineNumber(int lineNumber)
 {
+    setCurrentItem(NULL);
+
+    if (lineNumber == -1) {
+        return;
+    }
+
     int nrTop = topLevelItemCount();
     for (int i = 0; i < nrTop; ++i) {
         if (selectLineNumberOne(topLevelItem(i), lineNumber)) {
@@ -163,9 +204,13 @@ bool VOutline::selectLineNumberOne(QTreeWidgetItem *item, int lineNumber)
     if (!item) {
         return false;
     }
-    QJsonObject itemJson = item->data(0, Qt::UserRole).toJsonObject();
-    int itemLineNum = itemJson["line_number"].toInt();
-    if (itemLineNum == lineNumber) {
+
+    const VHeader *header = getHeaderFromItem(item);
+    if (!header) {
+        return false;
+    }
+
+    if (header->lineNumber == lineNumber) {
         // Select this item
         setCurrentItem(item);
         return true;
@@ -329,3 +374,18 @@ QList<QTreeWidgetItem *> VOutline::getVisibleChildItems(const QTreeWidgetItem *p
     }
     return items;
 }
+
+const VHeader *VOutline::getHeaderFromItem(QTreeWidgetItem *p_item) const
+{
+    const VHeader *header = NULL;
+
+    int index = p_item->data(0, Qt::UserRole).toInt();
+    if (index < 0 || index >= outline.headers.size()) {
+        return header;
+    }
+
+    header = &(outline.headers[index]);
+    Q_ASSERT(header->index == index);
+
+    return header;
+}

+ 14 - 1
src/voutline.h

@@ -36,9 +36,13 @@ private slots:
     void handleCurItemChanged(QTreeWidgetItem *p_curItem, QTreeWidgetItem *p_preItem);
 
 private:
-    void updateTreeFromOutline(const VToc &toc);
+    // Update tree according to outline.
+    void updateTreeFromOutline();
+
+    // @index: the index in @headers.
     void updateTreeByLevel(const QVector<VHeader> &headers, int &index, QTreeWidgetItem *parent,
                            QTreeWidgetItem *last, int level);
+
     void expandTree();
     void selectAnchor(const QString &anchor);
     bool selectAnchorOne(QTreeWidgetItem *item, const QString &anchor);
@@ -47,6 +51,15 @@ private:
     QList<QTreeWidgetItem *> getVisibleItems() const;
     QList<QTreeWidgetItem *> getVisibleChildItems(const QTreeWidgetItem *p_item) const;
 
+    // Fill the info of @p_item.
+    void fillItem(QTreeWidgetItem *p_item, const VHeader &p_header);
+
+    // Check if @p_toc is valid.
+    void checkOutline(const VToc &p_toc) const;
+
+    // Return NULL if no corresponding header in outline.
+    const VHeader *getHeaderFromItem(QTreeWidgetItem *p_item) const;
+
     VToc outline;
     VAnchor curHeader;
 

+ 31 - 8
src/vtoc.h

@@ -14,30 +14,53 @@ enum VHeaderType
 
 struct VHeader
 {
-    VHeader() : level(1), lineNumber(-1) {}
-    VHeader(int level, const QString &name, const QString &anchor, int lineNumber)
-        : level(level), name(name), anchor(anchor), lineNumber(lineNumber) {}
+    VHeader() : level(1), lineNumber(-1), index(-1) {}
+    VHeader(int level, const QString &name, const QString &anchor, int lineNumber, int index)
+        : level(level), name(name), anchor(anchor), lineNumber(lineNumber), index(index) {}
     int level;
     QString name;
     QString anchor;
     int lineNumber;
+
+    // Index in the outline, based on 0.
+    int index;
+
+    // Whether it is an empty (fake) header.
+    bool isEmpty() const
+    {
+        if (anchor.isEmpty()) {
+            return lineNumber == -1;
+        } else {
+            return anchor == "#";
+        }
+    }
 };
 
 struct VAnchor
 {
-    VAnchor() : lineNumber(-1), m_outlineIndex(0) {}
-    VAnchor(const VFile *file, const QString &anchor, int lineNumber)
-        : m_file(file), anchor(anchor), lineNumber(lineNumber), m_outlineIndex(0) {}
+    VAnchor() : m_file(NULL), lineNumber(-1), m_outlineIndex(-1) {}
+
+    VAnchor(const VFile *file, const QString &anchor, int lineNumber, int outlineIndex = -1)
+        : m_file(file), anchor(anchor), lineNumber(lineNumber), m_outlineIndex(outlineIndex) {}
+
+    // The file this anchor points to.
     const VFile *m_file;
+
+    // The string anchor. For Web view.
     QString anchor;
+
+    // The line number anchor. For edit view.
     int lineNumber;
-    // Index of this anchor in VToc outline.
+
+    // Index of the header for this anchor in VToc outline.
+    // Used to translate current header between read and edit mode.
     int m_outlineIndex;
 
     bool operator==(const VAnchor &p_anchor) const {
         return (p_anchor.m_file == m_file
                 && p_anchor.anchor == anchor
-                && p_anchor.lineNumber == lineNumber);
+                && p_anchor.lineNumber == lineNumber
+                && p_anchor.m_outlineIndex == m_outlineIndex);
     }
 };