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.
91 void DirCmpReport::SetRootPaths(const PathContext &paths)
93 m_rootPaths.SetLeft(paths.GetLeft());
94 m_rootPaths.SetRight(paths.GetRight());
95 m_sTitle = strutils::format_string2(_("Compare %1 with %2"),
96 m_rootPaths.GetLeft(), m_rootPaths.GetRight());
100 * @brief Set column-count.
102 void DirCmpReport::SetColumns(int columns)
104 m_nColumns = columns;
108 * @brief Set file compare reporter functor
110 void DirCmpReport::SetFileCmpReport(IFileCmpReport *pFileCmpReport)
112 m_pFileCmpReport.reset(pFileCmpReport);
115 static ULONG GetLength32(CFile const &f)
117 ULONGLONG length = f.GetLength();
118 if (length > ULONG_MAX)
120 return static_cast<ULONG>(length);
123 static HGLOBAL ConvertToUTF16ForClipboard(HGLOBAL hMem, int codepage)
125 size_t len = GlobalSize(hMem);
126 HGLOBAL hMemW = GlobalAlloc(GMEM_DDESHARE|GMEM_MOVEABLE|GMEM_ZEROINIT, (len + 1) * sizeof(wchar_t));
127 if (hMemW == nullptr)
129 LPCSTR pstr = reinterpret_cast<LPCSTR>(GlobalLock(hMem));
130 LPWSTR pwstr = reinterpret_cast<LPWSTR>(GlobalLock(hMemW));
131 if (pstr == nullptr || pwstr == nullptr)
133 int wlen = MultiByteToWideChar(codepage, 0, pstr, static_cast<int>(len), pwstr, static_cast<int>(len + 1));
134 if (len > 0 && pstr[len - 1] != '\0')
140 hMemW = GlobalReAlloc(hMemW, wlen * sizeof(wchar_t), 0);
146 * @brief Generate report and save it to file.
147 * @param [out] errStr Empty if succeeded, otherwise contains error message.
148 * @return `true` if report was created, `false` if user canceled report.
150 bool DirCmpReport::GenerateReport(String &errStr)
152 assert(m_pList != nullptr);
153 assert(m_pFile == nullptr);
157 if (m_bCopyToClipboard)
159 if (!OpenClipboard(NULL))
161 if (!EmptyClipboard())
163 CSharedFile file(GMEM_DDESHARE|GMEM_MOVEABLE|GMEM_ZEROINIT);
165 bool savedIncludeFileCmpReport = m_bIncludeFileCmpReport;
166 m_bIncludeFileCmpReport = false;
167 GenerateReport(m_nReportType);
168 HGLOBAL hMem = file.Detach();
169 SetClipboardData(CF_UNICODETEXT, ConvertToUTF16ForClipboard(hMem, m_bOutputUTF8 ? CP_UTF8 : CP_THREAD_ACP));
171 // If report type is HTML, render CF_HTML format as well
172 if (m_nReportType == REPORT_TYPE_SIMPLEHTML)
174 // Reconstruct the CSharedFile object
176 file.CSharedFile::CSharedFile(GMEM_DDESHARE|GMEM_MOVEABLE|GMEM_ZEROINIT);
177 // Write preliminary CF_HTML header with all offsets zero
178 static const char header[] =
182 "StartFragment:%09d\n"
183 "EndFragment:%09d\n";
184 static const char start[] = "<html><body>\n<!--StartFragment -->";
185 static const char end[] = "\n<!--EndFragment -->\n</body>\n</html>\n";
186 char buffer[MAX_PATH_FULL];
187 int cbHeader = wsprintfA(buffer, header, 0, 0, 0, 0);
188 file.Write(buffer, cbHeader);
189 file.Write(start, sizeof start - 1);
190 GenerateHTMLHeaderBodyPortion();
191 GenerateXmlHtmlContent(false);
192 file.Write(end, sizeof end); // include terminating zero
193 DWORD size = GetLength32(file);
194 // Rewrite CF_HTML header with valid offsets
196 wsprintfA(buffer, header, cbHeader,
197 static_cast<int>(size - 1),
198 static_cast<int>(cbHeader + sizeof start - 1),
199 static_cast<int>(size - sizeof end + 1));
200 file.Write(buffer, cbHeader);
201 SetClipboardData(CF_HTML, GlobalReAlloc(file.Detach(), size, 0));
204 m_bIncludeFileCmpReport = savedIncludeFileCmpReport;
206 if (!m_sReportFile.empty())
209 paths::SplitFilename(m_sReportFile, &path, nullptr, nullptr);
210 if (!paths::CreateIfNeeded(path))
212 errStr = _("Folder does not exist.");
215 CFile file(m_sReportFile.c_str(),
216 CFile::modeWrite|CFile::modeCreate|CFile::shareDenyWrite);
218 GenerateReport(m_nReportType);
222 catch (CException *e)
224 e->ReportError(MB_ICONSTOP);
232 * @brief Generate report of given type.
233 * @param [in] nReportType Type of report.
235 void DirCmpReport::GenerateReport(REPORT_TYPE nReportType)
239 case REPORT_TYPE_SIMPLEHTML:
240 m_bOutputUTF8 = true;
241 GenerateHTMLHeader();
242 GenerateXmlHtmlContent(false);
243 GenerateHTMLFooter();
245 case REPORT_TYPE_SIMPLEXML:
246 m_bOutputUTF8 = true;
248 GenerateXmlHtmlContent(true);
251 case REPORT_TYPE_COMMALIST:
252 m_bOutputUTF8 = false;
253 m_sSeparator = _T(",");
257 case REPORT_TYPE_TABLIST:
258 m_bOutputUTF8 = false;
259 m_sSeparator = _T("\t");
267 * @brief Write text to report file.
268 * @param [in] pszText Text to write to report file.
270 void DirCmpReport::WriteString(const String& sText)
272 std::string sOctets(m_bOutputUTF8 ? ucr::toUTF8(sText) : ucr::toThreadCP(sText));
273 const char *pchOctets = sOctets.c_str();
274 void *pvOctets = const_cast<char *>(pchOctets);
275 size_t cchAhead = sOctets.length();
276 while (const char *pchAhead = (const char *)memchr(pchOctets, '\n', cchAhead))
278 size_t cchLine = pchAhead - pchOctets;
279 m_pFile->Write(pchOctets, static_cast<unsigned>(cchLine));
280 static const char eol[] = { '\r', '\n' };
281 m_pFile->Write(eol, sizeof eol);
283 pchOctets += cchLine;
286 m_pFile->Write(pchOctets, static_cast<unsigned>(cchAhead));
291 * @brief Write text to report file while turning special chars to entities.
292 * @param [in] sText Text to write to report file.
294 void DirCmpReport::WriteStringEntityAware(const String& sText)
296 WriteString(ucr::toTString(CMarkdown::Entities(ucr::toUTF8(sText))));
300 * @brief Generate header-data for report.
302 void DirCmpReport::GenerateHeader()
304 WriteString(m_sTitle);
305 WriteString(_T("\n"));
306 WriteString(GetCurrentTimeString());
307 WriteString(_T("\n"));
308 for (int currCol = 0; currCol < m_nColumns; currCol++)
310 WriteString(m_pList->GetColumnName(currCol));
311 // Add col-separator, but not after last column
312 if (currCol < m_nColumns - 1)
313 WriteString(m_sSeparator);
318 * @brief Generate report content (compared items).
320 void DirCmpReport::GenerateContent()
322 int nRows = m_pList->GetRowCount();
324 // Report:Detail. All currently displayed columns will be added
325 for (int currRow = 0; currRow < nRows; currRow++)
327 if (m_myStruct && m_myStruct->context->GetAbortable()->ShouldAbort())
329 WriteString(_T("\n"));
330 DIFFITEM* pdi = reinterpret_cast<DIFFITEM*>(m_pList->GetItemData(currRow));
331 if (reinterpret_cast<uintptr_t>(pdi) == -1)
334 m_myStruct->context->m_pCompareStats->BeginCompare(pdi, 0);
335 for (int currCol = 0; currCol < m_nColumns; currCol++)
337 String value = m_pList->GetItemText(currRow, currCol);
338 if (value.find(m_sSeparator) != String::npos) {
339 WriteString(_T("\""));
341 WriteString(_T("\""));
346 // Add col-separator, but not after last column
347 if (currCol < m_nColumns - 1)
348 WriteString(m_sSeparator);
351 m_myStruct->context->m_pCompareStats->AddItem(-1);
357 * @brief Generate simple html report header.
359 void DirCmpReport::GenerateHTMLHeader()
361 WriteString(_T("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\"\n")
362 _T("\t\"http://www.w3.org/TR/html4/loose.dtd\">\n")
363 _T("<html>\n<head>\n")
364 _T("\t<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n")
366 WriteStringEntityAware(m_sTitle);
367 WriteString(_T("</title>\n"));
368 WriteString(_T("\t<style type=\"text/css\">\n\t<!--\n"));
369 WriteString(_T("\t\tbody {\n"));
370 WriteString(_T("\t\t\tfont-family: sans-serif;\n"));
371 WriteString(_T("\t\t\tfont-size: smaller;\n"));
372 WriteString(_T("\t\t}\n"));
373 WriteString(_T("\t\ttable {\n"));
374 WriteString(_T("\t\t\tborder-collapse: collapse;\n"));
375 WriteString(_T("\t\t\tborder: 1px solid gray;\n"));
376 WriteString(_T("\t\t}\n"));
377 WriteString(_T("\t\tth,td {\n"));
378 WriteString(_T("\t\t\tpadding: 3px;\n"));
379 WriteString(_T("\t\t\ttext-align: left;\n"));
380 WriteString(_T("\t\t\tvertical-align: middle;\n"));
381 WriteString(_T("\t\t\tborder: 1px solid gray;\n"));
382 WriteString(_T("\t\t}\n"));
383 WriteString(_T("\t\tth {\n"));
384 WriteString(_T("\t\t\tcolor: white;\n"));
385 WriteString(_T("\t\t\tbackground: blue;\n"));
386 WriteString(_T("\t\t\tpadding: 4px 4px;\n"));
387 WriteString(_T("\t\t\tbackground: linear-gradient(mediumblue, darkblue);\n"));
388 WriteString(_T("\t\t}\n"));
390 std::vector<bool> usedIcon(m_pList->GetIconCount());
392 for (int i = 0; i < m_pList->GetRowCount(); ++i)
394 usedIcon[m_pList->GetIconIndex(i)] = true;
395 maxIndent = (std::max)(m_pList->GetIndent(i), maxIndent);
397 for (int i = 0; i < m_pList->GetIconCount(); ++i)
401 std::ostringstream stream;
402 Poco::Base64Encoder enc(stream);
403 enc.rdbuf()->setLineLength(0);
404 enc << m_pList->GetIconPNGData(i);
406 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())));
409 for (int i = 0; i < maxIndent + 1; ++i)
410 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));
411 WriteString(_T("\t-->\n\t</style>\n"));
412 WriteString(_T("</head>\n<body>\n"));
413 GenerateHTMLHeaderBodyPortion();
417 * @brief Generate body portion of simple html report header (w/o body tag).
419 void DirCmpReport::GenerateHTMLHeaderBodyPortion()
421 WriteString(_T("<h2>"));
422 WriteString(m_sTitle);
423 WriteString(_T("</h2>\n<p>"));
424 WriteString(GetCurrentTimeString());
425 WriteString(_T("</p>\n"));
426 WriteString(_T("<table border=\"1\">\n<tr>\n"));
428 for (int currCol = 0; currCol < m_nColumns; currCol++)
430 WriteString(_T("<th>"));
431 WriteStringEntityAware(m_pList->GetColumnName(currCol));
432 WriteString(_T("</th>"));
434 WriteString(_T("</tr>\n"));
438 * @brief Generate simple xml report header.
440 void DirCmpReport::GenerateXmlHeader()
442 WriteString(_T("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
443 _T("<WinMergeDiffReport version=\"2\">\n")
445 WriteStringEntityAware(m_rootPaths.GetLeft());
446 WriteString(_T("</left>\n")
448 WriteStringEntityAware(m_rootPaths.GetRight());
449 WriteString(_T("</right>\n")
451 WriteStringEntityAware(GetCurrentTimeString());
452 WriteString(_T("</time>\n"));
454 // Add column headers
455 const String rowEl = _T("column_name");
456 WriteString(BeginEl(rowEl));
457 for (int currCol = 0; currCol < m_nColumns; currCol++)
459 const String colEl = m_colRegKeys[currCol];
460 WriteString(BeginEl(colEl));
461 WriteStringEntityAware(m_pList->GetColumnName(currCol));
462 WriteString(EndEl(colEl));
464 WriteString(EndEl(rowEl) + _T("\n"));
468 * @brief Generate simple html or xml report content.
470 void DirCmpReport::GenerateXmlHtmlContent(bool xml)
472 String sFileName, sParentDir;
473 paths::SplitFilename((const TCHAR *)m_pFile->GetFilePath(), &sParentDir, &sFileName, nullptr);
474 String sRelDestDir = sFileName.substr(0, sFileName.find_last_of(_T("."))) + _T(".files");
475 String sDestDir = paths::ConcatPath(sParentDir, sRelDestDir);
476 if (!xml && m_bIncludeFileCmpReport && m_pFileCmpReport != nullptr)
477 paths::CreateIfNeeded(sDestDir);
479 int nRows = m_pList->GetRowCount();
481 // Report:Detail. All currently displayed columns will be added
482 for (int currRow = 0; currRow < nRows; currRow++)
484 if (m_myStruct && m_myStruct->context->GetAbortable()->ShouldAbort())
486 DIFFITEM* pdi = reinterpret_cast<DIFFITEM*>(m_pList->GetItemData(currRow));
487 if (reinterpret_cast<uintptr_t>(pdi) == -1)
491 m_myStruct->context->m_pCompareStats->BeginCompare(pdi, 0);
492 if (!xml && m_bIncludeFileCmpReport && m_pFileCmpReport != nullptr)
493 (*m_pFileCmpReport.get())(REPORT_TYPE_SIMPLEHTML, m_pList.get(), currRow, sDestDir, sLinkPath);
495 String rowEl = _T("tr");
498 rowEl = _T("filediff");
499 WriteString(BeginEl(rowEl));
503 COLORREF backcolor = m_pList->GetBackColor(currRow);
504 COLORREF textcolor = m_pList->GetTextColor(currRow);
505 String attr = strutils::format(_T("style='%sbackground-color: #%02x%02x%02x'"),
506 textcolor == 0 ? _T("") : strutils::format(_T("color: #%02x%02x%02x; "),
507 GetRValue(textcolor), GetGValue(textcolor), GetBValue(textcolor)).c_str(),
508 GetRValue(backcolor), GetGValue(backcolor), GetBValue(backcolor));
509 WriteString(BeginEl(rowEl, attr));
511 for (int currCol = 0; currCol < m_nColumns; currCol++)
513 String colEl = _T("td");
516 colEl = m_colRegKeys[currCol];
517 WriteString(BeginEl(colEl));
522 WriteString(BeginEl(colEl, strutils::format(_T("class=\"icon%d indent%d\""), m_pList->GetIconIndex(currRow), m_pList->GetIndent(currRow))));
524 WriteString(BeginEl(colEl));
526 if (currCol == 0 && !sLinkPath.empty())
528 WriteString(_T("<a href=\""));
529 WriteString(sRelDestDir);
530 WriteString(_T("/"));
531 WriteString(sLinkPath);
532 WriteString(_T("\">"));
533 WriteString(m_pList->GetItemText(currRow, currCol));
534 WriteString(_T("</a>"));
538 WriteStringEntityAware(m_pList->GetItemText(currRow, currCol));
540 WriteString(EndEl(colEl));
542 WriteString(EndEl(rowEl) + _T("\n"));
544 m_myStruct->context->m_pCompareStats->AddItem(-1);
547 WriteString(_T("</table>\n"));
551 * @brief Generate simple html report footer.
553 void DirCmpReport::GenerateHTMLFooter()
555 WriteString(_T("</body>\n</html>\n"));
559 * @brief Generate simple xml report header.
561 void DirCmpReport::GenerateXmlFooter()
563 WriteString(_T("</WinMergeDiffReport>\n"));