diff --git a/src/resources/markdown-it.js b/src/resources/markdown-it.js index c5b7ccc6..b3055cd2 100644 --- a/src/resources/markdown-it.js +++ b/src/resources/markdown-it.js @@ -51,7 +51,8 @@ var mdit = window.markdownit({ return hljs.highlightAuto(str).value; } } else { - return str; + // Use external default escaping. + return ''; } } }); @@ -67,7 +68,7 @@ mdit = mdit.use(window.markdownitHeadingAnchor, { toc.push({ level: getHeadingLevel(openToken.tag), anchor: anchor, - title: escapeHtml(inlineToken.content) + title: mdit.utils.escapeHtml(inlineToken.content) }); } }); diff --git a/src/utils/vutils.cpp b/src/utils/vutils.cpp index 5df6f28c..9c934ba0 100644 --- a/src/utils/vutils.cpp +++ b/src/utils/vutils.cpp @@ -216,6 +216,9 @@ QVector VUtils::fetchImagesFromMarkdownFile(VFile *p_file, return images; } + // Used to de-duplicate the links. Url as the key. + QSet fetchedLinks; + QVector regions = fetchImageRegionsUsingParser(text); QRegExp regExp(c_imageLinkRegExp); QString basePath = p_file->fetchBasePath(); @@ -231,6 +234,7 @@ QVector VUtils::fetchImagesFromMarkdownFile(VFile *p_file, QString imageUrl = regExp.capturedTexts()[2].trimmed(); ImageLink link; + link.m_url = imageUrl; QFileInfo info(basePath, imageUrl); if (info.exists()) { if (info.isNativePath()) { @@ -254,8 +258,11 @@ QVector VUtils::fetchImagesFromMarkdownFile(VFile *p_file, } if (link.m_type & p_type) { - images.push_back(link); - qDebug() << "fetch one image:" << link.m_type << link.m_path; + if (!fetchedLinks.contains(link.m_url)) { + fetchedLinks.insert(link.m_url); + images.push_back(link); + qDebug() << "fetch one image:" << link.m_type << link.m_path << link.m_url; + } } } @@ -266,6 +273,24 @@ QVector VUtils::fetchImagesFromMarkdownFile(VFile *p_file, return images; } +QString VUtils::imageLinkUrlToPath(const QString &p_basePath, const QString &p_url) +{ + QString path; + QFileInfo info(p_basePath, p_url); + if (info.exists()) { + if (info.isNativePath()) { + // Local file. + path = QDir::cleanPath(info.absoluteFilePath()); + } else { + path = p_url; + } + } else { + path = QUrl(p_url).toString(); + } + + return path; +} + bool VUtils::makePath(const QString &p_path) { if (p_path.isEmpty()) { diff --git a/src/utils/vutils.h b/src/utils/vutils.h index e31ae1b7..bf998d94 100644 --- a/src/utils/vutils.h +++ b/src/utils/vutils.h @@ -67,6 +67,10 @@ struct ImageLink }; QString m_path; + + // The url text in the link. + QString m_url; + ImageLinkType m_type; }; @@ -121,6 +125,9 @@ public: static QVector fetchImagesFromMarkdownFile(VFile *p_file, ImageLink::ImageLinkType p_type = ImageLink::All); + // Return the absolute path of @p_url according to @p_basePath. + static QString imageLinkUrlToPath(const QString &p_basePath, const QString &p_url); + // Create directories along the @p_path. // @p_path could be /home/tamlok/abc, /home/tamlok/abc/. static bool makePath(const QString &p_path); diff --git a/src/vconstants.h b/src/vconstants.h index ddbc30f5..26ca8ccf 100644 --- a/src/vconstants.h +++ b/src/vconstants.h @@ -128,4 +128,13 @@ enum class CursorBlock LeftSide }; +enum class UpdateAction +{ + // The info of a file/directory has been changed. + InfoChanged = 0, + + // The file/directory has been moved. + Moved +}; + #endif diff --git a/src/vdirectorytree.cpp b/src/vdirectorytree.cpp index 937a31ea..b460f0be 100644 --- a/src/vdirectorytree.cpp +++ b/src/vdirectorytree.cpp @@ -602,7 +602,7 @@ void VDirectoryTree::editDirectoryInfo() fillTreeItem(curItem, curDir); - emit directoryUpdated(curDir); + emit directoryUpdated(curDir, UpdateAction::InfoChanged); } } @@ -856,7 +856,7 @@ void VDirectoryTree::pasteDirectories(VDirectory *p_destDir, } // Broadcast this update - emit directoryUpdated(destDir); + emit directoryUpdated(destDir, p_isCut ? UpdateAction::Moved : UpdateAction::InfoChanged); } } diff --git a/src/vdirectorytree.h b/src/vdirectorytree.h index eb2df598..399c8eba 100644 --- a/src/vdirectorytree.h +++ b/src/vdirectorytree.h @@ -11,6 +11,7 @@ #include "vdirectory.h" #include "vnotebook.h" #include "vnavigationmode.h" +#include "vconstants.h" class VEditArea; class QLabel; @@ -35,7 +36,7 @@ public: signals: void currentDirectoryChanged(VDirectory *p_directory); - void directoryUpdated(const VDirectory *p_directory); + void directoryUpdated(const VDirectory *p_directory, UpdateAction p_act); public slots: // Set directory tree to display a given notebook @p_notebook. diff --git a/src/veditarea.cpp b/src/veditarea.cpp index 31ceb534..4fa50062 100644 --- a/src/veditarea.cpp +++ b/src/veditarea.cpp @@ -521,19 +521,19 @@ bool VEditArea::isFileOpened(const VFile *p_file) return !findTabsByFile(p_file).isEmpty(); } -void VEditArea::handleFileUpdated(const VFile *p_file) +void VEditArea::handleFileUpdated(const VFile *p_file, UpdateAction p_act) { int nrWin = splitter->count(); for (int i = 0; i < nrWin; ++i) { - getWindow(i)->updateFileInfo(p_file); + getWindow(i)->updateFileInfo(p_file, p_act); } } -void VEditArea::handleDirectoryUpdated(const VDirectory *p_dir) +void VEditArea::handleDirectoryUpdated(const VDirectory *p_dir, UpdateAction p_act) { int nrWin = splitter->count(); for (int i = 0; i < nrWin; ++i) { - getWindow(i)->updateDirectoryInfo(p_dir); + getWindow(i)->updateDirectoryInfo(p_dir, p_act); } } diff --git a/src/veditarea.h b/src/veditarea.h index 291e7c23..c41501c6 100644 --- a/src/veditarea.h +++ b/src/veditarea.h @@ -115,8 +115,10 @@ public slots: // Scroll current tab to @p_header. void scrollToHeader(const VHeaderPointer &p_header); - void handleFileUpdated(const VFile *p_file); - void handleDirectoryUpdated(const VDirectory *p_dir); + void handleFileUpdated(const VFile *p_file, UpdateAction p_act); + + void handleDirectoryUpdated(const VDirectory *p_dir, UpdateAction p_act); + void handleNotebookUpdated(const VNotebook *p_notebook); private slots: diff --git a/src/vedittab.cpp b/src/vedittab.cpp index 9636ef50..0832b3ca 100644 --- a/src/vedittab.cpp +++ b/src/vedittab.cpp @@ -188,3 +188,9 @@ void VEditTab::reloadFromDisk() void VEditTab::writeBackupFile() { } + +void VEditTab::handleFileOrDirectoryChange(bool p_isFile, UpdateAction p_act) +{ + Q_UNUSED(p_isFile); + Q_UNUSED(p_act); +} diff --git a/src/vedittab.h b/src/vedittab.h index 64d23e9f..a261d39c 100644 --- a/src/vedittab.h +++ b/src/vedittab.h @@ -111,6 +111,9 @@ public: // Reload file from disk and reload the editor. void reloadFromDisk(); + // Handle the change of file or directory, such as the file has been moved. + virtual void handleFileOrDirectoryChange(bool p_isFile, UpdateAction p_act); + public slots: // Enter edit mode virtual void editFile() = 0; diff --git a/src/veditwindow.cpp b/src/veditwindow.cpp index 835326ee..08be256d 100644 --- a/src/veditwindow.cpp +++ b/src/veditwindow.cpp @@ -793,18 +793,20 @@ void VEditWindow::handleTabVimStatusUpdated(const VVim *p_vim) } } -void VEditWindow::updateFileInfo(const VFile *p_file) +void VEditWindow::updateFileInfo(const VFile *p_file, UpdateAction p_act) { if (!p_file) { return; } + int idx = findTabByFile(p_file); if (idx > -1) { updateTabStatus(idx); + getTab(idx)->handleFileOrDirectoryChange(true, p_act); } } -void VEditWindow::updateDirectoryInfo(const VDirectory *p_dir) +void VEditWindow::updateDirectoryInfo(const VDirectory *p_dir, UpdateAction p_act) { if (!p_dir) { return; @@ -816,6 +818,7 @@ void VEditWindow::updateDirectoryInfo(const VDirectory *p_dir) QPointer file = editor->getFile(); if (p_dir->containsFile(file)) { updateTabStatus(i); + editor->handleFileOrDirectoryChange(false, p_act); } } } diff --git a/src/veditwindow.h b/src/veditwindow.h index 8b64f55d..0dc5de2c 100644 --- a/src/veditwindow.h +++ b/src/veditwindow.h @@ -43,8 +43,10 @@ public: // Scroll current tab to header @p_header. void scrollToHeader(const VHeaderPointer &p_header); - void updateFileInfo(const VFile *p_file); - void updateDirectoryInfo(const VDirectory *p_dir); + void updateFileInfo(const VFile *p_file, UpdateAction p_act); + + void updateDirectoryInfo(const VDirectory *p_dir, UpdateAction p_act); + void updateNotebookInfo(const VNotebook *p_notebook); VEditTab *getCurrentTab() const; diff --git a/src/vfilelist.cpp b/src/vfilelist.cpp index 3578afb4..7970e4d0 100644 --- a/src/vfilelist.cpp +++ b/src/vfilelist.cpp @@ -247,7 +247,7 @@ void VFileList::fileInfo(VNoteFile *p_file) fillItem(item, p_file); } - emit fileUpdated(p_file); + emit fileUpdated(p_file, UpdateAction::InfoChanged); } } @@ -800,7 +800,7 @@ void VFileList::pasteFiles(VDirectory *p_destDir, if (destFile) { ++nrPasted; - emit fileUpdated(destFile); + emit fileUpdated(destFile, p_isCut ? UpdateAction::Moved : UpdateAction::InfoChanged); } } diff --git a/src/vfilelist.h b/src/vfilelist.h index 8a24baf1..b8923068 100644 --- a/src/vfilelist.h +++ b/src/vfilelist.h @@ -69,7 +69,7 @@ signals: OpenFileMode p_mode = OpenFileMode::Read, bool p_forceMode = false); - void fileUpdated(const VNoteFile *p_file); + void fileUpdated(const VNoteFile *p_file, UpdateAction p_act); private slots: void contextMenuRequested(QPoint pos); diff --git a/src/vmdeditoperations.cpp b/src/vmdeditoperations.cpp index def725d8..2093f046 100644 --- a/src/vmdeditoperations.cpp +++ b/src/vmdeditoperations.cpp @@ -86,14 +86,15 @@ void VMdEditOperations::insertImageFromQImage(const QString &title, const QStrin return; } - QString md = QString("![%1](%2/%3)").arg(title).arg(folderInLink).arg(fileName); + QString url = QString("%1/%2").arg(folderInLink).arg(fileName); + QString md = QString("![%1](%2)").arg(title).arg(url); insertTextAtCurPos(md); qDebug() << "insert image" << title << filePath; VMdEditor *mdEditor = dynamic_cast(m_editor); Q_ASSERT(mdEditor); - mdEditor->imageInserted(filePath); + mdEditor->imageInserted(filePath, url); } void VMdEditOperations::insertImageFromPath(const QString &title, const QString &path, @@ -126,14 +127,15 @@ void VMdEditOperations::insertImageFromPath(const QString &title, const QString return; } - QString md = QString("![%1](%2/%3)").arg(title).arg(folderInLink).arg(fileName); + QString url = QString("%1/%2").arg(folderInLink).arg(fileName); + QString md = QString("![%1](%2)").arg(title).arg(url); insertTextAtCurPos(md); qDebug() << "insert image" << title << filePath; VMdEditor *mdEditor = dynamic_cast(m_editor); Q_ASSERT(mdEditor); - mdEditor->imageInserted(filePath); + mdEditor->imageInserted(filePath, url); } bool VMdEditOperations::insertImageFromURL(const QUrl &imageUrl) diff --git a/src/vmdeditor.cpp b/src/vmdeditor.cpp index 7ed673b5..56964562 100644 --- a/src/vmdeditor.cpp +++ b/src/vmdeditor.cpp @@ -125,6 +125,7 @@ void VMdEditor::beginEdit() void VMdEditor::endEdit() { setReadOnlyAndHighlightCurrentLine(true); + clearUnusedImages(); } @@ -138,6 +139,10 @@ void VMdEditor::saveFile() m_file->setContent(toPlainText()); setModified(false); + + clearUnusedImages(); + + initInitImages(); } void VMdEditor::reloadFile() @@ -544,7 +549,7 @@ void VMdEditor::clearUnusedImages() QVector images = VUtils::fetchImagesFromMarkdownFile(m_file, ImageLink::LocalRelativeInternal); - QVector unusedImages; + QSet unusedImages; if (!m_insertedImages.isEmpty()) { for (int i = 0; i < m_insertedImages.size(); ++i) { @@ -563,7 +568,7 @@ void VMdEditor::clearUnusedImages() // This inserted image is no longer in the file. if (j == images.size()) { - unusedImages.push_back(link.m_path); + unusedImages.insert(link.m_path); } } @@ -584,7 +589,7 @@ void VMdEditor::clearUnusedImages() // Original local relative image is no longer in the file. if (j == images.size()) { - unusedImages.push_back(link.m_path); + unusedImages.insert(link.m_path); } } @@ -621,27 +626,27 @@ void VMdEditor::clearUnusedImages() g_config->setConfirmImagesCleanUp(dialog.getAskAgainEnabled()); for (auto const & item : items) { - unusedImages.push_back(item.m_name); + unusedImages.insert(item.m_name); } } } - for (int i = 0; i < unusedImages.size(); ++i) { + for (auto const & item : unusedImages) { bool ret = false; if (m_file->getType() == FileType::Note) { const VNoteFile *tmpFile = dynamic_cast((VFile *)m_file); - ret = VUtils::deleteFile(tmpFile->getNotebook(), unusedImages[i], false); + ret = VUtils::deleteFile(tmpFile->getNotebook(), item, false); } else if (m_file->getType() == FileType::Orphan) { const VOrphanFile *tmpFile = dynamic_cast((VFile *)m_file); - ret = VUtils::deleteFile(tmpFile, unusedImages[i], false); + ret = VUtils::deleteFile(tmpFile, item, false); } else { Q_ASSERT(false); } if (!ret) { - qWarning() << "fail to delete unused original image" << unusedImages[i]; + qWarning() << "fail to delete unused original image" << item; } else { - qDebug() << "delete unused image" << unusedImages[i]; + qDebug() << "delete unused image" << item; } } } @@ -731,10 +736,11 @@ void VMdEditor::insertFromMimeData(const QMimeData *p_source) VTextEdit::insertFromMimeData(p_source); } -void VMdEditor::imageInserted(const QString &p_path) +void VMdEditor::imageInserted(const QString &p_path, const QString &p_url) { ImageLink link; link.m_path = p_path; + link.m_url = p_url; if (m_file->useRelativeImageFolder()) { link.m_type = ImageLink::LocalRelativeInternal; } else { @@ -907,3 +913,80 @@ void VMdEditor::setContent(const QString &p_content, bool p_modified) setPlainText(p_content); } } + +void VMdEditor::refreshPreview() +{ + m_previewMgr->refreshPreview(); +} + +void VMdEditor::updateInitAndInsertedImages(bool p_fileChanged, UpdateAction p_act) +{ + if (p_fileChanged && p_act == UpdateAction::InfoChanged) { + return; + } + + if (!isModified()) { + Q_ASSERT(m_insertedImages.isEmpty()); + m_insertedImages.clear(); + + if (!m_initImages.isEmpty()) { + // Re-generate init images. + initInitImages(); + } + + return; + } + + // Update init images. + QVector tmp = m_initImages; + initInitImages(); + Q_ASSERT(tmp.size() == m_initImages.size()); + + QDir dir(m_file->fetchBasePath()); + + // File has been moved. + if (p_fileChanged) { + // Since we clear unused images once user save the note, all images + // in m_initImages now are moved already. + + // Update inserted images. + // Inserted images should be moved manually here. Then update all the + // paths. + for (auto & link : m_insertedImages) { + if (link.m_type == ImageLink::LocalAbsolute) { + continue; + } + + QString newPath = QDir::cleanPath(dir.absoluteFilePath(link.m_url)); + if (VUtils::equalPath(link.m_path, newPath)) { + continue; + } + + if (!VUtils::copyFile(link.m_path, newPath, true)) { + VUtils::showMessage(QMessageBox::Warning, + tr("Warning"), + tr("Fail to move unsaved inserted image %1 to %2.") + .arg(link.m_path) + .arg(newPath), + tr("Please check it manually to avoid image loss."), + QMessageBox::Ok, + QMessageBox::Ok, + this); + continue; + } + + link.m_path = newPath; + } + } else { + // Directory changed. + // Update inserted images. + for (auto & link : m_insertedImages) { + if (link.m_type == ImageLink::LocalAbsolute) { + continue; + } + + QString newPath = QDir::cleanPath(dir.absoluteFilePath(link.m_url)); + link.m_path = newPath; + } + } +} diff --git a/src/vmdeditor.h b/src/vmdeditor.h index b35baa39..35aebe57 100644 --- a/src/vmdeditor.h +++ b/src/vmdeditor.h @@ -45,7 +45,8 @@ public: // An image has been inserted. The image is relative. // @p_path is the absolute path of the inserted image. - void imageInserted(const QString &p_path); + // @p_url is the URL text within (). + void imageInserted(const QString &p_path, const QString &p_url); // Scroll to header @p_blockNumber. // Return true if @p_blockNumber is valid to scroll to. @@ -59,6 +60,11 @@ public: void setContent(const QString &p_content, bool p_modified = false) Q_DECL_OVERRIDE; + void refreshPreview(); + + // Update m_initImages and m_insertedImages to handle the change of the note path. + void updateInitAndInsertedImages(bool p_fileChanged, UpdateAction p_act); + public slots: bool jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat) Q_DECL_OVERRIDE; @@ -217,5 +223,4 @@ private: bool m_freshEdit; }; - #endif // VMDEDITOR_H diff --git a/src/vmdtab.cpp b/src/vmdtab.cpp index 152c9ade..bb6c7ae0 100644 --- a/src/vmdtab.cpp +++ b/src/vmdtab.cpp @@ -982,3 +982,18 @@ void VMdTab::updateCursorStatus() { emit statusUpdated(fetchTabInfo(VEditTabInfo::InfoType::Cursor)); } + +void VMdTab::handleFileOrDirectoryChange(bool p_isFile, UpdateAction p_act) +{ + // Reload the web view with new base URL. + m_headerFromEditMode = m_currentHeader; + m_webViewer->setHtml(VUtils::generateHtmlTemplate(m_mdConType, false), + m_file->getBaseUrl()); + + if (m_editor) { + m_editor->updateInitAndInsertedImages(p_isFile, p_act); + + // Refresh the previewed images in edit mode. + m_editor->refreshPreview(); + } +} diff --git a/src/vmdtab.h b/src/vmdtab.h index 85358fb6..755761ac 100644 --- a/src/vmdtab.h +++ b/src/vmdtab.h @@ -85,6 +85,8 @@ public: void reload() Q_DECL_OVERRIDE; + void handleFileOrDirectoryChange(bool p_isFile, UpdateAction p_act) Q_DECL_OVERRIDE; + public slots: // Enter edit mode. void editFile() Q_DECL_OVERRIDE; diff --git a/src/vnotefile.cpp b/src/vnotefile.cpp index ea85a7fb..09f96a02 100644 --- a/src/vnotefile.cpp +++ b/src/vnotefile.cpp @@ -511,8 +511,15 @@ bool VNoteFile::copyFile(VDirectory *p_destDir, // Copy images. QDir parentDir(destFile->fetchBasePath()); + QSet processedImages; for (int i = 0; i < images.size(); ++i) { const ImageLink &link = images[i]; + if (processedImages.contains(link.m_path)) { + continue; + } + + processedImages.insert(link.m_path); + if (!QFileInfo::exists(link.m_path)) { VUtils::addErrMsg(p_errMsg, tr("Source image %1 does not exist.") .arg(link.m_path)); diff --git a/src/vpreviewmanager.cpp b/src/vpreviewmanager.cpp index 82389e3b..d46e25d7 100644 --- a/src/vpreviewmanager.cpp +++ b/src/vpreviewmanager.cpp @@ -375,3 +375,14 @@ void VPreviewManager::clearBlockObsoletePreviewInfo(long long p_timeStamp, m_editor->relayout(affectedBlocks); } + +void VPreviewManager::refreshPreview() +{ + if (!m_previewEnabled) { + return; + } + + clearPreview(); + + requestUpdateImageLinks(); +} diff --git a/src/vpreviewmanager.h b/src/vpreviewmanager.h index 53375b0c..6fb47066 100644 --- a/src/vpreviewmanager.h +++ b/src/vpreviewmanager.h @@ -26,6 +26,9 @@ public: // Clear all the preview. void clearPreview(); + // Refresh all the preview. + void refreshPreview(); + public slots: // Image links were updated from the highlighter. void imageLinksUpdated(const QVector &p_imageRegions);