summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJaakko Keränen <jaakko.keranen@iki.fi>2021-05-02 09:07:15 +0300
committerJaakko Keränen <jaakko.keranen@iki.fi>2021-05-02 09:07:15 +0300
commita14895149fad1724e0f4b4df1fd5834bddc4ead4 (patch)
tree15795172fe0f47c6ed06a707a520d506d768e564
parenteae0ef64d071e2702fffc1d00f223124f8c6d8b4 (diff)
Added a Gempub helper
`Gempub` opens and parses a Gempub archive and provides access to the contents in a common way.
-rw-r--r--CMakeLists.txt2
-rw-r--r--src/gempub.c210
-rw-r--r--src/gempub.h58
-rw-r--r--src/gmrequest.c21
-rw-r--r--src/gmutil.c17
-rw-r--r--src/gmutil.h5
-rw-r--r--src/mimehooks.c112
-rw-r--r--src/ui/documentwidget.c145
8 files changed, 390 insertions, 180 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 1813fd70..52c6a6e5 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -111,6 +111,8 @@ set (SOURCES
111 src/defs.h 111 src/defs.h
112 src/feeds.c 112 src/feeds.c
113 src/feeds.h 113 src/feeds.h
114 src/gempub.c
115 src/gempub.h
114 src/gmcerts.c 116 src/gmcerts.c
115 src/gmcerts.h 117 src/gmcerts.h
116 src/gmdocument.c 118 src/gmdocument.c
diff --git a/src/gempub.c b/src/gempub.c
new file mode 100644
index 00000000..45a4c1b3
--- /dev/null
+++ b/src/gempub.c
@@ -0,0 +1,210 @@
1/* Copyright 2021 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
23#include "gempub.h"
24#include "gmutil.h"
25#include "lang.h"
26#include "defs.h"
27#include "ui/util.h"
28
29#include <the_Foundation/archive.h>
30#include <the_Foundation/file.h>
31#include <the_Foundation/path.h>
32
33const char *mimeType_Gempub = "application/gpub+zip";
34
35struct Impl_Gempub {
36 iArchive *arch;
37 iString baseUrl;
38 iString props[max_GempubProperty];
39};
40
41iDefineTypeConstruction(Gempub)
42
43void init_Gempub(iGempub *d) {
44 d->arch = NULL;
45 init_String(&d->baseUrl);
46 iForIndices(i, d->props) {
47 init_String(&d->props[i]);
48 }
49}
50
51void deinit_Gempub(iGempub *d) {
52 iForIndices(i, d->props) {
53 deinit_String(&d->props[i]);
54 }
55 deinit_String(&d->baseUrl);
56 iRelease(d->arch);
57}
58
59static iBool parseMetadata_Gempub_(iGempub *d) {
60 iAssert(isOpen_Archive(d->arch));
61 /* Parse the metadata and check if the required contents are present. */
62 const iBlock *metadata = dataCStr_Archive(d->arch, "metadata.txt");
63 if (!metadata) {
64 return iFalse;
65 }
66 static const char *labels[max_GempubProperty] = {
67 "title:",
68 "index:",
69 "author:",
70 "language:",
71 "description:",
72 "published:",
73 "publishDate:",
74 "revisionDate:",
75 "copyright:",
76 "license:",
77 "version:",
78 "cover:",
79 };
80 /* Default values. */
81 setCStr_String(&d->props[title_GempubProperty], "Untitled Book");
82 setCStr_String(&d->props[cover_GempubProperty],
83 entryCStr_Archive(d->arch, "cover.jpg") ? "cover.jpg" :
84 entryCStr_Archive(d->arch, "cover.png") ? "cover.png" : "");
85 setCStr_String(&d->props[index_GempubProperty], "index.gmi");
86 iRangecc line = iNullRange;
87 while (nextSplit_Rangecc(range_Block(metadata), "\n", &line)) {
88 iRangecc clean = line;
89 trim_Rangecc(&clean);
90 iForIndices(i, d->props) {
91 if (startsWithCase_Rangecc(clean, labels[i])) {
92 setRange_String(&d->props[i],
93 (iRangecc){ clean.start + strlen(labels[i]), clean.end });
94 trim_String(&d->props[i]);
95 }
96 }
97 }
98 return iTrue;
99}
100
101iBool open_Gempub(iGempub *d, const iBlock *data) {
102 close_Gempub(d);
103 d->arch = new_Archive();
104 if (openData_Archive(d->arch, data) && parseMetadata_Gempub_(d)) {
105 return iTrue;
106 }
107 close_Gempub(d);
108 return iFalse;
109}
110
111iBool openFile_Gempub(iGempub *d, const iString *path) {
112 close_Gempub(d);
113 iFile *f = new_File(path);
114 if (open_File(f, readOnly_FileMode)) {
115 iBlock *data = readAll_File(f);
116 open_Gempub(d, data);
117 delete_Block(data);
118 setBaseUrl_Gempub(d, collect_String(makeFileUrl_String(path)));
119 }
120 iRelease(f);
121 return isOpen_Gempub(d);
122}
123
124iBool openUrl_Gempub(iGempub *d, const iString *url) {
125 if (!openFile_Gempub(d, collect_String(localFilePathFromUrl_String(url)))) {
126 setBaseUrl_Gempub(d, url);
127 close_Gempub(d);
128 return iFalse;
129 }
130 return iTrue;
131}
132
133void close_Gempub(iGempub *d) {
134 if (d->arch) {
135 iReleasePtr(&d->arch);
136 }
137 iForIndices(i, d->props) {
138 clear_String(&d->props[i]);
139 }
140}
141
142iBool isOpen_Gempub(const iGempub *d) {
143 return d->arch != NULL;
144}
145
146void setBaseUrl_Gempub(iGempub *d, const iString *url) {
147 set_String(&d->baseUrl, url);
148}
149
150static iBool hasProperty_Gempub_(const iGempub *d, enum iGempubProperty prop) {
151 return !isEmpty_String(&d->props[prop]);
152}
153
154static void appendProperty_Gempub_(const iGempub *d, const char *label,
155 enum iGempubProperty prop, iString *out) {
156 if (hasProperty_Gempub_(d, prop)) {
157 appendFormat_String(out, "%s %s\n", label, cstr_String(&d->props[prop]));
158 }
159}
160
161static iBool isRemote_Gempub_(const iGempub *d) {
162 return !equalCase_Rangecc(urlScheme_String(&d->baseUrl), "file");
163}
164
165iBlock *coverPageSource_Gempub(const iGempub *d) {
166 iAssert(!isEmpty_String(&d->baseUrl));
167 const iString *baseUrl = withSpacesEncoded_String(&d->baseUrl);
168 iString *out = new_String();
169 format_String(out, "# %s\n",
170 cstr_String(&d->props[title_GempubProperty]));
171 if (!isEmpty_String(&d->props[description_GempubProperty])) {
172 appendFormat_String(out, "%s\n", cstr_String(&d->props[description_GempubProperty]));
173 }
174 appendCStr_String(out, "\n");
175 appendProperty_Gempub_(d, "Author:", author_GempubProperty, out);
176 if (!isRemote_Gempub_(d)) {
177 appendFormat_String(out, "\n=> %s " book_Icon " View Gempub contents\n",
178 cstrCollect_String(concat_Path(baseUrl, &d->props[index_GempubProperty])));
179 if (hasProperty_Gempub_(d, cover_GempubProperty)) {
180 appendFormat_String(out, "\n=> %s Cover image\n",
181 cstrCollect_String(concat_Path(baseUrl, &d->props[cover_GempubProperty])));
182 }
183 }
184 else {
185 iString *key = collectNew_String(); /* TODO: add a helper for this */
186 toString_Sym(SDLK_s, KMOD_PRIMARY, key);
187 appendCStr_String(out, "\nThis Gempub book can be viewed after it has been saved locally. ");
188 appendFormat_String(out,
189 cstr_Lang("error.unsupported.suggestsave"),
190 cstr_String(key),
191 saveToDownloads_Label);
192 translate_Lang(out);
193 appendCStr_String(out, "\n");
194 }
195 appendCStr_String(out, "\n## About this book\n");
196 appendProperty_Gempub_(d, "Version:", version_GempubProperty, out);
197 appendProperty_Gempub_(d, "Revision date:", revisionDate_GempubProperty, out);
198 if (hasProperty_Gempub_(d, publishDate_GempubProperty)) {
199 appendProperty_Gempub_(d, "Publish date:", publishDate_GempubProperty, out);
200 }
201 else {
202 appendProperty_Gempub_(d, "Published:", published_GempubProperty, out);
203 }
204 appendProperty_Gempub_(d, "Language:", language_GempubProperty, out);
205 appendProperty_Gempub_(d, "License:", license_GempubProperty, out);
206 appendProperty_Gempub_(d, "\u00a9", copyright_GempubProperty, out);
207 iBlock *output = copy_Block(utf8_String(out));
208 delete_String(out);
209 return output;
210}
diff --git a/src/gempub.h b/src/gempub.h
new file mode 100644
index 00000000..41d14824
--- /dev/null
+++ b/src/gempub.h
@@ -0,0 +1,58 @@
1/* Copyright 2021 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
23#pragma once
24
25#include <the_Foundation/string.h>
26
27iDeclareType(Gempub)
28iDeclareTypeConstruction(Gempub)
29
30enum iGempubProperty {
31 title_GempubProperty,
32 index_GempubProperty,
33 author_GempubProperty,
34 language_GempubProperty,
35 description_GempubProperty,
36 published_GempubProperty,
37 publishDate_GempubProperty,
38 revisionDate_GempubProperty,
39 copyright_GempubProperty,
40 license_GempubProperty,
41 version_GempubProperty,
42 cover_GempubProperty,
43 max_GempubProperty
44};
45
46iBool open_Gempub (iGempub *, const iBlock *data);
47iBool openFile_Gempub (iGempub *, const iString *path);
48iBool openUrl_Gempub (iGempub *, const iString *url);
49void close_Gempub (iGempub *);
50
51void setBaseUrl_Gempub (iGempub *, const iString *baseUrl);
52
53iBool isOpen_Gempub (const iGempub *);
54iBlock * coverPageSource_Gempub (const iGempub *);
55
56const iString *property_Gempub (const iGempub *, enum iGempubProperty);
57
58extern const char *mimeType_Gempub;
diff --git a/src/gmrequest.c b/src/gmrequest.c
index 8ab37c61..cdf4e582 100644
--- a/src/gmrequest.c
+++ b/src/gmrequest.c
@@ -535,21 +535,6 @@ void setUrl_GmRequest(iGmRequest *d, const iString *url) {
535 urlEncodeSpaces_String(&d->url); 535 urlEncodeSpaces_String(&d->url);
536} 536}
537 537
538static const iString *findContainerArchive_(const iString *path) {
539 iBeginCollect();
540 while (!isEmpty_String(path) && cmp_String(path, ".")) {
541 iString *dir = newRange_String(dirName_Path(path));
542 if (endsWithCase_String(dir, ".zip") ||
543 endsWithCase_String(dir, ".gpub")) {
544 iEndCollect();
545 return collect_String(dir);
546 }
547 path = collect_String(dir);
548 }
549 iEndCollect();
550 return NULL;
551}
552
553static iBool isDirectory_(const iString *path) { 538static iBool isDirectory_(const iString *path) {
554 /* TODO: move this to the_Foundation */ 539 /* TODO: move this to the_Foundation */
555 iFileInfo *info = new_FileInfo(path); 540 iFileInfo *info = new_FileInfo(path);
@@ -652,7 +637,7 @@ void submit_GmRequest(iGmRequest *d) {
652 } 637 }
653 else if (open_File(f, readOnly_FileMode)) { 638 else if (open_File(f, readOnly_FileMode)) {
654 resp->statusCode = success_GmStatusCode; 639 resp->statusCode = success_GmStatusCode;
655 setCStr_String(&resp->meta, mediaTypeFromPath_String(path)); 640 setCStr_String(&resp->meta, mediaType_Path(path));
656 /* TODO: Detect text files based on contents? E.g., is the content valid UTF-8. */ 641 /* TODO: Detect text files based on contents? E.g., is the content valid UTF-8. */
657 set_Block(&resp->body, collect_Block(readAll_File(f))); 642 set_Block(&resp->body, collect_Block(readAll_File(f)));
658 d->state = receivingBody_GmRequestState; 643 d->state = receivingBody_GmRequestState;
@@ -660,7 +645,7 @@ void submit_GmRequest(iGmRequest *d) {
660 } 645 }
661 else { 646 else {
662 /* It could be a path inside an archive. */ 647 /* It could be a path inside an archive. */
663 const iString *container = findContainerArchive_(path); 648 const iString *container = findContainerArchive_Path(path);
664 if (container) { 649 if (container) {
665 iArchive *arch = iClob(new_Archive()); 650 iArchive *arch = iClob(new_Archive());
666 if (openFile_Archive(arch, container)) { 651 if (openFile_Archive(arch, container)) {
@@ -748,7 +733,7 @@ void submit_GmRequest(iGmRequest *d) {
748 const iBlock *data = data_Archive(arch, entryPath); 733 const iBlock *data = data_Archive(arch, entryPath);
749 if (data) { 734 if (data) {
750 resp->statusCode = success_GmStatusCode; 735 resp->statusCode = success_GmStatusCode;
751 setCStr_String(&resp->meta, mediaTypeFromPath_String(entryPath)); 736 setCStr_String(&resp->meta, mediaType_Path(entryPath));
752 set_Block(&resp->body, data); 737 set_Block(&resp->body, data);
753 } 738 }
754 else { 739 else {
diff --git a/src/gmutil.c b/src/gmutil.c
index 9f53ae87..a9d70fbb 100644
--- a/src/gmutil.c
+++ b/src/gmutil.c
@@ -438,7 +438,22 @@ iString *localFilePathFromUrl_String(const iString *d) {
438 return path; 438 return path;
439} 439}
440 440
441const char *mediaTypeFromPath_String(const iString *path) { 441const iString *findContainerArchive_Path(const iString *path) {
442 iBeginCollect();
443 while (!isEmpty_String(path) && cmp_String(path, ".")) {
444 iString *dir = newRange_String(dirName_Path(path));
445 if (endsWithCase_String(dir, ".zip") ||
446 endsWithCase_String(dir, ".gpub")) {
447 iEndCollect();
448 return collect_String(dir);
449 }
450 path = collect_String(dir);
451 }
452 iEndCollect();
453 return NULL;
454}
455
456const char *mediaType_Path(const iString *path) {
442 if (endsWithCase_String(path, ".gmi") || endsWithCase_String(path, ".gemini")) { 457 if (endsWithCase_String(path, ".gmi") || endsWithCase_String(path, ".gemini")) {
443 return "text/gemini; charset=utf-8"; 458 return "text/gemini; charset=utf-8";
444 } 459 }
diff --git a/src/gmutil.h b/src/gmutil.h
index 7aedbc47..84f31e2e 100644
--- a/src/gmutil.h
+++ b/src/gmutil.h
@@ -119,7 +119,10 @@ iString * localFilePathFromUrl_String(const iString *);
119void urlEncodeSpaces_String (iString *); 119void urlEncodeSpaces_String (iString *);
120const iString * withSpacesEncoded_String(const iString *); 120const iString * withSpacesEncoded_String(const iString *);
121 121
122const char * mediaTypeFromPath_String (const iString *path); 122const char * mediaType_Path (const iString *path);
123iRangecc mediaTypeWithoutParameters_Rangecc (iRangecc mime); 123iRangecc mediaTypeWithoutParameters_Rangecc (iRangecc mime);
124 124
125const iString * findContainerArchive_Path (const iString *path);
126
127
125const iString * feedEntryOpenCommand_String (const iString *url, int newTab); /* checks fragment */ 128const iString * feedEntryOpenCommand_String (const iString *url, int newTab); /* checks fragment */
diff --git a/src/mimehooks.c b/src/mimehooks.c
index 931c8138..364449ec 100644
--- a/src/mimehooks.c
+++ b/src/mimehooks.c
@@ -23,9 +23,9 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
23#include "mimehooks.h" 23#include "mimehooks.h"
24#include "defs.h" 24#include "defs.h"
25#include "gmutil.h" 25#include "gmutil.h"
26#include "gempub.h"
26#include "app.h" 27#include "app.h"
27 28
28#include <the_Foundation/archive.h>
29#include <the_Foundation/file.h> 29#include <the_Foundation/file.h>
30#include <the_Foundation/fileinfo.h> 30#include <the_Foundation/fileinfo.h>
31#include <the_Foundation/path.h> 31#include <the_Foundation/path.h>
@@ -203,112 +203,20 @@ finished:
203 return output; 203 return output;
204} 204}
205 205
206static void appendGemPubProperty_(iString *out, const char *key, const iString *value) { 206iBlock *translateGemPubCoverPage_(const iBlock *source, const iString *requestUrl) {
207 if (!isEmpty_String(value)) {
208 appendFormat_String(out, "%s %s\n", key, cstr_String(value));
209 }
210}
211
212iBlock *translateGemPubCoverPage_(const iString *mime, const iBlock *source,
213 const iString *requestUrl) {
214 iBlock *output = NULL; 207 iBlock *output = NULL;
215 iArchive *arch = new_Archive(); 208 iGempub *gempub = new_Gempub();
216 if (openData_Archive(arch, source)) { 209 if (open_Gempub(gempub, source)) {
217 /* Parse the metadata and check if the required contents are present. */ 210 setBaseUrl_Gempub(gempub, requestUrl);
218 const iBlock *metadata = dataCStr_Archive(arch, "metadata.txt"); 211 output = newCStr_Block("20 text/gemini; charset=utf-8\r\n");
219 if (!metadata) { 212 append_Block(output, collect_Block(coverPageSource_Gempub(gempub)));
220 goto cleanup;
221 }
222 enum iGemPubProperty {
223 title_GemPubProperty,
224 index_GemPubProperty,
225 author_GemPubProperty,
226 language_GemPubProperty,
227 description_GemPubProperty,
228 published_GemPubProperty,
229 publishDate_GemPubProperty,
230 revisionDate_GemPubProperty,
231 copyright_GemPubProperty,
232 license_GemPubProperty,
233 version_GemPubProperty,
234 cover_GemPubProperty,
235 max_GemPubProperty
236 };
237 static const char *labels[max_GemPubProperty] = {
238 "title:",
239 "index:",
240 "author:",
241 "language:",
242 "description:",
243 "published:",
244 "publishDate:",
245 "revisionDate:",
246 "copyright:",
247 "license:",
248 "version:",
249 "cover:",
250 };
251 iString *props[max_GemPubProperty];
252 iForIndices(i, props) {
253 props[i] = collectNew_String();
254 }
255 /* Default values. */
256 setCStr_String(props[title_GemPubProperty], "Untitled Book");
257 setCStr_String(props[cover_GemPubProperty],
258 entryCStr_Archive(arch, "cover.jpg") ? "cover.jpg" :
259 entryCStr_Archive(arch, "cover.png") ? "cover.png" : "");
260 setCStr_String(props[index_GemPubProperty], "index.gmi");
261 iRangecc line = iNullRange;
262 while (nextSplit_Rangecc(range_Block(metadata), "\n", &line)) {
263 iRangecc clean = line;
264 trim_Rangecc(&clean);
265 iForIndices(i, props) {
266 if (startsWithCase_Rangecc(clean, labels[i])) {
267 setRange_String(props[i], (iRangecc){ clean.start + strlen(labels[i]), clean.end });
268 trim_String(props[i]);
269 }
270 }
271 }
272 const iString *baseUrl = withSpacesEncoded_String(requestUrl);
273 iString *out = new_String();
274 format_String(out, "20 text/gemini; charset=utf-8\r\n"
275 "# %s\n",
276 cstr_String(props[title_GemPubProperty]));
277 if (!isEmpty_String(props[description_GemPubProperty])) {
278 appendFormat_String(out, "%s\n", cstr_String(props[description_GemPubProperty]));
279 }
280 appendCStr_String(out, "\n");
281 appendGemPubProperty_(out, "Author:", props[author_GemPubProperty]);
282 appendFormat_String(out, "\n=> %s " book_Icon " Book index page\n",
283 cstrCollect_String(concat_Path(baseUrl, props[index_GemPubProperty])));
284 if (!isEmpty_String(props[cover_GemPubProperty])) {
285 appendFormat_String(out, "\n=> %s/%s Cover image\n",
286 cstr_String(baseUrl),
287 cstr_String(props[cover_GemPubProperty]));
288 }
289 appendCStr_String(out, "\n## About this book\n");
290 appendGemPubProperty_(out, "Version:", props[version_GemPubProperty]);
291 appendGemPubProperty_(out, "Revision date:", props[revisionDate_GemPubProperty]);
292 if (!isEmpty_String(props[publishDate_GemPubProperty])) {
293 appendGemPubProperty_(out, "Publish date:", props[publishDate_GemPubProperty]);
294 }
295 else {
296 appendGemPubProperty_(out, "Published:", props[published_GemPubProperty]);
297 }
298 appendGemPubProperty_(out, "Language:", props[language_GemPubProperty]);
299 appendGemPubProperty_(out, "License:", props[license_GemPubProperty]);
300 appendGemPubProperty_(out, "\u00a9", props[copyright_GemPubProperty]);
301 output = copy_Block(utf8_String(out));
302 delete_String(out);
303 } 213 }
304cleanup: 214 delete_Gempub(gempub);
305 iRelease(arch);
306 return output; 215 return output;
307} 216}
308 217
309/*----------------------------------------------------------------------------------------------*/ 218/*----------------------------------------------------------------------------------------------*/
310 219
311static const char *gpubMimeType_MimeHooks_ = "application/gpub+zip";
312static const char *mimeHooksFilename_MimeHooks_ = "mimehooks.txt"; 220static const char *mimeHooksFilename_MimeHooks_ = "mimehooks.txt";
313 221
314struct Impl_MimeHooks { 222struct Impl_MimeHooks {
@@ -331,7 +239,7 @@ void deinit_MimeHooks(iMimeHooks *d) {
331static iBool checkGemPub_(const iString *mime, const iString *requestUrl) { 239static iBool checkGemPub_(const iString *mime, const iString *requestUrl) {
332 /* Only process GemPub in local files. */ 240 /* Only process GemPub in local files. */
333 return (equalCase_Rangecc(urlScheme_String(requestUrl), "file") && 241 return (equalCase_Rangecc(urlScheme_String(requestUrl), "file") &&
334 startsWithCase_String(mime, gpubMimeType_MimeHooks_)); 242 startsWithCase_String(mime, mimeType_Gempub));
335} 243}
336 244
337iBool willTryFilter_MimeHooks(const iMimeHooks *d, const iString *mime) { 245iBool willTryFilter_MimeHooks(const iMimeHooks *d, const iString *mime) {
@@ -367,7 +275,7 @@ iBlock *tryFilter_MimeHooks(const iMimeHooks *d, const iString *mime, const iBlo
367 } 275 }
368 /* Built-in filters. */ 276 /* Built-in filters. */
369 if (checkGemPub_(mime, requestUrl)) { 277 if (checkGemPub_(mime, requestUrl)) {
370 iBlock *result = translateGemPubCoverPage_(mime, body, requestUrl); 278 iBlock *result = translateGemPubCoverPage_(body, requestUrl);
371 if (result) { 279 if (result) {
372 return result; 280 return result;
373 } 281 }
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index 410b793e..f146a2df 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -30,6 +30,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
30#include "bookmarks.h" 30#include "bookmarks.h"
31#include "command.h" 31#include "command.h"
32#include "defs.h" 32#include "defs.h"
33#include "gempub.h"
33#include "gmcerts.h" 34#include "gmcerts.h"
34#include "gmdocument.h" 35#include "gmdocument.h"
35#include "gmrequest.h" 36#include "gmrequest.h"
@@ -1051,7 +1052,7 @@ static void updateFetchProgress_DocumentWidget_(iDocumentWidget *d) {
1051 1052
1052static const char *zipPageHeading_(const iRangecc mime) { 1053static const char *zipPageHeading_(const iRangecc mime) {
1053 if (equalCase_Rangecc(mime, "application/gpub+zip")) { 1054 if (equalCase_Rangecc(mime, "application/gpub+zip")) {
1054 return book_Icon " Gempub Book"; 1055 return book_Icon " Gempub";
1055 } 1056 }
1056 iRangecc type = iNullRange; 1057 iRangecc type = iNullRange;
1057 nextSplit_Rangecc(mime, "/", &type); /* skip the part before the slash */ 1058 nextSplit_Rangecc(mime, "/", &type); /* skip the part before the slash */
@@ -1065,6 +1066,56 @@ static const char *zipPageHeading_(const iRangecc mime) {
1065 return cstrCollect_String(heading); 1066 return cstrCollect_String(heading);
1066} 1067}
1067 1068
1069static void postProcessRequestContent_DocumentWidget_(iDocumentWidget *d) {
1070 if (!cmpCase_String(&d->sourceMime, "application/octet-stream") ||
1071 !cmpCase_String(&d->sourceMime, mimeType_Gempub) ||
1072 endsWithCase_String(d->mod.url, ".gpub")) {
1073 iGempub *gempub = new_Gempub();
1074 if (open_Gempub(gempub, &d->sourceContent)) {
1075 setBaseUrl_Gempub(gempub, d->mod.url);
1076 /* TODO: just return a String from coverPageSource_Gempub... */
1077 setSource_DocumentWidget(d, collect_String(newBlock_String(collect_Block(coverPageSource_Gempub(gempub)))));
1078 setCStr_String(&d->sourceMime, mimeType_Gempub);
1079 }
1080 delete_Gempub(gempub);
1081 }
1082 /* Gempub: Preload cover image. */ {
1083 /* TODO: move to gempub.c along with other related code */
1084 iString *localPath = localFilePathFromUrl_String(d->mod.url);
1085 if (localPath) {
1086 if (!iCmpStr(mediaType_Path(localPath), "application/gpub+zip")) {
1087 iArchive *arch = iClob(new_Archive());
1088 if (openFile_Archive(arch, localPath)) {
1089 iBool haveImage = iFalse;
1090 for (size_t linkId = 1; ; linkId++) {
1091 const iString *linkUrl = linkUrl_GmDocument(d->doc, linkId);
1092 if (!linkUrl) break;
1093 if (findLinkImage_Media(media_GmDocument(d->doc), linkId)) {
1094 continue; /* got this already */
1095 }
1096 if (linkFlags_GmDocument(d->doc, linkId) & imageFileExtension_GmLinkFlag) {
1097 iString *imgEntryPath = collect_String(localFilePathFromUrl_String(linkUrl));
1098 remove_Block(&imgEntryPath->chars, 0, size_String(localPath) + 1 /* slash, too */);
1099 setData_Media(media_GmDocument(d->doc),
1100 linkId,
1101 collectNewCStr_String(mediaType_Path(linkUrl)),
1102 data_Archive(arch, imgEntryPath),
1103 0);
1104 haveImage = iTrue;
1105 }
1106 }
1107 if (haveImage) {
1108 redoLayout_GmDocument(d->doc);
1109 updateVisible_DocumentWidget_(d);
1110 invalidate_DocumentWidget_(d);
1111 }
1112 }
1113 }
1114 delete_String(localPath);
1115 }
1116 }
1117}
1118
1068static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse *response, 1119static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse *response,
1069 const iBool isInitialUpdate) { 1120 const iBool isInitialUpdate) {
1070 if (d->state == ready_RequestState) { 1121 if (d->state == ready_RequestState) {
@@ -1113,12 +1164,17 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse
1113 iString *key = collectNew_String(); 1164 iString *key = collectNew_String();
1114 toString_Sym(SDLK_s, KMOD_PRIMARY, key); 1165 toString_Sym(SDLK_s, KMOD_PRIMARY, key);
1115 format_String(&str, "# %s\n" 1166 format_String(&str, "# %s\n"
1116 "%s is a compressed archive.\n\n%s\n\n", 1167 "%s is a compressed archive.\n\n",
1117 zipPageHeading_(param), 1168 zipPageHeading_(param),
1118 cstr_Rangecc(baseName_Path(d->mod.url)), 1169 cstr_Rangecc(baseName_Path(d->mod.url)));
1119 format_CStr(cstr_Lang("error.unsupported.suggestsave"), 1170 iString *localPath = localFilePathFromUrl_String(d->mod.url);
1120 cstr_String(key), 1171 if (!localPath) {
1121 saveToDownloads_Label)); 1172 appendFormat_String(&str, "%s\n\n",
1173 format_CStr(cstr_Lang("error.unsupported.suggestsave"),
1174 cstr_String(key),
1175 saveToDownloads_Label));
1176 }
1177 delete_String(localPath);
1122 if (equalCase_Rangecc(urlScheme_String(d->mod.url), "file")) { 1178 if (equalCase_Rangecc(urlScheme_String(d->mod.url), "file")) {
1123 appendFormat_String(&str, "=> %s/ View archive contents\n", 1179 appendFormat_String(&str, "=> %s/ View archive contents\n",
1124 cstr_String(withSpacesEncoded_String(d->mod.url))); 1180 cstr_String(withSpacesEncoded_String(d->mod.url)));
@@ -1188,43 +1244,6 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse
1188 if (setSource) { 1244 if (setSource) {
1189 setSource_DocumentWidget(d, &str); 1245 setSource_DocumentWidget(d, &str);
1190 } 1246 }
1191 if (isRequestFinished) {
1192 /* Gempub: Preload cover image. */ {
1193 /* TODO: move to a gempub.c along with other related code */
1194 iString *localPath = localFilePathFromUrl_String(d->mod.url);
1195 if (localPath) {
1196 if (!iCmpStr(mediaTypeFromPath_String(localPath), "application/gpub+zip")) {
1197 iArchive *arch = iClob(new_Archive());
1198 if (openFile_Archive(arch, localPath)) {
1199 iBool haveImage = iFalse;
1200 for (size_t linkId = 1; ; linkId++) {
1201 const iString *linkUrl = linkUrl_GmDocument(d->doc, linkId);
1202 if (!linkUrl) break;
1203 if (findLinkImage_Media(media_GmDocument(d->doc), linkId)) {
1204 continue; /* got this already */
1205 }
1206 if (linkFlags_GmDocument(d->doc, linkId) & imageFileExtension_GmLinkFlag) {
1207 iString *imgEntryPath = collect_String(localFilePathFromUrl_String(linkUrl));
1208 remove_Block(&imgEntryPath->chars, 0, size_String(localPath) + 1 /* slash, too */);
1209 setData_Media(media_GmDocument(d->doc),
1210 linkId,
1211 collectNewCStr_String(mediaTypeFromPath_String(linkUrl)),
1212 data_Archive(arch, imgEntryPath),
1213 0);
1214 haveImage = iTrue;
1215 }
1216 }
1217 if (haveImage) {
1218 redoLayout_GmDocument(d->doc);
1219 updateVisible_DocumentWidget_(d);
1220 invalidate_DocumentWidget_(d);
1221 }
1222 }
1223 }
1224 delete_String(localPath);
1225 }
1226 }
1227 }
1228 deinit_String(&str); 1247 deinit_String(&str);
1229 } 1248 }
1230} 1249}
@@ -1291,8 +1310,13 @@ static void cacheRunGlyphs_(void *data, const iGmRun *run) {
1291} 1310}
1292 1311
1293static void cacheDocumentGlyphs_DocumentWidget_(const iDocumentWidget *d) { 1312static void cacheDocumentGlyphs_DocumentWidget_(const iDocumentWidget *d) {
1294 if (isExposed_Window(get_Window())) { 1313 if (isFinishedLaunching_App() && isExposed_Window(get_Window())) {
1295 render_GmDocument(d->doc, (iRangei){ 0, size_GmDocument(d->doc).y }, cacheRunGlyphs_, NULL); 1314 /* Just cache the top of the document, since this is what we usually need. */
1315 int maxY = height_Widget(&d->widget) * 2;
1316 if (maxY == 0) {
1317 maxY = size_GmDocument(d->doc).y;
1318 }
1319 render_GmDocument(d->doc, (iRangei){ 0, maxY }, cacheRunGlyphs_, NULL);
1296 } 1320 }
1297} 1321}
1298 1322
@@ -1302,26 +1326,28 @@ static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) {
1302 const iGmResponse *resp = recent->cachedResponse; 1326 const iGmResponse *resp = recent->cachedResponse;
1303 clear_ObjectList(d->media); 1327 clear_ObjectList(d->media);
1304 reset_GmDocument(d->doc); 1328 reset_GmDocument(d->doc);
1305 d->state = fetching_RequestState;
1306 d->initNormScrollY = recent->normScrollY;
1307 resetWideRuns_DocumentWidget_(d); 1329 resetWideRuns_DocumentWidget_(d);
1308 /* Use the cached response data. */ 1330 d->state = fetching_RequestState;
1309 updateTrust_DocumentWidget_(d, resp); 1331 /* Do the fetch. */ {
1310 d->sourceTime = resp->when; 1332 d->initNormScrollY = recent->normScrollY;
1311 d->sourceStatus = success_GmStatusCode; 1333 /* Use the cached response data. */
1312 format_String(&d->sourceHeader, cstr_Lang("pageinfo.header.cached")); 1334 updateTrust_DocumentWidget_(d, resp);
1313 d->drawBufs->flags |= updateTimestampBuf_DrawBufsFlag; 1335 d->sourceTime = resp->when;
1314 set_Block(&d->sourceContent, &resp->body); 1336 d->sourceStatus = success_GmStatusCode;
1315 updateDocument_DocumentWidget_(d, resp, iTrue); 1337 format_String(&d->sourceHeader, cstr_Lang("pageinfo.header.cached"));
1316 init_Anim(&d->altTextOpacity, 0); 1338 set_Block(&d->sourceContent, &resp->body);
1339 updateDocument_DocumentWidget_(d, resp, iTrue);
1340 postProcessRequestContent_DocumentWidget_(d);
1341 }
1317 d->state = ready_RequestState; 1342 d->state = ready_RequestState;
1343 init_Anim(&d->altTextOpacity, 0);
1318 reset_SmoothScroll(&d->scrollY); 1344 reset_SmoothScroll(&d->scrollY);
1319 init_Anim(&d->scrollY.pos, d->initNormScrollY * size_GmDocument(d->doc).y); 1345 init_Anim(&d->scrollY.pos, d->initNormScrollY * size_GmDocument(d->doc).y);
1320 updateSideOpacity_DocumentWidget_(d, iFalse); 1346 updateSideOpacity_DocumentWidget_(d, iFalse);
1321 d->drawBufs->flags |= updateSideBuf_DrawBufsFlag;
1322 updateVisible_DocumentWidget_(d); 1347 updateVisible_DocumentWidget_(d);
1323 moveSpan_SmoothScroll(&d->scrollY, 0, 0); /* clamp position to new max */ 1348 moveSpan_SmoothScroll(&d->scrollY, 0, 0); /* clamp position to new max */
1324 cacheDocumentGlyphs_DocumentWidget_(d); 1349 cacheDocumentGlyphs_DocumentWidget_(d);
1350 d->drawBufs->flags |= updateTimestampBuf_DrawBufsFlag | updateSideBuf_DrawBufsFlag;
1325 postCommandf_Root(as_Widget(d)->root, "document.changed doc:%p url:%s", d, cstr_String(d->mod.url)); 1351 postCommandf_Root(as_Widget(d)->root, "document.changed doc:%p url:%s", d, cstr_String(d->mod.url));
1326 return iTrue; 1352 return iTrue;
1327 } 1353 }
@@ -2143,10 +2169,12 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2143 checkResponse_DocumentWidget_(d); 2169 checkResponse_DocumentWidget_(d);
2144 init_Anim(&d->scrollY.pos, d->initNormScrollY * size_GmDocument(d->doc).y); /* TODO: unless user already scrolled! */ 2170 init_Anim(&d->scrollY.pos, d->initNormScrollY * size_GmDocument(d->doc).y); /* TODO: unless user already scrolled! */
2145 d->state = ready_RequestState; 2171 d->state = ready_RequestState;
2172 postProcessRequestContent_DocumentWidget_(d);
2146 /* The response may be cached. */ 2173 /* The response may be cached. */
2147 if (d->request) { 2174 if (d->request) {
2148 if (!equal_Rangecc(urlScheme_String(d->mod.url), "about") && 2175 if (!equal_Rangecc(urlScheme_String(d->mod.url), "about") &&
2149 startsWithCase_String(meta_GmRequest(d->request), "text/")) { 2176 (startsWithCase_String(meta_GmRequest(d->request), "text/") ||
2177 !cmp_String(&d->sourceMime, mimeType_Gempub))) {
2150 setCachedResponse_History(d->mod.history, lockResponse_GmRequest(d->request)); 2178 setCachedResponse_History(d->mod.history, lockResponse_GmRequest(d->request));
2151 unlockResponse_GmRequest(d->request); 2179 unlockResponse_GmRequest(d->request);
2152 } 2180 }
@@ -2856,6 +2884,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
2856 0, 0, NULL }); 2884 0, 0, NULL });
2857 } 2885 }
2858 if (willUseProxy_App(scheme) || isGemini || 2886 if (willUseProxy_App(scheme) || isGemini ||
2887 equalCase_Rangecc(scheme, "file") ||
2859 equalCase_Rangecc(scheme, "finger") || 2888 equalCase_Rangecc(scheme, "finger") ||
2860 equalCase_Rangecc(scheme, "gopher")) { 2889 equalCase_Rangecc(scheme, "gopher")) {
2861 isNative = iTrue; 2890 isNative = iTrue;