markdowneditor.cpp 57 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796
  1. #include "markdowneditor.h"
  2. #include <QRegularExpression>
  3. #include <QApplication>
  4. #include <QClipboard>
  5. #include <QMimeData>
  6. #include <QFileInfo>
  7. #include <QDir>
  8. #include <QMimeDatabase>
  9. #include <QClipboard>
  10. #include <QMenu>
  11. #include <QAction>
  12. #include <QShortcut>
  13. #include <QProgressDialog>
  14. #include <QTemporaryFile>
  15. #include <QTimer>
  16. #include <QBuffer>
  17. #include <QPainter>
  18. #include <QHash>
  19. #include <vtextedit/markdowneditorconfig.h>
  20. #include <vtextedit/previewmgr.h>
  21. #include <vtextedit/markdownutils.h>
  22. #include <vtextedit/vtextedit.h>
  23. #include <vtextedit/texteditutils.h>
  24. #include <vtextedit/markdownutils.h>
  25. #include <vtextedit/networkutils.h>
  26. #include <vtextedit/theme.h>
  27. #include <vtextedit/previewdata.h>
  28. #include <vtextedit/textblockdata.h>
  29. #include <widgets/dialogs/linkinsertdialog.h>
  30. #include <widgets/dialogs/imageinsertdialog.h>
  31. #include <widgets/dialogs/tableinsertdialog.h>
  32. #include <widgets/messageboxhelper.h>
  33. #include <widgets/dialogs/selectdialog.h>
  34. #include <buffer/buffer.h>
  35. #include <buffer/markdownbuffer.h>
  36. #include <utils/fileutils.h>
  37. #include <utils/pathutils.h>
  38. #include <utils/htmlutils.h>
  39. #include <utils/widgetutils.h>
  40. #include <utils/webutils.h>
  41. #include <utils/imageutils.h>
  42. #include <utils/clipboardutils.h>
  43. #include <core/exception.h>
  44. #include <core/markdowneditorconfig.h>
  45. #include <core/texteditorconfig.h>
  46. #include <core/configmgr.h>
  47. #include <core/editorconfig.h>
  48. #include <core/vnotex.h>
  49. #include <core/fileopenparameters.h>
  50. #include <imagehost/imagehostutils.h>
  51. #include <imagehost/imagehost.h>
  52. #include <imagehost/imagehostmgr.h>
  53. #include "previewhelper.h"
  54. #include "../outlineprovider.h"
  55. #include "markdowntablehelper.h"
  56. using namespace vnotex;
  57. MarkdownEditor::Heading::Heading(const QString &p_name,
  58. int p_level,
  59. const QString &p_sectionNumber,
  60. int p_blockNumber)
  61. : m_name(p_name),
  62. m_level(p_level),
  63. m_sectionNumber(p_sectionNumber),
  64. m_blockNumber(p_blockNumber)
  65. {
  66. }
  67. MarkdownEditor::MarkdownEditor(const MarkdownEditorConfig &p_config,
  68. const QSharedPointer<vte::MarkdownEditorConfig> &p_editorConfig,
  69. const QSharedPointer<vte::TextEditorParameters> &p_editorParas,
  70. QWidget *p_parent)
  71. : vte::VMarkdownEditor(p_editorConfig, p_editorParas, p_parent),
  72. m_config(p_config)
  73. {
  74. setupShortcuts();
  75. connect(m_textEdit, &vte::VTextEdit::canInsertFromMimeDataRequested,
  76. this, &MarkdownEditor::handleCanInsertFromMimeData);
  77. connect(m_textEdit, &vte::VTextEdit::insertFromMimeDataRequested,
  78. this, &MarkdownEditor::handleInsertFromMimeData);
  79. connect(m_textEdit, &vte::VTextEdit::contextMenuEventRequested,
  80. this, &MarkdownEditor::handleContextMenuEvent);
  81. connect(getHighlighter(), &vte::PegMarkdownHighlighter::headersUpdated,
  82. this, &MarkdownEditor::updateHeadings);
  83. setupTableHelper();
  84. m_headingTimer = new QTimer(this);
  85. m_headingTimer->setInterval(500);
  86. m_headingTimer->setSingleShot(true);
  87. connect(m_headingTimer, &QTimer::timeout,
  88. this, &MarkdownEditor::currentHeadingChanged);
  89. connect(m_textEdit, &vte::VTextEdit::cursorLineChanged,
  90. m_headingTimer, QOverload<>::of(&QTimer::start));
  91. m_sectionNumberTimer = new QTimer(this);
  92. m_sectionNumberTimer->setInterval(1000);
  93. m_sectionNumberTimer->setSingleShot(true);
  94. connect(m_sectionNumberTimer, &QTimer::timeout,
  95. this, [this]() {
  96. updateSectionNumber(m_headings);
  97. });
  98. updateFromConfig(false);
  99. }
  100. MarkdownEditor::~MarkdownEditor()
  101. {
  102. }
  103. void MarkdownEditor::setPreviewHelper(PreviewHelper *p_helper)
  104. {
  105. auto highlighter = getHighlighter();
  106. connect(highlighter, &vte::PegMarkdownHighlighter::codeBlocksUpdated,
  107. p_helper, &PreviewHelper::codeBlocksUpdated);
  108. connect(highlighter, &vte::PegMarkdownHighlighter::mathBlocksUpdated,
  109. p_helper, &PreviewHelper::mathBlocksUpdated);
  110. auto previewMgr = getPreviewMgr();
  111. connect(p_helper, &PreviewHelper::inplacePreviewCodeBlockUpdated,
  112. previewMgr, &vte::PreviewMgr::updateCodeBlocks);
  113. connect(p_helper, &PreviewHelper::inplacePreviewMathBlockUpdated,
  114. previewMgr, &vte::PreviewMgr::updateMathBlocks);
  115. connect(p_helper, &PreviewHelper::potentialObsoletePreviewBlocksUpdated,
  116. previewMgr, &vte::PreviewMgr::checkBlocksForObsoletePreview);
  117. }
  118. void MarkdownEditor::typeHeading(int p_level)
  119. {
  120. enterInsertModeIfApplicable();
  121. vte::MarkdownUtils::typeHeading(m_textEdit, p_level);
  122. }
  123. void MarkdownEditor::typeBold()
  124. {
  125. enterInsertModeIfApplicable();
  126. vte::MarkdownUtils::typeBold(m_textEdit);
  127. }
  128. void MarkdownEditor::typeItalic()
  129. {
  130. enterInsertModeIfApplicable();
  131. vte::MarkdownUtils::typeItalic(m_textEdit);
  132. }
  133. void MarkdownEditor::typeStrikethrough()
  134. {
  135. enterInsertModeIfApplicable();
  136. vte::MarkdownUtils::typeStrikethrough(m_textEdit);
  137. }
  138. void MarkdownEditor::typeMark()
  139. {
  140. enterInsertModeIfApplicable();
  141. vte::MarkdownUtils::typeMark(m_textEdit);
  142. }
  143. void MarkdownEditor::typeUnorderedList()
  144. {
  145. enterInsertModeIfApplicable();
  146. vte::MarkdownUtils::typeUnorderedList(m_textEdit);
  147. }
  148. void MarkdownEditor::typeOrderedList()
  149. {
  150. enterInsertModeIfApplicable();
  151. vte::MarkdownUtils::typeOrderedList(m_textEdit);
  152. }
  153. void MarkdownEditor::typeTodoList(bool p_checked)
  154. {
  155. enterInsertModeIfApplicable();
  156. vte::MarkdownUtils::typeTodoList(m_textEdit, p_checked);
  157. }
  158. void MarkdownEditor::typeCode()
  159. {
  160. enterInsertModeIfApplicable();
  161. vte::MarkdownUtils::typeCode(m_textEdit);
  162. }
  163. void MarkdownEditor::typeCodeBlock()
  164. {
  165. enterInsertModeIfApplicable();
  166. vte::MarkdownUtils::typeCodeBlock(m_textEdit);
  167. }
  168. void MarkdownEditor::typeMath()
  169. {
  170. enterInsertModeIfApplicable();
  171. vte::MarkdownUtils::typeMath(m_textEdit);
  172. }
  173. void MarkdownEditor::typeMathBlock()
  174. {
  175. enterInsertModeIfApplicable();
  176. vte::MarkdownUtils::typeMathBlock(m_textEdit);
  177. }
  178. void MarkdownEditor::typeQuote()
  179. {
  180. enterInsertModeIfApplicable();
  181. vte::MarkdownUtils::typeQuote(m_textEdit);
  182. }
  183. void MarkdownEditor::typeLink()
  184. {
  185. QString linkText;
  186. QString linkUrl;
  187. // Try get Url or text from selection.
  188. auto cursor = m_textEdit->textCursor();
  189. QRegularExpression urlReg("[\\.\\\\/]");
  190. if (cursor.hasSelection()) {
  191. auto text = vte::TextEditUtils::getSelectedText(cursor).trimmed();
  192. if (!text.isEmpty() && !text.contains(QLatin1Char('\n'))) {
  193. if (text.contains(urlReg) && QUrl::fromUserInput(text).isValid()) {
  194. linkUrl = text;
  195. } else {
  196. linkText = text;
  197. }
  198. }
  199. }
  200. // Fetch link from clipboard.
  201. if (linkUrl.isEmpty() && linkText.isEmpty()) {
  202. const auto clipboard = QApplication::clipboard();
  203. const auto mimeData = clipboard->mimeData();
  204. const QString text = mimeData->text().trimmed();
  205. // No multi-line.
  206. if (!text.isEmpty() && !text.contains(QLatin1Char('\n'))) {
  207. if (text.contains(urlReg) && QUrl::fromUserInput(text).isValid()) {
  208. linkUrl = text;
  209. } else {
  210. linkText = text;
  211. }
  212. }
  213. }
  214. LinkInsertDialog dialog(tr("Insert Link"), linkText, linkUrl, false, this);
  215. if (dialog.exec() == QDialog::Accepted) {
  216. linkText = dialog.getLinkText();
  217. linkUrl = dialog.getLinkUrl();
  218. enterInsertModeIfApplicable();
  219. vte::MarkdownUtils::typeLink(m_textEdit, linkText, linkUrl);
  220. }
  221. }
  222. void MarkdownEditor::typeImage()
  223. {
  224. Q_ASSERT(m_buffer);
  225. ImageInsertDialog dialog(tr("Insert Image"), "", "", "", true, this);
  226. // Try fetch image from clipboard.
  227. {
  228. QClipboard *clipboard = QApplication::clipboard();
  229. const QMimeData *mimeData = clipboard->mimeData();
  230. QUrl url;
  231. if (mimeData->hasImage()) {
  232. QImage im = qvariant_cast<QImage>(mimeData->imageData());
  233. if (im.isNull()) {
  234. return;
  235. }
  236. dialog.setImage(im);
  237. dialog.setImageSource(ImageInsertDialog::Source::ImageData);
  238. } else if (mimeData->hasUrls()) {
  239. QList<QUrl> urls = mimeData->urls();
  240. if (urls.size() == 1) {
  241. url = urls[0];
  242. }
  243. } else if (mimeData->hasText()) {
  244. url = QUrl::fromUserInput(mimeData->text());
  245. }
  246. if (url.isValid()) {
  247. if (url.isLocalFile()) {
  248. dialog.setImagePath(url.toLocalFile());
  249. } else {
  250. dialog.setImagePath(url.toString());
  251. }
  252. }
  253. }
  254. if (dialog.exec() != QDialog::Accepted) {
  255. return;
  256. }
  257. enterInsertModeIfApplicable();
  258. if (dialog.getImageSource() == ImageInsertDialog::Source::LocalFile) {
  259. insertImageToBufferFromLocalFile(dialog.getImageTitle(),
  260. dialog.getImageAltText(),
  261. dialog.getImagePath(),
  262. dialog.getScaledWidth());
  263. } else {
  264. auto image = dialog.getImage();
  265. if (!image.isNull()) {
  266. insertImageToBufferFromData(dialog.getImageTitle(),
  267. dialog.getImageAltText(),
  268. image,
  269. dialog.getScaledWidth());
  270. }
  271. }
  272. }
  273. void MarkdownEditor::typeTable()
  274. {
  275. TableInsertDialog dialog(tr("Insert Table"), this);
  276. if (dialog.exec() != QDialog::Accepted) {
  277. return;
  278. }
  279. auto cursor = m_textEdit->textCursor();
  280. cursor.beginEditBlock();
  281. if (cursor.hasSelection()) {
  282. cursor.setPosition(qMax(cursor.selectionStart(), cursor.selectionEnd()));
  283. }
  284. bool newBlock = !cursor.atBlockEnd();
  285. if (!newBlock && !cursor.atBlockStart()) {
  286. QString text = cursor.block().text().trimmed();
  287. if (!text.isEmpty() && text != QStringLiteral(">")) {
  288. // Insert a new block before inserting table.
  289. newBlock = true;
  290. }
  291. }
  292. if (newBlock) {
  293. auto indentationStr = vte::TextEditUtils::fetchIndentationSpaces(cursor.block());
  294. vte::TextEditUtils::insertBlock(cursor, false);
  295. cursor.insertText(indentationStr);
  296. }
  297. cursor.endEditBlock();
  298. m_textEdit->setTextCursor(cursor);
  299. // Insert table.
  300. m_tableHelper->insertTable(dialog.getRowCount(), dialog.getColumnCount(), dialog.getAlignment());
  301. }
  302. void MarkdownEditor::setBuffer(Buffer *p_buffer)
  303. {
  304. m_buffer = p_buffer;
  305. }
  306. bool MarkdownEditor::insertImageToBufferFromLocalFile(const QString &p_title,
  307. const QString &p_altText,
  308. const QString &p_srcImagePath,
  309. int p_scaledWidth,
  310. int p_scaledHeight,
  311. bool p_insertText,
  312. QString *p_urlInLink)
  313. {
  314. auto destFileName = generateImageFileNameToInsertAs(p_title, QFileInfo(p_srcImagePath).suffix());
  315. QString destFilePath;
  316. if (m_imageHost) {
  317. // Save to image host.
  318. QByteArray ba;
  319. try {
  320. ba = FileUtils::readFile(p_srcImagePath);
  321. } catch (Exception &e) {
  322. MessageBoxHelper::notify(MessageBoxHelper::Warning,
  323. tr("Failed to read local image file (%1) (%2).").arg(p_srcImagePath, e.what()),
  324. this);
  325. return false;
  326. }
  327. destFilePath = saveToImageHost(ba, destFileName);
  328. if (destFilePath.isEmpty()) {
  329. return false;
  330. }
  331. } else {
  332. try {
  333. destFilePath = m_buffer->insertImage(p_srcImagePath, destFileName);
  334. } catch (Exception &e) {
  335. MessageBoxHelper::notify(MessageBoxHelper::Warning,
  336. tr("Failed to insert image from local file (%1) (%2).").arg(p_srcImagePath, e.what()),
  337. this);
  338. return false;
  339. }
  340. }
  341. insertImageLink(p_title, p_altText, destFilePath, p_scaledWidth, p_scaledHeight, p_insertText, p_urlInLink);
  342. return true;
  343. }
  344. QString MarkdownEditor::generateImageFileNameToInsertAs(const QString &p_title, const QString &p_suffix)
  345. {
  346. return FileUtils::generateRandomFileName(p_title, p_suffix);
  347. }
  348. bool MarkdownEditor::insertImageToBufferFromData(const QString &p_title,
  349. const QString &p_altText,
  350. const QImage &p_image,
  351. int p_scaledWidth,
  352. int p_scaledHeight)
  353. {
  354. // Save as PNG by default.
  355. const QString format("png");
  356. const auto destFileName = generateImageFileNameToInsertAs(p_title, format);
  357. QString destFilePath;
  358. if (m_imageHost) {
  359. // Save to image host.
  360. QByteArray ba;
  361. QBuffer buffer(&ba);
  362. buffer.open(QIODevice::WriteOnly);
  363. p_image.save(&buffer, format.toStdString().c_str());
  364. destFilePath = saveToImageHost(ba, destFileName);
  365. if (destFilePath.isEmpty()) {
  366. return false;
  367. }
  368. } else {
  369. try {
  370. destFilePath = m_buffer->insertImage(p_image, destFileName);
  371. } catch (Exception &e) {
  372. MessageBoxHelper::notify(MessageBoxHelper::Warning,
  373. tr("Failed to insert image from data (%1).").arg(e.what()),
  374. this);
  375. return false;
  376. }
  377. }
  378. insertImageLink(p_title, p_altText, destFilePath, p_scaledWidth, p_scaledHeight);
  379. return true;
  380. }
  381. void MarkdownEditor::insertImageLink(const QString &p_title,
  382. const QString &p_altText,
  383. const QString &p_destImagePath,
  384. int p_scaledWidth,
  385. int p_scaledHeight,
  386. bool p_insertText,
  387. QString *p_urlInLink)
  388. {
  389. const auto urlInLink = getRelativeLink(p_destImagePath);
  390. if (p_urlInLink) {
  391. *p_urlInLink = urlInLink;
  392. }
  393. static_cast<MarkdownBuffer *>(m_buffer)->addInsertedImage(p_destImagePath, urlInLink);
  394. if (p_insertText) {
  395. const auto imageLink = vte::MarkdownUtils::generateImageLink(p_title,
  396. urlInLink,
  397. p_altText,
  398. p_scaledWidth,
  399. p_scaledHeight);
  400. m_textEdit->insertPlainText(imageLink);
  401. }
  402. }
  403. void MarkdownEditor::handleCanInsertFromMimeData(const QMimeData *p_source, bool *p_handled, bool *p_allowed)
  404. {
  405. m_shouldTriggerRichPaste = ConfigMgr::getInst().getEditorConfig().getMarkdownEditorConfig().getRichPasteByDefaultEnabled();
  406. if (m_plainTextPasteAsked) {
  407. m_shouldTriggerRichPaste = false;
  408. return;
  409. }
  410. if (m_richPasteAsked) {
  411. m_shouldTriggerRichPaste = true;
  412. *p_handled = true;
  413. *p_allowed = true;
  414. return;
  415. }
  416. if (QGuiApplication::keyboardModifiers() == Qt::ShiftModifier) {
  417. m_shouldTriggerRichPaste = !m_shouldTriggerRichPaste;
  418. }
  419. if (m_shouldTriggerRichPaste) {
  420. *p_handled = true;
  421. *p_allowed = true;
  422. return;
  423. }
  424. if (p_source->hasImage()) {
  425. m_shouldTriggerRichPaste = true;
  426. *p_handled = true;
  427. *p_allowed = true;
  428. return;
  429. }
  430. if (p_source->hasUrls()) {
  431. *p_handled = true;
  432. *p_allowed = true;
  433. return;
  434. }
  435. }
  436. void MarkdownEditor::handleInsertFromMimeData(const QMimeData *p_source, bool *p_handled)
  437. {
  438. if (!m_shouldTriggerRichPaste) {
  439. // Default paste.
  440. // Give tips about the Rich Paste and Parse to Markdown And Paste features.
  441. VNoteX::getInst().showStatusMessageShort(
  442. tr("For advanced paste, try the \"Rich Paste\" and \"Parse to Markdown and Paste\" on the editor's context menu"));
  443. return;
  444. }
  445. m_shouldTriggerRichPaste = false;
  446. if (processHtmlFromMimeData(p_source)) {
  447. *p_handled = true;
  448. return;
  449. }
  450. if (processImageFromMimeData(p_source)) {
  451. *p_handled = true;
  452. return;
  453. }
  454. if (processUrlFromMimeData(p_source)) {
  455. *p_handled = true;
  456. return;
  457. }
  458. if (processMultipleUrlsFromMimeData(p_source)) {
  459. *p_handled = true;
  460. return;
  461. }
  462. }
  463. bool MarkdownEditor::processHtmlFromMimeData(const QMimeData *p_source)
  464. {
  465. if (!p_source->hasHtml()) {
  466. return false;
  467. }
  468. const QString html(p_source->html());
  469. // Process <img>.
  470. QRegularExpression reg("<img ([^>]*)src=\"([^\"]+)\"([^>]*)>");
  471. QRegularExpressionMatch match;
  472. if (html.indexOf(reg, 0, &match) != -1 && HtmlUtils::hasOnlyImgTag(html)) {
  473. if (p_source->hasImage()) {
  474. // Both image data and URL are embedded.
  475. SelectDialog dialog(tr("Insert From Clipboard"), this);
  476. dialog.addSelection(tr("Insert From URL"), 0);
  477. dialog.addSelection(tr("Insert From Image Data"), 1);
  478. dialog.addSelection(tr("Insert As Image Link"), 2);
  479. if (dialog.exec() == QDialog::Accepted) {
  480. int selection = dialog.getSelection();
  481. if (selection == 1) {
  482. // Insert from image data.
  483. insertImageFromMimeData(p_source);
  484. return true;
  485. } else if (selection == 2) {
  486. // Insert as link.
  487. auto imageLink = vte::MarkdownUtils::generateImageLink("", match.captured(2), "");
  488. m_textEdit->insertPlainText(imageLink);
  489. return true;
  490. }
  491. } else {
  492. return true;
  493. }
  494. }
  495. insertImageFromUrl(match.captured(2));
  496. return true;
  497. }
  498. return false;
  499. }
  500. bool MarkdownEditor::processImageFromMimeData(const QMimeData *p_source)
  501. {
  502. if (!p_source->hasImage()) {
  503. return false;
  504. }
  505. // Image url in the clipboard.
  506. if (p_source->hasText()) {
  507. SelectDialog dialog(tr("Insert From Clipboard"), this);
  508. dialog.addSelection(tr("Insert As Image"), 0);
  509. dialog.addSelection(tr("Insert As Text"), 1);
  510. dialog.addSelection(tr("Insert As Image Link"), 2);
  511. if (dialog.exec() == QDialog::Accepted) {
  512. int selection = dialog.getSelection();
  513. if (selection == 1) {
  514. // Insert as text.
  515. Q_ASSERT(p_source->hasText() && p_source->hasImage());
  516. m_textEdit->insertFromMimeDataOfBase(p_source);
  517. return true;
  518. } else if (selection == 2) {
  519. // Insert as link.
  520. auto imageLink = vte::MarkdownUtils::generateImageLink("", p_source->text(), "");
  521. m_textEdit->insertPlainText(imageLink);
  522. return true;
  523. }
  524. } else {
  525. return true;
  526. }
  527. }
  528. insertImageFromMimeData(p_source);
  529. return true;
  530. }
  531. bool MarkdownEditor::processUrlFromMimeData(const QMimeData *p_source)
  532. {
  533. const auto urls = p_source->urls();
  534. if (urls.size() > 1) {
  535. return false;
  536. }
  537. QUrl url;
  538. if (p_source->hasUrls()) {
  539. if (urls.size() == 1) {
  540. url = urls[0];
  541. }
  542. } else if (p_source->hasText()) {
  543. // Try to get URL from text.
  544. const QString text = p_source->text();
  545. if (QFileInfo::exists(text)) {
  546. url = QUrl::fromLocalFile(text);
  547. } else {
  548. url.setUrl(text);
  549. if (url.scheme() != QStringLiteral("https") && url.scheme() != QStringLiteral("http")) {
  550. url.clear();
  551. }
  552. }
  553. }
  554. if (!url.isValid()) {
  555. return false;
  556. }
  557. const bool isImage = PathUtils::isImageUrl(PathUtils::urlToPath(url));
  558. QString localFile = url.toLocalFile();
  559. if (!url.isLocalFile() || !QFileInfo::exists(localFile)) {
  560. localFile.clear();
  561. }
  562. bool isTextFile = false;
  563. if (!isImage && !localFile.isEmpty()) {
  564. const auto mimeType = QMimeDatabase().mimeTypeForFile(localFile);
  565. if (mimeType.isValid() && mimeType.inherits(QStringLiteral("text/plain"))) {
  566. isTextFile = true;
  567. }
  568. }
  569. SelectDialog dialog(tr("Insert From Clipboard"), this);
  570. if (isImage) {
  571. dialog.addSelection(tr("Insert As Image"), 0);
  572. dialog.addSelection(tr("Insert As Image Link"), 1);
  573. if (!localFile.isEmpty()) {
  574. dialog.addSelection(tr("Insert As Relative Image Link"), 7);
  575. }
  576. }
  577. dialog.addSelection(tr("Insert As Link"), 2);
  578. if (!localFile.isEmpty()) {
  579. dialog.addSelection(tr("Insert As Relative Link"), 3);
  580. if (m_buffer->isAttachmentSupported() && !m_buffer->isAttachment(localFile) && !PathUtils::isDir(localFile)) {
  581. dialog.addSelection(tr("Attach And Insert Link"), 6);
  582. }
  583. }
  584. dialog.addSelection(tr("Insert As Text"), 4);
  585. if (!localFile.isEmpty() && isTextFile) {
  586. dialog.addSelection(tr("Insert File Content"), 5);
  587. }
  588. // FIXME: After calling dialog.exec(), p_source->hasUrl() returns false.
  589. if (dialog.exec() == QDialog::Accepted) {
  590. bool relativeLink = false;
  591. switch (dialog.getSelection()) {
  592. case 0:
  593. {
  594. // Insert As Image.
  595. insertImageFromUrl(PathUtils::urlToPath(url));
  596. return true;
  597. }
  598. case 7:
  599. // Insert As Relative Image Link.
  600. relativeLink = true;
  601. Q_FALLTHROUGH();
  602. case 1:
  603. {
  604. // Insert As Image Link.
  605. QString urlInLink;
  606. if (relativeLink) {
  607. urlInLink = getRelativeLink(localFile);
  608. } else {
  609. urlInLink = url.toString(QUrl::EncodeSpaces);
  610. }
  611. enterInsertModeIfApplicable();
  612. const auto imageLink = vte::MarkdownUtils::generateImageLink("", urlInLink, "");
  613. m_textEdit->insertPlainText(imageLink);
  614. return true;
  615. }
  616. case 6:
  617. {
  618. // Attach And Insert Link.
  619. QStringList fileList;
  620. fileList << localFile;
  621. fileList = m_buffer->addAttachment(QString(), fileList);
  622. // Update localFile to point to the attachment file.
  623. localFile = fileList[0];
  624. Q_FALLTHROUGH();
  625. }
  626. case 3:
  627. // Insert As Relative link.
  628. relativeLink = true;
  629. Q_FALLTHROUGH();
  630. case 2:
  631. {
  632. // Insert As Link.
  633. QString linkText;
  634. if (!localFile.isEmpty()) {
  635. linkText = QFileInfo(localFile).fileName();
  636. }
  637. QString linkUrl;
  638. if (relativeLink) {
  639. Q_ASSERT(!localFile.isEmpty());
  640. linkUrl = getRelativeLink(localFile);
  641. } else {
  642. linkUrl = url.toString(QUrl::EncodeSpaces);
  643. }
  644. LinkInsertDialog linkDialog(tr("Insert Link"), linkText, linkUrl, false, this);
  645. if (linkDialog.exec() == QDialog::Accepted) {
  646. linkText = linkDialog.getLinkText();
  647. linkUrl = linkDialog.getLinkUrl();
  648. enterInsertModeIfApplicable();
  649. vte::MarkdownUtils::typeLink(m_textEdit, linkText, linkUrl);
  650. }
  651. return true;
  652. }
  653. case 4:
  654. {
  655. // Insert As Text.
  656. enterInsertModeIfApplicable();
  657. if (p_source->hasText()) {
  658. m_textEdit->insertPlainText(p_source->text());
  659. } else {
  660. m_textEdit->insertPlainText(url.toString());
  661. }
  662. return true;
  663. }
  664. case 5:
  665. {
  666. // Insert File Content.
  667. Q_ASSERT(!localFile.isEmpty() && isTextFile);
  668. enterInsertModeIfApplicable();
  669. m_textEdit->insertPlainText(FileUtils::readTextFile(localFile));
  670. return true;
  671. }
  672. default:
  673. Q_ASSERT(false);
  674. break;
  675. }
  676. } else {
  677. // Nothing happens.
  678. return true;
  679. }
  680. return false;
  681. }
  682. bool MarkdownEditor::processMultipleUrlsFromMimeData(const QMimeData *p_source) {
  683. const auto urls = p_source->urls();
  684. if (urls.size() <= 1) {
  685. return false;
  686. }
  687. bool isProcessed = false;
  688. // Judgment if all QMimeData are images.
  689. bool isAllImage = true;
  690. for (const QUrl &url : urls) {
  691. if (!PathUtils::isImageUrl(PathUtils::urlToPath(url))) {
  692. isAllImage = false;
  693. break;
  694. }
  695. }
  696. SelectDialog dialog(tr("Insert From Clipboard (%n items)", "", urls.size()), this);
  697. if (isAllImage) {
  698. dialog.addSelection(tr("Insert As Image"), 0);
  699. }
  700. if (m_buffer->isAttachmentSupported()) {
  701. dialog.addSelection(tr("Attach And Insert Link"), 1);
  702. }
  703. dialog.setMinimumWidth(400);
  704. if (dialog.exec() == QDialog::Accepted) {
  705. switch (dialog.getSelection()) {
  706. case 0:
  707. {
  708. // Insert As Image.
  709. for (const QUrl &url : urls) {
  710. insertImageFromUrl(PathUtils::urlToPath(url), true);
  711. m_textEdit->insertPlainText("\n\n");
  712. }
  713. isProcessed = true;
  714. break;
  715. }
  716. case 1:
  717. {
  718. // Attach And Insert Link.
  719. QStringList fileList;
  720. for (const QUrl &url : urls) {
  721. fileList << url.toLocalFile();
  722. }
  723. fileList = m_buffer->addAttachment(QString(), fileList);
  724. enterInsertModeIfApplicable();
  725. for (int i = 0; i < fileList.length(); ++i) {
  726. vte::MarkdownUtils::typeLink(
  727. m_textEdit, QFileInfo(fileList[i]).fileName(),
  728. getRelativeLink(fileList[i]));
  729. m_textEdit->insertPlainText("\n\n");
  730. }
  731. isProcessed = true;
  732. break;
  733. }
  734. }
  735. }
  736. return isProcessed;
  737. }
  738. void MarkdownEditor::insertImageFromMimeData(const QMimeData *p_source)
  739. {
  740. QImage image = qvariant_cast<QImage>(p_source->imageData());
  741. if (image.isNull()) {
  742. return;
  743. }
  744. ImageInsertDialog dialog(tr("Insert Image From Clipboard"), "", "", "", false, this);
  745. dialog.setImage(image);
  746. if (dialog.exec() == QDialog::Accepted) {
  747. enterInsertModeIfApplicable();
  748. insertImageToBufferFromData(dialog.getImageTitle(),
  749. dialog.getImageAltText(),
  750. image,
  751. dialog.getScaledWidth());
  752. }
  753. }
  754. void MarkdownEditor::insertImageFromUrl(const QString &p_url, bool p_quiet)
  755. {
  756. if (p_quiet) {
  757. insertImageToBufferFromLocalFile("", "", p_url, 0);
  758. } else {
  759. ImageInsertDialog dialog(tr("Insert Image From URL"), "", "", "", false, this);
  760. dialog.setImagePath(p_url);
  761. if (dialog.exec() == QDialog::Accepted) {
  762. enterInsertModeIfApplicable();
  763. if (dialog.getImageSource() == ImageInsertDialog::Source::LocalFile) {
  764. insertImageToBufferFromLocalFile(dialog.getImageTitle(),
  765. dialog.getImageAltText(),
  766. dialog.getImagePath(),
  767. dialog.getScaledWidth());
  768. } else {
  769. auto image = dialog.getImage();
  770. if (!image.isNull()) {
  771. insertImageToBufferFromData(dialog.getImageTitle(),
  772. dialog.getImageAltText(),
  773. image,
  774. dialog.getScaledWidth());
  775. }
  776. }
  777. }
  778. }
  779. }
  780. QString MarkdownEditor::getRelativeLink(const QString &p_path)
  781. {
  782. if (PathUtils::isLocalFile(p_path)) {
  783. auto relativePath = PathUtils::relativePath(PathUtils::parentDirPath(m_buffer->getContentPath()), p_path);
  784. auto link = PathUtils::encodeSpacesInPath(QDir::fromNativeSeparators(relativePath));
  785. if (m_config.getPrependDotInRelativeLink()) {
  786. PathUtils::prependDotIfRelative(link);
  787. }
  788. return link;
  789. } else {
  790. return p_path;
  791. }
  792. }
  793. const QVector<MarkdownEditor::Heading> &MarkdownEditor::getHeadings() const
  794. {
  795. return m_headings;
  796. }
  797. int MarkdownEditor::getCurrentHeadingIndex() const
  798. {
  799. int blockNumber = m_textEdit->textCursor().blockNumber();
  800. return getHeadingIndexByBlockNumber(blockNumber);
  801. }
  802. void MarkdownEditor::updateHeadings(const QVector<vte::peg::ElementRegion> &p_headerRegions)
  803. {
  804. bool needUpdateSectionNumber = false;
  805. if (isReadOnly()) {
  806. m_sectionNumberEnabled = false;
  807. } else {
  808. needUpdateSectionNumber = m_config.getSectionNumberMode() == MarkdownEditorConfig::SectionNumberMode::Edit;
  809. if (m_overriddenSectionNumber != OverrideState::NoOverride) {
  810. needUpdateSectionNumber = m_overriddenSectionNumber == OverrideState::ForceEnable;
  811. }
  812. if (needUpdateSectionNumber) {
  813. m_sectionNumberEnabled = true;
  814. } else if (m_sectionNumberEnabled) {
  815. // On -> Off. We still need to do the clean up.
  816. needUpdateSectionNumber = true;
  817. m_sectionNumberEnabled = false;
  818. }
  819. }
  820. QVector<Heading> headings;
  821. headings.reserve(p_headerRegions.size());
  822. // Assume that each block contains only one line.
  823. // Only support # syntax for now.
  824. auto doc = document();
  825. for (auto const &reg : p_headerRegions) {
  826. auto block = doc->findBlock(reg.m_startPos);
  827. if (!block.isValid()) {
  828. continue;
  829. }
  830. if (!block.contains(reg.m_endPos - 1)) {
  831. qWarning() << "header accross multiple blocks, starting from block" << block.blockNumber() << block.text();
  832. }
  833. auto match = vte::MarkdownUtils::matchHeader(block.text());
  834. if (match.m_matched) {
  835. Heading heading(match.m_header,
  836. match.m_level,
  837. match.m_sequence,
  838. block.blockNumber());
  839. headings.append(heading);
  840. }
  841. }
  842. OutlineProvider::makePerfectHeadings(headings, m_headings);
  843. if (needUpdateSectionNumber) {
  844. // Use a timer to kick off the update to let user have time to undo.
  845. m_sectionNumberTimer->start();
  846. }
  847. emit headingsChanged();
  848. emit currentHeadingChanged();
  849. }
  850. int MarkdownEditor::getHeadingIndexByBlockNumber(int p_blockNumber) const
  851. {
  852. if (m_headings.isEmpty()) {
  853. return -1;
  854. }
  855. int left = 0, right = m_headings.size() - 1;
  856. while (left < right) {
  857. int mid = left + (right - left + 1) / 2;
  858. int val = m_headings[mid].m_blockNumber;
  859. if (val == -1) {
  860. // Search to right.
  861. for (int i = mid + 1; i <= right; ++i) {
  862. if (m_headings[i].m_blockNumber != -1) {
  863. mid = i;
  864. val = m_headings[i].m_blockNumber;
  865. break;
  866. }
  867. }
  868. if (val == -1) {
  869. // Search to left.
  870. for (int i = mid - 1; i >= left; --i) {
  871. if (m_headings[i].m_blockNumber != -1) {
  872. mid = i;
  873. val = m_headings[i].m_blockNumber;
  874. break;
  875. }
  876. }
  877. }
  878. }
  879. if (val == -1) {
  880. // No more valid values.
  881. break;
  882. }
  883. if (val == p_blockNumber) {
  884. return mid;
  885. } else if (val > p_blockNumber) {
  886. // Skip the -1 headings.
  887. // Bad case: [0, 2, 3, 43, 44, -1, 46, 60].
  888. // If not skipped, [left, right] will be stuck at [4, 5].
  889. right = mid - 1;
  890. while (right >= left && m_headings[right].m_blockNumber == -1) {
  891. --right;
  892. }
  893. } else {
  894. left = mid;
  895. }
  896. }
  897. if (m_headings[left].m_blockNumber <= p_blockNumber && m_headings[left].m_blockNumber != -1) {
  898. return left;
  899. }
  900. return -1;
  901. }
  902. void MarkdownEditor::scrollToHeading(int p_idx)
  903. {
  904. if (p_idx < 0 || p_idx >= m_headings.size()) {
  905. return;
  906. }
  907. if (m_headings[p_idx].m_blockNumber == -1) {
  908. return;
  909. }
  910. scrollToLine(m_headings[p_idx].m_blockNumber, true);
  911. }
  912. void MarkdownEditor::handleContextMenuEvent(QContextMenuEvent *p_event, bool *p_handled, QScopedPointer<QMenu> *p_menu)
  913. {
  914. const auto &editorConfig = ConfigMgr::getInst().getEditorConfig();
  915. *p_handled = true;
  916. p_menu->reset(m_textEdit->createStandardContextMenu(p_event->pos()));
  917. auto menu = p_menu->data();
  918. const auto actions = menu->actions();
  919. QAction *firstAct = actions.isEmpty() ? nullptr : actions.first();
  920. // QAction *copyAct = WidgetUtils::findActionByObjectName(actions, "edit-copy");
  921. QAction *pasteAct = WidgetUtils::findActionByObjectName(actions, "edit-paste");
  922. const bool hasSelection = m_textEdit->hasSelection();
  923. if (!hasSelection) {
  924. auto readAct = new QAction(tr("&Read"), menu);
  925. WidgetUtils::addActionShortcutText(readAct, editorConfig.getShortcut(EditorConfig::Shortcut::EditRead));
  926. connect(readAct, &QAction::triggered,
  927. this, &MarkdownEditor::readRequested);
  928. menu->insertAction(firstAct, readAct);
  929. if (firstAct) {
  930. menu->insertSeparator(firstAct);
  931. }
  932. prependContextSensitiveMenu(menu, p_event->pos());
  933. }
  934. if (pasteAct && pasteAct->isEnabled()) {
  935. QClipboard *clipboard = QApplication::clipboard();
  936. const QMimeData *mimeData = clipboard->mimeData();
  937. // Rich Paste or Plain Text Paste.
  938. const bool richPasteByDefault = editorConfig.getMarkdownEditorConfig().getRichPasteByDefaultEnabled();
  939. auto altPasteAct = new QAction(richPasteByDefault ? tr("Paste as Plain Text") : tr("Rich Paste"), menu);
  940. WidgetUtils::addActionShortcutText(altPasteAct,
  941. editorConfig.getShortcut(EditorConfig::Shortcut::AltPaste));
  942. connect(altPasteAct, &QAction::triggered,
  943. this, &MarkdownEditor::altPaste);
  944. WidgetUtils::insertActionAfter(menu, pasteAct, altPasteAct);
  945. if (mimeData->hasHtml()) {
  946. // Parse to Markdown and Paste.
  947. auto parsePasteAct = new QAction(tr("Parse to Markdown and Paste"), menu);
  948. WidgetUtils::addActionShortcutText(parsePasteAct,
  949. editorConfig.getShortcut(EditorConfig::Shortcut::ParseToMarkdownAndPaste));
  950. connect(parsePasteAct, &QAction::triggered,
  951. this, &MarkdownEditor::parseToMarkdownAndPaste);
  952. WidgetUtils::insertActionAfter(menu, altPasteAct, parsePasteAct);
  953. }
  954. }
  955. {
  956. menu->addSeparator();
  957. auto snippetAct = menu->addAction(tr("Insert Snippet"), this, &MarkdownEditor::applySnippetRequested);
  958. WidgetUtils::addActionShortcutText(snippetAct,
  959. editorConfig.getShortcut(EditorConfig::Shortcut::ApplySnippet));
  960. }
  961. if (!hasSelection) {
  962. appendImageHostMenu(menu);
  963. }
  964. appendSpellCheckMenu(p_event, menu);
  965. }
  966. void MarkdownEditor::altPaste()
  967. {
  968. const bool richPasteByDefault = ConfigMgr::getInst().getEditorConfig().getMarkdownEditorConfig().getRichPasteByDefaultEnabled();
  969. if (richPasteByDefault) {
  970. // Paste as plain text.
  971. m_plainTextPasteAsked = true;
  972. m_richPasteAsked = false;
  973. } else {
  974. // Rich paste.
  975. m_plainTextPasteAsked = false;
  976. m_richPasteAsked = true;
  977. }
  978. // handleCanInsertFromMimeData() is called before this function. Call it manually.
  979. if (m_textEdit->canPaste()) {
  980. m_textEdit->paste();
  981. }
  982. m_plainTextPasteAsked = false;
  983. m_richPasteAsked = false;
  984. }
  985. void MarkdownEditor::setupShortcuts()
  986. {
  987. const auto &editorConfig = ConfigMgr::getInst().getEditorConfig();
  988. // Alt paste.
  989. {
  990. auto shortcut = WidgetUtils::createShortcut(editorConfig.getShortcut(EditorConfig::Shortcut::AltPaste),
  991. this);
  992. if (shortcut) {
  993. connect(shortcut, &QShortcut::activated,
  994. this, &MarkdownEditor::altPaste);
  995. }
  996. }
  997. // Parse to Markdown and Paste.
  998. {
  999. auto shortcut = WidgetUtils::createShortcut(editorConfig.getShortcut(EditorConfig::Shortcut::ParseToMarkdownAndPaste),
  1000. this);
  1001. if (shortcut) {
  1002. connect(shortcut, &QShortcut::activated,
  1003. this, &MarkdownEditor::parseToMarkdownAndPaste);
  1004. }
  1005. }
  1006. }
  1007. void MarkdownEditor::parseToMarkdownAndPaste()
  1008. {
  1009. if (isReadOnly()) {
  1010. return;
  1011. }
  1012. QClipboard *clipboard = QApplication::clipboard();
  1013. const QMimeData *mimeData = clipboard->mimeData();
  1014. QString html(mimeData->html());
  1015. if (!html.isEmpty()) {
  1016. emit htmlToMarkdownRequested(0, ++m_timeStamp, html);
  1017. }
  1018. }
  1019. void MarkdownEditor::handleHtmlToMarkdownData(quint64 p_id, TimeStamp p_timeStamp, const QString &p_text)
  1020. {
  1021. Q_UNUSED(p_id);
  1022. if (m_timeStamp == p_timeStamp && !p_text.isEmpty()) {
  1023. QString text(p_text);
  1024. const auto &editorConfig = ConfigMgr::getInst().getEditorConfig().getMarkdownEditorConfig();
  1025. if (editorConfig.getFetchImagesInParseAndPaste()) {
  1026. fetchImagesToLocalAndReplace(text);
  1027. }
  1028. insertText(text);
  1029. }
  1030. }
  1031. static QString purifyImageTitle(QString p_title)
  1032. {
  1033. return p_title.remove(QRegularExpression("[\\r\\n\\[\\]]"));
  1034. }
  1035. void MarkdownEditor::fetchImagesToLocalAndReplace(QString &p_text)
  1036. {
  1037. auto regs = vte::MarkdownUtils::fetchImageRegionsViaParser(p_text);
  1038. if (regs.isEmpty()) {
  1039. return;
  1040. }
  1041. // Sort it in ascending order.
  1042. std::sort(regs.begin(), regs.end());
  1043. QProgressDialog proDlg(tr("Fetching images to local..."),
  1044. tr("Abort"),
  1045. 0,
  1046. regs.size(),
  1047. this);
  1048. proDlg.setWindowModality(Qt::WindowModal);
  1049. proDlg.setWindowTitle(tr("Fetch Images To Local"));
  1050. QRegularExpression zhihuRegExp("^https?://www\\.zhihu\\.com/equation\\?tex=(.+)$");
  1051. QRegularExpression regExp(vte::MarkdownUtils::c_imageLinkRegExp);
  1052. for (int i = regs.size() - 1; i >= 0; --i) {
  1053. proDlg.setValue(regs.size() - 1 - i);
  1054. if (proDlg.wasCanceled()) {
  1055. break;
  1056. }
  1057. const auto &reg = regs[i];
  1058. QString linkText = p_text.mid(reg.m_startPos, reg.m_endPos - reg.m_startPos);
  1059. QRegularExpressionMatch match;
  1060. if (linkText.indexOf(regExp, 0, &match) == -1) {
  1061. continue;
  1062. }
  1063. qDebug() << "fetching image link" << linkText;
  1064. const QString imageTitle = purifyImageTitle(match.captured(1).trimmed());
  1065. QString imageUrl = match.captured(2).trimmed();
  1066. const int maxUrlLength = 100;
  1067. QString urlToDisplay(imageUrl);
  1068. if (urlToDisplay.size() > maxUrlLength) {
  1069. urlToDisplay = urlToDisplay.left(maxUrlLength) + "...";
  1070. }
  1071. proDlg.setLabelText(tr("Fetching image (%1)").arg(urlToDisplay));
  1072. // Handle equation from zhihu.com like http://www.zhihu.com/equation?tex=P.
  1073. QRegularExpressionMatch zhihuMatch;
  1074. if (imageUrl.indexOf(zhihuRegExp, 0, &zhihuMatch) != -1) {
  1075. QString tex = zhihuMatch.captured(1).trimmed();
  1076. // Remove the +.
  1077. tex.replace(QChar('+'), " ");
  1078. tex = QUrl::fromPercentEncoding(tex.toUtf8());
  1079. if (tex.isEmpty()) {
  1080. continue;
  1081. }
  1082. tex = "$" + tex + "$";
  1083. p_text.replace(reg.m_startPos,
  1084. reg.m_endPos - reg.m_startPos,
  1085. tex);
  1086. continue;
  1087. }
  1088. // Only handle absolute file path or network path.
  1089. QString srcImagePath;
  1090. QFileInfo info(WebUtils::purifyUrl(imageUrl));
  1091. // For network image.
  1092. QScopedPointer<QTemporaryFile> tmpFile;
  1093. if (info.exists()) {
  1094. if (info.isAbsolute()) {
  1095. // Absolute local path.
  1096. srcImagePath = info.absoluteFilePath();
  1097. }
  1098. } else {
  1099. // Network path.
  1100. // Prepend the protocol if missing.
  1101. if (imageUrl.startsWith(QStringLiteral("//"))) {
  1102. imageUrl.prepend(QStringLiteral("https:"));
  1103. }
  1104. QByteArray data = vte::NetworkAccess::request(QUrl(imageUrl)).m_data;
  1105. if (!data.isEmpty()) {
  1106. // Prefer the suffix from the real data.
  1107. auto suffix = ImageUtils::guessImageSuffix(data);
  1108. if (suffix.isEmpty()) {
  1109. suffix = info.suffix();
  1110. } else if (info.suffix() != suffix) {
  1111. qWarning() << "guess a different suffix from image data" << info.suffix() << suffix;
  1112. }
  1113. tmpFile.reset(FileUtils::createTemporaryFile(suffix));
  1114. if (tmpFile->open() && tmpFile->write(data) > -1) {
  1115. srcImagePath = tmpFile->fileName();
  1116. }
  1117. // Need to close it explicitly to flush cache of small file.
  1118. tmpFile->close();
  1119. }
  1120. }
  1121. if (srcImagePath.isEmpty()) {
  1122. continue;
  1123. }
  1124. // Insert image without inserting text.
  1125. QString urlInLink;
  1126. bool ret = insertImageToBufferFromLocalFile(imageTitle,
  1127. QString(),
  1128. srcImagePath,
  1129. 0,
  1130. 0,
  1131. false,
  1132. &urlInLink);
  1133. if (!ret || urlInLink.isEmpty()) {
  1134. continue;
  1135. }
  1136. // Replace URL in link.
  1137. QString newLink = QString("![%1](%2%3%4)")
  1138. .arg(imageTitle, urlInLink, match.captured(3), match.captured(6));
  1139. p_text.replace(reg.m_startPos,
  1140. reg.m_endPos - reg.m_startPos,
  1141. newLink);
  1142. }
  1143. proDlg.setValue(regs.size());
  1144. }
  1145. static bool updateHeadingSectionNumber(QTextCursor &p_cursor,
  1146. const QTextBlock &p_block,
  1147. const QString &p_sectionNumber,
  1148. bool p_endingDot)
  1149. {
  1150. if (!p_block.isValid()) {
  1151. return false;
  1152. }
  1153. QString text = p_block.text();
  1154. auto match = vte::MarkdownUtils::matchHeader(text);
  1155. Q_ASSERT(match.m_matched);
  1156. bool isSequence = false;
  1157. if (!match.m_sequence.isEmpty()) {
  1158. // Check if this sequence is the real sequence matching current style.
  1159. if (match.m_sequence.endsWith('.')) {
  1160. isSequence = p_endingDot;
  1161. } else {
  1162. isSequence = !p_endingDot;
  1163. }
  1164. }
  1165. int start = match.m_level + 1;
  1166. int end = match.m_level + match.m_spacesAfterMarker;
  1167. if (isSequence) {
  1168. end += match.m_sequence.size() + match.m_spacesAfterSequence;
  1169. }
  1170. Q_ASSERT(start <= end);
  1171. p_cursor.setPosition(p_block.position() + start);
  1172. if (start != end) {
  1173. p_cursor.setPosition(p_block.position() + end, QTextCursor::KeepAnchor);
  1174. }
  1175. if (p_sectionNumber.isEmpty()) {
  1176. p_cursor.removeSelectedText();
  1177. } else {
  1178. p_cursor.insertText(p_sectionNumber + ' ');
  1179. }
  1180. return true;
  1181. }
  1182. bool MarkdownEditor::updateSectionNumber(const QVector<Heading> &p_headings)
  1183. {
  1184. SectionNumber sectionNumber(7, 0);
  1185. int baseLevel = m_config.getSectionNumberBaseLevel();
  1186. if (baseLevel < 1 || baseLevel > 6) {
  1187. baseLevel = 1;
  1188. }
  1189. bool changed = false;
  1190. bool endingDot = m_config.getSectionNumberStyle() == MarkdownEditorConfig::SectionNumberStyle::DigDotDigDot;
  1191. auto doc = document();
  1192. QTextCursor cursor(doc);
  1193. cursor.beginEditBlock();
  1194. for (const auto &heading : p_headings) {
  1195. OutlineProvider::increaseSectionNumber(sectionNumber, heading.m_level, baseLevel);
  1196. auto sectionStr = m_sectionNumberEnabled ? OutlineProvider::joinSectionNumber(sectionNumber, endingDot) : QString();
  1197. if (heading.m_blockNumber > -1 && sectionStr != heading.m_sectionNumber) {
  1198. if (updateHeadingSectionNumber(cursor,
  1199. doc->findBlockByNumber(heading.m_blockNumber),
  1200. sectionStr,
  1201. endingDot)) {
  1202. changed = true;
  1203. }
  1204. }
  1205. }
  1206. cursor.endEditBlock();
  1207. return changed;
  1208. }
  1209. void MarkdownEditor::overrideSectionNumber(OverrideState p_state)
  1210. {
  1211. if (m_overriddenSectionNumber == p_state) {
  1212. return;
  1213. }
  1214. m_overriddenSectionNumber = p_state;
  1215. getHighlighter()->updateHighlight();
  1216. }
  1217. void MarkdownEditor::updateFromConfig(bool p_initialized)
  1218. {
  1219. if (m_config.getTextEditorConfig().getZoomDelta() != 0) {
  1220. zoom(m_config.getTextEditorConfig().getZoomDelta());
  1221. }
  1222. if (p_initialized) {
  1223. getHighlighter()->updateHighlight();
  1224. }
  1225. }
  1226. void MarkdownEditor::setupTableHelper()
  1227. {
  1228. m_tableHelper = new MarkdownTableHelper(this, this);
  1229. connect(getHighlighter(), &vte::PegMarkdownHighlighter::tableBlocksUpdated,
  1230. m_tableHelper, &MarkdownTableHelper::updateTableBlocks);
  1231. }
  1232. QRgb MarkdownEditor::getPreviewBackground() const
  1233. {
  1234. auto th = theme();
  1235. const auto &fmt = th->editorStyle(vte::Theme::EditorStyle::Preview);
  1236. return fmt.m_backgroundColor;
  1237. }
  1238. void MarkdownEditor::setImageHost(ImageHost *p_host)
  1239. {
  1240. // It may be different than the global default image host.
  1241. m_imageHost = p_host;
  1242. }
  1243. static QString generateImageHostFileName(const Buffer *p_buffer, const QString &p_destFileName)
  1244. {
  1245. auto destPath = ImageHostUtils::generateRelativePath(p_buffer);
  1246. if (destPath.isEmpty()) {
  1247. destPath = p_destFileName;
  1248. } else {
  1249. destPath += "/" + p_destFileName;
  1250. }
  1251. return destPath;
  1252. }
  1253. QString MarkdownEditor::saveToImageHost(const QByteArray &p_imageData, const QString &p_destFileName)
  1254. {
  1255. Q_ASSERT(m_imageHost);
  1256. const auto destPath = generateImageHostFileName(m_buffer, p_destFileName);
  1257. QString errMsg;
  1258. QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
  1259. auto targetUrl = m_imageHost->create(p_imageData, destPath, errMsg);
  1260. QApplication::restoreOverrideCursor();
  1261. if (targetUrl.isEmpty()) {
  1262. MessageBoxHelper::notify(MessageBoxHelper::Warning,
  1263. tr("Failed to upload image to image host (%1) as (%2).").arg(m_imageHost->getName(), destPath),
  1264. QString(),
  1265. errMsg,
  1266. this);
  1267. }
  1268. return targetUrl;
  1269. }
  1270. void MarkdownEditor::appendImageHostMenu(QMenu *p_menu)
  1271. {
  1272. p_menu->addSeparator();
  1273. auto subMenu = p_menu->addMenu(tr("Upload Images To Image Host"));
  1274. const auto &hosts = ImageHostMgr::getInst().getImageHosts();
  1275. if (hosts.isEmpty()) {
  1276. auto act = subMenu->addAction(tr("None"));
  1277. act->setEnabled(false);
  1278. return;
  1279. }
  1280. for (const auto &host : hosts) {
  1281. auto act = subMenu->addAction(host->getName(),
  1282. this,
  1283. &MarkdownEditor::uploadImagesToImageHost);
  1284. act->setData(host->getName());
  1285. }
  1286. }
  1287. void MarkdownEditor::uploadImagesToImageHost()
  1288. {
  1289. auto act = static_cast<QAction *>(sender());
  1290. auto host = ImageHostMgr::getInst().find(act->data().toString());
  1291. Q_ASSERT(host);
  1292. // Only LocalRelativeInternal images.
  1293. // Descending order of the link position.
  1294. auto images = vte::MarkdownUtils::fetchImagesFromMarkdownText(m_buffer->getContent(),
  1295. m_buffer->getResourcePath(),
  1296. vte::MarkdownLink::TypeFlag::LocalRelativeInternal);
  1297. if (images.isEmpty()) {
  1298. return;
  1299. }
  1300. QProgressDialog proDlg(tr("Uploading local images..."),
  1301. tr("Abort"),
  1302. 0,
  1303. images.size(),
  1304. this);
  1305. proDlg.setWindowModality(Qt::WindowModal);
  1306. proDlg.setWindowTitle(tr("Upload Images To Image Host"));
  1307. QHash<QString, QString> uploadedImages;
  1308. int cnt = 0;
  1309. auto cursor = m_textEdit->textCursor();
  1310. cursor.beginEditBlock();
  1311. for (int i = 0; i < images.size(); ++i) {
  1312. const auto &link = images[i];
  1313. auto it = uploadedImages.find(link.m_path);
  1314. if (it != uploadedImages.end()) {
  1315. cursor.setPosition(link.m_urlInLinkPos);
  1316. cursor.setPosition(link.m_urlInLinkPos + link.m_urlInLink.size(), QTextCursor::KeepAnchor);
  1317. cursor.insertText(it.value());
  1318. continue;
  1319. }
  1320. proDlg.setValue(i + 1);
  1321. if (proDlg.wasCanceled()) {
  1322. break;
  1323. }
  1324. proDlg.setLabelText(tr("Upload image (%1)").arg(link.m_path));
  1325. Q_ASSERT(i == 0 || link.m_urlInLinkPos < images[i - 1].m_urlInLinkPos);
  1326. QByteArray ba;
  1327. try {
  1328. ba = FileUtils::readFile(link.m_path);
  1329. } catch (Exception &e) {
  1330. MessageBoxHelper::notify(MessageBoxHelper::Warning,
  1331. tr("Failed to read local image file (%1) (%2).").arg(link.m_path, e.what()),
  1332. this);
  1333. continue;
  1334. }
  1335. if (ba.isEmpty()) {
  1336. qWarning() << "Skipped uploading empty image" << link.m_path;
  1337. continue;
  1338. }
  1339. const auto destPath = generateImageHostFileName(m_buffer, PathUtils::fileName(link.m_path));
  1340. QString errMsg;
  1341. QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
  1342. const auto targetUrl = host->create(ba, destPath, errMsg);
  1343. QApplication::restoreOverrideCursor();
  1344. if (targetUrl.isEmpty()) {
  1345. MessageBoxHelper::notify(MessageBoxHelper::Warning,
  1346. tr("Failed to upload image to image host (%1) as (%2).").arg(host->getName(), destPath),
  1347. QString(),
  1348. errMsg,
  1349. this);
  1350. continue;
  1351. }
  1352. // Update the link URL.
  1353. cursor.setPosition(link.m_urlInLinkPos);
  1354. cursor.setPosition(link.m_urlInLinkPos + link.m_urlInLink.size(), QTextCursor::KeepAnchor);
  1355. cursor.insertText(targetUrl);
  1356. uploadedImages.insert(link.m_path, targetUrl);
  1357. ++cnt;
  1358. }
  1359. cursor.endEditBlock();
  1360. proDlg.setValue(images.size());
  1361. if (cnt > 0) {
  1362. m_textEdit->setTextCursor(cursor);
  1363. }
  1364. }
  1365. void MarkdownEditor::prependContextSensitiveMenu(QMenu *p_menu, const QPoint &p_pos)
  1366. {
  1367. auto cursor = m_textEdit->cursorForPosition(p_pos);
  1368. const int pos = cursor.position();
  1369. const auto block = cursor.block();
  1370. Q_ASSERT(!p_menu->isEmpty());
  1371. auto firstAct = p_menu->actions().at(0);
  1372. bool ret = prependImageMenu(p_menu, firstAct, pos, block);
  1373. if (ret) {
  1374. return;
  1375. }
  1376. ret = prependLinkMenu(p_menu, firstAct, pos, block);
  1377. if (ret) {
  1378. return;
  1379. }
  1380. if (prependInPlacePreviewMenu(p_menu, firstAct, pos, block)) {
  1381. p_menu->insertSeparator(firstAct);
  1382. }
  1383. }
  1384. bool MarkdownEditor::prependImageMenu(QMenu *p_menu, QAction *p_before, int p_cursorPos, const QTextBlock &p_block)
  1385. {
  1386. const auto text = p_block.text();
  1387. if (!vte::MarkdownUtils::hasImageLink(text)) {
  1388. return false;
  1389. }
  1390. QString imgPath;
  1391. const auto &regions = getHighlighter()->getImageRegions();
  1392. for (const auto &reg : regions) {
  1393. if (!reg.contains(p_cursorPos) && (!reg.contains(p_cursorPos - 1) || p_cursorPos != p_block.position() + text.size())) {
  1394. continue;
  1395. }
  1396. if (reg.m_endPos > p_block.position() + text.size()) {
  1397. return true;
  1398. }
  1399. const auto linkText = text.mid(reg.m_startPos - p_block.position(), reg.m_endPos - reg.m_startPos);
  1400. int linkWidth = 0;
  1401. int linkHeight = 0;
  1402. const auto shortUrl = vte::MarkdownUtils::fetchImageLinkUrl(linkText, linkWidth, linkHeight);
  1403. if (shortUrl.isEmpty()) {
  1404. return true;
  1405. }
  1406. imgPath = vte::MarkdownUtils::linkUrlToPath(getBasePath(), shortUrl);
  1407. break;
  1408. }
  1409. {
  1410. auto act = new QAction(tr("View Image"), p_menu);
  1411. connect(act, &QAction::triggered,
  1412. p_menu, [imgPath]() {
  1413. WidgetUtils::openUrlByDesktop(PathUtils::pathToUrl(imgPath));
  1414. });
  1415. p_menu->insertAction(p_before, act);
  1416. }
  1417. {
  1418. auto act = new QAction(tr("Copy Image URL"), p_menu);
  1419. connect(act, &QAction::triggered,
  1420. p_menu, [imgPath]() {
  1421. ClipboardUtils::setLinkToClipboard(imgPath);
  1422. });
  1423. p_menu->insertAction(p_before, act);
  1424. }
  1425. if (QFileInfo::exists(imgPath)) {
  1426. // Local image.
  1427. auto act = new QAction(tr("Copy Image"), p_menu);
  1428. connect(act, &QAction::triggered,
  1429. p_menu, [imgPath]() {
  1430. auto clipboard = QApplication::clipboard();
  1431. clipboard->clear();
  1432. auto img = FileUtils::imageFromFile(imgPath);
  1433. if (!img.isNull()) {
  1434. ClipboardUtils::setImageToClipboard(clipboard, img);
  1435. }
  1436. });
  1437. p_menu->insertAction(p_before, act);
  1438. } else {
  1439. // Online image.
  1440. prependInPlacePreviewMenu(p_menu, p_before, p_cursorPos, p_block);
  1441. }
  1442. p_menu->insertSeparator(p_before);
  1443. return true;
  1444. }
  1445. bool MarkdownEditor::prependInPlacePreviewMenu(QMenu *p_menu, QAction *p_before, int p_cursorPos, const QTextBlock &p_block)
  1446. {
  1447. auto data = vte::TextBlockData::get(p_block);
  1448. if (!data) {
  1449. return false;
  1450. }
  1451. auto previewData = data->getBlockPreviewData();
  1452. if (!previewData) {
  1453. return false;
  1454. }
  1455. QPixmap image;
  1456. QRgb background = 0;
  1457. const int pib = p_cursorPos - p_block.position();
  1458. for (const auto &info : previewData->getPreviewData()) {
  1459. const auto *imageData = info->getImageData();
  1460. if (!imageData) {
  1461. continue;
  1462. }
  1463. if (imageData->contains(pib) || (imageData->contains(pib - 1) && pib == p_block.length() - 1)) {
  1464. const auto *img = findImageFromDocumentResourceMgr(imageData->m_imageName);
  1465. if (img) {
  1466. image = *img;
  1467. background = imageData->m_backgroundColor;
  1468. }
  1469. break;
  1470. }
  1471. }
  1472. if (image.isNull()) {
  1473. return false;
  1474. }
  1475. auto act = new QAction(tr("Copy In-Place Preview"), p_menu);
  1476. connect(act, &QAction::triggered,
  1477. p_menu, [this, image, background]() {
  1478. QColor color(background);
  1479. if (background == 0) {
  1480. color = m_textEdit->palette().color(QPalette::Base);
  1481. }
  1482. QImage img(image.size(), QImage::Format_ARGB32);
  1483. img.fill(color);
  1484. QPainter painter(&img);
  1485. painter.drawPixmap(img.rect(), image);
  1486. auto clipboard = QApplication::clipboard();
  1487. clipboard->clear();
  1488. ClipboardUtils::setImageToClipboard(clipboard, img);
  1489. });
  1490. p_menu->insertAction(p_before, act);
  1491. return true;
  1492. }
  1493. bool MarkdownEditor::prependLinkMenu(QMenu *p_menu, QAction *p_before, int p_cursorPos, const QTextBlock &p_block)
  1494. {
  1495. const auto text = p_block.text();
  1496. QRegularExpression regExp(vte::MarkdownUtils::c_linkRegExp);
  1497. QString linkText;
  1498. const int pib = p_cursorPos - p_block.position();
  1499. auto matchIter = regExp.globalMatch(text);
  1500. while (matchIter.hasNext()) {
  1501. auto match = matchIter.next();
  1502. if (pib >= match.capturedStart() && pib < match.capturedEnd()) {
  1503. linkText = match.captured(2);
  1504. break;
  1505. }
  1506. }
  1507. if (linkText.isEmpty()) {
  1508. return false;
  1509. }
  1510. const auto linkUrl = vte::MarkdownUtils::linkUrlToPath(getBasePath(), linkText);
  1511. {
  1512. auto act = new QAction(tr("Open Link"), p_menu);
  1513. connect(act, &QAction::triggered,
  1514. p_menu, [linkUrl]() {
  1515. emit VNoteX::getInst().openFileRequested(linkUrl, QSharedPointer<FileOpenParameters>::create());
  1516. });
  1517. p_menu->insertAction(p_before, act);
  1518. }
  1519. {
  1520. auto act = new QAction(tr("Copy Link"), p_menu);
  1521. connect(act, &QAction::triggered,
  1522. p_menu, [linkUrl]() {
  1523. ClipboardUtils::setLinkToClipboard(linkUrl);
  1524. });
  1525. p_menu->insertAction(p_before, act);
  1526. }
  1527. p_menu->insertSeparator(p_before);
  1528. return true;
  1529. }