OSDN Git Service

Add Expand Different Subfolders menu item (#1964)
[winmerge-jp/winmerge-jp.git] / Src / DirCmpReport.cpp
1 /** 
2  * @file  DirCmpReport.cpp
3  *
4  * @brief Implementation file for DirCmpReport
5  *
6  */
7
8 #include "stdafx.h"
9 #include <ctime>
10 #include <cassert>
11 #include <sstream>
12 #include <algorithm>
13 #include <Poco/Base64Encoder.h>
14 #include "locality.h"
15 #include "DirCmpReport.h"
16 #include "paths.h"
17 #include "unicoder.h"
18 #include "markdown.h"
19 #include "CompareStats.h"
20 #include "DiffItem.h"
21 #include "DiffThread.h"
22 #include "IAbortable.h"
23
24 UINT CF_HTML = RegisterClipboardFormat(_T("HTML Format"));
25
26 /**
27  * @brief Return current time as string.
28  * @return Current time as String.
29  */
30 static String GetCurrentTimeString()
31 {
32         time_t nTime = 0;
33         time(&nTime);
34         _int64 nTime64 = nTime;
35         String str = locality::TimeString(&nTime64);
36         return str;
37 }
38
39 /**
40  * @brief Format string as beginning tag.
41  * @param [in] elName String to format as beginning tag.
42  * @return String formatted as beginning tag.
43  */
44 static String BeginEl(const String& elName, const String& attr = _T(""))
45 {
46         if (attr.empty())
47                 return strutils::format(_T("<%s>"), elName);
48         else
49                 return strutils::format(_T("<%s %s>"), elName, attr);
50 }
51
52 /**
53  * @brief Format string as ending tag.
54  * @param [in] elName String to format as ending tag.
55  * @return String formatted as ending tag.
56  */
57 static String EndEl(const String& elName)
58 {
59         return strutils::format(_T("</%s>"), elName);
60 }
61
62 /**
63  * @brief Constructor.
64  */
65 DirCmpReport::DirCmpReport(const std::vector<String> & colRegKeys)
66 : m_pList(nullptr)
67 , m_pFile(nullptr)
68 , m_nColumns(0)
69 , m_colRegKeys(colRegKeys)
70 , m_sSeparator(_T(","))
71 , m_pFileCmpReport(nullptr)
72 , m_bIncludeFileCmpReport(false)
73 , m_bOutputUTF8(false)
74 , m_myStruct(nullptr)
75 , m_bCopyToClipboard(false)
76 , m_nReportType(REPORT_TYPE_COMMALIST)
77 {
78 }
79
80 /**
81  * @brief Set UI-list pointer.
82  */
83 void DirCmpReport::SetList(IListCtrl *pList)
84 {
85         m_pList.reset(pList);
86 }
87
88 /**
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.
91  */
92 void DirCmpReport::SetRootPaths(const PathContext &paths)
93 {
94         if (paths.GetSize() < 3)
95         {
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());
100         }
101         else {
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());
107         }
108 }
109
110 /**
111  * @brief Set column-count.
112  */
113 void DirCmpReport::SetColumns(int columns)
114 {
115         m_nColumns = columns;
116 }
117
118 /**
119  * @brief Set file compare reporter functor
120  */
121 void DirCmpReport::SetFileCmpReport(IFileCmpReport *pFileCmpReport)
122 {
123         m_pFileCmpReport.reset(pFileCmpReport);
124 }
125
126 static ULONG GetLength32(CFile const &f)
127 {
128         ULONGLONG length = f.GetLength();
129         if (length > ULONG_MAX)
130                 length = ULONG_MAX;
131         return static_cast<ULONG>(length);
132 }
133
134 static HGLOBAL ConvertToUTF16ForClipboard(HGLOBAL hMem, int codepage)
135 {
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)
139                 return nullptr;
140         LPCSTR pstr = reinterpret_cast<LPCSTR>(GlobalLock(hMem));
141         LPWSTR pwstr = reinterpret_cast<LPWSTR>(GlobalLock(hMemW));
142         if (pstr == nullptr || pwstr == nullptr)
143                 return 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')
146         {
147                 pwstr[wlen] = 0;
148                 ++wlen;
149         }
150         GlobalUnlock(hMemW);
151         hMemW = GlobalReAlloc(hMemW, wlen * sizeof(wchar_t), 0);
152         GlobalUnlock(hMem);
153         return hMemW;
154 }
155
156 /**
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.
160  */
161 bool DirCmpReport::GenerateReport(String &errStr)
162 {
163         assert(m_pList != nullptr);
164         assert(m_pFile == nullptr);
165         bool bRet = false;
166         try
167         {
168                 if (m_bCopyToClipboard)
169                 {
170                         if (!OpenClipboard(NULL))
171                                 return false;
172                         if (!EmptyClipboard())
173                                 return false;
174                         CSharedFile file(GMEM_DDESHARE|GMEM_MOVEABLE|GMEM_ZEROINIT);
175                         m_pFile = &file;
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));
181                         GlobalFree(hMem);
182                         // If report type is HTML, render CF_HTML format as well
183                         if (m_nReportType == REPORT_TYPE_SIMPLEHTML)
184                         {
185                                 // Reconstruct the CSharedFile object
186                                 file.~CSharedFile();
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[] =
190                                         "Version:0.9\n"
191                                         "StartHTML:%09d\n"
192                                         "EndHTML:%09d\n"
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
206                                 file.SeekToBegin();
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));
213                         }
214                         CloseClipboard();
215                         m_bIncludeFileCmpReport = savedIncludeFileCmpReport;
216                 }
217                 if (!m_sReportFile.empty())
218                 {
219                         String path;
220                         paths::SplitFilename(m_sReportFile, &path, nullptr, nullptr);
221                         if (!paths::CreateIfNeeded(path))
222                         {
223                                 errStr = _("Folder does not exist.");
224                                 return false;
225                         }
226                         CFile file(m_sReportFile.c_str(),
227                                 CFile::modeWrite|CFile::modeCreate|CFile::shareDenyWrite);
228                         m_pFile = &file;
229                         GenerateReport(m_nReportType);
230                 }
231                 bRet = true;
232         }
233         catch (CException *e)
234         {
235                 e->ReportError(MB_ICONSTOP);
236                 e->Delete();
237         }
238         m_pFile = nullptr;
239         return bRet;
240 }
241
242 /**
243  * @brief Generate report of given type.
244  * @param [in] nReportType Type of report.
245  */
246 void DirCmpReport::GenerateReport(REPORT_TYPE nReportType)
247 {
248         switch (nReportType)
249         {
250         case REPORT_TYPE_SIMPLEHTML:
251                 m_bOutputUTF8 = true;
252                 GenerateHTMLHeader();
253                 GenerateXmlHtmlContent(false);
254                 GenerateHTMLFooter();
255                 break;
256         case REPORT_TYPE_SIMPLEXML:
257                 m_bOutputUTF8 = true;
258                 GenerateXmlHeader();
259                 GenerateXmlHtmlContent(true);
260                 GenerateXmlFooter();
261                 break;
262         case REPORT_TYPE_COMMALIST:
263                 m_bOutputUTF8 = false;
264                 m_sSeparator = _T(",");
265                 GenerateHeader();
266                 GenerateContent();
267                 break;
268         case REPORT_TYPE_TABLIST:
269                 m_bOutputUTF8 = false;
270                 m_sSeparator = _T("\t");
271                 GenerateHeader();
272                 GenerateContent();
273                 break;
274         }
275 }
276
277 /**
278  * @brief Write text to report file.
279  * @param [in] pszText Text to write to report file.
280  */
281 void DirCmpReport::WriteString(const String& sText)
282 {
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))
287         {
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);
292                 ++cchLine;
293                 pchOctets += cchLine;
294                 cchAhead -= cchLine;
295         }
296         m_pFile->Write(pchOctets, static_cast<unsigned>(cchAhead));
297
298 }
299
300 /**
301  * @brief Write text to report file while turning special chars to entities.
302  * @param [in] sText Text to write to report file.
303  */
304 void DirCmpReport::WriteStringEntityAware(const String& sText)
305 {
306         WriteString(ucr::toTString(CMarkdown::Entities(ucr::toUTF8(sText))));
307 }
308
309 /**
310  * @brief Generate header-data for report.
311  */
312 void DirCmpReport::GenerateHeader()
313 {
314         WriteString(m_sTitle);
315         WriteString(_T("\n"));
316         WriteString(GetCurrentTimeString());
317         WriteString(_T("\n"));
318         for (int currCol = 0; currCol < m_nColumns; currCol++)
319         {
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);
324         }
325 }
326
327 /**
328  * @brief Generate report content (compared items).
329  */
330 void DirCmpReport::GenerateContent()
331 {
332         int nRows = m_pList->GetRowCount();
333
334         // Report:Detail. All currently displayed columns will be added
335         for (int currRow = 0; currRow < nRows; currRow++)
336         {
337                 if (m_myStruct && m_myStruct->context->GetAbortable()->ShouldAbort())
338                         break;
339                 WriteString(_T("\n"));
340                 DIFFITEM* pdi = reinterpret_cast<DIFFITEM*>(m_pList->GetItemData(currRow));
341                 if (reinterpret_cast<uintptr_t>(pdi) == -1)
342                         continue;
343                 if (m_myStruct)
344                         m_myStruct->context->m_pCompareStats->BeginCompare(pdi, 0);
345                 for (int currCol = 0; currCol < m_nColumns; currCol++)
346                 {
347                         String value = m_pList->GetItemText(currRow, currCol);
348                         if (value.find(m_sSeparator) != String::npos) {
349                                 WriteString(_T("\""));
350                                 WriteString(value);
351                                 WriteString(_T("\""));
352                         }
353                         else
354                                 WriteString(value);
355
356                         // Add col-separator, but not after last column
357                         if (currCol < m_nColumns - 1)
358                                 WriteString(m_sSeparator);
359                 }
360                 if (m_myStruct)
361                         m_myStruct->context->m_pCompareStats->AddItem(-1);
362         }
363
364 }
365
366 /**
367  * @brief Generate simple html report header.
368  */
369 void DirCmpReport::GenerateHTMLHeader()
370 {
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")
375                 _T("\t<title>"));
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"));
400
401         std::vector<bool> usedIcon(m_pList->GetIconCount());
402         int maxIndent = 0;
403         for (int i = 0; i < m_pList->GetRowCount(); ++i)
404         {
405                 usedIcon[m_pList->GetIconIndex(i)] = true;
406                 maxIndent = (std::max)(m_pList->GetIndent(i), maxIndent);
407         }
408         for (int i = 0; i < m_pList->GetIconCount(); ++i)
409         {
410                 if (usedIcon[i])
411                 {
412                         std::ostringstream stream;
413                         Poco::Base64Encoder enc(stream);
414                         enc.rdbuf()->setLineLength(0);
415                         enc << m_pList->GetIconPNGData(i);
416                         enc.close();
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())));
418                 }
419         }
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();
425 }
426
427 /**
428  * @brief Generate body portion of simple html report header (w/o body tag).
429  */
430 void DirCmpReport::GenerateHTMLHeaderBodyPortion()
431 {
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"));
438
439         for (int currCol = 0; currCol < m_nColumns; currCol++)
440         {
441                 WriteString(_T("<th>"));
442                 WriteStringEntityAware(m_pList->GetColumnName(currCol));
443                 WriteString(_T("</th>"));
444         }
445         WriteString(_T("</tr>\n"));
446 }
447
448 /**
449  * @brief Generate simple xml report header.
450  */
451 void DirCmpReport::GenerateXmlHeader()
452 {
453         WriteString(_T("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
454                                 _T("<WinMergeDiffReport version=\"2\">\n")
455                                 _T("<left>"));
456         WriteStringEntityAware(m_rootPaths.GetLeft());
457         WriteString(_T("</left>\n"));
458         if (m_rootPaths.GetSize() == 3)
459         {
460                 WriteString(_T("<middle>"));
461                 WriteStringEntityAware(m_rootPaths.GetMiddle());
462                 WriteString(_T("</middle>\n"));
463         }
464         WriteString(_T("<right>"));
465         WriteStringEntityAware(m_rootPaths.GetRight());
466         WriteString(_T("</right>\n")
467                                 _T("<time>"));
468         WriteStringEntityAware(GetCurrentTimeString());
469         WriteString(_T("</time>\n"));
470
471         // Add column headers
472         const String rowEl = _T("column_name");
473         WriteString(BeginEl(rowEl));
474         for (int currCol = 0; currCol < m_nColumns; currCol++)
475         {
476                 const String colEl = m_colRegKeys[currCol];
477                 WriteString(BeginEl(colEl));
478                 WriteStringEntityAware(m_pList->GetColumnName(currCol));
479                 WriteString(EndEl(colEl));
480         }
481         WriteString(EndEl(rowEl) + _T("\n"));
482 }
483
484 /**
485  * @brief Generate simple html or xml report content.
486  */
487 void DirCmpReport::GenerateXmlHtmlContent(bool xml)
488 {
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);
495
496         int nRows = m_pList->GetRowCount();
497
498         // Report:Detail. All currently displayed columns will be added
499         for (int currRow = 0; currRow < nRows; currRow++)
500         {
501                 if (m_myStruct && m_myStruct->context->GetAbortable()->ShouldAbort())
502                         break;
503                 DIFFITEM* pdi = reinterpret_cast<DIFFITEM*>(m_pList->GetItemData(currRow));
504                 if (reinterpret_cast<uintptr_t>(pdi) == -1)
505                         continue;
506                 String sLinkPath;
507                 if (m_myStruct)
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);
511
512                 strutils::replace(sLinkPath, _T("%"), _T("%25"));
513                 strutils::replace(sLinkPath, _T("#"), _T("%23"));
514
515                 String rowEl = _T("tr");
516                 if (xml)
517                 {
518                         rowEl = _T("filediff");
519                         WriteString(BeginEl(rowEl));
520                 }
521                 else
522                 {
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));
530                 }
531                 for (int currCol = 0; currCol < m_nColumns; currCol++)
532                 {
533                         String colEl = _T("td");
534                         if (xml)
535                         {
536                                 colEl = m_colRegKeys[currCol];
537                                 WriteString(BeginEl(colEl));
538                         }
539                         else
540                         {
541                                 if (currCol == 0)
542                                         WriteString(BeginEl(colEl, strutils::format(_T("class=\"icon%d indent%d\""), m_pList->GetIconIndex(currRow), m_pList->GetIndent(currRow))));
543                                 else
544                                         WriteString(BeginEl(colEl));
545                         }
546                         if (currCol == 0 && !sLinkPath.empty())
547                         {
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>"));
555                         }
556                         else
557                         {
558                                 WriteStringEntityAware(m_pList->GetItemText(currRow, currCol));
559                         }
560                         WriteString(EndEl(colEl));
561                 }
562                 WriteString(EndEl(rowEl) + _T("\n"));
563                 if (m_myStruct)
564                         m_myStruct->context->m_pCompareStats->AddItem(-1);
565         }
566         if (!xml)
567                 WriteString(_T("</table>\n"));
568 }
569
570 /**
571  * @brief Generate simple html report footer.
572  */
573 void DirCmpReport::GenerateHTMLFooter()
574 {
575         WriteString(_T("</body>\n</html>\n"));
576 }
577
578 /**
579  * @brief Generate simple xml report header.
580  */
581 void DirCmpReport::GenerateXmlFooter()
582 {
583         WriteString(_T("</WinMergeDiffReport>\n"));
584 }
585