1 /************************************************************************
2 **
3 **  Copyright (C) 2016-2020 Kevin B. Hendricks, Stratford, Ontario Canada
4 **  Copyright (C) 2009-2011 Strahinja Markovic  <strahinja.markovic@gmail.com>
5 **
6 **  This file is part of Sigil.
7 **
8 **  Sigil is free software: you can redistribute it and/or modify
9 **  it under the terms of the GNU General Public License as published by
10 **  the Free Software Foundation, either version 3 of the License, or
11 **  (at your option) any later version.
12 **
13 **  Sigil is distributed in the hope that it will be useful,
14 **  but WITHOUT ANY WARRANTY; without even the implied warranty of
15 **  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 **  GNU General Public License for more details.
17 **
18 **  You should have received a copy of the GNU General Public License
19 **  along with Sigil.  If not, see <http://www.gnu.org/licenses/>.
20 **
21 *************************************************************************/
22 
23 #include <QtCore/QXmlStreamWriter>
24 
25 #include "BookManipulation/Book.h"
26 #include "Exporters/NCXWriter.h"
27 #include "Misc/Utility.h"
28 #include "ResourceObjects/HTMLResource.h"
29 #include "ResourceObjects/Resource.h"
30 #include "ResourceObjects/NCXResource.h"
31 #include "sigil_constants.h"
32 
NCXWriter(const Book * book,QIODevice & device)33 NCXWriter::NCXWriter(const Book *book, QIODevice &device)
34     :
35     XMLWriter(book, device),
36     m_Headings(),
37     m_TOCRootEntry(TOCModel::TOCEntry()),
38     m_version(book->GetConstOPF()->GetEpubVersion()),
39     m_ncxresource(book->GetConstNCX())
40 
41 {
42     // Remove the Nav resource from list of HTMLResources if it exists (EPUB3)
43     QList<HTMLResource*> htmlresources = book->GetFolderKeeper()->GetResourceTypeList<HTMLResource>(true);
44     HTMLResource* nav_resource = book->GetConstOPF()->GetNavResource();
45     if (nav_resource) {
46         htmlresources.removeOne(nav_resource);
47     }
48 
49     m_Headings = Headings::MakeHeadingHeirarchy(Headings::GetHeadingList(htmlresources));
50 }
51 
52 
NCXWriter(const Book * book,QIODevice & device,TOCModel::TOCEntry toc_root_entry)53 NCXWriter::NCXWriter(const Book *book, QIODevice &device, TOCModel::TOCEntry toc_root_entry)
54     :
55     XMLWriter(book, device),
56     m_TOCRootEntry(toc_root_entry),
57     m_version(book->GetConstOPF()->GetEpubVersion()),
58     m_ncxresource(book->GetConstNCX())
59 
60 {
61 }
62 
63 
WriteXMLFromHeadings()64 void NCXWriter::WriteXMLFromHeadings()
65 {
66     m_TOCRootEntry = ConvertHeadingsToTOC();
67     WriteXML();
68 }
69 
70 
WriteXML()71 void NCXWriter::WriteXML()
72 {
73     m_Writer->writeStartDocument();
74     if (m_version.startsWith('2')) {
75         m_Writer->writeDTD("<!DOCTYPE ncx PUBLIC \"-//NISO//DTD ncx 2005-1//EN\"\n"
76                            "   \"http://www.daisy.org/z3986/2005/ncx-2005-1.dtd\">\n");
77     }
78     m_Writer->writeStartElement("ncx");
79     m_Writer->writeAttribute("xmlns", "http://www.daisy.org/z3986/2005/ncx/");
80     m_Writer->writeAttribute("version", "2005-1");
81     WriteHead();
82     WriteDocTitle();
83     WriteNavMap();
84     m_Writer->writeEndElement();
85     m_Writer->writeEndDocument();
86 }
87 
88 
WriteHead()89 void NCXWriter::WriteHead()
90 {
91     m_Writer->writeStartElement("head");
92     m_Writer->writeEmptyElement("meta");
93     m_Writer->writeAttribute("name", "dtb:uid");
94     m_Writer->writeAttribute("content", m_Book->GetPublicationIdentifier());
95     m_Writer->writeEmptyElement("meta");
96     m_Writer->writeAttribute("name", "dtb:depth");
97     m_Writer->writeAttribute("content", QString::number(GetTOCDepth()));
98     m_Writer->writeEmptyElement("meta");
99     m_Writer->writeAttribute("name", "dtb:totalPageCount");
100     m_Writer->writeAttribute("content", "0");
101     m_Writer->writeEmptyElement("meta");
102     m_Writer->writeAttribute("name", "dtb:maxPageNumber");
103     m_Writer->writeAttribute("content", "0");
104     m_Writer->writeEndElement();
105 }
106 
107 
WriteDocTitle()108 void NCXWriter::WriteDocTitle()
109 {
110     QString document_title;
111     QStringList titles = m_Book->GetMetadataValues("dc:title");
112 
113     if (titles.isEmpty()) {
114         document_title = "Unknown";
115     } else { // FIXME: handle multiple titles
116         document_title = titles.first();
117     }
118 
119     m_Writer->writeStartElement("docTitle");
120     m_Writer->writeTextElement("text", document_title);
121     m_Writer->writeEndElement();
122 }
123 
124 
WriteNavMap()125 void NCXWriter::WriteNavMap()
126 {
127     int play_order = 1;
128     m_Writer->writeStartElement("navMap");
129 
130     if (!m_TOCRootEntry.children.isEmpty()) {
131         // The NavMap is written recursively;
132         // WriteNavPoint is called for each entry in the tree
133         foreach(TOCModel::TOCEntry entry, m_TOCRootEntry.children) {
134             WriteNavPoint(entry, play_order);
135         }
136     } else {
137         // No headings? Well the spec *demands* an NCX file
138         // with a NavMap with at least one NavPoint, so we
139         // write a dummy one.
140         WriteFallbackNavPoint();
141     }
142 
143     m_Writer->writeEndElement();
144 }
145 
146 
WriteFallbackNavPoint()147 void NCXWriter::WriteFallbackNavPoint()
148 {
149     m_Writer->writeStartElement("navPoint");
150     m_Writer->writeAttribute("id", QString("navPoint-%1").arg(1));
151     m_Writer->writeAttribute("playOrder", QString("%1").arg(1));
152     m_Writer->writeStartElement("navLabel");
153     m_Writer->writeTextElement("text", "Start");
154     m_Writer->writeEndElement();
155     QList<HTMLResource *> html_resources = m_Book->GetFolderKeeper()->GetResourceTypeList<HTMLResource>(true);
156     Q_ASSERT(!html_resources.isEmpty());
157     m_Writer->writeEmptyElement("content");
158     QString srcpath = html_resources.at(0)->GetRelativePathFromResource(m_ncxresource);
159     m_Writer->writeAttribute("src", Utility::URLEncodePath(srcpath));
160     m_Writer->writeEndElement();
161 }
162 
163 
ConvertHeadingsToTOC()164 TOCModel::TOCEntry NCXWriter::ConvertHeadingsToTOC()
165 {
166     TOCModel::TOCEntry toc_root;
167     foreach(const Headings::Heading & heading, m_Headings) {
168         toc_root.children.append(ConvertHeadingWalker(heading));
169     }
170     return toc_root;
171 }
172 
173 
ConvertHeadingWalker(const Headings::Heading & heading)174 TOCModel::TOCEntry NCXWriter::ConvertHeadingWalker(const Headings::Heading &heading)
175 {
176     TOCModel::TOCEntry toc_child;
177 
178     if (heading.include_in_toc) {
179         toc_child.text = heading.text;
180 
181         QString heading_file = heading.resource_file->GetRelativePath();
182         QString id_to_use = heading.id;
183 
184         // If this heading appears right after a section break,
185         // then it "represents" and links to its file; otherwise,
186         // we link to the heading element directly
187         toc_child.target = Utility::URLEncodePath(heading_file);
188         if (!heading.at_file_start) {
189             toc_child.target = toc_child.target + "#" + Utility::URLEncodePath(id_to_use);
190         }
191     }
192 
193     foreach(Headings::Heading child_heading, heading.children) {
194         toc_child.children.append(ConvertHeadingWalker(child_heading));
195     }
196     return toc_child;
197 }
198 
199 
200 // Note TOCModel::TOCEntry target is now a URLEncoded book path with a possible fragment added
201 // This allows mixing targets created from the Nav and the NCX to both be properly
202 // represented in a TOCEntry since they are properly converted to book paths
WriteNavPoint(const TOCModel::TOCEntry & entry,int & play_order)203 void NCXWriter::WriteNavPoint(const TOCModel::TOCEntry &entry, int &play_order)
204 {
205     m_Writer->writeStartElement("navPoint");
206     m_Writer->writeAttribute("id", QString("navPoint-%1").arg(play_order));
207     m_Writer->writeAttribute("playOrder", QString("%1").arg(play_order));
208     play_order++;
209     m_Writer->writeStartElement("navLabel");
210     // Compress whitespace that pretty-print may add.
211     m_Writer->writeTextElement("text", entry.text.simplified());
212     m_Writer->writeEndElement();
213     m_Writer->writeEmptyElement("content");
214     QString srctarget = ConvertBookPathToNCXRelative(entry.target);
215     m_Writer->writeAttribute("src", srctarget);
216     foreach(TOCModel::TOCEntry child, entry.children) {
217         WriteNavPoint(child, play_order);
218     }
219     m_Writer->writeEndElement();
220 }
221 
222 
GetTOCDepth() const223 int NCXWriter::GetTOCDepth() const
224 {
225     int max_depth = 0;
226     foreach(TOCModel::TOCEntry entry, m_TOCRootEntry.children) {
227         int current_depth = 0;
228         TOCDepthWalker(entry , current_depth, max_depth);
229     }
230     return max_depth;
231 }
232 
233 
TOCDepthWalker(const TOCModel::TOCEntry & entry,int & current_depth,int & max_depth) const234 void NCXWriter::TOCDepthWalker(const TOCModel::TOCEntry &entry , int &current_depth, int &max_depth) const
235 {
236     current_depth++;
237 
238     if (current_depth > max_depth) {
239         max_depth = current_depth;
240     }
241 
242     foreach(TOCModel::TOCEntry child_entry , entry.children) {
243         int new_current_depth = current_depth;
244         TOCDepthWalker(child_entry, new_current_depth, max_depth);
245     }
246 }
247 
248 
ConvertBookPathToNCXRelative(const QString & bookpath)249 QString NCXWriter::ConvertBookPathToNCXRelative(const QString & bookpath)
250 {
251     QString ncx_bkpath = m_ncxresource->GetRelativePath();
252     // split off any fragment added to bookpath destination
253     QStringList pieces = bookpath.split('#', QString::KeepEmptyParts);
254     QString dest_bkpath = pieces.at(0);
255     QString fragment = "";
256     if (pieces.size() > 1) fragment = pieces.at(1);
257     QString new_href = Utility::buildRelativePath(ncx_bkpath, Utility::URLDecodePath(dest_bkpath));
258     new_href = Utility::URLEncodePath(new_href);
259     if (!fragment.isEmpty()) {
260         new_href = new_href + "#" + fragment;
261     }
262     return new_href;
263 }
264