2 * @file DirCmpReport.cpp
4 * @brief Implementation file for DirCmpReport
13 #include <Poco/Base64Encoder.h>
15 #include "DirCmpReport.h"
19 #include "CompareStats.h"
21 #include "DiffThread.h"
22 #include "IAbortable.h"
24 UINT CF_HTML = RegisterClipboardFormat(_T("HTML Format"));
27 * @brief Return current time as string.
28 * @return Current time as String.
30 static String GetCurrentTimeString()
34 _int64 nTime64 = nTime;
35 String str = locality::TimeString(&nTime64);
40 * @brief Format string as beginning tag.
41 * @param [in] elName String to format as beginning tag.
42 * @return String formatted as beginning tag.
44 static String BeginEl(const String& elName, const String& attr = _T(""))
47 return strutils::format(_T("<%s>"), elName);
49 return strutils::format(_T("<%s %s>"), elName, attr);
53 * @brief Format string as ending tag.
54 * @param [in] elName String to format as ending tag.
55 * @return String formatted as ending tag.
57 static String EndEl(const String& elName)
59 return strutils::format(_T("</%s>"), elName);
65 DirCmpReport::DirCmpReport(const std::vector<String> & colRegKeys)
69 , m_colRegKeys(colRegKeys)
70 , m_sSeparator(_T(","))
71 , m_pFileCmpReport(nullptr)
72 , m_bIncludeFileCmpReport(false)
73 , m_bOutputUTF8(false)
75 , m_bCopyToClipboard(false)
76 , m_nReportType(REPORT_TYPE_COMMALIST)
81 * @brief Set UI-list pointer.
83 void DirCmpReport::SetList(IListCtrl *pList)
89 * @brief Set root-paths of current compare so we can add them to report.
90 * @param [in] paths Root path information for the directory for which the report is generated.
92 void DirCmpReport::SetRootPaths(const PathContext &paths)
94 if (paths.GetSize() < 3)
96 m_rootPaths.SetLeft(paths.GetLeft());
97 m_rootPaths.SetRight(paths.GetRight());
98 m_sTitle = strutils::format_string2(_("Compare %1 with %2"),
99 m_rootPaths.GetLeft(), m_rootPaths.GetRight());
102 m_rootPaths.SetLeft(paths.GetLeft());
103 m_rootPaths.SetMiddle(paths.GetMiddle());
104 m_rootPaths.SetRight(paths.GetRight());
105 m_sTitle = strutils::format_string3(_("Compare %1 with %2 and %3"),
106 m_rootPaths.GetLeft(), m_rootPaths.GetMiddle(), m_rootPaths.GetRight());
111 * @brief Set column-count.
113 void DirCmpReport::SetColumns(int columns)
115 m_nColumns = columns;
119 * @brief Set file compare reporter functor
121 void DirCmpReport::SetFileCmpReport(IFileCmpReport *pFileCmpReport)
123 m_pFileCmpReport.reset(pFileCmpReport);
126 static ULONG GetLength32(CFile const &f)
128 ULONGLONG length = f.GetLength();
129 if (length > ULONG_MAX)
131 return static_cast<ULONG>(length);
134 static HGLOBAL ConvertToUTF16ForClipboard(HGLOBAL hMem, int codepage)
136 size_t len = GlobalSize(hMem);
137 HGLOBAL hMemW = GlobalAlloc(GMEM_DDESHARE|GMEM_MOVEABLE|GMEM_ZEROINIT, (len + 1) * sizeof(wchar_t));
138 if (hMemW == nullptr)
140 LPCSTR pstr = reinterpret_cast<LPCSTR>(GlobalLock(hMem));
141 LPWSTR pwstr = reinterpret_cast<LPWSTR>(GlobalLock(hMemW));
142 if (pstr == nullptr || pwstr == nullptr)
144 int wlen = MultiByteToWideChar(codepage, 0, pstr, static_cast<int>(len), pwstr, static_cast<int>(len + 1));
145 if (len > 0 && pstr[len - 1] != '\0')
151 hMemW = GlobalReAlloc(hMemW, wlen * sizeof(wchar_t), 0);
157 * @brief Generate report and save it to file.
158 * @param [out] errStr Empty if succeeded, otherwise contains error message.
159 * @return `true` if report was created, `false` if user canceled report.
161 bool DirCmpReport::GenerateReport(String &errStr)
163 assert(m_pList != nullptr);
164 assert(m_pFile == nullptr);
168 if (m_bCopyToClipboard)
170 if (!OpenClipboard(NULL))
172 if (!EmptyClipboard())
174 CSharedFile file(GMEM_DDESHARE|GMEM_MOVEABLE|GMEM_ZEROINIT);
176 bool savedIncludeFileCmpReport = m_bIncludeFileCmpReport;
177 m_bIncludeFileCmpReport = false;
178 GenerateReport(m_nReportType);
179 HGLOBAL hMem = file.Detach();
180 SetClipboardData(CF_UNICODETEXT, ConvertToUTF16ForClipboard(hMem, m_bOutputUTF8 ? CP_UTF8 : CP_THREAD_ACP));
182 // If report type is HTML, render CF_HTML format as well
183 if (m_nReportType == REPORT_TYPE_SIMPLEHTML)
185 // Reconstruct the CSharedFile object
187 file.CSharedFile::CSharedFile(GMEM_DDESHARE|GMEM_MOVEABLE|GMEM_ZEROINIT);
188 // Write preliminary CF_HTML header with all offsets zero
189 static const char header[] =
193 "StartFragment:%09d\n"
194 "EndFragment:%09d\n";
195 static const char start[] = "<html><body>\n<!--StartFragment -->";
196 static const char end[] = "\n<!--EndFragment -->\n</body>\n</html>\n";
197 char buffer[MAX_PATH_FULL];
198 int cbHeader = wsprintfA(buffer, header, 0, 0, 0, 0);
199 file.Write(buffer, cbHeader);
200 file.Write(start, sizeof start - 1);
201 GenerateHTMLHeaderBodyPortion();
202 GenerateXmlHtmlContent(false);
203 file.Write(end, sizeof end); // include terminating zero
204 DWORD size = GetLength32(file);
205 // Rewrite CF_HTML header with valid offsets
207 wsprintfA(buffer, header, cbHeader,
208 static_cast<int>(size - 1),
209 static_cast<int>(cbHeader + sizeof start - 1),
210 static_cast<int>(size - sizeof end + 1));
211 file.Write(buffer, cbHeader);
212 SetClipboardData(CF_HTML, GlobalReAlloc(file.Detach(), size, 0));
215 m_bIncludeFileCmpReport = savedIncludeFileCmpReport;
217 if (!m_sReportFile.empty())
220 paths::SplitFilename(m_sReportFile, &path, nullptr, nullptr);
221 if (!paths::CreateIfNeeded(path))
223 errStr = _("Folder does not exist.");
226 CFile file(m_sReportFile.c_str(),
227 CFile::modeWrite|CFile::modeCreate|CFile::shareDenyWrite);
229 GenerateReport(m_nReportType);
233 catch (CException *e)
235 e->ReportError(MB_ICONSTOP);
243 * @brief Generate report of given type.
244 * @param [in] nReportType Type of report.
246 void DirCmpReport::GenerateReport(REPORT_TYPE nReportType)
250 case REPORT_TYPE_SIMPLEHTML:
251 m_bOutputUTF8 = true;
252 GenerateHTMLHeader();
253 GenerateXmlHtmlContent(false);
254 GenerateHTMLFooter();
256 case REPORT_TYPE_SIMPLEXML:
257 m_bOutputUTF8 = true;
259 GenerateXmlHtmlContent(true);
262 case REPORT_TYPE_COMMALIST:
263 m_bOutputUTF8 = false;
264 m_sSeparator = _T(",");
268 case REPORT_TYPE_TABLIST:
269 m_bOutputUTF8 = false;
270 m_sSeparator = _T("\t");
278 * @brief Write text to report file.
279 * @param [in] pszText Text to write to report file.
281 void DirCmpReport::WriteString(const String& sText)
283 std::string sOctets(m_bOutputUTF8 ? ucr::toUTF8(sText) : ucr::toThreadCP(sText));
284 const char *pchOctets = sOctets.c_str();
285 size_t cchAhead = sOctets.length();
286 while (const char *pchAhead = (const char *)memchr(pchOctets, '\n', cchAhead))
288 size_t cchLine = pchAhead - pchOctets;
289 m_pFile->Write(pchOctets, static_cast<unsigned>(cchLine));
290 static const char eol[] = { '\r', '\n' };
291 m_pFile->Write(eol, sizeof eol);
293 pchOctets += cchLine;
296 m_pFile->Write(pchOctets, static_cast<unsigned>(cchAhead));
301 * @brief Write text to report file while turning special chars to entities.
302 * @param [in] sText Text to write to report file.
304 void DirCmpReport::WriteStringEntityAware(const String& sText)
306 WriteString(ucr::toTString(CMarkdown::Entities(ucr::toUTF8(sText))));
310 * @brief Generate header-data for report.
312 void DirCmpReport::GenerateHeader()
314 WriteString(m_sTitle);
315 WriteString(_T("\n"));
316 WriteString(GetCurrentTimeString());
317 WriteString(_T("\n"));
318 for (int currCol = 0; currCol < m_nColumns; currCol++)
320 WriteString(m_pList->GetColumnName(currCol));
321 // Add col-separator, but not after last column
322 if (currCol < m_nColumns - 1)
323 WriteString(m_sSeparator);
328 * @brief Generate report content (compared items).
330 void DirCmpReport::GenerateContent()
332 int nRows = m_pList->GetRowCount();
334 // Report:Detail. All currently displayed columns will be added
335 for (int currRow = 0; currRow < nRows; currRow++)
337 if (m_myStruct && m_myStruct->context->GetAbortable()->ShouldAbort())
339 WriteString(_T("\n"));
340 DIFFITEM* pdi = reinterpret_cast<DIFFITEM*>(m_pList->GetItemData(currRow));
341 if (reinterpret_cast<uintptr_t>(pdi) == -1)
344 m_myStruct->context->m_pCompareStats->BeginCompare(pdi, 0);
345 for (int currCol = 0; currCol < m_nColumns; currCol++)
347 String value = m_pList->GetItemText(currRow, currCol);
348 if (value.find(m_sSeparator) != String::npos) {
349 WriteString(_T("\""));
351 WriteString(_T("\""));
356 // Add col-separator, but not after last column
357 if (currCol < m_nColumns - 1)
358 WriteString(m_sSeparator);
361 m_myStruct->context->m_pCompareStats->AddItem(-1);
367 * @brief Generate simple html report header.
369 void DirCmpReport::GenerateHTMLHeader()
371 WriteString(_T("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\"\n")
372 _T("\t\"http://www.w3.org/TR/html4/loose.dtd\">\n")
373 _T("<html>\n<head>\n")
374 _T("\t<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n")
376 WriteStringEntityAware(m_sTitle);
377 WriteString(_T("</title>\n"));
378 WriteString(_T("\t<style type=\"text/css\">\n\t<!--\n"));
379 WriteString(_T("\t\tbody {\n"));
380 WriteString(_T("\t\t\tfont-family: sans-serif;\n"));
381 WriteString(_T("\t\t\tfont-size: smaller;\n"));
382 WriteString(_T("\t\t}\n"));
383 WriteString(_T("\t\ttable {\n"));
384 WriteString(_T("\t\t\tborder-collapse: collapse;\n"));
385 WriteString(_T("\t\t\tborder: 1px solid gray;\n"));
386 WriteString(_T("\t\t}\n"));
387 WriteString(_T("\t\tth,td {\n"));
388 WriteString(_T("\t\t\tpadding: 3px;\n"));
389 WriteString(_T("\t\t\ttext-align: left;\n"));
390 WriteString(_T("\t\t\tvertical-align: middle;\n"));
391 WriteString(_T("\t\t\tborder: 1px solid gray;\n"));
392 WriteString(_T("\t\t}\n"));
393 WriteString(_T("\t\tth {\n"));
394 WriteString(_T("\t\t\tcolor: white;\n"));
395 WriteString(_T("\t\t\tbackground: blue;\n"));
396 WriteString(_T("\t\t\tpadding: 4px 4px;\n"));
397 WriteString(_T("\t\t\tbackground: linear-gradient(mediumblue, darkblue);\n"));
398 WriteString(_T("\t\t\tposition: sticky; top: 0;\n"));
399 WriteString(_T("\t\t}\n"));
401 std::vector<bool> usedIcon(m_pList->GetIconCount());
403 for (int i = 0; i < m_pList->GetRowCount(); ++i)
405 usedIcon[m_pList->GetIconIndex(i)] = true;
406 maxIndent = (std::max)(m_pList->GetIndent(i), maxIndent);
408 for (int i = 0; i < m_pList->GetIconCount(); ++i)
412 std::ostringstream stream;
413 Poco::Base64Encoder enc(stream);
414 enc.rdbuf()->setLineLength(0);
415 enc << m_pList->GetIconPNGData(i);
417 WriteString(strutils::format(_T("\t\t.icon%d { background-image: url('data:image/png;base64,%s'); background-repeat: no-repeat; background-size: 16px 16px; }\n"), i, ucr::toTString(stream.str())));
420 for (int i = 0; i < maxIndent + 1; ++i)
421 WriteString(strutils::format(_T("\t\t.indent%d { padding-left: %dpx; background-position: %dpx center; }\n"), i, 2 * 2 + 16 + 8 * i, 2 + 8 * i));
422 WriteString(_T("\t-->\n\t</style>\n"));
423 WriteString(_T("</head>\n<body>\n"));
424 GenerateHTMLHeaderBodyPortion();
428 * @brief Generate body portion of simple html report header (w/o body tag).
430 void DirCmpReport::GenerateHTMLHeaderBodyPortion()
432 WriteString(_T("<h2>"));
433 WriteString(m_sTitle);
434 WriteString(_T("</h2>\n<p>"));
435 WriteString(GetCurrentTimeString());
436 WriteString(_T("</p>\n"));
437 WriteString(_T("<table border=\"1\">\n<tr>\n"));
439 for (int currCol = 0; currCol < m_nColumns; currCol++)
441 WriteString(_T("<th>"));
442 WriteStringEntityAware(m_pList->GetColumnName(currCol));
443 WriteString(_T("</th>"));
445 WriteString(_T("</tr>\n"));
449 * @brief Generate simple xml report header.
451 void DirCmpReport::GenerateXmlHeader()
453 WriteString(_T("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
454 _T("<WinMergeDiffReport version=\"2\">\n")
456 WriteStringEntityAware(m_rootPaths.GetLeft());
457 WriteString(_T("</left>\n"));
458 if (m_rootPaths.GetSize() == 3)
460 WriteString(_T("<middle>"));
461 WriteStringEntityAware(m_rootPaths.GetMiddle());
462 WriteString(_T("</middle>\n"));
464 WriteString(_T("<right>"));
465 WriteStringEntityAware(m_rootPaths.GetRight());
466 WriteString(_T("</right>\n")
468 WriteStringEntityAware(GetCurrentTimeString());
469 WriteString(_T("</time>\n"));
471 // Add column headers
472 const String rowEl = _T("column_name");
473 WriteString(BeginEl(rowEl));
474 for (int currCol = 0; currCol < m_nColumns; currCol++)
476 const String colEl = m_colRegKeys[currCol];
477 WriteString(BeginEl(colEl));
478 WriteStringEntityAware(m_pList->GetColumnName(currCol));
479 WriteString(EndEl(colEl));
481 WriteString(EndEl(rowEl) + _T("\n"));
485 * @brief Generate simple html or xml report content.
487 void DirCmpReport::GenerateXmlHtmlContent(bool xml)
489 String sFileName, sParentDir;
490 paths::SplitFilename((const tchar_t *)m_pFile->GetFilePath(), &sParentDir, &sFileName, nullptr);
491 String sRelDestDir = sFileName.substr(0, sFileName.find_last_of(_T('.'))) + _T(".files");
492 String sDestDir = paths::ConcatPath(sParentDir, sRelDestDir);
493 if (!xml && m_bIncludeFileCmpReport && m_pFileCmpReport != nullptr)
494 paths::CreateIfNeeded(sDestDir);
496 int nRows = m_pList->GetRowCount();
498 // Report:Detail. All currently displayed columns will be added
499 for (int currRow = 0; currRow < nRows; currRow++)
501 if (m_myStruct && m_myStruct->context->GetAbortable()->ShouldAbort())
503 DIFFITEM* pdi = reinterpret_cast<DIFFITEM*>(m_pList->GetItemData(currRow));
504 if (reinterpret_cast<uintptr_t>(pdi) == -1)
508 m_myStruct->context->m_pCompareStats->BeginCompare(pdi, 0);
509 if (!xml && m_bIncludeFileCmpReport && m_pFileCmpReport != nullptr)
510 (*m_pFileCmpReport.get())(REPORT_TYPE_SIMPLEHTML, m_pList.get(), currRow, sDestDir, sLinkPath);
512 strutils::replace(sLinkPath, _T("%"), _T("%25"));
513 strutils::replace(sLinkPath, _T("#"), _T("%23"));
515 String rowEl = _T("tr");
518 rowEl = _T("filediff");
519 WriteString(BeginEl(rowEl));
523 COLORREF backcolor = m_pList->GetBackColor(currRow);
524 COLORREF textcolor = m_pList->GetTextColor(currRow);
525 String attr = strutils::format(_T("style='%sbackground-color: #%02x%02x%02x'"),
526 textcolor == 0 ? _T("") : strutils::format(_T("color: #%02x%02x%02x; "),
527 GetRValue(textcolor), GetGValue(textcolor), GetBValue(textcolor)).c_str(),
528 GetRValue(backcolor), GetGValue(backcolor), GetBValue(backcolor));
529 WriteString(BeginEl(rowEl, attr));
531 for (int currCol = 0; currCol < m_nColumns; currCol++)
533 String colEl = _T("td");
536 colEl = m_colRegKeys[currCol];
537 WriteString(BeginEl(colEl));
542 WriteString(BeginEl(colEl, strutils::format(_T("class=\"icon%d indent%d\""), m_pList->GetIconIndex(currRow), m_pList->GetIndent(currRow))));
544 WriteString(BeginEl(colEl));
546 if (currCol == 0 && !sLinkPath.empty())
548 WriteString(_T("<a href=\""));
549 WriteString(sRelDestDir);
550 WriteString(_T("/"));
551 WriteString(sLinkPath);
552 WriteString(_T("\">"));
553 WriteStringEntityAware(m_pList->GetItemText(currRow, currCol));
554 WriteString(_T("</a>"));
558 WriteStringEntityAware(m_pList->GetItemText(currRow, currCol));
560 WriteString(EndEl(colEl));
562 WriteString(EndEl(rowEl) + _T("\n"));
564 m_myStruct->context->m_pCompareStats->AddItem(-1);
567 WriteString(_T("</table>\n"));
571 * @brief Generate simple html report footer.
573 void DirCmpReport::GenerateHTMLFooter()
575 WriteString(_T("</body>\n</html>\n"));
579 * @brief Generate simple xml report header.
581 void DirCmpReport::GenerateXmlFooter()
583 WriteString(_T("</WinMergeDiffReport>\n"));