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