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 ¤t_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