1 /************************************************************************
2 **
3 **  Copyright (C) 2015-2021 Kevin B. Hendricks, Stratford, Ontario, Canada
4 **  Copyright (C) 2013      John Schember <john@nachtimwald.com>
5 **  Copyright (C) 2013      Dave Heiland
6 **
7 **  This file is part of Sigil.
8 **
9 **  Sigil is free software: you can redistribute it and/or modify
10 **  it under the terms of the GNU General Public License as published by
11 **  the Free Software Foundation, either version 3 of the License, or
12 **  (at your option) any later version.
13 **
14 **  Sigil is distributed in the hope that it will be useful,
15 **  but WITHOUT ANY WARRANTY; without even the implied warranty of
16 **  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 **  GNU General Public License for more details.
18 **
19 **  You should have received a copy of the GNU General Public License
20 **  along with Sigil.  If not, see <http://www.gnu.org/licenses/>.
21 **
22 *************************************************************************/
23 
24 #include <QtCore/QFile>
25 #include <QtCore/QFileInfo>
26 #include <QtWidgets/QFileDialog>
27 #include <QtGui/QFont>
28 #include <QtWidgets/QMessageBox>
29 #include <QtWidgets/QPushButton>
30 
31 #include "sigil_exception.h"
32 #include "BookManipulation/FolderKeeper.h"
33 #include "BookManipulation/XhtmlDoc.h"
34 #include "Dialogs/ReportsWidgets/LinksWidget.h"
35 #include "Misc/HTMLSpellCheck.h"
36 #include "Misc/NumericItem.h"
37 #include "Misc/SettingsStore.h"
38 #include "Misc/Utility.h"
39 #include "ResourceObjects/HTMLResource.h"
40 
41 static const QString SETTINGS_GROUP = "reports";
42 static const QString DEFAULT_REPORT_FILE = "LinksReport.csv";
43 
LinksWidget()44 LinksWidget::LinksWidget()
45     :
46     m_ItemModel(new QStandardItemModel),
47     m_LastDirSaved(QString()),
48     m_LastFileSaved(QString())
49 {
50     ui.setupUi(this);
51     connectSignalsSlots();
52 }
53 
~LinksWidget()54 LinksWidget::~LinksWidget()
55 {
56     delete m_ItemModel;
57 }
58 
CreateReport(QSharedPointer<Book> book)59 void LinksWidget::CreateReport(QSharedPointer<Book> book)
60 {
61     m_Book = book;
62     m_HTMLResources = m_Book->GetFolderKeeper()->GetResourceTypeList<HTMLResource>(false);
63 
64     SetupTable();
65 }
66 
SetupTable(int sort_column,Qt::SortOrder sort_order)67 void LinksWidget::SetupTable(int sort_column, Qt::SortOrder sort_order)
68 {
69     m_ItemModel->clear();
70     QStringList header;
71     header.append(tr("File"));
72     header.append(tr("Line"));
73     header.append(tr("ID"));
74     header.append(tr("Text"));
75     header.append(tr("Target File"));
76     header.append(tr("Target ID"));
77     header.append(tr("Target Exists?"));
78     header.append(tr("Target Text"));
79     header.append(tr("Target's Target File"));
80     header.append(tr("Target's Target ID"));
81     header.append(tr("Match?"));
82     m_ItemModel->setHorizontalHeaderLabels(header);
83     ui.fileTree->setSelectionBehavior(QAbstractItemView::SelectRows);
84     ui.fileTree->setModel(m_ItemModel);
85     ui.fileTree->header()->setSortIndicatorShown(true);
86     ui.fileTree->header()->setToolTip(
87         tr("Report shows all source and target links using the anchor tag \"a\".")
88     );
89 
90     // Key is book path of html file
91     QHash<QString, QList<XhtmlDoc::XMLElement> > links = m_Book->GetLinkElements();
92 
93     // Key is book path of html file
94     QHash<QString, QStringList> all_ids = m_Book->GetIdsInHTMLFiles();
95 
96     // html_filenames is a list of html book paths
97     QStringList html_filenames;
98     foreach(Resource *resource, m_HTMLResources) {
99         html_filenames.append(resource->GetRelativePath());
100     }
101 
102     foreach(Resource *resource, m_HTMLResources) {
103         QString filepath = resource->GetRelativePath();
104         QString path = resource->GetFullPath();
105         QString file_spname = resource->ShortPathName();
106 
107         foreach(XhtmlDoc::XMLElement element, links[filepath]) {
108             QList<QStandardItem *> rowItems;
109 
110             // Source file
111             QStandardItem *item = new QStandardItem();
112             QString source_file = file_spname;
113             item->setText(source_file);
114             item->setToolTip(filepath);
115             item->setData(filepath);
116             rowItems << item;
117 
118             // Source Line Number
119             item = new QStandardItem();
120             QString lineno = QString::number(element.lineno);
121             item->setText(lineno);
122             item->setToolTip(filepath);
123             rowItems << item;
124 
125             // Source id
126             item = new QStandardItem();
127             QString source_id = element.attributes["id"];
128             item->setText(source_id);
129             rowItems << item;
130 
131             // Source text
132             item = new QStandardItem();
133             item->setText(element.text);
134             rowItems << item;
135 
136             // Source target file & id
137             QString href = element.attributes["href"];
138             QUrl url(href);
139             QString href_file;
140             QString href_id;
141             bool is_target_file = false;
142             if (url.scheme().isEmpty() || url.scheme() == "file") {
143                 href_file = url.path();
144                 href_id = url.fragment();
145                 is_target_file = true;
146             } else {
147                 // Just show url
148                 href_file = href;
149             }
150             item = new QStandardItem();
151             item->setText(href_file);
152             rowItems << item;
153 
154             item = new QStandardItem();
155             item->setText(href_id);
156             rowItems << item;
157 
158             // Target exists in book
159             QString target_valid = tr("n/a");
160             QString bkpath;
161             if (is_target_file) {
162                 if (!href.isEmpty()) {
163                     target_valid = tr("no");
164                     // first handle the case of local internal link (just fragment)
165                     if (href_file.isEmpty()) {
166                         bkpath = filepath;
167                     } else {
168                         // find bookpath of target
169                         bkpath = Utility::buildBookPath(href_file, resource->GetFolder());
170                     }
171                     if (html_filenames.contains(bkpath)) {
172                         if (href_id.isEmpty() || all_ids[bkpath].contains(href_id)) {
173                             target_valid = tr("yes");
174                         }
175                     }
176                 }
177             }
178             item = new QStandardItem();
179             item->setText(target_valid);
180             rowItems << item;
181 
182             if (is_target_file && !href_id.isEmpty()) {
183                 // Find the target element for this link
184                 // As long as an anchor tag was used!
185                 XhtmlDoc::XMLElement target;
186                 bool found = false;
187 
188                 foreach(XhtmlDoc::XMLElement target_element, links[bkpath]) {
189                     if (href_id == target_element.attributes["id"]) {
190                         target = target_element;
191                         found = true;
192                         break;
193                     }
194                 }
195                 if (found) {
196 
197                     // Target Text
198                     item = new QStandardItem();
199                     item->setText(target.text);
200                     rowItems << item;
201 
202                     // Target's Target file and id
203                     QString target_href = target.attributes["href"];
204                     QUrl target_url(target_href);
205                     QString target_href_file;
206                     QString target_href_id;
207                     if (target_url.scheme().isEmpty() || target_url.scheme() == "file") {
208                         target_href_file = target_url.path();
209                         target_href_id = target_url.fragment();
210                     } else {
211                         // Just show url
212                         target_href_file = target_href;
213                     }
214 
215                     item = new QStandardItem();
216                     item->setText(target_href_file);
217                     rowItems << item;
218 
219                     item = new QStandardItem();
220                     item->setText(target_href_id);
221                     rowItems << item;
222 
223                     QString target_bkpath;
224                     // Match - destination link points to source
225                     if (target_href_file.isEmpty()) {
226                         target_bkpath = bkpath;
227                     } else {
228                         Resource * res =  m_Book->GetFolderKeeper()->GetResourceByBookPath(bkpath);
229                         target_bkpath = Utility::buildBookPath(target_href_file, res->GetFolder());
230                     }
231                     QString match = tr("no");
232                     if (!source_id.isEmpty() && filepath == target_bkpath && source_id == target_href_id) {
233                         match = tr("yes");
234                     }
235                     item = new QStandardItem();
236                     item->setText(match);
237                     rowItems << item;
238                 }
239             }
240             // Add item to table
241             m_ItemModel->appendRow(rowItems);
242             for (int i = 0; i < rowItems.count(); i++) {
243                 rowItems[i]->setEditable(false);
244             }
245         }
246     }
247 
248     for (int i = 0; i < ui.fileTree->header()->count(); i++) {
249         ui.fileTree->resizeColumnToContents(i);
250     }
251 }
252 
253 
FilterEditTextChangedSlot(const QString & text)254 void LinksWidget::FilterEditTextChangedSlot(const QString &text)
255 {
256     const QString lowercaseText = text.toLower();
257     QStandardItem *root_item = m_ItemModel->invisibleRootItem();
258     QModelIndex parent_index;
259     // Hide rows that don't contain the filter text
260     int first_visible_row = -1;
261 
262     for (int row = 0; row < root_item->rowCount(); row++) {
263         if (text.isEmpty() || root_item->child(row, 0)->text().toLower().contains(lowercaseText) ||
264             root_item->child(row, 2)->text().toLower().contains(lowercaseText) ||
265             root_item->child(row, 3)->text().toLower().contains(lowercaseText) ||
266             root_item->child(row, 4)->text().toLower().contains(lowercaseText) ||
267             root_item->child(row, 5)->text().toLower().contains(lowercaseText) ||
268             root_item->child(row, 6)->text().toLower().contains(lowercaseText)) {
269             ui.fileTree->setRowHidden(row, parent_index, false);
270 
271             if (first_visible_row == -1) {
272                 first_visible_row = row;
273             }
274         } else {
275             ui.fileTree->setRowHidden(row, parent_index, true);
276         }
277     }
278 
279     if (!text.isEmpty() && first_visible_row != -1) {
280         // Select the first non-hidden row
281         ui.fileTree->setCurrentIndex(root_item->child(first_visible_row, 0)->index());
282     } else {
283         // Clear current and selection, which clears preview image
284         ui.fileTree->setCurrentIndex(QModelIndex());
285     }
286 }
287 
DoubleClick()288 void LinksWidget::DoubleClick()
289 {
290     QModelIndex index = ui.fileTree->selectionModel()->selectedRows(0).first();
291     if (index.row() != m_ItemModel->rowCount() - 1) {
292         // IMPORTANT:  file name is in column 0, and line number is in column 1
293         // This should match order of header above
294         QString bookpath = m_ItemModel->item(index.row(), 0)->data().toString();
295         QString lineno = m_ItemModel->item(index.row(), 1)->text();
296         emit OpenFileRequest(bookpath, lineno.toInt(), -1);
297     }
298 }
299 
Save()300 void LinksWidget::Save()
301 {
302     QStringList report_info;
303     QStringList heading_row;
304 
305     // Get headings
306     for (int col = 0; col < ui.fileTree->header()->count(); col++) {
307         QStandardItem *item = m_ItemModel->horizontalHeaderItem(col);
308         QString text = "";
309         if (item) {
310             text = item->text();
311         }
312         heading_row << text;
313     }
314     report_info << Utility::createCSVLine(heading_row);
315 
316     // Get data from table
317     for (int row = 0; row < m_ItemModel->rowCount(); row++) {
318         QStringList data_row;
319         for (int col = 0; col < ui.fileTree->header()->count(); col++) {
320             QStandardItem *item = m_ItemModel->item(row, col);
321             QString text = "";
322             if (item) {
323                 text = item->text();
324             }
325             data_row << text;
326         }
327         report_info << Utility::createCSVLine(data_row);
328     }
329 
330     QString data = report_info.join('\n') + '\n';
331     // Save the file
332     ReadSettings();
333     QString filter_string = "*.csv;;*.txt;;*.*";
334     QString default_filter = "";
335     QString save_path = m_LastDirSaved + "/" + m_LastFileSaved;
336     QFileDialog::Options options = QFileDialog::Options();
337 #ifdef Q_OS_MAC
338     options = options | QFileDialog::DontUseNativeDialog;
339 #endif
340 
341     QString destination = QFileDialog::getSaveFileName(this,
342                                                        tr("Save Report As Comma Separated File"),
343                                                        save_path,
344                                                        filter_string,
345                                                        &default_filter,
346                                                        options);
347 
348     if (destination.isEmpty()) {
349         return;
350     }
351 
352     try {
353         Utility::WriteUnicodeTextFile(data, destination);
354     } catch (CannotOpenFile&) {
355         QMessageBox::warning(this, tr("Sigil"), tr("Cannot save report file."));
356     }
357 
358     m_LastDirSaved = QFileInfo(destination).absolutePath();
359     m_LastFileSaved = QFileInfo(destination).fileName();
360     WriteSettings();
361 }
362 
ReadSettings()363 void LinksWidget::ReadSettings()
364 {
365     SettingsStore settings;
366     settings.beginGroup(SETTINGS_GROUP);
367     // Last file open
368     m_LastDirSaved = settings.value("last_dir_saved").toString();
369     m_LastFileSaved = settings.value("last_file_saved_links").toString();
370 
371     if (m_LastFileSaved.isEmpty()) {
372         m_LastFileSaved = DEFAULT_REPORT_FILE;
373     }
374 
375     settings.endGroup();
376 }
377 
WriteSettings()378 void LinksWidget::WriteSettings()
379 {
380     SettingsStore settings;
381     settings.beginGroup(SETTINGS_GROUP);
382     // Last file open
383     settings.setValue("last_dir_saved", m_LastDirSaved);
384     settings.setValue("last_file_saved_links", m_LastFileSaved);
385     settings.endGroup();
386 }
387 
388 
connectSignalsSlots()389 void LinksWidget::connectSignalsSlots()
390 {
391     connect(ui.leFilter,  SIGNAL(textChanged(QString)),
392             this,         SLOT(FilterEditTextChangedSlot(QString)));
393     connect(ui.fileTree, SIGNAL(doubleClicked(const QModelIndex &)),
394             this,         SLOT(DoubleClick()));
395     connect(ui.buttonBox->button(QDialogButtonBox::Close), SIGNAL(clicked()), this, SIGNAL(CloseDialog()));
396     connect(ui.buttonBox->button(QDialogButtonBox::Save), SIGNAL(clicked()), this, SLOT(Save()));
397 }
398 
399