vutils.cpp 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311
  1. #include "vutils.h"
  2. #include <QFile>
  3. #include <QDir>
  4. #include <QDebug>
  5. #include <QRegExp>
  6. #include <QClipboard>
  7. #include <QApplication>
  8. #include <QMimeData>
  9. #include <QJsonObject>
  10. #include <QJsonDocument>
  11. #include <QDateTime>
  12. #include <QFileInfo>
  13. #include <QImageReader>
  14. #include <QKeyEvent>
  15. #include <QScreen>
  16. #include <cmath>
  17. #include <QLocale>
  18. #include <QPushButton>
  19. #include <QElapsedTimer>
  20. #include <QValidator>
  21. #include <QRegExpValidator>
  22. #include <QRegExp>
  23. #include <QKeySequence>
  24. #include <QComboBox>
  25. #include <QStyledItemDelegate>
  26. #include <QWebEngineView>
  27. #include <QAction>
  28. #include "vorphanfile.h"
  29. #include "vnote.h"
  30. #include "vnotebook.h"
  31. #include "hgmarkdownhighlighter.h"
  32. #include "vpreviewpage.h"
  33. extern VConfigManager *g_config;
  34. QVector<QPair<QString, QString>> VUtils::s_availableLanguages;
  35. const QString VUtils::c_imageLinkRegExp = QString("\\!\\[([^\\]]*)\\]\\(([^\\)\"]+)\\s*(\"(\\\\.|[^\"\\)])*\")?\\s*\\)");
  36. const QString VUtils::c_imageTitleRegExp = QString("[\\w\\(\\)@#%\\*\\-\\+=\\?<>\\,\\.\\s]*");
  37. const QString VUtils::c_fileNameRegExp = QString("[^\\\\/:\\*\\?\"<>\\|]*");
  38. const QString VUtils::c_fencedCodeBlockStartRegExp = QString("^(\\s*)```([^`\\s]*)\\s*[^`]*$");
  39. const QString VUtils::c_fencedCodeBlockEndRegExp = QString("^(\\s*)```$");
  40. const QString VUtils::c_previewImageBlockRegExp = QString("[\\n|^][ |\\t]*\\xfffc[ |\\t]*(?=\\n)");
  41. const QString VUtils::c_headerRegExp = QString("^(#{1,6})\\s+(((\\d+\\.)+(?=\\s))?\\s*(\\S.*)?)$");
  42. const QString VUtils::c_headerPrefixRegExp = QString("^(#{1,6}\\s+((\\d+\\.)+(?=\\s))?\\s*)($|(\\S.*)?$)");
  43. void VUtils::initAvailableLanguage()
  44. {
  45. if (!s_availableLanguages.isEmpty()) {
  46. return;
  47. }
  48. s_availableLanguages.append(QPair<QString, QString>("en_US", "English (US)"));
  49. s_availableLanguages.append(QPair<QString, QString>("zh_CN", "Chinese"));
  50. }
  51. QString VUtils::readFileFromDisk(const QString &filePath)
  52. {
  53. QFile file(filePath);
  54. if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
  55. qWarning() << "fail to open file" << filePath << "to read";
  56. return QString();
  57. }
  58. QString fileText(file.readAll());
  59. file.close();
  60. qDebug() << "read file content:" << filePath;
  61. return fileText;
  62. }
  63. bool VUtils::writeFileToDisk(const QString &p_filePath, const QString &p_text)
  64. {
  65. QFile file(p_filePath);
  66. if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
  67. qWarning() << "fail to open file" << p_filePath << "to write";
  68. return false;
  69. }
  70. QTextStream stream(&file);
  71. stream << p_text;
  72. file.close();
  73. qDebug() << "write file content:" << p_filePath;
  74. return true;
  75. }
  76. bool VUtils::writeFileToDisk(const QString &p_filePath, const QByteArray &p_data)
  77. {
  78. QFile file(p_filePath);
  79. if (!file.open(QIODevice::WriteOnly)) {
  80. qWarning() << "fail to open file" << p_filePath << "to write";
  81. return false;
  82. }
  83. file.write(p_data);
  84. file.close();
  85. qDebug() << "write file content:" << p_filePath;
  86. return true;
  87. }
  88. bool VUtils::writeJsonToDisk(const QString &p_filePath, const QJsonObject &p_json)
  89. {
  90. QFile file(p_filePath);
  91. // We use Unix LF for config file.
  92. if (!file.open(QIODevice::WriteOnly)) {
  93. qWarning() << "fail to open file" << p_filePath << "to write";
  94. return false;
  95. }
  96. QJsonDocument doc(p_json);
  97. if (-1 == file.write(doc.toJson())) {
  98. return false;
  99. }
  100. return true;
  101. }
  102. QJsonObject VUtils::readJsonFromDisk(const QString &p_filePath)
  103. {
  104. QFile file(p_filePath);
  105. if (!file.open(QIODevice::ReadOnly)) {
  106. qWarning() << "fail to open file" << p_filePath << "to read";
  107. return QJsonObject();
  108. }
  109. return QJsonDocument::fromJson(file.readAll()).object();
  110. }
  111. QString VUtils::generateImageFileName(const QString &path,
  112. const QString &title,
  113. const QString &format)
  114. {
  115. QRegExp regExp("\\W");
  116. QString baseName(title.toLower());
  117. // Remove non-character chars.
  118. baseName.remove(regExp);
  119. // Constrain the length of the name.
  120. baseName.truncate(10);
  121. if (!baseName.isEmpty()) {
  122. baseName.prepend('_');
  123. }
  124. // Add current time and random number to make the name be most likely unique
  125. baseName = baseName + '_' + QString::number(QDateTime::currentDateTime().toTime_t());
  126. baseName = baseName + '_' + QString::number(qrand());
  127. QDir dir(path);
  128. QString imageName = baseName + "." + format.toLower();
  129. int index = 1;
  130. while (fileExists(dir, imageName, true)) {
  131. imageName = QString("%1_%2.%3").arg(baseName).arg(index++)
  132. .arg(format.toLower());
  133. }
  134. return imageName;
  135. }
  136. QString VUtils::fileNameFromPath(const QString &p_path)
  137. {
  138. if (p_path.isEmpty()) {
  139. return p_path;
  140. }
  141. return QFileInfo(QDir::cleanPath(p_path)).fileName();
  142. }
  143. QString VUtils::basePathFromPath(const QString &p_path)
  144. {
  145. if (p_path.isEmpty()) {
  146. return p_path;
  147. }
  148. return QFileInfo(QDir::cleanPath(p_path)).path();
  149. }
  150. QVector<ImageLink> VUtils::fetchImagesFromMarkdownFile(VFile *p_file,
  151. ImageLink::ImageLinkType p_type)
  152. {
  153. Q_ASSERT(p_file->getDocType() == DocType::Markdown);
  154. QVector<ImageLink> images;
  155. bool isOpened = p_file->isOpened();
  156. if (!isOpened && !p_file->open()) {
  157. return images;
  158. }
  159. const QString &text = p_file->getContent();
  160. if (text.isEmpty()) {
  161. if (!isOpened) {
  162. p_file->close();
  163. }
  164. return images;
  165. }
  166. // Used to de-duplicate the links. Url as the key.
  167. QSet<QString> fetchedLinks;
  168. QVector<VElementRegion> regions = fetchImageRegionsUsingParser(text);
  169. QRegExp regExp(c_imageLinkRegExp);
  170. QString basePath = p_file->fetchBasePath();
  171. for (int i = 0; i < regions.size(); ++i) {
  172. const VElementRegion &reg = regions[i];
  173. QString linkText = text.mid(reg.m_startPos, reg.m_endPos - reg.m_startPos);
  174. bool matched = regExp.exactMatch(linkText);
  175. if (!matched) {
  176. // Image links with reference format will not match.
  177. continue;
  178. }
  179. QString imageUrl = regExp.capturedTexts()[2].trimmed();
  180. ImageLink link;
  181. link.m_url = imageUrl;
  182. QFileInfo info(basePath, imageUrl);
  183. if (info.exists()) {
  184. if (info.isNativePath()) {
  185. // Local file.
  186. link.m_path = QDir::cleanPath(info.absoluteFilePath());
  187. if (QDir::isRelativePath(imageUrl)) {
  188. link.m_type = p_file->isInternalImageFolder(VUtils::basePathFromPath(link.m_path)) ?
  189. ImageLink::LocalRelativeInternal : ImageLink::LocalRelativeExternal;
  190. } else {
  191. link.m_type = ImageLink::LocalAbsolute;
  192. }
  193. } else {
  194. link.m_type = ImageLink::Resource;
  195. link.m_path = imageUrl;
  196. }
  197. } else {
  198. QUrl url(imageUrl);
  199. link.m_path = url.toString();
  200. link.m_type = ImageLink::Remote;
  201. }
  202. if (link.m_type & p_type) {
  203. if (!fetchedLinks.contains(link.m_url)) {
  204. fetchedLinks.insert(link.m_url);
  205. images.push_back(link);
  206. qDebug() << "fetch one image:" << link.m_type << link.m_path << link.m_url;
  207. }
  208. }
  209. }
  210. if (!isOpened) {
  211. p_file->close();
  212. }
  213. return images;
  214. }
  215. QString VUtils::imageLinkUrlToPath(const QString &p_basePath, const QString &p_url)
  216. {
  217. QString path;
  218. QFileInfo info(p_basePath, p_url);
  219. if (info.exists()) {
  220. if (info.isNativePath()) {
  221. // Local file.
  222. path = QDir::cleanPath(info.absoluteFilePath());
  223. } else {
  224. path = p_url;
  225. }
  226. } else {
  227. path = QUrl(p_url).toString();
  228. }
  229. return path;
  230. }
  231. bool VUtils::makePath(const QString &p_path)
  232. {
  233. if (p_path.isEmpty()) {
  234. return true;
  235. }
  236. bool ret = true;
  237. QDir dir;
  238. if (dir.mkpath(p_path)) {
  239. qDebug() << "make path" << p_path;
  240. } else {
  241. qWarning() << "fail to make path" << p_path;
  242. ret = false;
  243. }
  244. return ret;
  245. }
  246. QJsonObject VUtils::clipboardToJson()
  247. {
  248. QClipboard *clipboard = QApplication::clipboard();
  249. const QMimeData *mimeData = clipboard->mimeData();
  250. QJsonObject obj;
  251. if (mimeData->hasText()) {
  252. QString text = mimeData->text();
  253. obj = QJsonDocument::fromJson(text.toUtf8()).object();
  254. qDebug() << "Json object in clipboard" << obj;
  255. }
  256. return obj;
  257. }
  258. ClipboardOpType VUtils::operationInClipboard()
  259. {
  260. QJsonObject obj = clipboardToJson();
  261. if (obj.contains(ClipboardConfig::c_type)) {
  262. return (ClipboardOpType)obj[ClipboardConfig::c_type].toInt();
  263. }
  264. return ClipboardOpType::Invalid;
  265. }
  266. bool VUtils::copyFile(const QString &p_srcFilePath, const QString &p_destFilePath, bool p_isCut)
  267. {
  268. QString srcPath = QDir::cleanPath(p_srcFilePath);
  269. QString destPath = QDir::cleanPath(p_destFilePath);
  270. if (srcPath == destPath) {
  271. return true;
  272. }
  273. QDir dir;
  274. if (!dir.mkpath(basePathFromPath(p_destFilePath))) {
  275. qWarning() << "fail to create directory" << basePathFromPath(p_destFilePath);
  276. return false;
  277. }
  278. if (p_isCut) {
  279. QFile file(srcPath);
  280. if (!file.rename(destPath)) {
  281. qWarning() << "fail to copy file" << srcPath << destPath;
  282. return false;
  283. }
  284. } else {
  285. if (!QFile::copy(srcPath, destPath)) {
  286. qWarning() << "fail to copy file" << srcPath << destPath;
  287. return false;
  288. }
  289. }
  290. return true;
  291. }
  292. bool VUtils::copyDirectory(const QString &p_srcDirPath, const QString &p_destDirPath, bool p_isCut)
  293. {
  294. QString srcPath = QDir::cleanPath(p_srcDirPath);
  295. QString destPath = QDir::cleanPath(p_destDirPath);
  296. if (srcPath == destPath) {
  297. return true;
  298. }
  299. if (QFileInfo::exists(destPath)) {
  300. qWarning() << QString("target directory %1 already exists").arg(destPath);
  301. return false;
  302. }
  303. // QDir.rename() could not move directory across drives.
  304. // Make sure target directory exists.
  305. QDir destDir(destPath);
  306. if (!destDir.exists()) {
  307. if (!destDir.mkpath(destPath)) {
  308. qWarning() << QString("fail to create target directory %1").arg(destPath);
  309. return false;
  310. }
  311. }
  312. // Handle directory recursively.
  313. QDir srcDir(srcPath);
  314. Q_ASSERT(srcDir.exists() && destDir.exists());
  315. QFileInfoList nodes = srcDir.entryInfoList(QDir::Dirs | QDir::Files | QDir::Hidden
  316. | QDir::NoSymLinks | QDir::NoDotAndDotDot);
  317. for (int i = 0; i < nodes.size(); ++i) {
  318. const QFileInfo &fileInfo = nodes.at(i);
  319. QString name = fileInfo.fileName();
  320. if (fileInfo.isDir()) {
  321. if (!copyDirectory(srcDir.filePath(name), destDir.filePath(name), p_isCut)) {
  322. return false;
  323. }
  324. } else {
  325. Q_ASSERT(fileInfo.isFile());
  326. if (!copyFile(srcDir.filePath(name), destDir.filePath(name), p_isCut)) {
  327. return false;
  328. }
  329. }
  330. }
  331. if (p_isCut) {
  332. if (!destDir.rmdir(srcPath)) {
  333. qWarning() << QString("fail to delete source directory %1 after cut").arg(srcPath);
  334. return false;
  335. }
  336. }
  337. return true;
  338. }
  339. int VUtils::showMessage(QMessageBox::Icon p_icon,
  340. const QString &p_title,
  341. const QString &p_text,
  342. const QString &p_infoText,
  343. QMessageBox::StandardButtons p_buttons,
  344. QMessageBox::StandardButton p_defaultBtn,
  345. QWidget *p_parent,
  346. MessageBoxType p_type)
  347. {
  348. QMessageBox msgBox(p_icon, p_title, p_text, p_buttons, p_parent);
  349. msgBox.setInformativeText(p_infoText);
  350. msgBox.setDefaultButton(p_defaultBtn);
  351. if (p_type == MessageBoxType::Danger) {
  352. QPushButton *okBtn = dynamic_cast<QPushButton *>(msgBox.button(QMessageBox::Ok));
  353. if (okBtn) {
  354. setDynamicProperty(okBtn, "DangerBtn");
  355. }
  356. }
  357. QPushButton *defaultBtn = dynamic_cast<QPushButton *>(msgBox.button(p_defaultBtn));
  358. if (defaultBtn) {
  359. setDynamicProperty(defaultBtn, "SpecialBtn");
  360. }
  361. return msgBox.exec();
  362. }
  363. QString VUtils::generateCopiedFileName(const QString &p_dirPath,
  364. const QString &p_fileName,
  365. bool p_completeBaseName)
  366. {
  367. QDir dir(p_dirPath);
  368. if (!dir.exists() || !dir.exists(p_fileName)) {
  369. return p_fileName;
  370. }
  371. QFileInfo fi(p_fileName);
  372. QString baseName = p_completeBaseName ? fi.completeBaseName() : fi.baseName();
  373. QString suffix = p_completeBaseName ? fi.suffix() : fi.completeSuffix();
  374. int index = 0;
  375. QString fileName;
  376. do {
  377. QString seq;
  378. if (index > 0) {
  379. seq = QString("%1").arg(QString::number(index), 3, '0');
  380. }
  381. index++;
  382. fileName = QString("%1_copy%2").arg(baseName).arg(seq);
  383. if (!suffix.isEmpty()) {
  384. fileName = fileName + "." + suffix;
  385. }
  386. } while (fileExists(dir, fileName, true));
  387. return fileName;
  388. }
  389. QString VUtils::generateCopiedDirName(const QString &p_parentDirPath, const QString &p_dirName)
  390. {
  391. QDir dir(p_parentDirPath);
  392. QString name = p_dirName;
  393. QString dirPath = dir.filePath(name);
  394. int index = 0;
  395. while (QDir(dirPath).exists()) {
  396. QString seq;
  397. if (index > 0) {
  398. seq = QString::number(index);
  399. }
  400. index++;
  401. name = QString("%1_copy%2").arg(p_dirName).arg(seq);
  402. dirPath = dir.filePath(name);
  403. }
  404. return name;
  405. }
  406. const QVector<QPair<QString, QString>>& VUtils::getAvailableLanguages()
  407. {
  408. if (s_availableLanguages.isEmpty()) {
  409. initAvailableLanguage();
  410. }
  411. return s_availableLanguages;
  412. }
  413. bool VUtils::isValidLanguage(const QString &p_lang)
  414. {
  415. for (auto const &lang : getAvailableLanguages()) {
  416. if (lang.first == p_lang) {
  417. return true;
  418. }
  419. }
  420. return false;
  421. }
  422. bool VUtils::isImageURL(const QUrl &p_url)
  423. {
  424. QString urlStr;
  425. if (p_url.isLocalFile()) {
  426. urlStr = p_url.toLocalFile();
  427. } else {
  428. urlStr = p_url.toString();
  429. }
  430. return isImageURLText(urlStr);
  431. }
  432. bool VUtils::isImageURLText(const QString &p_url)
  433. {
  434. QFileInfo info(p_url);
  435. return QImageReader::supportedImageFormats().contains(info.suffix().toLower().toLatin1());
  436. }
  437. qreal VUtils::calculateScaleFactor()
  438. {
  439. // const qreal refHeight = 1152;
  440. // const qreal refWidth = 2048;
  441. const qreal refDpi = 96;
  442. qreal dpi = QGuiApplication::primaryScreen()->logicalDotsPerInch();
  443. qreal factor = dpi / refDpi;
  444. return factor < 1 ? 1 : factor;
  445. }
  446. bool VUtils::realEqual(qreal p_a, qreal p_b)
  447. {
  448. return std::abs(p_a - p_b) < 1e-8;
  449. }
  450. QChar VUtils::keyToChar(int p_key)
  451. {
  452. if (p_key >= Qt::Key_A && p_key <= Qt::Key_Z) {
  453. return QChar('a' + p_key - Qt::Key_A);
  454. }
  455. return QChar();
  456. }
  457. QString VUtils::getLocale()
  458. {
  459. QString locale = g_config->getLanguage();
  460. if (locale == "System" || !isValidLanguage(locale)) {
  461. locale = QLocale::system().name();
  462. }
  463. return locale;
  464. }
  465. void VUtils::sleepWait(int p_milliseconds)
  466. {
  467. if (p_milliseconds <= 0) {
  468. return;
  469. }
  470. QElapsedTimer t;
  471. t.start();
  472. while (t.elapsed() < p_milliseconds) {
  473. QCoreApplication::processEvents();
  474. }
  475. }
  476. DocType VUtils::docTypeFromName(const QString &p_name)
  477. {
  478. if (p_name.isEmpty()) {
  479. return DocType::Unknown;
  480. }
  481. const QHash<int, QList<QString>> &suffixes = g_config->getDocSuffixes();
  482. QString suf = QFileInfo(p_name).suffix().toLower();
  483. for (auto it = suffixes.begin(); it != suffixes.end(); ++it) {
  484. if (it.value().contains(suf)) {
  485. return DocType(it.key());
  486. }
  487. }
  488. return DocType::Unknown;
  489. }
  490. QString VUtils::generateSimpleHtmlTemplate(const QString &p_body)
  491. {
  492. QString html(VNote::s_simpleHtmlTemplate);
  493. return html.replace(HtmlHolder::c_bodyHolder, p_body);
  494. }
  495. QString VUtils::generateHtmlTemplate(MarkdownConverterType p_conType)
  496. {
  497. return generateHtmlTemplate(VNote::s_markdownTemplate, p_conType);
  498. }
  499. QString VUtils::generateHtmlTemplate(MarkdownConverterType p_conType,
  500. const QString &p_renderBg,
  501. const QString &p_renderStyle,
  502. const QString &p_renderCodeBlockStyle,
  503. bool p_isPDF,
  504. bool p_wkhtmltopdf)
  505. {
  506. Q_ASSERT((p_isPDF && p_wkhtmltopdf) || !p_wkhtmltopdf);
  507. QString templ = VNote::generateHtmlTemplate(g_config->getRenderBackgroundColor(p_renderBg),
  508. g_config->getCssStyleUrl(p_renderStyle),
  509. g_config->getCodeBlockCssStyleUrl(p_renderCodeBlockStyle),
  510. p_isPDF);
  511. return generateHtmlTemplate(templ, p_conType, p_wkhtmltopdf);
  512. }
  513. QString VUtils::generateHtmlTemplate(const QString &p_template,
  514. MarkdownConverterType p_conType,
  515. bool p_wkhtmltopdf)
  516. {
  517. QString jsFile, extraFile;
  518. switch (p_conType) {
  519. case MarkdownConverterType::Marked:
  520. jsFile = "qrc" + VNote::c_markedJsFile;
  521. extraFile = "<script src=\"qrc" + VNote::c_markedExtraFile + "\"></script>\n";
  522. break;
  523. case MarkdownConverterType::Hoedown:
  524. jsFile = "qrc" + VNote::c_hoedownJsFile;
  525. // Use Marked to highlight code blocks.
  526. extraFile = "<script src=\"qrc" + VNote::c_markedExtraFile + "\"></script>\n";
  527. break;
  528. case MarkdownConverterType::MarkdownIt:
  529. {
  530. jsFile = "qrc" + VNote::c_markdownitJsFile;
  531. extraFile = "<script src=\"qrc" + VNote::c_markdownitExtraFile + "\"></script>\n" +
  532. "<script src=\"qrc" + VNote::c_markdownitAnchorExtraFile + "\"></script>\n" +
  533. "<script src=\"qrc" + VNote::c_markdownitTaskListExtraFile + "\"></script>\n" +
  534. /*
  535. "<script src=\"qrc" + VNote::c_markdownitSubExtraFile + "\"></script>\n" +
  536. "<script src=\"qrc" + VNote::c_markdownitSupExtraFile + "\"></script>\n" +
  537. */
  538. "<script src=\"qrc" + VNote::c_markdownitFootnoteExtraFile + "\"></script>\n";
  539. MarkdownitOption opt = g_config->getMarkdownitOption();
  540. QString optJs = QString("<script>var VMarkdownitOption = {"
  541. "html: %1, breaks: %2, linkify: %3};"
  542. "</script>\n")
  543. .arg(opt.m_html ? "true" : "false")
  544. .arg(opt.m_breaks ? "true" : "false")
  545. .arg(opt.m_linkify ? "true" : "false");
  546. extraFile += optJs;
  547. break;
  548. }
  549. case MarkdownConverterType::Showdown:
  550. jsFile = "qrc" + VNote::c_showdownJsFile;
  551. extraFile = "<script src=\"qrc" + VNote::c_showdownExtraFile + "\"></script>\n" +
  552. "<script src=\"qrc" + VNote::c_showdownAnchorExtraFile + "\"></script>\n";
  553. break;
  554. default:
  555. Q_ASSERT(false);
  556. }
  557. if (g_config->getEnableMermaid()) {
  558. extraFile += "<link rel=\"stylesheet\" type=\"text/css\" href=\"" + g_config->getMermaidCssStyleUrl() + "\"/>\n" +
  559. "<script src=\"qrc" + VNote::c_mermaidApiJsFile + "\"></script>\n" +
  560. "<script>var VEnableMermaid = true;</script>\n";
  561. }
  562. if (g_config->getEnableFlowchart()) {
  563. extraFile += "<script src=\"qrc" + VNote::c_raphaelJsFile + "\"></script>\n" +
  564. "<script src=\"qrc" + VNote::c_flowchartJsFile + "\"></script>\n" +
  565. "<script>var VEnableFlowchart = true;</script>\n";
  566. }
  567. if (g_config->getEnableMathjax()) {
  568. QString mj = g_config->getMathjaxJavascript();
  569. if (p_wkhtmltopdf) {
  570. // Chante MathJax to be rendered as SVG.
  571. // If rendered as HTML, it will make the font of <code> messy.
  572. QRegExp reg("(Mathjax\\.js\\?config=)\\S+", Qt::CaseInsensitive);
  573. mj.replace(reg, QString("\\1%1").arg("TeX-MML-AM_SVG"));
  574. }
  575. extraFile += "<script type=\"text/x-mathjax-config\">"
  576. "MathJax.Hub.Config({\n"
  577. " tex2jax: {inlineMath: [['$','$'], ['\\\\(','\\\\)']],\n"
  578. "processEscapes: true,\n"
  579. "processClass: \"tex2jax_process|language-mathjax|lang-mathjax\"},\n"
  580. " showProcessingMessages: false,\n"
  581. " messageStyle: \"none\"});\n"
  582. "</script>\n"
  583. "<script type=\"text/javascript\" async src=\"" + mj + "\"></script>\n" +
  584. "<script>var VEnableMathjax = true;</script>\n";
  585. if (p_wkhtmltopdf) {
  586. extraFile += "<script>var VRemoveMathjaxScript = true;</script>\n";
  587. }
  588. }
  589. if (g_config->getEnableImageCaption()) {
  590. extraFile += "<script>var VEnableImageCaption = true;</script>\n";
  591. }
  592. if (g_config->getEnableCodeBlockLineNumber()) {
  593. extraFile += "<script src=\"qrc" + VNote::c_highlightjsLineNumberExtraFile + "\"></script>\n" +
  594. "<script>var VEnableHighlightLineNumber = true;</script>\n";
  595. }
  596. if (g_config->getEnableFlashAnchor()) {
  597. extraFile += "<script>var VEnableFlashAnchor = true;</script>\n";
  598. }
  599. extraFile += "<script>var VStylesToInline = '" + g_config->getStylesToInlineWhenCopied() + "';</script>\n";
  600. QString htmlTemplate(p_template);
  601. htmlTemplate.replace(HtmlHolder::c_JSHolder, jsFile);
  602. if (!extraFile.isEmpty()) {
  603. htmlTemplate.replace(HtmlHolder::c_extraHolder, extraFile);
  604. }
  605. return htmlTemplate;
  606. }
  607. QString VUtils::generateExportHtmlTemplate(const QString &p_renderBg, bool p_includeMathJax)
  608. {
  609. QString templ = VNote::generateExportHtmlTemplate(g_config->getRenderBackgroundColor(p_renderBg));
  610. QString extra;
  611. if (p_includeMathJax) {
  612. extra += "<script type=\"text/x-mathjax-config\">\n"
  613. "MathJax.Hub.Config({\n"
  614. "showProcessingMessages: false,\n"
  615. "messageStyle: \"none\",\n"
  616. "SVG: {\n"
  617. "minScaleAdjust: 100,\n"
  618. "styles: {\n"
  619. "\".MathJax_SVG\": {\n"
  620. "\"font-size\": \"2em !important\"\n"
  621. "}\n"
  622. "}\n"
  623. "}\n"
  624. "});\n"
  625. "</script>\n";
  626. QString mj = g_config->getMathjaxJavascript();
  627. // Chante MathJax to be rendered as SVG.
  628. QRegExp reg("(Mathjax\\.js\\?config=)\\S+", Qt::CaseInsensitive);
  629. mj.replace(reg, QString("\\1%1").arg("TeX-MML-AM_SVG"));
  630. extra += "<script type=\"text/javascript\" async src=\"" + mj + "\"></script>\n";
  631. }
  632. if (!extra.isEmpty()) {
  633. templ.replace(HtmlHolder::c_extraHolder, extra);
  634. }
  635. return templ;
  636. }
  637. QString VUtils::getFileNameWithSequence(const QString &p_directory,
  638. const QString &p_baseFileName,
  639. bool p_completeBaseName)
  640. {
  641. QDir dir(p_directory);
  642. if (!dir.exists() || !dir.exists(p_baseFileName)) {
  643. return p_baseFileName;
  644. }
  645. // Append a sequence.
  646. QFileInfo fi(p_baseFileName);
  647. QString baseName = p_completeBaseName ? fi.completeBaseName() : fi.baseName();
  648. QString suffix = p_completeBaseName ? fi.suffix() : fi.completeSuffix();
  649. int seq = 1;
  650. QString fileName;
  651. do {
  652. fileName = QString("%1_%2").arg(baseName).arg(QString::number(seq++), 3, '0');
  653. if (!suffix.isEmpty()) {
  654. fileName = fileName + "." + suffix;
  655. }
  656. } while (fileExists(dir, fileName, true));
  657. return fileName;
  658. }
  659. QString VUtils::getDirNameWithSequence(const QString &p_directory,
  660. const QString &p_baseDirName)
  661. {
  662. QDir dir(p_directory);
  663. if (!dir.exists() || !dir.exists(p_baseDirName)) {
  664. return p_baseDirName;
  665. }
  666. // Append a sequence.
  667. int seq = 1;
  668. QString fileName;
  669. do {
  670. fileName = QString("%1_%2").arg(p_baseDirName).arg(QString::number(seq++), 3, '0');
  671. } while (fileExists(dir, fileName, true));
  672. return fileName;
  673. }
  674. QString VUtils::getRandomFileName(const QString &p_directory)
  675. {
  676. Q_ASSERT(!p_directory.isEmpty());
  677. QString name;
  678. QDir dir(p_directory);
  679. do {
  680. name = QString::number(QDateTime::currentDateTimeUtc().toTime_t());
  681. name = name + '_' + QString::number(qrand());
  682. } while (fileExists(dir, name, true));
  683. return name;
  684. }
  685. bool VUtils::checkPathLegal(const QString &p_path)
  686. {
  687. // Ensure every part of the p_path is a valid file name until we come to
  688. // an existing parent directory.
  689. if (p_path.isEmpty()) {
  690. return false;
  691. }
  692. if (QFileInfo::exists(p_path)) {
  693. #if defined(Q_OS_WIN)
  694. // On Windows, "/" and ":" will also make exists() return true.
  695. if (p_path.startsWith('/') || p_path == ":") {
  696. return false;
  697. }
  698. #endif
  699. return true;
  700. }
  701. bool ret = false;
  702. int pos;
  703. QString basePath = basePathFromPath(p_path);
  704. QString fileName = fileNameFromPath(p_path);
  705. QValidator *validator = new QRegExpValidator(QRegExp(c_fileNameRegExp));
  706. while (!fileName.isEmpty()) {
  707. QValidator::State validFile = validator->validate(fileName, pos);
  708. if (validFile != QValidator::Acceptable) {
  709. break;
  710. }
  711. if (QFileInfo::exists(basePath)) {
  712. ret = true;
  713. #if defined(Q_OS_WIN)
  714. // On Windows, "/" and ":" will also make exists() return true.
  715. if (basePath.startsWith('/') || basePath == ":") {
  716. ret = false;
  717. }
  718. #endif
  719. break;
  720. }
  721. fileName = fileNameFromPath(basePath);
  722. basePath = basePathFromPath(basePath);
  723. }
  724. delete validator;
  725. return ret;
  726. }
  727. bool VUtils::checkFileNameLegal(const QString &p_name)
  728. {
  729. if (p_name.isEmpty()) {
  730. return false;
  731. }
  732. QRegExp exp(c_fileNameRegExp);
  733. return exp.exactMatch(p_name);
  734. }
  735. bool VUtils::equalPath(const QString &p_patha, const QString &p_pathb)
  736. {
  737. QString a = QDir::cleanPath(p_patha);
  738. QString b = QDir::cleanPath(p_pathb);
  739. #if defined(Q_OS_WIN)
  740. a = a.toLower();
  741. b = b.toLower();
  742. #endif
  743. return a == b;
  744. }
  745. bool VUtils::splitPathInBasePath(const QString &p_base,
  746. const QString &p_path,
  747. QStringList &p_parts)
  748. {
  749. p_parts.clear();
  750. QString a = QDir::cleanPath(p_base);
  751. QString b = QDir::cleanPath(p_path);
  752. #if defined(Q_OS_WIN)
  753. if (!b.toLower().startsWith(a.toLower())) {
  754. return false;
  755. }
  756. #else
  757. if (!b.startsWith(a)) {
  758. return false;
  759. }
  760. #endif
  761. if (a.size() == b.size()) {
  762. return true;
  763. }
  764. Q_ASSERT(a.size() < b.size());
  765. if (b.at(a.size()) != '/') {
  766. return false;
  767. }
  768. p_parts = b.right(b.size() - a.size() - 1).split("/", QString::SkipEmptyParts);
  769. qDebug() << QString("split path %1 based on %2 to %3 parts").arg(p_path).arg(p_base).arg(p_parts.size());
  770. return true;
  771. }
  772. void VUtils::decodeUrl(QString &p_url)
  773. {
  774. QHash<QString, QString> maps;
  775. maps.insert("%20", " ");
  776. for (auto it = maps.begin(); it != maps.end(); ++it) {
  777. p_url.replace(it.key(), it.value());
  778. }
  779. }
  780. QString VUtils::getShortcutText(const QString &p_keySeq)
  781. {
  782. return QKeySequence(p_keySeq).toString(QKeySequence::NativeText);
  783. }
  784. bool VUtils::deleteDirectory(const VNotebook *p_notebook,
  785. const QString &p_path,
  786. bool p_skipRecycleBin)
  787. {
  788. if (p_skipRecycleBin) {
  789. QDir dir(p_path);
  790. return dir.removeRecursively();
  791. } else {
  792. // Move it to the recycle bin folder.
  793. return deleteFile(p_notebook->getRecycleBinFolderPath(), p_path);
  794. }
  795. }
  796. bool VUtils::deleteDirectory(const QString &p_path)
  797. {
  798. if (p_path.isEmpty()) {
  799. return true;
  800. }
  801. QDir dir(p_path);
  802. return dir.removeRecursively();
  803. }
  804. bool VUtils::emptyDirectory(const VNotebook *p_notebook,
  805. const QString &p_path,
  806. bool p_skipRecycleBin)
  807. {
  808. QDir dir(p_path);
  809. if (!dir.exists()) {
  810. return true;
  811. }
  812. QFileInfoList nodes = dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::Hidden
  813. | QDir::NoSymLinks | QDir::NoDotAndDotDot);
  814. for (int i = 0; i < nodes.size(); ++i) {
  815. const QFileInfo &fileInfo = nodes.at(i);
  816. if (fileInfo.isDir()) {
  817. if (!deleteDirectory(p_notebook, fileInfo.absoluteFilePath(), p_skipRecycleBin)) {
  818. return false;
  819. }
  820. } else {
  821. Q_ASSERT(fileInfo.isFile());
  822. if (!deleteFile(p_notebook, fileInfo.absoluteFilePath(), p_skipRecycleBin)) {
  823. return false;
  824. }
  825. }
  826. }
  827. return true;
  828. }
  829. bool VUtils::deleteFile(const VNotebook *p_notebook,
  830. const QString &p_path,
  831. bool p_skipRecycleBin)
  832. {
  833. if (p_skipRecycleBin) {
  834. QFile file(p_path);
  835. return file.remove();
  836. } else {
  837. // Move it to the recycle bin folder.
  838. return deleteFile(p_notebook->getRecycleBinFolderPath(), p_path);
  839. }
  840. }
  841. bool VUtils::deleteFile(const QString &p_path)
  842. {
  843. QFile file(p_path);
  844. bool ret = file.remove();
  845. if (ret) {
  846. qDebug() << "deleted file" << p_path;
  847. } else {
  848. qWarning() << "fail to delete file" << p_path;
  849. }
  850. return ret;
  851. }
  852. bool VUtils::deleteFile(const VOrphanFile *p_file,
  853. const QString &p_path,
  854. bool p_skipRecycleBin)
  855. {
  856. if (p_skipRecycleBin) {
  857. QFile file(p_path);
  858. return file.remove();
  859. } else {
  860. // Move it to the recycle bin folder.
  861. return deleteFile(p_file->fetchRecycleBinFolderPath(), p_path);
  862. }
  863. }
  864. static QString getRecycleBinSubFolderToUse(const QString &p_folderPath)
  865. {
  866. QDir dir(p_folderPath);
  867. return QDir::cleanPath(dir.absoluteFilePath(QDateTime::currentDateTime().toString("yyyyMMdd")));
  868. }
  869. bool VUtils::deleteFile(const QString &p_recycleBinFolderPath,
  870. const QString &p_path)
  871. {
  872. QString binPath = getRecycleBinSubFolderToUse(p_recycleBinFolderPath);
  873. QDir binDir(binPath);
  874. if (!binDir.exists()) {
  875. binDir.mkpath(binPath);
  876. if (!binDir.exists()) {
  877. return false;
  878. }
  879. }
  880. QString destName = getFileNameWithSequence(binPath,
  881. fileNameFromPath(p_path),
  882. true);
  883. qDebug() << "try to move" << p_path << "to" << binPath << "as" << destName;
  884. if (!binDir.rename(p_path, binDir.filePath(destName))) {
  885. qWarning() << "fail to move" << p_path << "to" << binDir.filePath(destName);
  886. return false;
  887. }
  888. return true;
  889. }
  890. QVector<VElementRegion> VUtils::fetchImageRegionsUsingParser(const QString &p_content)
  891. {
  892. Q_ASSERT(!p_content.isEmpty());
  893. QVector<VElementRegion> regs;
  894. QByteArray ba = p_content.toUtf8();
  895. const char *data = (const char *)ba.data();
  896. int len = ba.size();
  897. pmh_element **result = NULL;
  898. char *content = new char[len + 1];
  899. memcpy(content, data, len);
  900. content[len] = '\0';
  901. pmh_markdown_to_elements(content, pmh_EXT_NONE, &result);
  902. if (!result) {
  903. return regs;
  904. }
  905. pmh_element *elem = result[pmh_IMAGE];
  906. while (elem != NULL) {
  907. if (elem->end <= elem->pos) {
  908. elem = elem->next;
  909. continue;
  910. }
  911. regs.push_back(VElementRegion(elem->pos, elem->end));
  912. elem = elem->next;
  913. }
  914. pmh_free_elements(result);
  915. return regs;
  916. }
  917. QString VUtils::displayDateTime(const QDateTime &p_dateTime)
  918. {
  919. QString res = p_dateTime.date().toString(Qt::DefaultLocaleLongDate);
  920. res += " " + p_dateTime.time().toString();
  921. return res;
  922. }
  923. bool VUtils::fileExists(const QDir &p_dir, const QString &p_name, bool p_forceCaseInsensitive)
  924. {
  925. if (!p_forceCaseInsensitive) {
  926. return p_dir.exists(p_name);
  927. }
  928. QString name = p_name.toLower();
  929. QStringList names = p_dir.entryList(QDir::Dirs | QDir::Files | QDir::Hidden
  930. | QDir::NoSymLinks | QDir::NoDotAndDotDot);
  931. foreach (const QString &str, names) {
  932. if (str.toLower() == name) {
  933. return true;
  934. }
  935. }
  936. return false;
  937. }
  938. void VUtils::addErrMsg(QString *p_msg, const QString &p_str)
  939. {
  940. if (!p_msg) {
  941. return;
  942. }
  943. if (p_msg->isEmpty()) {
  944. *p_msg = p_str;
  945. } else {
  946. *p_msg = *p_msg + '\n' + p_str;
  947. }
  948. }
  949. QStringList VUtils::filterFilePathsToOpen(const QStringList &p_files)
  950. {
  951. QStringList paths;
  952. for (int i = 0; i < p_files.size(); ++i) {
  953. if (p_files[i].startsWith('-')) {
  954. continue;
  955. }
  956. QString path = validFilePathToOpen(p_files[i]);
  957. if (!path.isEmpty()) {
  958. paths.append(path);
  959. }
  960. }
  961. return paths;
  962. }
  963. QString VUtils::validFilePathToOpen(const QString &p_file)
  964. {
  965. if (QFileInfo::exists(p_file)) {
  966. QFileInfo fi(p_file);
  967. if (fi.isFile()) {
  968. // Need to use absolute path here since VNote may be launched
  969. // in different working directory.
  970. return QDir::cleanPath(fi.absoluteFilePath());
  971. }
  972. }
  973. return QString();
  974. }
  975. bool VUtils::isControlModifierForVim(int p_modifiers)
  976. {
  977. #if defined(Q_OS_MACOS) || defined(Q_OS_MAC)
  978. return p_modifiers == Qt::MetaModifier;
  979. #else
  980. return p_modifiers == Qt::ControlModifier;
  981. #endif
  982. }
  983. void VUtils::touchFile(const QString &p_file)
  984. {
  985. QFile file(p_file);
  986. if (!file.open(QIODevice::WriteOnly)) {
  987. qWarning() << "fail to touch file" << p_file;
  988. return;
  989. }
  990. file.close();
  991. }
  992. bool VUtils::isMetaKey(int p_key)
  993. {
  994. return p_key == Qt::Key_Control
  995. || p_key == Qt::Key_Shift
  996. || p_key == Qt::Key_Meta
  997. || p_key == Qt::Key_Alt;
  998. }
  999. QComboBox *VUtils::getComboBox(QWidget *p_parent)
  1000. {
  1001. QComboBox *box = new QComboBox(p_parent);
  1002. QStyledItemDelegate *itemDelegate = new QStyledItemDelegate(box);
  1003. box->setItemDelegate(itemDelegate);
  1004. return box;
  1005. }
  1006. void VUtils::setDynamicProperty(QWidget *p_widget, const char *p_prop, bool p_val)
  1007. {
  1008. p_widget->setProperty(p_prop, p_val);
  1009. p_widget->style()->unpolish(p_widget);
  1010. p_widget->style()->polish(p_widget);
  1011. }
  1012. QWebEngineView *VUtils::getWebEngineView(QWidget *p_parent)
  1013. {
  1014. QWebEngineView *viewer = new QWebEngineView(p_parent);
  1015. VPreviewPage *page = new VPreviewPage(viewer);
  1016. page->setBackgroundColor(Qt::transparent);
  1017. viewer->setPage(page);
  1018. viewer->setZoomFactor(g_config->getWebZoomFactor());
  1019. return viewer;
  1020. }
  1021. QString VUtils::getFileNameWithLocale(const QString &p_name, const QString &p_locale)
  1022. {
  1023. QString locale = p_locale.isEmpty() ? getLocale() : p_locale;
  1024. locale = locale.split('_')[0];
  1025. QFileInfo fi(p_name);
  1026. QString baseName = fi.completeBaseName();
  1027. QString suffix = fi.suffix();
  1028. if (suffix.isEmpty()) {
  1029. return QString("%1_%2").arg(baseName).arg(locale);
  1030. } else {
  1031. return QString("%1_%2.%3").arg(baseName).arg(locale).arg(suffix);
  1032. }
  1033. }
  1034. QString VUtils::getDocFile(const QString &p_name)
  1035. {
  1036. QDir dir(VNote::c_docFileFolder);
  1037. QString name(getFileNameWithLocale(p_name));
  1038. if (!dir.exists(name)) {
  1039. name = getFileNameWithLocale(p_name, "en_US");
  1040. }
  1041. return dir.filePath(name);
  1042. }
  1043. QString VUtils::getCaptainShortcutSequenceText(const QString &p_operation)
  1044. {
  1045. QString capKey = g_config->getShortcutKeySequence("CaptainMode");
  1046. QString sec = g_config->getCaptainShortcutKeySequence(p_operation);
  1047. QKeySequence seq(capKey + "," + sec);
  1048. if (!seq.isEmpty()) {
  1049. return seq.toString(QKeySequence::NativeText);
  1050. }
  1051. return QString();
  1052. }
  1053. QString VUtils::getAvailableFontFamily(const QStringList &p_families)
  1054. {
  1055. QStringList availFamilies = QFontDatabase().families();
  1056. for (int i = 0; i < p_families.size(); ++i) {
  1057. QString family = p_families[i].trimmed();
  1058. if (family.isEmpty()) {
  1059. continue;
  1060. }
  1061. for (int j = 0; j < availFamilies.size(); ++j) {
  1062. QString availFamily = availFamilies[j];
  1063. availFamily.remove(QRegExp("\\[.*\\]"));
  1064. availFamily = availFamily.trimmed();
  1065. if (family == availFamily
  1066. || family.toLower() == availFamily.toLower()) {
  1067. qDebug() << "matched font family" << availFamilies[j];
  1068. return availFamilies[j];
  1069. }
  1070. }
  1071. }
  1072. return QString();
  1073. }
  1074. bool VUtils::fixTextWithShortcut(QAction *p_act, const QString &p_shortcut)
  1075. {
  1076. QString keySeq = g_config->getShortcutKeySequence(p_shortcut);
  1077. if (!keySeq.isEmpty()) {
  1078. p_act->setText(QString("%1\t%2").arg(p_act->text()).arg(VUtils::getShortcutText(keySeq)));
  1079. return true;
  1080. }
  1081. return false;
  1082. }
  1083. bool VUtils::fixTextWithCaptainShortcut(QAction *p_act, const QString &p_shortcut)
  1084. {
  1085. QString keyText = VUtils::getCaptainShortcutSequenceText(p_shortcut);
  1086. if (!keyText.isEmpty()) {
  1087. p_act->setText(QString("%1\t%2").arg(p_act->text()).arg(keyText));
  1088. return true;
  1089. }
  1090. return false;
  1091. }